mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfcdaf6f10 | ||
|
|
4162717401 | ||
|
|
f9a10d233a | ||
|
|
4474fd861b | ||
|
|
2acbb5c0b6 | ||
|
|
f235d216b1 | ||
|
|
5ee30f163a | ||
|
|
73eea50003 | ||
|
|
8bdff4fde9 | ||
|
|
37ed2d312e | ||
|
|
09dcc22cd4 | ||
|
|
6d5354d006 | ||
|
|
d5103f65df | ||
|
|
343757b1c3 | ||
|
|
f8f0ddb3aa | ||
|
|
063c426206 | ||
|
|
649bab46bd | ||
|
|
e72ced9f8a | ||
|
|
7cd766b1a7 | ||
|
|
a13851095d | ||
|
|
bbaae51a2e | ||
|
|
5381766631 | ||
|
|
6498e5a120 | ||
|
|
beadf34d49 | ||
|
|
e6b368d4ff | ||
|
|
46b91f16d3 | ||
|
|
fd3783f93d | ||
|
|
44523d2ea1 | ||
|
|
1f70c29f75 | ||
|
|
d333b84923 | ||
|
|
309d666ac8 |
+9
-5
@@ -1,15 +1,19 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
"commonjs": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "commonjs"
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:cypress/recommended"],
|
||||
"plugins": ["prettier", "cypress"],
|
||||
"rules": {
|
||||
"no-console": "error",
|
||||
"no-async-promise-executor": "off",
|
||||
"prettier/prettier": "error",
|
||||
"cypress/no-assigning-return-values": "off"
|
||||
"cypress/no-assigning-return-values": "off",
|
||||
"cypress/unsafe-to-chain-command": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,8 +105,8 @@ jobs:
|
||||
with:
|
||||
environment: production
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
sourcemaps: './build/assets'
|
||||
url_prefix: '~/assets'
|
||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
report_success:
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ const relativeFilePaths = files => [...files.map(file => path.relative(cwd, file
|
||||
|
||||
module.exports = {
|
||||
'{services,updater}/**/*.{js,jsx}': files => `eslint --fix ${files.join(' ')} --report-unused-disable-directives`,
|
||||
'*.{js,json,md,yml,yaml,html}': files => `prettier --write ${files.join(' ')}`,
|
||||
'*.{js,json,md,yml,yaml,html,css}': files => `prettier --write ${files.join(' ')}`,
|
||||
'*.css': files => `stylelint --fix ${files.join(' ')}`,
|
||||
Dockerfile: files =>
|
||||
`${dockerCmdBase} hadolint/hadolint:v2.12.0 hadolint --ignore DL3006 ${relativeFilePaths(files)}`,
|
||||
|
||||
+3
-2
@@ -5,5 +5,6 @@
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"jsxSingleQuote": false
|
||||
}
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
|
||||
+2
-18
@@ -1,22 +1,6 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard", "stylelint-prettier/recommended"],
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"rules": {
|
||||
"selector-type-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreTypes": ["/^--/", "--"]
|
||||
}
|
||||
],
|
||||
"selector-pseudo-class-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignorePseudoClasses": ["global"]
|
||||
}
|
||||
],
|
||||
"prettier/prettier": true,
|
||||
"selector-class-pattern": null,
|
||||
"custom-property-pattern": null,
|
||||
"value-no-vendor-prefix": null,
|
||||
"property-no-vendor-prefix": null
|
||||
"selector-class-pattern": "^[a-z][a-zA-Z-]*$"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"aaron-bond.better-comments",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"github.vscode-github-actions",
|
||||
"stylelint.vscode-stylelint"
|
||||
]
|
||||
}
|
||||
@@ -227,7 +227,8 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
|
||||
cy.contains('TKT10004, 581328, AYTKT10004, A581328 Tietokantojen perusteet')
|
||||
cy.get('[data-cy=unify_radio_regular]').click()
|
||||
cy.cs('providerCheckboxUniversity').should('have.class', 'checked').click()
|
||||
cy.cs('providerCheckboxOpenUni').should('have.class', 'checked').click()
|
||||
cy.contains('TKT10004, 581328 Tietokantojen perusteet')
|
||||
})
|
||||
|
||||
@@ -280,7 +281,7 @@ describe('Course Statistics tests', () => {
|
||||
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
|
||||
cy.contains(/^TKT20003, 582219$/).click()
|
||||
cy.contains('TKT20003, 582219 Käyttöjärjestelmät')
|
||||
cy.get('[data-cy=unify_radio_regular]').click()
|
||||
cy.cs('providerCheckboxOpenUni').click()
|
||||
cy.get('tbody > :nth-child(3) > :nth-child(2) .level').click()
|
||||
cy.contains('Population of course Käyttöjärjestelmät 2019-2020')
|
||||
cy.contains('Language distribution').click()
|
||||
@@ -465,12 +466,12 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('If no data available, unify toggle is disabled', () => {
|
||||
it('If no data available, toggle is disabled', () => {
|
||||
cy.get("input[placeholder='Search by a course code']").type('AYCSM14111')
|
||||
cy.contains('AYCSM14111').click()
|
||||
cy.contains('AYCSM14111 Avoin yo: Full Stack -websovelluskehitys: React Native')
|
||||
cy.get('[data-cy=unify_radio_regular]').find('input').should('be.disabled')
|
||||
cy.get('[data-cy=unify_radio_unify]').find('input').should('not.be.disabled')
|
||||
cy.cs('providerCheckboxUniversity').find('input').should('be.disabled')
|
||||
cy.cs('providerCheckboxOpenUni').find('input').should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('Has right to see all the students, because course provider is TKT', () => {
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('Population Statistics tests', () => {
|
||||
// This test sometimes fails on headless mode. It seems that the click on the
|
||||
// 'Fetch class with new settings' button doesn't always trigger history.push()
|
||||
// so the page doesn't reload. This is why waiting also doesn't help.
|
||||
it('Advanced settings work', () => {
|
||||
it('Advanced settings work', { retries: 2 }, () => {
|
||||
cy.visit(pathToCSBach2017)
|
||||
cy.get('[data-cy=advanced-toggle]').click()
|
||||
// only spring
|
||||
|
||||
@@ -55,15 +55,11 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Basic information contains correct credits', () => {
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Total, Major credits, Non-major credits, Non-degree credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
[2021, 0, 0, 0, 0, 0],
|
||||
[2020, 4928, 4736, 25, 0, 167],
|
||||
[2019, 7737, 7266, 99, 23, 349],
|
||||
[2018, 6730, 6289, 84, 85, 272],
|
||||
[2017, 2900, 2452, 33, 27, 388],
|
||||
[2020, 4511, 4511, 0, 0, 167],
|
||||
[2019, 7117, 7097, 0, 0, 349],
|
||||
[2018, 5747, 5742, 0, 0, 272],
|
||||
[2017, 1910, 1910, 0, 0, 388],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
@@ -101,12 +97,10 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
const creditTableContents = [
|
||||
// [Year, Total, Major students credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0]),
|
||||
[2021, 0, 0, 0],
|
||||
[2020, 4224, 3954, 167],
|
||||
[2019, 5812, 5357, 349],
|
||||
[2018, 4639, 4302, 272],
|
||||
[2017, 1968, 1580, 388],
|
||||
[2020, 4511, 4511, 0, 0, 167],
|
||||
[2019, 7117, 7097, 0, 0, 349],
|
||||
[2018, 5747, 5742, 0, 0, 272],
|
||||
[2017, 1910, 1910, 0, 0, 388],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
@@ -141,13 +135,11 @@ describe('Studyprogramme overview', () => {
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
|
||||
const creditTableContents = [
|
||||
// [Year, Total, Major students credits, Non-major students credits, Non-degree credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0, 0],
|
||||
['2020 - 2021', 412, 342, 10, 0, 60],
|
||||
['2019 - 2020', 7546, 7319, 34, 0, 193],
|
||||
['2018 - 2019', 8866, 8380, 115, 43, 328],
|
||||
['2017 - 2018', 5220, 4702, 82, 92, 344],
|
||||
// Credit table doesnt show (later) years without data
|
||||
['2020 - 2021', 352, 352, 0, 0, 60],
|
||||
['2019 - 2020', 6966, 6966, 0, 0, 193],
|
||||
['2018 - 2019', 7797, 7777, 0, 0, 328],
|
||||
['2017 - 2018', 4170, 4165, 0, 0, 344],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
@@ -161,11 +153,10 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', 'Transferred away')
|
||||
|
||||
cy.get('[data-cy=Graph-CreditsProducedByTheStudyprogramme')
|
||||
.should('contain', 'Major students credits')
|
||||
.should('contain', 'Non-major students credits')
|
||||
.should('contain', 'Transferred credits')
|
||||
.should('contain', 7266)
|
||||
.should('contain', 6289)
|
||||
.should('contain', 'Degree students')
|
||||
.should('contain', 'Transferred')
|
||||
.should('contain', 388)
|
||||
.should('contain', 7097)
|
||||
|
||||
cy.get('[data-cy=Graph-GraduatedAndThesisWritersOfTheProgramme')
|
||||
.should('contain', 'Graduated students')
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
import './commands'
|
||||
require('./commands')
|
||||
|
||||
// This error is benign and seems to be only present in Cypress
|
||||
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
|
||||
|
||||
Generated
+1818
-1198
File diff suppressed because it is too large
Load Diff
+12
-14
@@ -32,24 +32,22 @@
|
||||
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
|
||||
"devDependencies": {
|
||||
"concurrently": "^7.4.0",
|
||||
"cypress": "^12.17.4",
|
||||
"eslint": "^8.25.0",
|
||||
"cypress": "^13.6.4",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.9",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"prettier": "^2.5.1",
|
||||
"stylelint": "^14.13.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"stylelint-prettier": "^2.0.0"
|
||||
"prettier": "3.2.5",
|
||||
"stylelint": "^16.2.1",
|
||||
"stylelint-config-standard": "^36.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
commonjs: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: 'eslint:recommended',
|
||||
overrides: [
|
||||
{
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
files: ['.eslintrc.{js,cjs}'],
|
||||
parserOptions: {
|
||||
sourceType: 'script',
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
},
|
||||
}
|
||||
Generated
+52
-66
@@ -285,17 +285,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
|
||||
"integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
|
||||
"version": "0.7.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
||||
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.6.2",
|
||||
@@ -303,9 +303,9 @@
|
||||
"integrity": "sha512-KcfkBq9H4PI6Vpu5B/KoPeuVDAbmi+2mDBqGPGUgoL7yXQtcWGu2vJWmmRkneWK3Rh0nIAX192Aa87AqKHYChQ=="
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.4.tgz",
|
||||
"integrity": "sha512-uAaSWegu2lymY18l+s5nmcXu3sFeeTOl1zhSGoYzcr6T3wz1M+3OcW4UjfPhIhHGd13tIMRDsEpR+d8w/MexwQ=="
|
||||
"version": "13.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz",
|
||||
"integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw=="
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
@@ -705,9 +705,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dottie": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz",
|
||||
"integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg=="
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
|
||||
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
@@ -842,9 +842,9 @@
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -901,27 +901,13 @@
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"node_modules/gelf-pro": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/gelf-pro/-/gelf-pro-1.3.3.tgz",
|
||||
"integrity": "sha512-gohbipAj/okE7pUgRSkIMEiqAS2M3q5YWnoejUDA9qwBeqbJr8LSwe3E7YW2QFK7hi4tZje1ZnKQe0296zdW/w==",
|
||||
"version": "1.3.11",
|
||||
"resolved": "https://registry.npmjs.org/gelf-pro/-/gelf-pro-1.3.11.tgz",
|
||||
"integrity": "sha512-y6DOxU40U4Sd+ECSLtMMtOTig1gEmnIHnvfocyvH+okc6LSfd7ADirT1tbzpAMzOXIgDG/2j9psZ1ck3HXfjEA==",
|
||||
"dependencies": {
|
||||
"async": "2.6.2",
|
||||
"lodash": "4.17.14"
|
||||
"lodash": "~4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/gelf-pro/node_modules/async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
|
||||
"integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.11"
|
||||
}
|
||||
},
|
||||
"node_modules/gelf-pro/node_modules/lodash": {
|
||||
"version": "4.17.14",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
|
||||
"integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw=="
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
|
||||
@@ -1081,9 +1067,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/inflection": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.2.tgz",
|
||||
"integrity": "sha512-cmZlljCRTBFouT8UzMzrGcVEvkv6D/wBdcdKG7J1QH5cXjtU75Dm+P27v9EKu/Y43UYyCJd1WC4zLebRrC8NBw==",
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
|
||||
"integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
|
||||
"engines": [
|
||||
"node >= 0.4.0"
|
||||
]
|
||||
@@ -1344,11 +1330,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.37",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.37.tgz",
|
||||
"integrity": "sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==",
|
||||
"version": "0.5.45",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz",
|
||||
"integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==",
|
||||
"dependencies": {
|
||||
"moment": ">= 2.9.0"
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -1386,9 +1372,9 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.9.9",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.9.9.tgz",
|
||||
"integrity": "sha512-sbn6mioS2w0lq1O6PpGtsv6Gy8roWM+o3o4Sqjd6DudrL/nOugY+KyJUimoWzHnf9OkO0T6broHFnYE/R05t9A==",
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.1.tgz",
|
||||
"integrity": "sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==",
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.2"
|
||||
}
|
||||
@@ -1613,9 +1599,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
|
||||
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
|
||||
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
@@ -1841,9 +1827,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/retry-as-promised": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-6.1.0.tgz",
|
||||
"integrity": "sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA=="
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz",
|
||||
"integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA=="
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
@@ -1936,9 +1922,9 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/sequelize": {
|
||||
"version": "6.24.0",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.24.0.tgz",
|
||||
"integrity": "sha512-mPo7Q7gWkrsstjR2aw8ahkrj8RUJCozz3kedAz2B5ZIdLoQEH1z2tvaeJONI8R6RqeuyRQosx9Yn3s9yQyo6lQ==",
|
||||
"version": "6.37.0",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.0.tgz",
|
||||
"integrity": "sha512-MS6j6aXqWzB3fe9FhmfpQMgVC16bBdYroJCqIqR0l9M2ko8pZdKoi/0PiNWgMyFQDXUHxXyAOG3K07CbnOhteQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1946,21 +1932,21 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/validator": "^13.7.1",
|
||||
"debug": "^4.3.3",
|
||||
"dottie": "^2.0.2",
|
||||
"inflection": "^1.13.2",
|
||||
"@types/debug": "^4.1.8",
|
||||
"@types/validator": "^13.7.17",
|
||||
"debug": "^4.3.4",
|
||||
"dottie": "^2.0.6",
|
||||
"inflection": "^1.13.4",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"pg-connection-string": "^2.5.0",
|
||||
"retry-as-promised": "^6.1.0",
|
||||
"semver": "^7.3.5",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"pg-connection-string": "^2.6.1",
|
||||
"retry-as-promised": "^7.0.4",
|
||||
"semver": "^7.5.4",
|
||||
"sequelize-pool": "^7.1.0",
|
||||
"toposort-class": "^1.0.1",
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "^13.7.0",
|
||||
"validator": "^13.9.0",
|
||||
"wkx": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2259,9 +2245,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
|
||||
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==",
|
||||
"version": "13.11.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
|
||||
"integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
|
||||
@@ -32,5 +32,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.3"
|
||||
},
|
||||
"overrides": {
|
||||
"winston-gelf-transporter": {
|
||||
"gelf-pro": "^1.3.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
const { getCreditStatsForRapodiff } = require('../services/studyprogramme/rapoCredits')
|
||||
const { getCreditStatsForRapodiff } = require('../services/studyprogramme/studyprogrammeCredits')
|
||||
const { parseCsv } = require('./helpers')
|
||||
const _ = require('lodash')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const router = require('express').Router()
|
||||
const { getBasicStatsForStudytrack } = require('../services/studyprogramme/studyprogrammeBasics')
|
||||
const { getCreditStatsForStudytrack } = require('../services/studyprogramme/studyprogrammeCredits')
|
||||
const { getProgrammeCreditStats } = require('../services/studyprogramme/studyprogrammeCredits')
|
||||
const { getGraduationStatsForStudytrack } = require('../services/studyprogramme/studyprogrammeGraduations')
|
||||
const {
|
||||
getStudyprogrammeCoursesForStudytrack,
|
||||
@@ -10,8 +10,6 @@ const { getStudytrackStatsForStudyprogramme } = require('../services/studyprogra
|
||||
const {
|
||||
getBasicStats,
|
||||
setBasicStats,
|
||||
getCreditStats,
|
||||
setCreditStats,
|
||||
getGraduationStats,
|
||||
setGraduationStats,
|
||||
getStudytrackStats,
|
||||
@@ -56,26 +54,14 @@ router.get('/v2/studyprogrammes/:id/basicstats', async (req, res) => {
|
||||
return res.json(updated)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/creditstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const combinedProgramme = req.query?.combined_programme
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
const data = await getCreditStats(code, combinedProgramme, yearType, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
const updatedStats = await getCreditStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
combinedProgramme,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
if (updatedStats) await setCreditStats(updatedStats, yearType, specialGroups)
|
||||
return res.json(updatedStats)
|
||||
router.get('/v2/studyprogrammes/creditstats', async (req, res) => {
|
||||
const { codes: codesListString, isAcademicYear, includeSpecials } = req.query
|
||||
const codes = JSON.parse(codesListString)
|
||||
const stats = {}
|
||||
for (const code of codes) {
|
||||
stats[code] = await getProgrammeCreditStats(code, isAcademicYear !== 'false', includeSpecials)
|
||||
}
|
||||
return res.json({ stats })
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/graduationstats', async (req, res) => {
|
||||
|
||||
@@ -33,18 +33,21 @@ const setBasicStats = async (data, yearType, specialGroups) => {
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getCreditStats = async (id, combinedProgramme, yearType, specialGroups) => {
|
||||
const getCreditStats = async (id, isAcademicYear, specialGroups) => {
|
||||
if (!isUpdatedNewProgramme(id)) return null
|
||||
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
|
||||
const redisKey = createRedisKeyForCreditStats(searchkey, yearType, specialGroups)
|
||||
const redisKey = createRedisKeyForCreditStats(id, isAcademicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR', specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setCreditStats = async (data, yearType, specialGroups) => {
|
||||
const setCreditStats = async (data, isAcademicYear, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, specialGroups)
|
||||
const redisKey = createRedisKeyForCreditStats(
|
||||
id,
|
||||
isAcademicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR',
|
||||
specialGroups ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED'
|
||||
)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
|
||||
@@ -136,9 +136,8 @@ const getFacultyCreditStatsForStudytrack = async ({
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
|
||||
const queryParameters = { studyprogramme, since, years, isAcademicYear, facultyProgrammes, facultyCode }
|
||||
const { majors, facultyNonMajors, otherNonMajors, facultyOthers, nonDegree } = await getFacultyRegularCreditStats(
|
||||
queryParameters
|
||||
)
|
||||
const { majors, facultyNonMajors, otherNonMajors, facultyOthers, nonDegree } =
|
||||
await getFacultyRegularCreditStats(queryParameters)
|
||||
|
||||
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
|
||||
const dataFound = [majors, facultyNonMajors, otherNonMajors].some(d => d.graphStats.length) // , transferred
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
const { mapToProviders } = require('../../util/utils')
|
||||
const { defineYear, getCorrectStartDate } = require('./studyprogrammeHelpers')
|
||||
const { getCourseCodesForStudyProgramme, allTransfers } = require('.')
|
||||
const { getCreditsForStudyProgramme, getTransferredCredits } = require('./creditGetters')
|
||||
const { getStudyRights } = require('./studyrightFinders')
|
||||
|
||||
/**
|
||||
* Rapo-kategoriat 9.2.2024, joiden perusteella tämä koodi on tehty. Numerot täsmäävät vain 2022 alkaen, koska sisu/oodi ero.
|
||||
|
||||
'basic'
|
||||
Perustutkinto-opiskelijat: niiden opiskelijoiden opintopisteet, joilla on oikeus suorittaa alempi tai
|
||||
ylempi korkeakoulututkinto.
|
||||
|
||||
'incoming-exchange'
|
||||
Saapuvat vaihto-opiskelijat: opintosuoritukset, jotka ovat saapuvan kansainvälisen opiskelijan suorittamia.
|
||||
Sisältää kaikki vaihto-opiskelijan opiskeluoikeuden aikana syntyneet opintopisteet.
|
||||
|
||||
'agreement'
|
||||
Korkeakoulujen väliset yhteistyöopinnot: opintosuoritukset, jotka on tehty korkeakoulujen väliseen
|
||||
yhteistyösopimukseen perustuvalla opiskeluoikeudella.
|
||||
|
||||
'open-uni'
|
||||
Avoimen opiskeluoikeus, ei tutkinto-opiskelija: opintosuoritukset, joiden suorituksen luokittelu on
|
||||
"avoimena yliopisto-opintona suoritettu" ja opiskelija ei ole tutkinto-opiskelija tai kansainvälinen vaihto-opiskelija.
|
||||
|
||||
'separate'
|
||||
Erillisoikeus: opintosuoritukset, joiden suorituksen luokittelu on "erillisellä opiskeluoikeudella" tai
|
||||
"opettajankoulutuksen erillisellä opiskeluikeudella" suoritettu. Ei sisällä kansainvälisten vaihto-opiskelijoiden suorituksia.
|
||||
Ulkomailta hyväksiluetut opintopisteet: ulkomailta hyväksilutetut opintopisteet.
|
||||
|
||||
'transferred'
|
||||
Muut hyväksiluetut opintopisteet: kotimaassa suoritetut, hyväksiluetut opintopisteet.
|
||||
*/
|
||||
|
||||
const getCategory = (extent, hadDegreeStudyright) => {
|
||||
if ([1, 2, 3, 4].includes(extent) || hadDegreeStudyright) return 'basic' // Rapo-kategoria: Perustutkinto-opiskelijat
|
||||
if (extent === 7 || extent === 34) return 'incoming-exchange' // Saapuvat vaihto-opiskelijat
|
||||
if (extent === 9 && !hadDegreeStudyright) return 'open-uni' // Avoimen opiskeluoikeus, ei tutkinto-opiskelija
|
||||
if (extent === 14) return 'agreement' // Korkeakoulujen väliset yhteistyöopinnot
|
||||
if (extent === 99) return 'separate' // Erillisoikeus
|
||||
return 'other'
|
||||
}
|
||||
|
||||
/*
|
||||
If credit didn't include studyright (pre-sisu times mostly), try to find out relevant
|
||||
other way. First check for same faculty as credit, but if not found, take first
|
||||
without (both have to be active when course was completed)
|
||||
*/
|
||||
const findRelevantStudyright = (attainmentDate, studyrights) => {
|
||||
return studyrights.find(studyright => {
|
||||
const startDate = getCorrectStartDate(studyright)
|
||||
if (!studyright.graduated) return new Date(attainmentDate) >= new Date(startDate)
|
||||
return (
|
||||
new Date(studyright.startdate).getTime() <= new Date(attainmentDate).getTime() &&
|
||||
new Date(attainmentDate).getTime() <= new Date(studyright.enddate).getTime()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// At the given date, student had SOME degree-studyright.
|
||||
const isDegreeStudent = (studyrights, date) =>
|
||||
studyrights?.some(sr => {
|
||||
const rightExtentCode = [1, 2, 3, 4].includes(sr.extentcode)
|
||||
const rightDates =
|
||||
new Date(sr.startdate).getTime() <= new Date(date).getTime() &&
|
||||
new Date(date).getTime() <= new Date(sr.enddate).getTime()
|
||||
// it is possible to also check if student had active semester enrollment to the studyright here.
|
||||
// However, it is unsure if rapo requires this (seems like maybe not). shouldn't make a huge difference.
|
||||
return rightExtentCode && rightDates
|
||||
})
|
||||
|
||||
/* includes all specials, is calendar year, since 2017-01-01 */
|
||||
const getCreditStatsForRapodiff = async (programmeCode, isAcademicYear, specialIncluded = true) => {
|
||||
const since = new Date('2017-01-01')
|
||||
const providercode = mapToProviders([programmeCode])[0]
|
||||
const courses = await getCourseCodesForStudyProgramme(providercode)
|
||||
const credits = await getCreditsForStudyProgramme(providercode, courses, since)
|
||||
const students = [...new Set(credits.map(({ studentNumber }) => studentNumber))]
|
||||
|
||||
const transfers = (await allTransfers(programmeCode, since)).map(t => t.studyrightid)
|
||||
|
||||
let studyrights = await getStudyRights(students)
|
||||
if (!specialIncluded) {
|
||||
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
|
||||
}
|
||||
|
||||
const studyrightIdToStudyrightMap = studyrights.reduce((obj, cur) => {
|
||||
obj[cur.actual_studyrightid] = cur
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
const studentNumberToStudyrightsMap = studyrights.reduce((obj, cur) => {
|
||||
if (!obj[cur.studentNumber]) obj[cur.studentNumber] = []
|
||||
obj[cur.studentNumber].push(cur)
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
const stats = {}
|
||||
|
||||
credits.forEach(({ attainmentDate, studentNumber, credits, studyrightId }) => {
|
||||
const relevantStudyright = studyrightId
|
||||
? studyrightIdToStudyrightMap[studyrightId]
|
||||
: findRelevantStudyright(attainmentDate, studentNumberToStudyrightsMap[studentNumber])
|
||||
if (!relevantStudyright) return
|
||||
const attainmentYear = defineYear(attainmentDate, isAcademicYear)
|
||||
|
||||
const hadDegreeStudyright = isDegreeStudent(studentNumberToStudyrightsMap[studentNumber], attainmentDate)
|
||||
const category = getCategory(relevantStudyright.extentcode, hadDegreeStudyright)
|
||||
if (!stats[attainmentYear]) stats[attainmentYear] = { transferred: 0 }
|
||||
stats[attainmentYear].total += credits
|
||||
if (!stats[attainmentYear][category]) {
|
||||
stats[attainmentYear][category] = 0
|
||||
}
|
||||
stats[attainmentYear][category] += credits
|
||||
})
|
||||
|
||||
const transferredCredits = await getTransferredCredits(providercode, since)
|
||||
|
||||
transferredCredits.forEach(({ attainment_date, credits }) => {
|
||||
const attainmentYear = defineYear(attainment_date, isAcademicYear)
|
||||
if (!stats[attainmentYear]) stats[attainmentYear] = { transferred: 0 }
|
||||
stats[attainmentYear].transferred += credits || 0
|
||||
// Transferred not counted in total
|
||||
})
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCreditStatsForRapodiff,
|
||||
}
|
||||
@@ -47,7 +47,6 @@ const getGraduatedStats = async ({ studyprogramme, since, years, isAcademicYear,
|
||||
includeAllSpecials,
|
||||
includeTransferredTo: includeAllSpecials,
|
||||
})
|
||||
|
||||
const studyrights = await graduatedStudyRights(studyprogramme, since, studentnumbersOfTheYear)
|
||||
studyrights.forEach(({ enddate, studyrightElements }) => {
|
||||
// Check that the study right element ending to graduation belong to study programme
|
||||
|
||||
@@ -1,189 +1,141 @@
|
||||
const { indexOf } = require('lodash')
|
||||
|
||||
const { mapToProviders } = require('../../util/utils')
|
||||
const {
|
||||
getStatsBasis,
|
||||
defineYear,
|
||||
isMajorStudentCredit,
|
||||
isNonMajorCredit,
|
||||
getYearsArray,
|
||||
getStartDate,
|
||||
isSpecialGroupCredit,
|
||||
tableTitles,
|
||||
} = require('./studyprogrammeHelpers')
|
||||
const { allTransfers, getCourseCodesForStudyProgramme } = require('.')
|
||||
const { getTransferredCredits, getCreditsForStudyProgramme } = require('./creditGetters')
|
||||
const { defineYear, getCorrectStartDate } = require('./studyprogrammeHelpers')
|
||||
const { getCourseCodesForStudyProgramme, allTransfers } = require('.')
|
||||
const { getCreditsForStudyProgramme, getTransferredCredits } = require('./creditGetters')
|
||||
const { getStudyRights } = require('./studyrightFinders')
|
||||
const { getCreditStats, setCreditStats } = require('../analyticsService')
|
||||
|
||||
const createGraphStats = (majors, nonMajors, nonDegree, transferred) => {
|
||||
return [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major students credits',
|
||||
data: nonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-degree credits',
|
||||
data: nonDegree.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Transferred credits',
|
||||
data: transferred.graphStats,
|
||||
},
|
||||
]
|
||||
/**
|
||||
* Rapo-kategoriat 9.2.2024, joiden perusteella tämä koodi on tehty. Numerot täsmäävät vain 2022 alkaen, koska sisu/oodi ero.
|
||||
|
||||
'basic'
|
||||
Perustutkinto-opiskelijat: niiden opiskelijoiden opintopisteet, joilla on oikeus suorittaa alempi tai
|
||||
ylempi korkeakoulututkinto.
|
||||
|
||||
'incoming-exchange'
|
||||
Saapuvat vaihto-opiskelijat: opintosuoritukset, jotka ovat saapuvan kansainvälisen opiskelijan suorittamia.
|
||||
Sisältää kaikki vaihto-opiskelijan opiskeluoikeuden aikana syntyneet opintopisteet.
|
||||
|
||||
'agreement'
|
||||
Korkeakoulujen väliset yhteistyöopinnot: opintosuoritukset, jotka on tehty korkeakoulujen väliseen
|
||||
yhteistyösopimukseen perustuvalla opiskeluoikeudella.
|
||||
|
||||
'open-uni'
|
||||
Avoimen opiskeluoikeus, ei tutkinto-opiskelija: opintosuoritukset, joiden suorituksen luokittelu on
|
||||
"avoimena yliopisto-opintona suoritettu" ja opiskelija ei ole tutkinto-opiskelija tai kansainvälinen vaihto-opiskelija.
|
||||
|
||||
'separate'
|
||||
Erillisoikeus: opintosuoritukset, joiden suorituksen luokittelu on "erillisellä opiskeluoikeudella" tai
|
||||
"opettajankoulutuksen erillisellä opiskeluikeudella" suoritettu. Ei sisällä kansainvälisten vaihto-opiskelijoiden suorituksia.
|
||||
Ulkomailta hyväksiluetut opintopisteet: ulkomailta hyväksilutetut opintopisteet.
|
||||
|
||||
'transferred'
|
||||
Muut hyväksiluetut opintopisteet: kotimaassa suoritetut, hyväksiluetut opintopisteet.
|
||||
*/
|
||||
|
||||
const getCategory = (extent, hadDegreeStudyright) => {
|
||||
if ([1, 2, 3, 4].includes(extent) || hadDegreeStudyright) return 'basic' // Rapo-kategoria: Perustutkinto-opiskelijat
|
||||
if (extent === 7 || extent === 34) return 'incoming-exchange' // Saapuvat vaihto-opiskelijat
|
||||
if (extent === 9 && !hadDegreeStudyright) return 'open-uni' // Avoimen opiskeluoikeus, ei tutkinto-opiskelija
|
||||
if (extent === 14) return 'agreement' // Korkeakoulujen väliset yhteistyöopinnot
|
||||
if (extent === 99) return 'separate' // Erillisoikeus
|
||||
return 'other'
|
||||
}
|
||||
|
||||
const createTableStats = (reversedYears, includeAllSpecials, majors, nonMajors, transferred, nonDegree) => {
|
||||
const tableStats = reversedYears.map(year =>
|
||||
includeAllSpecials
|
||||
? [
|
||||
year,
|
||||
majors.tableStats[year] +
|
||||
nonMajors.tableStats[year] +
|
||||
transferred.tableStats[year] +
|
||||
nonDegree.tableStats[year],
|
||||
majors.tableStats[year],
|
||||
nonMajors.tableStats[year],
|
||||
nonDegree.tableStats[year],
|
||||
transferred.tableStats[year],
|
||||
]
|
||||
: [
|
||||
year,
|
||||
majors.tableStats[year] +
|
||||
nonMajors.tableStats[year] +
|
||||
transferred.tableStats[year] +
|
||||
nonDegree.tableStats[year],
|
||||
majors.tableStats[year],
|
||||
transferred.tableStats[year],
|
||||
]
|
||||
)
|
||||
return tableStats
|
||||
/*
|
||||
If credit didn't include studyright (pre-sisu times mostly), try to find out relevant
|
||||
other way. First check for same faculty as credit, but if not found, take first
|
||||
without (both have to be active when course was completed)
|
||||
*/
|
||||
const findRelevantStudyright = (attainmentDate, studyrights) => {
|
||||
return studyrights.find(studyright => {
|
||||
const startDate = getCorrectStartDate(studyright)
|
||||
if (!studyright.graduated) return new Date(attainmentDate) >= new Date(startDate)
|
||||
return (
|
||||
new Date(studyright.startdate).getTime() <= new Date(attainmentDate).getTime() &&
|
||||
new Date(attainmentDate).getTime() <= new Date(studyright.enddate).getTime()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Fetches all the credits for the studyprogramme and divides them into major-students and non-major students credits
|
||||
// Division is done on the basis that whether the student had a primary studyright to the programme on the attainmentDate
|
||||
// If special groups are excluded, the transfer students are filtered away from the major-students as well
|
||||
const getRegularCreditStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
|
||||
let majors = getStatsBasis(years)
|
||||
let nonMajors = getStatsBasis(years)
|
||||
let nonDegree = getStatsBasis(years)
|
||||
if (!studyprogramme) {
|
||||
return { majors, nonMajors, nonDegree }
|
||||
}
|
||||
const providercode = mapToProviders([studyprogramme])[0]
|
||||
// At the given date, student had SOME degree-studyright.
|
||||
const isDegreeStudent = (studyrights, date) =>
|
||||
studyrights?.some(sr => {
|
||||
const rightExtentCode = [1, 2, 3, 4].includes(sr.extentcode)
|
||||
const rightDates =
|
||||
new Date(sr.startdate).getTime() <= new Date(date).getTime() &&
|
||||
new Date(date).getTime() <= new Date(sr.enddate).getTime()
|
||||
// it is possible to also check if student had active semester enrollment to the studyright here.
|
||||
// However, it is unsure if rapo requires this (seems like maybe not). shouldn't make a huge difference.
|
||||
return rightExtentCode && rightDates
|
||||
})
|
||||
|
||||
const getProgrammeCreditStats = async (programmeCode, isAcademicYear, specialIncluded = true) => {
|
||||
let data = await getCreditStats(programmeCode, isAcademicYear, specialIncluded)
|
||||
if (data) return data
|
||||
data = await computeProgrammeCreditStats(programmeCode, isAcademicYear, specialIncluded)
|
||||
await setCreditStats(data, isAcademicYear, specialIncluded)
|
||||
return data
|
||||
}
|
||||
|
||||
/* includes all specials, is calendar year, since 2017-01-01 */
|
||||
const computeProgrammeCreditStats = async (programmeCode, isAcademicYear, specialIncluded = true) => {
|
||||
const since = new Date('2017-01-01')
|
||||
const providercode = mapToProviders([programmeCode])[0]
|
||||
const courses = await getCourseCodesForStudyProgramme(providercode)
|
||||
|
||||
const credits = await getCreditsForStudyProgramme(providercode, courses, since)
|
||||
|
||||
const students = [...new Set(credits.map(({ studentNumber }) => studentNumber))]
|
||||
|
||||
const transfers = (await allTransfers(programmeCode, since)).map(t => t.studyrightid)
|
||||
|
||||
let studyrights = await getStudyRights(students)
|
||||
|
||||
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid)
|
||||
|
||||
if (!includeAllSpecials) {
|
||||
if (!specialIncluded) {
|
||||
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
|
||||
}
|
||||
|
||||
const studyrightIdToStudyrightMap = studyrights.reduce((obj, cur) => {
|
||||
obj[cur.actual_studyrightid] = cur
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
const studentNumberToStudyrightsMap = studyrights.reduce((obj, cur) => {
|
||||
if (!obj[cur.studentNumber]) obj[cur.studentNumber] = []
|
||||
obj[cur.studentNumber].push(cur)
|
||||
return obj
|
||||
}, {})
|
||||
credits.forEach(({ studentNumber, attainmentDate, credits }) => {
|
||||
const studentStudyrights = studentNumberToStudyrightsMap[studentNumber] || []
|
||||
|
||||
const stats = {}
|
||||
|
||||
credits.forEach(({ attainmentDate, studentNumber, credits, studyrightId }) => {
|
||||
const relevantStudyright = studyrightId
|
||||
? studyrightIdToStudyrightMap[studyrightId]
|
||||
: findRelevantStudyright(attainmentDate, studentNumberToStudyrightsMap[studentNumber])
|
||||
if (!relevantStudyright) return
|
||||
const attainmentYear = defineYear(attainmentDate, isAcademicYear)
|
||||
|
||||
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainmentDate, transfers)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isMajorStudentCredit(studentStudyrights, attainmentDate, studyprogramme)) {
|
||||
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
majors.tableStats[attainmentYear] += credits || 0
|
||||
} else if (isNonMajorCredit(studentStudyrights, attainmentDate, studyprogramme)) {
|
||||
nonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
nonMajors.tableStats[attainmentYear] += credits || 0
|
||||
} else {
|
||||
nonDegree.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
nonDegree.tableStats[attainmentYear] += credits || 0
|
||||
const hadDegreeStudyright = isDegreeStudent(studentNumberToStudyrightsMap[studentNumber], attainmentDate)
|
||||
const category = getCategory(relevantStudyright.extentcode, hadDegreeStudyright)
|
||||
if (!stats[attainmentYear]) stats[attainmentYear] = { transferred: 0 }
|
||||
stats[attainmentYear].total += credits
|
||||
if (!stats[attainmentYear][category]) {
|
||||
stats[attainmentYear][category] = 0
|
||||
}
|
||||
stats[attainmentYear][category] += credits
|
||||
})
|
||||
|
||||
return { majors, nonMajors, nonDegree }
|
||||
}
|
||||
const transferredCredits = await getTransferredCredits(providercode, since)
|
||||
|
||||
// Fetches all the credits with the type "transferred / hyväksiluettu" and divides them by year
|
||||
const getTransferredCreditStats = async ({ studyprogramme, since, years, isAcademicYear }) => {
|
||||
const providercode = mapToProviders([studyprogramme])[0]
|
||||
const credits = await getTransferredCredits(providercode, since)
|
||||
const { graphStats, tableStats } = getStatsBasis(years)
|
||||
|
||||
credits.forEach(({ attainment_date, credits }) => {
|
||||
transferredCredits.forEach(({ attainment_date, credits }) => {
|
||||
const attainmentYear = defineYear(attainment_date, isAcademicYear)
|
||||
graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
tableStats[attainmentYear] += credits || 0
|
||||
if (!stats[attainmentYear]) stats[attainmentYear] = { transferred: 0 }
|
||||
stats[attainmentYear].transferred += credits || 0
|
||||
// Transferred not counted in total
|
||||
})
|
||||
|
||||
return { graphStats, tableStats }
|
||||
}
|
||||
|
||||
// Fetches all credits for the studytrack and combines them into the statistics for the table
|
||||
// and graph in the studyprogramme overview
|
||||
const getCreditStatsForStudytrack = async ({ studyprogramme, combinedProgramme, settings }) => {
|
||||
const { isAcademicYear, includeAllSpecials } = settings
|
||||
const since = getStartDate(studyprogramme, isAcademicYear)
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials }
|
||||
const queryParametersSecondProg = {
|
||||
studyprogramme: combinedProgramme,
|
||||
since,
|
||||
years,
|
||||
isAcademicYear,
|
||||
includeAllSpecials,
|
||||
}
|
||||
const { majors, nonMajors, nonDegree } = await getRegularCreditStats(queryParameters)
|
||||
const transferred = await getTransferredCreditStats(queryParameters)
|
||||
|
||||
const {
|
||||
majors: majorsSecondProg,
|
||||
nonMajors: nonMajorsSecondProg,
|
||||
nonDegree: nonDegreeSecondProg,
|
||||
} = await getRegularCreditStats(queryParametersSecondProg)
|
||||
const transferredSecondProg = await getTransferredCreditStats(queryParametersSecondProg)
|
||||
|
||||
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
|
||||
const titles = tableTitles['credits'][includeAllSpecials ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED']
|
||||
const dataFound = [majors, nonMajors, transferred].some(d => d.graphStats.length)
|
||||
|
||||
if (!dataFound) return null
|
||||
const dataFoundSecondProg = [majorsSecondProg, nonMajorsSecondProg, transferredSecondProg].some(
|
||||
d => d.graphStats.length
|
||||
)
|
||||
return {
|
||||
id: combinedProgramme ? `${studyprogramme}-${combinedProgramme}` : studyprogramme,
|
||||
years,
|
||||
tableStats: createTableStats(reversedYears, includeAllSpecials, majors, nonMajors, transferred, nonDegree),
|
||||
graphStats: createGraphStats(majors, nonMajors, nonDegree, transferred),
|
||||
tableStatsSecondProg: dataFoundSecondProg
|
||||
? createTableStats(
|
||||
reversedYears,
|
||||
includeAllSpecials,
|
||||
majorsSecondProg,
|
||||
nonMajorsSecondProg,
|
||||
transferredSecondProg,
|
||||
nonDegreeSecondProg
|
||||
)
|
||||
: [],
|
||||
graphStatsSecondProg: dataFoundSecondProg
|
||||
? createGraphStats(majorsSecondProg, nonMajorsSecondProg, nonDegreeSecondProg, transferredSecondProg)
|
||||
: [],
|
||||
titles,
|
||||
}
|
||||
return { stats, id: programmeCode }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCreditStatsForStudytrack,
|
||||
getProgrammeCreditStats,
|
||||
computeProgrammeCreditStats,
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ const checkStartdate = async (id, startdate) => {
|
||||
const addGraduation = async (studyright, isAcademicYear, amounts, times) => {
|
||||
const startdate = await checkStartdate(studyright.studyrightid, studyright.startdate)
|
||||
const graduationYear = defineYear(studyright.enddate, isAcademicYear)
|
||||
|
||||
const totalTimeToGraduation = moment(studyright.enddate).diff(moment(startdate), 'months')
|
||||
const statutoryAbsences = await getStatutoryAbsences(studyright.studentNumber, startdate, studyright.enddate)
|
||||
const timeToGraduation = totalTimeToGraduation - statutoryAbsences
|
||||
@@ -101,13 +100,13 @@ const getThesisStats = async ({ studyprogramme, since, years, isAcademicYear, in
|
||||
}
|
||||
|
||||
const getGraduationTimeStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
|
||||
let graduationAmounts = getYearsObject({ years })
|
||||
let graduationTimes = getYearsObject({ years, emptyArrays: true })
|
||||
const graduationAmounts = getYearsObject({ years })
|
||||
const graduationTimes = getYearsObject({ years, emptyArrays: true })
|
||||
if (!studyprogramme) return { times: { medians: [], goal: 0 }, doCombo: false, comboTimes: { medians: [], goal: 0 } }
|
||||
// for bc+ms combo
|
||||
const doCombo = studyprogramme.startsWith('MH') && !['MH30_001', 'MH30_003'].includes(studyprogramme)
|
||||
let graduationAmountsCombo = getYearsObject({ years })
|
||||
let graduationTimesCombo = getYearsObject({ years, emptyArrays: true })
|
||||
const graduationAmountsCombo = getYearsObject({ years })
|
||||
const graduationTimesCombo = getYearsObject({ years, emptyArrays: true })
|
||||
|
||||
const studentnumbers = await getCorrectStudentnumbers({
|
||||
codes: [studyprogramme],
|
||||
@@ -116,11 +115,13 @@ const getGraduationTimeStats = async ({ studyprogramme, since, years, isAcademic
|
||||
includeAllSpecials,
|
||||
includeTransferredTo: includeAllSpecials,
|
||||
})
|
||||
|
||||
const studyrights = await graduatedStudyRights(studyprogramme, since, studentnumbers)
|
||||
|
||||
// for masters, separate bc+ms combo from just ms students (except some medical progs)
|
||||
for (const right of studyrights) {
|
||||
const elements = right.studyrightElements.filter(
|
||||
element => new Date(element.enddate).toDateString() === new Date(right.enddate).toDateString()
|
||||
)
|
||||
if (elements.length === 0) continue
|
||||
if (doCombo && right.studyrightid.slice(-2) === '-2') {
|
||||
await addGraduation(right, isAcademicYear, graduationAmountsCombo, graduationTimesCombo)
|
||||
} else {
|
||||
@@ -161,11 +162,11 @@ const getTransferStudyrightMap = async (studyprogramme, since) => {
|
||||
const formatStats = (stats, years) => {
|
||||
const tableStats = Object.values(stats)
|
||||
.filter(p => years.map(year => p[year]).find(started => started !== 0)) // Filter out programmes with no-one started between the selected years
|
||||
.map(p => [p.code, getId(p.code), p.name['fi'], ...years.map(year => p[year])])
|
||||
.map(p => [p.code, getId(p.code), p.name.fi, ...years.map(year => p[year])])
|
||||
|
||||
const graphStats = Object.values(stats)
|
||||
.filter(p => years.map(year => p[year]).find(started => started !== 0)) // Filter out programmes with no-one started between the selected years
|
||||
.map(p => ({ name: p.name['fi'], code: p.code, data: years.map(year => p[year]) }))
|
||||
.map(p => ({ name: p.name.fi, code: p.code, data: years.map(year => p[year]) }))
|
||||
|
||||
return { tableStats, graphStats }
|
||||
}
|
||||
@@ -176,10 +177,12 @@ const getProgrammesBeforeStarting = async ({ studyprogramme, since, years, isAca
|
||||
|
||||
// Get studyrights for bachelor studyprogrammes. This excludes studytracks.
|
||||
const programmes = await getAllProgrammes()
|
||||
let studyrightsBeforeMasters = await previousStudyrights(programmes, allStudents)
|
||||
const studyrightsBeforeMasters = await previousStudyrights(programmes, allStudents)
|
||||
const studyprogrammeCodes = uniqBy(studyrightsBeforeMasters, 'code').map(s => ({ code: s.code, name: s.name }))
|
||||
const stats = {}
|
||||
studyprogrammeCodes.forEach(c => (stats[c.code] = { ...c, ...getYearsObject({ years }) }))
|
||||
studyprogrammeCodes.forEach(c => {
|
||||
stats[c.code] = { ...c, ...getYearsObject({ years }) }
|
||||
})
|
||||
|
||||
// Map transfers to the Master's programme
|
||||
const transfers = await getTransferStudyrightMap(studyprogramme, since)
|
||||
@@ -214,10 +217,12 @@ const getProgrammesAfterGraduation = async ({ studyprogramme, since, years, isAc
|
||||
|
||||
// Get studyrights for masters studyprogramme. This excludes studytracks.
|
||||
const programmes = await getAllProgrammes()
|
||||
let studyrightsAfterThisProgramme = await followingStudyrights(since, programmes, graduatedStudents)
|
||||
const studyrightsAfterThisProgramme = await followingStudyrights(since, programmes, graduatedStudents)
|
||||
const studyprogrammeCodes = uniqBy(studyrightsAfterThisProgramme, 'code').map(s => ({ code: s.code, name: s.name }))
|
||||
const stats = {}
|
||||
studyprogrammeCodes.forEach(c => (stats[c.code] = { ...c, ...getYearsObject({ years }) }))
|
||||
studyprogrammeCodes.forEach(c => {
|
||||
stats[c.code] = { ...c, ...getYearsObject({ years }) }
|
||||
})
|
||||
|
||||
const transfers = await getTransferStudyrightMap(studyprogramme, since)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const { getBasicStatsForStudytrack } = require('./studyprogrammeBasics')
|
||||
const { getCreditStatsForStudytrack } = require('./studyprogrammeCredits')
|
||||
const { getGraduationStatsForStudytrack } = require('./studyprogrammeGraduations')
|
||||
const { getStudytrackStatsForStudyprogramme } = require('./studytrackStats')
|
||||
const { setBasicStats, setCreditStats, setGraduationStats, setStudytrackStats } = require('../analyticsService')
|
||||
const logger = require('../../util/logger')
|
||||
const { computeProgrammeCreditStats } = require('./studyprogrammeCredits')
|
||||
|
||||
const updateBasicView = async (code, combinedProgramme) => {
|
||||
const specialCalendar = {
|
||||
@@ -37,15 +37,16 @@ const updateBasicView = async (code, combinedProgramme) => {
|
||||
})
|
||||
await setBasicStats(basicStats, option.yearType, option.specialGroups)
|
||||
|
||||
const creditStats = await getCreditStatsForStudytrack({
|
||||
studyprogramme: code,
|
||||
combinedProgramme,
|
||||
settings: {
|
||||
isAcademicYear: option.yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: option.specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
await setCreditStats(creditStats, option.yearType, option.specialGroups)
|
||||
const creditStats = await computeProgrammeCreditStats(
|
||||
code,
|
||||
option.yearType === 'ACADEMIC_YEAR',
|
||||
option.specialGroups === 'SPECIAL_INCLUDED'
|
||||
)
|
||||
await setCreditStats(
|
||||
creditStats,
|
||||
option.yearType === 'ACADEMIC_YEAR',
|
||||
option.specialGroups === 'SPECIAL_INCLUDED'
|
||||
)
|
||||
|
||||
const graduationStats = await getGraduationStatsForStudytrack({
|
||||
studyprogramme: code,
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
"quotes": ["error", "single", { "avoidEscape": true }],
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"no-console": "error",
|
||||
"no-param-reassign": ["error", { "props": false }]
|
||||
"no-param-reassign": ["error", { "props": false }],
|
||||
"no-continue": "off"
|
||||
},
|
||||
"plugins": ["react", "react-hooks"],
|
||||
"settings": {
|
||||
|
||||
@@ -10,4 +10,4 @@ COPY ./package* ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "start"]
|
||||
CMD ["node_modules/.bin/vite"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
Generated
+15
-123
@@ -11,7 +11,6 @@
|
||||
"@sentry/tracing": "^6.18.2",
|
||||
"axios": "^0.21.4",
|
||||
"chroma-js": "^2.4.2",
|
||||
"formik": "^2.2.9",
|
||||
"highcharts": "^9.3.3",
|
||||
"highcharts-custom-events": "^3.0.10",
|
||||
"immer": "^9.0.12",
|
||||
@@ -20,18 +19,16 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^6.14.1",
|
||||
"react": "^16.14.0",
|
||||
"react-datetime": "^3.1.1",
|
||||
"react-datetime": "^3.2.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-highcharts": "^16.1.0",
|
||||
"react-markdown": "^6.0.3",
|
||||
"react-range": "^1.8.12",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-reverse-portal": "^2.1.0",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-scroll-to-component": "^1.0.2",
|
||||
"scheduler": "^0.23.0",
|
||||
"semantic-ui-css": "^2.5.0",
|
||||
"semantic-ui-react": "^2.1.2",
|
||||
"semantic-ui-react": "^2.1.5",
|
||||
"unfuck-spa-shibboleth-session": "^3.1.4",
|
||||
"use-context-selector": "^1.3.9",
|
||||
"uuid": "^3.4.0",
|
||||
@@ -1149,40 +1146,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/component-clone": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/component-clone/-/component-clone-0.2.2.tgz",
|
||||
"integrity": "sha512-MHxnjmZLxTCVtmaHQ1LdjO5Hxx4d9akIzqGA8BDAGKGKkoB8Vf+oLEl9cwKkYln6BdfGwoXMU6xXGFnp1s/Eow==",
|
||||
"dependencies": {
|
||||
"component-type": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz",
|
||||
"integrity": "sha512-U8EviusIm8Fc5vMbs9opNX8r/hAz8PFYOu003AR1OVkCnDSTaBHB8inMn97yIbkGlI+dcdsItTBjgiZkVVzxYg=="
|
||||
},
|
||||
"node_modules/component-raf": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/component-raf/-/component-raf-1.2.0.tgz",
|
||||
"integrity": "sha512-MF1EHzsI2u04n8HGUW36E4bCzC0VqkJsRdbEcGU2hf5+EcnPLZckKS5F5V4E+XfPWjrBAP3Qp1TG79TbVGyd0A=="
|
||||
},
|
||||
"node_modules/component-tween": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/component-tween/-/component-tween-1.2.0.tgz",
|
||||
"integrity": "sha512-fQCubCyoosovh5VgnnnBsHnq6E0zjDpbAvW48t6glHIFWteowyP7cicPnzj9i3kuaFxI8P+zsSNao+26mQFG4A==",
|
||||
"dependencies": {
|
||||
"component-clone": "0.2.2",
|
||||
"component-emitter": "1.2.0",
|
||||
"component-type": "1.1.0",
|
||||
"ease-component": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/component-type": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/component-type/-/component-type-1.1.0.tgz",
|
||||
"integrity": "sha512-rLfU88D3tytip79EqPsLD+ND+Fl8HsIhz5KkOEeNidNVN1GUO9AWEs8+DPLtZR63BecAC883DuF2GZ1DM78BbA=="
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
@@ -1216,26 +1179,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==",
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
|
||||
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ease-component": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ease-component/-/ease-component-1.0.0.tgz",
|
||||
"integrity": "sha512-9Ouvcal2rQpKVw3uwVbs4BKyiQyA4LZD9VOjb+qor3jUPKEl/1hn60oKW6NEVj/Fw924skDsKXyCasPX4rRH5Q=="
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
|
||||
@@ -1301,9 +1251,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -1319,29 +1269,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/formik": {
|
||||
"version": "2.2.9",
|
||||
"resolved": "https://registry.npmjs.org/formik/-/formik-2.2.9.tgz",
|
||||
"integrity": "sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://opencollective.com/formik"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"deepmerge": "^2.1.1",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react-fast-compare": "^2.0.1",
|
||||
"tiny-warning": "^1.0.2",
|
||||
"tslib": "^1.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
@@ -1764,15 +1691,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-datetime": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-datetime/-/react-datetime-3.1.1.tgz",
|
||||
"integrity": "sha512-gHCTjAniCcMb6jdXpz+MpVe/uCeaHNDOofg+l41nLlJI3uBLBMV40CQbGB2TCTUpCzGT1mCs4vQzKGMjXO/WWQ==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-datetime/-/react-datetime-3.2.0.tgz",
|
||||
"integrity": "sha512-w5XdeNIGzBht9CadaZIJhKUhEcDTgH0XokKxGPCxeeJRYL7B3HIKA8CM6Q0xej2JFJt0n5d+zi3maMwaY3262A==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.5.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"moment": "^2.16.0",
|
||||
"react": "^16.5.0 || ^17.0.0"
|
||||
"react": "^16.5.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
@@ -1798,11 +1725,6 @@
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
|
||||
},
|
||||
"node_modules/react-highcharts": {
|
||||
"version": "16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-highcharts/-/react-highcharts-16.1.0.tgz",
|
||||
@@ -1923,15 +1845,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"node_modules/react-reverse-portal": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-reverse-portal/-/react-reverse-portal-2.1.1.tgz",
|
||||
"integrity": "sha512-FzuVLYEigKPB0NuMNLWymCgVp+P1h1MY57fQxhmY22idzz6El1rsXK5+bQ+wXvEa0smUtqTDcpM77epnXDV9wg==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz",
|
||||
@@ -1982,18 +1895,6 @@
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scroll-to-component": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-scroll-to-component/-/react-scroll-to-component-1.0.2.tgz",
|
||||
"integrity": "sha512-KPvTGta2E5hVnjt5aB37EIPep36ya95Juw0tC9Dc14Czu5C37qWIUYkjS+7ni0IQcEhq+0FncHtwtPxeZZQ5Bg==",
|
||||
"dependencies": {
|
||||
"scroll-to": "0.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^0.14.0 || ^15.0.0 || ^16.0.0",
|
||||
"react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz",
|
||||
@@ -2089,15 +1990,6 @@
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scroll-to": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/scroll-to/-/scroll-to-0.0.2.tgz",
|
||||
"integrity": "sha512-Jej5WQ8RvLFNQwCvJtiPfJ0wECCed5KdLnGPi1Cbd+djxcpwP1iInwhGAmzT87LTQcwxqLGd64zCX+1e0MmlLA==",
|
||||
"dependencies": {
|
||||
"component-raf": "1.2.0",
|
||||
"component-tween": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semantic-ui-css": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/semantic-ui-css/-/semantic-ui-css-2.5.0.tgz",
|
||||
@@ -2107,9 +1999,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semantic-ui-react": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/semantic-ui-react/-/semantic-ui-react-2.1.3.tgz",
|
||||
"integrity": "sha512-dd2pqalMk0ycJoHf7hn4mYd0xohg0NMk7ohlpPVOigCfMLrKk2e3NZdRk0yBetvD+qJXPqZ04jA43bDwqOM5OA==",
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/semantic-ui-react/-/semantic-ui-react-2.1.5.tgz",
|
||||
"integrity": "sha512-nIqmmUNpFHfovEb+RI2w3E2/maZQutd8UIWyRjf1SLse+XF51hI559xbz/sLN3O6RpLjr/echLOOXwKCirPy3Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.5",
|
||||
"@fluentui/react-component-event-listener": "~0.63.0",
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"@sentry/tracing": "^6.18.2",
|
||||
"axios": "^0.21.4",
|
||||
"chroma-js": "^2.4.2",
|
||||
"formik": "^2.2.9",
|
||||
"highcharts": "^9.3.3",
|
||||
"highcharts-custom-events": "^3.0.10",
|
||||
"immer": "^9.0.12",
|
||||
@@ -21,18 +20,16 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^6.14.1",
|
||||
"react": "^16.14.0",
|
||||
"react-datetime": "^3.1.1",
|
||||
"react-datetime": "^3.2.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-highcharts": "^16.1.0",
|
||||
"react-markdown": "^6.0.3",
|
||||
"react-range": "^1.8.12",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-reverse-portal": "^2.1.0",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-scroll-to-component": "^1.0.2",
|
||||
"scheduler": "^0.23.0",
|
||||
"semantic-ui-css": "^2.5.0",
|
||||
"semantic-ui-react": "^2.1.2",
|
||||
"semantic-ui-react": "^2.1.5",
|
||||
"unfuck-spa-shibboleth-session": "^3.1.4",
|
||||
"use-context-selector": "^1.3.9",
|
||||
"uuid": "^3.4.0",
|
||||
|
||||
@@ -15,6 +15,7 @@ export const studyProgrammeToolTips = {
|
||||
`,
|
||||
StudentsOfTheStudyprogramme: `
|
||||
**Started studying**: Sisältää kyseisenä vuonna aloittaneet opiskelijat (aloitetut opinto-oikeudet). Opiskelija on myös voinut myöhemmin keskeyttää opintonsa tai siirtyä pois ohjelmasta.\n
|
||||
|
||||
**Graduated**: Sisältää kyseisenä vuonna valmistuneet opiskelijat riippumatta siitä, minä vuonna opiskeluoikeus ohjelmaan on myönnetty. Jaottelu on tehty opinto-oikeuden päättymispäivän (=valmistumispäivän) perusteella.
|
||||
|
||||
**Transferred away**: Sisältää kyseisenä vuonna pois ohjelmasta siirtyneet opiskelijat (opinto-oikeudet). Sama opiskelija voi esiintyä tilastoissa useana vuonna, mikäli hän on siirtynyt ohjelmaan ja siitä pois useasti.
|
||||
@@ -24,16 +25,28 @@ export const studyProgrammeToolTips = {
|
||||
HUOM! Ohjelmaan siirtyneiden määrä on erityisen suuri vuonna 2020, jolloin vanhoista koulutusohjelmista luovuttiin ja opiskelijat siirtyivät uusiin ohjelmiin.
|
||||
`,
|
||||
CreditsProducedByTheStudyprogramme: `
|
||||
Sisältää opintopisteet suoritusvuosittain. Suoritukset on jaoteltu Sisussa näkyvän kurssin suorituspäivän mukaan.
|
||||
Sisältää opintopisteet suoritusvuosittain. Suoritukset on jaoteltu Sisussa näkyvän kurssin suorituspäivän mukaan. Kategoriat vastaavat Rapon kategorioita. Pistemäärät ovat osin epätarkkoja ennen vuotta 2022 sisu-oodi-erojen vuoksi.
|
||||
|
||||
**Major students' credits**: Sisältää ne opintopisteet, joiden suorittajalla on suoritushetkellä ollut ensisijainen opinto-oikeus kyseiseen koulutusohjelmaan. Sisältää myös ohjelmaan siirtyneet ensisijaiset opiskelijat.
|
||||
Total-kategoriaan ei lasketa mukaan hyväksiluettuja opintopisteitä.
|
||||
|
||||
**Non-major students' credits**: Sisältää koulutusohjelman tuottamat opintopisteet, joiden suorittajalla EI ole ollut suoritushetkellä ensisijaista opinto-oikeutta kyseiseen koulutusohjelmaan. Sisältää
|
||||
esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat. Sisältää myös opintopisteet, jotka opiskelija on suorittanut koulutusohjelmaan ennen opintojen aloittamista koulutusohjelmassa tai opintojen päätyttyä.
|
||||
**Degree students**: Niiden opiskelijoiden opintopisteet, joilla on oikeus suorittaa alempi tai ylempi korkeakoulututkinto.
|
||||
|
||||
**Open university**: Avoimen opiskeluoikeus, ei tutkinto-opiskelija: opintosuoritukset, joiden suorituksen luokittelu on "avoimena yliopisto-opintona suoritettu" ja opiskelija ei ole tutkinto-opiskelija tai kansainvälinen vaihto-opiskelija.
|
||||
|
||||
**Exchange students**: Saapuvat vaihto-opiskelijat: opintosuoritukset, jotka ovat saapuvan kansainvälisen opiskelijan suorittamia.
|
||||
Sisältää kaikki vaihto-opiskelijan opiskeluoikeuden aikana syntyneet opintopisteet.
|
||||
|
||||
**Transferred**: Hyväksiluetut opintopisteet
|
||||
|
||||
Lisäksi painamalla "Show special categories" -valintaa, saat näkyviin myös harvinaisemmat kategoriat:
|
||||
|
||||
**Special**:
|
||||
Opintosuoritukset, joiden suorituksen luokittelu on "erillisellä opiskeluoikeudella" tai "opettajankoulutuksen erillisellä opiskeluikeudella" suoritettu. Ei sisällä kansainvälisten vaihto-opiskelijoiden suorituksia.
|
||||
Ulkomailta hyväksiluetut opintopisteet: ulkomailta hyväksilutetut opintopisteet.
|
||||
|
||||
**Other university**:
|
||||
Korkeakoulujen väliset yhteistyöopinnot: opintosuoritukset, jotka on tehty korkeakoulujen väliseen yhteistyösopimukseen perustuvalla opiskeluoikeudella.
|
||||
|
||||
**Non-degree students**: Opiskelijat, joilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
|
||||
|
||||
**Transferred credits**: Sisältää opintopisteet, jotka on suoritettu kyseisenä vuonna, ja hyväksiluettu tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet. **Luku ei sisälly total-sarakkeeseen**.
|
||||
`,
|
||||
StudentsOfProgrammeCourses: `
|
||||
Sisältää kurssien opiskelijat.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.subRow {
|
||||
background-color: var(--lightSilver);
|
||||
}
|
||||
|
||||
.courseNameCell {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const AttemptsPane = ({ availableStats, datasets, separate, userHasAccess
|
||||
<Pane
|
||||
availableStats={availableStats}
|
||||
datasets={datasets}
|
||||
initialSettings={{ viewMode: 'ATTEMPTS', separate }}
|
||||
initialSettings={{ viewMode: 'ATTEMPTS', separate, splitDirection: 'row' }}
|
||||
settingsComponent={AttemptsTableSettings}
|
||||
tableComponent={AttemptsTable}
|
||||
userHasAccessToAllStats={userHasAccessToAllStats}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import { GradeDistributionChart } from './charts/GradeDistributionChart'
|
||||
import { DirectionToggle } from './settings/common/DirectionToggle'
|
||||
import { GradeDistributionChartSettings } from './settings/GradeDistributionChartSettings'
|
||||
import { DirectionToggle } from './common/DirectionToggle'
|
||||
import { PaneContent } from './common/PaneContent'
|
||||
|
||||
export const GradeDistributionPane = ({ datasets, userHasAccessToAllStats }) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const StudentsPane = ({ availableStats, datasets, separate, userHasAccess
|
||||
<Pane
|
||||
availableStats={availableStats}
|
||||
datasets={datasets}
|
||||
initialSettings={{ viewMode: 'STUDENTS', separate, showDetails: false }}
|
||||
initialSettings={{ viewMode: 'STUDENTS', separate, showDetails: false, splitDirection: 'row' }}
|
||||
settingsComponent={StudentsTableSettings}
|
||||
tableComponent={StudentsTable}
|
||||
userHasAccessToAllStats={userHasAccessToAllStats}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import { PassRateChart } from '../charts/PassRateChart'
|
||||
import { PassRateChartSettings } from '../settings/PassRateChartSettings'
|
||||
import { DirectionToggle } from './DirectionToggle'
|
||||
import { PaneContent } from './PaneContent'
|
||||
|
||||
/**
|
||||
@@ -21,28 +20,31 @@ export const Pane = ({
|
||||
updateQuery,
|
||||
}) => {
|
||||
const [settings, setSettings] = useState(initialSettings)
|
||||
const [splitDirection, setSplitDirection] = useState('row')
|
||||
|
||||
const setSplitDirection = splitDirection => {
|
||||
setSettings({ ...settings, splitDirection })
|
||||
}
|
||||
|
||||
const toggleSeparate = separate => {
|
||||
setSettings({ ...settings, separate })
|
||||
updateQuery(separate)
|
||||
}
|
||||
|
||||
const halfWidth = datasets.filter(dataset => dataset).length > 1 && splitDirection === 'row'
|
||||
const styleContainer = { display: 'flex', flexDirection: splitDirection, justifyContent: 'space-between' }
|
||||
const halfWidth = datasets.filter(dataset => dataset).length > 1 && settings.splitDirection === 'row'
|
||||
const styleContainer = { display: 'flex', flexDirection: settings.splitDirection, justifyContent: 'space-between' }
|
||||
const styleData = { flexGrow: 1, flexBasis: 1, maxWidth: halfWidth ? '49%' : '100%' }
|
||||
|
||||
return (
|
||||
<PaneContent>
|
||||
<div style={{ display: 'flex', marginBottom: '2em' }}>
|
||||
<div style={{ marginBottom: '1em' }}>
|
||||
<Settings
|
||||
availableStats={availableStats}
|
||||
datasets={datasets}
|
||||
onChange={setSettings}
|
||||
onSeparateChange={toggleSeparate}
|
||||
setSplitDirection={setSplitDirection}
|
||||
value={settings}
|
||||
/>
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
<DirectionToggle datasets={datasets} setSplitDirection={setSplitDirection} splitDirection={splitDirection} />
|
||||
</div>
|
||||
<div style={styleContainer}>
|
||||
{datasets
|
||||
|
||||
+45
-29
@@ -1,37 +1,53 @@
|
||||
import React from 'react'
|
||||
import { Menu, Radio } from 'semantic-ui-react'
|
||||
import { Radio, Segment, SegmentGroup } from 'semantic-ui-react'
|
||||
import { DirectionToggle } from './common/DirectionToggle'
|
||||
import { HelpButton } from './common/HelpButton'
|
||||
import { UnifyRadioButtons } from './common/UnifyRadioButtons'
|
||||
import { ProviderOrganization } from './common/ProviderOrganization'
|
||||
|
||||
export const AttemptsTableSettings = ({ value, onChange, availableStats, onSeparateChange }) => {
|
||||
const { showGrades, separate } = value
|
||||
export const AttemptsTableSettings = ({
|
||||
availableStats,
|
||||
datasets,
|
||||
onChange,
|
||||
onSeparateChange,
|
||||
setSplitDirection,
|
||||
value,
|
||||
}) => {
|
||||
const { showGrades, separate, splitDirection } = value
|
||||
|
||||
const elements = [
|
||||
<Radio
|
||||
checked={showGrades}
|
||||
data-cy="gradeToggle"
|
||||
label="Show grades"
|
||||
key="gradeToggle"
|
||||
onChange={() => onChange({ ...value, showGrades: !showGrades })}
|
||||
toggle
|
||||
/>,
|
||||
<Radio
|
||||
checked={separate}
|
||||
data-cy="separateToggle"
|
||||
label="Separate by semesters"
|
||||
key="separateToggle"
|
||||
onChange={() => onSeparateChange(!separate)}
|
||||
toggle
|
||||
/>,
|
||||
<ProviderOrganization availableStats={availableStats} key="providerOrganization" />,
|
||||
]
|
||||
|
||||
if (datasets.filter(dataset => dataset).length > 1) {
|
||||
elements.push(<DirectionToggle setSplitDirection={setSplitDirection} splitDirection={splitDirection} />)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu style={{ flexWrap: 'wrap' }} secondary>
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Show grades"
|
||||
data-cy="gradeToggle"
|
||||
checked={showGrades}
|
||||
onChange={() => onChange({ ...value, showGrades: !showGrades })}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Separate by semesters"
|
||||
data-cy="separateToggle"
|
||||
checked={separate}
|
||||
onChange={() => onSeparateChange(!separate)}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<HelpButton tab="Tables" viewMode="ATTEMPTS" />
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
<UnifyRadioButtons availableStats={availableStats} />
|
||||
<div style={{ alignItems: 'center', display: 'flex' }}>
|
||||
<SegmentGroup horizontal>
|
||||
{elements.map(element => (
|
||||
<Segment key={element.key} style={{ alignItems: 'center', display: 'flex' }}>
|
||||
{element}
|
||||
</Segment>
|
||||
))}
|
||||
</SegmentGroup>
|
||||
<HelpButton tab="Tables" viewMode="ATTEMPTS" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+53
-38
@@ -1,46 +1,61 @@
|
||||
import React from 'react'
|
||||
import { Menu, Radio } from 'semantic-ui-react'
|
||||
import { Radio, Segment, SegmentGroup } from 'semantic-ui-react'
|
||||
import { DirectionToggle } from './common/DirectionToggle'
|
||||
import { HelpButton } from './common/HelpButton'
|
||||
import { UnifyRadioButtons } from './common/UnifyRadioButtons'
|
||||
import { ProviderOrganization } from './common/ProviderOrganization'
|
||||
|
||||
export const StudentsTableSettings = ({ availableStats, onChange, onSeparateChange, value }) => {
|
||||
const { showDetails, showGrades, separate } = value
|
||||
export const StudentsTableSettings = ({
|
||||
availableStats,
|
||||
datasets,
|
||||
onChange,
|
||||
onSeparateChange,
|
||||
setSplitDirection,
|
||||
value,
|
||||
}) => {
|
||||
const { showDetails, showGrades, separate, splitDirection } = value
|
||||
|
||||
const elements = [
|
||||
<Radio
|
||||
checked={showDetails}
|
||||
data-cy="detailToggle"
|
||||
label="Show details"
|
||||
key="detailToggle"
|
||||
onChange={() => onChange({ ...value, showDetails: !showDetails })}
|
||||
toggle
|
||||
/>,
|
||||
<Radio
|
||||
checked={showGrades}
|
||||
data-cy="gradeToggle"
|
||||
label="Show grades"
|
||||
key="gradeToggle"
|
||||
onChange={() => onChange({ ...value, showGrades: !showGrades })}
|
||||
toggle
|
||||
/>,
|
||||
<Radio
|
||||
checked={separate}
|
||||
data-cy="separateToggle"
|
||||
label="Separate by semesters"
|
||||
key="separateToggle"
|
||||
onChange={() => onSeparateChange(!separate)}
|
||||
toggle
|
||||
/>,
|
||||
<ProviderOrganization availableStats={availableStats} key="providerOrganization" />,
|
||||
]
|
||||
|
||||
if (datasets.filter(dataset => dataset).length > 1) {
|
||||
elements.push(<DirectionToggle setSplitDirection={setSplitDirection} splitDirection={splitDirection} />)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu style={{ flexWrap: 'wrap' }} secondary>
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Show details"
|
||||
data-cy="detailToggle"
|
||||
checked={showDetails}
|
||||
onChange={() => onChange({ ...value, showDetails: !showDetails })}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Show grades"
|
||||
data-cy="gradeToggle"
|
||||
checked={showGrades}
|
||||
onChange={() => onChange({ ...value, showGrades: !showGrades })}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Separate by semesters"
|
||||
data-cy="separateToggle"
|
||||
checked={separate}
|
||||
onChange={() => onSeparateChange(!separate)}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<HelpButton tab="Tables" viewMode="STUDENTS" />
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
<UnifyRadioButtons availableStats={availableStats} />
|
||||
<div style={{ alignItems: 'center', display: 'flex' }}>
|
||||
<SegmentGroup horizontal>
|
||||
{elements.map(element => (
|
||||
<Segment key={element.key} style={{ alignItems: 'center', display: 'flex' }}>
|
||||
{element}
|
||||
</Segment>
|
||||
))}
|
||||
</SegmentGroup>
|
||||
<HelpButton tab="Tables" viewMode="STUDENTS" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,12 +1,10 @@
|
||||
import React from 'react'
|
||||
import { Icon, Menu } from 'semantic-ui-react'
|
||||
|
||||
export const DirectionToggle = ({ datasets, setSplitDirection, splitDirection }) => {
|
||||
if (datasets.filter(dataset => dataset).length <= 1) return null
|
||||
|
||||
export const DirectionToggle = ({ setSplitDirection, splitDirection }) => {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1em' }}>
|
||||
<label>Split direction: </label>
|
||||
<div style={{ alignItems: 'center', display: 'flex', flexDirection: 'column', gap: '1em' }}>
|
||||
<label>Split direction</label>
|
||||
<Menu style={{ margin: 0 }}>
|
||||
<Menu.Item active={splitDirection === 'row'} onClick={() => setSplitDirection('row')}>
|
||||
<Icon name="arrows alternate horizontal" />
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { Popup, Button, Icon } from 'semantic-ui-react'
|
||||
import { Button, Icon, Popup } from 'semantic-ui-react'
|
||||
import { courseStatisticsToolTips } from 'common/InfoToolTips'
|
||||
|
||||
export const HelpButton = ({ tab, viewMode }) => {
|
||||
@@ -17,7 +17,7 @@ export const HelpButton = ({ tab, viewMode }) => {
|
||||
on="click"
|
||||
wide="very"
|
||||
trigger={
|
||||
<Button icon labelPosition="left">
|
||||
<Button icon labelPosition="left" style={{ height: '50px', marginLeft: '30px' }}>
|
||||
<Icon name="question circle outline" />
|
||||
Help
|
||||
</Button>
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Checkbox, Segment, SegmentGroup } from 'semantic-ui-react'
|
||||
import { toggleOpenAndRegularCourses } from 'redux/coursesearch'
|
||||
|
||||
export const ProviderOrganization = ({ availableStats }) => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const openOrRegular = useSelector(state => state.courseSearch.openOrRegular)
|
||||
|
||||
const toggleValue = (_event, { value }) => {
|
||||
const options = {
|
||||
regularStats: {
|
||||
regularStats: 'openStats',
|
||||
openStats: 'unifyStats',
|
||||
unifyStats: 'openStats',
|
||||
},
|
||||
openStats: {
|
||||
openStats: 'regularStats',
|
||||
regularStats: 'unifyStats',
|
||||
unifyStats: 'regularStats',
|
||||
},
|
||||
}
|
||||
return dispatch(toggleOpenAndRegularCourses(options[value][openOrRegular]))
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ alignItems: 'center', display: 'flex', flexDirection: 'column' }}>
|
||||
<label style={{ marginBottom: '10px' }}>Provider organization(s)</label>
|
||||
<SegmentGroup horizontal style={{ margin: '0' }}>
|
||||
<Segment>
|
||||
<Checkbox
|
||||
checked={openOrRegular === 'regularStats' || openOrRegular === 'unifyStats'}
|
||||
data-cy="providerCheckboxUniversity"
|
||||
disabled={!availableStats.university}
|
||||
label={availableStats.university ? 'University' : 'University (no data)'}
|
||||
name="radioGroup"
|
||||
onChange={toggleValue}
|
||||
value="regularStats"
|
||||
/>
|
||||
</Segment>
|
||||
<Segment>
|
||||
<Checkbox
|
||||
checked={openOrRegular === 'openStats' || openOrRegular === 'unifyStats'}
|
||||
data-cy="providerCheckboxOpenUni"
|
||||
disabled={!availableStats.open}
|
||||
label={availableStats.open ? 'Open uni' : 'Open uni (no data)'}
|
||||
name="radioGroup"
|
||||
onChange={toggleValue}
|
||||
value="openStats"
|
||||
/>
|
||||
</Segment>
|
||||
</SegmentGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
import React from 'react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { Form, Menu, Radio } from 'semantic-ui-react'
|
||||
import { toggleOpenAndRegularCourses } from 'redux/coursesearch'
|
||||
|
||||
export const UnifyRadioButtons = ({ availableStats }) => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const openOrRegular = useSelector(state => state.courseSearch.openOrRegular)
|
||||
const toggleUnifyRadioValue = (event, { value }) => {
|
||||
dispatch(toggleOpenAndRegularCourses(value))
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu secondary style={{ marginLeft: '0.5rem' }}>
|
||||
<Form>
|
||||
<Form.Group>
|
||||
<Form.Field>
|
||||
<b>Provider organization:</b>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Radio
|
||||
label={availableStats.university ? 'university' : 'university (no data)'}
|
||||
name="radioGroup"
|
||||
value="regularStats"
|
||||
checked={openOrRegular === 'regularStats'}
|
||||
disabled={!availableStats.university}
|
||||
onChange={toggleUnifyRadioValue}
|
||||
data-cy="unify_radio_regular"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Radio
|
||||
label={availableStats.open ? 'open' : 'open (no data)'}
|
||||
name="radioGroup"
|
||||
value="openStats"
|
||||
checked={openOrRegular === 'openStats'}
|
||||
onChange={toggleUnifyRadioValue}
|
||||
disabled={!availableStats.open}
|
||||
data-cy="unify_radio_open"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Radio
|
||||
label={availableStats.unify ? 'both' : 'both (no data)'}
|
||||
name="radioGroup"
|
||||
value="unifyStats"
|
||||
checked={openOrRegular === 'unifyStats'}
|
||||
onChange={toggleUnifyRadioValue}
|
||||
disabled={!availableStats.unify}
|
||||
data-cy="unify_radio_unify"
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@media only screen and (min-width: 3240px) {
|
||||
@media only screen and (width >= 3240px) {
|
||||
.programmes-overview,
|
||||
.faculty-overview,
|
||||
.faculty-courses {
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 3240px) {
|
||||
@media only screen and (width >= 3240px) {
|
||||
.programmes-overview .section-container,
|
||||
.faculty-overview .section-container,
|
||||
.faculty-courses .section-container {
|
||||
@@ -155,7 +155,6 @@
|
||||
display: grid;
|
||||
grid-template-rows: repeat(4, 30%);
|
||||
grid-template-columns: repeat(3, 30%);
|
||||
row-gap: 0;
|
||||
column-gap: 8px;
|
||||
gap: 0 8px;
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
+2
-3
@@ -1,8 +1,7 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { Segment, Header, Button } from 'semantic-ui-react'
|
||||
import './filterTray.css'
|
||||
import { FilterViewContext } from '../FilterViewContext'
|
||||
import { FilterCard } from '../filters/common/FilterCard'
|
||||
import { FilterViewContext } from './FilterViewContext'
|
||||
import { FilterCard } from './filters/common/FilterCard'
|
||||
|
||||
export const FilterTray = () => {
|
||||
const {
|
||||
@@ -1,210 +0,0 @@
|
||||
/** #filter-tray is used repeatedly as a selector to achieve higher specificity
|
||||
over semantic-ui without using !important everywhere. */
|
||||
|
||||
.filter-tray-toggle {
|
||||
transform: rotate(-90deg);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.filter-tray-toggle .button {
|
||||
width: 100vh;
|
||||
border-radius: 0;
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 5rem;
|
||||
padding-right: 5rem;
|
||||
}
|
||||
|
||||
.filter-tray-toggle .button .button-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
letter-spacing: 0.06rem;
|
||||
}
|
||||
|
||||
.filter-tray-toggle .button .button-label .icon {
|
||||
margin-right: 0.6em;
|
||||
}
|
||||
|
||||
.filter-tray-toggle .button .label {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.filter-tray-toggle .button .button-label > .label {
|
||||
padding: 0.5em 0.7em 0.3em;
|
||||
margin-top: -2px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
#filter-ribbon-container {
|
||||
position: fixed;
|
||||
top: 19px;
|
||||
right: 253px;
|
||||
}
|
||||
|
||||
#filter-ribbon {
|
||||
transform: rotate(90deg);
|
||||
font-size: 10px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.filter-tray-toggle.inline-toggle button {
|
||||
background-color: #2d2e2e !important;
|
||||
}
|
||||
|
||||
.filter-tray-toggle.inline-toggle button:hover {
|
||||
background-color: #202121 !important;
|
||||
}
|
||||
|
||||
#filter-tray {
|
||||
background-color: #1b1c1d;
|
||||
height: 100vh;
|
||||
align-content: flex-start;
|
||||
justify-content: center;
|
||||
margin: 0 0 0 3rem;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding: 1.5rem 0 10rem;
|
||||
}
|
||||
|
||||
#filter-tray-container > .header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-height: 64px;
|
||||
padding: 1.1rem 1rem 1rem 1.7rem;
|
||||
border-bottom: 1px solid #a0a0a0;
|
||||
align-items: center;
|
||||
margin-bottom: 0 !important;
|
||||
margin-top: 63px;
|
||||
margin-left: 3rem;
|
||||
width: auto;
|
||||
color: white;
|
||||
order: -2;
|
||||
}
|
||||
|
||||
#filter-tray .comparator-input > .label {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#filter-tray .comparator-input > .label > .dropdown {
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
#filter-tray > .header > .content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#filter-tray > .header > .content > .label {
|
||||
margin-left: 1.5rem;
|
||||
padding-top: 0.7em;
|
||||
}
|
||||
|
||||
#filter-tray .segmentContainer {
|
||||
margin-left: 35px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#filter-tray .card {
|
||||
background-color: rgb(255 255 255 / 8%);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#filter-tray .card .card-header-text {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#filter-tray > .card > .content > .header {
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 0.8rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#filter-tray .card.active-filter {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
#filter-tray.cards .content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#filter-tray.cards .description,
|
||||
#filter-tray.cards .extra {
|
||||
padding: 0 0.8rem 0.8rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#filter-tray .card .extra {
|
||||
margin: 0 0 0 auto;
|
||||
padding-top: 0.8rem;
|
||||
}
|
||||
|
||||
#filter-tray .checkbox {
|
||||
margin-left: 0.8rem;
|
||||
}
|
||||
|
||||
#filter-tray label {
|
||||
color: #fff;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#filter-tray .card .labeled.input .label {
|
||||
min-width: 76px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#filter-close-button {
|
||||
background: #1b1c1d;
|
||||
}
|
||||
|
||||
#filter-tray .description-text {
|
||||
color: #9e9e9e;
|
||||
font-style: italic;
|
||||
margin: 1rem 0.2rem 0;
|
||||
}
|
||||
|
||||
#filter-tray .card-content {
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
#filter-tray .total-credits-filter .description {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
#filter-tray .total-credits-filter .form {
|
||||
position: relative;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
#filter-tray .button.toggle.active {
|
||||
background-color: #2085d0 !important;
|
||||
}
|
||||
|
||||
#filter-tray .flex-centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#filter-tray .or::before {
|
||||
color: #fff;
|
||||
background-color: #2e2e2f;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
#filter-tray .filter-option-count {
|
||||
color: #ababab;
|
||||
margin-left: 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#filter-tray .courses-filter .description {
|
||||
padding-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#filter-tray .course-card .header {
|
||||
color: #fff;
|
||||
padding: 0.7rem 0.5rem;
|
||||
padding-bottom: 0;
|
||||
font-size: 0.95rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#filter-tray .course-card .header > .button {
|
||||
margin: 0 0 0 0.5rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
#filter-tray .course-card .description {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
color: #fff;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.7rem 0.5rem;
|
||||
}
|
||||
|
||||
#filter-tray .course-card .description > .dropdown {
|
||||
margin-left: 0.8rem;
|
||||
}
|
||||
|
||||
#filter-tray .course-filter-selection {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.hidden-focusable {
|
||||
height: 0 !important;
|
||||
width: 0 !important;
|
||||
border: none !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { Dropdown } from 'semantic-ui-react'
|
||||
|
||||
import { CourseCard } from './CourseCard'
|
||||
import { FilterType } from './filterType'
|
||||
import './courseFilter.css'
|
||||
import { createFilter } from '../createFilter'
|
||||
import { useLanguage } from '../../../LanguagePicker/useLanguage'
|
||||
|
||||
|
||||
@@ -5,21 +5,63 @@ import { images, checkUserAccess } from '../../common'
|
||||
import { useTitle } from '../../common/hooks'
|
||||
import { Changelog } from './Changelog'
|
||||
|
||||
const FrontPageItem = ({ title, content }) => {
|
||||
return (
|
||||
<>
|
||||
<Header as="h3">{title}</Header>
|
||||
{content}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const FrontPage = () => {
|
||||
const { rights, roles } = useGetAuthorizedUserQuery()
|
||||
const { rights, roles, iamRights } = useGetAuthorizedUserQuery()
|
||||
const [showFullChangelog, setShowFullChangelog] = useState(false)
|
||||
|
||||
useTitle()
|
||||
|
||||
const showItems = {
|
||||
populations: roles.includes('admin') || rights.length > 0,
|
||||
studyProgramme: roles.includes('admin') || rights.length > 0,
|
||||
students: checkUserAccess(['studyGuidanceGroups', 'admin'], roles) || rights.length > 0,
|
||||
courseStatistics: checkUserAccess(['courseStatistics', 'admin'], roles) || rights.length > 0,
|
||||
teachers: checkUserAccess(['teachers', 'admin'], roles),
|
||||
university: true,
|
||||
feedback: true,
|
||||
}
|
||||
const items = [
|
||||
{
|
||||
show: true,
|
||||
title: 'University',
|
||||
content: <p>View tables and diagrams about study progress of different faculties</p>,
|
||||
},
|
||||
{
|
||||
show: roles.includes('admin') || rights.length > 0 || iamRights.length > 0,
|
||||
title: 'Programmes',
|
||||
content: (
|
||||
<List bulleted>
|
||||
<List.Item>
|
||||
<i>Class statistics:</i> View details of a specific year of a study programme
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<i>Overview:</i> View statistics of a programme across all years
|
||||
</List.Item>
|
||||
</List>
|
||||
),
|
||||
},
|
||||
{
|
||||
show: checkUserAccess(['courseStatistics', 'admin'], roles) || rights.length > 0,
|
||||
title: 'Courses',
|
||||
content: <p>View statistics about course attempts, completions and grades</p>,
|
||||
},
|
||||
{
|
||||
show: checkUserAccess(['studyGuidanceGroups', 'admin'], roles) || rights.length > 0,
|
||||
title: 'Students',
|
||||
content: <p>View detailed information for a given student</p>,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
title: 'Feedback',
|
||||
content: (
|
||||
<p>
|
||||
For questions and suggestions, please use the{' '}
|
||||
<a href="https://oodikone.helsinki.fi/feedback">feedback form</a> or shoot an email to{' '}
|
||||
<a href="mailto:grp-toska@helsinki.fi">grp-toska@helsinki.fi</a>.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -50,45 +92,15 @@ export const FrontPage = () => {
|
||||
<Grid columns="two" style={{ marginTop: '40px' }} divided>
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<Header as="h3">University</Header>
|
||||
<p>View tables and diagrams about study progress of different faculties.</p>
|
||||
{showItems.populations && (
|
||||
<>
|
||||
<Divider section />
|
||||
<Header as="h3">Study programme</Header>
|
||||
<List bulleted>
|
||||
<List.Item>
|
||||
<i>Search by Class:</i> Query a student population specified by a starting year and a study
|
||||
right. Oodikone will show you interactive statistics and visualizations for the population to
|
||||
be explored.
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<i>Overview:</i> View student progress and annual productivity for a given study programme.
|
||||
</List.Item>
|
||||
</List>
|
||||
</>
|
||||
{items.map(
|
||||
(item, index) =>
|
||||
item.show && (
|
||||
<React.Fragment key={item.title}>
|
||||
<FrontPageItem title={item.title} content={item.content} />
|
||||
{index !== items.length - 1 ? <Divider section /> : null}
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
{showItems.courseStatistics && (
|
||||
<>
|
||||
<Divider section />
|
||||
<Header as="h3">Courses</Header>
|
||||
<p>View statistics about course attempts, completions and grades.</p>
|
||||
</>
|
||||
)}
|
||||
{showItems.students && (
|
||||
<>
|
||||
<Divider section />
|
||||
<Header as="h3">Student statistics</Header>
|
||||
<p>View detailed information for a given student.</p>
|
||||
</>
|
||||
)}
|
||||
<Divider section />
|
||||
<Header as="h3">Feedback</Header>
|
||||
<p>
|
||||
For questions and suggestions, please use the{' '}
|
||||
<a href="https://oodikone.helsinki.fi/feedback">feedback form</a> or shoot an email to{' '}
|
||||
<a href="mailto:grp-toska@helsinki.fi">grp-toska@helsinki.fi</a>.
|
||||
</p>
|
||||
</Grid.Column>
|
||||
<Grid.Column style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Changelog showFullChangelog={false} />
|
||||
@@ -102,7 +114,7 @@ export const FrontPage = () => {
|
||||
)}
|
||||
</Container>
|
||||
<a href="https://toska.dev/">
|
||||
<Image src={images.toskaLogo} size="medium" centered style={{ bottom: 0 }} />
|
||||
<Image src={images.toskaLogo} alt="Logo of Toska" size="medium" centered style={{ bottom: 0 }} />
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
min-width: 600px !important;
|
||||
padding: 5px 0 5px 5px !important;
|
||||
margin: 0 5px 5px !important;
|
||||
background-color: var(--lightSilver) !important;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
}
|
||||
|
||||
.progressBarHidden {
|
||||
-webkit-animation-name: hide;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-name: hide;
|
||||
animation-duration: 2s;
|
||||
animation-fill-mode: forwards;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable no-continue */
|
||||
|
||||
import _ from 'lodash'
|
||||
import React, { useState, useMemo, useEffect } from 'react'
|
||||
import { Modal, Table, Button, Checkbox } from 'semantic-ui-react'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react'
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react'
|
||||
import { Icon, Card, Portal, Popup, Button } from 'semantic-ui-react'
|
||||
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal'
|
||||
|
||||
import './style.css'
|
||||
|
||||
@@ -61,54 +60,31 @@ const Content = ({ children, ...rest }) => {
|
||||
export const FigureContainer = ({ children, style }) => {
|
||||
const [isFullscreen, setFullscreen] = useState(false)
|
||||
|
||||
const portalNode = useMemo(
|
||||
() =>
|
||||
createHtmlPortalNode({
|
||||
attributes: {
|
||||
class: 'ui card fluid',
|
||||
},
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (isFullscreen) {
|
||||
document.body.classList.add('figure-fullscreen')
|
||||
portalNode.element.style = `
|
||||
position: sticky;
|
||||
margin: 0;
|
||||
inset: 0;
|
||||
height: 100vh;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
`
|
||||
} else {
|
||||
document.body.classList.remove('figure-fullscreen')
|
||||
portalNode.element.style = `
|
||||
overflow: hidden;
|
||||
maxHeight: 100%;
|
||||
`
|
||||
Object.assign(portalNode.element.style, style)
|
||||
}
|
||||
|
||||
return () => document.body.classList.remove('figure-fullscreen')
|
||||
}, [isFullscreen, portalNode])
|
||||
}, [isFullscreen])
|
||||
|
||||
return (
|
||||
<FigureContext.Provider value={{ isFullscreen, setFullscreen }}>
|
||||
<InPortal node={portalNode}>{children}</InPortal>
|
||||
{!isFullscreen && <OutPortal node={portalNode} />}
|
||||
<Portal
|
||||
open={isFullscreen}
|
||||
style={{
|
||||
width: '100% !important',
|
||||
height: '100% !important',
|
||||
}}
|
||||
>
|
||||
<div style={{ inset: '70px 0px 0px 0px', position: 'absolute', zIndex: 900, height: '100000vh' }}>
|
||||
{isFullscreen && <OutPortal node={portalNode} />}
|
||||
</div>
|
||||
</Portal>
|
||||
{isFullscreen ? (
|
||||
<Portal open={isFullscreen}>
|
||||
<div style={{ inset: '70px 0px 0px 0px', position: 'absolute', zIndex: 3, height: '10000vh' }}>
|
||||
<Card fluid style={{ position: 'sticky', margin: 0, inset: 0, height: '100vh', zIndex: 2 }}>
|
||||
{children}
|
||||
</Card>
|
||||
</div>
|
||||
</Portal>
|
||||
) : (
|
||||
<Card fluid style={style}>
|
||||
{children}
|
||||
</Card>
|
||||
)}
|
||||
</FigureContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ export const createHeaders = (columns, columnDepth, dispatch) => {
|
||||
const [column] = stack.splice(0, 1)
|
||||
|
||||
if (column.parent?.mergeHeader) {
|
||||
continue // eslint-disable-line
|
||||
continue
|
||||
}
|
||||
|
||||
const currentDepth = column.depth ?? 0
|
||||
@@ -257,7 +257,6 @@ export const tableStateReducer = (...args) =>
|
||||
state.expandedGroups.push(group)
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
})[type]()
|
||||
})
|
||||
|
||||
|
||||
@@ -112,19 +112,15 @@
|
||||
position: absolute;
|
||||
outline: 0;
|
||||
top: 100%;
|
||||
min-width: -webkit-max-content;
|
||||
min-width: -moz-max-content;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
font-size: 1em;
|
||||
text-shadow: none;
|
||||
text-align: left;
|
||||
-webkit-box-shadow: 0 2px 3px 0 rgb(34 36 38 / 15%);
|
||||
box-shadow: 0 2px 3px 0 rgb(34 36 38 / 15%);
|
||||
border: 1px solid rgb(34 36 38 / 15%);
|
||||
border-radius: 0.28571429rem;
|
||||
-webkit-transition: opacity 0.1s ease;
|
||||
transition: opacity 0.1s ease;
|
||||
z-index: 11;
|
||||
will-change: transform, opacity;
|
||||
@@ -144,7 +140,6 @@ i.filter.icon {
|
||||
position: static;
|
||||
overflow-y: auto;
|
||||
border: none;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0 !important;
|
||||
margin: 0 !important;
|
||||
@@ -172,9 +167,7 @@ i.filter.icon {
|
||||
font-size: 1rem;
|
||||
text-transform: none;
|
||||
font-weight: 400;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.filter-menu .item:hover {
|
||||
|
||||
+3
-1
@@ -9,7 +9,9 @@
|
||||
|
||||
.enrollment-label-no-margin {
|
||||
border-radius: 0.28rem;
|
||||
box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5;
|
||||
box-shadow:
|
||||
0 1px 3px 0 #d4d4d5,
|
||||
0 0 0 1px #d4d4d5;
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Hide year header from datetime picker in overview */
|
||||
.studyguidancegroupoverview__yeartagselector .rdtSwitch {
|
||||
.guidance-group-overview-year-tag-selector .rdtSwitch {
|
||||
display: none;
|
||||
}
|
||||
|
||||
+67
-42
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Link, useHistory } from 'react-router-dom'
|
||||
import { Formik } from 'formik'
|
||||
import { Form, Button, Icon, Modal } from 'semantic-ui-react'
|
||||
import { Form, Button, Icon, Modal, Message } from 'semantic-ui-react'
|
||||
import Datetime from 'react-datetime'
|
||||
|
||||
import { textAndDescriptionSearch } from 'common'
|
||||
@@ -46,53 +45,78 @@ const cellContent = { flexGrow: 1 }
|
||||
const EditTagModal = ({ group, tagName, toggleEdit, selectFieldItems, open }) => {
|
||||
const [changeStudyGuidanceGroupTags, { isLoading }] = useChangeStudyGuidanceGroupTagsMutation()
|
||||
const { getTextIn } = useLanguage()
|
||||
const initialState = { [tagName]: '' }
|
||||
const [formValues, setFormValues] = useState(initialState)
|
||||
const [formErrors, setFormErrors] = useState({})
|
||||
|
||||
const onSubmit = values => {
|
||||
changeStudyGuidanceGroupTags({ groupId: group.id, tags: values })
|
||||
toggleEdit()
|
||||
useEffect(() => {
|
||||
setFormValues(initialState)
|
||||
setFormErrors({})
|
||||
}, [group, tagName, open])
|
||||
|
||||
const validate = values => {
|
||||
const tags = { studyProgramme: 'Study programme', year: 'Starting year' }
|
||||
const errors = {}
|
||||
if (!values[tagName]) {
|
||||
errors[tagName] = `${tags[tagName]} is required!`
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
const handleChange = (name, value) => {
|
||||
setFormValues(prev => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
const errors = validate(formValues)
|
||||
setFormErrors(errors)
|
||||
|
||||
if (Object.keys(errors).length === 0) {
|
||||
changeStudyGuidanceGroupTags({ groupId: group.id, tags: formValues })
|
||||
toggleEdit()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal onClose={toggleEdit} open={open}>
|
||||
<Formik
|
||||
initialValues={{ [tagName]: '' }}
|
||||
onSubmit={values => onSubmit(values)}
|
||||
validate={values => (!values[tagName] ? { [tagName]: `${tagName} is required` } : {})}
|
||||
>
|
||||
{formik => (
|
||||
<>
|
||||
<Modal.Header>{getTextIn(group.name)}</Modal.Header>
|
||||
<Modal.Content>
|
||||
<AssociateTagForm group={group} tagName={tagName} selectFieldItems={selectFieldItems} formik={formik} />
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button content="Cancel" labelPosition="right" icon="trash" onClick={toggleEdit} negative />
|
||||
<Button
|
||||
content="Save"
|
||||
type="submit"
|
||||
labelPosition="right"
|
||||
icon="checkmark"
|
||||
onClick={formik.handleSubmit}
|
||||
disabled={isLoading}
|
||||
positive
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
<>
|
||||
<Modal.Header>{getTextIn(group.name)}</Modal.Header>
|
||||
<Modal.Content>
|
||||
<AssociateTagForm
|
||||
group={group}
|
||||
tagName={tagName}
|
||||
selectFieldItems={selectFieldItems}
|
||||
formValues={formValues}
|
||||
handleChange={handleChange}
|
||||
formErrors={formErrors}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button content="Cancel" labelPosition="right" icon="trash" onClick={toggleEdit} negative />
|
||||
<Button
|
||||
content="Save"
|
||||
type="submit"
|
||||
labelPosition="right"
|
||||
icon="checkmark"
|
||||
onClick={handleSubmit}
|
||||
disabled={isLoading}
|
||||
positive
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const AssociateTagForm = ({ group, tagName, selectFieldItems, formik }) => (
|
||||
const AssociateTagForm = ({ group, tagName, selectFieldItems, formValues, handleChange, formErrors }) => (
|
||||
<>
|
||||
<p>
|
||||
{tagName === 'studyProgramme'
|
||||
? 'Edit associated study programme for this group:'
|
||||
: 'Edit associated starting year for this group:'}
|
||||
</p>
|
||||
<Form onSubmit={formik.handleSubmit} style={{ ...cellWrapper, alignItems: 'center' }}>
|
||||
<div style={cellContent}>
|
||||
<Form style={{ alignItems: 'center' }}>
|
||||
<div>
|
||||
{tagName === 'studyProgramme' ? (
|
||||
<Form.Select
|
||||
name={tagName}
|
||||
@@ -103,16 +127,16 @@ const AssociateTagForm = ({ group, tagName, selectFieldItems, formik }) => (
|
||||
}
|
||||
options={selectFieldItems}
|
||||
closeOnChange
|
||||
value={formik.values[tagName]}
|
||||
onChange={(_, value) => formik.setFieldValue(tagName, value?.value)}
|
||||
value={formValues[tagName]}
|
||||
onChange={(_, { value }) => handleChange(tagName, value)}
|
||||
/>
|
||||
) : (
|
||||
<Datetime
|
||||
className="studyguidancegroupoverview__yeartagselector"
|
||||
className="guidance-group-overview-year-tag-selector"
|
||||
name={tagName}
|
||||
dateFormat="YYYY"
|
||||
timeFormat={false}
|
||||
initialvalue={group.tags?.[tagName]}
|
||||
initialValue={group.tags?.[tagName]}
|
||||
inputProps={{ readOnly: true }}
|
||||
renderYear={(props, year) => {
|
||||
const shiftBy = 2 // fix to start from 2017 instead of 2019
|
||||
@@ -121,7 +145,7 @@ const AssociateTagForm = ({ group, tagName, selectFieldItems, formik }) => (
|
||||
...props,
|
||||
key: props.key - shiftBy,
|
||||
className:
|
||||
`${formik.values[tagName]}` === formattedAndShiftedYear.substring(0, 4)
|
||||
`${formValues[tagName]}` === formattedAndShiftedYear.substring(0, 4)
|
||||
? 'rdtYear rdtActive'
|
||||
: 'rdtYear',
|
||||
'data-value': props['data-value'] - shiftBy,
|
||||
@@ -141,12 +165,13 @@ const AssociateTagForm = ({ group, tagName, selectFieldItems, formik }) => (
|
||||
)
|
||||
}}
|
||||
closeOnSelect
|
||||
value={formik.values[tagName]}
|
||||
onChange={value => formik.setFieldValue(tagName, value?.format('YYYY'))}
|
||||
value={formValues[tagName]}
|
||||
onChange={value => handleChange(tagName, value?.format('YYYY'))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
{formErrors[tagName] && <Message negative icon="exclamation circle" header={`${formErrors[tagName]}`} />}
|
||||
</>
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Divider, Loader, Message } from 'semantic-ui-react'
|
||||
|
||||
import { useGetBasicStatsQuery, useGetCreditStatsQuery, useGetGraduationStatsQuery } from 'redux/studyProgramme'
|
||||
import { studyProgrammeToolTips } from 'common/InfoToolTips'
|
||||
import { getGraduationGraphTitle } from 'common'
|
||||
import { CreditsProduced } from 'components/common/CreditsProduced'
|
||||
import { LineGraph } from './LineGraph'
|
||||
import { StackedBarChart } from './StackedBarChart'
|
||||
import { BarChart } from './BarChart'
|
||||
@@ -11,11 +13,21 @@ import { BreakdownBarChart } from '../BreakdownBarChart'
|
||||
import { DataTable } from './DataTable'
|
||||
import { Toggle } from '../Toggle'
|
||||
import { InfoBox } from '../../Info/InfoBox'
|
||||
|
||||
import { getGraduationGraphTitle } from '../../../common'
|
||||
import '../studyprogramme.css'
|
||||
|
||||
const getDivider = (title, toolTipText) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
</div>
|
||||
<InfoBox content={studyProgrammeToolTips[toolTipText]} />
|
||||
</>
|
||||
)
|
||||
|
||||
const isNewProgramme = code => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code)
|
||||
|
||||
const getGraduatedText = code => {
|
||||
if (code.slice(0, 1) === 'T' || code.slice(0, 3) === 'LIS') {
|
||||
return 'Graduated of the programme'
|
||||
@@ -34,27 +46,27 @@ export const BasicOverview = ({
|
||||
const [showMedian, setShowMedian] = useState(false)
|
||||
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
|
||||
const special = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
|
||||
const basics = useGetBasicStatsQuery({ id: studyprogramme, combinedProgramme, yearType, specialGroups: special })
|
||||
const credits = useGetCreditStatsQuery({ id: studyprogramme, combinedProgramme, yearType, specialGroups: special })
|
||||
const basics = useGetBasicStatsQuery({
|
||||
id: studyprogramme,
|
||||
combinedProgramme,
|
||||
yearType,
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
})
|
||||
const credits = useGetCreditStatsQuery({
|
||||
codes: [studyprogramme, combinedProgramme].filter(Boolean),
|
||||
isAcademicYear: academicYear,
|
||||
specialGroups: !specialGroups,
|
||||
})
|
||||
const graduations = useGetGraduationStatsQuery({
|
||||
id: studyprogramme,
|
||||
combinedProgramme,
|
||||
yearType,
|
||||
specialGroups: special,
|
||||
})
|
||||
|
||||
const doCombo = graduations?.data?.doCombo
|
||||
const timesData = graduations?.data?.graduationTimes
|
||||
const timesDataSecondProgramme = graduations?.data?.graduationTimesSecondProgramme
|
||||
const getDivider = (title, toolTipText) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
</div>
|
||||
<InfoBox content={studyProgrammeToolTips[toolTipText]} />
|
||||
</>
|
||||
)
|
||||
|
||||
const displayMedian = () => (
|
||||
<>
|
||||
@@ -167,42 +179,15 @@ export const BasicOverview = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{credits.isSuccess && credits.data && (
|
||||
{credits?.data?.stats?.[studyprogramme]?.stats && (
|
||||
<>
|
||||
{' '}
|
||||
{getDivider('Credits produced by the studyprogramme', 'CreditsProducedByTheStudyprogramme')}
|
||||
<div className="section-container">
|
||||
<StackedBarChart
|
||||
cypress="CreditsProducedByTheStudyprogramme"
|
||||
data={credits?.data?.graphStats}
|
||||
labels={credits?.data?.years}
|
||||
/>
|
||||
<DataTable
|
||||
cypress="CreditsProducedByTheStudyprogramme"
|
||||
data={credits?.data?.tableStats}
|
||||
titles={credits?.data?.titles}
|
||||
/>
|
||||
</div>
|
||||
{combinedProgramme && credits?.data?.graphStatsSecondProg?.length > 0 && (
|
||||
<>
|
||||
<h4>
|
||||
{combinedProgramme === 'MH90_001'
|
||||
? 'Credits produced by the licentiate programme'
|
||||
: 'Credits produced by the master programme'}
|
||||
</h4>
|
||||
<div className="section-container">
|
||||
<StackedBarChart
|
||||
cypress="CreditsProducedByTheSecondProg"
|
||||
data={credits?.data?.graphStatsSecondProg}
|
||||
labels={credits?.data?.years}
|
||||
/>
|
||||
<DataTable
|
||||
cypress="CreditsProducedByTheSecondProg"
|
||||
data={credits?.data?.tableStatsSecondProg}
|
||||
titles={credits?.data?.titles}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<CreditsProduced
|
||||
data={credits?.data?.stats?.[studyprogramme]?.stats}
|
||||
secondData={credits?.data?.stats?.[combinedProgramme]?.stats}
|
||||
academicYear={academicYear}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{graduations.isSuccess && graduations.data && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@media only screen and (min-width: 3240px) {
|
||||
@media only screen and (width >= 3240px) {
|
||||
.studytrack-overview,
|
||||
.studyprogramme-overview,
|
||||
.studyprogramme-courses {
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 3240px) {
|
||||
@media only screen and (width >= 3240px) {
|
||||
.studytrack-overview .section-container,
|
||||
.studyprogramme-overview .section-container,
|
||||
.studyprogramme-courses .section-container {
|
||||
@@ -14,6 +14,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.studyprogramme-courses {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { DataTable } from 'components/StudyProgramme/BasicOverview/DataTable'
|
||||
import { StackedBarChart } from 'components/StudyProgramme/BasicOverview/StackedBarChart'
|
||||
import { Toggle } from 'components/StudyProgramme/Toggle'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
const getFormattedYear = (year, academicYear) => (academicYear ? `${year} - ${year + 1}` : `${year}`)
|
||||
|
||||
const makeGraphData = (data, showAll, isAcademicYear) => {
|
||||
if (!data) return null
|
||||
const allKeys = [
|
||||
...new Set(
|
||||
Object.keys(data)
|
||||
.flatMap(year => Object.keys(data[year]))
|
||||
.filter(key => key !== 'total' && key !== 'other')
|
||||
.filter(key => showAll || !['agreement', 'special', 'separate'].includes(key))
|
||||
),
|
||||
]
|
||||
const names = {
|
||||
agreement: 'Other university',
|
||||
special: 'Special studyright: non-degree',
|
||||
separate: 'Separate studies',
|
||||
basic: 'Degree students',
|
||||
'open-uni': 'Open university',
|
||||
transferred: 'Transferred',
|
||||
'incoming-exchange': 'Exchange students',
|
||||
}
|
||||
const currentYear = new Date().getFullYear()
|
||||
const years = []
|
||||
const graphStats = []
|
||||
for (let year = 2017; year <= currentYear; year++) {
|
||||
allKeys.forEach(key => {
|
||||
if (!graphStats.find(k => k.name === names[key])) {
|
||||
graphStats.push({ name: names[key], data: [] })
|
||||
}
|
||||
graphStats.find(k => k.name === names[key]).data.push(data[getFormattedYear(year, isAcademicYear)]?.[key] || 0)
|
||||
})
|
||||
years.push(getFormattedYear(year, isAcademicYear))
|
||||
}
|
||||
return { data: graphStats, years }
|
||||
}
|
||||
|
||||
const makeTableStats = (data, showAll, isAcademicYear) => {
|
||||
if (!data) return null
|
||||
const currentYear = new Date().getFullYear()
|
||||
const tableStats = []
|
||||
for (let year = currentYear; year >= 2017; year--) {
|
||||
const yearData = data[getFormattedYear(year, isAcademicYear)]
|
||||
if (!yearData) continue
|
||||
const basic = yearData.basic || 0
|
||||
const openUni = yearData['open-uni'] || 0
|
||||
const exchange = yearData['incoming-exchange'] || 0
|
||||
const special = yearData.special || 0
|
||||
const other = yearData.other || 0
|
||||
const agreement = yearData.agreement || 0
|
||||
const transferred = yearData.transferred || 0
|
||||
const total = basic + openUni + special + other + agreement
|
||||
|
||||
/* TODO: Other-category missing for now, clarify what go in that, and fix those */
|
||||
const yearStats = [
|
||||
getFormattedYear(year, isAcademicYear),
|
||||
Math.round(total),
|
||||
Math.round(basic),
|
||||
Math.round(openUni),
|
||||
Math.round(exchange),
|
||||
Math.round(transferred),
|
||||
]
|
||||
if (showAll) yearStats.push(Math.round(special), Math.round(agreement))
|
||||
tableStats.push(yearStats)
|
||||
}
|
||||
const titles = ['', 'Total', 'Degree students', 'Open university', 'Exchange students', 'Transferred']
|
||||
if (showAll) titles.push('Special', 'Other university')
|
||||
return { data: tableStats, titles }
|
||||
}
|
||||
|
||||
export const CreditsProduced = ({ data, secondData, academicYear }) => {
|
||||
const [showAll, setShowAll] = useState(false)
|
||||
if (!data) return null
|
||||
|
||||
const tableStats = makeTableStats(data, showAll, academicYear)
|
||||
const creditGraphStats = makeGraphData(data, showAll, academicYear)
|
||||
|
||||
const secondTableStats = secondData ? makeTableStats(secondData, showAll, academicYear) : null
|
||||
const secondCreditGraphStats = secondData ? makeGraphData(secondData, showAll, academicYear) : null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Toggle
|
||||
cypress="showAllCreditsToggle"
|
||||
firstLabel="Show special categories"
|
||||
value={showAll}
|
||||
setValue={setShowAll}
|
||||
/>
|
||||
</div>
|
||||
<div className="section-container">
|
||||
<StackedBarChart
|
||||
cypress="CreditsProducedByTheStudyprogramme"
|
||||
data={creditGraphStats.data}
|
||||
labels={creditGraphStats.years}
|
||||
/>
|
||||
<DataTable cypress="CreditsProducedByTheStudyprogramme" data={tableStats.data} titles={tableStats.titles} />
|
||||
</div>
|
||||
{secondData && (
|
||||
<div>
|
||||
{' '}
|
||||
<h4>Credits produced by the licentiate programme</h4>
|
||||
<div className="section-container">
|
||||
<StackedBarChart
|
||||
cypress="CreditsProducedByTheStudyprogramme"
|
||||
data={secondCreditGraphStats.data}
|
||||
labels={secondCreditGraphStats.years}
|
||||
/>
|
||||
<DataTable
|
||||
cypress="CreditsProducedByTheStudyprogramme"
|
||||
data={secondTableStats.data}
|
||||
titles={secondTableStats.titles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useLocalStorage } from 'common/hooks'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import reactScrollToComponent from 'react-scroll-to-component'
|
||||
import { Accordion } from 'semantic-ui-react'
|
||||
|
||||
const titleStyle = { paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }
|
||||
@@ -18,7 +17,7 @@ export const PanelView = ({ panels: initialPanels, viewTitle }) => {
|
||||
const [newestIndex, setNewestIndex] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (newestIndex) reactScrollToComponent(refs.current[newestIndex], { align: 'bottom' })
|
||||
if (newestIndex) refs.current[newestIndex].scrollIntoView({ behavior: 'smooth', block: 'end' })
|
||||
}, [newestIndex])
|
||||
|
||||
const togglePanel = index => {
|
||||
|
||||
@@ -7,8 +7,10 @@ const studyprogrammeApi = RTKApi.injectEndpoints({
|
||||
`/v2/studyprogrammes/${id}/basicstats?year_type=${yearType}&special_groups=${specialGroups}&combined_programme=${combinedProgramme}`,
|
||||
}),
|
||||
getCreditStats: builder.query({
|
||||
query: ({ id, yearType, specialGroups, combinedProgramme }) =>
|
||||
`/v2/studyprogrammes/${id}/creditstats?year_type=${yearType}&special_groups=${specialGroups}&combined_programme=${combinedProgramme}`,
|
||||
query: ({ codes, isAcademicYear, specialGroups }) =>
|
||||
`/v2/studyprogrammes/creditstats?codes=${JSON.stringify(
|
||||
codes
|
||||
)}&isAcademicYear=${isAcademicYear}&includeSpecials=${specialGroups}`,
|
||||
}),
|
||||
getGraduationStats: builder.query({
|
||||
query: ({ id, yearType, specialGroups, combinedProgramme }) =>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@custom-media --mobile (max-width: 768px);
|
||||
|
||||
:root {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -14,7 +12,6 @@ html {
|
||||
}
|
||||
|
||||
.segmentContainer {
|
||||
align-content: center;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@@ -54,7 +51,6 @@ html {
|
||||
.fixed-header thead tr:first-child th,
|
||||
.fixed-header thead tr:nth-child(2) th {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background-color: white !important;
|
||||
|
||||
@@ -18,6 +18,6 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
outDir: 'build',
|
||||
sourcemap: true,
|
||||
sourcemap: 'hidden',
|
||||
},
|
||||
})
|
||||
|
||||
+4
-46
@@ -7,7 +7,6 @@
|
||||
"name": "sis-updater-scheduler",
|
||||
"dependencies": {
|
||||
"async": "^3.2.3",
|
||||
"axios": "^0.21.4",
|
||||
"cron": "^1.8.2",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
@@ -200,14 +199,6 @@
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -616,25 +607,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -671,27 +643,13 @@
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"node_modules/gelf-pro": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/gelf-pro/-/gelf-pro-1.3.3.tgz",
|
||||
"integrity": "sha512-gohbipAj/okE7pUgRSkIMEiqAS2M3q5YWnoejUDA9qwBeqbJr8LSwe3E7YW2QFK7hi4tZje1ZnKQe0296zdW/w==",
|
||||
"version": "1.3.11",
|
||||
"resolved": "https://registry.npmjs.org/gelf-pro/-/gelf-pro-1.3.11.tgz",
|
||||
"integrity": "sha512-y6DOxU40U4Sd+ECSLtMMtOTig1gEmnIHnvfocyvH+okc6LSfd7ADirT1tbzpAMzOXIgDG/2j9psZ1ck3HXfjEA==",
|
||||
"dependencies": {
|
||||
"async": "2.6.2",
|
||||
"lodash": "4.17.14"
|
||||
"lodash": "~4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/gelf-pro/node_modules/async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
|
||||
"integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.11"
|
||||
}
|
||||
},
|
||||
"node_modules/gelf-pro/node_modules/lodash": {
|
||||
"version": "4.17.14",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
|
||||
"integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw=="
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^3.2.3",
|
||||
"axios": "^0.21.4",
|
||||
"cron": "^1.8.2",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
@@ -23,5 +22,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.3"
|
||||
},
|
||||
"overrides": {
|
||||
"winston-gelf-transporter": {
|
||||
"gelf-pro": "^1.3.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ module.exports.EXIT_AFTER_IMMEDIATES = process.env.EXIT_AFTER_IMMEDIATES === 'ye
|
||||
module.exports.SCHEDULE_IMMEDIATE = process.env.SCHEDULE_IMMEDIATE?.split(',') || []
|
||||
module.exports.ENABLE_WORKER_REPORTING =
|
||||
module.exports.EXIT_AFTER_IMMEDIATES || process.env.ENABLE_WORKER_REPORTING === 'yes'
|
||||
module.exports.SLACK_WEBHOOK = process.env.SLACK_WEBHOOK
|
||||
|
||||
// Sentry
|
||||
module.exports.sentryRelease = process.env.SENTRY_RELEASE || ''
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const axios = require('axios').default
|
||||
const { NATS_GROUP, SIS_PURGE_CHANNEL, REDIS_LAST_PREPURGE_INFO } = require('./config')
|
||||
const { NATS_GROUP, SIS_PURGE_CHANNEL, REDIS_LAST_PREPURGE_INFO, SLACK_WEBHOOK } = require('./config')
|
||||
const { logger } = require('./utils/logger')
|
||||
const { stan, opts } = require('./utils/stan')
|
||||
const { set: redisSet, get: redisGet } = require('./utils/redis')
|
||||
@@ -28,12 +27,20 @@ let collectedPrePurgeTableData = {} // Collect data from nats, since synchronous
|
||||
let stanChannel // Channel is initialized once for purge
|
||||
|
||||
const sendToSlack = async text => {
|
||||
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK
|
||||
logger.info('Sending to slack', { text })
|
||||
if (!SLACK_WEBHOOK) return logger.info('SLACK_WEBHOOK environment variable must be set')
|
||||
|
||||
try {
|
||||
axios.post(SLACK_WEBHOOK, { text })
|
||||
const res = await fetch(SLACK_WEBHOOK, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ text }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to send to slack')
|
||||
}
|
||||
} catch (err) {
|
||||
logger.info('Failed to send to slack', { err: err.meta })
|
||||
}
|
||||
|
||||
+4
-18
@@ -454,27 +454,13 @@
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"node_modules/gelf-pro": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/gelf-pro/-/gelf-pro-1.3.3.tgz",
|
||||
"integrity": "sha512-gohbipAj/okE7pUgRSkIMEiqAS2M3q5YWnoejUDA9qwBeqbJr8LSwe3E7YW2QFK7hi4tZje1ZnKQe0296zdW/w==",
|
||||
"version": "1.3.11",
|
||||
"resolved": "https://registry.npmjs.org/gelf-pro/-/gelf-pro-1.3.11.tgz",
|
||||
"integrity": "sha512-y6DOxU40U4Sd+ECSLtMMtOTig1gEmnIHnvfocyvH+okc6LSfd7ADirT1tbzpAMzOXIgDG/2j9psZ1ck3HXfjEA==",
|
||||
"dependencies": {
|
||||
"async": "2.6.2",
|
||||
"lodash": "4.17.14"
|
||||
"lodash": "~4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/gelf-pro/node_modules/async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
|
||||
"integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.11"
|
||||
}
|
||||
},
|
||||
"node_modules/gelf-pro/node_modules/lodash": {
|
||||
"version": "4.17.14",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz",
|
||||
"integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw=="
|
||||
},
|
||||
"node_modules/get-package-type": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||
|
||||
@@ -21,5 +21,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.3"
|
||||
},
|
||||
"overrides": {
|
||||
"winston-gelf-transporter": {
|
||||
"gelf-pro": "^1.3.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,7 +535,10 @@ const updateEnrollments = async (enrollments, personIdToStudentNumber) => {
|
||||
studyRightIdToEducationType
|
||||
)
|
||||
|
||||
const mappedEnrollments = enrollments.filter(({ document_state }) => document_state === 'ACTIVE').map(mapEnrollment)
|
||||
const mappedEnrollments = enrollments
|
||||
.filter(({ document_state }) => document_state === 'ACTIVE')
|
||||
.map(mapEnrollment)
|
||||
.filter(({ studentnumber }) => studentnumber != null)
|
||||
await bulkCreate(Enrollment, mappedEnrollments)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user