diff --git a/domain/curriculum.js b/domain/curriculum.js index 93632caf..4528d081 100644 --- a/domain/curriculum.js +++ b/domain/curriculum.js @@ -150,6 +150,83 @@ function curriculumInfo({ programmeTermYear = {}, curriculum }) { } } +function curriculumInfoFromLadok({ programmeTermYear = {}, curriculum }) { + let code = '' + let specializationName = null + let isCommon = true + + const participations = {} + const isFirstSpec = false + + const { programmeSpecialization, studyYears } = curriculum + const { studyYear } = programmeTermYear + + if (programmeSpecialization) { + code = programmeSpecialization.programmeSpecializationCode + specializationName = programmeSpecialization.title + isCommon = false + } + + const [curriculumStudyYear] = studyYears.filter(s => Math.abs(s.yearNumber) === Math.abs(studyYear)) + + if (curriculumStudyYear) { + for (const course of curriculumStudyYear.courses) { + if (!participations[course.Valvillkor]) participations[course.Valvillkor] = [] + + const termCode = course.startperiod.code.startsWith('HT') ? '1' : '2' + const year = course.startperiod.code.replace(/[^0-9]/g, '') + const term = `${year}${termCode}` + + const creditsPerPeriod = [0, 0, 0, 0, 0, 0] + + course.Tillfallesperioder.forEach(period => { + if (period.Lasperiodsfordelning) { + period.Lasperiodsfordelning.forEach(lasperiod => { + const periodIndex = + { + P1: 1, + P2: 2, + P3: 3, + P4: 4, + }[lasperiod.Lasperiodskod] || 0 + + creditsPerPeriod[periodIndex] += lasperiod.Omfattningsvarde + }) + } else { + creditsPerPeriod[1] += period.Omfattningsvarde // Default to first period + } + }) + + participations[course.Valvillkor].push({ + course: { + courseCode: course.kod, + title: course.benamning, + credits: course.omfattning.number, + formattedCredits: course.omfattning.formattedWithUnit, + educationalLevel: course.utbildningstyp?.level?.name, + electiveCondition: course.Valvillkor, + }, + applicationCodes: [course.tillfalleskod], + term, + creditsPerPeriod, + }) + + participations[course.Valvillkor].sort((a, b) => a.term.localeCompare(b.term)) + } + } + + const hasInfo = Object.keys(participations).length !== 0 + + return { + code, + specializationName, + isCommon, + participations, + isFirstSpec, + hasInfo, + } +} + function setFirstSpec(cis) { for (let i = 0; i < cis.length; i++) { const ci = cis[i] @@ -164,6 +241,7 @@ const ELECTIVE_CONDITIONS = ['ALL', 'O', 'VV', 'R', 'V'] module.exports = { curriculumInfo, + curriculumInfoFromLadok, setFirstSpec, filterCourseRoundsForNthYear, ELECTIVE_CONDITIONS, diff --git a/package-lock.json b/package-lock.json index cec17827..e9d2f994 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,10 +94,11 @@ } }, "../studadm-om-kursen-packages/packages/om-kursen-ladok-client": { - "version": "0.0.1", + "name": "@kth/om-kursen-ladok-client", + "version": "1.1.0", "dependencies": { - "ladok-attributvarde-utils": "file:../ladok-attributvarde-utils", - "ladok-mellanlager-client": "file:../ladok-mellanlager-client" + "@kth/ladok-attributvarde-utils": "file:../ladok-attributvarde-utils", + "@kth/ladok-mellanlager-client": "file:../ladok-mellanlager-client" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/public/js/app/pages/Appendix1.jsx b/public/js/app/pages/Appendix1.jsx index 2fe7fb5e..8cf3d687 100644 --- a/public/js/app/pages/Appendix1.jsx +++ b/public/js/app/pages/Appendix1.jsx @@ -21,7 +21,7 @@ import { courseLink } from '../util/links' function CourseListTableRow({ course }) { const { language } = useStore() const t = translate(language) - const { code, name, comment, credits, creditAbbr, level } = course + const { code, name, comment, credits, creditAbbr, formattedCredits, formattedLevel, level } = course const courselink = courseLink(code, language) return ( @@ -33,8 +33,10 @@ function CourseListTableRow({ course }) { {name} {comment && {comment}} - {`${formatCredits(language, credits)} ${creditAbbr}`} - {`${t('programme_edulevel')[level]}`} + + {formattedCredits ? formattedCredits : `${formatCredits(language, credits)} ${creditAbbr}`} + + {formattedLevel ? formattedLevel : `${t('programme_edulevel')[level]}`} ) } @@ -44,8 +46,10 @@ const courseType = PropTypes.shape({ name: PropTypes.string.isRequired, comment: PropTypes.oneOfType([PropTypes.string, undefined]).isRequired, credits: PropTypes.number.isRequired, - creditAbbr: PropTypes.string.isRequired, - level: PropTypes.string.isRequired, + creditAbbr: PropTypes.string, + formattedCredits: PropTypes.string, + level: PropTypes.string, + formattedLevel: PropTypes.string, }) CourseListTableRow.propTypes = { diff --git a/public/js/app/pages/Curriculum.jsx b/public/js/app/pages/Curriculum.jsx index 449fba41..90b936c3 100644 --- a/public/js/app/pages/Curriculum.jsx +++ b/public/js/app/pages/Curriculum.jsx @@ -43,11 +43,14 @@ function CourseTableRow({ creditsPerPeriod, }) { const { language } = useStore() + console.log('here', credits) return ( {courseNameCellData} {applicationCodeCellData} - {`${formatCredits(language, credits)} ${creditUnitAbbr}`} + + {creditUnitAbbr ? `${formatCredits(language, credits)} ${creditUnitAbbr}` : credits} + ) @@ -59,8 +62,11 @@ function CourseTableRows({ participations }) { return participations.map(participation => { const { course, applicationCodes, term, creditsPerPeriod } = participation - const { courseCode, title, credits, creditUnitAbbr, comment } = course - const translatedCreditUnitAbbr = translateCreditUnitAbbr(language, creditUnitAbbr) + console.log(course) + + const { courseCode, title, credits, formattedCredits, creditUnitAbbr, comment } = course + let translatedCreditUnitAbbr + if (!formattedCredits) translatedCreditUnitAbbr = translateCreditUnitAbbr(language, creditUnitAbbr) const currentTerm = getCurrentTerm() const courseNameCellData = ( <> @@ -75,7 +81,7 @@ function CourseTableRows({ participations }) { courseCode={courseCode} courseNameCellData={courseNameCellData} applicationCodeCellData={applicationCodeCellData} - credits={credits} + credits={formattedCredits ? formattedCredits : credits} creditUnitAbbr={translatedCreditUnitAbbr} creditsPerPeriod={creditsPerPeriod} /> diff --git a/server/controllers/appendix1Ctrl.js b/server/controllers/appendix1Ctrl.js index c2751e7c..778cb3d7 100644 --- a/server/controllers/appendix1Ctrl.js +++ b/server/controllers/appendix1Ctrl.js @@ -45,10 +45,10 @@ async function getIndex(req, res, next) { const options = { applicationStore, lang, programmeCode, term } log.info(`Starting to fill in application store ${storeId} on server side `, { programmeCode }) - const { programmeName } = await fetchAndFillProgrammeDetails(options, storeId) + const { programmeName, tillfalleUid } = await fetchAndFillProgrammeDetails(options, storeId) fillStoreWithQueryParams(options) - await fetchAndFillCurriculumList(options) + await fetchAndFillCurriculumList(options, tillfalleUid) const compressedStoreCode = getCompressedStoreCode(applicationStore) log.info(`${storeId} store was filled in and compressed on server side`, { programmeCode }) diff --git a/server/controllers/appendix2Ctrl.js b/server/controllers/appendix2Ctrl.js index fdde112e..1defe127 100644 --- a/server/controllers/appendix2Ctrl.js +++ b/server/controllers/appendix2Ctrl.js @@ -54,9 +54,9 @@ async function getIndex(req, res, next) { const options = { applicationStore, lang, programmeCode, term } log.info(`Starting to fill application store, for ${storeId}`) - const { programmeName } = await fetchAndFillProgrammeDetails(options, storeId) + const { programmeName, tillfalleUid } = await fetchAndFillProgrammeDetails(options, storeId) fillStoreWithQueryParams(options) - await fetchAndFillSpecializations(options) + await fetchAndFillSpecializations(options, tillfalleUid) const compressedStoreCode = getCompressedStoreCode(applicationStore) log.info(`${storeId} store was filled in and compressed on server side`) diff --git a/server/controllers/curriculumCtrl.js b/server/controllers/curriculumCtrl.js index 2e6b3763..0953c18a 100644 --- a/server/controllers/curriculumCtrl.js +++ b/server/controllers/curriculumCtrl.js @@ -5,13 +5,15 @@ const { server: serverConfig } = require('../configuration') const i18n = require('../../i18n') const koppsApi = require('../kopps/koppsApi') -const { curriculumInfo, setFirstSpec } = require('../../domain/curriculum') +const { curriculumInfo, curriculumInfoFromLadok, setFirstSpec } = require('../../domain/curriculum') const { calculateStartTerm } = require('../../domain/academicYear') const { createProgrammeBreadcrumbs } = require('../utils/breadcrumbUtil') const { getServerSideFunctions } = require('../utils/serverSideRendering') const { programmeFullName } = require('../utils/programmeFullName') +const { getProgramStructure, getActiveProgramTillfalle } = require('../ladok/ladokApi') + const { fillStoreWithQueryParams, fetchAndFillProgrammeDetails, @@ -79,21 +81,32 @@ function _compareCurriculum(a, b) { * @param {string} options.term * @param {string} storeId */ -async function _fetchAndFillCurriculumByStudyYear(options, storeId) { +async function _fetchAndFillCurriculumByStudyYear(options, storeId, tillfalleUid) { const { applicationStore, lang, programmeCode, studyYear, term } = options const { studyProgrammeId, statusCode } = await fetchAndFillStudyProgrammeVersion({ ...options, storeId }) if (!studyProgrammeId) { _setErrorMissingAdmission(applicationStore, statusCode) return } // react NotFound - const { curriculums, statusCode: secondStatusCode } = await koppsApi.listCurriculums(studyProgrammeId, lang) - applicationStore.setStatusCode(secondStatusCode) - if (secondStatusCode !== 200) return // react NotFound - - const curriculumsWithCourseRounds = await _addCourseRounds(curriculums, programmeCode, term, studyYear, lang) - applicationStore.setCurriculums(curriculumsWithCourseRounds) - const curriculumInfos = curriculumsWithCourseRounds - .map(curriculum => curriculumInfo({ programmeTermYear: { programStartTerm: term, studyYear }, curriculum })) + + let curriculumData + + if (tillfalleUid) { + curriculumData = await getProgramStructure(tillfalleUid, lang) + } else { + const { curriculums, statusCode: secondStatusCode } = await koppsApi.listCurriculums(studyProgrammeId, lang) + applicationStore.setStatusCode(secondStatusCode) + if (secondStatusCode !== 200) return // react NotFound + curriculumData = await _addCourseRounds(curriculums, programmeCode, term, studyYear, lang) + } + + applicationStore.setCurriculums(curriculumData) + const curriculumInfos = curriculumData + .map(curriculum => { + if (tillfalleUid) + return curriculumInfoFromLadok({ programmeTermYear: { programStartTerm: term, studyYear }, curriculum }) + else return curriculumInfo({ programmeTermYear: { programStartTerm: term, studyYear }, curriculum }) + }) .filter(ci => ci.hasInfo) curriculumInfos.sort(_compareCurriculum) setFirstSpec(curriculumInfos) @@ -134,10 +147,11 @@ async function getIndex(req, res, next) { const options = { applicationStore, lang, programmeCode, term, studyYear } log.info(`Starting to fill in application store ${storeId} on server side `, { programmeCode }) - const { programmeName } = await fetchAndFillProgrammeDetails(options, storeId) + + const { programmeName, tillfalleUid } = await fetchAndFillProgrammeDetails(options, storeId) fillStoreWithQueryParams(options) - await _fetchAndFillCurriculumByStudyYear(options, storeId) + await _fetchAndFillCurriculumByStudyYear(options, storeId, tillfalleUid) const compressedStoreCode = getCompressedStoreCode(applicationStore) log.info(`${storeId} store was filled in and compressed`, { programmeCode }) diff --git a/server/ladok/ladokApi.js b/server/ladok/ladokApi.js index 81d82aab..43bc4d3e 100644 --- a/server/ladok/ladokApi.js +++ b/server/ladok/ladokApi.js @@ -9,6 +9,20 @@ async function searchCourses(pattern, lang) { return courses } +async function getActiveProgramTillfalle(programCode, startPeriod, lang) { + const client = createApiClient(serverConfig.ladokMellanlagerApi) + const courses = await client.getActiveProgramTillfalle(programCode, startPeriod, lang) + return courses +} + +async function getProgramStructure(programCode, lang) { + const client = createApiClient(serverConfig.ladokMellanlagerApi) + const courses = await client.getUtbildningstilfalleStructure(programCode, lang) + return courses +} + module.exports = { searchCourses, + getProgramStructure, + getActiveProgramTillfalle, } diff --git a/server/stores/programmeStoreSSR.js b/server/stores/programmeStoreSSR.js index 5f161c67..025927bf 100644 --- a/server/stores/programmeStoreSSR.js +++ b/server/stores/programmeStoreSSR.js @@ -3,6 +3,7 @@ const log = require('@kth/log') const { browser: browserConfig, server: serverConfig } = require('../configuration') const koppsApi = require('../kopps/koppsApi') const { programmeLink } = require('../../domain/links') +const { getActiveProgramTillfalle, getProgramStructure } = require('../ladok/ladokApi') /** * add props to a MobX-stores on server side @@ -43,15 +44,36 @@ function fillBrowserConfigWithHostUrl({ applicationStore }) { * @param {string} storeId * @returns {object} */ -async function fetchAndFillProgrammeDetails({ applicationStore, lang, programmeCode }, storeId = '') { +async function fetchAndFillProgrammeDetails({ applicationStore, term, lang, programmeCode }, storeId = '') { log.info('Fetching programme from KOPPs API, programmeCode:', programmeCode) - const { programme, statusCode } = await koppsApi.getProgramme(programmeCode, lang) - applicationStore.setStatusCode(statusCode) - if (statusCode !== 200 || !programme) { - log.debug('Failed to fetch from KOPPs api, programmeCode:', programmeCode) - return - } // react NotFound + let programDetails + + const year = term.slice(0, 4) + const convertedTerm = `${term.endsWith('1') ? 'VT' : 'HT'}${term.slice(0, 4)}` + + if (year >= 2024) { + // TODO - this is for test now, the exact year needs to be changed after the actual user stories are ready + const program = await getActiveProgramTillfalle(programmeCode, convertedTerm, lang) + programDetails = { + title: program.benamning, + lengthInStudyYears: program.lengthInStudyYears, + creditUnitAbbr: program.creditUnitAbbr, + owningSchoolCode: program.organisation.name, + credits: program.omfattning.number, + titleOtherLanguage: program.organisation.nameOther, + educationalLevel: program.tilltradesniva.name, + tillfalleUid: program.uid, + } + } else { + const { programme, statusCode } = await koppsApi.getProgramme(programmeCode, lang) + programDetails = programme + applicationStore.setStatusCode(statusCode) + if (statusCode !== 200 || !programme) { + log.debug('Failed to fetch from KOPPs api, programmeCode:', programmeCode) + return + } // react NotFound + } log.info('Successfully fetched programme from KOPPs API, programmeCode:', programmeCode) @@ -63,7 +85,7 @@ async function fetchAndFillProgrammeDetails({ applicationStore, lang, programmeC credits, titleOtherLanguage, educationalLevel, - } = programme + } = programDetails applicationStore.setProgrammeName(programmeName) applicationStore.setLengthInStudyYears(lengthInStudyYears) if (storeId === 'appendix1' || storeId === 'pdfStore') { @@ -78,7 +100,7 @@ async function fetchAndFillProgrammeDetails({ applicationStore, lang, programmeC applicationStore.setEducationalLevel(educationalLevel) } // eslint-disable-next-line consistent-return - return { programmeName, ...programme } + return { programmeName, ...programDetails } } /** @@ -97,7 +119,6 @@ async function fetchAndFillStudyProgrammeVersion({ applicationStore, lang, progr if (statusCode !== 200) return { statusCode } // react NotFound if ( - storeId === 'appendix1' || storeId === 'eligibility' || storeId === 'extent' || storeId === 'implementation' || @@ -214,6 +235,85 @@ function _parseCurriculumsAndFillStore(applicationStore, curriculums) { }) } +function _parseCurriculumsAndFillStoreFromStructure(applicationStore, curriculums) { + curriculums.forEach(curriculum => { + if (curriculum.programmeSpecialization) { + // Specialization + const { programmeSpecialization, studyYears } = curriculum + const { programmeSpecializationCode: code, title } = programmeSpecialization + + applicationStore.addSpecialization({ + code, + title, + studyYears: studyYears.reduce((years, studyYear) => { + if (studyYear.courses.length) { + years.push(studyYear.yearNumber) + } + + studyYear.courses.forEach(course => { + const { + kod: courseCode, + benamning: name, + omfattning: { number: credits, formattedWithUnit: formattedCredits }, + utbildningstyp: { + level: { name: level }, + }, + Valvillkor: electiveCondition, + } = course + + applicationStore.addElectiveConditionCourse( + { + code: courseCode, + name, + credits, + formattedCredits, + formattedLevel: level, + }, + electiveCondition, + studyYear.yearNumber, + code + ) + }) + return years + }, []), + }) + } else { + // Common + const { studyYears } = curriculum + studyYears.forEach(studyYear => { + if (studyYear.courses.length) { + applicationStore.addStudyYear(studyYear.yearNumber) + } + + studyYear.courses.forEach(course => { + const { + kod: courseCode, + benamning: name, + omfattning: { number: credits, formattedWithUnit: formattedCredits }, + utbildningstyp: { + level: { name: level }, + }, + Valvillkor: electiveCondition, + } = course + + applicationStore.addElectiveConditionCourse( + { + code: courseCode, + name, + credits, + formattedCredits, + formattedLevel: level, + }, + electiveCondition, + studyYear.yearNumber, + 'Common' + ) + }) + }) + } + }) +} + /** * Appendix 2 * @@ -243,15 +343,24 @@ function _parseSpecializations(curriculums) { * @param {string} options.programmeCode * @param {string} options.term */ -async function fetchAndFillCurriculumList(options) { +async function fetchAndFillCurriculumList(options, tillfalleUid) { const { applicationStore, lang } = options - const { studyProgrammeId } = await fetchAndFillStudyProgrammeVersion({ ...options, storeId: 'appendix1' }) - if (!studyProgrammeId) return - const { curriculums, statusCode: secondStatusCode } = await koppsApi.listCurriculums(studyProgrammeId, lang) - applicationStore.setStatusCode(secondStatusCode) - if (secondStatusCode !== 200) return // react NotFound - _parseCurriculumsAndFillStore(applicationStore, curriculums) + let curriculumData + + if (tillfalleUid) { + curriculumData = await getProgramStructure(tillfalleUid, lang) + } else { + const { studyProgrammeId } = await fetchAndFillStudyProgrammeVersion({ ...options }) // we are not using the programmeStudy data here so I removed the option of saving it to store + if (!studyProgrammeId) return + const { curriculums, statusCode: secondStatusCode } = await koppsApi.listCurriculums(studyProgrammeId, lang) + curriculumData = curriculums + applicationStore.setStatusCode(secondStatusCode) + if (secondStatusCode !== 200) return // react NotFound + } + + if (tillfalleUid) _parseCurriculumsAndFillStoreFromStructure(applicationStore, curriculumData) + else _parseCurriculumsAndFillStore(applicationStore, curriculumData) return } @@ -263,15 +372,21 @@ async function fetchAndFillCurriculumList(options) { * @param {string} options.programmeCode * @param {string} options.term */ -async function fetchAndFillSpecializations(options) { +async function fetchAndFillSpecializations(options, tillfalleUid) { const { applicationStore, lang } = options - const { studyProgrammeId } = await fetchAndFillStudyProgrammeVersion({ ...options, storeId: 'appendix2' }) - if (!studyProgrammeId) return - const { curriculums, statusCode: secondStatusCode } = await koppsApi.listCurriculums(studyProgrammeId, lang) - applicationStore.setStatusCode(secondStatusCode) - if (secondStatusCode !== 200) return // react NotFound + let curriculumData + if (!tillfalleUid) { + const { studyProgrammeId } = await fetchAndFillStudyProgrammeVersion({ ...options, storeId: 'appendix2' }) + if (!studyProgrammeId) return + const { curriculums, statusCode: secondStatusCode } = await koppsApi.listCurriculums(studyProgrammeId, lang) + curriculumData = curriculums + applicationStore.setStatusCode(secondStatusCode) + if (secondStatusCode !== 200) return // react NotFound + } else { + curriculumData = await getProgramStructure(tillfalleUid, lang) + } - const specializations = _parseSpecializations(curriculums) + const specializations = _parseSpecializations(curriculumData) applicationStore.setSpecializations(specializations) return }