diff --git a/.env.in b/.env.in index f89541c..6ba352a 100644 --- a/.env.in +++ b/.env.in @@ -1,4 +1,8 @@ KURS_PM_DATA_API_KEY=[Available in Azure KeyVault] KURSPLAN_API_KEY=[Available in Azure KeyVault] -REDIS_URI=[Available in Azure KeyVault] \ No newline at end of file +REDIS_URI=[Available in Azure KeyVault] + +# Ladok Mellanlager connection +LADOK_AUTH_CLIENT_SECRET=[Available in Azure KeyVault] +LADOK_OCP_APIM_SUBSCRIPTION_KEY=[Available in Azure KeyVault] \ No newline at end of file diff --git a/config/serverSettings.js b/config/serverSettings.js index ec6078a..9ca867f 100644 --- a/config/serverSettings.js +++ b/config/serverSettings.js @@ -49,6 +49,19 @@ module.exports = { kursplanApi: unpackNodeApiConfig('KURSPLAN_API_URI', devKursplanApi), }, + // TODO(Ladok-POC): Replace devDefaults and add values to ref/prod.parameters.json when final mellanlager is deployed + ladokMellanlagerApi: { + clientId: getEnv('LADOK_AUTH_CLIENT_ID', devDefaults('fc1c0e6e-c17b-4b1c-9e34-003ca528740f')), + clientSecret: getEnv('LADOK_AUTH_CLIENT_SECRET', null), + tokenUrl: getEnv( + 'LADOK_AUTH_TOKEN_URL', + devDefaults('https://login.microsoftonline.com/acd7f330-d613-48d9-85f2-258b1ac4a015/oauth2/v2.0/token') + ), + scope: getEnv('LADOK_AUTH_SCOPE', devDefaults('api://97fe6696-a7b2-4f7f-adce-726426e35c1c/.default')), + baseUrl: getEnv('LADOK_BASE_URL', devDefaults('https://apim-mellanlagring2.azure-api.net')), + ocpApimSupscriptionKey: getEnv('LADOK_OCP_APIM_SUBSCRIPTION_KEY', null), + }, + // koppsApi: unpackKOPPSConfig('KOPPS_URI', devKoppsApi), koppsApi: unpackNodeApiConfig('KOPPS_URI', devKoppsApi), diff --git a/package-lock.json b/package-lock.json index 11cb23b..0165a7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "kth-node-i18n": "^1.0.18", "kth-node-redis": "^3.3.0", "kth-style": "^10.3.0", + "om-kursen-ladok-client": "file:../studadm-om-kursen-packages/packages/om-kursen-ladok-client", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.12.0", @@ -82,6 +83,13 @@ "node": "18" } }, + "../studadm-om-kursen-packages/packages/om-kursen-ladok-client": { + "version": "0.0.1", + "dependencies": { + "ladok-attributvarde-utils": "file:../ladok-attributvarde-utils", + "ladok-mellanlager-client": "file:../ladok-mellanlager-client" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -13221,6 +13229,10 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/om-kursen-ladok-client": { + "resolved": "../studadm-om-kursen-packages/packages/om-kursen-ladok-client", + "link": true + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", diff --git a/package.json b/package.json index ed8d39a..a5687a8 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "kth-node-i18n": "^1.0.18", "kth-node-redis": "^3.3.0", "kth-style": "^10.3.0", + "om-kursen-ladok-client": "file:../studadm-om-kursen-packages/packages/om-kursen-ladok-client", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.12.0", diff --git a/public/js/app/pages/AboutCourseMemo.jsx b/public/js/app/pages/AboutCourseMemo.jsx index 7b76ed3..89d10d8 100644 --- a/public/js/app/pages/AboutCourseMemo.jsx +++ b/public/js/app/pages/AboutCourseMemo.jsx @@ -217,18 +217,11 @@ function AboutCourseMemo({ mockKursPmDataApi = false, mockMixKoppsApi = false }) const [webContext] = useWebContext() - const { - allTypeMemos, - memoDatas, - courseCode, - language: userLangAbbr, - userLanguageIndex, - allRoundsFromKopps, - } = webContext + const { allTypeMemos, memoDatas, courseCode, language: userLangAbbr, userLanguageIndex, allRoundInfos } = webContext const isThisTest = !!mockKursPmDataApi - const [allRounds, setAllRounds] = useState(allRoundsFromKopps) + const [allRounds, setAllRounds] = useState(allRoundInfos) const webAndPdfMiniMemos = isThisTest ? mockKursPmDataApi : allTypeMemos const allRoundsMockOrReal = isThisTest ? mockMixKoppsApi : allRounds diff --git a/server/controllers/helperFunctions.js b/server/controllers/helperFunctions.js new file mode 100644 index 0000000..5e9a815 --- /dev/null +++ b/server/controllers/helperFunctions.js @@ -0,0 +1,23 @@ +const createRoundInfos = ladokRounds => { + return ladokRounds.map(ladokRound => { + const { forstaUndervisningsdatum, sistaUndervisningsdatum, utbildningstillfalleskod, kortnamn, startperiod } = + ladokRound + return { + round: { + firstTuitionDate: forstaUndervisningsdatum.date, + lastTuitionDate: sistaUndervisningsdatum.date, + startWeek: forstaUndervisningsdatum, + endWeek: sistaUndervisningsdatum, + applicationCode: utbildningstillfalleskod, + shortName: kortnamn, + startTerm: { + term: startperiod.inDigits, + }, + }, + } + }) +} + +module.exports = { + createRoundInfos, +} diff --git a/server/controllers/memoCtrl.js b/server/controllers/memoCtrl.js index 49c2072..e8e847c 100644 --- a/server/controllers/memoCtrl.js +++ b/server/controllers/memoCtrl.js @@ -11,6 +11,7 @@ const serverPaths = require('../server').getPaths() const { browser, server: serverConfig } = require('../configuration') const { getMemoDataById, getMemoVersion, getMiniMemosPdfAndWeb } = require('../kursPmDataApi') const { getDetailedInformation, getCourseRoundTerms } = require('../koppsApi') +const { getLadokCourseData, getActiveCourseRoundsByCourseCodeAndFromTerm } = require('../ladokApi') const { createBreadcrumbs } = require('../utils/breadcrumbUtil') const { getServerSideFunctions } = require('../utils/serverSideRendering') const { createServerSideContext } = require('../ssr-context/createServerSideContext') @@ -21,6 +22,7 @@ const { } = require('./memoCtrlHelpers') const { getLastYearsTerm, extractTerm } = require('../utils/term') const { redirectToAboutCourseConfig } = require('../utils/helpers') +const { createRoundInfos } = require('./helperFunctions') const locales = { sv, en } @@ -200,19 +202,17 @@ async function getContent(req, res, next) { let fromTerm = semester ?? extractTerm(courseCode, finalMemoEndPoint) - const { - courseMainSubjects, - recruitmentText, - title, - credits, - creditUnitAbbr, - infoContactName, - examiners, - roundInfos, - } = await getDetailedInformation(courseCode, languagesContext.memoLanguage, fromTerm) + const { title, credits, creditUnitAbbr } = await getLadokCourseData(courseCode, responseLanguage) + const ladokRounds = await getActiveCourseRoundsByCourseCodeAndFromTerm(courseCode, fromTerm, responseLanguage) + const { infoContactName, examiners } = await getDetailedInformation( + courseCode, + languagesContext.memoLanguage, + fromTerm + ) + + const roundInfos = createRoundInfos(ladokRounds) const courseContext = { - courseMainSubjects, title, credits, creditUnitAbbr, @@ -330,11 +330,10 @@ async function getOldContent(req, res, next) { memoDatas: [], } - const { courseMainSubjects, recruitmentText, title, credits, creditUnitAbbr, infoContactName, examiners } = - await getDetailedInformation(courseCode, languagesContext.memoLanguage) + const { title, credits, creditUnitAbbr } = await getLadokCourseData(courseCode) + const { infoContactName, examiners } = await getDetailedInformation(courseCode, languagesContext.memoLanguage) const courseContext = { - courseMainSubjects, title, credits, creditUnitAbbr, @@ -416,11 +415,12 @@ async function getAboutContent(req, res, next) { const fromTerm = getLastYearsTerm() - const { title, credits, creditUnitAbbr, infoContactName, examiners, roundInfos } = await getDetailedInformation( - courseCode, - responseLanguage, - fromTerm - ) + const { title, credits, creditUnitAbbr } = await getLadokCourseData(courseCode, responseLanguage) + const ladokRounds = await getActiveCourseRoundsByCourseCodeAndFromTerm(courseCode, fromTerm, responseLanguage) + const { infoContactName, examiners } = await getDetailedInformation(courseCode, responseLanguage, fromTerm) + + const roundInfos = createRoundInfos(ladokRounds) + webContext.title = title webContext.credits = credits webContext.creditUnitAbbr = creditUnitAbbr @@ -429,7 +429,7 @@ async function getAboutContent(req, res, next) { webContext.memoDatas = enrichMemoDatasWithOutdatedFlag(rawMemos, roundInfos) webContext.allTypeMemos = await getMiniMemosPdfAndWeb(courseCode) - webContext.allRoundsFromKopps = await _getAllRoundsWithApplicationCodes(roundInfos) + webContext.allRoundInfos = await _getAllRoundsWithApplicationCodes(roundInfos) // TODO: Proper language constant const shortDescription = (responseLanguage === 'sv' ? 'Om kursen ' : 'About course ') + courseCode @@ -532,12 +532,11 @@ async function _getAllRoundsWithApplicationCodes(roundInfos) { const allRounds = [] roundInfos.map(roundInfo => { const { round } = roundInfo - const { firstTuitionDate, lastTuitionDate, startTerm, applicationCodes } = round + const { firstTuitionDate, lastTuitionDate, startTerm, applicationCode } = round const { term } = startTerm if (isDateWithInCurrentOrFutureSemester(firstTuitionDate, lastTuitionDate)) { round.term = term - const { applicationCode = '' } = applicationCodes[0] - round.applicationCode = applicationCode + round.applicationCode = applicationCode || '' delete round.applicationCodes allRounds.push(round) } diff --git a/server/controllers/memoCtrlHelpers.js b/server/controllers/memoCtrlHelpers.js index 5727954..6eed51f 100644 --- a/server/controllers/memoCtrlHelpers.js +++ b/server/controllers/memoCtrlHelpers.js @@ -125,6 +125,7 @@ async function findMatchingRound(memo, roundInfos) { function lookForMatchingRoundInRoundInfos(semester, memoApplicationCodes, roundInfos) { const matchingRoundInfo = roundInfos.find(({ round: { applicationCodes } }) => { + if (!applicationCodes) return false const hasMatchingTermAndApplicationCode = applicationCodes.some(({ applicationCode, term }) => { return term === semester && memoApplicationCodes.includes(applicationCode) }) diff --git a/server/koppsApi.js b/server/koppsApi.js index c84d999..acb7f46 100644 --- a/server/koppsApi.js +++ b/server/koppsApi.js @@ -55,21 +55,10 @@ async function getDetailedInformation(courseCode, language, fromTerm) { try { const res = await client.getAsync({ uri, useCache: true }) if (res.body) { - const { mainSubjects, course, examiners, roundInfos } = res.body - const isCreditNotStandard = - course && - course.credits && - course.credits.toString().indexOf('.') < 0 && - course.credits.toString().indexOf(',') < 0 + const { examiners } = res.body return { - courseMainSubjects: mainSubjects && mainSubjects.length > 0 ? mainSubjects.join(', ') : '', - recruitmentText: course && course.recruitmentText ? course.recruitmentText : '', - title: course && course.title ? course.title : '', - credits: isCreditNotStandard ? course.credits + '.0' : course.credits || '', - creditUnitAbbr: course && course.creditUnitAbbr ? course.creditUnitAbbr : '', - infoContactName: course && course.infoContactName ? course.infoContactName : '', + infoContactName: '', examiners: createPersonHtml(examiners), - roundInfos: roundInfos || [], } } @@ -83,8 +72,6 @@ async function getDetailedInformation(courseCode, language, fromTerm) { language ) return { - courseMainSubjects: '', - recruitmentText: '', title: '', credits: '', creditUnitAbbr: '', @@ -94,8 +81,6 @@ async function getDetailedInformation(courseCode, language, fromTerm) { } catch (err) { log.error('Kopps is not available', err) return { - courseMainSubjects: '', - recruitmentText: '', title: '', credits: '', creditUnitAbbr: '', diff --git a/server/ladokApi.js b/server/ladokApi.js new file mode 100644 index 0000000..59a87fe --- /dev/null +++ b/server/ladokApi.js @@ -0,0 +1,36 @@ +'use strict' + +const { createApiClient } = require('om-kursen-ladok-client') +const serverConfig = require('./configuration').server + +async function getLadokCourseData(courseCode, lang) { + const client = createApiClient(serverConfig.ladokMellanlagerApi) + const course = await client.getLatestCourseVersion(courseCode, lang) + const { + benamning: ladokCourseTitle, + omfattning: ladokCourseCredits, + utbildningstyp: { creditsUnitCode: ladokCreditUnitAbbr }, + } = course + + const isCreditNotStandard = + ladokCourseCredits && + ladokCourseCredits.toString().indexOf('.') < 0 && + ladokCourseCredits.toString().indexOf(',') < 0 + + return { + title: ladokCourseTitle ?? '', + credits: isCreditNotStandard ? ladokCourseCredits + '.0' : ladokCourseCredits || '', + creditUnitAbbr: ladokCreditUnitAbbr ?? '', + } +} + +async function getActiveCourseRoundsByCourseCodeAndFromTerm(courseCode, fromTerm, lang) { + const client = createApiClient(serverConfig.ladokMellanlagerApi) + const rounds = await client.getActiveCourseRoundsFromTerm(courseCode, fromTerm, lang) + return rounds +} + +module.exports = { + getLadokCourseData, + getActiveCourseRoundsByCourseCodeAndFromTerm, +} diff --git a/server/ssr-context/createServerSideContext.js b/server/ssr-context/createServerSideContext.js index cf95bea..310f390 100644 --- a/server/ssr-context/createServerSideContext.js +++ b/server/ssr-context/createServerSideContext.js @@ -15,7 +15,6 @@ function createServerSideContext() { const context = { browserConfig: {}, courseCode: '', - courseMainSubjects: '', language: 'sv', memoData: {}, memoDatas: [], diff --git a/test/mock-api/responses.js b/test/mock-api/responses.js index faae36f..872e52e 100644 --- a/test/mock-api/responses.js +++ b/test/mock-api/responses.js @@ -82,8 +82,6 @@ module.exports = { titleOther: 'Algebra and Geometry', cancelled: false, deactivated: false, - recruitmentText: - '

Algebra och geometri är en grundläggande kurs i linjär algebra med vektorgeometri. Ett centralt begrepp i kursen är linjäritet som ligger till grund för stora delar av användningen av matematik inom såväl naturvetenskap som inom ingenjörstillämpningar.

', credits: 7.5, creditUnitLabel: 'Högskolepoäng', creditUnitAbbr: 'hp', @@ -1118,7 +1116,6 @@ module.exports = { }, }, ], - mainSubjects: ['Matematik', 'Teknik'], examinationSets: { 20072: { startingTerm: {