diff --git a/i18n/messages.en.js b/i18n/messages.en.js index 2b1ea27a..17ef8489 100644 --- a/i18n/messages.en.js +++ b/i18n/messages.en.js @@ -104,7 +104,7 @@ module.exports = { 'Course offering may be cancelled if number of admitted are less than minimum of places. If there are more applicants than number of places selection will be made.', round_seats_info: 'The selection results are based on:', syllabus_info: - '

• A course goes different course offerings. To see information about a specific course offering, choose semester and course offering. The course syllabus information will be updated depending on the chosen semester. Information from the course syllabus is marked with *.

• Please note: regulations in course syllabus are rules that are generally applicable and binding for both employees and students.

• If you have not chosen semester and course offering, you will see course information from the current or future course syllabus. The valid period of the course syllabus is stated on the page.

', + '

A course goes different course offerings. To see information about a specific course offering, choose semester and course offering. The course syllabus information will be updated depending on the chosen semester. Information from the course syllabus is marked with *

', sideMenu: { aria_label: 'Sub menu', page_about_course: 'About course', diff --git a/i18n/messages.se.js b/i18n/messages.se.js index 2129681f..2da99936 100644 --- a/i18n/messages.se.js +++ b/i18n/messages.se.js @@ -106,7 +106,7 @@ module.exports = { 'Kursomgången kan komma att ställas in om antalet antagna understiger minimiantalet platser. Vid fler sökande än platser kommer urval att ske.', round_seats_info: 'Urvalet sker baserat på:', syllabus_info: - '

• En kurs undervisas i olika kursomgångar. För att se information om en specifik kursomgång behöver du välja termin och kursomgång. Information från kursplan kommer att uppdateras beroende på vald termin. Information från kursplan är markerad med *.

• Observera: bestämmelser i kursplaner är regler som är generellt tillämpbara och bindande för såväl anställda som studenter.

• Har du inte valt termin och kursomgång ser du kursinformation från nuvarande eller kommande kursplan. På sidan anges den period som information från kursplan gäller för.

', + '

En kurs undervisas i olika kursomgångar. För att se information om en specifik kursomgång behöver du välja termin och kursomgång. Information från kursplan kommer att uppdateras beroende på vald termin. Information från kursplan är markerad med *

', sideMenu: { aria_label: 'Undermeny', page_about_course: 'Om kursen ', diff --git a/package-lock.json b/package-lock.json index 4f745037..0cc1ebb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@kth/monitor": "^4.3.1", "@kth/server": "^4.1.0", "@kth/session": "^3.0.9", - "@kth/style": "^0.18.12", + "@kth/style": "^1.4.2", "@kth/ug-rest-api-helper": "^1.0.26", "axios": "^1.7.2", "body-parser": "^1.20.2", @@ -3539,9 +3539,9 @@ } }, "node_modules/@kth/style": { - "version": "0.18.12", - "resolved": "https://registry.npmjs.org/@kth/style/-/style-0.18.12.tgz", - "integrity": "sha512-QtRdFEJcpHNgtUdqrpCn9EVXOQw9Iu6tYLsFO0qgCfgbWWMzSq8d52NoV6rJ6JSNeqhA8tnwdXHprhtLTstA/w==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@kth/style/-/style-1.4.2.tgz", + "integrity": "sha512-Z6EhjUVzhw+QITpnFsdLu51xogsgEXQiB1E+hEI2HsaFxTOrxkq6qbnN/uK1WWeTuKfi+A6szaVubPXswWRtyw==", "peerDependencies": { "react": "*" } @@ -4339,17 +4339,6 @@ "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -6007,9 +5996,9 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -6020,7 +6009,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -7950,18 +7939,18 @@ } }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/enhanced-resolve": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", - "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "license": "MIT", "dependencies": { @@ -9032,37 +9021,37 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -9347,13 +9336,13 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -14036,10 +14025,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "license": "MIT" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -14068,9 +14060,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -14993,9 +14985,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "license": "MIT" }, "node_modules/path-type": { @@ -15672,12 +15664,12 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -16562,9 +16554,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -16600,6 +16592,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -16617,15 +16618,15 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -18628,13 +18629,12 @@ } }, "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "dev": true, "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -18643,7 +18643,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", diff --git a/package.json b/package.json index 9c41771e..07a4a1c8 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@kth/monitor": "^4.3.1", "@kth/server": "^4.1.0", "@kth/session": "^3.0.9", - "@kth/style": "^0.18.12", + "@kth/style": "^1.4.2", "@kth/ug-rest-api-helper": "^1.0.26", "axios": "^1.7.2", "body-parser": "^1.20.2", diff --git a/public/css/_shared.scss b/public/css/_shared.scss index e3581ddc..2bea5ad9 100644 --- a/public/css/_shared.scss +++ b/public/css/_shared.scss @@ -16,6 +16,15 @@ } } } + .kth-local-navigation--mobile { + padding: 0; + } + label:has(+ .form-group), + label:has(+ input) { + @include typography.font-heading-xs; + display: block; + margin-bottom: 0.25rem; + } } #mainContent { @@ -159,10 +168,3 @@ p { .select-wrapper::after { background-color: var(--color-primary); } - -label:has(+ .form-group), -label:has(+ input) { - @include typography.font-heading-xs; - display: block; - margin-bottom: 0.25rem; -} diff --git a/public/css/kursinfo-web.scss b/public/css/kursinfo-web.scss index 224214d3..8a4aefd8 100644 --- a/public/css/kursinfo-web.scss +++ b/public/css/kursinfo-web.scss @@ -110,9 +110,18 @@ } .roundInformation { + position: relative; padding: 20px; margin-bottom: 30px; background-color: var(--color-background-alt); + .person { + min-height: 31px; + img { + max-width: 40px; + overflow: hidden; + white-space: nowrap; + } + } h3 { margin-block-end: 1rem; @@ -187,7 +196,41 @@ background-color: var(--color-background); } } +.shimmer-effect::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(-45deg, var(--color-background-alt) 40%, #fafafa 50%, var(--color-background-alt) 60%); + background-size: 300%; + background-position-x: 100%; + animation: shimmer 1s infinite linear; + z-index: 10; + pointer-events: none; +} +.fadeIn > * { + animation: fadeIn 1.5s ease forwards; +} +@keyframes shimmer { + to { + background-position-x: 0%; + } +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + 33.33% { + opacity: 0; + } + 100% { + opacity: 1; + } +} .course-section-list { @include prose.prose; diff --git a/public/js/app/components/CourseSectionList.jsx b/public/js/app/components/CourseSectionList.jsx index 6a8c9db7..04cd51f9 100644 --- a/public/js/app/components/CourseSectionList.jsx +++ b/public/js/app/components/CourseSectionList.jsx @@ -63,7 +63,7 @@ function CourseSectionList({ courseInfo = {}, partToShow, syllabus = {}, syllabu ...eligibility, { header: translation.courseInformation.course_prerequisites, - text: courseInfo.course_prerequisites, + text: courseInfo.course_recommended_prerequisites, infoModal: { description: translation.courseInformation.course_prerequisites_description, closeLabel: translation.courseLabels.label_close, diff --git a/public/js/app/components/RoundInformation/PlannedModules.jsx b/public/js/app/components/RoundInformation/PlannedModules.jsx deleted file mode 100644 index 07190655..00000000 --- a/public/js/app/components/RoundInformation/PlannedModules.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import { usePlannedModules } from '../../hooks/usePlannedModules' - -export const PlannedModules = ({ courseCode, courseRound, selectedSemester }) => { - const { plannedModules } = usePlannedModules({ - courseCode, - semester: selectedSemester, - applicationCode: courseRound.round_application_code, - }) - return -} diff --git a/public/js/app/components/RoundInformation/RoundInformation.jsx b/public/js/app/components/RoundInformation/RoundInformation.jsx index 75addb3c..638f7d4c 100644 --- a/public/js/app/components/RoundInformation/RoundInformation.jsx +++ b/public/js/app/components/RoundInformation/RoundInformation.jsx @@ -1,9 +1,11 @@ -import React from 'react' +import React, { useEffect, useMemo, useState } from 'react' import Alert from '../../components-shared/Alert' import BankIdAlert from '../../components/BankIdAlert' import { useLanguage } from '../../hooks/useLanguage' import { useRoundUtils } from '../../hooks/useRoundUtils' +import { useCourseEmployees } from '../../hooks/useCourseEmployees' +import { usePlannedModules } from '../../hooks/usePlannedModules' import { RoundInformationInfoGrid } from './RoundInformationInfoGrid' import { RoundInformationContacts } from './RoundInformationContacts' @@ -14,13 +16,57 @@ function RoundInformation({ courseCode, courseData, courseRound, semesterRoundSt const selectedRoundHeader = createRoundHeader(courseRound) const { selectedSemester } = semesterRoundState + const memoizedCourseRound = useMemo(() => courseRound, [courseRound]) + + const memoizedParams = useMemo( + () => ({ + courseCode, + selectedSemester, + applicationCode: memoizedCourseRound?.round_application_code, + }), + [courseCode, selectedSemester, memoizedCourseRound?.round_application_code] + ) + + const { + courseRoundEmployees, + isError: courseEmployeesError, + isLoading: courseEmployeesLoading, + } = useCourseEmployees(memoizedParams) + + const { + plannedModules, + isError: plannedModulesError, + isLoading: plannedModulesIsLoading, + } = usePlannedModules(memoizedParams) + + const isLoading = courseEmployeesLoading || plannedModulesIsLoading + const isError = courseEmployeesError || plannedModulesError + + const [isLoaderVisible, setIsLoaderVisible] = useState(false) + + useEffect(() => { + let timer + if (isLoading) { + setIsLoaderVisible(true) + } else { + timer = setTimeout(() => { + setIsLoaderVisible(false) + }, 300) + } + return () => clearTimeout(timer) + }, [isLoading]) + return ( -
+

{translation.courseRoundInformation.round_header} {selectedRoundHeader}

- + @@ -29,12 +75,7 @@ function RoundInformation({ courseCode, courseData, courseRound, semesterRoundSt )}

{translation.courseLabels.header_contact}

- +
) } diff --git a/public/js/app/components/RoundInformation/RoundInformationContacts.jsx b/public/js/app/components/RoundInformation/RoundInformationContacts.jsx index a80bfdde..2c9a100e 100644 --- a/public/js/app/components/RoundInformation/RoundInformationContacts.jsx +++ b/public/js/app/components/RoundInformation/RoundInformationContacts.jsx @@ -1,16 +1,10 @@ import React from 'react' import { useLanguage } from '../../hooks/useLanguage' import { useMissingInfo } from '../../hooks/useMissingInfo' -import { useCourseEmployees } from '../../hooks/useCourseEmployees' -function RoundInformationContacts({ courseCode, courseData, courseRound, selectedSemester }) { +function RoundInformationContacts({ courseData, courseRoundEmployees }) { const { translation } = useLanguage() const { missingInfoLabel } = useMissingInfo() - const { courseRoundEmployees } = useCourseEmployees({ - courseCode, - selectedSemester, - applicationCode: courseRound?.round_application_code, - }) return (
diff --git a/public/js/app/components/RoundInformation/RoundInformationInfoGrid.jsx b/public/js/app/components/RoundInformation/RoundInformationInfoGrid.jsx index b14f5104..cdd2cb98 100644 --- a/public/js/app/components/RoundInformation/RoundInformationInfoGrid.jsx +++ b/public/js/app/components/RoundInformation/RoundInformationInfoGrid.jsx @@ -3,7 +3,6 @@ import { useLanguage } from '../../hooks/useLanguage' import InfoModal from '../InfoModal' import { CourseMemoLink } from './CourseMemoLink' import { CourseScheduleLink } from './CourseScheduleLink' -import { PlannedModules } from './PlannedModules' // Calculates if a "Show more" button should be displayed, and creates props for content and button elements. const useShowMoreContent = content => { @@ -70,7 +69,7 @@ const Item = ({ children, html, title, infoModalContent }) => { ) } -function RoundInformationInfoGrid({ courseCode, courseRound, selectedSemester }) { +function RoundInformationInfoGrid({ courseCode, courseRound, plannedModules }) { const { translation } = useLanguage() return ( @@ -117,9 +116,7 @@ function RoundInformationInfoGrid({ courseCode, courseRound, selectedSemester })

{courseRound.round_seats || translation.courseRoundInformation.round_no_seats_limit}

- - - + diff --git a/public/js/app/components/__tests__/RoundInformation.test.js b/public/js/app/components/__tests__/RoundInformation.test.js index 047838c2..1cc2adeb 100644 --- a/public/js/app/components/__tests__/RoundInformation.test.js +++ b/public/js/app/components/__tests__/RoundInformation.test.js @@ -25,7 +25,7 @@ const defaultSemesterRoundState = { } describe('Component ', () => { - beforeAll(() => { + beforeEach(() => { usePlannedModules.mockReturnValue({ plannedModules: 'somePlannedModules', }) @@ -72,7 +72,7 @@ describe('Component ', () => { courseRound: mockCourseRound, } - useCourseEmployees.mockReturnValueOnce({ + useCourseEmployees.mockReturnValue({ courseRoundEmployees: { examiners: `${examinersData}'`, responsibles: `${responsiblesData}`, diff --git a/public/js/app/components/__tests__/RoundInformationContacts.test.js b/public/js/app/components/__tests__/RoundInformationContacts.test.js index 201aea16..4871bcfe 100644 --- a/public/js/app/components/__tests__/RoundInformationContacts.test.js +++ b/public/js/app/components/__tests__/RoundInformationContacts.test.js @@ -22,14 +22,7 @@ const withinNextSibling = element => within(nextSibling(element)) describe('Component ', () => { describe('examiners, responsibles and teachers', () => { test('shoud show headers with "missing info" text when data is missing', () => { - render( - - ) + render() const examinerLabel = screen.getByText('Examiner') expect(nextSibling(examinerLabel)).toHaveTextContent('No information inserted') @@ -41,19 +34,14 @@ describe('Component ', () => { }) test('shoud show headers with data inserted as html', () => { - useCourseEmployees.mockReturnValue({ - courseRoundEmployees: { - examiners: '

Test examiners

', - responsibles: '

Test responsibles

', - teachers: '

Test teachers

', - }, - }) render( Test examiners

', + responsibles: '

Test responsibles

', + teachers: '

Test teachers

', + }} /> ) const examinerLabel = screen.getByText('Examiner') @@ -71,58 +59,12 @@ describe('Component ', () => { expect(teacherLink).toHaveTextContent('Test teachers') expect(teacherLink).toHaveAttribute('href', '/profile/testteachers/') }) - - test('should update data when prop changes', () => { - useCourseEmployees.mockImplementation(({ applicationCode }) => - applicationCode === '1111' - ? { courseRoundEmployees: { examiners: 'Examiner for 1111' } } - : applicationCode === '3333' - ? { courseRoundEmployees: { examiners: 'Examiner for 3333' } } - : { courseRoundEmployees: { examiners: '' } } - ) - - const { rerender } = render( - - ) - - expect(nextSibling(screen.getByText('Examiner'))).toHaveTextContent('Examiner for 1111') - - rerender( - - ) - expect(nextSibling(screen.getByText('Examiner'))).toHaveTextContent('No information inserted') - - rerender( - - ) - expect(nextSibling(screen.getByText('Examiner'))).toHaveTextContent('Examiner for 3333') - }) }) describe('cource contact', () => { test('should show header and content for course contact name', () => { render( - + ) const contactLabel = screen.getByText('Contact') expect(contactLabel).toBeInTheDocument() @@ -131,12 +73,7 @@ describe('Component ', () => { "shoud NOT show header if contact name is '%s'", contactNameArg => { render( - + ) const contactLabel = screen.queryByText('Contact') expect(contactLabel).not.toBeInTheDocument() diff --git a/public/js/app/hooks/__tests__/useApi.test.js b/public/js/app/hooks/__tests__/useApi.test.js index 13f6cfcd..04d8e07a 100644 --- a/public/js/app/hooks/__tests__/useApi.test.js +++ b/public/js/app/hooks/__tests__/useApi.test.js @@ -39,9 +39,10 @@ describe('useApi', () => { }) test('calls apiToCall with given parameters', async () => { - const { result } = renderHook(() => useApi(apiToCall, { one: 'one', two: 2 }, null, null)) + const apiParams1 = { one: 'one', two: 2 } + const { result } = renderHook(() => useApi(apiToCall, apiParams1, null, null)) - expect(apiToCall).toHaveBeenLastCalledWith({ one: 'one', two: 2 }) + expect(apiToCall).toHaveBeenLastCalledWith(apiParams1) await waitFor(() => expect(result.current.data).toStrictEqual('somePlannedModules')) @@ -50,15 +51,18 @@ describe('useApi', () => { data: 'someData', }) - const { result: result2 } = renderHook(() => useApi(anotherApiToCall, { two: 'two', three: 3 }, null, null)) + const apiParams2 = { two: 'two', three: 3 } - expect(anotherApiToCall).toHaveBeenLastCalledWith({ two: 'two', three: 3 }) + const { result: result2 } = renderHook(() => useApi(anotherApiToCall, apiParams2, null, null)) + + expect(anotherApiToCall).toHaveBeenLastCalledWith(apiParams2) await waitFor(() => expect(result2.current.data).toStrictEqual('someData')) }) test('returns data from apiToCall', async () => { - const { result } = renderHook(() => useApi(apiToCall, {}, null, null)) + const params = {} + const { result } = renderHook(() => useApi(apiToCall, params, null, null)) await waitFor(() => expect(result.current.data).toStrictEqual('somePlannedModules')) }) @@ -90,28 +94,28 @@ describe('useApi', () => { }) ) - const { result } = renderHook(() => useApi(apiToCall, defaultParams, defaultValue, null)) + const { result: firstResult } = renderHook(() => useApi(apiToCall, defaultParams, defaultValue, null)) - expect(result.current.data).toStrictEqual(defaultValue) - expect(result.current.isError).toStrictEqual(false) + expect(firstResult.current.data).toStrictEqual(defaultValue) + expect(firstResult.current.isError).toStrictEqual(false) jest.advanceTimersByTime(100) expect(apiToCall).toHaveBeenCalledTimes(1) - await waitFor(() => expect(result.current.data).toStrictEqual('someNewPlannedModules')) - await waitFor(() => expect(result.current.isError).toStrictEqual(false)) + await waitFor(() => expect(firstResult.current.data).toStrictEqual('someNewPlannedModules')) + await waitFor(() => expect(firstResult.current.isError).toStrictEqual(false)) - result.current.setApiParams({ other: 'params' }) + const { result: secodResult } = renderHook(() => useApi(apiToCall, defaultParams, defaultValue, null)) - await waitFor(() => expect(result.current.data).toStrictEqual(defaultValue)) - await waitFor(() => expect(result.current.isError).toStrictEqual(false)) + await waitFor(() => expect(secodResult.current.data).toStrictEqual(defaultValue)) + await waitFor(() => expect(secodResult.current.isError).toStrictEqual(false)) jest.advanceTimersByTime(100) expect(apiToCall).toHaveBeenCalledTimes(2) - await waitFor(() => expect(result.current.data).toStrictEqual('someOtherPlannedModules')) - await waitFor(() => expect(result.current.isError).toStrictEqual(false)) + await waitFor(() => expect(secodResult.current.data).toStrictEqual('someOtherPlannedModules')) + await waitFor(() => expect(secodResult.current.isError).toStrictEqual(false)) jest.useRealTimers() }) @@ -186,18 +190,19 @@ describe('useApi', () => { }) ) - const { result } = renderHook(() => useApi(apiToCall, defaultParams, null, null)) + const { result: firstResult } = renderHook(() => useApi(apiToCall, defaultParams, null, null)) jest.advanceTimersByTime(100) - await waitFor(() => expect(result.current.isError).toStrictEqual(true)) + await waitFor(() => expect(firstResult.current.isError).toStrictEqual(true)) - result.current.setApiParams({ ...defaultParams, applicationCode: 54321 }) + const newParams = { ...defaultParams, applicationCode: 54321 } + const { result: secodResult } = renderHook(() => useApi(apiToCall, newParams, null, null)) - await waitFor(() => expect(result.current.isError).toStrictEqual(false)) + await waitFor(() => expect(secodResult.current.isError).toStrictEqual(false)) jest.advanceTimersByTime(100) - await waitFor(() => expect(result.current.isError).toStrictEqual(true)) + await waitFor(() => expect(secodResult.current.isError).toStrictEqual(true)) jest.useRealTimers() }) diff --git a/public/js/app/hooks/useApi.js b/public/js/app/hooks/useApi.js index f8b70421..2dd6d7c0 100644 --- a/public/js/app/hooks/useApi.js +++ b/public/js/app/hooks/useApi.js @@ -1,20 +1,26 @@ import { useEffect, useState } from 'react' import { STATUS } from './api/status' -export const useApi = (apiToCall, initialApiParams, defaultValue, defaulValueIfNullResponse) => { - const [apiParams, setApiParams] = useState(initialApiParams) +export const useApi = (apiToCall, apiParams, defaultValue, defaulValueIfNullResponse) => { const [data, setData] = useState(defaultValue) const [isError, setIsError] = useState(false) + const [isLoading, setIsLoading] = useState(true) useEffect(() => { const fetchData = async () => { + setIsLoading(true) setData(defaultValue) setIsError(false) - - const result = await apiToCall(apiParams) - - setData(result.data || defaulValueIfNullResponse) - setIsError(result.status === STATUS.ERROR) + try { + const result = await apiToCall(apiParams) + setData(result.data || defaulValueIfNullResponse) + setIsError(result.status === STATUS.ERROR) + } catch (error) { + setIsError(true) + setData(defaulValueIfNullResponse) + } finally { + setIsLoading(false) + } } fetchData() @@ -27,6 +33,6 @@ export const useApi = (apiToCall, initialApiParams, defaultValue, defaulValueIfN return { data, isError, - setApiParams, + isLoading, } } diff --git a/public/js/app/hooks/useCourseEmployees.js b/public/js/app/hooks/useCourseEmployees.js index 0b0a0df8..fb0186bf 100644 --- a/public/js/app/hooks/useCourseEmployees.js +++ b/public/js/app/hooks/useCourseEmployees.js @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { useMemo } from 'react' import { useWebContext } from '../context/WebContext' import { useApi } from './useApi' import { getCourseEmployees } from './api/getCourseEmployees' @@ -8,24 +8,21 @@ export const useCourseEmployees = ({ courseCode, selectedSemester, applicationCo const { uri } = context.paths.api.employees - const { data, isError, setApiParams } = useApi( - getCourseEmployees, - { + const requestData = useMemo( + () => ({ uri, courseCode, selectedSemester, applicationCode, - }, - {}, - {} + }), + [uri, courseCode, selectedSemester, applicationCode] ) - useEffect(() => { - setApiParams({ uri, courseCode, selectedSemester, applicationCode }) - }, [applicationCode, courseCode, selectedSemester, setApiParams, uri]) + const { data, isError, isLoading } = useApi(getCourseEmployees, requestData) return { courseRoundEmployees: data, isError, + isLoading, } } diff --git a/public/js/app/hooks/usePlannedModules.js b/public/js/app/hooks/usePlannedModules.js index 018dce15..5132ff96 100644 --- a/public/js/app/hooks/usePlannedModules.js +++ b/public/js/app/hooks/usePlannedModules.js @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { useMemo } from 'react' import { useWebContext } from '../context/WebContext' import { getPlannedModules } from './api/getPlannedModules' import { useApi } from './useApi' @@ -6,27 +6,29 @@ import { useMissingInfo } from './useMissingInfo' const MISSING_INFO = '' -export const usePlannedModules = ({ courseCode, semester, applicationCode }) => { +export const usePlannedModules = ({ courseCode, selectedSemester, applicationCode }) => { const context = useWebContext() const { missingInfoLabel } = useMissingInfo() const basePath = context.paths.api.plannedSchemaModules.uri - const { data, isError, setApiParams } = useApi( - getPlannedModules, - { basePath, courseCode, semester, applicationCode }, - null, - MISSING_INFO + const requestData = useMemo( + () => ({ + basePath, + courseCode, + semester: selectedSemester, + applicationCode, + }), + [basePath, courseCode, selectedSemester, applicationCode] ) - useEffect(() => { - setApiParams({ basePath, courseCode, semester, applicationCode }) - }, [applicationCode, basePath, courseCode, semester, setApiParams]) + const { data, isError, isLoading } = useApi(getPlannedModules, requestData, null, MISSING_INFO) const plannedModules = data === MISSING_INFO ? missingInfoLabel : data return { plannedModules, isError, + isLoading, } } diff --git a/server/apiCalls/getFilteredData.js b/server/apiCalls/getFilteredData.js index 1df3ab1a..b0c28b72 100644 --- a/server/apiCalls/getFilteredData.js +++ b/server/apiCalls/getFilteredData.js @@ -44,12 +44,9 @@ function _parseCourseDefaultInformation(courseDetails, language) { : INFORM_IF_IMPORTANT_INFO_IS_MISSING_ABOUT_MIN_FIELD_OF_STUDY[language], course_possibility_to_addition: parseOrSetEmpty(course.possibilityToAddition, language), course_possibility_to_completions: parseOrSetEmpty(course.possibilityToCompletion, language), - course_prerequisites: parseOrSetEmpty(course.prerequisites, language), course_recruitment_text: parseOrSetEmpty(course.recruitmentText, language, true), course_required_equipment: parseOrSetEmpty(course.requiredEquipment, language), course_suggested_addon_studies: parseOrSetEmpty(course.addOn, language), - course_supplemental_information_url: parseOrSetEmpty(course.supplementaryInfoUrl, language), - course_supplemental_information_url_text: parseOrSetEmpty(course.supplementaryInfoUrlName, language), course_state: parseOrSetEmpty(course.state, language, true), } } @@ -243,13 +240,15 @@ const getFilteredData = async ({ courseCode, language, memoList }) => { //* **** Course information that is static on the course side *****// const courseDefaultInformation = _parseCourseDefaultInformation(courseDetails, language) - const { sellingText, courseDisposition, supplementaryInfo, imageInfo } = await courseApi.getCourseInfo(courseCode) + const { sellingText, courseDisposition, recommendedPrerequisites, supplementaryInfo, imageInfo } = + await courseApi.getCourseInfo(courseCode) const courseInfo = { ...courseDefaultInformation, sellingText: resolveText(sellingText, language), imageFromAdmin: imageInfo, course_disposition: resolveText(courseDisposition, language), + course_recommended_prerequisites: resolveText(recommendedPrerequisites, language), course_supplemental_information: resolveText(supplementaryInfo, language), } diff --git a/server/apiCalls/kursinfoApi.js b/server/apiCalls/kursinfoApi.js index 2de5f603..0e964f74 100644 --- a/server/apiCalls/kursinfoApi.js +++ b/server/apiCalls/kursinfoApi.js @@ -14,6 +14,7 @@ async function _getCourseInfo(courseCode) { imageInfo: '', supplementaryInfo: { sv: '', en: '' }, courseDisposition: { sv: '', en: '' }, + recommendedPrerequisites: { sv: '', en: '' }, } if (res.statusCode === 200 && res.body) { @@ -21,6 +22,7 @@ async function _getCourseInfo(courseCode) { return { sellingText: body.sellingText ?? defaultValues.sellingText, courseDisposition: body.courseDisposition ?? defaultValues.courseDisposition, + recommendedPrerequisites: body.recommendedPrerequisites ?? defaultValues.recommendedPrerequisites, supplementaryInfo: body.supplementaryInfo ?? defaultValues.supplementaryInfo, imageInfo: body.imageInfo ?? defaultValues.imageInfo, } diff --git a/server/controllers/__tests__/courseCtrl.test.js b/server/controllers/__tests__/courseCtrl.test.js index b57945d8..66ce6126 100644 --- a/server/controllers/__tests__/courseCtrl.test.js +++ b/server/controllers/__tests__/courseCtrl.test.js @@ -48,7 +48,6 @@ jest.mock('../../apiCalls/kursinfoApi', () => ({ getCourseInfo: () => ({ sellingText: { sv: '

Fantastisk kurs

', en: '

This course is awesome

' }, courseDisposition: { sv: '

Kursupplägg på svenska

', en: '

Course Disposition in english

' }, - supplementaryInfo: { sv: '

Övrig info

', en: '

Extra info

' }, imageInfo: 'own_image', }), })) @@ -112,131 +111,129 @@ describe('Discontinued course to test', () => { ) expect(testResponse.html).toMatchInlineSnapshot(` - { - "applicationStore": {}, - "basename": "/student/kurser/kurs", - "context": { - "activeSemesters": [], - "browserConfig": { - "session": {}, - "sessionSecret": "xxx", - }, - "courseCode": "FCK3305", - "courseData": { - "courseInfo": { - "course_application_info": "

Kursen ges inte läsåret 22/23.

Kontakta examinator / kursansvarig för information.

", - "course_code": "FCK3305", - "course_contact_name": "Ingen information tillagd", - "course_department": "CBH/Kemi", - "course_department_code": "CE", - "course_department_link": "CBH/Kemi", - "course_disposition": "

Kursupplägg på svenska

", - "course_education_type_id": null, - "course_examiners": "

Examiner 1

", - "course_grade_scale": "P, F", - "course_last_exam": [], - "course_level_code": "RESEARCH", - "course_literature": "

Litteratur anvisas vid kursstart.

", - "course_main_subject": "Denna kurs tillhör inget huvudområde.", - "course_possibility_to_addition": "Ingen information tillagd", - "course_possibility_to_completions": "Ingen information tillagd", - "course_prerequisites": "Ingen information tillagd", - "course_recruitment_text": "

Teori och metoder inom glykovetenskap.

", - "course_required_equipment": "Ingen information tillagd", - "course_state": "ESTABLISHED", - "course_suggested_addon_studies": "Ingen information tillagd", - "course_supplemental_information": "

Övrig info

", - "course_supplemental_information_url": "Ingen information tillagd", - "course_supplemental_information_url_text": "Ingen information tillagd", - "imageFromAdmin": "own_image", - "sellingText": "

Fantastisk kurs

", - }, - "courseTitleData": { - "course_code": "FCK3305", - "course_credits": 7.5, - "course_credits_text": "hp", - "course_other_title": "Carbohydrate Technologies in Glycoscience", - "course_title": "Kolhydratteknik inom glykovetenskap", - }, - "emptySyllabusData": { - "course_additional_regulations": "", - "course_content": "Ingen information tillagd", - "course_decision_to_discontinue": "", - "course_eligibility": "Ingen information tillagd", - "course_ethical": "", - "course_examination": "Ingen information tillagd", - "course_examination_comments": "", - "course_goals": "Ingen information tillagd", - "course_literature": "Ingen information tillagd", - "course_literature_comment": "Ingen information tillagd", - "course_required_equipment": "", - "course_requirments_for_final_grade": "", - "course_transitional_reg": "", - "course_valid_from": undefined, - "course_valid_to": undefined, - }, - "language": "sv", - "roundsBySemester": {}, - "syllabusList": [ - { - "course_additional_regulations": "", - "course_content": "

Glykovetenskap är ett tvärvetenskapligt forskningsområde som fokuserar på att förstå strukturer och funktionella roller för glykaner (kolhydrater) i biologiska system. Det täcker ämnesområden som biologi, biokemi, kemi, medicin, materialvetenskap, nanoteknologi och beräkningsvetenskap. Kursens mål är att ge en översikt över aktuell kunskap och teknik inom glykovetenskap, utmaningar och möjligheter för bred tillämpning av kolhydratteknik inom hälsa, energi och materialvetenskap, samt god teoretisk insikt och praktiska färdigheter i hur kolhydratteknik kan bidra till hållbar utveckling inom energi- och materialvetenskap.

Ämnen som avhandlas omfattar kolhydratteknik inom hälsa, energi och materialvetenskap, glykaners funktioner, kolhydratanalys av komplexa kolhydrater, glykaner och glykokonjugat, uppbyggnaden av växters cellvägg, kolhydrataktiva enzymer, enzymatisk nedbrytning av växtbiomassa och modifiering av växtbaserade fibrer, biosyntes och av cellulosa och kitin, skapande av nya kompositer genom bioteknologisk modifiering av växtcellväggen, modifiering av glykaner genom att förändra syntesvägar, omvandling av växtbiomassa till finkemikalier och råmaterial, polymera material och nanomaterial, användande av biomassabaserade nanomaterial för nya material och tillämpningar.

", - "course_decision_to_discontinue": "Ingen information tillagd", - "course_eligibility": "

Behörig till studier på forskarnivå. Goda kunskaper i engelska.

", - "course_ethical": "
  • Vid grupparbete har alla i gruppen ansvar för gruppens arbete.
  • Vid examination ska varje student ärligt redovisa hjälp som erhållits och källor som använts.
  • Vid muntlig examination ska varje student kunna redogöra för hela uppgiften och hela lösningen.
", - "course_examination": "
  • INL1 - - Inlämningsuppgift, - 2,0 hp, - betygsskala: P, F -
  • LAB1 - - Laborationer, - 4,0 hp, - betygsskala: P, F -
  • TEN1 - - Skriftlig tentamen, - 1,5 hp, - betygsskala: P, F -
", - "course_examination_comments": "Examinator beslutar, baserat på rekommendation från KTH:s samordnare för funktionsnedsättning, om eventuell anpassad examination för studenter med dokumenterad, varaktig funktionsnedsättning.

Examinator får medge annan examinationsform vid omexamination av enstaka studenter.

Betygskriterier redovisas i kurs-PM.

", - "course_goals": "

Efter fullföljande av kursen förväntas studenten kunna

  • Visa kunskap om kolhydraters mångfald, dess betydelse för biologiska system, samt hur de kan förändra struktur och funktion hos andra biologiska molekyler.
  • Visa kunskap om cellväggens struktur och funktion hos vedbildande växter, samt övergripande förståelse för hur dess sammansättning kan förändras för att möjliggöra nya tillämpningar, t.ex. för att underlätta bearbetning för energi- och biomaterialproduktion.
  • Visa förmåga att redogöra och reflektera över koncept och metoder som används för att producera byggstenar från växtbiomassa, och hur de kan sättas ihop till nya material med skräddarsydda egenskaper och funktionaliteter.
  • Visa förmåga att planera och utföra praktiska experiment inom kolhydratteknik, samt att analysera och redogöra resultaten i form av skriftliga rapporter.
  • Visa förmåga att identifiera och diskutera hur kolhydratteknik kan bidra till en hållbar samhällsutveckling inom konsumtion, produktion och material, t.ex. genom att återanvända redan existerande produkter, eller tillverkning av nya resurssmarta och förnyelsebara material.
", - "course_literature": "Ingen information tillagd", - "course_literature_comment": "Ingen information tillagd", - "course_required_equipment": "Ingen information tillagd", - "course_requirments_for_final_grade": "

Godkänd skriftlig tentamen, godkända inlämningsuppgifter kopplade till föreläsningarna, 100% närvaro på laborationer och slutförande av laborationer, samt godkända laborationsrapporter.

", - "course_transitional_reg": "", - "course_valid_from": { - "semesterNumber": 2, - "year": 2019, - }, - "course_valid_to": undefined, - }, - ], - }, - "employees": { - "responsibles": [], - "teachers": [], - }, - "hostUrl": undefined, - "initiallySelectedRoundIndex": undefined, - "initiallySelectedSemester": null, - "isCancelledOrDeactivated": false, - "lang": "sv", - "paths": { - "system": { - "monitor": { - "uri": "/_monitor", - }, - "robots": { - "uri": "/robots.txt", - }, - }, - }, - "proxyPrefixPath": { - "uri": "/student/kurser/kurs", +{ + "applicationStore": {}, + "basename": "/student/kurser/kurs", + "context": { + "activeSemesters": [], + "browserConfig": { + "session": {}, + "sessionSecret": "xxx", + }, + "courseCode": "FCK3305", + "courseData": { + "courseInfo": { + "course_application_info": "

Kursen ges inte läsåret 22/23.

Kontakta examinator / kursansvarig för information.

", + "course_code": "FCK3305", + "course_contact_name": "Ingen information tillagd", + "course_department": "CBH/Kemi", + "course_department_code": "CE", + "course_department_link": "CBH/Kemi", + "course_disposition": "

Kursupplägg på svenska

", + "course_education_type_id": null, + "course_examiners": "

Examiner 1

", + "course_grade_scale": "P, F", + "course_last_exam": [], + "course_level_code": "RESEARCH", + "course_literature": "

Litteratur anvisas vid kursstart.

", + "course_main_subject": "Denna kurs tillhör inget huvudområde.", + "course_possibility_to_addition": "Ingen information tillagd", + "course_possibility_to_completions": "Ingen information tillagd", + "course_recommended_prerequisites": "", + "course_recruitment_text": "

Teori och metoder inom glykovetenskap.

", + "course_required_equipment": "Ingen information tillagd", + "course_state": "ESTABLISHED", + "course_suggested_addon_studies": "Ingen information tillagd", + "course_supplemental_information": "", + "imageFromAdmin": "own_image", + "sellingText": "

Fantastisk kurs

", + }, + "courseTitleData": { + "course_code": "FCK3305", + "course_credits": 7.5, + "course_credits_text": "hp", + "course_other_title": "Carbohydrate Technologies in Glycoscience", + "course_title": "Kolhydratteknik inom glykovetenskap", + }, + "emptySyllabusData": { + "course_additional_regulations": "", + "course_content": "Ingen information tillagd", + "course_decision_to_discontinue": "", + "course_eligibility": "Ingen information tillagd", + "course_ethical": "", + "course_examination": "Ingen information tillagd", + "course_examination_comments": "", + "course_goals": "Ingen information tillagd", + "course_literature": "Ingen information tillagd", + "course_literature_comment": "Ingen information tillagd", + "course_required_equipment": "", + "course_requirments_for_final_grade": "", + "course_transitional_reg": "", + "course_valid_from": undefined, + "course_valid_to": undefined, + }, + "language": "sv", + "roundsBySemester": {}, + "syllabusList": [ + { + "course_additional_regulations": "", + "course_content": "

Glykovetenskap är ett tvärvetenskapligt forskningsområde som fokuserar på att förstå strukturer och funktionella roller för glykaner (kolhydrater) i biologiska system. Det täcker ämnesområden som biologi, biokemi, kemi, medicin, materialvetenskap, nanoteknologi och beräkningsvetenskap. Kursens mål är att ge en översikt över aktuell kunskap och teknik inom glykovetenskap, utmaningar och möjligheter för bred tillämpning av kolhydratteknik inom hälsa, energi och materialvetenskap, samt god teoretisk insikt och praktiska färdigheter i hur kolhydratteknik kan bidra till hållbar utveckling inom energi- och materialvetenskap.

Ämnen som avhandlas omfattar kolhydratteknik inom hälsa, energi och materialvetenskap, glykaners funktioner, kolhydratanalys av komplexa kolhydrater, glykaner och glykokonjugat, uppbyggnaden av växters cellvägg, kolhydrataktiva enzymer, enzymatisk nedbrytning av växtbiomassa och modifiering av växtbaserade fibrer, biosyntes och av cellulosa och kitin, skapande av nya kompositer genom bioteknologisk modifiering av växtcellväggen, modifiering av glykaner genom att förändra syntesvägar, omvandling av växtbiomassa till finkemikalier och råmaterial, polymera material och nanomaterial, användande av biomassabaserade nanomaterial för nya material och tillämpningar.

", + "course_decision_to_discontinue": "Ingen information tillagd", + "course_eligibility": "

Behörig till studier på forskarnivå. Goda kunskaper i engelska.

", + "course_ethical": "
  • Vid grupparbete har alla i gruppen ansvar för gruppens arbete.
  • Vid examination ska varje student ärligt redovisa hjälp som erhållits och källor som använts.
  • Vid muntlig examination ska varje student kunna redogöra för hela uppgiften och hela lösningen.
", + "course_examination": "
  • INL1 - + Inlämningsuppgift, + 2,0 hp, + betygsskala: P, F +
  • LAB1 - + Laborationer, + 4,0 hp, + betygsskala: P, F +
  • TEN1 - + Skriftlig tentamen, + 1,5 hp, + betygsskala: P, F +
", + "course_examination_comments": "Examinator beslutar, baserat på rekommendation från KTH:s samordnare för funktionsnedsättning, om eventuell anpassad examination för studenter med dokumenterad, varaktig funktionsnedsättning.

Examinator får medge annan examinationsform vid omexamination av enstaka studenter.

Betygskriterier redovisas i kurs-PM.

", + "course_goals": "

Efter fullföljande av kursen förväntas studenten kunna

  • Visa kunskap om kolhydraters mångfald, dess betydelse för biologiska system, samt hur de kan förändra struktur och funktion hos andra biologiska molekyler.
  • Visa kunskap om cellväggens struktur och funktion hos vedbildande växter, samt övergripande förståelse för hur dess sammansättning kan förändras för att möjliggöra nya tillämpningar, t.ex. för att underlätta bearbetning för energi- och biomaterialproduktion.
  • Visa förmåga att redogöra och reflektera över koncept och metoder som används för att producera byggstenar från växtbiomassa, och hur de kan sättas ihop till nya material med skräddarsydda egenskaper och funktionaliteter.
  • Visa förmåga att planera och utföra praktiska experiment inom kolhydratteknik, samt att analysera och redogöra resultaten i form av skriftliga rapporter.
  • Visa förmåga att identifiera och diskutera hur kolhydratteknik kan bidra till en hållbar samhällsutveckling inom konsumtion, produktion och material, t.ex. genom att återanvända redan existerande produkter, eller tillverkning av nya resurssmarta och förnyelsebara material.
", + "course_literature": "Ingen information tillagd", + "course_literature_comment": "Ingen information tillagd", + "course_required_equipment": "Ingen information tillagd", + "course_requirments_for_final_grade": "

Godkänd skriftlig tentamen, godkända inlämningsuppgifter kopplade till föreläsningarna, 100% närvaro på laborationer och slutförande av laborationer, samt godkända laborationsrapporter.

", + "course_transitional_reg": "", + "course_valid_from": { + "semesterNumber": 2, + "year": 2019, }, + "course_valid_to": undefined, }, - "location": undefined, - } - `) + ], + }, + "employees": { + "responsibles": [], + "teachers": [], + }, + "hostUrl": undefined, + "initiallySelectedRoundIndex": undefined, + "initiallySelectedSemester": null, + "isCancelledOrDeactivated": false, + "lang": "sv", + "paths": { + "system": { + "monitor": { + "uri": "/_monitor", + }, + "robots": { + "uri": "/robots.txt", + }, + }, + }, + "proxyPrefixPath": { + "uri": "/student/kurser/kurs", + }, + }, + "location": undefined, +} +`) }) }) diff --git a/server/controllers/mocks/mockedDiscontinuedCourse.js b/server/controllers/mocks/mockedDiscontinuedCourse.js index ee6f4011..c535ad6d 100644 --- a/server/controllers/mocks/mockedDiscontinuedCourse.js +++ b/server/controllers/mocks/mockedDiscontinuedCourse.js @@ -11,7 +11,6 @@ const mockedDiscontinuedCourse = { }, educationalLevelCode: 'RESEARCH', gradeScaleCode: 'PF', - supplementaryInfo: '

Ersätter kurs FBB3640.

', title: 'Kolhydratteknik inom glykovetenskap', titleOther: 'Carbohydrate Technologies in Glycoscience', cancelled: false, diff --git a/server/views/helpers/index.js b/server/views/helpers/index.js index 66658a90..ee1d44ff 100644 --- a/server/views/helpers/index.js +++ b/server/views/helpers/index.js @@ -1,5 +1,5 @@ 'use strict' - +const handlebars = require('handlebars') const registerHeaderContentHelper = require('@kth/kth-node-web-common/lib/handlebars/helpers/headerContent') const { registerBreadcrumbHelper } = require('@kth/kth-node-web-common/lib/handlebars/helpers/breadcrumbs') const { registerLanguageLinkHelper } = require('@kth/kth-node-web-common/lib/handlebars/helpers/languageLink') @@ -41,3 +41,5 @@ require('@kth/kth-node-web-common/lib/handlebars/helpers/contentedit') const i18n = require('../../../i18n') require('@kth/kth-node-web-common/lib/handlebars/helpers/createI18nHelper')(i18n) require('@kth/kth-node-web-common/lib/handlebars/helpers/safe') + +handlebars.registerHelper('eq', (var1, var2) => var1.toString() === var2.toString()) diff --git a/server/views/partials/headerLogotype.handlebars b/server/views/partials/headerLogotype.handlebars index f9e8d7b7..6239219e 100644 --- a/server/views/partials/headerLogotype.handlebars +++ b/server/views/partials/headerLogotype.handlebars @@ -1,6 +1,9 @@
- Till KTH:s startsida + {{#if (eq theme "external")}} Till KTH:s startsida + {{else}} + Till KTH:s startsida + {{/if}}
\ No newline at end of file