From ba1f643192dd3bb18a05029d10e5b0b8ce8e23bf Mon Sep 17 00:00:00 2001 From: Karl Andin Date: Mon, 11 Nov 2024 13:02:40 +0100 Subject: [PATCH 1/4] fix(KUI-1585): remove rendering of alert using createRoot by moving useStatisticsAsync one step up in react tree --- .../components/statistics/AnalysesSummary.jsx | 4 +-- .../components/statistics/MemosSummary.jsx | 5 +-- .../components/statistics/StatisticsAlert.jsx | 10 +++--- .../components/statistics/StatisticsForm.jsx | 11 +++--- .../statistics/StatisticsResults.jsx | 22 +++--------- public/js/app/hooks/statisticsUseAsync.js | 36 +------------------ public/js/app/pages/CourseStatisticsPage.jsx | 10 ++++-- 7 files changed, 29 insertions(+), 69 deletions(-) diff --git a/public/js/app/components/statistics/AnalysesSummary.jsx b/public/js/app/components/statistics/AnalysesSummary.jsx index 09cdba70..d2aa26f9 100644 --- a/public/js/app/components/statistics/AnalysesSummary.jsx +++ b/public/js/app/components/statistics/AnalysesSummary.jsx @@ -7,7 +7,7 @@ import { useLanguage } from '../../hooks/useLanguage' import { TableSummary } from './TableSummaryRows' import { seasons as seasonLib } from './domain/index' import { Charts } from './Chart' -import { Results } from './index' +import { Results, StatisticsAlert } from './index' function getSchoolNumbers(school) { const { numberOfCourses, numberOfUniqAnalyses } = school @@ -100,7 +100,6 @@ function AnalysesNumbersCharts({ statisticsResult }) { function AnalysesNumbersChartsYearAgo({ statisticsResult }) { const { school, documentType, seasons, year } = statisticsResult - if (!documentType) return null const oneYearAgo = Number(year) - 1 const state = useStatisticsAsync({ seasons, year: oneYearAgo, documentType, school }, 'once') @@ -109,6 +108,7 @@ function AnalysesNumbersChartsYearAgo({ statisticsResult }) { return ( <> + {error?.errorType && {error.errorExtraText}} diff --git a/public/js/app/components/statistics/MemosSummary.jsx b/public/js/app/components/statistics/MemosSummary.jsx index 166ea51e..80b9552a 100644 --- a/public/js/app/components/statistics/MemosSummary.jsx +++ b/public/js/app/components/statistics/MemosSummary.jsx @@ -7,7 +7,7 @@ import { useLanguage } from '../../hooks/useLanguage' import { TableSummary } from './TableSummaryRows' import { Charts } from './Chart' import { periods as periodsLib } from './domain/index' -import { Results } from './index' +import { Results, StatisticsAlert } from './index' function getSchoolNumbers(school = {}) { return [ @@ -145,7 +145,6 @@ function MemosNumbersCharts({ statisticsResult }) { function MemosNumbersChartsYearAgo({ statisticsResult }) { const { school, documentType, periods, year } = statisticsResult - if (!documentType) return null const oneYearAgo = Number(year) - 1 const state = useStatisticsAsync({ periods, year: oneYearAgo, documentType, school }, 'once') @@ -154,6 +153,8 @@ function MemosNumbersChartsYearAgo({ statisticsResult }) { return ( <> + {error?.errorType && {error.errorExtraText}} + diff --git a/public/js/app/components/statistics/StatisticsAlert.jsx b/public/js/app/components/statistics/StatisticsAlert.jsx index e9cc4487..8433eef7 100644 --- a/public/js/app/components/statistics/StatisticsAlert.jsx +++ b/public/js/app/components/statistics/StatisticsAlert.jsx @@ -1,11 +1,12 @@ -import React from 'react' import PropTypes from 'prop-types' -import Alert from '../../components-shared/Alert' +import React from 'react' import i18n from '../../../../../i18n' +import Alert from '../../components-shared/Alert' import { ERROR_ASYNC } from '../../hooks/statisticsUseAsync' +import { useLanguage } from '../../hooks/useLanguage' -function StatisticsAlert({ alertType, languageIndex, children }) { - // We cannot use useLanguage or useWebContext here, as this component will be rendered outside of our component tree +function StatisticsAlert({ alertType, children }) { + const { languageIndex } = useLanguage() const { statisticsLabels } = i18n.messages[languageIndex] const { header = '', help = '', text = '' } = statisticsLabels[alertType] || {} @@ -20,7 +21,6 @@ function StatisticsAlert({ alertType, languageIndex, children }) { StatisticsAlert.propTypes = { alertType: PropTypes.oneOf([...Object.values(ERROR_ASYNC), null]).isRequired, - languageIndex: PropTypes.oneOf([0, 1]).isRequired, } export default StatisticsAlert diff --git a/public/js/app/components/statistics/StatisticsForm.jsx b/public/js/app/components/statistics/StatisticsForm.jsx index 756407d8..5dec5a3a 100644 --- a/public/js/app/components/statistics/StatisticsForm.jsx +++ b/public/js/app/components/statistics/StatisticsForm.jsx @@ -1,10 +1,10 @@ +import PropTypes from 'prop-types' import React, { useReducer } from 'react' import { Col, Row } from 'reactstrap' -import PropTypes from 'prop-types' import { useLanguage } from '../../hooks/useLanguage' import { DOCS, studyLengthParamName } from './domain/formConfigurations' -import { CheckboxOption, DropdownOption, RadioboxOption } from './index' +import { CheckboxOption, DropdownOption, RadioboxOption, StatisticsAlert } from './index' const paramsReducer = (state, action) => { const { value, type } = action @@ -24,7 +24,7 @@ const paramsReducer = (state, action) => { } } -function StatisticsForm({ onSubmit }) { +function StatisticsForm({ onSubmit, resultError }) { const [state, setState] = useReducer(paramsReducer, {}) const [stateMode, setStateMode] = React.useState('init') const { documentType = null } = state @@ -97,7 +97,9 @@ function StatisticsForm({ onSubmit }) { )} -
+ {resultError?.errorType && ( + {resultError.errorExtraText} + )} @@ -113,6 +115,7 @@ function StatisticsForm({ onSubmit }) { StatisticsForm.propTypes = { onSubmit: PropTypes.func.isRequired, + resultError: PropTypes.object, } export default StatisticsForm diff --git a/public/js/app/components/statistics/StatisticsResults.jsx b/public/js/app/components/statistics/StatisticsResults.jsx index 1cfae45a..421d1e43 100644 --- a/public/js/app/components/statistics/StatisticsResults.jsx +++ b/public/js/app/components/statistics/StatisticsResults.jsx @@ -3,9 +3,8 @@ import React from 'react' import PropTypes from 'prop-types' import { Col, Row } from 'reactstrap' -import { STATUS, ERROR_ASYNC, useStatisticsAsync } from '../../hooks/statisticsUseAsync' +import { ERROR_ASYNC, STATUS } from '../../hooks/statisticsUseAsync' -import { periods, schools, seasons } from './domain/index' import { documentTypes } from './domain/formConfigurations' import { ResultNumbersSummary, Results, StatisticsDataTable } from './index' @@ -49,11 +48,8 @@ SortableCoursesAndDocuments.defaultProps = { statisticsStatus: null, } -function StatisticsResults({ chosenOptions }) { - const state = useStatisticsAsync(chosenOptions, 'onChange') - - const { data: statisticsResult, status: statisticsStatus, error = {} } = state || {} - +function StatisticsResults({ result }) { + const { data: statisticsResult, status: statisticsStatus, error = {} } = result || {} return ( @@ -68,17 +64,7 @@ function StatisticsResults({ chosenOptions }) { } StatisticsResults.propTypes = { - chosenOptions: PropTypes.shape({ - documentType: PropTypes.oneOf(documentTypes()), - year: PropTypes.number, - periods: PropTypes.arrayOf(PropTypes.oneOf(periods.orderedPeriods())), - school: PropTypes.oneOf(schools.orderedSchoolsFormOptions()), - seasons: PropTypes.arrayOf(PropTypes.oneOf(seasons.orderedSeasons())), - }), -} - -StatisticsResults.defaultProps = { - chosenOptions: {}, + result: PropTypes.object, } export default StatisticsResults diff --git a/public/js/app/hooks/statisticsUseAsync.js b/public/js/app/hooks/statisticsUseAsync.js index 46d5fb8a..d8ab9dd2 100644 --- a/public/js/app/hooks/statisticsUseAsync.js +++ b/public/js/app/hooks/statisticsUseAsync.js @@ -1,7 +1,5 @@ import React, { useEffect } from 'react' -import { createRoot } from 'react-dom/client' import { useWebContext } from '../context/WebContext' -import { StatisticsAlert } from '../components/statistics/index' import fetchStatistics from './api/statisticsApi' import { useLanguage } from './useLanguage' @@ -81,32 +79,13 @@ function useAsync(asyncCallback, initialState) { return state } -function renderAlertToTop(error = {}, languageIndex) { - const { errorType = '', errorExtraText = '' } = error - const alertContainer = document.getElementById('alert-placeholder') - const alertContainerRoot = createRoot(alertContainer) - if (alertContainer) { - alertContainerRoot.render( - - {errorExtraText} - - ) - } -} -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() -} - function _getThisHost(thisHostBaseUrl) { return thisHostBaseUrl.slice(-1) === '/' ? thisHostBaseUrl.slice(0, -1) : thisHostBaseUrl } function useStatisticsAsync(chosenOptions, loadType = 'onChange') { const { proxyPrefixPath } = useWebContext() - const { languageShortname, languageIndex } = useLanguage() + const { languageShortname } = useLanguage() const { documentType } = chosenOptions const dependenciesList = loadType === 'onChange' ? [chosenOptions] : [] const asyncCallback = React.useCallback(() => { @@ -121,19 +100,6 @@ function useStatisticsAsync(chosenOptions, loadType = 'onChange') { const state = useAsync(asyncCallback, initialStatus) - const { status: statisticsStatus, error = {} } = state || {} - const { errorType = '' } = error - - useEffect(() => { - let isMounted = true - if (isMounted) { - if (errorType && errorType !== null) { - renderAlertToTop(error, languageIndex) - } else dismountTopAlert() - } - return () => (isMounted = false) - }, [statisticsStatus]) - return state } diff --git a/public/js/app/pages/CourseStatisticsPage.jsx b/public/js/app/pages/CourseStatisticsPage.jsx index e000c688..ec6552ec 100644 --- a/public/js/app/pages/CourseStatisticsPage.jsx +++ b/public/js/app/pages/CourseStatisticsPage.jsx @@ -2,8 +2,9 @@ import React from 'react' import { introductionTexts } from '../components/statistics/StatisticsTexts' -import { StatisticsForm, StatisticsResults } from '../components/statistics/index' import { findMissingParametersKeys, hasValue } from '../components/statistics/domain/validation' +import { StatisticsForm, StatisticsResults } from '../components/statistics/index' +import { useStatisticsAsync } from '../hooks/statisticsUseAsync' import { useLanguage } from '../hooks/useLanguage' function _parseValues({ documentType, periods, school, seasons, year }) { @@ -61,13 +62,16 @@ function CourseStatisticsPage() { const missingParams = findMissingParametersKeys(finalSearchParams) setHasSubmittedEmptyValue(missingParams.length > 0) } + + const resultState = useStatisticsAsync(params, 'onChange') + return (

{labels.pageHeader}

{texts.pageDescription()}

{labels.formLabels.formHeader}

- - + +
) } From 024b64d42cf68160c67b4b7c60f559eae8c16c71 Mon Sep 17 00:00:00 2001 From: Karl Andin Date: Mon, 11 Nov 2024 13:38:18 +0100 Subject: [PATCH 2/4] fix(KUI-1585): remove use of deprected defaultProps --- public/js/app/components/statistics/MemosSummary.jsx | 1 - .../app/components/statistics/ResultNumbersSummary.jsx | 1 - public/js/app/components/statistics/Results.jsx | 7 +------ .../js/app/components/statistics/StatisticsResults.jsx | 9 +-------- 4 files changed, 2 insertions(+), 16 deletions(-) diff --git a/public/js/app/components/statistics/MemosSummary.jsx b/public/js/app/components/statistics/MemosSummary.jsx index 80b9552a..46ec0e0a 100644 --- a/public/js/app/components/statistics/MemosSummary.jsx +++ b/public/js/app/components/statistics/MemosSummary.jsx @@ -183,5 +183,4 @@ function MemosSummary({ statisticsResult }) { ) } -MemosSummary.defaultProps = {} export default MemosSummary diff --git a/public/js/app/components/statistics/ResultNumbersSummary.jsx b/public/js/app/components/statistics/ResultNumbersSummary.jsx index 054bdafc..468dc3fe 100644 --- a/public/js/app/components/statistics/ResultNumbersSummary.jsx +++ b/public/js/app/components/statistics/ResultNumbersSummary.jsx @@ -56,5 +56,4 @@ ResultNumbersSummary.propTypes = { ]), } -ResultNumbersSummary.defaultProps = {} export default ResultNumbersSummary diff --git a/public/js/app/components/statistics/Results.jsx b/public/js/app/components/statistics/Results.jsx index 4c3eee72..556d7a86 100644 --- a/public/js/app/components/statistics/Results.jsx +++ b/public/js/app/components/statistics/Results.jsx @@ -28,7 +28,7 @@ const errorItalicParagraph = (error = {}, labels) => { ) } -function Results({ statisticsStatus, error = {}, children }) { +function Results({ statisticsStatus = null, error = {}, children }) { const { translation: { statisticsLabels }, } = useLanguage() @@ -57,9 +57,4 @@ Results.propTypes = { }), } -Results.defaultProps = { - error: {}, - statisticsStatus: null, -} - export default Results diff --git a/public/js/app/components/statistics/StatisticsResults.jsx b/public/js/app/components/statistics/StatisticsResults.jsx index 421d1e43..01a7db78 100644 --- a/public/js/app/components/statistics/StatisticsResults.jsx +++ b/public/js/app/components/statistics/StatisticsResults.jsx @@ -8,7 +8,7 @@ import { ERROR_ASYNC, STATUS } from '../../hooks/statisticsUseAsync' import { documentTypes } from './domain/formConfigurations' import { ResultNumbersSummary, Results, StatisticsDataTable } from './index' -function SortableCoursesAndDocuments({ statisticsStatus, error = {}, statisticsResult }) { +function SortableCoursesAndDocuments({ statisticsStatus = null, error = {}, statisticsResult }) { return ( @@ -41,13 +41,6 @@ SortableCoursesAndDocuments.propTypes = { }), } -SortableCoursesAndDocuments.defaultProps = { - languageIndex: 0, - error: {}, - statisticsResult: {}, - statisticsStatus: null, -} - function StatisticsResults({ result }) { const { data: statisticsResult, status: statisticsStatus, error = {} } = result || {} return ( From 3b3877693a218d73625c2937f53edeaec4102f84 Mon Sep 17 00:00:00 2001 From: Karl Andin Date: Tue, 12 Nov 2024 13:25:43 +0100 Subject: [PATCH 3/4] fix(KUI-1585): fix some warnings --- public/js/app/components/statistics/Chart.jsx | 2 +- .../statistics/ResultNumbersSummary.jsx | 17 +++++++---------- .../statistics/StatisticsDataTable.jsx | 4 +++- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/public/js/app/components/statistics/Chart.jsx b/public/js/app/components/statistics/Chart.jsx index 85f91760..e946b6ac 100644 --- a/public/js/app/components/statistics/Chart.jsx +++ b/public/js/app/components/statistics/Chart.jsx @@ -66,7 +66,7 @@ function Charts({ chartNames = [], schools = {} }) { function Chart({ data = [], label = '' }) { const styles = { - font: { fontFamily: 'Open Sans', fontSize: 'font-heading-xs' }, + font: { fontFamily: 'Figtree, sans-serif', fontSize: 16 }, } return ( diff --git a/public/js/app/components/statistics/ResultNumbersSummary.jsx b/public/js/app/components/statistics/ResultNumbersSummary.jsx index 468dc3fe..d66fc379 100644 --- a/public/js/app/components/statistics/ResultNumbersSummary.jsx +++ b/public/js/app/components/statistics/ResultNumbersSummary.jsx @@ -45,15 +45,12 @@ function ResultNumbersSummary({ statisticsResult }) { } ResultNumbersSummary.propTypes = { - statisticsResult: PropTypes.oneOf([ - PropTypes.shape({ - combinedMemosPerSchool: PropTypes.shape({}), - documentType: PropTypes.oneOf(documentTypes()), - koppsApiBasePath: PropTypes.string, - documentsApiBasePath: PropTypes.string, - school: PropTypes.oneOf(schools.orderedSchoolsFormOptions()), - }), - ]), + statisticsResult: PropTypes.shape({ + combinedMemosPerSchool: PropTypes.shape({}), + documentType: PropTypes.oneOf(documentTypes()), + koppsApiBasePath: PropTypes.string, + documentsApiBasePath: PropTypes.string, + school: PropTypes.oneOf(schools.orderedSchoolsFormOptions()), + }), } - export default ResultNumbersSummary diff --git a/public/js/app/components/statistics/StatisticsDataTable.jsx b/public/js/app/components/statistics/StatisticsDataTable.jsx index a88c7acd..c952eab4 100644 --- a/public/js/app/components/statistics/StatisticsDataTable.jsx +++ b/public/js/app/components/statistics/StatisticsDataTable.jsx @@ -342,7 +342,9 @@ function StatisticsDataTable({ statisticsResult }) { selector: group => group[columnName], sortable: true, wrap: true, - minWidth: '137px', + style: { + minWidth: '137px', + }, })) let dataRows = [] if (isMemoPage) { From 7277aa6a824436e21ea7606cbce1309d6c75baa6 Mon Sep 17 00:00:00 2001 From: Karl Andin Date: Tue, 12 Nov 2024 12:41:44 +0100 Subject: [PATCH 4/4] fix(KUI-1585): fix bugs in course analysis by simplifying semester logic. The main changes are: - faulty week logic for determining semester is removed and only now semester from Kopps is used. - only one semester can be selected, to avoid issues of added same course offering multiple time. - ST is removed, course offerings will be included in HT och VT depending on semester from Kopps (ST was caluclated from weeks before and is no actual semester) --- domain/statistics/seasons.js | 14 +- i18n/messages.en.js | 4 +- i18n/messages.se.js | 4 +- .../components/statistics/AnalysesSummary.jsx | 20 +- .../components/statistics/RadioboxOption.jsx | 2 +- .../statistics/StatisticsDataTable.jsx | 4 +- .../components/statistics/StatisticsForm.jsx | 15 +- .../statistics/StatisticsResults.jsx | 2 - .../__tests__/StatisticsDataTable.test.js | 2 +- .../__tests__/StatisticsFormEN.test.js | 47 ++- .../__tests__/StatisticsFormSV.test.js | 59 ++-- .../statistics/domain/formConfigurations.js | 6 +- .../domain/formConfigurationsEN.test.js | 7 +- .../domain/formConfigurationsSV.test.js | 9 +- .../components/statistics/domain/seasons.js | 24 -- .../statistics/domain/seasons.test.js | 72 ----- .../statistics/texts/analysisTexts.js | 11 +- public/js/app/hooks/api/statisticsApi.js | 9 +- public/js/app/pages/CourseStatisticsPage.jsx | 8 +- .../CourseStatisticsPageCompilationSV.test.js | 11 +- server/apiCalls/transformers/analyses.js | 13 +- .../transformers/offerings-analysis.test.js | 162 ++++++++++ .../transformers/offerings-memos.test.js | 49 +++ .../offerings-over-several-semesters.test.js | 302 ------------------ server/apiCalls/transformers/offerings.js | 90 ++---- .../apiCalls/transformers/offerings.test.js | 274 ---------------- server/controllers/statisticsCtrl.js | 22 +- 27 files changed, 350 insertions(+), 892 deletions(-) delete mode 100644 public/js/app/components/statistics/domain/seasons.test.js create mode 100644 server/apiCalls/transformers/offerings-analysis.test.js create mode 100644 server/apiCalls/transformers/offerings-memos.test.js delete mode 100644 server/apiCalls/transformers/offerings-over-several-semesters.test.js delete mode 100644 server/apiCalls/transformers/offerings.test.js diff --git a/domain/statistics/seasons.js b/domain/statistics/seasons.js index 086a2e81..8d0ec04d 100644 --- a/domain/statistics/seasons.js +++ b/domain/statistics/seasons.js @@ -1,16 +1,10 @@ const i18n = require('../../i18n') -// eslint-disable-next-line no-unused-vars const seasonConstants = { - SPRING_TERM_NUMBER: 1, // Minimum possible term number. - AUTUMN_TERM_NUMBER: 2, // Maximum possible term number. - SUMMER_TERM_NUMBER: 0, + SPRING_TERM_NUMBER: 1, + AUTUMN_TERM_NUMBER: 2, } -const orderedSeasons = () => [ - seasonConstants.AUTUMN_TERM_NUMBER, - seasonConstants.SPRING_TERM_NUMBER, - seasonConstants.SUMMER_TERM_NUMBER, -] +const orderedSeasons = () => [seasonConstants.AUTUMN_TERM_NUMBER, seasonConstants.SPRING_TERM_NUMBER] /** * @param {number} seasonNumber * @param {number} langIndex @@ -21,8 +15,6 @@ function labelSeason(seasonNumber, langIndex) { const { statisticsLabels: labels } = i18n.messages[langIndex] switch (Number(seasonNumber)) { - case seasonConstants.SUMMER_TERM_NUMBER: - return labels.seasonSummer case seasonConstants.AUTUMN_TERM_NUMBER: return labels.seasonAutumn case seasonConstants.SPRING_TERM_NUMBER: diff --git a/i18n/messages.en.js b/i18n/messages.en.js index 17ef8489..d88e8e40 100644 --- a/i18n/messages.en.js +++ b/i18n/messages.en.js @@ -311,14 +311,14 @@ module.exports = { documentType: 'Area', periods: 'Study period', school: 'School', - seasons: 'Semester', + semester: 'Semester', year: 'Year', }, formShortIntro: { documentType: 'Select the area you would like to see statistics for ', periods: 'Choose one or more option(s)', school: 'Select school', - seasons: 'Choose one or more option(s)', + semester: 'Choose one semester', year: 'Select year', }, missingParameters: { text: extra => `You have to select a ${extra} to view statistics.` }, diff --git a/i18n/messages.se.js b/i18n/messages.se.js index 2da99936..9306f7b3 100644 --- a/i18n/messages.se.js +++ b/i18n/messages.se.js @@ -284,14 +284,14 @@ module.exports = { documentType: 'Område', periods: 'Läsperiod', school: 'Skola', - seasons: 'Termin', + semester: 'Termin', year: 'År', }, formShortIntro: { documentType: 'Välj det område du vill se statistik för', periods: 'Kryssa i ett eller flera val', school: 'Välj skola', - seasons: 'Kryssa i ett eller flera val', + semester: 'Välj en termin', year: 'Välj år', }, missingParameters: { diff --git a/public/js/app/components/statistics/AnalysesSummary.jsx b/public/js/app/components/statistics/AnalysesSummary.jsx index d2aa26f9..16cb0c15 100644 --- a/public/js/app/components/statistics/AnalysesSummary.jsx +++ b/public/js/app/components/statistics/AnalysesSummary.jsx @@ -25,7 +25,7 @@ function addAllSchoolsData({ totalCourses, totalUniqPublishedAnalyses }) { return allSchools } -function Captions({ school, year, seasons }) { +function Captions({ school, year, semester }) { const { languageIndex, translation: { @@ -41,7 +41,7 @@ function Captions({ school, year, seasons }) { return schoolName } - const seasonsStr = seasons.map(season => seasonLib.labelSeason(season, languageIndex)).join(', ') + const semesterStr = seasonLib.labelSeason(semester, languageIndex) return ( @@ -53,8 +53,8 @@ function Captions({ school, year, seasons }) { {`: ${year}`} - - {`: ${seasonsStr}`} + + {`: ${semesterStr}`} ) @@ -63,12 +63,12 @@ function Captions({ school, year, seasons }) { function AnalysesNumbersTable({ statisticsResult }) { const { translation } = useLanguage() const { analysesNumbersTable } = translation.statisticsLabels.summaryLabels - const { combinedAnalysesPerSchool, year, seasons, school } = statisticsResult + const { combinedAnalysesPerSchool, year, semester, school } = statisticsResult const cellNames = ['totalCourses', 'totalUniqPublishedAnalyses'] return ( <> - + - + ) } function AnalysesNumbersChartsYearAgo({ statisticsResult }) { - const { school, documentType, seasons, year } = statisticsResult + const { school, documentType, semester, year } = statisticsResult const oneYearAgo = Number(year) - 1 - const state = useStatisticsAsync({ seasons, year: oneYearAgo, documentType, school }, 'once') + const state = useStatisticsAsync({ semester, year: oneYearAgo, documentType, school }, 'once') const { data: statisticsResultYearAgo, status: statisticsStatus, error = {} } = state || {} diff --git a/public/js/app/components/statistics/RadioboxOption.jsx b/public/js/app/components/statistics/RadioboxOption.jsx index 168522d1..c8a1ffa6 100644 --- a/public/js/app/components/statistics/RadioboxOption.jsx +++ b/public/js/app/components/statistics/RadioboxOption.jsx @@ -71,7 +71,7 @@ function RadioboxOption({ paramName, onChange }) { } RadioboxOption.propTypes = { - paramName: PropTypes.oneOf(['documentType', 'school']).isRequired, + paramName: PropTypes.oneOf(['documentType', 'school', 'semester']).isRequired, onChange: PropTypes.func.isRequired, } diff --git a/public/js/app/components/statistics/StatisticsDataTable.jsx b/public/js/app/components/statistics/StatisticsDataTable.jsx index c952eab4..f8a0a645 100644 --- a/public/js/app/components/statistics/StatisticsDataTable.jsx +++ b/public/js/app/components/statistics/StatisticsDataTable.jsx @@ -303,7 +303,7 @@ function StatisticsDataTable({ statisticsResult }) { (statisticsResult.offeringsWithAnalyses && statisticsResult.offeringsWithAnalyses.length === 0) ) return - const { year, offeringsWithMemos, periods = [], seasons = [], offeringsWithAnalyses } = statisticsResult + const { year, offeringsWithMemos, periods = [], semester = '', offeringsWithAnalyses } = statisticsResult const isMemoPage = offeringsWithMemos && offeringsWithMemos.length > 0 ? true : false const isAnalysisPage = offeringsWithAnalyses && offeringsWithAnalyses.length > 0 ? true : false @@ -404,7 +404,7 @@ function StatisticsDataTable({ statisticsResult }) { fileName={ isMemoPage ? `course-information-statistics-memos-${year}-periods-${periods.join('-')}` - : `course-information-statistics-analyses-${year}-periods-${seasons.join('-')}` + : `course-information-statistics-analyses-${year}-periods-${semester}` } > diff --git a/public/js/app/components/statistics/StatisticsForm.jsx b/public/js/app/components/statistics/StatisticsForm.jsx index 5dec5a3a..105933ee 100644 --- a/public/js/app/components/statistics/StatisticsForm.jsx +++ b/public/js/app/components/statistics/StatisticsForm.jsx @@ -85,12 +85,15 @@ function StatisticsForm({ onSubmit, resultError }) {
- {/* depends on type of document to dropdown */} - + {documentType === 'courseAnalysis' ? ( + + ) : ( + + )} diff --git a/public/js/app/components/statistics/StatisticsResults.jsx b/public/js/app/components/statistics/StatisticsResults.jsx index 01a7db78..d62ffc07 100644 --- a/public/js/app/components/statistics/StatisticsResults.jsx +++ b/public/js/app/components/statistics/StatisticsResults.jsx @@ -31,9 +31,7 @@ SortableCoursesAndDocuments.propTypes = { languageIndex: PropTypes.oneOf([0, 1]), statisticsStatus: PropTypes.oneOf([...Object.values(STATUS), null]), statisticsResult: PropTypes.shape({ - documentsApiBasePath: PropTypes.string, documentType: PropTypes.oneOf(documentTypes()), - koppsApiBasePath: PropTypes.string, }), error: PropTypes.shape({ errorType: PropTypes.oneOf([...Object.values(ERROR_ASYNC), '']), diff --git a/public/js/app/components/statistics/__tests__/StatisticsDataTable.test.js b/public/js/app/components/statistics/__tests__/StatisticsDataTable.test.js index 84d7809a..4de94d3c 100644 --- a/public/js/app/components/statistics/__tests__/StatisticsDataTable.test.js +++ b/public/js/app/components/statistics/__tests__/StatisticsDataTable.test.js @@ -38,7 +38,7 @@ const statisticsResultForMemo = { const statisticsResultForAnalysis = { year: 2019, - seasons: ['1'], + semester: '1', offeringsWithAnalyses: [ { endDate: '2020-01-14', diff --git a/public/js/app/components/statistics/__tests__/StatisticsFormEN.test.js b/public/js/app/components/statistics/__tests__/StatisticsFormEN.test.js index d81d0b84..369e2aff 100644 --- a/public/js/app/components/statistics/__tests__/StatisticsFormEN.test.js +++ b/public/js/app/components/statistics/__tests__/StatisticsFormEN.test.js @@ -22,6 +22,7 @@ describe('Component in english', () => { beforeAll(() => { jest.useFakeTimers({ advanceTimers: true }) jest.setSystemTime(new Date(2023, 3, 1)) + submittedResults = undefined }) afterAll(() => { @@ -267,10 +268,11 @@ describe('Component in english', () => { expect(screen.getByRole('heading', { name: /semester/i })).toBeInTheDocument() await userEvent.click(screen.getByLabelText(/autumn/i)) - await userEvent.click(screen.getByLabelText(/summer/i)) - expect(screen.getByLabelText(/autumn/i)).toBeChecked() - expect(screen.getByLabelText(/summer/i)).toBeChecked() + + await userEvent.click(screen.getByLabelText(/spring/i)) + expect(screen.getByLabelText(/spring/i)).toBeChecked() + expect(screen.getByLabelText(/autumn/i)).not.toBeChecked() await userEvent.click(btn) expect(submittedResults).toMatchInlineSnapshot(` @@ -278,10 +280,7 @@ describe('Component in english', () => { "documentType": "courseAnalysis", "periods": [], "school": undefined, - "seasons": [ - 2, - 0, - ], + "semester": "1", "year": undefined, } `) @@ -296,7 +295,7 @@ describe('Component in english', () => { expect(courseAnalysis).toBeChecked() expect(screen.getByLabelText(/autumn/i)).not.toBeChecked() - expect(screen.getByLabelText(/summer/i)).not.toBeChecked() + expect(screen.getByLabelText(/spring/i)).not.toBeChecked() await userEvent.click(btn) expect(submittedResults).toMatchInlineSnapshot(` @@ -304,7 +303,7 @@ describe('Component in english', () => { "documentType": "courseAnalysis", "periods": [], "school": undefined, - "seasons": [], + "semester": undefined, "year": undefined, } `) @@ -349,10 +348,11 @@ describe('Component in english', () => { expect(screen.getByRole('heading', { name: /semester/i })).toBeInTheDocument() await userEvent.click(screen.getByLabelText(/autumn/i)) - await userEvent.click(screen.getByLabelText(/summer/i)) - expect(screen.getByLabelText(/autumn/i)).toBeChecked() - expect(screen.getByLabelText(/summer/i)).toBeChecked() + + await userEvent.click(screen.getByLabelText(/spring/i)) + expect(screen.getByLabelText(/spring/i)).toBeChecked() + expect(screen.getByLabelText(/autumn/i)).not.toBeChecked() await userEvent.selectOptions(screen.getByRole('combobox', { name: /Select year/i }), '2019') @@ -362,10 +362,7 @@ describe('Component in english', () => { "documentType": "courseAnalysis", "periods": [], "school": "SCI", - "seasons": [ - 2, - 0, - ], + "semester": "1", "year": 2019, } `) @@ -402,7 +399,7 @@ describe('Component in english', () => { "documentType": "courseAnalysis", "periods": [], "school": undefined, - "seasons": [], + "semester": undefined, "year": undefined, } `) @@ -546,10 +543,8 @@ describe('Component in english', () => { await userEvent.selectOptions(screen.getByRole('combobox', { name: /Select year/i }), '2019') await userEvent.click(screen.getByLabelText(/spring/i)) - await userEvent.click(screen.getByLabelText(/summer/i)) - expect(screen.getByLabelText(/spring/i)).toBeChecked() - expect(screen.getByLabelText(/summer/i)).toBeChecked() + expect(screen.getByLabelText(/autumn/i)).not.toBeChecked() const btn = screen.getByRole('button', { name: /show statistics/i }) await userEvent.click(btn) @@ -558,10 +553,7 @@ describe('Component in english', () => { { "documentType": "courseAnalysis", "school": "EECS", - "seasons": [ - 1, - 0, - ], + "semester": "1", "year": 2019, } `) @@ -574,6 +566,7 @@ describe('Component in english', () => { await userEvent.click(screen.getByLabelText(/autumn/i)) expect(screen.getByLabelText(/autumn/i)).toBeChecked() + expect(screen.getByLabelText(/spring/i)).not.toBeChecked() await userEvent.click(btn) @@ -581,11 +574,7 @@ describe('Component in english', () => { { "documentType": "courseAnalysis", "school": "ITM", - "seasons": [ - 1, - 0, - 2, - ], + "semester": "2", "year": 2020, } `) diff --git a/public/js/app/components/statistics/__tests__/StatisticsFormSV.test.js b/public/js/app/components/statistics/__tests__/StatisticsFormSV.test.js index 7985892e..f09c2d83 100644 --- a/public/js/app/components/statistics/__tests__/StatisticsFormSV.test.js +++ b/public/js/app/components/statistics/__tests__/StatisticsFormSV.test.js @@ -27,6 +27,7 @@ describe('Component ', () => { beforeAll(() => { jest.useFakeTimers({ advanceTimers: true }) jest.setSystemTime(new Date(2023, 3, 1)) + submittedResults = undefined }) afterAll(() => { @@ -268,10 +269,11 @@ describe('Component ', () => { expect(screen.getByRole('heading', { name: /termin/i })).toBeInTheDocument() await userEvent.click(screen.getByLabelText(/HT/i)) - await userEvent.click(screen.getByLabelText(/sommar/i)) - expect(screen.getByLabelText(/HT/i)).toBeChecked() - expect(screen.getByLabelText(/sommar/i)).toBeChecked() + + await userEvent.click(screen.getByLabelText(/VT/i)) + expect(screen.getByLabelText(/VT/i)).toBeChecked() + expect(screen.getByLabelText(/HT/i)).not.toBeChecked() await userEvent.click(btn) expect(submittedResults).toMatchInlineSnapshot(` @@ -279,10 +281,7 @@ describe('Component ', () => { "documentType": "courseAnalysis", "periods": [], "school": undefined, - "seasons": [ - 2, - 0, - ], + "semester": "1", "year": undefined, } `) @@ -297,7 +296,7 @@ describe('Component ', () => { expect(courseAnalysis).toBeChecked() expect(screen.getByLabelText(/HT/i)).not.toBeChecked() - expect(screen.getByLabelText(/sommar/i)).not.toBeChecked() + expect(screen.getByLabelText(/VT/i)).not.toBeChecked() await userEvent.click(btn) expect(submittedResults).toMatchInlineSnapshot(` @@ -305,7 +304,7 @@ describe('Component ', () => { "documentType": "courseAnalysis", "periods": [], "school": undefined, - "seasons": [], + "semester": undefined, "year": undefined, } `) @@ -320,12 +319,14 @@ describe('Component ', () => { await userEvent.click(courseMemo) expect(courseMemo).toBeChecked() + const btn = screen.getByRole('button', { name: /visa statistik/i }) + await userEvent.click(btn) + expect(submittedResults).toMatchInlineSnapshot(` { - "documentType": "courseAnalysis", + "documentType": "courseMemo", "periods": [], "school": undefined, - "seasons": [], "year": undefined, } `) @@ -333,12 +334,14 @@ describe('Component ', () => { await userEvent.click(courseAnalysis) expect(courseAnalysis).toBeChecked() + await userEvent.click(btn) + expect(submittedResults).toMatchInlineSnapshot(` { "documentType": "courseAnalysis", "periods": [], "school": undefined, - "seasons": [], + "semester": undefined, "year": undefined, } `) @@ -383,10 +386,11 @@ describe('Component ', () => { expect(screen.getByRole('heading', { name: /termin/i })).toBeInTheDocument() await userEvent.click(screen.getByLabelText(/HT/i)) - await userEvent.click(screen.getByLabelText(/sommar/i)) - expect(screen.getByLabelText(/HT/i)).toBeChecked() - expect(screen.getByLabelText(/sommar/i)).toBeChecked() + + await userEvent.click(screen.getByLabelText(/VT/i)) + expect(screen.getByLabelText(/VT/i)).toBeChecked() + expect(screen.getByLabelText(/HT/i)).not.toBeChecked() await userEvent.selectOptions(screen.getByRole('combobox', { name: /välj år/i }), '2019') @@ -396,10 +400,7 @@ describe('Component ', () => { "documentType": "courseAnalysis", "periods": [], "school": "SCI", - "seasons": [ - 2, - 0, - ], + "semester": "1", "year": 2019, } `) @@ -477,11 +478,9 @@ describe('Component ', () => { await userEvent.selectOptions(screen.getByRole('combobox', { name: /välj år/i }), '2020') - await userEvent.click(screen.getByLabelText(/HT/i)) - await userEvent.click(screen.getByLabelText(/sommar/i)) - - expect(screen.getByLabelText(/HT/i)).toBeChecked() - expect(screen.getByLabelText(/sommar/i)).toBeChecked() + await userEvent.click(screen.getByLabelText(/VT/i)) + expect(screen.getByLabelText(/VT/i)).toBeChecked() + expect(screen.getByLabelText(/HT/i)).not.toBeChecked() const btn = screen.getByRole('button', { name: /visa statistik/i }) await userEvent.click(btn) @@ -490,10 +489,7 @@ describe('Component ', () => { { "documentType": "courseAnalysis", "school": "allSchools", - "seasons": [ - 2, - 0, - ], + "semester": "1", "year": 2020, } `) @@ -504,7 +500,8 @@ describe('Component ', () => { await userEvent.selectOptions(screen.getByRole('combobox', { name: /välj år/i }), '2020') await userEvent.click(screen.getByLabelText(/HT/i)) - expect(screen.getByLabelText(/HT/i)).not.toBeChecked() + expect(screen.getByLabelText(/HT/i)).toBeChecked() + expect(screen.getByLabelText(/VT/i)).not.toBeChecked() await userEvent.click(btn) @@ -512,9 +509,7 @@ describe('Component ', () => { { "documentType": "courseAnalysis", "school": "ITM", - "seasons": [ - 0, - ], + "semester": "2", "year": 2020, } `) diff --git a/public/js/app/components/statistics/domain/formConfigurations.js b/public/js/app/components/statistics/domain/formConfigurations.js index dadb2cfc..6030abc8 100644 --- a/public/js/app/components/statistics/domain/formConfigurations.js +++ b/public/js/app/components/statistics/domain/formConfigurations.js @@ -5,14 +5,14 @@ const PARAMS = { documentType: 'documentType', periods: 'periods', school: 'school', - seasons: 'seasons', + semester: 'semester', year: 'year', } const DOCS = { courseMemo: 'courseMemo', courseAnalysis: 'courseAnalysis' } const documentTypes = () => [DOCS.courseMemo, DOCS.courseAnalysis] -const studyLengthParamName = documentType => (documentType === DOCS.courseMemo ? PARAMS.periods : PARAMS.seasons) +const studyLengthParamName = documentType => (documentType === DOCS.courseMemo ? PARAMS.periods : PARAMS.semester) const paramsByDocumentType = documentType => [ PARAMS.documentType, @@ -51,7 +51,7 @@ function getOptionsValues(paramName, langIndex) { return parseData(schools.orderedSchoolsFormOptions(), langIndex) case PARAMS.year: return parseData(year.getYears(), langIndex) - case PARAMS.seasons: // Course analysis + case PARAMS.semester: // Course analysis return parseData(seasons.orderedSeasons(), langIndex, seasons.labelSeason) case PARAMS.periods: // Kurs-pm return parseData(periods.orderedPeriods(), langIndex, periods.labelPeriod) diff --git a/public/js/app/components/statistics/domain/formConfigurationsEN.test.js b/public/js/app/components/statistics/domain/formConfigurationsEN.test.js index 67b3e904..2640d388 100644 --- a/public/js/app/components/statistics/domain/formConfigurationsEN.test.js +++ b/public/js/app/components/statistics/domain/formConfigurationsEN.test.js @@ -102,7 +102,7 @@ describe('Get list of for each option on statistics page in English', () => { }) test('get options list of seasons seasons', () => { - const seasons = getOptionsValues(PARAMS.seasons, EN_INDEX) + const seasons = getOptionsValues(PARAMS.semester, EN_INDEX) expect(seasons).toMatchInlineSnapshot(` [ { @@ -115,11 +115,6 @@ describe('Get list of for each option on statistics page in English', () => { "label": "Spring", "value": 1, }, - { - "id": 0, - "label": "Summer", - "value": 0, - }, ] `) }) diff --git a/public/js/app/components/statistics/domain/formConfigurationsSV.test.js b/public/js/app/components/statistics/domain/formConfigurationsSV.test.js index 40533d1e..16bb6b51 100644 --- a/public/js/app/components/statistics/domain/formConfigurationsSV.test.js +++ b/public/js/app/components/statistics/domain/formConfigurationsSV.test.js @@ -102,13 +102,13 @@ describe('Get list of for each option on statistics page in Swedish', () => { }) test('get autumn options out of list of seasons seasons', () => { - const seasons = getOptionsValues(PARAMS.seasons, SV_INDEX) + const seasons = getOptionsValues(PARAMS.semester, SV_INDEX) const autumn = seasons.find(season => (season.id = 2)) expect(autumn.label).toBe('HT') }) test('get options list of seasons seasons', () => { - const seasons = getOptionsValues(PARAMS.seasons, SV_INDEX) + const seasons = getOptionsValues(PARAMS.semester, SV_INDEX) expect(seasons).toMatchInlineSnapshot(` [ @@ -122,11 +122,6 @@ describe('Get list of for each option on statistics page in Swedish', () => { "label": "VT", "value": 1, }, - { - "id": 0, - "label": "Sommar", - "value": 0, - }, ] `) }) diff --git a/public/js/app/components/statistics/domain/seasons.js b/public/js/app/components/statistics/domain/seasons.js index 0125973d..aa04019a 100644 --- a/public/js/app/components/statistics/domain/seasons.js +++ b/public/js/app/components/statistics/domain/seasons.js @@ -1,31 +1,7 @@ import { labelSeason, orderedSeasons, seasonConstants } from '../../../../../../domain/statistics/seasons.js' -/** - * @param {array} seasons - * @param {number | string} seasons[] - * @returns {array} - */ -function parseSeasons(seasons) { - return seasons.map(season => Number(season)).sort() -} - -/** - * @param {array} seasons - * @param {number | string} seasons[] - * @returns {array} - */ -function parseToSpringOrAutumnSeasons({ seasons = [] }) { - const seasonsNumbers = parseSeasons(seasons) - if (seasonsNumbers.includes(seasonConstants.SUMMER_TERM_NUMBER)) - return [seasonConstants.SPRING_TERM_NUMBER, seasonConstants.AUTUMN_TERM_NUMBER] - - return seasonsNumbers -} - export default { labelSeason, orderedSeasons, - parseSeasons, - parseToSpringOrAutumnSeasons, seasonConstants, } diff --git a/public/js/app/components/statistics/domain/seasons.test.js b/public/js/app/components/statistics/domain/seasons.test.js deleted file mode 100644 index e49a699c..00000000 --- a/public/js/app/components/statistics/domain/seasons.test.js +++ /dev/null @@ -1,72 +0,0 @@ -import { seasons as seasonsLib } from './index' -// seasonConstants - -describe('Get list of seasons', () => { - test('get both seasons if summer is chosen', () => { - const summer = [0] - const seasonsList = seasonsLib.parseToSpringOrAutumnSeasons({ seasons: summer }) - expect(seasonsList.length).toBe(2) - expect(seasonsList).toMatchInlineSnapshot(` - [ - 1, - 2, - ] - `) - }) - - test('get both seasons if all seasons are chosen', () => { - const summer = [0, 1, 2] - const seasonsList = seasonsLib.parseToSpringOrAutumnSeasons({ seasons: summer }) - expect(seasonsList.length).toBe(2) - expect(seasonsList).toMatchInlineSnapshot(` - [ - 1, - 2, - ] - `) - }) - - test('get both seasons if all seasons are chosen and are strings', () => { - const summer = ['0', '1', '2'] - const seasonsList = seasonsLib.parseToSpringOrAutumnSeasons({ seasons: summer }) - expect(seasonsList.length).toBe(2) - expect(seasonsList).toMatchInlineSnapshot(` - [ - 1, - 2, - ] - `) - }) - - test('get autumn season for Autumn and Spring', () => { - const seasons = [1, 2] - const seasonsList = seasonsLib.parseToSpringOrAutumnSeasons({ seasons }) - expect(seasonsList.length).toBe(2) - expect(seasonsList).toMatchInlineSnapshot(` - [ - 1, - 2, - ] - `) - }) - test('get autumn season for Autumn', () => { - const seasons = [1] - const seasonsList = seasonsLib.parseToSpringOrAutumnSeasons({ seasons }) - expect(seasonsList.length).toBe(1) - expect(seasonsList).toMatchInlineSnapshot(` - [ - 1, - ] - `) - }) - test('get autumn season for Spring', () => { - const seasons = [2] - const seasonsList = seasonsLib.parseToSpringOrAutumnSeasons({ seasons }) - expect(seasonsList.length).toBe(1) - expect(seasonsList).toMatchInlineSnapshot(` - [ - 2, - ] - `) - }) -}) diff --git a/public/js/app/components/statistics/texts/analysisTexts.js b/public/js/app/components/statistics/texts/analysisTexts.js index 69af2bd0..868b4cad 100644 --- a/public/js/app/components/statistics/texts/analysisTexts.js +++ b/public/js/app/components/statistics/texts/analysisTexts.js @@ -21,9 +21,7 @@ const englishAnalysisSummarySection = {

Course analyzes with course rounds that were not completed during semester 20221 are filtered out. This is done by discarding courses that do not meet the criteria: course.offered_semesters[{{'last-element'} - }].semester === 20221 and course.offered_semesters[{{'last-element'}}].end_week {'<='} (than last - week of a spring semester) and course.offered_semesters[{{'last-element'}}].end_week {'>='} (than - first week of a spring semester){' '} + }].semester === 20221

), @@ -53,7 +51,7 @@ const englishAnalysisSummarySection = { > the division of the academic year - : Spring week 3-23, Autumn week 35-2, Summer week 24-34. + .
  • The number of courses is calculated based on the number of unique course codes. Courses that have several @@ -88,8 +86,7 @@ const swedishAnalysisSummarySection = {

    Kursanalyser med kursomgångar som inte avslutades under terminen 20221 filtreras bort. Detta görs genom att förkasta kursomgångar som inte uppfyller kriterierna: course.offered_semesters[{{'sista - element'} - }].semester === 20221 och course.offered_semesters[{{'sista - element'}}].end_week {'<='} (VTs - sista veckan) och course.offered_semesters[{{'sista - element'}}].end_week {'>='} (VTs första veckan) + }].semester === 20221.

    ), @@ -117,7 +114,7 @@ const swedishAnalysisSummarySection = { > läsårsindelningen - : VT vecka 3-23, HT vecka 35-2, sommar vecka 24-34 . + .
  • Antal kurser räknas utifrån antal unika kurskoder. Kurser som har flera kurstillfällen med exakt samma start- diff --git a/public/js/app/hooks/api/statisticsApi.js b/public/js/app/hooks/api/statisticsApi.js index 1de234bc..0d8ff44c 100644 --- a/public/js/app/hooks/api/statisticsApi.js +++ b/public/js/app/hooks/api/statisticsApi.js @@ -1,6 +1,6 @@ import axios from 'axios' import { DOCS } from '../../components/statistics/domain/formConfigurations' -import { periods as periodsLib, seasons as seasonsLib } from '../../components/statistics/domain/index' +import { periods as periodsLib } from '../../components/statistics/domain/index' import { missingParametersError, findMissingParametersKeys, @@ -10,12 +10,7 @@ import { function _formQueryByDocumentType(documentType, params) { return documentType === DOCS.courseMemo ? { periods: periodsLib.parsePeriods(params.periods), seasons: periodsLib.parsePeriodsToOrdinarieSeasons(params) } - : { - // in analysis api, exists only autumn and spring semester - analysesSeasons: seasonsLib.parseToSpringOrAutumnSeasons({ seasons: params.seasons }), - // seasons chosen by user, summer/autumn/spring - seasons: seasonsLib.parseSeasons(params.seasons || []), - } + : { semester: Number(params.semester) } } /** diff --git a/public/js/app/pages/CourseStatisticsPage.jsx b/public/js/app/pages/CourseStatisticsPage.jsx index ec6552ec..faad4844 100644 --- a/public/js/app/pages/CourseStatisticsPage.jsx +++ b/public/js/app/pages/CourseStatisticsPage.jsx @@ -7,14 +7,14 @@ import { StatisticsForm, StatisticsResults } from '../components/statistics/inde import { useStatisticsAsync } from '../hooks/statisticsUseAsync' import { useLanguage } from '../hooks/useLanguage' -function _parseValues({ documentType, periods, school, seasons, year }) { +function _parseValues({ documentType, periods, school, semester, year }) { // clean params const optionsValues = {} if (hasValue(documentType)) optionsValues.documentType = documentType if (hasValue(periods)) optionsValues.periods = periods - if (hasValue(seasons)) optionsValues.seasons = seasons + if (hasValue(semester)) optionsValues.semester = semester if (hasValue(school)) optionsValues.school = school if (hasValue(year)) optionsValues.year = year @@ -44,13 +44,13 @@ function CourseStatisticsPage() { * @property {string} documentType * @property {array} periods * @property {string} school - * @property {array} seasons + * @property {string} semester * @property {number} year */ documentType: null, periods: null, school: null, - seasons: null, + semester: null, year: null, }) const [hasSubmittedEmptyValue, setHasSubmittedEmptyValue] = React.useState(false) diff --git a/public/js/app/pages/__tests__/CourseStatisticsPageCompilationSV.test.js b/public/js/app/pages/__tests__/CourseStatisticsPageCompilationSV.test.js index c6eaee6c..c68ba94c 100644 --- a/public/js/app/pages/__tests__/CourseStatisticsPageCompilationSV.test.js +++ b/public/js/app/pages/__tests__/CourseStatisticsPageCompilationSV.test.js @@ -298,8 +298,7 @@ describe('Component show compilation data in for course a koppsApiBasePath: 'https://api-r.referens.sys.kth.se/api/kopps/v2/', documentsApiBasePath: 'http://localhost/api/kurs-pm-data', school: 'allSchools', - seasons: ['0', '1', '2'], - semesters: ['20211', '20212'], + semester: '1', year: '2021', } @@ -319,17 +318,15 @@ describe('Component show compilation data in for course a // check all checkboxes await userEvent.click(screen.getByLabelText(/HT/i)) - await userEvent.click(screen.getByLabelText(/sommar/i)) - await userEvent.click(screen.getByLabelText(/VT/i)) - expect(screen.getByLabelText(/HT/i)).toBeChecked() - expect(screen.getByLabelText(/sommar/i)).toBeChecked() + await userEvent.click(screen.getByLabelText(/VT/i)) + expect(screen.getByLabelText(/HT/i)).not.toBeChecked() expect(screen.getByLabelText(/VT/i)).toBeChecked() const btn = screen.getByRole('button', { name: /visa statistik/i }) await userEvent.click(btn) const url = 'node/api/kursinfo/statistics/courseAnalysis/year/2021' - const paramsPeriod1 = { params: { l: 'sv', analysesSeasons: [1, 2], school: 'allSchools', seasons: [0, 1, 2] } } + const paramsPeriod1 = { params: { l: 'sv', semester: 1, school: 'allSchools' } } expect(axios.get).toHaveBeenCalledWith(url, paramsPeriod1) expect( diff --git a/server/apiCalls/transformers/analyses.js b/server/apiCalls/transformers/analyses.js index 554df9be..4100f920 100644 --- a/server/apiCalls/transformers/analyses.js +++ b/server/apiCalls/transformers/analyses.js @@ -1,5 +1,3 @@ -const log = require('@kth/log') - const { findAnalysesForApplicationCode } = require('./docs') /** @@ -8,9 +6,9 @@ const { findAnalysesForApplicationCode } = require('./docs') * @param {[]} analyses Collection of course analyses * @returns {[]} Array, each containing offerings and their documents. */ -const _analysesPerCourseOffering = async (parsedOfferings, analyses) => { +const _analysesPerCourseOffering = (parsedOfferings, analyses) => { const courseOfferings = [] - await parsedOfferings.forEach(offering => { + parsedOfferings.forEach(offering => { const { courseCode, firstSemester, courseRoundApplications } = offering const [courseRoundApplication] = courseRoundApplications const { course_round_application_code: applicationCode } = courseRoundApplication @@ -129,7 +127,6 @@ function _countAnalysesDataPerSchool(courseOfferings) { schools, ..._calculateTotals(schools), } - log.error('dataPerSchool', dataPerSchool) return dataPerSchool } @@ -139,13 +136,13 @@ function _countAnalysesDataPerSchool(courseOfferings) { * @param {[]} analyses Array containing existing analyses * @returns {{}} Collection with statistics per school, and totals, for analyses */ -async function analysesPerSchool(parsedOfferings, analyses) { +function analysesPerSchool(parsedOfferings, analyses) { // Matches analyses and analyses with course offerings. // Returns an object with two arrays, each containing offerings and their documents. - const offeringsWithAnalyses = await _analysesPerCourseOffering(parsedOfferings, analyses) // prev combinedDataPerDepartment + const offeringsWithAnalyses = _analysesPerCourseOffering(parsedOfferings, analyses) // prev combinedDataPerDepartment // Compiles statistics per school, including totals, for analyses. - const combinedAnalysesPerSchool = await _countAnalysesDataPerSchool(offeringsWithAnalyses) + const combinedAnalysesPerSchool = _countAnalysesDataPerSchool(offeringsWithAnalyses) return { offeringsWithAnalyses, combinedAnalysesPerSchool } } diff --git a/server/apiCalls/transformers/offerings-analysis.test.js b/server/apiCalls/transformers/offerings-analysis.test.js new file mode 100644 index 00000000..b0cde7c2 --- /dev/null +++ b/server/apiCalls/transformers/offerings-analysis.test.js @@ -0,0 +1,162 @@ +'use strict' + +import { filterOfferingsForAnalysis } from './offerings' + +describe('filterOfferingsForAnalysis', () => { + const autumnPrevious = { semester: '20192' } + const spring = { semester: '20201' } + const autumn = { semester: '20202' } + const springNext = { semester: '20211' } + const autumnNext = { semester: '20212' } + const school = 'CBH' + + const courseEndsInAutumn = { + course_code: 'AB0001', + first_yearsemester: '20202', + offered_semesters: [autumn], + school_code: school, + } + const courseEndsInAutumnStartInSpring = { + course_code: 'AB0002', + first_yearsemester: '20201', + offered_semesters: [spring, autumn], + school_code: school, + } + const courseEndsInAutumnOtherSchool = { + course_code: 'AB0003', + first_yearsemester: '20202', + offered_semesters: [autumn], + school_code: 'ITM', + } + const courseEndsInSpring = { + course_code: 'AB0004', + first_yearsemester: '20201', + offered_semesters: [spring], + school_code: school, + } + const courseEndsInSpringStartInAutumn = { + course_code: 'AB0005', + first_yearsemester: '20192', + offered_semesters: [autumnPrevious, spring], + school_code: school, + } + const courseEndsInAutumnNextYear = { + course_code: 'AB0006', + first_yearsemester: '20192', + offered_semesters: [spring, autumn, springNext, autumnNext], + school_code: school, + } + const courseEndsInSpringNextYearStartsInAutumn = { + course_code: 'AB0007', + first_yearsemester: '20201', + offered_semesters: [autumn, springNext], + school_code: school, + } + const coursesToFilter = [ + courseEndsInAutumnStartInSpring, + courseEndsInAutumn, + courseEndsInAutumnOtherSchool, + courseEndsInAutumnNextYear, + courseEndsInSpring, + courseEndsInSpringStartInAutumn, + courseEndsInSpringNextYearStartsInAutumn, + ] + it('should find offerings ending in autumn 2020 - all schools', () => { + const parsedOfferings = filterOfferingsForAnalysis(coursesToFilter, '20202', 'allSchools', 'sv') + expect(parsedOfferings.length).toBe(3) + + const expectedOffering1 = parsedOfferings.find(x => x.courseCode === 'AB0001') + expect(expectedOffering1).toBeDefined() + expect(expectedOffering1.firstSemester).toBe('20202') + expect(expectedOffering1.lastSemester).toBe('20202') + expect(expectedOffering1.lastSemesterLabel).toBe('HT') + + const expectedOffering2 = parsedOfferings.find(x => x.courseCode === 'AB0002') + expect(expectedOffering2).toBeDefined() + expect(expectedOffering2.firstSemester).toBe('20201') + expect(expectedOffering2.lastSemester).toBe('20202') + expect(expectedOffering2.lastSemesterLabel).toBe('HT') + + const expectedOffering3 = parsedOfferings.find(x => x.courseCode === 'AB0003') + expect(expectedOffering3).toBeDefined() + expect(expectedOffering3.firstSemester).toBe('20202') + expect(expectedOffering3.lastSemester).toBe('20202') + expect(expectedOffering3.lastSemesterLabel).toBe('HT') + }) + it('should find offerings ending in autumn 2020 - CBH school', () => { + const parsedOfferings = filterOfferingsForAnalysis(coursesToFilter, '20202', 'CBH', 'sv') + expect(parsedOfferings.length).toBe(2) + + const expectedOffering1 = parsedOfferings.find(x => x.courseCode === 'AB0001') + expect(expectedOffering1).toBeDefined() + expect(expectedOffering1.firstSemester).toBe('20202') + expect(expectedOffering1.lastSemester).toBe('20202') + expect(expectedOffering1.lastSemesterLabel).toBe('HT') + + const expectedOffering2 = parsedOfferings.find(x => x.courseCode === 'AB0002') + expect(expectedOffering2).toBeDefined() + expect(expectedOffering2.firstSemester).toBe('20201') + expect(expectedOffering2.lastSemester).toBe('20202') + expect(expectedOffering2.lastSemesterLabel).toBe('HT') + }) + it('should find offerings ending in spring 2020 - all schools', () => { + const parsedOfferings = filterOfferingsForAnalysis(coursesToFilter, '20201', 'allSchools', 'sv') + expect(parsedOfferings.length).toBe(2) + + const expectedOffering1 = parsedOfferings.find(x => x.courseCode === 'AB0004') + expect(expectedOffering1).toBeDefined() + expect(expectedOffering1.firstSemester).toBe('20201') + expect(expectedOffering1.lastSemester).toBe('20201') + expect(expectedOffering1.lastSemesterLabel).toBe('VT') + + const expectedOffering2 = parsedOfferings.find(x => x.courseCode === 'AB0005') + expect(expectedOffering2).toBeDefined() + expect(expectedOffering2.firstSemester).toBe('20192') + expect(expectedOffering2.lastSemester).toBe('20201') + expect(expectedOffering2.lastSemesterLabel).toBe('VT') + }) + + it('should map all relevant values', () => { + const offeredSemester1 = { end_week: '41', end_date: '2020-10-10', semester: '20212', start_date: '2020-02-10' } + const offeredSemester2 = { end_week: '46', end_date: '2020-11-10', semester: '20202', start_date: '2020-10-10' } + const course = { + connected_programs: [{ code: 'CINTE2', study_year: '2020', spec_code: 'iNTeresting' }], + course_code: 'SF1624', + department_name: 'CBH/Fiber- och Polymerteknologi', + first_yearsemester: '20202', + first_period: '20202P1', + offered_semesters: [offeredSemester1, offeredSemester2], + school_code: 'BIO', + } + const courses = [course] + + // Swedish + const parsedOfferings = filterOfferingsForAnalysis(courses, '20202', 'allSchools', 'sv') + expect(parsedOfferings.length).toBe(1) + + const [offering] = parsedOfferings + expect(offering.firstSemester).toBe(course.first_yearsemester) + expect(offering.startDate).toBe(course.offered_semesters[0].start_date) + expect(offering.endDate).toBe(course.offered_semesters[1].end_date) + expect(offering.schoolMainCode).toBe('CBH') + expect(offering.departmentName).toBe(course.department_name) + expect(offering.connectedPrograms).toBe('CINTE2-iNTeresting-2020') + expect(offering.courseCode).toBe(course.course_code) + expect(offering.lastSemester).toBe('20202') + expect(offering.lastSemesterLabel).toBe('HT') + + // English + const parsedOfferings_en = filterOfferingsForAnalysis(courses, '20202', 'allSchools', 'en') + expect(parsedOfferings_en.length).toBe(1) + const [offering_en] = parsedOfferings_en + expect(offering_en.firstSemester).toBe(course.first_yearsemester) + expect(offering_en.startDate).toBe(course.offered_semesters[0].start_date) + expect(offering_en.endDate).toBe(course.offered_semesters[1].end_date) + expect(offering_en.schoolMainCode).toBe('CBH') + expect(offering_en.departmentName).toBe(course.department_name) + expect(offering_en.connectedPrograms).toBe('CINTE2-iNTeresting-2020') + expect(offering_en.courseCode).toBe(course.course_code) + expect(offering_en.lastSemester).toBe('20202') + expect(offering_en.lastSemesterLabel).toBe('Autumn') + }) +}) diff --git a/server/apiCalls/transformers/offerings-memos.test.js b/server/apiCalls/transformers/offerings-memos.test.js new file mode 100644 index 00000000..72031b31 --- /dev/null +++ b/server/apiCalls/transformers/offerings-memos.test.js @@ -0,0 +1,49 @@ +'use strict' + +import { filterOfferingsForMemos, semestersInParsedOfferings } from './offerings' + +describe('Memos functions to parse and filter offerings', () => { + test('parse and filter offering for memos', () => { + const expectedCourse = { + connected_programs: [{ code: 'CINTE2', study_year: '2020', spec_code: 'iNTeresting' }], + course_code: 'SF1624', + department_name: 'CBH/Fiber- och Polymerteknologi', + first_yearsemester: '20202', + first_period: '20202P1', + offered_semesters: [{ end_date: '2021-01-10', semester: '20202', start_date: '2020-10-10' }], + school_code: 'BIO', + } + const tooOldCourse = { + connected_programs: [{ code: 'CINTE1', study_year: '2020', spec_code: 'k' }], + course_code: 'SF1625', + first_yearsemester: '20192', + first_period: '20192P1', + offered_semesters: [], + school_code: 'CBH', + } + const wrongSchoolCourse = { + connected_programs: [{ code: 'CINTE1', study_year: '2020', spec_code: 'D' }], + course_code: 'SF1629', + first_yearsemester: '20202', + first_period: '20202P1', + offered_semesters: [], + school_code: 'ITM', + } + const courses = [expectedCourse, tooOldCourse, wrongSchoolCourse] + const parsedOfferings = filterOfferingsForMemos(courses, ['20201', '20202'], ['1', '2'], 'CBH') + expect(parsedOfferings.length).toBe(1) + + const [offering] = parsedOfferings + expect(offering.firstSemester).toBe(expectedCourse.first_yearsemester) + expect(offering.startDate).toBe(expectedCourse.offered_semesters[0].start_date) + expect(offering.endDate).toBe(expectedCourse.offered_semesters[0].end_date) + expect(offering.schoolMainCode).toBe('CBH') + expect(offering.departmentName).toBe(expectedCourse.department_name) + expect(offering.connectedPrograms).toBe('CINTE2-iNTeresting-2020') + expect(offering.courseCode).toBe(expectedCourse.course_code) + expect(offering.period).toBe('P1') + + const semestersInMemos = semestersInParsedOfferings(parsedOfferings) + expect(semestersInMemos.length).toBe(1) + }) +}) diff --git a/server/apiCalls/transformers/offerings-over-several-semesters.test.js b/server/apiCalls/transformers/offerings-over-several-semesters.test.js deleted file mode 100644 index c4044e54..00000000 --- a/server/apiCalls/transformers/offerings-over-several-semesters.test.js +++ /dev/null @@ -1,302 +0,0 @@ -'use strict' - -import { - filterOfferingsForMemos, - filterOfferingsForAnalysis, - findCourseStartEndDates, - sortOfferedSemesters, -} from './offerings' - -const startSemester = { - teachers: [], - types: [], - semester: '20231', - start_date: '2023-06-12', - end_date: '2023-08-27', - start_week: '24', - end_week: '34', - matched_search_query: true, - max_group_count: '1', -} -const lastSemester = { - teachers: [], - types: [], - semester: '20232', - start_date: '2023-08-28', - end_date: '2023-10-29', - start_week: '35', - end_week: '43', - matched_search_query: false, - max_group_count: '1', -} - -const course = { - offered_semesters: [startSemester, lastSemester].reverse(), // make it wrong order - campus: 'KTH Campus', - course_code: 'LT1015', - course_name: 'Planering, bedömning och betygssättning', - course_name_en: 'Planning, Assessment and Grading', - department_name: 'ITM/Lärande', - department_code: 'MO', - school_code: 'ITM', - first_semester: 'VT23', - first_yearsemester: '20231', - - state: 'Godkänt', - first_period: '20231P5', - language: 'Svenska', - educational_level: 'grund', - educational_level_en: 'BASIC', - connected_programs: [ - { - elective_condition: 'O', - code: 'LÄRGR', - study_year: 2, - connection_state: 'APPROVED', - }, - ], - course_round_applications: [ - { - course_round_application_code: '45006', - course_round_category_code: 'PU', - course_round_type: 'ORD', - course_round_ladok_state: 'Påbörjad', - course_round_ladok_state_en: 'Started', - ladok_uid: 'de69ebaf-7226-11ec-a534-0f06b63e4534', - }, - ], -} - -const expectedCourseStartDatum = '2023-06-12' -const expectedCourseEndDatum = '2023-10-29' - -describe('Sort offered semesters and check if it parses the correct start and end date, course stretched over several semesters', () => { - test('sort offered semesters by the course oferrings end dates', () => { - const offeredSemesters = course.offered_semesters - - const [firstSemester, secondSemester] = sortOfferedSemesters(offeredSemesters) - expect(firstSemester.start_date).toBe(expectedCourseStartDatum) - expect(firstSemester.end_date).toBe('2023-08-27') - - expect(secondSemester.semester).toBe('20232') - expect(secondSemester.end_date).toBe(expectedCourseEndDatum) - }) - - test('sort reversed offered semesters by the course oferrings end dates', () => { - const offeredSemesters = course.offered_semesters.reverse() - - const [firstSemester, secondSemester] = sortOfferedSemesters(offeredSemesters) - expect(firstSemester.start_date).toBe(expectedCourseStartDatum) - expect(firstSemester.end_date).toBe('2023-08-27') - - expect(secondSemester.semester).toBe('20232') - expect(secondSemester.end_date).toBe(expectedCourseEndDatum) - }) - - test('compare sorting results so it is independent of semesters order', () => { - const offeredSemestersReversed = course.offered_semesters.reverse() - const offeredSemesters = course.offered_semesters - - const result1 = sortOfferedSemesters(offeredSemestersReversed) - const result2 = sortOfferedSemesters(offeredSemesters) - - expect(result1).toEqual(result2) - }) -}) - -describe('Find if it parses the correct start and end date, course stretched over several semesters', () => { - test('get start and end dates, week, last semester', () => { - const offeredSemesters = course.offered_semesters - - const { endDate, endWeek, lastSemester: lastCSemester, startDate } = findCourseStartEndDates(offeredSemesters) - expect(startDate).toBe(expectedCourseStartDatum) - expect(endWeek).toBe('43') - - expect(lastCSemester).toBe('20232') - expect(endDate).toBe(expectedCourseEndDatum) - }) - - test('compare dates results so it is independent of semesters order', () => { - const offeredSemestersReversed = course.offered_semesters.reverse() - const offeredSemesters = course.offered_semesters - - const result1 = findCourseStartEndDates(offeredSemestersReversed) - const result2 = findCourseStartEndDates(offeredSemesters) - - expect(result1).toEqual(result2) - }) -}) - -describe('Course Analysis check if it parses the correct start and end date, course stretched over several semesters', () => { - test('chosen summer season but course finished in autumn, filter out it', () => { - const result = filterOfferingsForAnalysis([course], ['20231', '20232'], ['0'], 'allSchools', 'sv') - expect(result.length).toBe(0) - expect(result).toMatchInlineSnapshot(`[]`) - }) - - test('chosen spring season but course finished in autumn, filter out it', () => { - const result = filterOfferingsForAnalysis([course], ['20231'], ['1'], 'allSchools', 'sv') - expect(result.length).toBe(0) - expect(result).toMatchInlineSnapshot(`[]`) - }) - - test('chosen autumn season and course finished in autumn, parse it, in swedish', () => { - const result = filterOfferingsForAnalysis([course], ['20232'], ['2'], 'allSchools', 'sv') - expect(result.length).toBe(1) - const [expected] = result - expect(expected.startDate).toBe(startSemester.start_date) - expect(expected.startDate).toBe(expectedCourseStartDatum) - - expect(expected.endDate).toBe(lastSemester.end_date) - expect(expected.endDate).toBe(expectedCourseEndDatum) - - expect(expected.firstSemester).toBe(course.first_yearsemester) - expect(expected.lastSemester).toBe(lastSemester.semester) - expect(expected.lastSemesterLabel).toBe('HT') - - expect(result).toMatchInlineSnapshot(` - [ - { - "connectedPrograms": "LÄRGR-2", - "courseCode": "LT1015", - "courseRoundApplications": [ - { - "course_round_application_code": "45006", - "course_round_category_code": "PU", - "course_round_ladok_state": "Påbörjad", - "course_round_ladok_state_en": "Started", - "course_round_type": "ORD", - "ladok_uid": "de69ebaf-7226-11ec-a534-0f06b63e4534", - }, - ], - "departmentName": "ITM/Lärande", - "endDate": "2023-10-29", - "firstSemester": "20231", - "lastSemester": "20232", - "lastSemesterLabel": "HT", - "schoolMainCode": "ITM", - "startDate": "2023-06-12", - }, - ] - `) - }) - - test('chosen autumn season and course finished in autumn, parse it, in english', () => { - const result = filterOfferingsForAnalysis([course], ['20231', '20232'], ['2'], 'allSchools', 'en') - const [expected] = result - expect(expected.startDate).toBe(startSemester.start_date) - expect(expected.endDate).toBe(lastSemester.end_date) - expect(expected.lastSemesterLabel).toBe('Autumn') - }) - - test('compare offerings results so it is independent of semesters order', () => { - const offeredSemestersReversed = course.offered_semesters.reverse() - const offeredSemesters = course.offered_semesters - const course1 = filterOfferingsForAnalysis( - [{ ...course, offered_semesters: offeredSemestersReversed }], - ['20231', '20232'], - ['2'], - 'allSchools', - 'en' - ) - - const course2 = filterOfferingsForAnalysis( - [{ ...course, offered_semesters: offeredSemesters }], - ['20231', '20232'], - ['2'], - 'allSchools', - 'en' - ) - - expect(course1).toEqual(course2) - }) -}) - -describe('Course memos check if it parse the correct start and end date, course stretched over several semesters', () => { - test('chosen autumn season but course starts in summer, filter out it', () => { - const result = filterOfferingsForMemos([course], ['20232'], ['2'], 'allSchools', 'sv') - expect(result.length).toBe(0) - expect(result).toMatchInlineSnapshot(`[]`) - }) - - test('chosen spring season but course starts in summer, filter out it', () => { - const result = filterOfferingsForMemos([course], ['20231'], ['1'], 'allSchools', 'sv') - expect(result.length).toBe(0) - expect(result).toMatchInlineSnapshot(`[]`) - }) - - test('chosen summer season P0 (early summer) and course starts in late summer P5, filter out it', () => { - const result = filterOfferingsForMemos([course], ['20231'], ['0'], 'allSchools', 'sv') - expect(result.length).toBe(0) - expect(result).toMatchInlineSnapshot(`[]`) - }) - - test('chosen summer season P5 (late summer) and course starts in summer P5, parse it, in swedish', () => { - const result = filterOfferingsForMemos([course], ['20231', '20232'], ['5'], 'allSchools', 'sv') - expect(result.length).toBe(1) - const [expected] = result - expect(expected.startDate).toBe(startSemester.start_date) - expect(expected.startDate).toBe(expectedCourseStartDatum) - - expect(expected.endDate).toBe(lastSemester.end_date) - expect(expected.endDate).toBe(expectedCourseEndDatum) - - expect(expected.firstSemester).toBe(course.first_yearsemester) - expect(expected.period).toBe('P5') - - expect(result).toMatchInlineSnapshot(` - [ - { - "connectedPrograms": "LÄRGR-2", - "courseCode": "LT1015", - "courseRoundApplications": [ - { - "course_round_application_code": "45006", - "course_round_category_code": "PU", - "course_round_ladok_state": "Påbörjad", - "course_round_ladok_state_en": "Started", - "course_round_type": "ORD", - "ladok_uid": "de69ebaf-7226-11ec-a534-0f06b63e4534", - }, - ], - "departmentName": "ITM/Lärande", - "endDate": "2023-10-29", - "firstSemester": "20231", - "period": "P5", - "schoolMainCode": "ITM", - "startDate": "2023-06-12", - }, - ] - `) - }) - - test('chosen summer season P5 (late summer) and course starts in summer P5, parse it, in english', () => { - const result = filterOfferingsForMemos([course], ['20231', '20232'], ['5'], 'allSchools', 'en') - const [expected] = result - expect(expected.startDate).toBe(startSemester.start_date) - expect(expected.endDate).toBe(lastSemester.end_date) - expect(expected.period).toBe('P5') - }) - - test('compare offerings results so it is independent of semesters order', () => { - const offeredSemestersReversed = course.offered_semesters.reverse() - const offeredSemesters = course.offered_semesters - const course1 = filterOfferingsForMemos( - [{ ...course, offered_semesters: offeredSemestersReversed }], - ['20231', '20232'], - ['5'], - 'allSchools', - 'en' - ) - - const course2 = filterOfferingsForMemos( - [{ ...course, offered_semesters: offeredSemesters }], - ['20231', '20232'], - ['5'], - 'allSchools', - 'en' - ) - - expect(course1).toEqual(course2) - }) -}) diff --git a/server/apiCalls/transformers/offerings.js b/server/apiCalls/transformers/offerings.js index 5280214d..9b691745 100644 --- a/server/apiCalls/transformers/offerings.js +++ b/server/apiCalls/transformers/offerings.js @@ -1,4 +1,4 @@ -const { labelSeason, seasonConstants } = require('../../../domain/statistics/seasons') +const { labelSeason } = require('../../../domain/statistics/seasons') const { isCorrectSchool, SCHOOL_MAP } = require('./schools') /** @@ -122,24 +122,6 @@ function filterOfferingsForMemos(courses = [], chosenSemesters = [], chosenPerio return parsedOfferings } -/** - * I så fall skulle logiken bli följande: - * Spring VT - kurser slutar mellan vecka 3-23, - * Autumn HT - kurser slutar mellan vecka 35-2, - * Summer Sommar - kurser slutar mellan veckor 24-34 - * @param {string} endWeek A number of a end week, the range is 0-35 - * @returns {number} Return number 0-2 for a season, 0 Summer, 1 Spring, 2 Autumn - */ - -function _parseTermSeasonForNthWeek(nthWeek) { - const week = Number(nthWeek) - let seasonNumber - if (week >= 35 || week <= 2) return seasonConstants.AUTUMN_TERM_NUMBER - if (week >= 3 && week <= 23) return seasonConstants.SPRING_TERM_NUMBER - if (week >= 24 && week <= 34) return seasonConstants.SUMMER_TERM_NUMBER - return seasonNumber -} - /** * Parses courses offerings from Kopps and returns an object with one list for course analyses which are created after course ends: * - List containing offerings that ends with semester parameter. This is used for course analyses. @@ -150,57 +132,47 @@ function _parseTermSeasonForNthWeek(nthWeek) { * @param {string} courses[].offered_semesters[].end_week - The end week of a course offering to calculate the end period * @param {string} courses[].offered_semesters[].semester - The current semester of a course offering * @param {string} courses[].offered_semesters[].start_date - The start date of a course offering - * @param {Object[]} chosenSemesters Semesters strings for which data is fetched - * @param {string} chosenSemesters[] Semester string chosen by user, 5 digits in string format - * @param {Object[]} chosenSeasons Seasons strings chosen by user to compare with end week's season to filter terms, 0-2 (summer, spring, autumn) - * @param {string} chosenSeasons[] Season string chosen by user to compare with end week's season to filter term, 1 digit in string format, 0-2 (summer, spring, autumn) + * @param {string} chosenSemester Semester string for which data is fetched, 5 digits in string format * @param {string} chosenSchool School name, or if all schools are chosen then 'allSchools * @param {string} language User interface language, "sv" or "en" * @returns {[]} Array, containing offerings’ relevant data */ -function filterOfferingsForAnalysis( - courses = [], - chosenSemesters = [], - chosenSeasons = [], - chosenSchool = '', - language = 'sv' -) { +function filterOfferingsForAnalysis(courses = [], chosenSemester, chosenSchool = '', language = 'sv') { const parsedOfferings = [] - if (Array.isArray(courses)) { - courses.forEach(course => { - // eslint-disable-next-line camelcase - const { - first_yearsemester: firstSemester, - offered_semesters: courseOfferedSemesters, - school_code: schoolCode, - course_round_applications: courseRoundApplications, - } = course - const { endDate, endWeek, lastSemester, startDate } = _findCourseStartEndDates(courseOfferedSemesters) - - const lastTermSeasonNumber = endDate ? _parseTermSeasonForNthWeek(endWeek) : '' - const lastTermSeasonLabel = endDate ? labelSeason(Number(lastTermSeasonNumber), language === 'en' ? 0 : 1) : '' - - const isFinishedInChosenSemesters = - chosenSemesters.includes(String(lastSemester)) && chosenSeasons.includes(String(lastTermSeasonNumber)) - const isChosenSchool = isCorrectSchool(chosenSchool, schoolCode) + courses.forEach(course => { + const { + first_yearsemester: firstSemester, + offered_semesters: courseOfferedSemesters, + school_code: schoolCode, + course_round_applications: courseRoundApplications, + } = course + + const { endDate, lastSemester, startDate } = _findCourseStartEndDates(courseOfferedSemesters) + + const lastTermSeasonNumber = lastSemester.at(4) + const lastTermSeasonLabel = lastTermSeasonNumber + ? labelSeason(Number(lastTermSeasonNumber), language === 'en' ? 0 : 1) + : '' + + const isFinishedInChosenSemesters = chosenSemester === lastSemester + const isChosenSchool = isCorrectSchool(chosenSchool, schoolCode) + + if (isFinishedInChosenSemesters && isChosenSchool) { + const offering = _formOffering(firstSemester, startDate, endDate, course) + parsedOfferings.push({ + ...offering, + lastSemesterLabel: lastTermSeasonLabel, + lastSemester, + courseRoundApplications, + }) + } + }) - if (isFinishedInChosenSemesters && isChosenSchool) { - const offering = _formOffering(firstSemester, startDate, endDate, course) - parsedOfferings.push({ - ...offering, - lastSemesterLabel: lastTermSeasonLabel, - lastSemester, - courseRoundApplications, - }) - } - }) - } return parsedOfferings } module.exports = { - parsePeriodForNthWeek: _parseTermSeasonForNthWeek, filterOfferingsForMemos, filterOfferingsForAnalysis, findCourseStartEndDates: _findCourseStartEndDates, diff --git a/server/apiCalls/transformers/offerings.test.js b/server/apiCalls/transformers/offerings.test.js deleted file mode 100644 index 18086688..00000000 --- a/server/apiCalls/transformers/offerings.test.js +++ /dev/null @@ -1,274 +0,0 @@ -'use strict' - -import { - parsePeriodForNthWeek, - filterOfferingsForMemos, - filterOfferingsForAnalysis, - semestersInParsedOfferings, -} from './offerings' - -describe('Memos functions to parse and filter offerings', () => { - test('parse and filter offering for memos', () => { - const expectedCourse = { - connected_programs: [{ code: 'CINTE2', study_year: '2020', spec_code: 'iNTeresting' }], - course_code: 'SF1624', - department_name: 'CBH/Fiber- och Polymerteknologi', - first_yearsemester: '20202', - first_period: '20202P1', - offered_semesters: [{ end_date: '2021-01-10', semester: '20202', start_date: '2020-10-10' }], - school_code: 'BIO', - } - const tooOldCourse = { - connected_programs: [{ code: 'CINTE1', study_year: '2020', spec_code: 'k' }], - course_code: 'SF1625', - first_yearsemester: '20192', - first_period: '20192P1', - offered_semesters: [], - school_code: 'CBH', - } - const wrongSchoolCourse = { - connected_programs: [{ code: 'CINTE1', study_year: '2020', spec_code: 'D' }], - course_code: 'SF1629', - first_yearsemester: '20202', - first_period: '20202P1', - offered_semesters: [], - school_code: 'ITM', - } - const courses = [expectedCourse, tooOldCourse, wrongSchoolCourse] - const parsedOfferings = filterOfferingsForMemos(courses, ['20201', '20202'], ['1', '2'], 'CBH') - expect(parsedOfferings.length).toBe(1) - - const [offering] = parsedOfferings - expect(offering.firstSemester).toBe(expectedCourse.first_yearsemester) - expect(offering.startDate).toBe(expectedCourse.offered_semesters[0].start_date) - expect(offering.endDate).toBe(expectedCourse.offered_semesters[0].end_date) - expect(offering.schoolMainCode).toBe('CBH') - expect(offering.departmentName).toBe(expectedCourse.department_name) - expect(offering.connectedPrograms).toBe('CINTE2-iNTeresting-2020') - expect(offering.courseCode).toBe(expectedCourse.course_code) - expect(offering.period).toBe('P1') - - const semestersInMemos = semestersInParsedOfferings(parsedOfferings) - expect(semestersInMemos.length).toBe(1) - }) -}) - -describe('Analysis functions to parse and filter offerings', () => { - test('return an autumn number for an end week', () => { - expect(parsePeriodForNthWeek(43)).toBe(2) - expect(parsePeriodForNthWeek('43')).toBe(2) - expect(parsePeriodForNthWeek(2)).toBe(2) - expect(parsePeriodForNthWeek('2')).toBe(2) - expect(parsePeriodForNthWeek(35)).toBe(2) - expect(parsePeriodForNthWeek('35')).toBe(2) - }) - test('return a summer number for an end week', () => { - expect(parsePeriodForNthWeek(24)).toBe(0) - expect(parsePeriodForNthWeek('24')).toBe(0) - expect(parsePeriodForNthWeek(28)).toBe(0) - expect(parsePeriodForNthWeek('27')).toBe(0) - expect(parsePeriodForNthWeek(34)).toBe(0) - expect(parsePeriodForNthWeek('34')).toBe(0) - }) - test('return a spring number for an end week', () => { - expect(parsePeriodForNthWeek(3)).toBe(1) - expect(parsePeriodForNthWeek('3')).toBe(1) - expect(parsePeriodForNthWeek(20)).toBe(1) - expect(parsePeriodForNthWeek('18')).toBe(1) - expect(parsePeriodForNthWeek(23)).toBe(1) - expect(parsePeriodForNthWeek('23')).toBe(1) - }) - test('find matching course offering. Course finishes in autumn, user has chosen summer, autumn and spring semesters.', () => { - const notexpectedSemester = { end_week: '41', end_date: '2020-10-10', semester: '20212', start_date: '2020-02-10' } - const expectedSemester = { end_week: '46', end_date: '2020-11-10', semester: '20202', start_date: '2020-10-10' } - const expectedCourse = { - connected_programs: [{ code: 'CINTE2', study_year: '2020', spec_code: 'iNTeresting' }], - course_code: 'SF1624', - department_name: 'CBH/Fiber- och Polymerteknologi', - first_yearsemester: '20202', - first_period: '20202P1', - offered_semesters: [notexpectedSemester, expectedSemester], - school_code: 'BIO', - } - const tooOldCourse = { - connected_programs: [{ code: 'CINTE1', study_year: '2020', spec_code: 'k' }], - course_code: 'SF1625', - first_yearsemester: '20192', - first_period: '20192P1', - offered_semesters: [], - school_code: 'CBH', - } - const wrongSchoolCourse = { - connected_programs: [{ code: 'CINTE1', study_year: '2020', spec_code: 'D' }], - course_code: 'SF1629', - first_yearsemester: '20202', - first_period: '20202P1', - offered_semesters: [], - school_code: 'ITM', - } - const courses = [expectedCourse, tooOldCourse, wrongSchoolCourse] - // Swedish - const parsedOfferings = filterOfferingsForAnalysis(courses, ['20201', '20202'], ['1', '2', '0'], 'CBH', 'sv') - expect(parsedOfferings.length).toBe(1) - - const [offering] = parsedOfferings - expect(offering.firstSemester).toBe(expectedCourse.first_yearsemester) - expect(offering.startDate).toBe(expectedCourse.offered_semesters[0].start_date) - expect(offering.endDate).toBe(expectedCourse.offered_semesters[1].end_date) - expect(offering.schoolMainCode).toBe('CBH') - expect(offering.departmentName).toBe(expectedCourse.department_name) - expect(offering.connectedPrograms).toBe('CINTE2-iNTeresting-2020') - expect(offering.courseCode).toBe(expectedCourse.course_code) - expect(offering.lastSemester).toBe('20202') - expect(offering.lastSemesterLabel).toBe('HT') - - // English - const parsedOfferings_en = filterOfferingsForAnalysis(courses, ['20201', '20202'], ['1', '2', '0'], 'CBH', 'en') - expect(parsedOfferings_en.length).toBe(1) - - const [offering_en] = parsedOfferings_en - expect(offering_en.firstSemester).toBe(expectedCourse.first_yearsemester) - expect(offering_en.startDate).toBe(expectedCourse.offered_semesters[0].start_date) - expect(offering_en.endDate).toBe(expectedCourse.offered_semesters[1].end_date) - expect(offering_en.schoolMainCode).toBe('CBH') - expect(offering_en.departmentName).toBe(expectedCourse.department_name) - expect(offering_en.connectedPrograms).toBe('CINTE2-iNTeresting-2020') - expect(offering_en.courseCode).toBe(expectedCourse.course_code) - expect(offering_en.lastSemester).toBe('20202') - expect(offering_en.lastSemesterLabel).toBe('Autumn') - }) - test('find no matching offering. Course finishes in autumn, user chosen summer semester.', () => { - const notexpectedSemester = { end_week: '40', end_date: '2021-10-10', semester: '20212', start_date: '2020-10-10' } - const notexpectedSemesterLast = { - end_week: '1', - end_date: '2021-01-10', - semester: '20202', - start_date: '2020-10-10', - } - const expectedCourse = { - connected_programs: [{ code: 'CINTE2', study_year: '2020', spec_code: 'iNTeresting' }], - course_code: 'SF1624', - department_name: 'CBH/Fiber- och Polymerteknologi', - first_yearsemester: '20202', - first_period: '20202P1', - offered_semesters: [notexpectedSemester, notexpectedSemesterLast], - school_code: 'BIO', - } - const tooOldCourse = { - connected_programs: [{ code: 'CINTE1', study_year: '2020', spec_code: 'k' }], - course_code: 'SF1625', - first_yearsemester: '20192', - first_period: '20192P1', - offered_semesters: [], - school_code: 'CBH', - } - const wrongSchoolCourse = { - connected_programs: [{ code: 'CINTE1', study_year: '2020', spec_code: 'D' }], - course_code: 'SF1629', - first_yearsemester: '20202', - first_period: '20202P1', - offered_semesters: [], - school_code: 'ITM', - } - const courses = [expectedCourse, tooOldCourse, wrongSchoolCourse] - const parsedOfferings = filterOfferingsForAnalysis(courses, ['20201', '20202'], ['0'], 'CBH', 'sv') - expect(parsedOfferings.length).toBe(0) - }) - - test('find no matching offering. Course finishes in autumn, user chosen spring semester.', () => { - const notexpectedSemester = { end_week: '40', end_date: '2021-10-10', semester: '20212', start_date: '2020-10-10' } - const notexpectedSemesterLast = { - end_week: '1', - end_date: '2021-01-10', - semester: '20202', - start_date: '2020-10-10', - } - const expectedCourse = { - connected_programs: [{ code: 'CINTE2', study_year: '2020', spec_code: 'iNTeresting' }], - course_code: 'SF1624', - department_name: 'CBH/Fiber- och Polymerteknologi', - first_yearsemester: '20202', - first_period: '20202P1', - offered_semesters: [notexpectedSemester, notexpectedSemesterLast], - school_code: 'BIO', - } - const tooOldCourse = { - connected_programs: [{ code: 'CINTE1', study_year: '2020', spec_code: 'k' }], - course_code: 'SF1625', - first_yearsemester: '20192', - first_period: '20192P1', - offered_semesters: [], - school_code: 'CBH', - } - const wrongSchoolCourse = { - connected_programs: [{ code: 'CINTE1', study_year: '2020', spec_code: 'D' }], - course_code: 'SF1629', - first_yearsemester: '20202', - first_period: '20202P1', - offered_semesters: [], - school_code: 'ITM', - } - const courses = [expectedCourse, tooOldCourse, wrongSchoolCourse] - const parsedOfferings = filterOfferingsForAnalysis(courses, ['20201', '20202'], ['1'], 'CBH', 'sv') - expect(parsedOfferings.length).toBe(0) - }) - - test('find no mathing summer course offering because of wrong year. Course finishes in summer 2022, user chosen summer semester in 2020.', () => { - const semester = { end_week: '28', end_date: '2022-07-15', semester: '20221', start_date: '2022-03-10' } - const course = { - connected_programs: [{ code: 'CINTE2', study_year: '2022', spec_code: 'iNTeresting' }], - course_code: 'SF1624', - department_name: 'CBH/Fiber- och Polymerteknologi', - first_yearsemester: '20221', - first_period: '20221P5', - offered_semesters: [semester], - school_code: 'EES', - } - const courses = [course] - const parsedOfferings = filterOfferingsForAnalysis(courses, ['20201', '20202'], ['0'], 'EECS', 'sv') - expect(parsedOfferings.length).toBe(0) - }) - - test('find matching summer course offering. Course finishes in summer 2022, user chosen summer semester 2022.', () => { - const semester = { end_week: '28', end_date: '2022-07-15', semester: '20221', start_date: '2022-03-10' } - const expectedCourse = { - connected_programs: [{ code: 'CINTE2', study_year: '2022', spec_code: 'iNTeresting' }], - course_code: 'SF1624', - department_name: 'CBH/Fiber- och Polymerteknologi', - first_yearsemester: '20221', - first_period: '20221P5', - offered_semesters: [semester], - school_code: 'EES', - } - const courses = [expectedCourse] - // Swedish - const parsedOfferings = filterOfferingsForAnalysis(courses, ['20221', '20222'], ['0'], 'EECS', 'sv') - expect(parsedOfferings.length).toBe(1) - - const [offering] = parsedOfferings - expect(offering.firstSemester).toBe(expectedCourse.first_yearsemester) - expect(offering.startDate).toBe(expectedCourse.offered_semesters[0].start_date) - expect(offering.endDate).toBe(expectedCourse.offered_semesters[0].end_date) - expect(offering.schoolMainCode).toBe('EECS') - expect(offering.departmentName).toBe(expectedCourse.department_name) - expect(offering.connectedPrograms).toBe('CINTE2-iNTeresting-2022') - expect(offering.courseCode).toBe(expectedCourse.course_code) - expect(offering.lastSemester).toBe('20221') - expect(offering.lastSemesterLabel).toBe('Sommar') - - // English - const parsedOfferings_en = filterOfferingsForAnalysis(courses, ['20221', '20222'], ['0'], 'EECS', 'en') - expect(parsedOfferings_en.length).toBe(1) - - const [offering_en] = parsedOfferings_en - expect(offering_en.firstSemester).toBe(expectedCourse.first_yearsemester) - expect(offering_en.startDate).toBe(expectedCourse.offered_semesters[0].start_date) - expect(offering_en.endDate).toBe(expectedCourse.offered_semesters[0].end_date) - expect(offering_en.schoolMainCode).toBe('EECS') - expect(offering_en.departmentName).toBe(expectedCourse.department_name) - expect(offering_en.connectedPrograms).toBe('CINTE2-iNTeresting-2022') - expect(offering_en.courseCode).toBe(expectedCourse.course_code) - expect(offering_en.lastSemester).toBe('20221') - expect(offering_en.lastSemesterLabel).toBe('Summer') - }) -}) diff --git a/server/controllers/statisticsCtrl.js b/server/controllers/statisticsCtrl.js index 518189f5..6e3ca295 100644 --- a/server/controllers/statisticsCtrl.js +++ b/server/controllers/statisticsCtrl.js @@ -144,8 +144,7 @@ async function fetchMemoStatistics(req, res, next) { * @param {Object} req.params * @param {string} req.params.year Year for which statistics will be fetched * @param {Object} req.query - * @param {Object[]} req.query.analysesSeasons - Transformed seasons chosen by users to use in analysis api, there are exists only autumn and/or spring semester, summer is replaced by autumn and spring seasons - * @param {Object[]} req.query.seasons - Seasons chosen by user in raw format, summer is not replaced + * @param {number} req.query.semester - Semester chosen by user in raw format * @param {string} req.query.school * @param {string} req.query.l - Language "sv" or "en" * @param {*} res @@ -157,19 +156,16 @@ async function fetchAnalysisStatistics(req, res, next) { log.info(` trying to fetch course analysis statistics `, { params, query }) const { year } = params - const { analysesSeasons, seasons, school, l: language } = query - // in analysis api, exists only autumn and/or spring semester, summer is replaced by autumn and spring seasons - if (!analysesSeasons) log.error('analysesSeasons must be set', analysesSeasons) - // seasons chosen by user, summer/autumn/spring - if (!seasons) log.error('seasons must be set', seasons) + const { semester, school, l: language } = query + if (!semester) log.error('semester must be set', semester) if (!school) log.error('school must be set', school) - const chosenSemesters = analysesSeasons.map(season => `${year}${season}`).sort() + const semesterWithYear = `${year}${semester}` try { - const courses = await _getCourses(chosenSemesters) + const courses = await _getCourses([semesterWithYear]) - const parsedOfferings = filterOfferingsForAnalysis(courses, chosenSemesters, seasons, school, language) + const parsedOfferings = filterOfferingsForAnalysis(courses, semesterWithYear, school, language) // Find start semesters found in parsed offerings. const startSemestersInAnalyses = semestersInParsedOfferings(parsedOfferings) @@ -177,7 +173,7 @@ async function fetchAnalysisStatistics(req, res, next) { const analyses = await courseAnalysesApi.getCourseAnalysesForStatistics(startSemestersInAnalyses) // Compiles statistics per school, including totals, for analyses. - const { offeringsWithAnalyses, combinedAnalysesPerSchool } = await analysesPerSchool(parsedOfferings, analyses) + const { offeringsWithAnalyses, combinedAnalysesPerSchool } = analysesPerSchool(parsedOfferings, analyses) return res.json({ combinedAnalysesPerSchool, // small table // in kursinfo-admin-web combinedMemosDataPerSchool, @@ -190,9 +186,7 @@ async function fetchAnalysisStatistics(req, res, next) { }${serverConfig.nodeApi.kursutvecklingApi.proxyBasePath}`, school, offeringsWithAnalyses, // big Table // in kursinfo-admin-web combinedDataPerDepartment, - seasons, - semesters: chosenSemesters, // prev semester - semestersInAnalyses: startSemestersInAnalyses, + semester, totalOfferings: courses.length, year, })