mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7180dd5a34 | ||
|
|
e98df56fab | ||
|
|
1b7f60c4a2 | ||
|
|
6a85dd10a1 | ||
|
|
679175385f | ||
|
|
84cfde0c39 | ||
|
|
878b391aac | ||
|
|
53129e7683 | ||
|
|
27eb8984b5 | ||
|
|
e1f5bc5ec2 | ||
|
|
3698b8d38b | ||
|
|
850a429165 | ||
|
|
9e1c0751d3 | ||
|
|
414495590f | ||
|
|
915128dce6 | ||
|
|
8cb8e3c595 | ||
|
|
432fd7dbc0 | ||
|
|
b07070dc7f | ||
|
|
2ef6c2a47f | ||
|
|
67de743838 | ||
|
|
37f7e06ba5 | ||
|
|
7e798821c8 | ||
|
|
04692463f0 | ||
|
|
c3fecddc61 | ||
|
|
bed3e64e1f | ||
|
|
98cb0f5e3e | ||
|
|
d062788864 | ||
|
|
f2b19d5f74 | ||
|
|
e624152e3e | ||
|
|
d296508eb5 | ||
|
|
88b00beb5d | ||
|
|
a9057b949a | ||
|
|
d1744d9fb8 | ||
|
|
3e83b800d7 | ||
|
|
49891f3779 | ||
|
|
0aaa745763 | ||
|
|
e4edf31057 | ||
|
|
8c8b8ccb1d | ||
|
|
dc339896a1 | ||
|
|
da0f76e2a8 | ||
|
|
0fa9d8a1ca | ||
|
|
95066dde6e | ||
|
|
e17ff572bf | ||
|
|
4eb748f76d | ||
|
|
e6147effdf | ||
|
|
7e333668c5 | ||
|
|
45fb6659a9 | ||
|
|
0f73e5025b | ||
|
|
69f0b4cb75 | ||
|
|
2a96a72151 | ||
|
|
30c2cfbfa0 | ||
|
|
e8c3a77ba6 | ||
|
|
e15894c2f5 | ||
|
|
566fb861b0 | ||
|
|
80873a3227 | ||
|
|
809c63ada9 | ||
|
|
f0bf56f6c8 | ||
|
|
e4214ef871 | ||
|
|
88e6939347 | ||
|
|
dc73aabbd3 | ||
|
|
1dd41b0b9b | ||
|
|
328437ced2 | ||
|
|
f79bd9f2f9 | ||
|
|
84bc0434fd | ||
|
|
96045d0e17 | ||
|
|
129c608108 | ||
|
|
d73aa95684 | ||
|
|
fe9e9d2527 | ||
|
|
e5a52f3db6 | ||
|
|
84bb527e5e | ||
|
|
bd7bf5c712 | ||
|
|
b6d3106ee9 | ||
|
|
d7bc7d5d2e | ||
|
|
8eb3d58658 | ||
|
|
8c85467bcd | ||
|
|
f59f41cc3e | ||
|
|
678c90cbda | ||
|
|
7cee60596c | ||
|
|
979d24d375 | ||
|
|
cdf912db9b | ||
|
|
51867d840b | ||
|
|
51137fa66f | ||
|
|
a394928e83 | ||
|
|
c7edc70ccb | ||
|
|
2aa2c9419c | ||
|
|
5b9728dc1d | ||
|
|
8c73a32631 | ||
|
|
459c214164 | ||
|
|
96235ed242 | ||
|
|
45ce3a70c9 | ||
|
|
c101efb961 | ||
|
|
0d47ad6cc7 | ||
|
|
9a338b79d5 | ||
|
|
0087226de1 | ||
|
|
8276359359 | ||
|
|
53214e822e | ||
|
|
20c24f2722 | ||
|
|
9b88f6c008 | ||
|
|
c1bab2fb63 | ||
|
|
e6c23839e3 | ||
|
|
909448bad1 | ||
|
|
565b6eb11c | ||
|
|
bb69fcf908 |
@@ -21,7 +21,7 @@ jobs:
|
||||
# This steps extracts release version from GITHUB_REF env var
|
||||
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
|
||||
id: extract_version
|
||||
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
- name: 'Build and push'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
@@ -50,13 +50,13 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 'Get current time'
|
||||
id: get_current_time
|
||||
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
- name: 'Extract release version'
|
||||
# Github context (e.g. github.ref) only has ref formatted as /refs/tags/vX.X.X
|
||||
# This steps extracts release version from GITHUB_REF env var
|
||||
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
|
||||
id: extract_version
|
||||
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
- name: 'Build and push'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Get current time'
|
||||
id: get_current_time
|
||||
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
- name: 'Build image and export it'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
|
||||
@@ -2,3 +2,11 @@
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
|
||||
if git grep -qE --cached "(it|describe)\.only" $(git diff --cached --name-only) ./cypress; then
|
||||
echo "\e[0;31mDo not commit .only-tests."
|
||||
exit 1
|
||||
else
|
||||
echo "No .only-tests found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -135,4 +135,89 @@ describe('Faculty overview', () => {
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Average graduation times', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('td', 'H60').click()
|
||||
cy.contains('Graduation times').click()
|
||||
})
|
||||
|
||||
it('User can view graduation graphs', () => {
|
||||
cy.get('[data-cy="Section-AverageGraduationTimes"]').should('be.visible')
|
||||
cy.get('[data-cy="Section-bachelor"]').should('be.visible')
|
||||
cy.get('[data-cy="Section-master"]').should('be.visible')
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.contains('Graduation year').should('be.visible')
|
||||
cy.contains('.message', "Click a bar to view that year's programme level breakdown").should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
it('Graphs display data', () => {
|
||||
cy.get('[data-cy="Section-bachelor"]').within(() => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('1 graduated').should('have.length', 1)
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('1 students graduated in year 2019')
|
||||
cy.contains('median study time: 40 months')
|
||||
cy.contains('0 graduated on time')
|
||||
cy.contains('1 graduated max year overtime')
|
||||
|
||||
cy.contains('1 graduated').click()
|
||||
cy.contains('Year 2019 by graduation year')
|
||||
cy.get('div[class="programmes-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-graph"]').within(() => {
|
||||
cy.contains('EDUK')
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('Kasvatustieteiden kandiohjelma')
|
||||
cy.contains('KH60_001')
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('1 graduated').should('have.length', 1)
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('1 students graduated in year 2020')
|
||||
cy.contains('median study time: 25 months')
|
||||
cy.contains('0 graduated over year late')
|
||||
cy.contains('1 graduated max year overtime')
|
||||
|
||||
cy.contains('1 graduated').click()
|
||||
cy.contains('Year 2020 by graduation year')
|
||||
cy.get('div[class="programmes-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-graph"]').within(() => {
|
||||
cy.contains('EDUM')
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('Kasvatustieteiden maisteriohjelma')
|
||||
cy.contains('MH60_001')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Graduation times grouping and time types can be toggled', () => {
|
||||
cy.get('[data-cy="GroupByToggle"]').click()
|
||||
cy.get('[data-cy="Section-bachelor"]').should('not.exist')
|
||||
cy.get('[data-cy="Section-master"]').should('be.visible')
|
||||
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('1 graduated (100 % of class)').should('have.length', 1)
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('From class of 2018, 1/undefined students have graduated')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.contains('1 graduated (100 % of class)').click()
|
||||
cy.get('div[class="programmes-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-graph"]').within(() => {
|
||||
cy.contains('Year 2018 by start year')
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('From class of 2018, 1/1 students have graduated')
|
||||
cy.contains('mean study time: 25 months')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/// <reference types="Cypress" />
|
||||
// Now "Class statistics" in UI
|
||||
|
||||
const moment = require('moment')
|
||||
const _ = require('lodash')
|
||||
@@ -58,7 +59,7 @@ describe('Population Statistics tests', () => {
|
||||
.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
.click()
|
||||
cy.contains('See class').click()
|
||||
cy.contains('Class statistics')
|
||||
cy.contains('class size')
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
})
|
||||
|
||||
@@ -68,10 +69,11 @@ describe('Population Statistics tests', () => {
|
||||
// This advanced settings does not work correctly
|
||||
// see https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3552
|
||||
// setPopStatsUntil('toukokuu 2020')
|
||||
|
||||
cy.get('.card').within(() => {
|
||||
cy.get('.header').within(() => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('Sample size: 170 students')
|
||||
cy.contains('class size 170 students')
|
||||
})
|
||||
cy.get('.card').within(() => {
|
||||
cy.contains('Excludes exchange students')
|
||||
cy.contains('Excludes students with non-degree study right')
|
||||
cy.contains('Excludes students who have transferred out of this programme')
|
||||
@@ -103,7 +105,6 @@ describe('Population Statistics tests', () => {
|
||||
it.skip('Advanced settings work', () => {
|
||||
cy.visit(pathToCSBach2017)
|
||||
cy.get('[data-cy=advanced-toggle]').click()
|
||||
cy.contains('Statistics until')
|
||||
// only spring
|
||||
cy.cs('toggle-fall').click()
|
||||
cy.contains('Fetch class').click()
|
||||
@@ -136,9 +137,8 @@ describe('Population Statistics tests', () => {
|
||||
cy.contains('Credit statistics').click()
|
||||
cy.contains('Credits Gained').click()
|
||||
cy.get("[data-cy='credits-gained-main-table']").should('contain', 'All students of the class')
|
||||
|
||||
const months = Math.ceil(moment.duration(moment().diff(moment('2017-08-1'))).asMonths())
|
||||
|
||||
const today = moment().endOf('month')
|
||||
const months = Math.round(moment.duration(moment(today).diff(moment('2017-08-01'))).asMonths())
|
||||
const limits = [1, ..._.range(1, 4).map(p => Math.ceil(months * ((p * 15) / 12))), null]
|
||||
const ranges = _.range(1, limits.length).map(i => _.slice(limits, i - 1, i + 1))
|
||||
|
||||
@@ -227,9 +227,10 @@ describe('Population Statistics tests', () => {
|
||||
it('Population statistics is visible', () => {
|
||||
cy.reload()
|
||||
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('class size 170 students')
|
||||
|
||||
cy.get('.card').within(() => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('Sample size: 170 students')
|
||||
cy.contains('Excludes exchange students')
|
||||
cy.contains('Excludes students with non-degree study right')
|
||||
cy.contains('Excludes students who have transferred out of this programme')
|
||||
|
||||
@@ -93,8 +93,8 @@ describe('Student Statistics tests', () => {
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.get('i.level.up.alternate.icon').eq(0).click()
|
||||
cy.contains('Class statistics')
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('class size')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -25,12 +25,14 @@ describe('Studyprogramme overview', () => {
|
||||
describe('Basic information -view works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.viewport(1536, 960)
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click({ force: true })
|
||||
// cy.viewport(1536, 960)
|
||||
})
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Basic information -tab loads', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
|
||||
@@ -39,7 +41,6 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Basic information contains correct students', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Started, Graduated, Transferred Away, Transferred to]
|
||||
@@ -55,23 +56,21 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Basic information contains correct credits', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
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, 5860, 5591, 102, 0, 167],
|
||||
[2019, 8696, 7737, 587, 23, 349],
|
||||
[2018, 7818, 6584, 857, 105, 272],
|
||||
[2017, 3085, 2492, 158, 47, 388],
|
||||
[2019, 8696, 7737, 577, 33, 349],
|
||||
[2018, 7818, 6584, 852, 110, 272],
|
||||
[2017, 3085, 2492, 153, 52, 388],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
})
|
||||
|
||||
it('Basic information contains correct thesis writers and graduates', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
@@ -87,7 +86,6 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it.skip('Special studyrights can be excluded and basic data changes accordingly', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen maisteriohjelma').click()
|
||||
cy.get('[data-cy=StudentToggle]').click()
|
||||
const years = getEmptyYears()
|
||||
const studentTableContents = [
|
||||
@@ -129,39 +127,39 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Year can be changed to academic year, and data changes accordingly', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen maisteriohjelma').click()
|
||||
cy.get('[data-cy=YearToggle]').click()
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.get('[data-cy=YearToggle]').click({ force: true })
|
||||
const isAcademicYear = true
|
||||
const years = getEmptyYears(isAcademicYear)
|
||||
const studentTableContents = [
|
||||
// [Year, Started, Graduated, Transferred away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0],
|
||||
['2020 - 2021', 5, 0, 0, 0],
|
||||
['2019 - 2020', 27, 0, 0, 0],
|
||||
['2018 - 2019', 4, 0, 0, 0],
|
||||
['2017 - 2018', 0, 0, 0, 0],
|
||||
['2020 - 2021', 12, 6, 3, 2],
|
||||
['2019 - 2020', 86, 41, 1, 0],
|
||||
['2018 - 2019', 162, 5, 0, 1],
|
||||
['2017 - 2018', 171, 1, 0, 0],
|
||||
]
|
||||
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
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]),
|
||||
['2021 - 2022', 0, 0, 0, 0],
|
||||
['2020 - 2021', 0, 0, 0, 0],
|
||||
['2019 - 2020', 338.5, 177, 154, 0, 7.5],
|
||||
['2018 - 2019', 173, 0, 173, 0, 0],
|
||||
['2017 - 2018', 42, 0, 42, 0, 0],
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0, 0],
|
||||
['2020 - 2021', 566, 486, 20, 0, 60],
|
||||
['2019 - 2020', 8595, 8271, 106, 0, 218],
|
||||
['2018 - 2019', 9992, 8855, 781, 53, 303],
|
||||
['2017 - 2018', 6055, 4792, 777, 142, 344],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
//cy.wait(10000)
|
||||
cy.get('[data-cy=YearToggle]').click()
|
||||
})
|
||||
|
||||
it('Basic information graphs render', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
|
||||
cy.get('[data-cy=Graph-StudentsOfTheStudyprogramme')
|
||||
.should('contain', 'Started')
|
||||
.should('contain', 'Graduated')
|
||||
@@ -171,8 +169,8 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', 'Major students credits')
|
||||
.should('contain', 'Non-major students credits')
|
||||
.should('contain', 'Transferred credits')
|
||||
.should('contain', 857)
|
||||
.should('contain', 587)
|
||||
.should('contain', 852)
|
||||
.should('contain', 577)
|
||||
|
||||
cy.get('[data-cy=Graph-GraduatedAndThesisWritersOfTheProgramme')
|
||||
.should('contain', 'Graduated students')
|
||||
@@ -183,10 +181,14 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', 25)
|
||||
.should('contain', 35)
|
||||
|
||||
cy.get('[data-cy=Graph-2019-AverageGraduationTimes]')
|
||||
.should('contain', '2019')
|
||||
.should('contain', 'n = 17')
|
||||
.should('contain', '23 kk')
|
||||
cy.get('[data-cy=graduation-times-graph]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2019')
|
||||
cy.contains('17 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 23 months')
|
||||
cy.contains('17 graduated on time')
|
||||
cy.contains('0 graduated max year overtime')
|
||||
})
|
||||
|
||||
cy.get('[data-cy=Graph-ProgrammesBeforeOrAfter')
|
||||
.should('contain', 'Tietojenkäsittelytieteen maisteriohjelma')
|
||||
@@ -198,24 +200,47 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Graduation times of master programmes', () => {
|
||||
it('are split into two graphs', () => {
|
||||
cy.init('/study-programme', 'admin')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(5000)
|
||||
cy.contains('a', 'Kasvatustieteiden maisteriohjelma').click({ force: true })
|
||||
cy.get('[data-cy=graduation-times-graphMaster]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2020')
|
||||
cy.contains('24 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 22 months')
|
||||
cy.contains('20 graduated on time')
|
||||
cy.contains('4 graduated max year overtime')
|
||||
})
|
||||
|
||||
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2020')
|
||||
cy.contains('0 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 0 months')
|
||||
cy.contains('0 graduated on time')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* Studytrack overview -tests*/
|
||||
describe('Studytrack overview works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
})
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Studytracks and student populations -tab loads', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
cy.get('[data-cy=Section-StudytrackOverview]')
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
|
||||
})
|
||||
|
||||
it('Students of the studyprogramme are shown correctly', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
const tableContents = [
|
||||
// [Year, All, Started studying, Currently enrolled, Absent, Cancelled, Graduated, Men, Women, Finnish]
|
||||
['2020 - 2021', 12, 10, 0, 0, 0, 0, 10, 2, 12],
|
||||
@@ -229,8 +254,6 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Student progress data is shown correctly', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
const tableContents = [
|
||||
['2020 - 2021', 12, 12, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2019 - 2020', 86, 11, 15, 35, 20, 5, 0, 0, 0],
|
||||
@@ -243,9 +266,6 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Studytrack overview graphs render', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
|
||||
cy.get('[data-cy=Graph-StudytrackProgress]')
|
||||
.should('contain', 'Less than 15 credits')
|
||||
.should('contain', '30-59 credits')
|
||||
@@ -254,10 +274,13 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', '5.6%') // The percentage for 15-29 credits in 2018-2019
|
||||
.should('contain', '12.8%') // The percentage for less than 15 credits in 2019-2020
|
||||
|
||||
cy.get('[data-cy=Graph-2018-AverageGraduationTimes]')
|
||||
.should('contain', '2018 - 2019')
|
||||
.should('contain', 'n = 11 / 162')
|
||||
.should('contain', '22 kk')
|
||||
cy.get('[data-cy=graduation-times-graph]').within(() => {
|
||||
cy.contains('Start year')
|
||||
cy.contains('2019 - 2020')
|
||||
cy.contains('42 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 31.5 months')
|
||||
cy.contains('39 graduated on time')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -285,16 +308,21 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('calendar year -> academic year toggle works', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.get('[data-cy=fromYear]').click().contains('2018').click()
|
||||
|
||||
cy.get('[data-cy=toYear').click().contains('2019').click()
|
||||
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(20000)
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
cy.get('tr').eq(1).contains('684')
|
||||
})
|
||||
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(20000)
|
||||
cy.get('[data-cy=calendarAcademicYearToggle]').first().click()
|
||||
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(20000)
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
cy.get('tr').eq(1).contains('772')
|
||||
})
|
||||
@@ -336,12 +364,12 @@ describe('Studyprogramme overview', () => {
|
||||
describe('Basic information -view works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
})
|
||||
|
||||
it('can create and delete tags for population', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
cy.contains('Create new tag').click()
|
||||
@@ -352,8 +380,6 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
it('can create personal tags', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
|
||||
@@ -367,9 +393,6 @@ describe('Studyprogramme overview', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
|
||||
const student = '010113437'
|
||||
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
cy.contains('Create new tag').click()
|
||||
@@ -390,7 +413,8 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
cy.go('back')
|
||||
cy.go('back')
|
||||
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
deleteTag(name)
|
||||
|
||||
cy.contains('Student statistics').click()
|
||||
@@ -434,7 +458,7 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
cy.get('[data-cy=Section-StudytrackOverview]')
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -339,6 +339,8 @@ const doctoralSchoolMap = {
|
||||
|
||||
const opetusVaradekaani = 'hy-varadekaanit-opetus'
|
||||
|
||||
const universityServices = 'hy-ypa-opa-dojo'
|
||||
|
||||
const dekaaniFacultyMap = {
|
||||
'hy-ttdk-dekanaatti': 'teologinen',
|
||||
'hy-oiktdk-dekanaatti': 'oikeustieteellinen',
|
||||
@@ -411,6 +413,7 @@ const relevantIAMs = []
|
||||
.concat(Object.keys(kosuFacultyMap))
|
||||
.concat(Object.keys(dekaaniFacultyMap))
|
||||
.concat(opetusVaradekaani)
|
||||
.concat(universityServices)
|
||||
.concat(doctoralIams)
|
||||
.concat(Object.keys(doctoralSchoolMap))
|
||||
.concat(universityWideGroups)
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
const { BIGINT, STRING, DATE, ARRAY } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.createTable('open_uni_population_searches', {
|
||||
id: {
|
||||
type: BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
user_id: {
|
||||
type: BIGINT,
|
||||
},
|
||||
name: {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
course_codes: {
|
||||
type: ARRAY(STRING),
|
||||
},
|
||||
created_at: DATE,
|
||||
updated_at: DATE,
|
||||
})
|
||||
},
|
||||
down: () => {},
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
up: queryInterface => {
|
||||
return queryInterface.bulkInsert('access_groups', [
|
||||
{
|
||||
group_code: 'openUniSearch',
|
||||
group_info: 'grants access to custom open uni course searches',
|
||||
},
|
||||
])
|
||||
},
|
||||
down: queryInterface => {
|
||||
return queryInterface.bulkDelete('access_groups', [
|
||||
{
|
||||
group_code: 'openUniSearch',
|
||||
},
|
||||
])
|
||||
},
|
||||
}
|
||||
@@ -5,6 +5,7 @@ const { requiredGroup } = require('../conf-backend')
|
||||
const _ = require('lodash')
|
||||
const { relevantIAMs } = require('../../config/IAMConfig')
|
||||
const { getOrganizationAccess } = require('../util/organizationAccess')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const parseIamGroups = iamGroups => iamGroups?.split(';').filter(Boolean) ?? []
|
||||
|
||||
@@ -32,6 +33,7 @@ const currentUserMiddleware = async (req, _res, next) => {
|
||||
const iamRights = Object.keys(await getOrganizationAccess({ iamGroups }))
|
||||
|
||||
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
|
||||
logger.error({ message: 'User does not have required iam group', meta: { iamGroups, iamRights } })
|
||||
throw new ApplicationError('User does not have required iam group', 403, { logoutUrl })
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,28 @@ const CustomPopulationSearch = sequelizeKone.define('custom_population_searches'
|
||||
},
|
||||
})
|
||||
|
||||
const OpenUniPopulationSearch = sequelizeKone.define(
|
||||
'open_uni_population_searches',
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true,
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.BIGINT,
|
||||
},
|
||||
name: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
courseCodes: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
},
|
||||
{ underscored: true, timestamps: true }
|
||||
)
|
||||
|
||||
const StudyGuidanceGroupTag = sequelizeKone.define(
|
||||
'study_guidance_group_tags',
|
||||
{
|
||||
@@ -162,6 +184,7 @@ module.exports = {
|
||||
ThesisTypeEnums,
|
||||
MandatoryCourseLabels,
|
||||
CustomPopulationSearch,
|
||||
OpenUniPopulationSearch,
|
||||
sequelizeKone,
|
||||
ExcludedCourse,
|
||||
StudyGuidanceGroupTag,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { Model, STRING, DATE, INTEGER } = require('sequelize')
|
||||
const { Model, STRING, DATE, INTEGER, BOOLEAN } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class Student extends Model {}
|
||||
@@ -16,7 +16,9 @@ Student.init(
|
||||
creditcount: { type: INTEGER },
|
||||
dateofuniversityenrollment: { type: DATE },
|
||||
email: { type: STRING },
|
||||
secondary_email: { type: STRING },
|
||||
national_student_number: { type: STRING },
|
||||
phone_number: { type: STRING },
|
||||
country_fi: { type: STRING },
|
||||
country_sv: { type: STRING },
|
||||
country_en: { type: STRING },
|
||||
@@ -25,6 +27,7 @@ Student.init(
|
||||
home_country_en: { type: STRING },
|
||||
gender_code: { type: INTEGER },
|
||||
sis_person_id: { type: STRING },
|
||||
dissemination_info_allowed: { type: BOOLEAN },
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ const trends = require('./routes/trends')
|
||||
const programmeModules = require('./routes/programmeModules')
|
||||
const studyGuidanceGroups = require('./routes/studyGuidanceGroups')
|
||||
const studyProgramme = require('./routes/studyProgramme')
|
||||
const customOpenUniSearch = require('./routes/customOpenUniSearch')
|
||||
const initializeSentry = require('./util/sentry')
|
||||
|
||||
const errorMiddleware = require('./middleware/errorMiddleware')
|
||||
@@ -57,6 +58,7 @@ module.exports = (app, url) => {
|
||||
app.use(url, tags)
|
||||
app.use(url, programmeModules)
|
||||
app.use(url, studyProgramme)
|
||||
app.use(`${url}/openunisearch`, auth.roles(['openUniSearch']), customOpenUniSearch)
|
||||
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
|
||||
app.use(`${url}/updater`, auth.roles(['admin']), updater)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const router = require('express').Router()
|
||||
const crypto = require('crypto')
|
||||
const Course = require('../services/courses')
|
||||
const { faculties } = require('../services/organisations')
|
||||
const { validateParamLength } = require('../util')
|
||||
|
||||
router.get('/v2/coursesmulti', async (req, res) => {
|
||||
@@ -43,4 +44,9 @@ router.get('/v3/courseyearlystats', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/faculties-only', async (_req, res) => {
|
||||
const facultyList = await faculties()
|
||||
res.json(facultyList)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const router = require('express').Router()
|
||||
var moment = require('moment-timezone')
|
||||
const { getCustomOpenUniCourses } = require('../services/openUni/openUniStats')
|
||||
const {
|
||||
getOpenUniSearches,
|
||||
createNewSearch,
|
||||
deleteSearch,
|
||||
updateSearch,
|
||||
} = require('../services/openUni/openUniManageSearches')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const courseCodes = JSON.parse(req.query?.courselist) || []
|
||||
const startdate = req.query?.startdate ? req.query?.startdate : moment('01-08-2017 00:00:00', 'DD-MM-YYYY')
|
||||
const enddate = req.query?.enddate ? req.query?.enddate : moment().endOf('day')
|
||||
|
||||
if (!Array.isArray(courseCodes)) return res.status(400).json({ error: 'Courses must be of type array' })
|
||||
const customOpenUniSearches = await getCustomOpenUniCourses(courseCodes, startdate, enddate)
|
||||
return res.json(customOpenUniSearches)
|
||||
})
|
||||
|
||||
router.get('/searches', async (req, res) => {
|
||||
const userId = req.user.id
|
||||
const foundSearches = await getOpenUniSearches(userId)
|
||||
return res.json(foundSearches)
|
||||
})
|
||||
|
||||
router.post('/searches', async (req, res) => {
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const userId = req.user.id
|
||||
const name = req.body?.name
|
||||
|
||||
if (!name) return res.status(400).json({ error: 'Name missing' })
|
||||
if (courseCodes && !Array.isArray(courseCodes))
|
||||
return res.status(400).json({ error: 'Course codes must be type of array' })
|
||||
|
||||
const createdSearch = await createNewSearch(userId, name, courseCodes)
|
||||
if (!createdSearch) return res.status(400).json({ error: '' })
|
||||
return res.status(201).json({
|
||||
id: createdSearch.id,
|
||||
userId: createdSearch.userId,
|
||||
name: createdSearch.name,
|
||||
courseList: createdSearch.courseCodes,
|
||||
updatedAt: createdSearch.updatedAt,
|
||||
})
|
||||
})
|
||||
|
||||
router.put('/searches/:id', async (req, res) => {
|
||||
const id = req.params?.id
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const userId = req.user.id
|
||||
if (!id || !userId) return res.status(422).end()
|
||||
const updatedSearch = await updateSearch(userId, id, courseCodes)
|
||||
|
||||
if (!updatedSearch) return res.status(404).json({ error: `Open uni search could not be found` })
|
||||
return res.json({
|
||||
id: updatedSearch.id,
|
||||
userId: updatedSearch.userId,
|
||||
name: updatedSearch.name,
|
||||
courseList: updatedSearch.courseCodes,
|
||||
updatedAt: updatedSearch.updatedAt,
|
||||
})
|
||||
})
|
||||
|
||||
router.delete('/searches/:id', async (req, res) => {
|
||||
const id = req.params?.id
|
||||
const userId = req.user.id
|
||||
if (!id || !userId) return res.status(422).end()
|
||||
|
||||
const deletedSearch = await deleteSearch(userId, id)
|
||||
if (!deletedSearch) return res.status(404).json({ error: `Open uni search could not be found` })
|
||||
return res.json(id)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -18,7 +18,7 @@ const {
|
||||
setCreditStats,
|
||||
getThesisWritersStats,
|
||||
setThesisWritersStats,
|
||||
//getGraduationStats,
|
||||
getGraduationStats,
|
||||
setGraduationStats,
|
||||
getFacultyStudentStats,
|
||||
setFacultyStudentStats,
|
||||
@@ -53,10 +53,8 @@ router.get('/:id/basicstats', async (req, res) => {
|
||||
const specialGroups = req.query?.special_groups
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
const data = await getBasicStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const wantedProgrammes = await getProgrammes(code, programmeFilter)
|
||||
if (!wantedProgrammes) return res.status(422).end()
|
||||
|
||||
@@ -90,14 +88,16 @@ router.get('/:id/creditstats', async (req, res) => {
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
const data = await getCreditStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
|
||||
let updatedStats = await combineFacultyCredits(code, programmes.data, yearType, specialGroups)
|
||||
// list of all programmes is also needed in classification of credits
|
||||
const allProgrammes = programmeFilter === 'ALL_PROGRAMMES' ? programmes : await getProgrammes(code, 'ALL_PROGRAMMES')
|
||||
|
||||
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType, specialGroups)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
@@ -114,7 +114,6 @@ router.get('/:id/thesisstats', async (req, res) => {
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
let updateStats = await combineFacultyThesisWriters(code, programmes.data, yearType, specialGroups)
|
||||
@@ -129,10 +128,10 @@ router.get('/:id/graduationtimes', async (req, res) => {
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
// const data = await getGraduationStats(code, programmeFilter)
|
||||
// if (data) {
|
||||
// return res.json(data)
|
||||
// }
|
||||
const data = await getGraduationStats(code, programmeFilter)
|
||||
if (data) {
|
||||
return res.json(data)
|
||||
}
|
||||
let updatedStats = await countGraduationTimes(code, programmeFilter)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setGraduationStats(updatedStats, programmeFilter)
|
||||
@@ -165,7 +164,6 @@ router.get('/:id/studentstats', async (req, res) => {
|
||||
const graduated = req.query?.graduated
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
const data = await getFacultyStudentStats(code, specialGroups, graduated)
|
||||
if (data) return res.json(data)
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
|
||||
@@ -10,6 +10,7 @@ const CourseService = require('../services/courses')
|
||||
const StatMergeService = require('../services/statMerger')
|
||||
const { mapToProviders } = require('../util/utils')
|
||||
const { encrypt, decrypt } = require('../services/encrypt')
|
||||
const { mapCodesToIds } = require('../services/studyprogrammeHelpers')
|
||||
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
|
||||
@@ -268,6 +269,7 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
...student,
|
||||
firstnames: '',
|
||||
lastname: '',
|
||||
phone_number: '',
|
||||
started: null,
|
||||
iv,
|
||||
studentNumber: encryptedData,
|
||||
@@ -380,6 +382,7 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
studentNumber: crypto.createHash('md5').update(`${studentNumber}${randomHash}`).digest('hex'),
|
||||
firstnames: '',
|
||||
lastname: '',
|
||||
phone_number: '',
|
||||
name: '',
|
||||
email: '',
|
||||
started: null,
|
||||
@@ -429,9 +432,11 @@ router.get('/v3/populationstatistics/studyprogrammes', async (req, res) => {
|
||||
} = req
|
||||
if (roles?.includes('admin')) {
|
||||
const studyrights = await StudyrightService.getAssociations()
|
||||
mapCodesToIds(studyrights.programmes)
|
||||
res.json(studyrights)
|
||||
} else {
|
||||
const studyrights = await StudyrightService.getFilteredAssociations(rights.concat(iamRights))
|
||||
mapCodesToIds(studyrights.programmes)
|
||||
res.json(studyrights)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,8 +4,6 @@ const {
|
||||
ElementDetail,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
Semester,
|
||||
SemesterEnrollment,
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
Student,
|
||||
@@ -19,7 +17,6 @@ const {
|
||||
formatFacultyTransfer,
|
||||
formatFacultyThesisWriter,
|
||||
formatOrganization,
|
||||
formatAbsence,
|
||||
isNewProgramme,
|
||||
mapCodesToIds,
|
||||
} = require('./facultyHelpers')
|
||||
@@ -125,15 +122,42 @@ const studyrightsByRightStartYear = async (faculty, since, graduated = 1) =>
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const bachelorStudyright = async id => {
|
||||
return await Studyright.findOne({
|
||||
attributes: ['studystartdate'],
|
||||
where: {
|
||||
studyrightid: id,
|
||||
extentcode: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
const getStudyRightsByExtent = async (faculty, academicYearStart, academicYearEnd, code, extents, graduated) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.between]: [academicYearStart, academicYearEnd],
|
||||
},
|
||||
code: code,
|
||||
},
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
extentcode: {
|
||||
[Op.in]: extents,
|
||||
},
|
||||
graduated: {
|
||||
[Op.in]: graduated,
|
||||
},
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const hasMasterRight = async id => {
|
||||
return await Studyright.findOne({
|
||||
@@ -256,29 +280,6 @@ const thesisWriters = async (provider, since, thesisTypes, students) =>
|
||||
})
|
||||
).map(formatFacultyThesisWriter)
|
||||
|
||||
const statutoryAbsences = async (studentnumber, startdate, enddate) =>
|
||||
(
|
||||
await SemesterEnrollment.findAll({
|
||||
attributes: ['semestercode'],
|
||||
include: {
|
||||
model: Semester,
|
||||
attributes: ['startdate', 'enddate'],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.gte]: startdate,
|
||||
},
|
||||
enddate: {
|
||||
[Op.lte]: enddate,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
studentnumber: studentnumber,
|
||||
statutory_absence: true,
|
||||
},
|
||||
})
|
||||
).map(formatAbsence)
|
||||
|
||||
// Some programme modules are not directly associated to a faculty (organization).
|
||||
// Some have intermediate organizations, such as department, so the connection must be digged up
|
||||
const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
|
||||
@@ -336,7 +337,6 @@ module.exports = {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
studyrightsByRightStartYear,
|
||||
bachelorStudyright,
|
||||
hasMasterRight,
|
||||
transferredInsideFaculty,
|
||||
transferredAway,
|
||||
@@ -345,7 +345,7 @@ module.exports = {
|
||||
thesisWriters,
|
||||
findFacultyProgrammeCodes,
|
||||
facultyOgranizationId,
|
||||
statutoryAbsences,
|
||||
getTransferredToAndAway,
|
||||
getTransferredInside,
|
||||
getStudyRightsByExtent,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const moment = require('moment')
|
||||
const {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
@@ -7,9 +8,11 @@ const {
|
||||
transferredTo,
|
||||
getTransferredToAndAway,
|
||||
getTransferredInside,
|
||||
studyrightsByRightStartYear,
|
||||
} = require('./faculty')
|
||||
const { getStatsBasis, getYearsArray, defineYear } = require('../studyprogrammeHelpers')
|
||||
const { findRightProgramme, isNewProgramme, checkTransfers, getExtentFilter } = require('./facultyHelpers')
|
||||
const { codes } = require('../../../config/programmeCodes')
|
||||
|
||||
const filterDuplicateStudyrights = studyrights => {
|
||||
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
|
||||
@@ -40,19 +43,18 @@ const addProgramme = async (
|
||||
facultyProgrammes,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
if (!(programme in stats)) {
|
||||
stats[programme] = {}
|
||||
let { progId, name, code } = facultyProgrammes.filter(prog => prog.code === programme)[0]
|
||||
if (!(progId in stats)) {
|
||||
stats[progId] = {}
|
||||
if (includeAllSpecials) {
|
||||
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0, 0, 0]))
|
||||
Object.keys(tableStats).forEach(year => (stats[progId][year] = [0, 0, 0]))
|
||||
} else {
|
||||
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0]))
|
||||
Object.keys(tableStats).forEach(year => (stats[progId][year] = [0]))
|
||||
}
|
||||
}
|
||||
stats[programme][transferYear][index] += 1
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
const name = facultyProgrammes.filter(prog => prog.code === programme)[0]?.name
|
||||
allBasics.programmeNames[programme] = name
|
||||
stats[progId][transferYear][index] += 1
|
||||
if (!(progId in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[progId] = { ...name, code: code }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +78,7 @@ const getFacultyStarters = async (
|
||||
const studyrightWhere = getExtentFilter(includeAllSpecials)
|
||||
const studyrights = await startedStudyrights(faculty, since, studyrightWhere)
|
||||
let filteredStudyrights = filterDuplicateStudyrights(studyrights)
|
||||
const keys = Object.keys(codes)
|
||||
const programmeTableStats = {}
|
||||
if (!includeAllSpecials) {
|
||||
filteredStudyrights = filteredStudyrights.filter(
|
||||
@@ -85,6 +88,11 @@ const getFacultyStarters = async (
|
||||
|
||||
filteredStudyrights.forEach(({ studystartdate, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'started')
|
||||
let programmeId = programme
|
||||
|
||||
if (keys.includes(programme)) {
|
||||
programmeId = codes[programme].toUpperCase()
|
||||
}
|
||||
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const startYear = defineYear(studystartdate, isAcademicYear)
|
||||
@@ -92,13 +100,13 @@ const getFacultyStarters = async (
|
||||
startedGraphStats[indexOf(yearsArray, startYear)] += 1
|
||||
startedTableStats[startYear] += 1
|
||||
|
||||
if (!(programme in programmeTableStats)) {
|
||||
programmeTableStats[programme] = { ...tableStats }
|
||||
if (!(programmeId in programmeTableStats)) {
|
||||
programmeTableStats[programmeId] = { ...tableStats }
|
||||
}
|
||||
programmeTableStats[programme][startYear] += 1
|
||||
programmeTableStats[programmeId][startYear] += 1
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programme] = programmeName
|
||||
if (!(programmeId in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programmeId] = { ...programmeName, code: programme }
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -129,7 +137,7 @@ const getFacultyGraduates = async (
|
||||
) => {
|
||||
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
|
||||
const graduatedTableStats = {}
|
||||
|
||||
const keys = Object.keys(codes)
|
||||
let studyrightWhere = getExtentFilter(includeAllSpecials)
|
||||
let graduatedRights = await graduatedStudyrights(faculty, since, studyrightWhere)
|
||||
const programmeTableStats = {}
|
||||
@@ -140,40 +148,44 @@ const getFacultyGraduates = async (
|
||||
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
|
||||
)
|
||||
}
|
||||
|
||||
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
|
||||
let programmeId = programme
|
||||
|
||||
if (keys.includes(programme)) {
|
||||
programmeId = codes[programme].toUpperCase()
|
||||
}
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const endYear = defineYear(enddate, isAcademicYear)
|
||||
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][0] += 1
|
||||
|
||||
if (!(programme in programmeTableStats)) {
|
||||
programmeTableStats[programme] = {}
|
||||
Object.keys(tableStats).forEach(year => (programmeTableStats[programme][year] = [0, 0, 0, 0, 0]))
|
||||
if (!(programmeId in programmeTableStats)) {
|
||||
programmeTableStats[programmeId] = {}
|
||||
Object.keys(tableStats).forEach(year => (programmeTableStats[programmeId][year] = [0, 0, 0, 0, 0]))
|
||||
}
|
||||
programmeTableStats[programme][endYear][0] += 1
|
||||
programmeTableStats[programmeId][endYear][0] += 1
|
||||
|
||||
if (extentcode === 1) {
|
||||
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][1] += 1
|
||||
programmeTableStats[programme][endYear][1] += 1
|
||||
programmeTableStats[programmeId][endYear][1] += 1
|
||||
} else if (extentcode === 2) {
|
||||
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][2] += 1
|
||||
programmeTableStats[programme][endYear][2] += 1
|
||||
programmeTableStats[programmeId][endYear][2] += 1
|
||||
} else if (extentcode === 4) {
|
||||
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][3] += 1
|
||||
programmeTableStats[programme][endYear][3] += 1
|
||||
programmeTableStats[programmeId][endYear][3] += 1
|
||||
} else {
|
||||
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][4] += 1
|
||||
programmeTableStats[programme][endYear][4] += 1
|
||||
programmeTableStats[programmeId][endYear][4] += 1
|
||||
}
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programme] = programmeName
|
||||
if (!(programmeId in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programmeId] = { code: programme, ...programmeName }
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -192,7 +204,18 @@ const getFacultyGraduates = async (
|
||||
countsGraduations[year] = graduatedTableStats[year]
|
||||
})
|
||||
}
|
||||
|
||||
// If includeAllSpecial false, insidetransfers should contain only students started after 1.8.2017.
|
||||
const getInsideTransfers = async (programmeCodes, allProgrammeCodes, since, includeAllSpecials, faculty) => {
|
||||
if (includeAllSpecials) {
|
||||
return await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
|
||||
}
|
||||
const insiders = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
|
||||
const studyrights = (await studyrightsByRightStartYear(faculty, moment('2017-08-01', 'YYYY-MM-DD'))).map(
|
||||
sr => sr.studyrightid
|
||||
)
|
||||
const filteredTransfers = insiders.filter(tr => studyrights.includes(tr.studyrightid))
|
||||
return filteredTransfers
|
||||
}
|
||||
const getFacultyTransfers = async (
|
||||
programmes,
|
||||
programmeCodes,
|
||||
@@ -205,10 +228,16 @@ const getFacultyTransfers = async (
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData,
|
||||
includeAllSpecials
|
||||
includeAllSpecials,
|
||||
faculty
|
||||
) => {
|
||||
const insideTransfers = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
|
||||
// Filter still away those that have started studying before 1.8.2017, when include all specials false.
|
||||
const insideTransfers = await getInsideTransfers(
|
||||
programmeCodes,
|
||||
allProgrammeCodes,
|
||||
since,
|
||||
includeAllSpecials,
|
||||
faculty
|
||||
)
|
||||
let transferGraphStats = []
|
||||
let transferTableStats = {}
|
||||
if (includeAllSpecials) {
|
||||
@@ -224,17 +253,19 @@ const getFacultyTransfers = async (
|
||||
for (const { transferdate, targetcode } of insideTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][0] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
0,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
if (isAcademicYear || !(transferYear > moment().year())) {
|
||||
transferTableStats[transferYear][0] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
0,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
}
|
||||
if (includeAllSpecials) {
|
||||
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
|
||||
@@ -242,34 +273,38 @@ const getFacultyTransfers = async (
|
||||
|
||||
for (const { transferdate, sourcecode } of awayTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][1] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
sourcecode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
1,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
if (isAcademicYear || !(transferYear > moment().year())) {
|
||||
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][1] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
sourcecode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
1,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const { transferdate, targetcode } of toTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][2] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
2,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
if (isAcademicYear || !(transferYear > moment().year())) {
|
||||
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][2] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
2,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +402,8 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData,
|
||||
includeAllSpecials
|
||||
includeAllSpecials,
|
||||
faculty
|
||||
)
|
||||
|
||||
// combine tableStats from all categories
|
||||
@@ -413,7 +449,11 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
|
||||
if (code in programmeData.transferred) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
|
||||
if (includeAllSpecials) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -95,6 +95,7 @@ const getFacultyRegularCreditStats = async ({
|
||||
nonDegree.tableStats[attainmentYear] += credits || 0
|
||||
}
|
||||
})
|
||||
|
||||
if (
|
||||
majors.graphStats.every(year => year === 0) &&
|
||||
facultyNonMajors.graphStats.every(year => year === 0) &&
|
||||
@@ -191,7 +192,7 @@ const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgram
|
||||
return updatedStats
|
||||
}
|
||||
|
||||
const combineFacultyCredits = async (faculty, programmes, yearType, specialGroups) => {
|
||||
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType, specialGroups) => {
|
||||
let counts = {}
|
||||
let years = []
|
||||
const titles =
|
||||
@@ -216,12 +217,12 @@ const combineFacultyCredits = async (faculty, programmes, yearType, specialGroup
|
||||
titles,
|
||||
}
|
||||
|
||||
const progCodes = programmes.map(p => p.code)
|
||||
const progCodes = allProgrammes.map(p => p.code)
|
||||
for (const prog of programmes) {
|
||||
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
|
||||
if (data) {
|
||||
allCredits.programmeNames[prog.code] = prog.name
|
||||
allCredits.programmeTableStats[prog.code] = data.tableStats
|
||||
allCredits.programmeNames[prog.progId] = { code: prog.code, ...prog.name }
|
||||
allCredits.programmeTableStats[prog.progId] = data.tableStats
|
||||
if (!allCredits.lastUpdated || new Date(data.lastUpdated) > new Date(allCredits.lastUpdated))
|
||||
allCredits.lastUpdated = data.lastUpdated
|
||||
|
||||
@@ -237,6 +238,7 @@ const combineFacultyCredits = async (faculty, programmes, yearType, specialGroup
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// save table stats and graph stats
|
||||
years.forEach(year => {
|
||||
allCredits.tableStats.push([year, ...counts[year]])
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
const moment = require('moment')
|
||||
const {
|
||||
graduatedStudyrights,
|
||||
studyrightsByRightStartYear,
|
||||
bachelorStudyright,
|
||||
hasMasterRight,
|
||||
statutoryAbsences,
|
||||
} = require('./faculty')
|
||||
const { graduatedStudyrights, studyrightsByRightStartYear, hasMasterRight } = require('./faculty')
|
||||
const { findRightProgramme, isNewProgramme } = require('./facultyHelpers')
|
||||
const { getYearsArray, getYearsObject, getMean, getMedian, defineYear } = require('../studyprogrammeHelpers')
|
||||
const { codes } = require('../../../config/programmeCodes')
|
||||
const { bachelorStudyright, countTimeCategories, getStatutoryAbsences } = require('../graduationHelpers')
|
||||
|
||||
const sortProgrammes = data => {
|
||||
const check = name => {
|
||||
@@ -37,7 +32,7 @@ const getSortedProgIds = progs => {
|
||||
|
||||
const findBachelorStartdate = async id => {
|
||||
const studyright = await bachelorStudyright(id)
|
||||
if (studyright) return studyright.studystartdate
|
||||
if (studyright) return studyright.startdate
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -48,25 +43,6 @@ const getProgrammeObjectBasis = (years, levels, emptyObject = true) => {
|
||||
)
|
||||
}
|
||||
|
||||
const getStatutoryAbsences = async (studentnumber, startdate, enddate) => {
|
||||
const absences = await statutoryAbsences(studentnumber, startdate, enddate)
|
||||
if (absences.length) {
|
||||
const absentMonths = absences.reduce((sum, ab) => sum + moment(ab.end).diff(moment(ab.start), 'months'), 0)
|
||||
return absentMonths
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const countTimeCategories = (times, goal) => {
|
||||
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
|
||||
times.forEach(time => {
|
||||
if (time <= goal) statistics.onTime += 1
|
||||
else if (time <= goal + 12) statistics.yearOver += 1
|
||||
else statistics.wayOver += 1
|
||||
})
|
||||
return statistics
|
||||
}
|
||||
|
||||
const hasMS = async (programme, elements, studyrightid) => {
|
||||
// Only bachelor studyright: socCom, kasvatustiede - varhaiskasvatus track, farmasia - applied only to Bc = farmaseutti,
|
||||
if (['KH74_001', '620050-ba', '620030-ba', '520091-ba'].includes(programme)) {
|
||||
|
||||
@@ -88,11 +88,6 @@ const formatOrganization = org => {
|
||||
return { id, name, code, parentId: parent_id }
|
||||
}
|
||||
|
||||
const formatAbsence = absence => {
|
||||
const { semestercode, semester } = absence
|
||||
return { semestercode, start: semester.startdate, end: semester.enddate }
|
||||
}
|
||||
|
||||
const newProgrammes = [/^KH/, /^MH/, /^T/, /^LI/, /^K-/, /^FI/, /^00901$/, /^00910$/]
|
||||
|
||||
const isNewProgramme = code => {
|
||||
@@ -143,7 +138,6 @@ module.exports = {
|
||||
formatFacultyTransfer,
|
||||
formatFacultyThesisWriter,
|
||||
formatOrganization,
|
||||
formatAbsence,
|
||||
isNewProgramme,
|
||||
checkTransfers,
|
||||
getExtentFilter,
|
||||
|
||||
@@ -7,22 +7,37 @@ const {
|
||||
getMasterCreditGraphStats,
|
||||
getDoctoralCreditGraphStats,
|
||||
getYearsArray,
|
||||
getCorrectStudentnumbers,
|
||||
tableTitles,
|
||||
getOnlyMasterCreditGraphStats,
|
||||
getOnlyMasterThresholds,
|
||||
getVetenaryCreditGraphStats,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const { studytrackStudents, allStudyrights } = require('../studyprogramme')
|
||||
const { studytrackStudents } = require('../studyprogramme')
|
||||
const { getStudyRightsByExtent, transferredAway, transferredTo, transferredInsideFaculty } = require('./faculty')
|
||||
|
||||
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, limits, limitKeys, prog) => {
|
||||
const getStudentData = (
|
||||
startDate,
|
||||
students,
|
||||
thresholdKeys,
|
||||
thresholdAmounts,
|
||||
limits,
|
||||
limitKeys,
|
||||
prog,
|
||||
earlyStudyRights = []
|
||||
) => {
|
||||
let data = {}
|
||||
let programmeData = {}
|
||||
let start = startDate
|
||||
thresholdKeys.forEach(t => (data[t] = 0))
|
||||
limitKeys.forEach(t => (programmeData[t] = 0))
|
||||
students.forEach(({ credits }) => {
|
||||
students.forEach(({ studentnumber, credits }) => {
|
||||
if (earlyStudyRights.length > 0) {
|
||||
const bcStudyright = earlyStudyRights.filter(sr => sr.studentnumber === studentnumber)
|
||||
start = bcStudyright.length > 0 ? bcStudyright[0].startdate : startDate
|
||||
}
|
||||
const creditcount = credits
|
||||
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
|
||||
.reduce((prev, curr) => (prev += curr.credits), 0)
|
||||
.filter(credit => moment(credit.attainment_date).isSameOrAfter(start))
|
||||
.reduce((prev, curr) => prev + curr.credits, 0)
|
||||
|
||||
data[thresholdKeys[0]] += creditcount < thresholdAmounts[0] ? 1 : 0
|
||||
data[thresholdKeys[1]] += creditcount >= thresholdAmounts[0] && creditcount < thresholdAmounts[1] ? 1 : 0
|
||||
@@ -49,42 +64,102 @@ const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, li
|
||||
}
|
||||
})
|
||||
|
||||
return [data, programmeData]
|
||||
return { data, programmeData }
|
||||
}
|
||||
|
||||
const combineFacultyStudentProgress = async (faculty, programmes, specialGroups, graduated) => {
|
||||
const createLimits = (months, creditsToAdd) => {
|
||||
return [
|
||||
[Math.ceil(months * (60 / 12)) + creditsToAdd, null],
|
||||
[Math.ceil(months * (45 / 12)) + creditsToAdd, Math.ceil(months * (60 / 12)) + creditsToAdd],
|
||||
[Math.ceil(months * (30 / 12)) + creditsToAdd, Math.ceil(months * (45 / 12)) + creditsToAdd],
|
||||
[Math.ceil(months * (15 / 12)) + creditsToAdd, Math.ceil(months * (30 / 12)) + creditsToAdd],
|
||||
[creditsToAdd + 1, Math.ceil(months * (15 / 12)) + creditsToAdd],
|
||||
[creditsToAdd, null],
|
||||
]
|
||||
}
|
||||
|
||||
const createYearlyTitles = (start, limitList) => {
|
||||
return [
|
||||
`${start} Credits`,
|
||||
`${limitList[4][0]} ≤ Credits < ${limitList[4][1]}`,
|
||||
`${limitList[3][0]} ≤ Credits < ${limitList[3][1]}`,
|
||||
`${limitList[2][0]} ≤ Credits < ${limitList[2][1]}`,
|
||||
`${limitList[1][0]} ≤ Credits < ${limitList[1][1]}`,
|
||||
`${limitList[0][0]} ≤ Credits`,
|
||||
]
|
||||
}
|
||||
|
||||
const filterOutTransfers = async (studyrights, programmes, allProgrammeCodes, since) => {
|
||||
const transferredIn = (await transferredTo(programmes, allProgrammeCodes, since)).map(tr => tr.studyrightid)
|
||||
const transferredOut = (await transferredAway(programmes, allProgrammeCodes, since)).map(tr => tr.studyrightid)
|
||||
const transferredInside = (await transferredInsideFaculty(programmes, allProgrammeCodes, since)).map(
|
||||
tr => tr.studyrightid
|
||||
)
|
||||
const filteredStudyrights = studyrights.filter(
|
||||
sr =>
|
||||
!transferredIn.includes(sr.studyrightid) &&
|
||||
!transferredOut.includes(sr.studyrightid) &&
|
||||
!(transferredInside.includes(sr.studyrightid) && moment(sr.startdate).isBefore('2017-08-01', 'YYYY-MM-DD'))
|
||||
)
|
||||
return filteredStudyrights
|
||||
}
|
||||
|
||||
const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCodes, specialGroups, graduated) => {
|
||||
const since = new Date('2017-08-01')
|
||||
const isAcademicYear = true
|
||||
const includeYearsCombined = true
|
||||
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
|
||||
const includeGraduated = graduated === 'GRADUATED_INCLUDED'
|
||||
const graduationStatus = [0]
|
||||
if (includeGraduated) {
|
||||
graduationStatus.push(1)
|
||||
}
|
||||
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
|
||||
|
||||
let allBachelorGraphStats = getBachelorCreditGraphStats(yearsArray)
|
||||
let allBsMsGraphstats = getMasterCreditGraphStats(yearsArray)
|
||||
let allMastersGraphStats = getOnlyMasterCreditGraphStats(yearsArray)
|
||||
let allDoctoralGraphStats = getDoctoralCreditGraphStats(yearsArray)
|
||||
|
||||
let bachelorsProgrammeStats = {}
|
||||
let bcMsProgrammeStats = {}
|
||||
let mastersProgrammeStats = {}
|
||||
let doctoralProgrammeStats = {}
|
||||
|
||||
let bachelorsTableStats = new Array(yearsArray.length)
|
||||
let bachelorMastersTableStats = new Array(yearsArray.length)
|
||||
let mastersTableStats = new Array(yearsArray.length)
|
||||
let doctoralTableStats = new Array(yearsArray.length)
|
||||
|
||||
let yearlyBachelorTitles = []
|
||||
let yearlyBcMsTitles = []
|
||||
let yearlyMasterTitles = []
|
||||
const titlesVet = [
|
||||
'',
|
||||
'All',
|
||||
'< 210 credits',
|
||||
'210-239 credits',
|
||||
'240-269 credits',
|
||||
'270-299 credits',
|
||||
'300-329 credits',
|
||||
'330-359 credits',
|
||||
' 360 ≤ credits ',
|
||||
]
|
||||
const progressStats = {
|
||||
id: faculty,
|
||||
years: yearsArray,
|
||||
bachelorsTableStats: new Array(yearsArray.length),
|
||||
bcMsTableStats: new Array(yearsArray.length),
|
||||
mastersTableStats: new Array(yearsArray.length),
|
||||
doctoralTableStats: new Array(yearsArray.length),
|
||||
bachelorsGraphStats: getBachelorCreditGraphStats(yearsArray),
|
||||
bcMsGraphStats: faculty === 'H90' ? getVetenaryCreditGraphStats(yearsArray) : getMasterCreditGraphStats(yearsArray),
|
||||
mastersGraphStats: getOnlyMasterCreditGraphStats(yearsArray),
|
||||
doctoralGraphStats: getDoctoralCreditGraphStats(yearsArray),
|
||||
bachelorTitles: tableTitles.creditProgress.bachelor,
|
||||
bcMsTitles: faculty === 'H90' ? titlesVet : tableTitles.creditProgress.master,
|
||||
mastersTitles: tableTitles.creditProgress.masterOnly,
|
||||
doctoralTitles: tableTitles.creditProgress.doctoral,
|
||||
yearlyBachelorTitles: [],
|
||||
yearlyBcMsTitles: [],
|
||||
yearlyMasterTitles: [],
|
||||
programmeNames: programmes.reduce(
|
||||
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
|
||||
{}
|
||||
),
|
||||
bachelorsProgrammeStats: {},
|
||||
bcMsProgrammeStats: {},
|
||||
mastersProgrammeStats: {},
|
||||
doctoralProgrammeStats: {},
|
||||
}
|
||||
|
||||
const reversedYears = [...yearsArray].reverse()
|
||||
reversedYears.forEach(year => {
|
||||
bachelorsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
bachelorMastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0]
|
||||
doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
progressStats.bachelorsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
progressStats.bcMsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
progressStats.mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0]
|
||||
progressStats.doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
})
|
||||
const limitKeys = ['zero', 't5', 't4', 't3', 't2', 't1']
|
||||
let bachelorlimits = []
|
||||
@@ -92,108 +167,50 @@ const combineFacultyStudentProgress = async (faculty, programmes, specialGroups,
|
||||
let bcmslimits = []
|
||||
for (const year of reversedYears) {
|
||||
const { startDate, endDate } = getAcademicYearDates(year, since)
|
||||
const today = moment()
|
||||
const lastDayOfMonth = moment(today).endOf('month')
|
||||
const lastDayOfMonth = moment().endOf('month')
|
||||
let months = Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(startDate))).asMonths())
|
||||
|
||||
if (months >= 36) {
|
||||
bachelorlimits = [
|
||||
[Math.ceil(36 * (60 / 12)), null],
|
||||
[Math.ceil(36 * (45 / 12)), Math.ceil(36 * (60 / 12))],
|
||||
[Math.ceil(36 * (30 / 12)), Math.ceil(36 * (45 / 12))],
|
||||
[Math.ceil(36 * (15 / 12)), Math.ceil(36 * (30 / 12))],
|
||||
[1, Math.ceil(36 * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
bachelorlimits = createLimits(36, 0)
|
||||
} else {
|
||||
bachelorlimits = [
|
||||
[Math.ceil(months * (60 / 12)), null],
|
||||
[Math.ceil(months * (45 / 12)), Math.ceil(months * (60 / 12))],
|
||||
[Math.ceil(months * (30 / 12)), Math.ceil(months * (45 / 12))],
|
||||
[Math.ceil(months * (15 / 12)), Math.ceil(months * (30 / 12))],
|
||||
[1, Math.ceil(months * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
bachelorlimits = createLimits(months, 0)
|
||||
}
|
||||
if (months >= 20) {
|
||||
masterlimits = [
|
||||
[Math.ceil(20 * (60 / 12)), null],
|
||||
[Math.ceil(20 * (45 / 12)), Math.ceil(20 * (60 / 12))],
|
||||
[Math.ceil(20 * (30 / 12)), Math.ceil(20 * (45 / 12))],
|
||||
[Math.ceil(20 * (15 / 12)), Math.ceil(20 * (30 / 12))],
|
||||
[1, Math.ceil(20 * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
if (months >= 24) {
|
||||
masterlimits = createLimits(24, 0)
|
||||
} else {
|
||||
masterlimits = [
|
||||
[Math.ceil(months * (60 / 12)), null],
|
||||
[Math.ceil(months * (45 / 12)), Math.ceil(months * (60 / 12))],
|
||||
[Math.ceil(months * (30 / 12)), Math.ceil(months * (45 / 12))],
|
||||
[Math.ceil(months * (15 / 12)), Math.ceil(months * (30 / 12))],
|
||||
[1, Math.ceil(months * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
masterlimits = createLimits(months, 0)
|
||||
}
|
||||
if (months >= 60) {
|
||||
bcmslimits = [
|
||||
[Math.ceil(60 * (60 / 12)) + 180, null],
|
||||
[Math.ceil(60 * (45 / 12)) + 180, Math.ceil(60 * (60 / 12)) + 180],
|
||||
[Math.ceil(60 * (30 / 12)) + 180, Math.ceil(60 * (45 / 12)) + 180],
|
||||
[Math.ceil(60 * (15 / 12)) + 180, Math.ceil(60 * (30 / 12)) + 180],
|
||||
[181, Math.ceil(60 * (15 / 12)) + 180],
|
||||
[180, null],
|
||||
]
|
||||
if (months >= 72 && faculty === 'H90') {
|
||||
bcmslimits = createLimits(72, 180)
|
||||
} else if (months >= 60) {
|
||||
bcmslimits = createLimits(60, 180)
|
||||
} else {
|
||||
bcmslimits = [
|
||||
[Math.ceil(months * (60 / 12)) + 180, null],
|
||||
[Math.ceil(months * (45 / 12)) + 180, Math.ceil(months * (60 / 12)) + 180],
|
||||
[Math.ceil(months * (30 / 12)) + 180, Math.ceil(months * (45 / 12)) + 180],
|
||||
[Math.ceil(months * (15 / 12)) + 180, Math.ceil(months * (30 / 12)) + 180],
|
||||
[181, Math.ceil(months * (15 / 12)) + 180],
|
||||
[180, null],
|
||||
]
|
||||
bcmslimits = createLimits(months, 180)
|
||||
}
|
||||
progressStats.yearlyBachelorTitles = [...progressStats.yearlyBachelorTitles, createYearlyTitles(0, bachelorlimits)]
|
||||
progressStats.yearlyMasterTitles = [...progressStats.yearlyMasterTitles, createYearlyTitles(0, masterlimits)]
|
||||
progressStats.yearlyBcMsTitles = [...progressStats.yearlyBcMsTitles, createYearlyTitles(180, bcmslimits)]
|
||||
const programmeCodes = programmes.map(prog => prog.code)
|
||||
|
||||
yearlyBachelorTitles.push([
|
||||
'0 Credits',
|
||||
`${bachelorlimits[4][0]} ≤ Credits < ${bachelorlimits[4][1]}`,
|
||||
`${bachelorlimits[3][0]} ≤ Credits < ${bachelorlimits[3][1]}`,
|
||||
`${bachelorlimits[2][0]} ≤ Credits < ${bachelorlimits[2][1]}`,
|
||||
`${bachelorlimits[1][0]} ≤ Credits < ${bachelorlimits[1][1]}`,
|
||||
`${bachelorlimits[0][0]} ≤ Credits`,
|
||||
])
|
||||
yearlyMasterTitles.push([
|
||||
'0 Credits',
|
||||
`${masterlimits[4][0]} ≤ Credits < ${masterlimits[4][1]}`,
|
||||
`${masterlimits[3][0]} ≤ Credits < ${masterlimits[3][1]}`,
|
||||
`${masterlimits[2][0]} ≤ Credits < ${masterlimits[2][1]}`,
|
||||
`${masterlimits[1][0]} ≤ Credits < ${masterlimits[1][1]}`,
|
||||
`${masterlimits[0][0]} ≤ Credits`,
|
||||
])
|
||||
yearlyBcMsTitles.push([
|
||||
'180 Credits',
|
||||
`${bcmslimits[4][0]} ≤ Credits < ${bcmslimits[4][1]}`,
|
||||
`${bcmslimits[3][0]} ≤ Credits < ${bcmslimits[3][1]}`,
|
||||
`${bcmslimits[2][0]} ≤ Credits < ${bcmslimits[2][1]}`,
|
||||
`${bcmslimits[1][0]} ≤ Credits < ${bcmslimits[1][1]}`,
|
||||
`${bcmslimits[0][0]} ≤ Credits`,
|
||||
])
|
||||
for (const programme of programmes) {
|
||||
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(programme.code)
|
||||
for (const { progId, code } of programmes) {
|
||||
let extentcodes = []
|
||||
if (includeAllSpecials) {
|
||||
extentcodes = [7, 9, 34, 22, 99, 14, 13]
|
||||
}
|
||||
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code)
|
||||
if (!creditThresholdKeys) return
|
||||
const studentnumbers = await getCorrectStudentnumbers({
|
||||
codes: [programme.code],
|
||||
startDate,
|
||||
endDate,
|
||||
includeAllSpecials,
|
||||
includeGraduated,
|
||||
})
|
||||
|
||||
const all = await allStudyrights(programme.code, studentnumbers)
|
||||
const students = await studytrackStudents(studentnumbers)
|
||||
|
||||
if (programme.code.includes('KH')) {
|
||||
const data = getStudentData(
|
||||
if (code.includes('KH')) {
|
||||
extentcodes.push(1)
|
||||
let studyrights = await getStudyRightsByExtent(faculty, startDate, endDate, code, extentcodes, graduationStatus)
|
||||
if (!includeAllSpecials) {
|
||||
studyrights = await filterOutTransfers(studyrights, programmeCodes, allProgrammeCodes, since)
|
||||
}
|
||||
const all = studyrights
|
||||
.filter(sr => sr.studyrightElements.some(element => element.code === code))
|
||||
.map(sr => sr.studentnumber)
|
||||
const students = await studytrackStudents(all)
|
||||
const { data: studentData, programmeData } = getStudentData(
|
||||
startDate,
|
||||
students,
|
||||
creditThresholdKeys,
|
||||
@@ -202,46 +219,49 @@ const combineFacultyStudentProgress = async (faculty, programmes, specialGroups,
|
||||
limitKeys,
|
||||
'KH'
|
||||
)
|
||||
const studentData = data[0]
|
||||
const programmeData = data[1]
|
||||
if (!(programme.code in bachelorsProgrammeStats)) {
|
||||
bachelorsProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
|
||||
|
||||
if (!(progId in progressStats.bachelorsProgrammeStats)) {
|
||||
progressStats.bachelorsProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
bachelorsTableStats[indexOf(reversedYears, year)][1] += all.length || 0
|
||||
progressStats.bachelorsTableStats[indexOf(reversedYears, year)][1] += all.length || 0
|
||||
if (year !== 'Total') {
|
||||
bachelorsProgrammeStats[programme.code][indexOf(reversedYears, year)] = limitKeys.map(
|
||||
progressStats.bachelorsProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(
|
||||
key => programmeData[key]
|
||||
)
|
||||
}
|
||||
for (const key of Object.keys(studentData)) {
|
||||
bachelorsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
progressStats.bachelorsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
studentData[key] || 0
|
||||
allBachelorGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
|
||||
progressStats.bachelorsGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
|
||||
}
|
||||
} else if (programme.code.includes('MH')) {
|
||||
const all_BcMs = all.filter(sr => sr.studyrightid.slice(-2) === '-2')
|
||||
const all_Ms = all.filter(sr => sr.studyrightid.slice(-2) !== '-2')
|
||||
const all_MsStudentnumbers = all_Ms.map(sr => sr.studentnumber)
|
||||
const all_BcMsStudentnumbers = all_BcMs.map(sr => sr.studentnumber)
|
||||
mastersTableStats[indexOf(reversedYears, year)][1] += all_Ms.length || 0
|
||||
bachelorMastersTableStats[indexOf(reversedYears, year)][1] += all_BcMs.length || 0
|
||||
} else if (code.includes('MH')) {
|
||||
extentcodes.push(2)
|
||||
let all = await getStudyRightsByExtent(faculty, startDate, endDate, code, extentcodes, graduationStatus)
|
||||
if (!includeAllSpecials) {
|
||||
all = await filterOutTransfers(all, programmeCodes, allProgrammeCodes, since)
|
||||
}
|
||||
const allMsStudents = all.filter(sr => sr.studyrightid.slice(-2) !== '-2').map(sr => sr.studentnumber)
|
||||
const allBcMsStudents = all.filter(sr => sr.studyrightid.slice(-2) === '-2').map(sr => sr.studentnumber)
|
||||
const studentsWithBachelor = all
|
||||
.filter(sr => sr.studyrightid.slice(-2) === '-2')
|
||||
.map(sr => ({ studentnumber: sr.studentnumber, startdate: sr.startdate }))
|
||||
const bcMsStudents = await studytrackStudents(allBcMsStudents)
|
||||
const msStudents = await studytrackStudents(allMsStudents)
|
||||
|
||||
const msStudents = students.filter(student => all_MsStudentnumbers.includes(student.studentnumber))
|
||||
const bcMsStudents = students.filter(student => all_BcMsStudentnumbers.includes(student.studentnumber))
|
||||
|
||||
const data = getStudentData(
|
||||
progressStats.mastersTableStats[indexOf(reversedYears, year)][1] += allMsStudents.length || 0
|
||||
progressStats.bcMsTableStats[indexOf(reversedYears, year)][1] += allBcMsStudents.length || 0
|
||||
const { data: bcMsStudentdata, programmeData: bcMsProgrammedata } = getStudentData(
|
||||
startDate,
|
||||
bcMsStudents,
|
||||
creditThresholdKeys,
|
||||
creditThresholdAmounts,
|
||||
bcmslimits,
|
||||
limitKeys,
|
||||
'MH'
|
||||
'MH',
|
||||
studentsWithBachelor
|
||||
)
|
||||
const bcMsStudentdata = data[0]
|
||||
const bcMsProgrammedata = data[1]
|
||||
const { msOnlyCreditThresholdKeys, msOnlyCreditThresholdAmount } = getOnlyMasterThresholds()
|
||||
const data2 = getStudentData(
|
||||
const { data: msStudentdata, programmeData: msProgrammedata } = getStudentData(
|
||||
startDate,
|
||||
msStudents,
|
||||
msOnlyCreditThresholdKeys,
|
||||
@@ -250,79 +270,59 @@ const combineFacultyStudentProgress = async (faculty, programmes, specialGroups,
|
||||
limitKeys,
|
||||
'MH'
|
||||
)
|
||||
const msStudentdata = data2[0]
|
||||
const msProgrammedata = data2[1]
|
||||
|
||||
if (!(programme.code in mastersProgrammeStats)) {
|
||||
mastersProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
|
||||
if (!(progId in progressStats.mastersProgrammeStats)) {
|
||||
progressStats.mastersProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (!(programme.code in bcMsProgrammeStats)) {
|
||||
bcMsProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
|
||||
if (!(progId in progressStats.bcMsProgrammeStats)) {
|
||||
progressStats.bcMsProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (year !== 'Total') {
|
||||
mastersProgrammeStats[programme.code][indexOf(reversedYears, year)] = limitKeys.map(
|
||||
progressStats.mastersProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(
|
||||
key => msProgrammedata[key]
|
||||
)
|
||||
bcMsProgrammeStats[programme.code][indexOf(reversedYears, year)] = limitKeys.map(
|
||||
progressStats.bcMsProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(
|
||||
key => bcMsProgrammedata[key]
|
||||
)
|
||||
}
|
||||
|
||||
for (const key of Object.keys(msStudentdata)) {
|
||||
mastersTableStats[indexOf(reversedYears, year)][indexOf(msOnlyCreditThresholdKeys, key) + 2] +=
|
||||
progressStats.mastersTableStats[indexOf(reversedYears, year)][indexOf(msOnlyCreditThresholdKeys, key) + 2] +=
|
||||
msStudentdata[key] || 0
|
||||
allMastersGraphStats[key].data[indexOf(yearsArray, year)] += msStudentdata[key] || 0
|
||||
progressStats.mastersGraphStats[key].data[indexOf(yearsArray, year)] += msStudentdata[key] || 0
|
||||
}
|
||||
for (const key of Object.keys(bcMsStudentdata)) {
|
||||
bachelorMastersTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
progressStats.bcMsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
bcMsStudentdata[key] || 0
|
||||
allBsMsGraphstats[key].data[indexOf(yearsArray, year)] += bcMsStudentdata[key] || 0
|
||||
progressStats.bcMsGraphStats[key].data[indexOf(yearsArray, year)] += bcMsStudentdata[key] || 0
|
||||
}
|
||||
} else {
|
||||
const data = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts, [], [], 'T')
|
||||
const studentData = data[0]
|
||||
doctoralTableStats[indexOf(reversedYears, year)][1] += all.length || 0
|
||||
if (!(programme.code in doctoralProgrammeStats)) {
|
||||
doctoralProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
|
||||
extentcodes.push(4)
|
||||
let studyrights = await getStudyRightsByExtent(faculty, startDate, endDate, code, extentcodes, graduationStatus)
|
||||
if (!includeAllSpecials) {
|
||||
studyrights = await filterOutTransfers(studyrights, programmeCodes, allProgrammeCodes, since)
|
||||
}
|
||||
const all = studyrights
|
||||
.filter(sr => sr.studyrightElements.some(element => element.code === code))
|
||||
.map(sr => sr.studentnumber)
|
||||
const students = await studytrackStudents(all)
|
||||
const { data } = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts, [], [], 'T')
|
||||
progressStats.doctoralTableStats[indexOf(reversedYears, year)][1] += all.length || 0
|
||||
if (!(progId in progressStats.doctoralProgrammeStats)) {
|
||||
progressStats.doctoralProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (year !== 'Total') {
|
||||
doctoralProgrammeStats[programme.code][indexOf(reversedYears, year)] = creditThresholdKeys.map(
|
||||
key => studentData[key]
|
||||
progressStats.doctoralProgrammeStats[progId][indexOf(reversedYears, year)] = creditThresholdKeys.map(
|
||||
key => data[key]
|
||||
)
|
||||
}
|
||||
for (const key of Object.keys(studentData)) {
|
||||
doctoralTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
studentData[key] || 0
|
||||
allDoctoralGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
|
||||
for (const key of Object.keys(data)) {
|
||||
progressStats.doctoralTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
data[key] || 0
|
||||
progressStats.doctoralGraphStats[key].data[indexOf(yearsArray, year)] += data[key] || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const facultyProgressStats = {
|
||||
id: faculty,
|
||||
years: yearsArray,
|
||||
bachelorsTableStats: bachelorsTableStats,
|
||||
bcMsTableStats: bachelorMastersTableStats,
|
||||
mastersTableStats: mastersTableStats,
|
||||
doctoralTableStats: doctoralTableStats,
|
||||
bachelorsGraphStats: allBachelorGraphStats,
|
||||
bcMsGraphStats: allBsMsGraphstats,
|
||||
mastersGraphStats: allMastersGraphStats,
|
||||
doctoralGraphStats: allDoctoralGraphStats,
|
||||
bachelorTitles: tableTitles.creditProgress.bachelor,
|
||||
bcMsTitles: tableTitles.creditProgress.master,
|
||||
mastersTitles: tableTitles.creditProgress.masterOnly,
|
||||
doctoralTitles: tableTitles.creditProgress.doctoral,
|
||||
yearlyBachelorTitles: yearlyBachelorTitles,
|
||||
yearlyBcMsTitles: yearlyBcMsTitles,
|
||||
yearlyMasterTitles: yearlyMasterTitles,
|
||||
programmeNames: programmes.reduce((obj, dataItem) => ({ ...obj, [dataItem.code]: dataItem.name }), {}),
|
||||
bachelorsProgrammeStats: bachelorsProgrammeStats,
|
||||
bcMsProgrammeStats: bcMsProgrammeStats,
|
||||
mastersProgrammeStats: mastersProgrammeStats,
|
||||
doctoralProgrammeStats: doctoralProgrammeStats,
|
||||
}
|
||||
return facultyProgressStats
|
||||
return progressStats
|
||||
}
|
||||
module.exports = { combineFacultyStudentProgress }
|
||||
|
||||
@@ -45,9 +45,9 @@ const getFacultyDataForYear = async ({ programmes, since, settings, year, years,
|
||||
allMale: 0,
|
||||
}
|
||||
|
||||
for (const programme of programmes) {
|
||||
for (const { progId, code } of programmes) {
|
||||
const studentnumbers = await getCorrectStudentnumbers({
|
||||
codes: [programme.code],
|
||||
codes: [code],
|
||||
startDate,
|
||||
endDate,
|
||||
includeAllSpecials,
|
||||
@@ -55,23 +55,23 @@ const getFacultyDataForYear = async ({ programmes, since, settings, year, years,
|
||||
})
|
||||
|
||||
// Get all the studyrights and students for the calculations
|
||||
let all = await allStudyrights(programme.code, studentnumbers)
|
||||
let all = await allStudyrights(code, studentnumbers)
|
||||
const students = await studytrackStudents(studentnumbers)
|
||||
const studentData = getStudentData(students)
|
||||
const started = await startedStudyrights(programme.code, startDate, studentnumbers)
|
||||
const enrolled = await enrolledStudents(programme.code, startDate, studentnumbers)
|
||||
const absent = await absentStudents(programme.code, studentnumbers)
|
||||
const inactive = await inactiveStudyrights(programme.code, studentnumbers)
|
||||
const graduated = await graduatedStudyRights(programme.code, startDate, studentnumbers)
|
||||
const started = await startedStudyrights(code, startDate, studentnumbers)
|
||||
const enrolled = await enrolledStudents(code, startDate, studentnumbers)
|
||||
const absent = await absentStudents(code, studentnumbers)
|
||||
const inactive = await inactiveStudyrights(code, studentnumbers)
|
||||
const graduated = await graduatedStudyRights(code, startDate, studentnumbers)
|
||||
|
||||
// Count stats for the programme
|
||||
if (!(programme.code in programmeTableStats)) {
|
||||
programmeTableStats[programme.code] = years.reduce((resultObj, yearKey) => {
|
||||
if (!(progId in programmeTableStats)) {
|
||||
programmeTableStats[progId] = years.reduce((resultObj, yearKey) => {
|
||||
return { ...resultObj, [yearKey]: [] }
|
||||
}, {})
|
||||
}
|
||||
|
||||
programmeTableStats[programme.code][year] = [
|
||||
programmeTableStats[progId][year] = [
|
||||
all.length,
|
||||
started.length,
|
||||
getPercentage(started.length, all.length),
|
||||
@@ -149,7 +149,10 @@ const combineFacultyStudents = async (code, programmes, specialGroups, graduated
|
||||
facultyTableStats: facultyTableStats,
|
||||
programmeStats: programmeTableStats,
|
||||
titles: tableTitles['studytracks'],
|
||||
programmeNames: programmes.reduce((obj, dataItem) => ({ ...obj, [dataItem.code]: dataItem.name }), {}),
|
||||
programmeNames: programmes.reduce(
|
||||
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
|
||||
{}
|
||||
),
|
||||
}
|
||||
return studentsData
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
|
||||
let programmeCounts = {}
|
||||
let programmeNames = {}
|
||||
|
||||
for (const { code, name } of facultyProgrammes) {
|
||||
for (const { progId, code, name } of facultyProgrammes) {
|
||||
const provider = mapToProviders([code])[0]
|
||||
const students = await getCorrectStudentnumbers({
|
||||
codes: [code],
|
||||
@@ -37,30 +37,30 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
|
||||
thesisCourseCodes?.forEach(({ attainment_date, courseUnitType }) => {
|
||||
const thesisYear = defineYear(attainment_date, isAcademicYear)
|
||||
|
||||
if (!(code in programmeCounts)) {
|
||||
programmeCounts[code] = {}
|
||||
if (!(progId in programmeCounts)) {
|
||||
programmeCounts[progId] = {}
|
||||
|
||||
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[code][year] = [0, 0, 0, 0, 0]))
|
||||
programmeNames[code] = name
|
||||
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0, 0, 0]))
|
||||
programmeNames[progId] = { ...name, code: code }
|
||||
}
|
||||
programmeCounts[code][thesisYear][0] += 1
|
||||
programmeCounts[progId][thesisYear][0] += 1
|
||||
|
||||
if (courseUnitType === thesisTypes[0]) {
|
||||
bachelors.graphStats[indexOf(years, thesisYear)] += 1
|
||||
bachelors.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][1] += 1
|
||||
programmeCounts[progId][thesisYear][1] += 1
|
||||
} else if (courseUnitType === thesisTypes[1]) {
|
||||
masters.graphStats[indexOf(years, thesisYear)] += 1
|
||||
masters.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][2] += 1
|
||||
programmeCounts[progId][thesisYear][2] += 1
|
||||
} else if (courseUnitType === thesisTypes[2]) {
|
||||
doctors.graphStats[indexOf(years, thesisYear)] += 1
|
||||
doctors.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][3] += 1
|
||||
programmeCounts[progId][thesisYear][3] += 1
|
||||
} else if (courseUnitType === thesisTypes[3]) {
|
||||
licentiates.graphStats[indexOf(years, thesisYear)] += 1
|
||||
licentiates.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][4] += 1
|
||||
programmeCounts[progId][thesisYear][4] += 1
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -97,13 +97,13 @@ const getFacultyThesisWritersForStudyTrack = async (
|
||||
allThesisWriters.graphStats.push({ name: 'Licentiates', data: licentiates.graphStats })
|
||||
|
||||
const programmes = programmeNames ? Object.keys(programmeNames) : facultyProgrammes.map(programme => programme.code)
|
||||
programmes.forEach(programmeCode => {
|
||||
programmes.forEach(programmeId => {
|
||||
reversedYears.forEach(year => {
|
||||
if (programmeCounts && programmeCode in programmeCounts) {
|
||||
if (!(programmeCode in allThesisWriters.programmeTableStats)) {
|
||||
allThesisWriters.programmeTableStats[programmeCode] = []
|
||||
if (programmeCounts && programmeId in programmeCounts) {
|
||||
if (!(programmeId in allThesisWriters.programmeTableStats)) {
|
||||
allThesisWriters.programmeTableStats[programmeId] = []
|
||||
}
|
||||
allThesisWriters.programmeTableStats[programmeCode].push([year, ...programmeCounts[programmeCode][year]])
|
||||
allThesisWriters.programmeTableStats[programmeId].push([year, ...programmeCounts[programmeId][year]])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
const moment = require('moment')
|
||||
const { Semester, SemesterEnrollment, Studyright } = require('../models')
|
||||
|
||||
// categorize graduation times
|
||||
|
||||
const countTimeCategories = (times, goal) => {
|
||||
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
|
||||
times.forEach(time => {
|
||||
if (time <= goal) statistics.onTime += 1
|
||||
else if (time <= goal + 12) statistics.yearOver += 1
|
||||
else statistics.wayOver += 1
|
||||
})
|
||||
return statistics
|
||||
}
|
||||
|
||||
// discount staturory absences from graduation times
|
||||
|
||||
const formatAbsence = absence => {
|
||||
const { semestercode, semester } = absence
|
||||
return { semestercode, start: semester.startdate, end: semester.enddate }
|
||||
}
|
||||
|
||||
const statutoryAbsences = async (studentnumber, startdate, enddate) =>
|
||||
(
|
||||
await SemesterEnrollment.findAll({
|
||||
attributes: ['semestercode'],
|
||||
include: {
|
||||
model: Semester,
|
||||
attributes: ['startdate', 'enddate'],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.gte]: startdate,
|
||||
},
|
||||
enddate: {
|
||||
[Op.lte]: enddate,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
studentnumber: studentnumber,
|
||||
statutory_absence: true,
|
||||
},
|
||||
})
|
||||
).map(formatAbsence)
|
||||
|
||||
const getStatutoryAbsences = async (studentnumber, startdate, enddate) => {
|
||||
const absences = await statutoryAbsences(studentnumber, startdate, enddate)
|
||||
if (absences.length) {
|
||||
const absentMonths = absences.reduce((sum, ab) => sum + moment(ab.end).diff(moment(ab.start), 'months'), 0)
|
||||
return absentMonths
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// count bachelor time in Bc+Ms studyrights to master graduation times
|
||||
|
||||
const bachelorStudyright = async id => {
|
||||
return await Studyright.findOne({
|
||||
attributes: ['startdate'],
|
||||
where: {
|
||||
studyrightid: id,
|
||||
extentcode: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
countTimeCategories,
|
||||
getStatutoryAbsences,
|
||||
bachelorStudyright,
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
const mapOpenCredits = credit => {
|
||||
const { course_code, attainment_date, student_studentnumber, grade } = credit
|
||||
return {
|
||||
course_code,
|
||||
attainment_date,
|
||||
student_studentnumber,
|
||||
grade,
|
||||
}
|
||||
}
|
||||
|
||||
const mapStundentInfo = student => {
|
||||
const { email, secondary_email, studentnumber, dissemination_info_allowed } = student
|
||||
return {
|
||||
studentnumber,
|
||||
email,
|
||||
secondary_email,
|
||||
dissemination_info_allowed,
|
||||
}
|
||||
}
|
||||
const mapOpenEnrollments = enrollment => {
|
||||
const { studentnumber, course_code, state, enrollment_date_time } = enrollment
|
||||
return {
|
||||
enrollmentStudentnumber: studentnumber,
|
||||
course_code,
|
||||
state,
|
||||
enrollment_date_time,
|
||||
}
|
||||
}
|
||||
|
||||
const mapStudyRights = studyright => {
|
||||
const { enddate, startdate, student } = studyright
|
||||
return {
|
||||
startdate,
|
||||
enddate,
|
||||
studyrightStudentnumber: student.studentnumber,
|
||||
}
|
||||
}
|
||||
|
||||
const mapCourseInfo = course => {
|
||||
const { code, name } = course
|
||||
return {
|
||||
label: code,
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { mapOpenCredits, mapOpenEnrollments, mapStundentInfo, mapStudyRights, mapCourseInfo }
|
||||
@@ -0,0 +1,65 @@
|
||||
const logger = require('../../util/logger')
|
||||
const {
|
||||
getOpenUniSearchesByUser,
|
||||
createOpenUniPopulationSearch,
|
||||
updateOpenUniPopulationSearch,
|
||||
deleteOpenUniSearch,
|
||||
} = require('./openUniSearches')
|
||||
|
||||
const getOpenUniSearches = async userId => {
|
||||
const savedSearches = await getOpenUniSearchesByUser(userId)
|
||||
if (savedSearches === undefined) return []
|
||||
const foundSearches = savedSearches.reduce(
|
||||
(res, search) => [
|
||||
...res,
|
||||
{
|
||||
id: search.id,
|
||||
userId: search.userId,
|
||||
name: search.name,
|
||||
courseList: search.courseCodes,
|
||||
updatedAt: search.updatedAt,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
return foundSearches
|
||||
}
|
||||
|
||||
const createNewSearch = async (userId, name, courseCodes) => {
|
||||
const savedSearches = await getOpenUniSearchesByUser(userId)
|
||||
if (savedSearches !== undefined) {
|
||||
const searchNames = savedSearches.map(search => search.name)
|
||||
if (searchNames.includes(name)) return null
|
||||
}
|
||||
try {
|
||||
const created = await createOpenUniPopulationSearch(userId, name, courseCodes)
|
||||
if (!created) return null
|
||||
return created
|
||||
} catch (e) {
|
||||
logger.error(`Couldn't create new open uni search instance: ${e}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSearch = async (userId, id) => {
|
||||
try {
|
||||
const deleted = await deleteOpenUniSearch(userId, id)
|
||||
if (!deleted) return null
|
||||
return deleted
|
||||
} catch (e) {
|
||||
logger.error(`Couldn't delete open uni search instance: ${e}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const updateSearch = async (userId, id, courseCodes) => {
|
||||
try {
|
||||
const updated = await updateOpenUniPopulationSearch(userId, id, courseCodes)
|
||||
if (!updated) return null
|
||||
return updated
|
||||
} catch (e) {
|
||||
logger.error(`Couldn't update open uni search instance: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getOpenUniSearches, createNewSearch, deleteSearch, updateSearch }
|
||||
@@ -0,0 +1,135 @@
|
||||
const { Credit, Enrollment, Studyright, Student, Course } = require('../../models')
|
||||
const { OpenUniPopulationSearch } = require('../../models/models_kone')
|
||||
const {
|
||||
mapOpenCredits,
|
||||
mapOpenEnrollments,
|
||||
mapStundentInfo,
|
||||
mapStudyRights,
|
||||
mapCourseInfo,
|
||||
} = require('./openUniHelpers')
|
||||
const { Op } = require('sequelize')
|
||||
|
||||
// 1. iteration: time is hardcoded. Check hyväksytty grades
|
||||
const getCredits = async (courseCodes, startdate) =>
|
||||
(
|
||||
await Credit.findAll({
|
||||
attributes: ['course_code', 'student_studentnumber', 'grade', 'attainment_date'],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: courseCodes,
|
||||
},
|
||||
is_open: true,
|
||||
attainment_date: {
|
||||
[Op.gte]: startdate,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(mapOpenCredits)
|
||||
|
||||
const getStudentInfo = async students =>
|
||||
(
|
||||
await Student.findAll({
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: students,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(mapStundentInfo)
|
||||
|
||||
const getEnrollments = async (courseCodes, startdate, enddate) =>
|
||||
(
|
||||
await Enrollment.findAll({
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: courseCodes,
|
||||
},
|
||||
is_open: true,
|
||||
enrollment_date_time: {
|
||||
[Op.and]: {
|
||||
[Op.lte]: enddate,
|
||||
[Op.gte]: startdate,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(mapOpenEnrollments)
|
||||
|
||||
const getCourseNames = async courseCodes =>
|
||||
(
|
||||
await Course.findAll({
|
||||
attributes: ['code', 'name'],
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: courseCodes,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(mapCourseInfo)
|
||||
|
||||
const getStudyRights = async students =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
where: {
|
||||
student_studentnumber: {
|
||||
[Op.in]: students.length > 0 ? students : { [Op.not]: null },
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(mapStudyRights)
|
||||
|
||||
const getOpenUniSearchesByUser = async userId => {
|
||||
return await OpenUniPopulationSearch.findAll({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const createOpenUniPopulationSearch = async (userId, name, courseCodes) => {
|
||||
return await OpenUniPopulationSearch.create({
|
||||
userId,
|
||||
name,
|
||||
courseCodes,
|
||||
})
|
||||
}
|
||||
|
||||
const updateOpenUniPopulationSearch = async (userId, id, courseCodes) => {
|
||||
const searchToUpdate = await OpenUniPopulationSearch.findOne({
|
||||
where: {
|
||||
userId: userId,
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!searchToUpdate) return null
|
||||
return await searchToUpdate.update({ courseCodes })
|
||||
}
|
||||
|
||||
const deleteOpenUniSearch = async (userId, id) => {
|
||||
return await OpenUniPopulationSearch.destroy({
|
||||
where: {
|
||||
userId: userId,
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCredits,
|
||||
getEnrollments,
|
||||
getStudyRights,
|
||||
getStudentInfo,
|
||||
getCourseNames,
|
||||
getOpenUniSearchesByUser,
|
||||
createOpenUniPopulationSearch,
|
||||
updateOpenUniPopulationSearch,
|
||||
deleteOpenUniSearch,
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
const moment = require('moment')
|
||||
const { getCredits, getStudyRights, getEnrollments, getStudentInfo, getCourseNames } = require('./openUniSearches')
|
||||
|
||||
const uniq = objects => [...new Set(objects)]
|
||||
|
||||
const calulateTotalsForStudent = async (studentStats, studentnumber) => {
|
||||
Object.keys(studentStats[studentnumber].courseInfo).forEach(course => {
|
||||
if (studentStats[studentnumber].courseInfo[course].status.passed) {
|
||||
studentStats[studentnumber].totals.passed += 1
|
||||
} else if (studentStats[studentnumber].courseInfo[course].status.failed) {
|
||||
studentStats[studentnumber].totals.failed += 1
|
||||
} else if (studentStats[studentnumber].courseInfo[course].status.unfinished) {
|
||||
studentStats[studentnumber].totals.unfinished += 1
|
||||
}
|
||||
})
|
||||
}
|
||||
const getCustomOpenUniCourses = async (courseCodes, startdate, enddate) => {
|
||||
const ayCourseCodes = courseCodes.map(courseCode => 'AY' + courseCode)
|
||||
const allCourseCodes = courseCodes.concat(ayCourseCodes)
|
||||
const allCredits = await getCredits(allCourseCodes, startdate)
|
||||
const allEnrollments = await getEnrollments(allCourseCodes, startdate, enddate)
|
||||
const students = uniq(allEnrollments.map(enrollment => enrollment.enrollmentStudentnumber))
|
||||
const courses = await getCourseNames(courseCodes)
|
||||
const passedGrades = ['1', '2', '3', '4', '5', 'Hyv.', 'hyv.', 'HT', 'TT']
|
||||
const failedGrades = ['Hyl.', 'HYL', '0']
|
||||
|
||||
const allStudyrights = await getStudyRights(students)
|
||||
const studentInfo = await getStudentInfo(students)
|
||||
// Filter out current studyrights:
|
||||
// Case 1: Both startdate and enddate are outside of the given interval
|
||||
// Case 2: Startdate is inside of the given interval and enddate is outside
|
||||
// Case 3: Startdate is before the interval start and the enddate is within the interval
|
||||
const studentsWithCurrentStudyRight = allStudyrights
|
||||
.filter(
|
||||
right =>
|
||||
moment(right.startdate).isBetween(startdate, moment()) ||
|
||||
(moment(right.startdate).isSameOrBefore(startdate) && moment(right.enddate).isSameOrAfter(moment())) ||
|
||||
moment(right.enddate).isSameOrBefore(moment())
|
||||
)
|
||||
.map(right => right.studyrightStudentnumber)
|
||||
const uniqueStudentsWithCurrentStudyRight = uniq(studentsWithCurrentStudyRight)
|
||||
|
||||
const studentStats = {}
|
||||
const getEmptyCourseInfo = () => {
|
||||
return {
|
||||
status: {
|
||||
passed: null,
|
||||
failed: null,
|
||||
unfinished: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for (const { studentnumber, email, secondary_email, dissemination_info_allowed } of studentInfo) {
|
||||
// Check if the student has existing studyright: if yes, then stop here
|
||||
if (!uniqueStudentsWithCurrentStudyRight.includes(studentnumber)) {
|
||||
if (!(studentnumber in studentStats)) {
|
||||
studentStats[studentnumber] = {
|
||||
courseInfo: courseCodes.reduce((acc, code) => ({ ...acc, [code]: getEmptyCourseInfo() }), {}),
|
||||
email: email,
|
||||
secondaryEmail: secondary_email,
|
||||
disseminationInfoAllowed: dissemination_info_allowed,
|
||||
totals: { passed: 0, failed: 0, unfinished: 0 },
|
||||
}
|
||||
}
|
||||
for (const { course_code, attainment_date, student_studentnumber, grade } of allCredits) {
|
||||
if (student_studentnumber === studentnumber) {
|
||||
const courseCode = course_code.replace('AY', '')
|
||||
if (
|
||||
passedGrades.includes(grade) &&
|
||||
(!studentStats[studentnumber].courseInfo[courseCode].status.passed ||
|
||||
moment(studentStats[studentnumber].courseInfo[courseCode].status.passed).isBefore(attainment_date, 'day'))
|
||||
) {
|
||||
studentStats[studentnumber].courseInfo[courseCode].status.passed = attainment_date
|
||||
} else if (
|
||||
failedGrades.includes(grade) &&
|
||||
!studentStats[studentnumber].courseInfo[courseCode].status.passed &&
|
||||
(!studentStats[studentnumber].courseInfo[courseCode].status.failed ||
|
||||
moment(studentStats[studentnumber].courseInfo[courseCode].status.failed).isBefore(attainment_date, 'day'))
|
||||
) {
|
||||
studentStats[studentnumber].courseInfo[courseCode].status.failed = attainment_date
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const { enrollmentStudentnumber, course_code, enrollment_date_time } of allEnrollments) {
|
||||
if (enrollmentStudentnumber === studentnumber) {
|
||||
const courseCode = course_code.replace('AY', '')
|
||||
if (
|
||||
!studentStats[studentnumber].courseInfo[courseCode].status.passed &&
|
||||
!studentStats[studentnumber].courseInfo[courseCode].status.failed &&
|
||||
(!studentStats[studentnumber].courseInfo[courseCode].status.unfinished ||
|
||||
moment(studentStats[studentnumber].courseInfo[courseCode].status.unfinished).isBefore(
|
||||
enrollment_date_time,
|
||||
'day'
|
||||
))
|
||||
) {
|
||||
studentStats[studentnumber].courseInfo[courseCode].status.unfinished = enrollment_date_time
|
||||
}
|
||||
}
|
||||
}
|
||||
await calulateTotalsForStudent(studentStats, studentnumber)
|
||||
}
|
||||
}
|
||||
const openUniStats = { students: studentStats, courses: courses }
|
||||
return openUniStats
|
||||
}
|
||||
|
||||
module.exports = { getCustomOpenUniCourses }
|
||||
@@ -48,6 +48,7 @@ const formatStudentForPopulationStatistics = (
|
||||
creditcount,
|
||||
abbreviatedname,
|
||||
email,
|
||||
phone_number,
|
||||
studyrights,
|
||||
studyplans,
|
||||
semester_enrollments,
|
||||
@@ -66,7 +67,6 @@ const formatStudentForPopulationStatistics = (
|
||||
enrollments,
|
||||
credits,
|
||||
startDate,
|
||||
endDate,
|
||||
startDateMoment,
|
||||
endDateMoment
|
||||
) => {
|
||||
@@ -104,6 +104,7 @@ const formatStudentForPopulationStatistics = (
|
||||
gender_code,
|
||||
gender: { gender_en, gender_fi, gender_sv },
|
||||
email,
|
||||
phoneNumber: phone_number,
|
||||
semesterenrollments: semester_enrollments || [],
|
||||
updatedAt: updatedAt || createdAt,
|
||||
tags: tags || [],
|
||||
@@ -232,6 +233,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
'creditcount',
|
||||
'abbreviatedname',
|
||||
'email',
|
||||
'phone_number',
|
||||
'updatedAt',
|
||||
'gender_code',
|
||||
'birthdate',
|
||||
@@ -782,15 +784,7 @@ const formatStudentsForApi = async (
|
||||
if (student.studentnumber in optionData) student.option = optionData[student.studentnumber]
|
||||
else student.option = null
|
||||
stats.students.push(
|
||||
formatStudentForPopulationStatistics(
|
||||
student,
|
||||
enrollments,
|
||||
credits,
|
||||
startDate,
|
||||
endDate,
|
||||
startDateMoment,
|
||||
endDateMoment
|
||||
)
|
||||
formatStudentForPopulationStatistics(student, enrollments, credits, startDate, startDateMoment, endDateMoment)
|
||||
)
|
||||
return stats
|
||||
},
|
||||
|
||||
@@ -36,9 +36,7 @@ const studytrackStudents = async studentnumbers =>
|
||||
separate: true,
|
||||
attributes: ['credits', 'attainment_date'],
|
||||
where: {
|
||||
isStudyModule: {
|
||||
[Op.eq]: null,
|
||||
},
|
||||
[Op.or]: [{ isStudyModule: null }, { isStudyModule: false }],
|
||||
credittypecode: {
|
||||
[Op.in]: [4, 9],
|
||||
},
|
||||
@@ -221,6 +219,36 @@ const graduatedStudyRights = async (studytrack, since, studentnumbers) =>
|
||||
})
|
||||
).map(formatStudyright)
|
||||
|
||||
const startedStudyRightsByStartdate = async (studytrack, since, studentnumbers) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
where: {
|
||||
code: studytrack,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: whereStudents(studentnumbers),
|
||||
},
|
||||
})
|
||||
).map(formatStudyright)
|
||||
|
||||
const inactiveStudyrights = async (studytrack, studentnumbers) => {
|
||||
const now = moment(new Date())
|
||||
const inactiveOrExpired = (
|
||||
@@ -741,6 +769,7 @@ module.exports = {
|
||||
allStudyrights,
|
||||
startedStudyrights,
|
||||
graduatedStudyRights,
|
||||
startedStudyRightsByStartdate,
|
||||
inactiveStudyrights,
|
||||
previousStudyrights,
|
||||
followingStudyrights,
|
||||
|
||||
@@ -14,17 +14,42 @@ const {
|
||||
getThesisType,
|
||||
alltimeStartDate,
|
||||
alltimeEndDate,
|
||||
getId,
|
||||
getGoal,
|
||||
} = require('./studyprogrammeHelpers')
|
||||
const {
|
||||
graduatedStudyRights,
|
||||
startedStudyRightsByStartdate,
|
||||
getThesisCredits,
|
||||
followingStudyrights,
|
||||
previousStudyrights,
|
||||
allStudyrights,
|
||||
allTransfers,
|
||||
} = require('./studyprogramme')
|
||||
const { countTimeCategories, bachelorStudyright, getStatutoryAbsences } = require('./graduationHelpers')
|
||||
const { getAllProgrammes } = require('./studyrights')
|
||||
|
||||
const checkStartdate = async (id, startdate) => {
|
||||
if (id.slice(-2) === '-1') {
|
||||
return startdate
|
||||
}
|
||||
const studyright = await bachelorStudyright(id.replace(/-2$/, '-1'))
|
||||
if (studyright) return studyright.startdate
|
||||
return null
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
amounts[graduationYear] += 1
|
||||
times[graduationYear] = [...times[graduationYear], timeToGraduation]
|
||||
}
|
||||
|
||||
const getGraduatedStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
|
||||
const { graphStats, tableStats } = getStatsBasis(years)
|
||||
|
||||
@@ -77,6 +102,10 @@ 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 })
|
||||
// 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 studentnumbers = await getCorrectStudentnumbers({
|
||||
codes: [studyprogramme],
|
||||
@@ -87,32 +116,112 @@ const getGraduationTimeStats = async ({ studyprogramme, since, years, isAcademic
|
||||
|
||||
const studyrights = await graduatedStudyRights(studyprogramme, since, studentnumbers)
|
||||
|
||||
studyrights.forEach(({ enddate, studystartdate }) => {
|
||||
const graduationYear = defineYear(enddate, isAcademicYear)
|
||||
const timeToGraduation = moment(enddate).diff(moment(studystartdate), 'months')
|
||||
graduationAmounts[graduationYear] += 1
|
||||
graduationTimes[graduationYear] = [...graduationTimes[graduationYear], timeToGraduation]
|
||||
})
|
||||
// The maximum amount of months in the graph depends on the studyprogramme intended graduation time
|
||||
const comparisonValue = studyprogramme.includes('KH') ? 72 : 48
|
||||
// for masters, separate bc+ms combo from just ms students (except some medical progs)
|
||||
for (const right of studyrights) {
|
||||
if (doCombo && right.studyrightid.slice(-2) === '-2') {
|
||||
await addGraduation(right, isAcademicYear, graduationAmountsCombo, graduationTimesCombo)
|
||||
} else {
|
||||
await addGraduation(right, isAcademicYear, graduationAmounts, graduationTimes)
|
||||
}
|
||||
}
|
||||
|
||||
const medians = getYearsObject({ years, emptyArrays: true })
|
||||
const means = getYearsObject({ years, emptyArrays: true })
|
||||
const goal = getGoal(studyprogramme)
|
||||
const times = { medians: [], means: [], goal }
|
||||
const comboTimes = { medians: [], means: [], goal: goal + 36 }
|
||||
|
||||
// HighCharts graph require the data to have this format (ie. actual value, "empty value")
|
||||
years.forEach(year => {
|
||||
const rev = [...years].reverse()
|
||||
for (const year of rev) {
|
||||
const median = getMedian(graduationTimes[year])
|
||||
const mean = getMean(graduationTimes[year])
|
||||
medians[year] = [
|
||||
['', median],
|
||||
['', comparisonValue - median],
|
||||
]
|
||||
means[year] = [
|
||||
['', mean],
|
||||
['', comparisonValue - mean],
|
||||
]
|
||||
const statistics = countTimeCategories(graduationTimes[year], goal)
|
||||
times.medians = [...times.medians, { y: median, amount: graduationAmounts[year], name: year, statistics }]
|
||||
times.means = [...times.means, { y: mean, amount: graduationAmounts[year], name: year, statistics }]
|
||||
|
||||
if (doCombo) {
|
||||
const median = getMedian(graduationTimesCombo[year])
|
||||
const mean = getMean(graduationTimesCombo[year])
|
||||
const statistics = countTimeCategories(graduationTimesCombo[year], goal + 36)
|
||||
comboTimes.medians = [
|
||||
...comboTimes.medians,
|
||||
{ y: median, amount: graduationAmountsCombo[year], name: year, statistics },
|
||||
]
|
||||
comboTimes.means = [
|
||||
...comboTimes.means,
|
||||
{ y: mean, amount: graduationAmountsCombo[year], name: year, statistics },
|
||||
]
|
||||
}
|
||||
}
|
||||
return { times, doCombo, comboTimes }
|
||||
}
|
||||
|
||||
const getGraduationTimeStatsByStartYear = async ({
|
||||
studyprogramme,
|
||||
since,
|
||||
years,
|
||||
isAcademicYear,
|
||||
includeAllSpecials,
|
||||
}) => {
|
||||
let amounts = getYearsObject({ years })
|
||||
let gradTimes = getYearsObject({ years, emptyArrays: true })
|
||||
let classSizes = getYearsObject({ years })
|
||||
// for bc+ms combo
|
||||
// const doCombo = studyprogramme.startsWith('MH') && !['MH30_001', 'MH30_003'].includes(studyprogramme)
|
||||
// let amountsCombo = getYearsObject({ years })
|
||||
// let timesCombo = getYearsObject({ years, emptyArrays: true })
|
||||
// let classSizesCombo = getYearsObject({ years })
|
||||
|
||||
const studentnumbers = await getCorrectStudentnumbers({
|
||||
codes: [studyprogramme],
|
||||
startDate: since,
|
||||
endDate: alltimeEndDate,
|
||||
includeAllSpecials,
|
||||
})
|
||||
return { medians, means, graduationAmounts }
|
||||
const studyrights = await startedStudyRightsByStartdate(studyprogramme, since, studentnumbers)
|
||||
|
||||
for (const right of studyrights) {
|
||||
const startYear = defineYear(right.startdate, isAcademicYear)
|
||||
classSizes[startYear] += 1
|
||||
// if (right.prioritycode !== 1 && right.prioritycode !== 30) {
|
||||
// console.log(right)
|
||||
// }
|
||||
if (right.graduated) {
|
||||
const totalTimeToGraduation = moment(right.enddate).diff(moment(right.startdate), 'months')
|
||||
const statutoryAbsences = await getStatutoryAbsences(right.studentnumber, right.startdate, right.enddate)
|
||||
const timeToGraduation = totalTimeToGraduation - statutoryAbsences
|
||||
|
||||
amounts[startYear] += 1
|
||||
gradTimes[startYear] = [...gradTimes[startYear], timeToGraduation]
|
||||
}
|
||||
}
|
||||
|
||||
const goal = getGoal(studyprogramme)
|
||||
const times = { medians: [], means: [], goal }
|
||||
const comboTimes = { medians: [], means: [], goal: goal + 36 }
|
||||
|
||||
const rev = [...years].reverse()
|
||||
for (const year of rev) {
|
||||
const median = getMedian(gradTimes[year])
|
||||
const mean = getMean(gradTimes[year])
|
||||
const statistics = countTimeCategories(gradTimes[year], goal)
|
||||
times.medians = [
|
||||
...times.medians,
|
||||
{ y: median, amount: amounts[year], name: year, statistics, classSize: classSizes[year] },
|
||||
]
|
||||
times.means = [
|
||||
...times.means,
|
||||
{ y: mean, amount: amounts[year], name: year, statistics, classSize: classSizes[year] },
|
||||
]
|
||||
|
||||
// if (doCombo) {
|
||||
// const median = getMedian(timesCombo[year])
|
||||
// const mean = getMean(timesCombo[year])
|
||||
// const statistics = countTimeCategories(timesCombo[year], goal + 36)
|
||||
// comboTimes.medians = [...comboTimes.medians, { y: median, amount: amountsCombo[year], name: year, statistics }]
|
||||
// comboTimes.means = [...comboTimes.means, { y: mean, amount: amountsCombo[year], name: year, statistics }]
|
||||
// }
|
||||
}
|
||||
|
||||
return { times, comboTimes }
|
||||
}
|
||||
|
||||
// Creates (studyrightid, transfer)-map out of programmes transfers
|
||||
@@ -126,7 +235,7 @@ 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, 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
|
||||
@@ -223,13 +332,16 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) =>
|
||||
const thesis = await getThesisStats(queryParameters)
|
||||
|
||||
const graduated = await getGraduatedStats(queryParameters)
|
||||
|
||||
const graduationTimeStats = await getGraduationTimeStats(queryParameters)
|
||||
const graduationTimeStatsByStartYear = await getGraduationTimeStatsByStartYear(queryParameters)
|
||||
|
||||
const programmesBeforeOrAfter = await getProgrammesBeforeOrAfter(studyprogramme, queryParameters)
|
||||
|
||||
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
|
||||
|
||||
const titles = ['', 'Graduated', 'Wrote thesis']
|
||||
const programmesBeforeOrAfterTitles = ['Code', 'Programme', ...years]
|
||||
const programmesBeforeOrAfterTitles = ['Code', 'Id', 'Programme', ...years]
|
||||
const tableStats = reversedYears.map(year => [year, graduated.tableStats[year], thesis.tableStats[year]])
|
||||
|
||||
return {
|
||||
@@ -247,9 +359,11 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) =>
|
||||
data: thesis.graphStats,
|
||||
},
|
||||
],
|
||||
graduationMedianTime: graduationTimeStats.medians,
|
||||
graduationMeanTime: graduationTimeStats.means,
|
||||
graduationAmounts: graduationTimeStats.graduationAmounts,
|
||||
graduationTimes: graduationTimeStats.times,
|
||||
doCombo: graduationTimeStats.doCombo,
|
||||
comboTimes: graduationTimeStats.comboTimes,
|
||||
graduationTimesByStartYear: graduationTimeStatsByStartYear.times,
|
||||
comboTimesByStartYear: graduationTimeStatsByStartYear.comboTimes,
|
||||
programmesBeforeOrAfterTableStats: programmesBeforeOrAfter?.tableStats,
|
||||
programmesBeforeOrAfterGraphStats: programmesBeforeOrAfter?.graphStats,
|
||||
programmesBeforeOrAfterTitles,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { mean } = require('lodash')
|
||||
const { studentnumbersWithAllStudyrightElements } = require('./populations')
|
||||
const { codes } = require('../../config/programmeCodes')
|
||||
|
||||
// Helper functions
|
||||
const getCorrectStudentnumbers = async ({ codes, startDate, endDate, includeAllSpecials, includeGraduated = true }) => {
|
||||
@@ -44,6 +45,7 @@ const resolveStudyRightCode = studyright_elements => {
|
||||
const formatStudyright = studyright => {
|
||||
const {
|
||||
studyrightid,
|
||||
startdate,
|
||||
studystartdate,
|
||||
enddate,
|
||||
givendate,
|
||||
@@ -57,6 +59,7 @@ const formatStudyright = studyright => {
|
||||
|
||||
return {
|
||||
studyrightid,
|
||||
startdate,
|
||||
studystartdate: defineStartDate(studyright_elements, studystartdate),
|
||||
enddate,
|
||||
givendate,
|
||||
@@ -288,7 +291,36 @@ const getMasterCreditGraphStats = years => ({
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
})
|
||||
|
||||
const getVetenaryCreditGraphStats = years => ({
|
||||
lte210: {
|
||||
name: 'Less than 210 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
lte240: {
|
||||
name: '210-239 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
lte270: {
|
||||
name: '240-269 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
lte300: {
|
||||
name: '270-299 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
lte330: {
|
||||
name: '300-329 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
lte360: {
|
||||
name: '330-359 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
mte360: {
|
||||
name: '360 or more credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
})
|
||||
const getOnlyMasterCreditGraphStats = years => ({
|
||||
lte15: {
|
||||
name: 'Less than 15 credits',
|
||||
@@ -361,11 +393,15 @@ const bachelorCreditAmounts = [15, 30, 60, 90, 120, 150, 180, 180]
|
||||
const masterCreditAmounts = [200, 220, 240, 260, 280, 300, 300]
|
||||
const onlyMasterCreditAmounts = [15, 30, 60, 90, 120, 120]
|
||||
const doctoralCreditAmounts = [50, 100, 150, 200, 250, 300, 300]
|
||||
|
||||
const vetenaryCreditThresholds = ['lte210', 'lte240', 'lte270', 'lte300', 'lte330', 'lte360', 'mte360']
|
||||
const vetenaryCreditAmounts = [210, 240, 270, 300, 330, 360, 360]
|
||||
const getOnlyMasterThresholds = () => {
|
||||
return { msOnlyCreditThresholdKeys: onlyMasterCreditThresholds, msOnlyCreditThresholdAmount: onlyMasterCreditAmounts }
|
||||
}
|
||||
const getCreditThresholds = studyprogramme => {
|
||||
if (studyprogramme === 'MH90_001') {
|
||||
return { creditThresholdKeys: vetenaryCreditThresholds, creditThresholdAmounts: vetenaryCreditAmounts }
|
||||
}
|
||||
if (studyprogramme.includes('KH')) {
|
||||
return { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
|
||||
}
|
||||
@@ -402,7 +438,7 @@ const tableTitles = {
|
||||
'90-119 credits',
|
||||
'120-149 credits',
|
||||
'150-179 credits',
|
||||
'>= 180 credits',
|
||||
'180 ≤ credits',
|
||||
],
|
||||
master: [
|
||||
'',
|
||||
@@ -413,7 +449,7 @@ const tableTitles = {
|
||||
'240-259 credits',
|
||||
'260-279 credits',
|
||||
'280-299 credits',
|
||||
'>= 300 credits',
|
||||
'300 ≤ credits',
|
||||
],
|
||||
masterOnly: [
|
||||
'',
|
||||
@@ -423,7 +459,7 @@ const tableTitles = {
|
||||
'30-59 credits',
|
||||
'60-89 credits',
|
||||
'90-119 credits',
|
||||
'>= 120 credits',
|
||||
'120 ≤ credits',
|
||||
],
|
||||
doctoral: [
|
||||
'',
|
||||
@@ -434,7 +470,7 @@ const tableTitles = {
|
||||
'150-199 credits',
|
||||
'200-249 credits',
|
||||
'250-299 credits',
|
||||
'>= 300 credits',
|
||||
'300 ≤ credits',
|
||||
],
|
||||
},
|
||||
studytracks: [
|
||||
@@ -457,6 +493,47 @@ const getCreditProgressTableTitles = studyprogramme => {
|
||||
return tableTitles.creditProgress.doctoral
|
||||
}
|
||||
|
||||
const mapCodesToIds = data => {
|
||||
// Add programme id e.g. TKT
|
||||
const keys = Object.keys(codes)
|
||||
const progs = Object.keys(data)
|
||||
|
||||
for (const prog of progs) {
|
||||
if (keys.includes(prog)) {
|
||||
data[prog].progId = codes[prog].toUpperCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getId = code => {
|
||||
if (Object.keys(codes).includes(code)) {
|
||||
return codes[code].toUpperCase()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const getGoal = programme => {
|
||||
if (programme.startsWith('KH') || programme.endsWith('-ba')) {
|
||||
return 36
|
||||
}
|
||||
if (programme.startsWith('MH') || programme.endsWith('-ma')) {
|
||||
if (['MH30_004', '420420-ma'].includes(programme)) {
|
||||
return 24 + 6
|
||||
}
|
||||
if (['MH30_001', 'MH30_003', '320011-ma', '320001-ma', '320002-ma'].includes(programme)) {
|
||||
return 36 + 24 + 12 // medical, no separate bachelor
|
||||
}
|
||||
return 24
|
||||
}
|
||||
if (programme.includes('T')) {
|
||||
return 48
|
||||
}
|
||||
if (programme.startsWith('LI')) {
|
||||
return 78
|
||||
}
|
||||
return 48 //unknown, likely old doctor or licentiate
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCorrectStudentnumbers,
|
||||
formatStudyright,
|
||||
@@ -478,6 +555,7 @@ module.exports = {
|
||||
getPercentage,
|
||||
getBachelorCreditGraphStats,
|
||||
getMasterCreditGraphStats,
|
||||
getVetenaryCreditGraphStats,
|
||||
getDoctoralCreditGraphStats,
|
||||
getOnlyMasterCreditGraphStats,
|
||||
getCreditGraphStats,
|
||||
@@ -486,4 +564,7 @@ module.exports = {
|
||||
getCreditProgressTableTitles,
|
||||
getOnlyMasterThresholds,
|
||||
isNonMajorCredit,
|
||||
mapCodesToIds,
|
||||
getId,
|
||||
getGoal,
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ const {
|
||||
getCreditGraphStats,
|
||||
tableTitles,
|
||||
getCreditProgressTableTitles,
|
||||
getGoal,
|
||||
} = require('./studyprogrammeHelpers')
|
||||
const {
|
||||
studytrackStudents,
|
||||
@@ -25,6 +26,7 @@ const {
|
||||
graduatedStudyRights,
|
||||
} = require('./studyprogramme')
|
||||
const { getAcademicYearDates } = require('../util/semester')
|
||||
const { countTimeCategories } = require('./graduationHelpers')
|
||||
|
||||
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) => {
|
||||
let data = { female: 0, male: 0, finnish: 0 }
|
||||
@@ -55,36 +57,30 @@ const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) =>
|
||||
return data
|
||||
}
|
||||
|
||||
const getGraduationTimeStats = async ({
|
||||
year,
|
||||
graduated,
|
||||
studyprogramme,
|
||||
track,
|
||||
graduationMeanTime,
|
||||
graduationMedianTime,
|
||||
graduationAmounts,
|
||||
}) => {
|
||||
const getGraduationTimeStats = async ({ year, graduated, track, graduationAmounts, graduationTimes, classSize }) => {
|
||||
if (year === 'Total') {
|
||||
return
|
||||
}
|
||||
|
||||
// Count how long each student took to graduate
|
||||
let graduationTimes = []
|
||||
graduated.forEach(({ enddate, studystartdate }) => {
|
||||
const timeToGraduation = moment(enddate).diff(moment(studystartdate), 'months')
|
||||
let times = []
|
||||
graduated.forEach(({ enddate, startdate }) => {
|
||||
const timeToGraduation = moment(enddate).diff(moment(startdate), 'months')
|
||||
graduationAmounts[track][year] += 1
|
||||
graduationTimes = [...graduationTimes, timeToGraduation]
|
||||
times = [...times, timeToGraduation]
|
||||
})
|
||||
|
||||
// The maximum amount of months in the graph depends on the studyprogramme intended graduation time
|
||||
const comparisonValue = studyprogramme.includes('KH') ? 72 : 48
|
||||
const median = getMedian(times)
|
||||
const mean = getMean(times)
|
||||
const statistics = countTimeCategories(times, graduationTimes.goal)
|
||||
|
||||
// HighCharts graph requires the data to have this format (ie. actual value, "empty value")
|
||||
const median = getMedian(graduationTimes)
|
||||
const mean = getMean(graduationTimes)
|
||||
graduationMedianTime[track][year] = [
|
||||
['', median],
|
||||
['', comparisonValue - median],
|
||||
graduationTimes[track].medians = [
|
||||
...graduationTimes[track].medians,
|
||||
{ y: median, amount: graduationAmounts[track][year], name: year, statistics, classSize },
|
||||
]
|
||||
graduationMeanTime[track][year] = [
|
||||
['', mean],
|
||||
['', comparisonValue - mean],
|
||||
graduationTimes[track].means = [
|
||||
...graduationTimes[track].means,
|
||||
{ y: mean, amount: graduationAmounts[track][year], name: year, statistics, classSize },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -109,9 +105,8 @@ const getStudytrackDataForTheYear = async ({
|
||||
mainStatsByTrack,
|
||||
creditGraphStats,
|
||||
creditTableStats,
|
||||
graduationMeanTime,
|
||||
graduationMedianTime,
|
||||
graduationAmounts,
|
||||
graduationTimes,
|
||||
totalAmounts,
|
||||
emptyTracks,
|
||||
} = data
|
||||
@@ -208,18 +203,28 @@ const getStudytrackDataForTheYear = async ({
|
||||
]
|
||||
|
||||
// Count stats for the graduation time charts grouped by year
|
||||
if (!(track in graduationTimes)) {
|
||||
graduationTimes[track] = { medians: [], means: [] }
|
||||
}
|
||||
totalAmounts[track][year] = await all.length
|
||||
await getGraduationTimeStats({
|
||||
year,
|
||||
graduated,
|
||||
studyprogramme,
|
||||
track,
|
||||
graduationMedianTime,
|
||||
graduationMeanTime,
|
||||
graduationAmounts,
|
||||
graduationTimes,
|
||||
classSize: totalAmounts[track][year],
|
||||
})
|
||||
})
|
||||
)
|
||||
Object.keys(graduationTimes).forEach(track => {
|
||||
if (track !== 'goal') {
|
||||
const sortedMedians = graduationTimes[track].medians.sort((a, b) => b.name.localeCompare(a.name))
|
||||
const sortedMeans = graduationTimes[track].means.sort((a, b) => b.name.localeCompare(a.name))
|
||||
graduationTimes[track].medians = sortedMedians
|
||||
graduationTimes[track].means = sortedMeans
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Defines the studytrack names for the studytrack selector
|
||||
@@ -241,9 +246,8 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
|
||||
const mainStatsByTrack = {}
|
||||
const creditGraphStats = {}
|
||||
const creditTableStats = {}
|
||||
const graduationMedianTime = {}
|
||||
const graduationMeanTime = {}
|
||||
const graduationAmounts = {}
|
||||
const graduationTimes = { goal: getGoal(studyprogramme) }
|
||||
const totalAmounts = {}
|
||||
const emptyTracks = new Map()
|
||||
|
||||
@@ -251,8 +255,6 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
|
||||
mainStatsByTrack[track] = []
|
||||
creditGraphStats[track] = getCreditGraphStats(studyprogramme, years)
|
||||
creditTableStats[track] = []
|
||||
graduationMedianTime[track] = getYearsObject({ years, emptyArrays: true })
|
||||
graduationMeanTime[track] = getYearsObject({ years, emptyArrays: true })
|
||||
graduationAmounts[track] = getYearsObject({ years })
|
||||
totalAmounts[track] = getYearsObject({ years })
|
||||
})
|
||||
@@ -262,9 +264,8 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
|
||||
mainStatsByTrack,
|
||||
creditGraphStats,
|
||||
creditTableStats,
|
||||
graduationMedianTime,
|
||||
graduationMeanTime,
|
||||
graduationAmounts,
|
||||
graduationTimes,
|
||||
totalAmounts,
|
||||
emptyTracks,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ const courseStatisticsGroup = 'grp-oodikone-basic-users'
|
||||
const facultyStatisticsGroup = 'grp-oodikone-users'
|
||||
const hyOneGroup = 'hy-one'
|
||||
const toskaGroup = 'grp-toska'
|
||||
const openUniGroup = 'hy-ypa-opa-dojo'
|
||||
|
||||
// Max 25 users can be stored in the cache
|
||||
const userDataCache = new LRU({
|
||||
@@ -272,7 +273,13 @@ const getUser = async ({ username, name, email, iamGroups, sisId }) => {
|
||||
newAccessGroups.push('facultyStatistics')
|
||||
if (iamGroups.includes(hyOneGroup) || currentAccessGroups.includes('teachers')) newAccessGroups.push('teachers')
|
||||
if (iamGroups.includes(toskaGroup) || currentAccessGroups.includes('admin')) newAccessGroups.push('admin')
|
||||
if (_.difference(newAccessGroups, currentAccessGroups).length > 0) {
|
||||
if (iamGroups.includes(openUniGroup)) newAccessGroups.push('openUniSearch')
|
||||
// In the difference method "the order and references of result values are determined by the first array." https://lodash.com/docs/4.17.15#difference
|
||||
// Both directions needs to be checked in order to update roles when the access should no longer exists.
|
||||
if (
|
||||
_.difference(newAccessGroups, currentAccessGroups).length > 0 ||
|
||||
_.difference(currentAccessGroups, newAccessGroups).length > 0
|
||||
) {
|
||||
userFromDb.setAccessgroup(
|
||||
(
|
||||
await AccessGroup.findAll({
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { Credit, Enrollment, Studyright } = require('../models')
|
||||
const moment = require('moment')
|
||||
const { Op } = require('sequelize')
|
||||
|
||||
const hasValidStudyright = (studyrights, date) => {
|
||||
const within = (right, date) => {
|
||||
const first = moment(right.startdate)
|
||||
const last = moment(right.enddate)
|
||||
return first.isSameOrBefore(date) && last.isSameOrAfter(date)
|
||||
}
|
||||
|
||||
return studyrights.find(r => within(r, date))
|
||||
}
|
||||
|
||||
const studyrightsOf = async student => {
|
||||
const rights = await Studyright.findAll({
|
||||
where: {
|
||||
student_studentnumber: student,
|
||||
},
|
||||
})
|
||||
|
||||
return rights
|
||||
}
|
||||
|
||||
const uniq = objects => [...new Set(objects)]
|
||||
|
||||
const courseInfo = async course_code => {
|
||||
const credits = await Credit.findAll({
|
||||
where: {
|
||||
course_code,
|
||||
is_open: true,
|
||||
grade: {
|
||||
[Op.in]: ['1', '2', '3', '4', '5', 'Hyv.'],
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.gte]: moment('8-1-2020', 'MM-DD-YYYY').toDate(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const enrollments = await Enrollment.findAll({
|
||||
where: {
|
||||
course_code,
|
||||
is_open: true,
|
||||
enrollment_date_time: {
|
||||
[Op.lte]: moment('8-1-2022', 'MM-DD-YYYY').toDate(),
|
||||
[Op.gte]: moment('8-1-2020', 'MM-DD-YYYY').toDate(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let avoin_credited = []
|
||||
|
||||
for (let credit of credits) {
|
||||
const { attainment_date, student_studentnumber: studentnumber } = credit
|
||||
const rights = await studyrightsOf(studentnumber)
|
||||
if (!hasValidStudyright(rights, attainment_date)) {
|
||||
avoin_credited.push(studentnumber)
|
||||
}
|
||||
}
|
||||
|
||||
let avoin_enrolled = []
|
||||
|
||||
for (let enrollment of enrollments) {
|
||||
const { enrollment_date_time, studentnumber } = enrollment
|
||||
const rights = await studyrightsOf(studentnumber)
|
||||
if (!hasValidStudyright(rights, enrollment_date_time)) {
|
||||
avoin_enrolled.push(studentnumber)
|
||||
}
|
||||
}
|
||||
|
||||
avoin_credited = uniq(avoin_credited)
|
||||
avoin_enrolled = uniq(avoin_enrolled)
|
||||
|
||||
return {
|
||||
credits,
|
||||
enrollments,
|
||||
avoin_credited,
|
||||
avoin_enrolled,
|
||||
}
|
||||
}
|
||||
|
||||
const unifiedCourseInfo = async course_code => {
|
||||
const a = await courseInfo(course_code)
|
||||
const b = await courseInfo('AY' + course_code)
|
||||
|
||||
avoin_credited = uniq(a.avoin_credited.concat(b.avoin_credited))
|
||||
avoin_enrolled = uniq(a.avoin_enrolled.concat(b.avoin_enrolled))
|
||||
|
||||
return {
|
||||
credits: uniq(a.credits.concat(b.credits)),
|
||||
enrollments: uniq(a.enrollments.concat(b.enrollments)),
|
||||
avoin_credited,
|
||||
avoin_enrolled,
|
||||
only_enrolled: avoin_enrolled.filter(e => !avoin_credited.includes(e)),
|
||||
}
|
||||
}
|
||||
|
||||
const statsOf = async courses => {
|
||||
console.log(Object.keys(courses).join('\n'))
|
||||
console.log()
|
||||
let students = []
|
||||
for (let course_code of Object.keys(courses)) {
|
||||
const { credits, enrollments, avoin_enrolled, avoin_credited, only_enrolled } = await unifiedCourseInfo(course_code)
|
||||
courses[course_code] = { credits, enrollments, avoin_enrolled, avoin_credited, only_enrolled }
|
||||
students = uniq(students.concat(avoin_enrolled).concat(avoin_credited))
|
||||
}
|
||||
|
||||
const rows = []
|
||||
|
||||
for (let s of students) {
|
||||
let string = s + ' '
|
||||
let score = 0
|
||||
for (let course_code of Object.keys(courses)) {
|
||||
const { enrollments, avoin_credited, only_enrolled } = courses[course_code]
|
||||
let course_info = '-'
|
||||
if (avoin_credited.includes(s)) {
|
||||
course_info = 'X'
|
||||
score += 10
|
||||
}
|
||||
|
||||
if (only_enrolled.includes(s)) {
|
||||
const studentsEnrolments = enrollments.filter(e => e.studentnumber === s)
|
||||
course_info = '' + studentsEnrolments.length
|
||||
|
||||
score += 1
|
||||
}
|
||||
|
||||
string += course_info + ' '
|
||||
}
|
||||
|
||||
const rights = await studyrightsOf(s)
|
||||
const stRight = hasValidStudyright(rights, new Date('2022-8-1')) ? 'yes' : 'no'
|
||||
|
||||
string += stRight
|
||||
|
||||
rows.push({ string, score })
|
||||
}
|
||||
|
||||
const byScore = (a, b) => b.score - a.score
|
||||
|
||||
console.log(
|
||||
rows
|
||||
.sort(byScore)
|
||||
.map(r => r.string)
|
||||
.join('\n')
|
||||
)
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
const courses_psyk_a = {
|
||||
'PSYK-111': null,
|
||||
'PSYK-121': null,
|
||||
'PSYK-131': null,
|
||||
'PSYK-141': null,
|
||||
'PSYK-181': null,
|
||||
}
|
||||
|
||||
const courses_psyk_p = {
|
||||
'PSYK-251': null,
|
||||
'PSYK-221': null,
|
||||
'PSYK-232': null,
|
||||
'PSYK-226': null,
|
||||
'PSYK-241': null,
|
||||
'PSYK-212': null,
|
||||
}
|
||||
|
||||
const courses_hist = {
|
||||
'HISK-110': null,
|
||||
'HISK-120': null,
|
||||
'HISK-131A': null,
|
||||
'HISK-131': null,
|
||||
'HISK-141': null,
|
||||
'HISK-142': null,
|
||||
}
|
||||
|
||||
await statsOf(courses_psyk_a)
|
||||
console.log()
|
||||
console.log()
|
||||
await statsOf(courses_psyk_p)
|
||||
console.log()
|
||||
console.log()
|
||||
await statsOf(courses_hist)
|
||||
|
||||
process.exit()
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -1,7 +1,8 @@
|
||||
export default {
|
||||
Advanced: `**Semesters:** Jos haluaa tarkastella vain syyslukukaudella tai kevätlukukaudella ilmoittautuneita, haluttu ajanjakso valitaan tästä.
|
||||
Advanced: `**Semesters:** Jos haluaa tarkastella vain syyslukukaudella tai kevätlukukaudella aloittaneita, haluttu ajanjakso valitaan tästä.
|
||||
|
||||
**Include:** Koulutusohjelmapopulaatiosta on oletusarvoisesti suodatettu pois vaihto-opiskelijat ja erillisopinto-oikeudella opiskelevat. Näitä ryhmiä voi sisällyttää mukaan lisäämällä ruksin haluamaansa kohtaan.`,
|
||||
**Include:** Koulutusohjelmapopulaatiosta on oletusarvoisesti suodatettu pois ohjelmasta poissiirtyneet opiskelijat. Heidät voi sisällyttää mukaan lisäämällä ruksin ja noutamalla tulokset uudestaan (Fetch class with new settings).`,
|
||||
// Koulutusohjelmapopulaatiosta on oletusarvoisesti suodatettu pois vaihto-opiskelijat ja erillisopinto-oikeudella opiskelevat. Näitä ryhmiä voi sisällyttää mukaan lisäämällä ruksin haluamaansa kohtaan.
|
||||
QueryCard: `**Valitun koulutusohjelmavuosikurssin perustiedot.**
|
||||
|
||||
**Sample size** luku sisältää läsnä- ja poissaolevaksi ilmoittautuneet opiskelijat, muttei "excludes"-ryhmiä, jotka voi määritellä "advanced settings" -kohdasta.
|
||||
|
||||
@@ -41,11 +41,29 @@ export default {
|
||||
**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.
|
||||
`,
|
||||
AverageGraduationTimes: `
|
||||
**Median time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina. Luku sisältää mahdolliset poissaolokuukaudet.
|
||||
**Median time**: Näyttää valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
|
||||
|
||||
**Mean time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina. Luku sisältää mahdolliset poissaolokuukaudet.
|
||||
**Mean time**: Näyttää valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina.
|
||||
|
||||
n = **valmistuneet opiskelijat / kaikki ohjelmassa kyseisenä vuonna alkaneet opinto-oikeudet**. Kandiohjelmissa tavoiteaika on 36 kk, maisteriohjelmissa 24 kk`,
|
||||
Valmistumisajoista on vähennetty lakisääteiset poissaolot.
|
||||
|
||||
Yksittäinen palkki kertoo, kuinka moni opiskelija on valmistunut kyseisenä vuonna.
|
||||
|
||||
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
|
||||
Vihreä **vaakapalkki** merkitsee, että keskimääräinen valmistumisaika on alle tai yhtä kuin tavoiteaika, keltainen palkki merkitsee korkeintaan vuoden ylitystä
|
||||
ja punainen yli vuoden tavoiteajan ylitystä.`,
|
||||
AverageGraduationTimesStudytracks: `
|
||||
**Median time**: Näyttää valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
|
||||
|
||||
**Mean time**: Näyttää valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina.
|
||||
|
||||
Valmistumisajoista on vähennetty lakisääteiset poissaolot.
|
||||
|
||||
Yksittäinen palkki kertoo, kuinka moni kyseisenä **lukukautena aloittaneista** opiskelijoista on valmistunut sekä heidän prosentuaalinen osuutensa koko vuosikurssista.
|
||||
|
||||
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
|
||||
Vihreä **vaakapalkki** merkitsee, että keskimääräinen valmistumisaika on alle tai yhtä kuin tavoiteaika, keltainen palkki merkitsee korkeintaan vuoden ylitystä
|
||||
ja punainen yli vuoden tavoiteajan ylitystä.`,
|
||||
ProgrammesBeforeOrAfter: `
|
||||
**Mikäli valittuna on kandiohjelma:** Sisältää maisteriohjelmat, joissa tämän kandiohjelman opiskelijat ovat aloittaneet valmistuttuaan kandeiksi. Vuosijaottelu on tehty maisteriohjelmassa aloittamisen perusteella.
|
||||
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
// Map programme codes to "letter indentifier"
|
||||
|
||||
export default {
|
||||
KH10_001: 'tuk',
|
||||
MH10_001: 'tum',
|
||||
KH20_001: 'on',
|
||||
MH20_001: 'otm',
|
||||
MH20_002: 'ibl',
|
||||
MH20_003: 'ggl',
|
||||
KH30_001: 'psyk',
|
||||
MH30_004: 'psym',
|
||||
KH30_002: 'logk',
|
||||
MH30_005: 'logm',
|
||||
MH30_001: 'll',
|
||||
MH30_002: 'tmed',
|
||||
MH30_003: 'hll',
|
||||
KH40_001: 'filk',
|
||||
KH40_002: 'ttk',
|
||||
KH40_003: 'kik',
|
||||
KH40_004: 'kok',
|
||||
KH40_005: 'kuka',
|
||||
KH40_006: 'hisk',
|
||||
MH40_001: 'ttm',
|
||||
MH40_002: 'kim',
|
||||
MH40_003: 'eng',
|
||||
MH40_004: 'rus',
|
||||
MH40_005: 'lingdig',
|
||||
MH40_006: 'tra',
|
||||
MH40_007: 'suku',
|
||||
MH40_008: 'nor',
|
||||
MH40_009: 'kir',
|
||||
MH40_010: 'kuma',
|
||||
MH40_011: 'ice',
|
||||
MH40_012: 'alku',
|
||||
MH40_014: 'spt',
|
||||
MH40_015: 'hism',
|
||||
KH50_001: 'mat',
|
||||
KH50_002: 'fys',
|
||||
KH50_003: 'kemk',
|
||||
KH50_004: 'mfkk',
|
||||
KH50_005: 'tkt',
|
||||
KH50_006: 'geok',
|
||||
KH50_007: 'maa',
|
||||
KH50_008: 'bsc',
|
||||
MH50_001: 'mast',
|
||||
MH50_002: 'lsi',
|
||||
MH50_003: 'tcm',
|
||||
MH50_004: 'paras',
|
||||
MH50_005: 'matres',
|
||||
MH50_006: 'atm',
|
||||
MH50_007: 'kem',
|
||||
MH50_008: 'mfkm',
|
||||
MH50_009: 'csm',
|
||||
MH50_010: 'data',
|
||||
MH50_011: 'geom',
|
||||
MH50_012: 'geog',
|
||||
MH50_013: 'usp',
|
||||
KH55_001: 'farm',
|
||||
MH55_001: 'prov',
|
||||
MH55_002: 'mpharm',
|
||||
KH57_001: 'bio',
|
||||
KH57_002: 'mole',
|
||||
KH57_003: 'env',
|
||||
MH57_001: 'eeb',
|
||||
MH57_002: 'ips',
|
||||
MH57_003: 'gmb',
|
||||
MH57_004: 'neuro',
|
||||
MH57_005: 'ecgs',
|
||||
KH60_001: 'eduk',
|
||||
MH60_001: 'edum',
|
||||
MH60_002: 'ce',
|
||||
KH70_001: 'pvk',
|
||||
KH70_002: 'yk',
|
||||
KH70_003: 'sosk',
|
||||
KH70_004: 'ecok',
|
||||
MH70_001: 'film',
|
||||
MH70_002: 'pvm',
|
||||
MH70_003: 'gpc',
|
||||
MH70_004: 'ym',
|
||||
MH70_005: 'cos',
|
||||
MH70_006: 'ens',
|
||||
MH70_007: 'msv',
|
||||
MH70_008: 'sosm',
|
||||
MH70_009: 'econ',
|
||||
MH70_011: 'sote',
|
||||
KH74_001: 'ksv',
|
||||
KH80_001: 'maatk',
|
||||
KH80_002: 'metsak',
|
||||
KH80_003: 'etk',
|
||||
KH80_004: 'yet',
|
||||
MH80_001: 'agri',
|
||||
MH80_002: 'agere',
|
||||
MH80_003: 'for',
|
||||
MH80_004: 'food',
|
||||
MH80_005: 'hnfb',
|
||||
MH80_006: 'ekm',
|
||||
MH80_007: 'mmb',
|
||||
KH90_001: 'elk',
|
||||
MH90_001: 'ell',
|
||||
T920101: 'dptheol',
|
||||
T920102: 'dplaw',
|
||||
T920111: 'philartsoc',
|
||||
T920103: 'dphistcult',
|
||||
T920104: 'dplang',
|
||||
T920105: 'sky',
|
||||
T920106: 'dpsocs',
|
||||
T920107: 'pyam',
|
||||
T920108: 'dpe',
|
||||
T920109: 'seduce',
|
||||
T920110: 'clic',
|
||||
T921101: 'dpbm',
|
||||
T921102: 'klto',
|
||||
T921103: 'docpop',
|
||||
T921104: 'findos',
|
||||
T921105: 'dpdr',
|
||||
T921106: 'ils',
|
||||
T921107: 'bandm',
|
||||
T921108: 'cvm',
|
||||
T921109: 'dphub',
|
||||
T922101: 'luova',
|
||||
T922102: 'dpps',
|
||||
T922103: 'denvi',
|
||||
T922104: 'agforee',
|
||||
T922105: 'mbdp',
|
||||
T922106: 'foodhealth',
|
||||
T923101: 'papu',
|
||||
T923102: 'geodoc',
|
||||
T923103: 'atm',
|
||||
T923104: 'chems',
|
||||
T923105: 'domast',
|
||||
T923106: 'matrena',
|
||||
T923107: 'docs',
|
||||
// manually added programs with no IAM, either abolished, or special co-operation programmes
|
||||
MH50_014: 'ech',
|
||||
MH40_013: 'mkk',
|
||||
MH70_010: 'imes',
|
||||
}
|
||||
@@ -107,11 +107,11 @@ export const getThesisGradeSpread = (series, isRelative) => {
|
||||
3: [],
|
||||
4: [],
|
||||
5: [],
|
||||
'Hyv.': [],
|
||||
}
|
||||
const newSeries = series.reduce(
|
||||
(acc, cur, i) => {
|
||||
const currentEntries = Object.entries(cur)
|
||||
|
||||
currentEntries.forEach(([k, v]) => {
|
||||
const merged = k === 'LA' ? 'LUB' : k
|
||||
acc[merged].push(v)
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import moment from 'moment'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Datetime from 'react-datetime'
|
||||
import { Modal, Form, Button, TextArea } from 'semantic-ui-react'
|
||||
import qs from 'query-string'
|
||||
import SearchHistory from 'components/SearchHistory'
|
||||
import {
|
||||
useCreateOpenUniCourseSearchMutation,
|
||||
useDeleteOpenUniCourseSearchMutation,
|
||||
useUpdateOpenUniCourseSearchMutation,
|
||||
} from 'redux/openUniPopulations'
|
||||
|
||||
const CustomOpenUniSearch = ({ setValues, savedSearches, location, history }) => {
|
||||
const [modal, setModal] = useState(false)
|
||||
const [input, setInput] = useState('')
|
||||
const [searchList, setSearches] = useState(savedSearches)
|
||||
const [name, setName] = useState('')
|
||||
const [startdate, setStartdate] = useState(moment('01-08-2017 00:00:00', 'DD-MM-YYYY'))
|
||||
const [enddate, setEnddate] = useState(moment().endOf('day'))
|
||||
const [selectedSearchId, setSelectedSearchId] = useState('')
|
||||
const [updateOpenUniCourseSearch, { isLoading: updateIsLoading, data: updatedData }] =
|
||||
useUpdateOpenUniCourseSearchMutation()
|
||||
const [createOpenUniCourseSearch, { isLoading: createIsLoading, data: createdData }] =
|
||||
useCreateOpenUniCourseSearchMutation()
|
||||
const [deleteOpenUniCourseSearch, { isLoading: deleteIsLoading, data: deletedData }] =
|
||||
useDeleteOpenUniCourseSearchMutation()
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedData) {
|
||||
const updatedSearches = searchList.map(s => (s.id === updatedData.id ? updatedData : s))
|
||||
setSearches(updatedSearches)
|
||||
}
|
||||
}, [updatedData])
|
||||
|
||||
useEffect(() => {
|
||||
if (createdData && !createdData.error) {
|
||||
const newList = searchList.concat(createdData)
|
||||
setSearches(newList)
|
||||
}
|
||||
}, [createdData])
|
||||
|
||||
useEffect(() => {
|
||||
if (deletedData) {
|
||||
const filteredsearches = searchList.filter(s => s.id !== deletedData)
|
||||
setSearches(filteredsearches)
|
||||
}
|
||||
}, [deletedData])
|
||||
|
||||
const pushQueryToUrl = query => {
|
||||
setImmediate(() => {
|
||||
const searchString = qs.stringify(query)
|
||||
history.push({ search: searchString })
|
||||
})
|
||||
}
|
||||
const parseQueryFromUrl = () => {
|
||||
const { courseCode, startdate, enddate } = qs.parse(location.search)
|
||||
let courseCodes = courseCode
|
||||
if (!Array.isArray(courseCode)) courseCodes = [courseCode]
|
||||
const query = {
|
||||
courseList: courseCodes,
|
||||
startdate: moment(startdate, 'DD-MM-YYYY').toISOString(),
|
||||
enddate: moment(enddate, 'DD-MM-YYYY').endOf('day').toISOString(),
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setImmediate(() => {
|
||||
if (!location.search) {
|
||||
setValues({})
|
||||
} else {
|
||||
const query = parseQueryFromUrl()
|
||||
setValues(query)
|
||||
}
|
||||
})
|
||||
}, [location.search])
|
||||
const clearForm = () => {
|
||||
setInput('')
|
||||
setName('')
|
||||
setStartdate(moment('01-08-2017 00-00-00', 'DD-MM-YYYY'))
|
||||
setEnddate(moment().endOf('day'))
|
||||
setSelectedSearchId('')
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setModal(false)
|
||||
clearForm()
|
||||
}
|
||||
|
||||
const onClicker = e => {
|
||||
e.preventDefault()
|
||||
const courseList = input.split(/[\s,]+/).map(code => code.trim().toUpperCase())
|
||||
const query = {
|
||||
courseCode: courseList,
|
||||
startdate: moment(startdate).format('DD-MM-YYYY'),
|
||||
enddate: moment(enddate).format('DD-MM-YYYY'),
|
||||
}
|
||||
setValues({ courseList, startdate: startdate.toISOString(), enddate: enddate.toISOString() })
|
||||
pushQueryToUrl(query)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
deleteOpenUniCourseSearch({ id: selectedSearchId })
|
||||
clearForm()
|
||||
}
|
||||
|
||||
const onSelectSearch = selectedId => {
|
||||
if (!selectedId) {
|
||||
clearForm()
|
||||
return
|
||||
}
|
||||
const { id: selectedSearchId } = selectedId
|
||||
const selectedSearch = searchList.find(search => search.id === selectedSearchId)
|
||||
if (selectedSearch) {
|
||||
setInput(selectedSearch.courseList.join(', '))
|
||||
setName(selectedSearch.name)
|
||||
setSelectedSearchId(selectedSearch.id)
|
||||
}
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
const courseList = input.split(/[\s,]+/).map(code => code.trim().toUpperCase())
|
||||
if (selectedSearchId !== '') {
|
||||
updateOpenUniCourseSearch({ id: selectedSearchId, courseList })
|
||||
} else {
|
||||
createOpenUniCourseSearch({ courseList, name })
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
trigger={
|
||||
<Button size="small" color="blue" onClick={() => setModal(true)} data-cy="custom-pop-search-button">
|
||||
Fetch Open Uni Students
|
||||
</Button>
|
||||
}
|
||||
open={modal}
|
||||
onClose={handleClose}
|
||||
size="small"
|
||||
>
|
||||
<Modal.Content>
|
||||
<Form>
|
||||
<h2> Fetch open uni course population</h2>
|
||||
<Form.Field>
|
||||
<Form.Field>
|
||||
<em> Insert name for this population if you wish to save it </em>
|
||||
<Form.Input
|
||||
disabled={selectedSearchId !== ''}
|
||||
value={name}
|
||||
placeholder="name"
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
</Form.Field>
|
||||
<em>Insert course code(s)</em>
|
||||
<TextArea
|
||||
value={input}
|
||||
placeholder="TKT12345, PSYK-123"
|
||||
onChange={e => setInput(e.target.value)}
|
||||
data-cy="s-no-input"
|
||||
/>
|
||||
</Form.Field>
|
||||
<SearchHistory
|
||||
header="Saved populations"
|
||||
items={searchList?.map(s => ({
|
||||
...s,
|
||||
text: s.name,
|
||||
timestamp: s.updatedAt,
|
||||
params: { id: s.id },
|
||||
}))}
|
||||
updateItem={() => null}
|
||||
handleSearch={onSelectSearch}
|
||||
/>
|
||||
<Form.Field>
|
||||
<em>Select beginning</em>
|
||||
<Datetime
|
||||
value={startdate}
|
||||
onChange={value => setStartdate(value)}
|
||||
timeFormat={false}
|
||||
locale="fi"
|
||||
isValidDate={date => date.isBefore(enddate)}
|
||||
closeOnSelect
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<em>Select ending for enrollments:</em>
|
||||
<br />
|
||||
<em>Attainments are fetched until today.</em>
|
||||
<Datetime
|
||||
value={enddate}
|
||||
onChange={value => setEnddate(value)}
|
||||
timeFormat={false}
|
||||
locale="fi"
|
||||
isValidDate={date => date.isAfter(startdate)}
|
||||
closeOnSelect
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button
|
||||
disabled={!name || updateIsLoading || createIsLoading}
|
||||
loading={updateIsLoading || createIsLoading}
|
||||
floated="left"
|
||||
icon="save"
|
||||
onClick={onSave}
|
||||
content="Save"
|
||||
/>
|
||||
<Button
|
||||
disabled={!selectedSearchId || deleteIsLoading}
|
||||
negative
|
||||
floated="left"
|
||||
icon="trash"
|
||||
onClick={onDelete}
|
||||
content="Delete"
|
||||
/>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<Button positive onClick={e => onClicker(e)} data-cy="search-button">
|
||||
Search population
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomOpenUniSearch
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import moment from 'moment'
|
||||
import { Loader, Icon } from 'semantic-ui-react'
|
||||
import { useGetOpenUniCourseStudentsQuery } from 'redux/openUniPopulations'
|
||||
import SortableTable from 'components/SortableTable'
|
||||
|
||||
const getTableData = studentsData => {
|
||||
return Object.keys(studentsData).reduce(
|
||||
(acc, student) => [
|
||||
...acc,
|
||||
{
|
||||
studentnumber: student,
|
||||
courseInfo: { ...studentsData[student].courseInfo },
|
||||
email: studentsData[student].email,
|
||||
secondaryEmail: studentsData[student].secondaryEmail,
|
||||
disseminationInfoAllowed: studentsData[student].disseminationInfoAllowed,
|
||||
totals: studentsData[student].totals,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
const getColumns = (labelsToCourses, language) => {
|
||||
const style = {
|
||||
verticalAlign: 'middle',
|
||||
textAlign: 'center',
|
||||
}
|
||||
const studentNbrColumn = [
|
||||
{
|
||||
key: 'studentnumber-parent',
|
||||
mergeHeader: true,
|
||||
// merge: true,
|
||||
title: 'Student Number',
|
||||
children: [
|
||||
{
|
||||
key: 'studentnumber-child',
|
||||
title: 'Student Number',
|
||||
cellProps: { style },
|
||||
getRowVal: s => s.studentnumber,
|
||||
getRowContent: s => s.studentnumber,
|
||||
child: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const statisticColumns = [
|
||||
{
|
||||
key: 'passed',
|
||||
title: 'Passed',
|
||||
cellProps: { style },
|
||||
headerProps: { title: 'Passed' },
|
||||
getRowVal: s => s.totals.passed,
|
||||
getRowContent: s => s.totals.passed,
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'failed',
|
||||
title: 'Failed',
|
||||
headerProps: { title: 'Failed' },
|
||||
getRowVal: s => s.totals.failed,
|
||||
getRowContent: s => s.totals.failed,
|
||||
cellProps: { style },
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'unfinished',
|
||||
title: 'Unfinished',
|
||||
headerProps: { title: 'Unfinished' },
|
||||
getRowVal: s => s.totals.unfinished,
|
||||
getRowContent: s => s.totals.unfinished,
|
||||
cellProps: { style },
|
||||
child: true,
|
||||
},
|
||||
]
|
||||
|
||||
const getDisseminationInfoAllowed = s => {
|
||||
if (s.disseminationInfoAllowed === null) return 'unknown'
|
||||
if (s.disseminationInfoAllowed) return 'yes'
|
||||
return 'no'
|
||||
}
|
||||
|
||||
const informationColumns = [
|
||||
{
|
||||
key: 'disseminationInfoAllowed',
|
||||
title: 'Marketing Allowed',
|
||||
getRowVal: s => getDisseminationInfoAllowed(s),
|
||||
getRowContent: s => getDisseminationInfoAllowed(s),
|
||||
headerProps: { title: 'Marketing Allowed' },
|
||||
child: true,
|
||||
childOf: 'Marketing Allowed',
|
||||
cellProps: { style },
|
||||
},
|
||||
{
|
||||
key: 'email-child',
|
||||
title: 'Email',
|
||||
getRowVal: s => (s.email ? s.email : ''),
|
||||
getRowContent: s => (s.email ? s.email : ''),
|
||||
headerProps: { title: 'Email' },
|
||||
childOf: 'Email',
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'secondary_email-child',
|
||||
title: 'Secondary Email',
|
||||
getRowVal: s => (s.secondaryEmail ? s.secondaryEmail : ''),
|
||||
getRowContent: s => (s.secondaryEmail ? s.secondaryEmail : ''),
|
||||
headerProps: { title: 'Secondary Email' },
|
||||
childOf: 'Secondary Email',
|
||||
child: true,
|
||||
},
|
||||
]
|
||||
const findRowContent = (s, courseCode) => {
|
||||
if (s.courseInfo[courseCode] === undefined) return null
|
||||
if (s.courseInfo[courseCode].status.passed) return <Icon fitted name="check" color="green" />
|
||||
if (s.courseInfo[courseCode].status.failed) return <Icon fitted name="times" color="red" />
|
||||
if (s.courseInfo[courseCode].status.unfinished) return <Icon fitted name="minus" color="grey" />
|
||||
return null
|
||||
}
|
||||
|
||||
const findRowValue = (s, courseCode, hidden = false) => {
|
||||
if (s.courseInfo[courseCode] === undefined) return ''
|
||||
if (s.courseInfo[courseCode].status.passed && hidden)
|
||||
return `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('DD-MM-YYYY')}`
|
||||
if (s.courseInfo[courseCode].status.failed && hidden)
|
||||
return `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('DD-MM-YYYY')}`
|
||||
if (s.courseInfo[courseCode].status.unfinished && hidden)
|
||||
return `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('DD-MM-YYYY')}`
|
||||
if (s.courseInfo[courseCode].status.passed) return 'Passed'
|
||||
if (s.courseInfo[courseCode].status.failed) return 'Failed'
|
||||
if (s.courseInfo[courseCode].status.unfinished) return 'Unfinished'
|
||||
return ''
|
||||
}
|
||||
const findProp = (s, courseCode) => {
|
||||
const propObj = {
|
||||
title: '',
|
||||
style,
|
||||
}
|
||||
if (s.courseInfo[courseCode] === undefined) return propObj
|
||||
if (s.courseInfo[courseCode].status.passed)
|
||||
return { ...propObj, title: `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('DD-MM-YYYY')}` }
|
||||
if (s.courseInfo[courseCode].status.failed)
|
||||
return { ...propObj, title: `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('DD-MM-YYYY')}` }
|
||||
if (s.courseInfo[courseCode].status.passed)
|
||||
return {
|
||||
...propObj,
|
||||
title: `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('DD-MM-YYYY')}`,
|
||||
}
|
||||
return propObj
|
||||
}
|
||||
|
||||
const columnsToShow = labelsToCourses.map(course => ({
|
||||
key: `${course.label}-${course.name[language]}`,
|
||||
title: (
|
||||
<div style={{ maxWidth: '15em', whiteSpace: 'normal', overflow: 'hidden', width: 'max-content' }}>
|
||||
<div>{course.label}</div>
|
||||
</div>
|
||||
),
|
||||
textTitle: `${course.label}-${course.name[language]}`,
|
||||
vertical: false,
|
||||
forceToolsMode: 'dangling',
|
||||
cellProps: s => findProp(s, course.label),
|
||||
headerProps: { title: `${course.label}-${course.name[language]}` },
|
||||
getRowVal: s => {
|
||||
return findRowValue(s, course.label)
|
||||
},
|
||||
getRowContent: s => {
|
||||
return findRowContent(s, course.label)
|
||||
},
|
||||
child: true,
|
||||
childOf: course.label,
|
||||
code: course.label,
|
||||
}))
|
||||
|
||||
const columnsToHide = labelsToCourses.map(course => ({
|
||||
key: `hidden-${course.label}-${course.name[language]}`,
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: `Dates-${course.label}-${course.name[language]}`,
|
||||
headerProps: {
|
||||
title: `Dates-${course.label}-${course.name[language]}`,
|
||||
style: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
cellProps: {
|
||||
style: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
getRowVal: s => {
|
||||
return findRowValue(s, course.label, true)
|
||||
},
|
||||
child: true,
|
||||
code: `hidden ${course.label}`,
|
||||
}))
|
||||
|
||||
const columns = []
|
||||
columns.push(
|
||||
{
|
||||
key: 'general',
|
||||
title: <b>Labels:</b>,
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: studentNbrColumn,
|
||||
},
|
||||
{
|
||||
key: 'Courses',
|
||||
title: <b>Courses:</b>,
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: columnsToShow,
|
||||
},
|
||||
{
|
||||
key: 'statistics',
|
||||
title: <b>Total of:</b>,
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: statisticColumns,
|
||||
},
|
||||
{
|
||||
key: 'information',
|
||||
title: <b>Information:</b>,
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: informationColumns,
|
||||
},
|
||||
{
|
||||
key: 'hiddencourses',
|
||||
title: '',
|
||||
mergeHeader: true,
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: columnsToHide,
|
||||
}
|
||||
)
|
||||
return columns
|
||||
}
|
||||
|
||||
const OpenUniPopulationResults = ({ fieldValues, language }) => {
|
||||
const { courseList, startdate, enddate } = fieldValues
|
||||
const [tableData, setData] = useState({ data: [], columns: [] })
|
||||
|
||||
const openUniStudentStats = useGetOpenUniCourseStudentsQuery({ courseList, startdate, enddate })
|
||||
const isFetchingOrLoading = openUniStudentStats.isLoading || openUniStudentStats.isFetching
|
||||
const isError = openUniStudentStats.isError || (openUniStudentStats.isSuccess && !openUniStudentStats.data)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isError && !isFetchingOrLoading) {
|
||||
const unOrderedlabels = openUniStudentStats?.data.courses
|
||||
const labelsToCourses = [...unOrderedlabels].sort((a, b) => a.label.localeCompare(b.label))
|
||||
setImmediate(() => {
|
||||
const data = getTableData(openUniStudentStats?.data.students)
|
||||
const columns = getColumns(labelsToCourses, language)
|
||||
setData({ data, columns })
|
||||
})
|
||||
}
|
||||
}, [openUniStudentStats])
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
if (isFetchingOrLoading) return <Loader active style={{ marginTop: '15em' }} />
|
||||
|
||||
return (
|
||||
<div style={{ paddingBottom: '50px' }}>
|
||||
<div style={{ maxHeight: '80vh', width: '100%' }}>
|
||||
{courseList.length > 0 && (
|
||||
<SortableTable
|
||||
title={`Open Uni Student Population (${Object.keys(openUniStudentStats?.data.students).length} students)`}
|
||||
getRowKey={s => s.studentNumber}
|
||||
tableProps={{
|
||||
celled: true,
|
||||
compact: 'very',
|
||||
padded: false,
|
||||
collapsing: true,
|
||||
basic: true,
|
||||
striped: true,
|
||||
singleLine: true,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
columns={tableData.columns}
|
||||
data={tableData.data}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OpenUniPopulationResults
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { withRouter, useLocation, useHistory } from 'react-router-dom'
|
||||
import { Message, Icon, Loader } from 'semantic-ui-react'
|
||||
import moment from 'moment'
|
||||
import { useGetSavedSearchesQuery } from 'redux/openUniPopulations'
|
||||
import OpenUniPopulationResults from './OpenUniPopulationResults'
|
||||
import { useTitle } from '../../common/hooks'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import CustomOpenUniSearch from './CustomOpenUniSearch'
|
||||
import TSA from '../../common/tsa'
|
||||
|
||||
const CustomOpenUniPopulation = () => {
|
||||
useTitle('Custom open uni population')
|
||||
const [fieldValues, setValues] = useState({})
|
||||
const { language } = useLanguage()
|
||||
const location = useLocation()
|
||||
const history = useHistory()
|
||||
const savedSearches = useGetSavedSearchesQuery()
|
||||
const isFetchingOrLoading = savedSearches.isLoading || savedSearches.isFetching
|
||||
const isError = savedSearches.isError || (savedSearches.isSuccess && !savedSearches.data)
|
||||
|
||||
useEffect(() => {
|
||||
if (fieldValues && fieldValues.courseList?.length > 0) {
|
||||
TSA.Influx.sendEvent({
|
||||
group: 'Custom Open Uni Usage',
|
||||
name: 'open uni populations',
|
||||
label: 'openUniPopulations',
|
||||
value: 1,
|
||||
})
|
||||
}
|
||||
}, [fieldValues])
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
if (isFetchingOrLoading) return <Loader active style={{ marginTop: '15em' }} />
|
||||
return (
|
||||
<div className="segmentContainer">
|
||||
<Message style={{ maxWidth: '800px', fontSize: '16px' }}>
|
||||
<Message.Header>Open uni student population</Message.Header>
|
||||
<p>
|
||||
Here you can create custom population using a list of courses. Clicking the blue custom population button will
|
||||
open a modal where you can enter a list of courses. <br />
|
||||
<Icon fitted name="check" color="green" />: Student passed the course. <br />
|
||||
<Icon fitted name="times" color="red" />: Student failed the course. <br />
|
||||
<Icon fitted name="minus" color="grey" />: Student has enrolled, but has not received any grade from the
|
||||
course. <br />
|
||||
<b>Empty cell</b>: Student has no enrollments for the course. <br />
|
||||
</p>
|
||||
</Message>
|
||||
<CustomOpenUniSearch
|
||||
setValues={setValues}
|
||||
savedSearches={savedSearches.data}
|
||||
location={location}
|
||||
history={history}
|
||||
/>
|
||||
<div style={{ paddingTop: '25px', paddingBottom: '10px', fontSize: '20px' }}>
|
||||
{fieldValues && fieldValues.courseList?.length > 0 && (
|
||||
<p>
|
||||
<span style={{ color: '#484848' }}>Beginning of the search for all fields:</span>
|
||||
<span style={{ paddingLeft: '10px', fontWeight: '600' }}>
|
||||
{moment(fieldValues.startdate).format('DD.MM.YYYY')}
|
||||
</span>
|
||||
<br />
|
||||
<span style={{ color: '#484848' }}>End of the search for enrollments:</span>
|
||||
<span style={{ paddingLeft: '30px', fontWeight: '600' }}>
|
||||
{moment(fieldValues.enddate).format('DD.MM.YYYY')}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ paddingTop: '25px' }}>
|
||||
{fieldValues && fieldValues.courseList?.length > 0 && (
|
||||
<OpenUniPopulationResults fieldValues={fieldValues} language={language} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(CustomOpenUniPopulation)
|
||||
@@ -195,7 +195,12 @@ const Overview = ({
|
||||
dataStats={basics?.data?.studentInfo.tableStats}
|
||||
dataProgrammeStats={basics?.data?.studentInfo.programmeTableStats}
|
||||
programmeNames={basics?.data?.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.studentInfo.programmeTableStats))}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(basics?.data?.studentInfo.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
basics?.data?.programmeNames[obj].code,
|
||||
])
|
||||
).map(listObj => listObj[0])}
|
||||
titles={basics?.data?.studentInfo.titles}
|
||||
sliceStart={1}
|
||||
language={language}
|
||||
@@ -231,7 +236,12 @@ const Overview = ({
|
||||
dataStats={credits?.data?.tableStats}
|
||||
dataProgrammeStats={credits?.data?.programmeTableStats}
|
||||
programmeNames={credits?.data?.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(credits?.data?.programmeTableStats))}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(credits?.data.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
credits?.data?.programmeNames[obj].code,
|
||||
])
|
||||
).map(listObj => listObj[0])}
|
||||
titles={credits?.data?.titles}
|
||||
extraHeight="EXTRA HEIGHT"
|
||||
sliceStart={2}
|
||||
@@ -266,7 +276,12 @@ const Overview = ({
|
||||
dataStats={basics?.data?.graduationInfo.tableStats}
|
||||
dataProgrammeStats={basics?.data?.graduationInfo.programmeTableStats}
|
||||
programmeNames={basics?.data?.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.graduationInfo.programmeTableStats))}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(basics?.data?.graduationInfo.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
basics?.data?.programmeNames[obj].code,
|
||||
])
|
||||
).map(listObj => listObj[0])}
|
||||
titles={basics?.data?.graduationInfo.titles}
|
||||
sliceStart={2}
|
||||
language={language}
|
||||
@@ -299,7 +314,12 @@ const Overview = ({
|
||||
dataStats={thesisWriters?.data.tableStats}
|
||||
dataProgrammeStats={thesisWriters?.data.programmeTableStats}
|
||||
programmeNames={thesisWriters?.data.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(thesisWriters?.data.programmeTableStats))}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(thesisWriters?.data.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
thesisWriters?.data?.programmeNames[obj].code,
|
||||
])
|
||||
).map(listObj => listObj[0])}
|
||||
titles={thesisWriters?.data?.titles}
|
||||
sliceStart={2}
|
||||
extraHeight="EXTRA HEIGHT"
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ const FacultyProgressTable = ({
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{data.map((yearArray, yearIndex) => (
|
||||
{data?.map((yearArray, yearIndex) => (
|
||||
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
|
||||
{yearIndex < lenOfYearArray ? (
|
||||
<BasicRow
|
||||
@@ -46,7 +46,7 @@ const FacultyProgressTable = ({
|
||||
/>
|
||||
) : (
|
||||
<Table.Row>
|
||||
{yearArray.map(value => (
|
||||
{yearArray?.map(value => (
|
||||
<Table.Cell key={`last-row-${Math.random()}`}>{value}</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
|
||||
+8
-4
@@ -94,16 +94,20 @@ const FacultyStudentDataTable = ({
|
||||
<Table.Row className="regular-row" key={`${year}-regular-row-${Math.random()}`}>
|
||||
<Table.Cell textAlign="left" style={{ paddingLeft: '50px' }} key={`${year}-${programme}`}>
|
||||
<b>
|
||||
{programme}
|
||||
{programme} - {programmeNames[programme].code}
|
||||
<br />
|
||||
{programmeNames[programme][language]
|
||||
? programmeNames[programme][language]
|
||||
: programmeNames[programme].fi}
|
||||
</b>
|
||||
{(requiredRights.rights?.includes(programme) ||
|
||||
{(requiredRights.rights?.includes(programmeNames[programme].code) ||
|
||||
requiredRights.isAdmin ||
|
||||
requiredRights.IAMrights?.includes(programme)) && (
|
||||
<PopulationLink studyprogramme={programme} year={year} years={calendarYears} />
|
||||
requiredRights.IAMrights?.includes(programmeNames[programme].code)) && (
|
||||
<PopulationLink
|
||||
studyprogramme={programmeNames[programme].code}
|
||||
year={year}
|
||||
years={calendarYears}
|
||||
/>
|
||||
)}
|
||||
</Table.Cell>
|
||||
{programmeStats[programme][year].map((value, valIdx) => {
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ const ProgrammeProgressChart = ({ data, labels, longLabels, names, language }) =
|
||||
formatter() {
|
||||
let tooltipString = `<b>${
|
||||
longLabels[this.x] && longLabels[this.x][language] ? longLabels[this.x][language] : longLabels[this.x].fi
|
||||
}</b><br /><p>${this.x}</p><br />`
|
||||
} </b><br /><p>${this.x} - ${longLabels[this.x]?.code}</p><br />`
|
||||
this.points.forEach(point => {
|
||||
tooltipString += `<span style="color:${point.color}">●</span> <b>${point.series.name}: ${point.y}</b>
|
||||
</span><br />`
|
||||
|
||||
+59
-30
@@ -95,7 +95,12 @@ const FacultyProgrammeOverview = ({
|
||||
programmeNames={studentStats?.data.programmeNames}
|
||||
titles={studentStats?.data.titles}
|
||||
years={studentStats?.data.years}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(studentStats?.data.programmeStats))}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(studentStats?.data.programmeStats).map(obj => [
|
||||
obj,
|
||||
studentStats?.data?.programmeNames[obj].code,
|
||||
])
|
||||
).map(listObj => listObj[0])}
|
||||
language={language}
|
||||
cypress="FacultyStudentStatsTable"
|
||||
requiredRights={requiredRights}
|
||||
@@ -128,7 +133,12 @@ const FacultyProgrammeOverview = ({
|
||||
data={progressStats?.data.bachelorsTableStats}
|
||||
programmeStats={progressStats?.data.bachelorsProgrammeStats}
|
||||
titles={progressStats?.data.bachelorTitles}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.bachelorsProgrammeStats))}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.bachelorsProgrammeStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
])
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
@@ -154,7 +164,12 @@ const FacultyProgrammeOverview = ({
|
||||
data={progressStats?.data.bcMsTableStats}
|
||||
programmeStats={progressStats?.data.bcMsProgrammeStats}
|
||||
titles={progressStats?.data.bcMsTitles}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.bcMsProgrammeStats))}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.bcMsProgrammeStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
])
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
@@ -163,32 +178,41 @@ const FacultyProgrammeOverview = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{getDivider('Master', 'MasterStudentsOfTheFacultyByStartingYear', 'no-infobox')}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyMastersProgress"
|
||||
data={{
|
||||
id: faculty.code,
|
||||
stats: progressStats?.data.mastersGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.mastersTableStats}
|
||||
programmeStats={progressStats?.data.mastersProgrammeStats}
|
||||
titles={progressStats?.data.mastersTitles}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.mastersProgrammeStats))}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyBachelorsProgressTable"
|
||||
progressTitles={progressStats?.data.yearlyMasterTitles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!(faculty.code === 'H90') && (
|
||||
<>
|
||||
{getDivider('Master', 'MasterStudentsOfTheFacultyByStartingYear', 'no-infobox')}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyMastersProgress"
|
||||
data={{
|
||||
id: faculty.code,
|
||||
stats: progressStats?.data.mastersGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.mastersTableStats}
|
||||
programmeStats={progressStats?.data.mastersProgrammeStats}
|
||||
titles={progressStats?.data.mastersTitles}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.mastersProgrammeStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
])
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyBachelorsProgressTable"
|
||||
progressTitles={progressStats?.data.yearlyMasterTitles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{getDivider('Doctor and licentiate', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
@@ -206,7 +230,12 @@ const FacultyProgrammeOverview = ({
|
||||
data={progressStats?.data.doctoralTableStats}
|
||||
programmeStats={progressStats?.data.doctoralProgrammeStats}
|
||||
titles={progressStats?.data.doctoralTitles}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.doctoralProgrammeStats))}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.doctoralProgrammeStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
])
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
|
||||
+11
-14
@@ -11,13 +11,6 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
}
|
||||
|
||||
const needsExtra = extraHeight === 'EXTRA HEIGHT'
|
||||
const manyProgrammes =
|
||||
labels.includes('KH57_001') ||
|
||||
labels.includes('KH50_001') ||
|
||||
labels.includes('KH70_001') ||
|
||||
labels.includes('KH40_001') ||
|
||||
labels.includes('KH30_001') ||
|
||||
labels.includes('KH80_001')
|
||||
const dataTranspose = transpose(data)
|
||||
.map((obj, idx) => ({ name: names[idx], data: obj, color: colors[idx] }))
|
||||
.reverse()
|
||||
@@ -41,11 +34,15 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
}
|
||||
|
||||
// Point width is 24 px different multipliers adjusts the height.
|
||||
const getFlexHeight = (len, needsExtra, manyProgrammes, labels) => {
|
||||
if (labels.includes('KH74_001')) return `${len * 24 * 5}px`
|
||||
if (needsExtra && manyProgrammes) return `${len * 24 * 1.5}px`
|
||||
if (needsExtra) return `${len * 24 * 2.5}px`
|
||||
return `${len * 24 * 1.5}px`
|
||||
const getFlexHeight = (len, needsExtra) => {
|
||||
if (len > 7 && needsExtra) return `${len * 24 * 1.5}px`
|
||||
if (len > 5 && !needsExtra) return `${len * 24 * 1.5}px`
|
||||
if (needsExtra && len <= 2) return `${len * 24 * 6}px`
|
||||
if (needsExtra && len <= 4) return `${len * 24 * 3}px`
|
||||
if (needsExtra) return `${len * 24 * 2}px`
|
||||
if (len <= 2) return `${len * 24 * 5}px`
|
||||
if (len <= 4) return `${len * 24 * 3}px`
|
||||
return `${len * 24}px`
|
||||
}
|
||||
|
||||
const getColor = change => {
|
||||
@@ -59,7 +56,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
chart: {
|
||||
type: 'bar',
|
||||
marginTop: 60,
|
||||
height: getFlexHeight(labels.length, needsExtra, manyProgrammes, labels),
|
||||
height: getFlexHeight(labels.length, needsExtra),
|
||||
},
|
||||
credits: {
|
||||
text: 'oodikone | TOSKA',
|
||||
@@ -111,7 +108,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
formatter() {
|
||||
let tooltipString = `<b>${
|
||||
longLabels[this.x] && longLabels[this.x][language] ? longLabels[this.x][language] : longLabels[this.x].fi
|
||||
}</b><br /><p>${this.x}</p><br />`
|
||||
}</b><br /><p>${this.x} - ${longLabels[this.x]?.code}</p><br />`
|
||||
const diffArray = differenceArray[this.x]
|
||||
this.points.forEach(point => {
|
||||
tooltipString += `<span style="color:${point.color}">●</span> <b>${point.series.name}: ${point.y}</b>
|
||||
|
||||
@@ -199,7 +199,7 @@ const BarChart = ({
|
||||
if (!facultyGraph) config.title.text = `Year ${year} by ${label.toLowerCase()}`
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${facultyGraph ? 'faculty' : 'programmes'}-graph`}>
|
||||
<ReactHighcharts config={config} />
|
||||
</div>
|
||||
)
|
||||
|
||||
+3
-5
@@ -33,10 +33,8 @@ const GraduationTimes = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider data-cy={`Section-${level}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
<div className={`graduation-times-${level}`} data-cy={`Section-${level}`}>
|
||||
<Divider horizontal>{title}</Divider>
|
||||
<div>
|
||||
{level === 'bcMsCombo' && groupBy === 'byStartYear' && (
|
||||
<div className="graduations-message">
|
||||
@@ -86,7 +84,7 @@ const GraduationTimes = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/*
|
||||
Order of the programme keys: KH -> MH -> T -> FI -> K- -> Numbers containing letters at end -> Y- -> Numbers
|
||||
Order of the programme keys (such as TKT, PSYK) is chosen by "old" code:
|
||||
KH -> MH -> T -> FI -> K- -> Numbers containing letters at end -> Y- -> Number
|
||||
Take
|
||||
*/
|
||||
const regexValuesAll = [
|
||||
/^KH/,
|
||||
@@ -26,12 +28,19 @@ const testKey = value => {
|
||||
}
|
||||
|
||||
const sortProgrammeKeys = programmeKeys => {
|
||||
return programmeKeys.sort((a, b) => {
|
||||
if (testKey(a) - testKey(b) === 0) {
|
||||
return a.localeCompare(b)
|
||||
}
|
||||
return testKey(a) - testKey(b)
|
||||
})
|
||||
try {
|
||||
return programmeKeys.sort((a, b) => {
|
||||
if (testKey(a[1]) - testKey(b[1]) === 0 && testKey(a[0]) - testKey(b[0])) {
|
||||
return a[1].localeCompare(b[1])
|
||||
}
|
||||
if (testKey(a[1]) - testKey(b[1]) === 0) {
|
||||
return a[0].localeCompare(b[0])
|
||||
}
|
||||
return testKey(a[1]) - testKey(b[1])
|
||||
})
|
||||
} catch (e) {
|
||||
return programmeKeys
|
||||
}
|
||||
}
|
||||
|
||||
export default sortProgrammeKeys
|
||||
|
||||
@@ -32,7 +32,9 @@ const Header = ({ children, actions, contextItems }) => {
|
||||
<Dropdown position="bottom center" direction="left" icon="ellipsis vertical">
|
||||
<Dropdown.Menu>
|
||||
{contextItems.map(({ label, onClick }) => (
|
||||
<Dropdown.Item onClick={onClick}>{label}</Dropdown.Item>
|
||||
<Dropdown.Item key={label} onClick={onClick}>
|
||||
{label}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
||||
@@ -30,9 +30,12 @@ const allNavigationItems = {
|
||||
reqRights: ['studyGuidanceGroups'],
|
||||
},
|
||||
customPopulations: {
|
||||
path: '/custompopulation',
|
||||
key: 'customPopulation',
|
||||
label: 'Custom populations',
|
||||
items: [
|
||||
{ path: '/custompopulation', key: 'customSearch', label: 'Search by Studentnumbers' },
|
||||
{ path: '/openunipopulation', key: 'openUniSearch', label: 'Fetch Open Uni Students by Courses' },
|
||||
],
|
||||
},
|
||||
updater: { path: '/updater', key: 'updater', label: 'Updater', reqRights: ['admin'] },
|
||||
feedback: { path: '/feedback', key: 'feedback', label: 'Give feedback' },
|
||||
@@ -50,8 +53,11 @@ const NavigationBar = () => {
|
||||
if (key === 'populations') {
|
||||
if (!isAdmin && rights.length === 0 && iamRights.length === 0) return
|
||||
}
|
||||
if (key === 'students' || key === 'customPopulations') {
|
||||
if (key === 'students') {
|
||||
if (!checkUserAccess(['admin', 'studyGuidanceGroups'], roles) && rights.length === 0) return
|
||||
}
|
||||
if (key === 'customPopulations') {
|
||||
if (!checkUserAccess(['admin', 'studyGuidanceGroups', 'openUniSearch'], roles) && rights.length === 0) return
|
||||
} else if (key === 'courseStatistics') {
|
||||
if (!checkUserAccess(['courseStatistics', 'admin'], roles) && rights.length === 0) return
|
||||
} else if (key === 'faculty') {
|
||||
@@ -75,23 +81,33 @@ const NavigationBar = () => {
|
||||
</span>
|
||||
</Menu.Item>
|
||||
)
|
||||
const showSearch = item => {
|
||||
if (item.key === 'class' || item.key === 'overview') return true
|
||||
if (checkUserAccess(['openUniSearch', 'admin'], roles) && item.key === 'openUniSearch') return true
|
||||
if ((checkUserAccess(['studyGuidanceGroups', 'admin'], roles) || rights.length > 0) && item.key === 'customSearch')
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
const renderNavigationRoutes = () =>
|
||||
Object.values(visibleNavigationItems).map(({ items, path, key, label, tag }) =>
|
||||
items ? (
|
||||
<Menu.Item as={Dropdown} key={`menu-item-drop-${key}`} tabIndex="-1" text={label} data-cy={`navbar-${key}`}>
|
||||
<Dropdown.Menu>
|
||||
{items.map(i => (
|
||||
<Dropdown.Item
|
||||
as={NavLink}
|
||||
key={`menu-item-${i.path}`}
|
||||
to={i.path}
|
||||
tabIndex="-1"
|
||||
data-cy={`navbar-${i.key}`}
|
||||
>
|
||||
{i.label}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
{items.map(
|
||||
i =>
|
||||
showSearch(i) && (
|
||||
<Dropdown.Item
|
||||
as={NavLink}
|
||||
key={`menu-item-${i.path}`}
|
||||
to={i.path}
|
||||
tabIndex="-1"
|
||||
data-cy={`navbar-${i.key}`}
|
||||
>
|
||||
{i.label}
|
||||
</Dropdown.Item>
|
||||
)
|
||||
)}
|
||||
</Dropdown.Menu>
|
||||
</Menu.Item>
|
||||
) : (
|
||||
|
||||
@@ -30,7 +30,8 @@ const mapCourseData = course => ({
|
||||
})
|
||||
|
||||
const GradeDistribution = ({ flat, onlyIamRights }) => {
|
||||
const { modules, courseStatistics, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
|
||||
const { modules, courseStatistics, onGoToCourseStatisticsClick, toggleGroupExpansion, expandedGroups } =
|
||||
UsePopulationCourseContext()
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const columns = [
|
||||
@@ -147,7 +148,13 @@ const GradeDistribution = ({ flat, onlyIamRights }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SortableTable title="Grade distribution of courses" data={data} columns={columns} />
|
||||
<SortableTable
|
||||
title="Grade distribution of courses"
|
||||
data={data}
|
||||
columns={columns}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
expandedGroups={expandedGroups}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+4
-1
@@ -27,7 +27,8 @@ const createModuleAggregateRow = ({ definition, children }) => ({
|
||||
})
|
||||
|
||||
const PassFailEnrollments = ({ flat, onlyIamRights }) => {
|
||||
const { modules, courseStatistics, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
|
||||
const { modules, courseStatistics, onGoToCourseStatisticsClick, toggleGroupExpansion, expandedGroups } =
|
||||
UsePopulationCourseContext()
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const columns = [
|
||||
@@ -236,6 +237,8 @@ const PassFailEnrollments = ({ flat, onlyIamRights }) => {
|
||||
title="Pass and fail statistics of courses with course enrollment details"
|
||||
data={data}
|
||||
columns={columns}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
expandedGroups={expandedGroups}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ const yearColumn = (year, cumulative) => ({
|
||||
})
|
||||
|
||||
const PassingSemesters = ({ onlyIamRights }) => {
|
||||
const { modules, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
|
||||
const { modules, onGoToCourseStatisticsClick, toggleGroupExpansion, expandedGroups } = UsePopulationCourseContext()
|
||||
const [cumulativeStats, setCumulativeStats] = useState(false)
|
||||
|
||||
const columns = useMemo(() => {
|
||||
@@ -154,6 +154,8 @@ const PassingSemesters = ({ onlyIamRights }) => {
|
||||
}
|
||||
columns={columns}
|
||||
data={data}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
expandedGroups={expandedGroups}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -9,7 +9,8 @@ import { getTextIn } from '../../../common'
|
||||
import { useStudentNameVisibility } from '../../StudentNameVisibilityToggle'
|
||||
|
||||
const Students = ({ filteredStudents }) => {
|
||||
const { courseStatistics, onGoToCourseStatisticsClick, modules } = UsePopulationCourseContext()
|
||||
const { courseStatistics, onGoToCourseStatisticsClick, modules, toggleGroupExpansion, expandedGroups } =
|
||||
UsePopulationCourseContext()
|
||||
const { visible: namesVisible, toggle: toggleStudentNames } = useStudentNameVisibility()
|
||||
const [page, setPage] = useState(0)
|
||||
|
||||
@@ -181,6 +182,8 @@ const Students = ({ filteredStudents }) => {
|
||||
}
|
||||
columns={columns}
|
||||
data={data}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
expandedGroups={expandedGroups}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -10,7 +10,6 @@ import './populationCourseStats.css'
|
||||
import { PopulationCourseContext } from './PopulationCourseContext'
|
||||
import sendEvent from '../../common/sendEvent'
|
||||
import GradeDistribution from './GradeDistribution'
|
||||
import PassFail from './PassFail'
|
||||
import PassFailEnrollments from './PassFailEnrollments'
|
||||
import Students from './Students'
|
||||
import { getTextIn } from '../../common'
|
||||
@@ -243,16 +242,24 @@ const PopulationCourseStats = props => {
|
||||
setFilterFields({ ...filterFields, [field]: '' })
|
||||
}
|
||||
|
||||
const toggleGroupExpansion = code => {
|
||||
const newExpandedGroups = new Set(expandedGroups)
|
||||
if (newExpandedGroups.has(code)) {
|
||||
newExpandedGroups.delete(code)
|
||||
sendAnalytics('Courses of Population collapsed group', code)
|
||||
const toggleGroupExpansion = (code, close = false, all = null) => {
|
||||
if (all) {
|
||||
sendAnalytics('Courses of Population expanded all groups', '')
|
||||
setExpandedGroups(new Set(all))
|
||||
} else if (close) {
|
||||
sendAnalytics('Courses of Population collapsed all groups', '')
|
||||
setExpandedGroups(new Set())
|
||||
} else {
|
||||
newExpandedGroups.add(code)
|
||||
sendAnalytics('Courses of Population expanded group', code)
|
||||
const newExpandedGroups = new Set(expandedGroups)
|
||||
if (newExpandedGroups.has(code)) {
|
||||
newExpandedGroups.delete(code)
|
||||
sendAnalytics('Courses of Population collapsed group', code)
|
||||
} else {
|
||||
newExpandedGroups.add(code)
|
||||
sendAnalytics('Courses of Population expanded group', code)
|
||||
}
|
||||
setExpandedGroups(newExpandedGroups)
|
||||
}
|
||||
setExpandedGroups(newExpandedGroups)
|
||||
}
|
||||
|
||||
const getFilterValue = field => (field in filterFields ? filterFields[field] || '' : '')
|
||||
@@ -287,23 +294,13 @@ const PopulationCourseStats = props => {
|
||||
sortCriteria,
|
||||
reversed,
|
||||
courses,
|
||||
toggleGroupExpansion,
|
||||
expandedGroups,
|
||||
}
|
||||
|
||||
const panes = [
|
||||
{
|
||||
menuItem: 'pass/fail',
|
||||
render: () => (
|
||||
<Tab.Pane className="menuTab">
|
||||
<PassFail
|
||||
expandedGroups={expandedGroups}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
onlyIamRights={props.onlyIamRights}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
),
|
||||
},
|
||||
{
|
||||
menuItem: 'pass/fail w/ enrollments ',
|
||||
render: () => (
|
||||
<Tab.Pane className="menuTab">
|
||||
<PassFailEnrollments
|
||||
|
||||
@@ -45,7 +45,7 @@ const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYea
|
||||
!mandatoryCourses.pending &&
|
||||
!queryHasBeenUpdated() &&
|
||||
!populationSelectedStudentCourses.pending &&
|
||||
mandatoryCourses.data
|
||||
mandatoryCourses.data.length > 0
|
||||
)
|
||||
fetch(mandatoryCourses.data.map(({ code }) => code))
|
||||
}, [query, filteredStudents, mandatoryCourses, populationSelectedStudentCourses])
|
||||
|
||||
@@ -62,7 +62,6 @@ const PopulationDetails = ({
|
||||
|
||||
const RenderCreditGainGraphs = () => {
|
||||
const { CreditAccumulation } = infotooltips.PopulationStatistics
|
||||
const { useFilterSelector } = useFilters()
|
||||
const studyPlanFilterIsActive = useFilterSelector(studyPlanFilter.selectors.isActive)
|
||||
|
||||
const graphs = (
|
||||
|
||||
@@ -10,10 +10,8 @@ import useLanguage from '../LanguagePicker/useLanguage'
|
||||
|
||||
const PopulationQueryCard = ({ population, query, removeSampleFn, units, tags }) => {
|
||||
const { language } = useLanguage()
|
||||
const { uuid, year, semesters, months, studentStatuses, tag, years } = query
|
||||
const { uuid, year, semesters, months, studentStatuses, tag } = query
|
||||
const tagname = tag && tags.length > 0 ? tags.find(t => t.tag_id === tag).tagname : ''
|
||||
const lowestYear = query.years ? Math.min(...query.years.map(year => Number(year))) : null
|
||||
const highestYear = query.years ? Math.max(...query.years.map(year => Number(year))) : null
|
||||
const { students } = population
|
||||
const header = units.map(u => getTextIn(u.name, language)).join(', ')
|
||||
const semesterList = semesters.map(s => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()).join(', ')
|
||||
@@ -23,19 +21,10 @@ const PopulationQueryCard = ({ population, query, removeSampleFn, units, tags })
|
||||
<>
|
||||
<Card className="cardContainer">
|
||||
<Card.Header className="cardHeader">
|
||||
<div>{header}</div>
|
||||
<div>Result details</div>
|
||||
</Card.Header>
|
||||
<Card.Meta>
|
||||
<div className="dateItem">
|
||||
<Icon name="calendar" size="small" />
|
||||
{!years
|
||||
? `${semesterList} /
|
||||
${year}-${Number(year) + 1}, showing ${months} months.`
|
||||
: `${semesterList} /
|
||||
started during ${lowestYear}-${highestYear}`}
|
||||
</div>
|
||||
{tag ? <div>{`Tagged with: ${tagname}`}</div> : null}
|
||||
<div>Sample size: {students.length} students</div>
|
||||
<div>{`Updated at ${reformatDate(minBy(students, 'updatedAt').updatedAt, DISPLAY_DATE_FORMAT)} `}</div>
|
||||
<div>{studentStatuses.includes('EXCHANGE') ? 'Includes' : 'Excludes'} exchange students</div>
|
||||
<div>
|
||||
|
||||
@@ -16,7 +16,6 @@ const FilterActiveNote = () => {
|
||||
|
||||
return (
|
||||
<Message color="blue" style={{ marginTop: '2.25rem' }}>
|
||||
<Message.Header>Note:</Message.Header>
|
||||
By default only students who have not transferred to this study programme are shown.
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<Button primary disabled={clicked} onClick={handleClick}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import qs from 'query-string'
|
||||
import { Form, Button, Message, Icon, Grid } from 'semantic-ui-react'
|
||||
import uuidv4 from 'uuid/v4'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import Datetime from 'react-datetime'
|
||||
import { sortBy, isEqual } from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
@@ -5,7 +5,6 @@ import { func, shape, object, bool, arrayOf } from 'prop-types'
|
||||
import { Form, Button } from 'semantic-ui-react'
|
||||
import moment from 'moment'
|
||||
import qs from 'query-string'
|
||||
import Datetime from 'react-datetime'
|
||||
import { get as lodashGet } from 'lodash'
|
||||
import PopulationQueryCard from '../PopulationQueryCard'
|
||||
import { removePopulation } from '../../redux/populations'
|
||||
@@ -88,15 +87,6 @@ class PopulationSearchHistory extends Component {
|
||||
return Math.ceil(moment.duration(moment().diff(moment(start))).asMonths())
|
||||
}
|
||||
|
||||
getMonths = (year, end, term) => {
|
||||
if (moment.isMoment(end)) {
|
||||
const lastDayOfMonth = moment(end).endOf('month')
|
||||
const start = term === 'FALL' ? `${year}-08-01` : `${year}-01-01`
|
||||
return Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(start))).asMonths())
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
handleSemesterSelection = (e, { value }) => {
|
||||
const { query } = this.state
|
||||
const semesters = query.semesters.includes(value)
|
||||
@@ -126,24 +116,6 @@ class PopulationSearchHistory extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleMonthsChange = value => {
|
||||
const { query } = this.state
|
||||
const months = this.getMonths(query.year, value, query.semesters.includes('FALL') ? 'FALL' : 'SPRING')
|
||||
this.setState({
|
||||
query: {
|
||||
...query,
|
||||
months,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
getMonthValue = (year, months) => {
|
||||
const start = `${year}-08-01`
|
||||
return moment(start)
|
||||
.add(months - 1, 'months')
|
||||
.format('MMMM YYYY')
|
||||
}
|
||||
|
||||
pushQueryToUrl = () => {
|
||||
const { studyRights, tag, years } = this.props.populations.query
|
||||
const { studentStatuses, semesters, months, year } = this.state.query
|
||||
@@ -162,8 +134,6 @@ class PopulationSearchHistory extends Component {
|
||||
this.props.history.push({ search: searchString })
|
||||
}
|
||||
|
||||
getMinSelection = (year, semester) => (semester === 'FALL' ? `${year}-08-01` : `${year}-01-01`)
|
||||
|
||||
removePopulation = uuid => this.props.removePopulation(uuid)
|
||||
|
||||
renderAdvancedSettingsSelector = () => {
|
||||
@@ -176,24 +146,9 @@ class PopulationSearchHistory extends Component {
|
||||
|
||||
return (
|
||||
<Form.Group style={{ flexDirection: 'column' }}>
|
||||
<Form.Field error={query.months < 0}>
|
||||
<b>Statistics until</b>
|
||||
<Datetime
|
||||
dateFormat="MMMM YYYY"
|
||||
closeOnSelect
|
||||
defaultValue={this.getMonthValue(query.year, query.months)}
|
||||
onChange={value => this.handleMonthsChange(value)}
|
||||
isValidDate={current =>
|
||||
current.isBefore(moment()) &&
|
||||
current.isAfter(this.getMinSelection(query.year, query.semesters[1] || query.semesters[0]))
|
||||
}
|
||||
className="adv-stats-until"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
{!populations.query.tag ? (
|
||||
<Form.Field style={{ marginTop: '15px' }}>
|
||||
<b>Semesters</b>
|
||||
<b>Starting semesters</b>
|
||||
<Form.Checkbox
|
||||
className="populationStatisticsRadio"
|
||||
key="FALL"
|
||||
@@ -218,7 +173,8 @@ class PopulationSearchHistory extends Component {
|
||||
) : null}
|
||||
<Form.Field style={{ marginTop: '15px' }}>
|
||||
<b>Include</b>
|
||||
<Form.Checkbox
|
||||
{/* Hide buggy options for now */}
|
||||
{/* <Form.Checkbox
|
||||
className="populationStatisticsRadio"
|
||||
key="EXCHANGE"
|
||||
label="Exchange students"
|
||||
@@ -235,7 +191,7 @@ class PopulationSearchHistory extends Component {
|
||||
name="studentStatusGroup"
|
||||
checked={studentStatuses.includes('NONDEGREE')}
|
||||
onChange={this.handleStudentStatusSelection}
|
||||
/>
|
||||
/> */}
|
||||
<Form.Checkbox
|
||||
className="populationStatisticsRadio"
|
||||
key="TRANSFERRED"
|
||||
|
||||
@@ -31,26 +31,24 @@ const PopulationSearch = ({ populationFound, history, location, loading }) => {
|
||||
|
||||
{location.search !== '' && !loading && (
|
||||
<Form>
|
||||
<Form.Group inline>
|
||||
<Form.Field>
|
||||
<Link to="/populations">
|
||||
<Button icon labelPosition="left" color="blue">
|
||||
<Icon name="left arrow" />
|
||||
Search new class
|
||||
</Button>
|
||||
</Link>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Radio
|
||||
toggle
|
||||
checked={onlyHopsCredits}
|
||||
onClick={() => {
|
||||
filterDispatch(hopsFilter.actions.toggle())
|
||||
}}
|
||||
label="Show only credits included in study plan"
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
<Form.Field>
|
||||
<Link to="/populations">
|
||||
<Button icon labelPosition="left" color="blue">
|
||||
<Icon name="left arrow" />
|
||||
Search new class
|
||||
</Button>
|
||||
</Link>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Radio
|
||||
toggle
|
||||
checked={onlyHopsCredits}
|
||||
onClick={() => {
|
||||
filterDispatch(hopsFilter.actions.toggle())
|
||||
}}
|
||||
label="Show only credits included in study plan"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<PopulationSearchHistory history={history} />
|
||||
</Form>
|
||||
|
||||
@@ -134,6 +134,7 @@ export default ({ students }) => {
|
||||
'all credits': s.allCredits,
|
||||
'hops credits': s.hopsCredits,
|
||||
'credits since start': s.credits,
|
||||
'phone number': s.phoneNumber,
|
||||
email: s.email,
|
||||
'transferred from': s.transferredStudyright ? transferFrom(s) : '',
|
||||
priority: priorityText(s.studyrights),
|
||||
|
||||
@@ -6,7 +6,9 @@ import { useGetSemestersQuery } from 'redux/semesters'
|
||||
import populationToData from 'selectors/populationDetails'
|
||||
import { getStudentTotalCredits } from 'common'
|
||||
import PopulationDetails from '../PopulationDetails'
|
||||
import { useLanguage, useTitle } from '../../common/hooks'
|
||||
import { useTitle } from '../../common/hooks'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import { getTextIn } from '../../common'
|
||||
import FilterView from '../FilterView'
|
||||
import PopulationSearch from '../PopulationSearch'
|
||||
import DataExport from './DataExport'
|
||||
@@ -30,7 +32,7 @@ import {
|
||||
|
||||
const PopulationStatistics = () => {
|
||||
const location = useLocation()
|
||||
const language = useLanguage()
|
||||
const { language } = useLanguage()
|
||||
const history = useHistory()
|
||||
// const { query, queryIsSet, isLoading, students } = useSelector(selectPopulations)
|
||||
const courses = useSelector(store => store.populationSelectedStudentCourses.data?.coursestatistics)
|
||||
@@ -43,6 +45,8 @@ const PopulationStatistics = () => {
|
||||
const { data: allSemesters } = useGetSemestersQuery()
|
||||
|
||||
const programmeCode = query?.studyRights?.programme
|
||||
const programmes = useSelector(store => store.populationProgrammes?.data?.programmes)
|
||||
const programmeName = programmes && programmeCode ? programmes[programmeCode]?.name : ''
|
||||
|
||||
const filters = [
|
||||
studentNumberFilter,
|
||||
@@ -91,6 +95,11 @@ const PopulationStatistics = () => {
|
||||
})
|
||||
}, [samples, programmeCode])
|
||||
|
||||
const title =
|
||||
location.search === ''
|
||||
? 'Class statistics'
|
||||
: `${getTextIn(programmeName, language)} ${query?.year} - ${Number(query?.year) + 1}`
|
||||
|
||||
return (
|
||||
<FilterView
|
||||
name="PopulationStatistics"
|
||||
@@ -108,9 +117,15 @@ const PopulationStatistics = () => {
|
||||
>
|
||||
{filteredStudents => (
|
||||
<div className="segmentContainer" style={{ flexGrow: 1 }}>
|
||||
<Header className="segmentTitle" size="large">
|
||||
Class statistics
|
||||
<Header className="segmentTitle" size="large" align="center">
|
||||
{title}
|
||||
{location.search !== '' && (
|
||||
<Header.Subheader>
|
||||
studytime {query?.months} months, class size {students.length} students
|
||||
</Header.Subheader>
|
||||
)}
|
||||
</Header>
|
||||
|
||||
<Segment className="contentSegment">
|
||||
<PopulationSearch history={history} location={location} />
|
||||
{location.search !== '' ? (
|
||||
|
||||
@@ -443,7 +443,8 @@ const GeneralTab = ({
|
||||
key: 'endDate',
|
||||
title: 'graduation date',
|
||||
filterType: 'date',
|
||||
getRowVal: s => new Date(studentToStudyrightEndMap[s.studentNumber]),
|
||||
getRowVal: s =>
|
||||
studentToStudyrightEndMap[s.studentNumber] ? new Date(studentToStudyrightEndMap[s.studentNumber]) : '',
|
||||
getRowContent: s =>
|
||||
studentToStudyrightEndMap[s.studentNumber]
|
||||
? reformatDate(studentToStudyrightEndMap[s.studentNumber], 'YYYY-MM-DD')
|
||||
|
||||
@@ -212,7 +212,7 @@ const CoursesTable = ({ students }) => {
|
||||
...sortedlabels
|
||||
.filter(({ code }) => visibleLabels.has(code))
|
||||
.map(e => ({
|
||||
key: e.id,
|
||||
key: e.id + e.code,
|
||||
title: (
|
||||
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
|
||||
<div>{e.code}</div>
|
||||
@@ -252,14 +252,14 @@ const CoursesTable = ({ students }) => {
|
||||
return getTotalRowVal(s, m)
|
||||
}
|
||||
|
||||
return hasPassedMandatory(s.studentNumber, m.code) ? 'Passed' : 'Not passed'
|
||||
return hasPassedMandatory(s.studentNumber, m.code) ? 'Passed' : ''
|
||||
},
|
||||
getRowExportVal: s => {
|
||||
if (s.total) {
|
||||
return getTotalRowVal(s, m)
|
||||
}
|
||||
|
||||
return hasPassedMandatory(s.studentNumber, m.code) ? 'Passed' : 'Not passed'
|
||||
return hasPassedMandatory(s.studentNumber, m.code) ? 'Passed' : ''
|
||||
},
|
||||
getRowContent: s => {
|
||||
if (s.total) return getTotalRowVal(s, m)
|
||||
|
||||
@@ -40,6 +40,7 @@ const Updater = React.lazy(() => retry(() => import('../Updater')))
|
||||
const Trends = React.lazy(() => retry(() => import('../Trends')))
|
||||
const StudyGuidanceGroups = React.lazy(() => retry(() => import('../StudyGuidanceGroups')))
|
||||
const FacultyStatistics = React.lazy(() => retry(() => import('../FacultyStatistics')))
|
||||
const CustomOpenUniPopulations = React.lazy(() => retry(() => import('../CustomOpenUniPopulation')))
|
||||
|
||||
const routes = {
|
||||
students: '/students/:studentNumber?',
|
||||
@@ -53,6 +54,7 @@ const routes = {
|
||||
trends: '/trends',
|
||||
studyGuidanceGroups: '/studyguidancegroups/:groupid?',
|
||||
faculties: '/faculties/:facultyCode?',
|
||||
customOpenUniPopulation: '/openunipopulation',
|
||||
}
|
||||
|
||||
const Routes = () => (
|
||||
@@ -65,7 +67,7 @@ const Routes = () => (
|
||||
<ProtectedRoute
|
||||
requiredRoles={['admin', 'facultyStatistics']}
|
||||
exact
|
||||
path="/faculties/:facultyCode?"
|
||||
path={routes.faculties}
|
||||
component={FacultyStatistics}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
@@ -98,6 +100,12 @@ const Routes = () => (
|
||||
path={routes.custompopulation}
|
||||
component={CustomPopulation}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
requiredRoles={['admin', 'openUniSearch']}
|
||||
exact
|
||||
path={routes.customOpenUniPopulation}
|
||||
component={CustomOpenUniPopulations}
|
||||
/>
|
||||
<ProtectedRoute requiredRoles={['admin']} requireUserHasRights exact path={routes.updater} component={Updater} />
|
||||
<ProtectedRoute
|
||||
requiredRoles={['studyGuidanceGroups']}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react'
|
||||
import moment from 'moment'
|
||||
import { sortBy } from 'lodash'
|
||||
import { Segment, Form, Header, Icon } from 'semantic-ui-react'
|
||||
import { arrayOf, date, func, shape, string, bool } from 'prop-types'
|
||||
import { arrayOf, func, shape, string, bool } from 'prop-types'
|
||||
|
||||
const SearchHistory = ({ items, handleSearch, updateItem, disabled, header }) => {
|
||||
const [selected, setSelected] = useState(null)
|
||||
@@ -41,7 +41,7 @@ const SearchHistory = ({ items, handleSearch, updateItem, disabled, header }) =>
|
||||
key: id,
|
||||
value: id,
|
||||
text,
|
||||
description: moment(timestamp).format('DD.MM LT'),
|
||||
description: moment(timestamp).format('DD.MM.YYYY LT'),
|
||||
}))}
|
||||
onChange={handleChange}
|
||||
closeOnChange
|
||||
@@ -65,7 +65,7 @@ SearchHistory.propTypes = {
|
||||
shape({
|
||||
text: string,
|
||||
params: shape({}),
|
||||
timestamp: date,
|
||||
timestamp: string,
|
||||
})
|
||||
).isRequired,
|
||||
handleSearch: func.isRequired,
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, { useState, useMemo } from 'react'
|
||||
import { Modal, Table, Button, Checkbox } from 'semantic-ui-react'
|
||||
import _ from 'lodash'
|
||||
import xlsx from 'xlsx'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getColumnTitle, getDataItemType, DataItemType } from './common'
|
||||
import ValueVisitor from './ValueVisitor'
|
||||
import ExportVisitor from './ExportVisitor'
|
||||
@@ -86,6 +87,10 @@ const ExportModal = ({ open, onOpen, onClose, data, columns }) => {
|
||||
return (
|
||||
<Modal onOpen={onOpen} onClose={onClose} open={open}>
|
||||
<Modal.Header>Export to Excel</Modal.Header>
|
||||
<Modal.Actions>
|
||||
<Button content="Cancel" onClick={() => onClose()} />
|
||||
<Button primary content="Export" onClick={() => handleExport()} />
|
||||
</Modal.Actions>
|
||||
<Modal.Content>
|
||||
<p>
|
||||
Exporting {flatData.length} rows into an Excel (.xlsx) file. Choose which columns you want to include in the
|
||||
@@ -106,9 +111,11 @@ const ExportModal = ({ open, onOpen, onClose, data, columns }) => {
|
||||
|
||||
<Table celled striped>
|
||||
<Table.Header>
|
||||
<Table.HeaderCell />
|
||||
<Table.HeaderCell>Column</Table.HeaderCell>
|
||||
<Table.HeaderCell>Sample Values</Table.HeaderCell>
|
||||
<Table.Row key="TitleRow">
|
||||
<Table.HeaderCell />
|
||||
<Table.HeaderCell>Column</Table.HeaderCell>
|
||||
<Table.HeaderCell>Sample Values</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{exportColumns.map(column => (
|
||||
@@ -121,6 +128,7 @@ const ExportModal = ({ open, onOpen, onClose, data, columns }) => {
|
||||
<div style={{ width: '0' }}>
|
||||
{sampledValues[column.key].map(value => (
|
||||
<span
|
||||
key={uuidv4()}
|
||||
style={{
|
||||
border: '1px solid #dedede',
|
||||
borderRadius: '3px',
|
||||
@@ -140,10 +148,6 @@ const ExportModal = ({ open, onOpen, onClose, data, columns }) => {
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button content="Cancel" onClick={() => onClose()} />
|
||||
<Button primary content="Export" onClick={() => handleExport()} />
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Icon, Popup } from 'semantic-ui-react'
|
||||
import FigureContainer from 'components/FigureContainer'
|
||||
import _ from 'lodash'
|
||||
import produce from 'immer'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import ExportModal from './ExportModal'
|
||||
import { row, group, getDataItemType, SortableTableContext, DataItemType } from './common'
|
||||
import DefaultColumnFilter from './defaultFilter'
|
||||
@@ -16,6 +17,12 @@ import GroupKeyVisitor from './GroupKeyVisitor'
|
||||
import ValueVisitor from './ValueVisitor'
|
||||
import './style.css'
|
||||
|
||||
const getKey = data => {
|
||||
if (data.studentnumber) return data.studentnumber
|
||||
if (data.id) return data.id
|
||||
return uuidv4()
|
||||
}
|
||||
|
||||
const computeColumnSpans = columns => {
|
||||
const stack = _.cloneDeep(columns)
|
||||
const spans = {}
|
||||
@@ -113,18 +120,22 @@ const Row = ({ data, isGroup, parents }) => {
|
||||
|
||||
if (content !== undefined && content !== null) {
|
||||
cells.push(
|
||||
<td colSpan={columnSpans[column.key]} {...cellProps}>
|
||||
<td colSpan={columnSpans[column.key]} key={column.key + cellProps.title} {...cellProps}>
|
||||
<ColumnContent column={column} data={data} isGroup={isGroup} parents={parents} />
|
||||
</td>
|
||||
)
|
||||
} else if (column.children && column.children.length > 0) {
|
||||
stack.splice(0, 0, ...column.children.map((c, i) => ({ ...c, childIndex: i })))
|
||||
} else {
|
||||
cells.push(<td {...cellProps} />)
|
||||
cells.push(<td key={`${column.key}-else-${cellProps.title}`} {...cellProps} />)
|
||||
}
|
||||
}
|
||||
|
||||
return <tr className={isGroup ? 'group-header-row' : ''}>{cells}</tr>
|
||||
return (
|
||||
<tr className={isGroup ? 'group-header-row' : ''} key={getKey(data)}>
|
||||
{cells}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const mergeColumnDefinitions = (original, overlay) => {
|
||||
@@ -401,6 +412,7 @@ const ColumnHeaderContent = React.memo(({ column, colSpan, state, dispatch, rowS
|
||||
verticalAlign: column.vertical ? 'top' : 'center',
|
||||
position: 'relative',
|
||||
overflow: toolsMode === 'floating' ? 'hidden' : '',
|
||||
display: toolsMode === 'none' ? 'none' : '',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (sortable) {
|
||||
@@ -605,17 +617,17 @@ const createHeaders = (columns, columnDepth, dispatch) => {
|
||||
stack = [...children, ...stack]
|
||||
}
|
||||
}
|
||||
|
||||
return rows.map(cells => <tr>{cells}</tr>)
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
return rows.map((cells, idx) => <tr key={idx}>{cells}</tr>)
|
||||
}
|
||||
|
||||
const getInitialState = defaultSort => () => ({
|
||||
const getInitialState = (defaultSort, expandedGroups) => () => ({
|
||||
columnOptions: !defaultSort
|
||||
? {}
|
||||
: {
|
||||
[defaultSort[0]]: { filterOptions: undefined, sort: defaultSort[1] },
|
||||
},
|
||||
expandedGroups: [],
|
||||
expandedGroups: !expandedGroups ? [] : Array.from(expandedGroups), // [],
|
||||
})
|
||||
|
||||
const tableStateReducer = (...args) =>
|
||||
@@ -801,9 +813,15 @@ const SortableTable = ({
|
||||
contextMenuItems: pContextMenuItems,
|
||||
singleLine = true,
|
||||
figure = true,
|
||||
toggleGroupExpansion,
|
||||
expandedGroups,
|
||||
}) => {
|
||||
const [exportModalOpen, setExportModalOpen] = useState(false)
|
||||
const [state, dispatch] = useReducer(tableStateReducer(defaultSort), null, getInitialState(defaultSort))
|
||||
const [state, dispatch] = useReducer(
|
||||
tableStateReducer(defaultSort),
|
||||
null,
|
||||
getInitialState(defaultSort, expandedGroups)
|
||||
)
|
||||
const groupDepth = useMemo(() => calculateGroupDepth(data), [data])
|
||||
|
||||
const toggleGroup = useCallback(
|
||||
@@ -812,6 +830,9 @@ const SortableTable = ({
|
||||
type: 'TOGGLE_GROUP',
|
||||
payload: { group: groupKey },
|
||||
})
|
||||
if (toggleGroupExpansion) {
|
||||
toggleGroupExpansion(groupKey)
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
@@ -823,6 +844,9 @@ const SortableTable = ({
|
||||
type: 'SET_UNFOLDED_GROUPS',
|
||||
payload: { groups },
|
||||
})
|
||||
if (toggleGroupExpansion) {
|
||||
toggleGroupExpansion('', false, groups)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
const handleFoldAllGroups = useCallback(() => {
|
||||
@@ -830,6 +854,9 @@ const SortableTable = ({
|
||||
type: 'SET_UNFOLDED_GROUPS',
|
||||
payload: { groups: [] },
|
||||
})
|
||||
if (toggleGroupExpansion) {
|
||||
toggleGroupExpansion('', true)
|
||||
}
|
||||
}, [data])
|
||||
|
||||
const [columns, columnsByKey] = useMemo(() => {
|
||||
@@ -898,7 +925,7 @@ const SortableTable = ({
|
||||
<thead style={{ position: 'sticky', top: 0, zIndex: 1 }}>{headers}</thead>
|
||||
<tbody>
|
||||
{sortedData.map(item => (
|
||||
<DataItem item={item} key={item.key} />
|
||||
<DataItem key={`dataItem-${getKey(item)}`} item={item} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useGetBasicStatsQuery, useGetCreditStatsQuery, useGetGraduationStatsQue
|
||||
import LineGraph from './LineGraph'
|
||||
import StackedBarChart from './StackedBarChart'
|
||||
import BarChart from './BarChart'
|
||||
import GaugeChart from './GaugeChart'
|
||||
import TimeBarChart from '../TimeBarChart'
|
||||
import DataTable from './DataTable'
|
||||
import Toggle from '../Toggle'
|
||||
import InfoBox from '../../Info/InfoBox'
|
||||
@@ -17,12 +17,16 @@ const isNewProgramme = code => code.includes('KH') || code.includes('MH') || /^(
|
||||
|
||||
const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYear, setAcademicYear }) => {
|
||||
const [showMeanTime, setShowMeanTime] = useState(false)
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [showByStartYear, setShowByStartYear] = useState(false)
|
||||
const toolTips = InfotoolTips.Studyprogramme
|
||||
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
|
||||
const special = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
|
||||
const basics = useGetBasicStatsQuery({ id: studyprogramme, yearType, specialGroups: special })
|
||||
const credits = useGetCreditStatsQuery({ id: studyprogramme, yearType, specialGroups: special })
|
||||
const graduations = useGetGraduationStatsQuery({ id: studyprogramme, yearType, specialGroups: special })
|
||||
const doCombo = graduations?.data?.doCombo
|
||||
const timesData = showByStartYear ? graduations?.data?.graduationTimesByStartYear : graduations?.data?.graduationTimes
|
||||
|
||||
const getDivider = (title, toolTipText) => (
|
||||
<>
|
||||
@@ -126,29 +130,40 @@ const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYea
|
||||
/>
|
||||
</div>
|
||||
{getDivider('Average graduation times', 'AverageGraduationTimes')}
|
||||
<Toggle
|
||||
cypress="GraduationTimeToggle"
|
||||
firstLabel="Median time"
|
||||
secondLabel="Mean time"
|
||||
value={showMeanTime}
|
||||
setValue={setShowMeanTime}
|
||||
/>
|
||||
<div className="section-container-centered">
|
||||
{graduations?.data?.years.map(year => (
|
||||
<GaugeChart
|
||||
cypress={`${year}-AverageGraduationTimes`}
|
||||
key={year}
|
||||
year={year}
|
||||
data={
|
||||
showMeanTime
|
||||
? graduations?.data?.graduationMeanTime?.[year]
|
||||
: graduations?.data?.graduationMedianTime?.[year]
|
||||
}
|
||||
graduationAmount={graduations?.data?.graduationAmounts?.[year]}
|
||||
totalAmount={graduations?.data?.totalAmounts?.[year]}
|
||||
studyprogramme={studyprogramme}
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="GraduationTimeToggle"
|
||||
firstLabel="Median time"
|
||||
secondLabel="Mean time"
|
||||
value={showMeanTime}
|
||||
setValue={setShowMeanTime}
|
||||
/>
|
||||
{/* <Toggle
|
||||
cypress="GroupByToggle"
|
||||
firstLabel="Group by: Graduation year"
|
||||
secondLabel="Starting Year"
|
||||
value={showByStartYear}
|
||||
setValue={setShowByStartYear}
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
<div className={`section-container${doCombo ? '' : '-centered'}`}>
|
||||
{doCombo && (
|
||||
<TimeBarChart
|
||||
data={showMeanTime ? graduations?.data?.comboTimes?.means : graduations?.data?.comboTimes?.medians}
|
||||
goal={graduations?.data?.comboTimes?.goal}
|
||||
showMeanTime={showMeanTime}
|
||||
title="Bachelor + Master studyright"
|
||||
byStartYear={showByStartYear}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
<TimeBarChart
|
||||
data={showMeanTime ? timesData?.means : timesData?.medians}
|
||||
goal={graduations?.data.graduationTimes?.goal}
|
||||
showMeanTime={showMeanTime}
|
||||
title={doCombo ? 'Master studyright' : ' '}
|
||||
byStartYear={showByStartYear}
|
||||
/>
|
||||
</div>
|
||||
{graduations?.data?.programmesBeforeOrAfterGraphStats && (
|
||||
<>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Loader, Message, Header, Form } from 'semantic-ui-react'
|
||||
import { getTextIn } from '../../../common'
|
||||
import SortableTable from '../../SortableTable'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
import codes from '../../../common/programmeCodes'
|
||||
|
||||
const StudyProgrammeSelector = ({ studyprogrammes, selected }) => {
|
||||
const [filter, setFilter] = useState('')
|
||||
@@ -37,12 +36,12 @@ const StudyProgrammeSelector = ({ studyprogrammes, selected }) => {
|
||||
return (
|
||||
programme.code.toLowerCase().includes(filter.toLocaleLowerCase()) ||
|
||||
programme.name[language].toLowerCase().includes(filter.toLocaleLowerCase()) ||
|
||||
(codes[programme.code] && codes[programme.code].toLowerCase().includes(filter.toLocaleLowerCase()))
|
||||
(programme.progId && programme.progId.toLowerCase().includes(filter.toLocaleLowerCase()))
|
||||
)
|
||||
return (
|
||||
programme.code.toLowerCase().includes(filter.toLocaleLowerCase()) ||
|
||||
programme.name.fi.toLowerCase().includes(filter.toLocaleLowerCase()) ||
|
||||
(codes[programme.code] && codes[programme.code].toLowerCase().includes(filter.toLocaleLowerCase()))
|
||||
(programme.progId && programme.progId.toLowerCase().includes(filter.toLocaleLowerCase()))
|
||||
)
|
||||
})
|
||||
|
||||
@@ -95,7 +94,7 @@ const StudyProgrammeSelector = ({ studyprogrammes, selected }) => {
|
||||
{
|
||||
key: 'programmeId',
|
||||
title: 'id',
|
||||
getRowVal: prog => (codes[prog.code] ? codes[prog.code] : prog.code),
|
||||
getRowVal: prog => prog.progId ?? `${prog.code}-id`,
|
||||
getRowContent: prog => (
|
||||
<Link
|
||||
style={{
|
||||
@@ -107,7 +106,7 @@ const StudyProgrammeSelector = ({ studyprogrammes, selected }) => {
|
||||
}}
|
||||
to={`/study-programme/${prog.code}`}
|
||||
>
|
||||
{codes[prog.code] ? codes[prog.code].toUpperCase() : ''}
|
||||
{prog.progId ?? ''}
|
||||
</Link>
|
||||
),
|
||||
cellProps: {
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
const ReactHighcharts = require('react-highcharts')
|
||||
|
||||
const GaugeChart = ({ cypress, data, year, graduationAmount, totalAmount, studyprogramme }) => {
|
||||
if (!data || !graduationAmount || !data.length) return null
|
||||
|
||||
const thresholdValues = studyprogramme.includes('KH') ? [36, 41] : [24, 27]
|
||||
|
||||
const getColor = data => {
|
||||
if (!studyprogramme.includes('KH') && !studyprogramme.includes('MH')) return ['#1d44a1', '#EBECF0']
|
||||
if (data <= thresholdValues[0]) return ['#90A959', '#EBECF0']
|
||||
if (data > thresholdValues[0] && data <= thresholdValues[1]) return ['#FEE191', '#EBECF0']
|
||||
return ['#FB6962', '#EBECF0']
|
||||
}
|
||||
|
||||
if (!data) return null
|
||||
const defaultConfig = {
|
||||
chart: {
|
||||
plotBackgroundColor: null,
|
||||
plotBorderWidth: 0,
|
||||
plotShadow: false,
|
||||
width: 260,
|
||||
height: 300,
|
||||
},
|
||||
title: {
|
||||
text: `${data[0][1]} kk`,
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
y: 60,
|
||||
},
|
||||
subtitle: {
|
||||
text: `${year}<br/> n = ${graduationAmount} / ${totalAmount}`,
|
||||
style: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: '15px',
|
||||
},
|
||||
},
|
||||
credits: {
|
||||
text: '',
|
||||
},
|
||||
exporting: {
|
||||
enabled: false,
|
||||
},
|
||||
accessibility: {
|
||||
point: {
|
||||
valueSuffix: '%',
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
dataLabels: {
|
||||
title: '{point.percentage:.1f}%',
|
||||
distance: -50,
|
||||
style: {
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
},
|
||||
},
|
||||
startAngle: -90,
|
||||
endAngle: 90,
|
||||
center: ['50%', '75%'],
|
||||
size: '100%',
|
||||
colors: getColor(data[0][1]),
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
name: '',
|
||||
innerSize: '40%',
|
||||
data,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-cy={`Graph-${cypress}`}>
|
||||
<ReactHighcharts config={defaultConfig} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GaugeChart
|
||||
@@ -5,10 +5,10 @@ import { useGetStudytrackStatsQuery } from 'redux/studyProgramme'
|
||||
import InfoBox from '../../Info/InfoBox'
|
||||
import BarChart from './BarChart'
|
||||
import BasicDataTable from './BasicDataTable'
|
||||
import GaugeChart from './GaugeChart'
|
||||
import StudytrackDataTable from './StudytrackDataTable'
|
||||
import StudytrackSelector from './StudytrackSelector'
|
||||
import Toggle from '../Toggle'
|
||||
import TimeBarChart from '../TimeBarChart'
|
||||
|
||||
import InfotoolTips from '../../../common/InfoToolTips'
|
||||
import '../studyprogramme.css'
|
||||
@@ -111,7 +111,7 @@ const StudytrackOverview = ({ studyprogramme, specialGroups, setSpecialGroups, g
|
||||
</div>
|
||||
{stats?.isSuccess && stats?.data?.includeGraduated && (
|
||||
<>
|
||||
{getDivider('Average graduation times', 'AverageGraduationTimes')}
|
||||
{getDivider('Average graduation times', 'AverageGraduationTimesStudytracks')}
|
||||
<Toggle
|
||||
firstLabel="Median time"
|
||||
secondLabel="Mean time"
|
||||
@@ -119,21 +119,17 @@ const StudytrackOverview = ({ studyprogramme, specialGroups, setSpecialGroups, g
|
||||
setValue={setShowMeanTime}
|
||||
/>
|
||||
<div className="section-container-centered">
|
||||
{stats?.data?.years.map(year => (
|
||||
<GaugeChart
|
||||
cypress={`${year.slice(0, 4)}-AverageGraduationTimes`}
|
||||
key={year}
|
||||
year={year}
|
||||
data={
|
||||
showMeanTime
|
||||
? stats?.data?.graduationMeanTime[track][year]
|
||||
: stats?.data?.graduationMedianTime[track][year]
|
||||
}
|
||||
graduationAmount={stats?.data?.graduationAmounts[track][year]}
|
||||
totalAmount={stats?.data?.totalAmounts[track][year]}
|
||||
studyprogramme={studyprogramme}
|
||||
/>
|
||||
))}
|
||||
<TimeBarChart
|
||||
data={
|
||||
showMeanTime
|
||||
? stats?.data?.graduationTimes[track].means
|
||||
: stats?.data?.graduationTimes[track].medians
|
||||
}
|
||||
goal={stats?.data?.graduationTimes.goal}
|
||||
showMeanTime={showMeanTime}
|
||||
byStartYear
|
||||
title={' '}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
import React from 'react'
|
||||
import ReactHighcharts from 'react-highcharts'
|
||||
|
||||
const TimeBarChart = ({ data, goal, showMeanTime, title, byStartYear }) => {
|
||||
if (!data) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const maxValue = data.reduce((max, { y }) => {
|
||||
return y > max ? y : max
|
||||
}, goal * 2)
|
||||
|
||||
const getPercentage = (amount, classSize) => {
|
||||
const percent = Math.round((amount / classSize) * 100 * 10) / 10
|
||||
return Number.isNaN(percent) ? 0 : percent
|
||||
}
|
||||
|
||||
const getDataLabel = (amount, classSize = undefined) => {
|
||||
if (byStartYear) {
|
||||
return `${amount} graduated (${getPercentage(amount, classSize)} % of class)`
|
||||
}
|
||||
return `${amount} graduated`
|
||||
}
|
||||
|
||||
const getHeight = () => {
|
||||
const t = data.length > 8 ? 35 : 55
|
||||
return data.length * t + 100
|
||||
}
|
||||
|
||||
const getTooltipText = (amount, y, year, statistics, classSize = undefined) => {
|
||||
const sortingText = byStartYear
|
||||
? `<b>From class of ${year}, ${amount}/${classSize} students have graduated</b>`
|
||||
: `<b>${amount} students graduated in year ${year}</b>`
|
||||
|
||||
const timeText = `<br /><p>${sortingText}, <br /><b>${
|
||||
showMeanTime ? 'mean' : 'median'
|
||||
} study time: ${y} months</p></b>`
|
||||
|
||||
const statisticsText = `<br /><p>${statistics.onTime} graduated on time</p><br />
|
||||
<p>${statistics.yearOver} graduated max year overtime</p>
|
||||
<br /><p>${statistics.wayOver} graduated over year late</p>`
|
||||
|
||||
return `${timeText}${statisticsText}`
|
||||
}
|
||||
|
||||
const config = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
width: 700,
|
||||
margin: [70, 0],
|
||||
height: getHeight(),
|
||||
},
|
||||
title: { text: title },
|
||||
tooltip: {
|
||||
backgroundColor: 'white',
|
||||
fontSize: '25px',
|
||||
// eslint-disable-next-line
|
||||
formatter: function() {
|
||||
return getTooltipText(this.point.amount, this.y, this.point.name, this.point.statistics, this.point?.classSize)
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
inside: true,
|
||||
overflow: 'allow',
|
||||
},
|
||||
pointPadding: 0.0,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data,
|
||||
dataLabels: [
|
||||
{
|
||||
align: 'left',
|
||||
color: '#424949',
|
||||
style: {
|
||||
textOutline: 'none',
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
formatter: function () {
|
||||
return getDataLabel(this.point.amount, this.point?.classSize)
|
||||
},
|
||||
},
|
||||
],
|
||||
showInLegend: false,
|
||||
zones: [
|
||||
{
|
||||
value: goal + 0.1,
|
||||
color: '#90A959',
|
||||
},
|
||||
{
|
||||
value: goal + 12.1,
|
||||
color: '#FEE191',
|
||||
},
|
||||
{
|
||||
color: '#FB6962',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
title: {
|
||||
text: byStartYear ? 'Start year' : 'Graduation year',
|
||||
align: 'high',
|
||||
offset: 0,
|
||||
rotation: 0,
|
||||
y: -10,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
min: 0,
|
||||
max: maxValue,
|
||||
title: { text: 'Graduation time (months)' },
|
||||
labels: {
|
||||
overflow: 'justify',
|
||||
},
|
||||
allowDecimals: false,
|
||||
showFirstLabel: false,
|
||||
plotLines: [
|
||||
{
|
||||
color: '#90A959',
|
||||
width: 2,
|
||||
value: goal,
|
||||
dashStyle: 'shortDash',
|
||||
},
|
||||
{
|
||||
color: '#FEE191',
|
||||
width: 2,
|
||||
value: goal + 12,
|
||||
dashStyle: 'shortDash',
|
||||
},
|
||||
],
|
||||
},
|
||||
credits: {
|
||||
text: 'oodikone | TOSKA',
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-cy={`graduation-times-graph${title.split(' ')[0]}`}>
|
||||
<ReactHighcharts config={config} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimeBarChart
|
||||
@@ -14,7 +14,6 @@ import { getTextIn } from '../../common'
|
||||
import { useTabs, useTitle } from '../../common/hooks'
|
||||
import TSA from '../../common/tsa'
|
||||
import Tags from './Tags'
|
||||
import codes from '../../common/programmeCodes'
|
||||
|
||||
import { getProgrammes } from '../../redux/populationProgrammes'
|
||||
|
||||
@@ -113,6 +112,7 @@ const StudyProgramme = props => {
|
||||
const { match } = props
|
||||
const { studyProgrammeId } = match.params
|
||||
const programmeName = programmes?.[studyProgrammeId] && getTextIn(programmes?.[studyProgrammeId].name, language)
|
||||
const programmeLetterId = programmes?.[studyProgrammeId]?.progId
|
||||
const panes = getPanes()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -147,7 +147,7 @@ const StudyProgramme = props => {
|
||||
<div align="center" style={{ padding: '30px' }}>
|
||||
<Header textAlign="center">{programmeName}</Header>
|
||||
<span>
|
||||
{codes[studyProgrammeId] ? `${codes[studyProgrammeId].toUpperCase()} - ` : ''} {studyProgrammeId}
|
||||
{programmeLetterId ? `${programmeLetterId} - ` : ''} {studyProgrammeId}
|
||||
</span>
|
||||
</div>
|
||||
<Tab panes={panes} activeIndex={tab} onTabChange={setTab} />
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ const getColumns = (language, showStudents) => {
|
||||
{
|
||||
key: 'course_code',
|
||||
title: 'Code ',
|
||||
export: false,
|
||||
export: true,
|
||||
getRowVal: course => course.code,
|
||||
getRowContent: course => course.code,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import uuidv4 from 'uuid/v4'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export const sendError = (message = 'Something went wrong') => ({
|
||||
type: 'ADD_ERROR',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { callController } from '../apiConnection'
|
||||
// Use that for new stuff or switch while refactoring
|
||||
|
||||
export const getFaculties = () => {
|
||||
const route = '/faculties'
|
||||
const route = '/faculties-only'
|
||||
const prefix = 'GET_FACULTIES_'
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { RTKApi } from 'apiConnection'
|
||||
|
||||
const openUniPopulationApi = RTKApi.injectEndpoints({
|
||||
endpoints: builder => ({
|
||||
getOpenUniCourseStudents: builder.query({
|
||||
query: ({ courseList, startdate, enddate }) =>
|
||||
`/openunisearch?courselist=${JSON.stringify(courseList)}&startdate=${startdate}&enddate=${enddate}`,
|
||||
}),
|
||||
getSavedSearches: builder.query({
|
||||
query: () => `/openunisearch/searches`,
|
||||
}),
|
||||
createOpenUniCourseSearch: builder.mutation({
|
||||
query: ({ courseList, name }) => ({
|
||||
url: '/openunisearch/searches',
|
||||
method: 'POST',
|
||||
body: {
|
||||
courselist: courseList,
|
||||
name,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
updateOpenUniCourseSearch: builder.mutation({
|
||||
query: ({ id, courseList }) => ({
|
||||
url: `/openunisearch/searches/${id}`,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
courselist: courseList,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
deleteOpenUniCourseSearch: builder.mutation({
|
||||
query: ({ id }) => ({
|
||||
url: `/openunisearch/searches/${id}`,
|
||||
method: 'DELETE',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
overrideExisting: false,
|
||||
})
|
||||
|
||||
export const {
|
||||
useGetOpenUniCourseStudentsQuery,
|
||||
useGetSavedSearchesQuery,
|
||||
useCreateOpenUniCourseSearchMutation,
|
||||
useDeleteOpenUniCourseSearchMutation,
|
||||
useUpdateOpenUniCourseSearchMutation,
|
||||
} = openUniPopulationApi
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
const { STRING } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.addColumn('student', 'secondary_email', STRING)
|
||||
},
|
||||
down: async queryInterface => {
|
||||
await queryInterface.removeColumn('student', 'secondary_email')
|
||||
},
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
const { BOOLEAN } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.addColumn('student', 'dissemination_info_allowed', BOOLEAN)
|
||||
},
|
||||
down: async queryInterface => {
|
||||
await queryInterface.removeColumn('student', 'dissemination_info_allowed')
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
const { STRING } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.addColumn('student', 'phone_number', STRING)
|
||||
},
|
||||
down: async queryInterface => {
|
||||
await queryInterface.removeColumn('student', 'phone_number')
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const { Model, STRING, DATE, INTEGER } = require('sequelize')
|
||||
const { Model, STRING, DATE, INTEGER, BOOLEAN } = require('sequelize')
|
||||
const { dbConnections } = require('../connection')
|
||||
|
||||
class Student extends Model {}
|
||||
@@ -16,6 +16,8 @@ Student.init(
|
||||
creditcount: { type: INTEGER },
|
||||
dateofuniversityenrollment: { type: DATE },
|
||||
email: { type: STRING },
|
||||
phone_number: { type: STRING },
|
||||
secondary_email: { type: STRING },
|
||||
national_student_number: { type: STRING },
|
||||
country_fi: { type: STRING },
|
||||
country_sv: { type: STRING },
|
||||
@@ -25,6 +27,7 @@ Student.init(
|
||||
home_country_en: { type: STRING },
|
||||
gender_code: { type: INTEGER },
|
||||
sis_person_id: { type: STRING },
|
||||
dissemination_info_allowed: { type: BOOLEAN },
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
|
||||
@@ -83,8 +83,19 @@ const calculateTotalCreditsFromAttainments = attainments => {
|
||||
return totalCredits
|
||||
}
|
||||
|
||||
const studentMapper = (attainments, studyRights, attainmentsToBeExluced) => student => {
|
||||
const { last_name, first_names, student_number, primary_email, gender_urn, oppija_id, date_of_birth, id } = student
|
||||
const studentMapper = (attainments, studyRights, attainmentsToBeExluced, disclosures) => student => {
|
||||
const {
|
||||
last_name,
|
||||
first_names,
|
||||
student_number,
|
||||
primary_email,
|
||||
secondary_email,
|
||||
phone_number,
|
||||
gender_urn,
|
||||
oppija_id,
|
||||
date_of_birth,
|
||||
id,
|
||||
} = student
|
||||
|
||||
// Filter out test student from oodi data
|
||||
if (student_number === '012023965') return null
|
||||
@@ -95,9 +106,13 @@ const studentMapper = (attainments, studyRights, attainmentsToBeExluced) => stud
|
||||
// https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2958
|
||||
const country = getCountry(student.country_urn)
|
||||
const home_country = student.citizenships ? getCountry(student.citizenships[0]) : null
|
||||
// "hy-luovutusehto-6" = Open University's dissemination of information on its course offerings
|
||||
const openUniDisseminationOfInfo = disclosures.filter(
|
||||
d => d.person_id === id && d.disclosure_category_id === 'hy-luovutusehto-6'
|
||||
)
|
||||
const marketingAllowed = openUniDisseminationOfInfo.length > 0 ? openUniDisseminationOfInfo[0].authorized : null
|
||||
|
||||
const studyRightsOfStudent = studyRights.filter(SR => SR.person_id === id)
|
||||
|
||||
const dateofuniversityenrollment =
|
||||
studyRightsOfStudent.length > 0 ? sortBy(studyRightsOfStudent.map(sr => sr.valid.startDate))[0] : null
|
||||
|
||||
@@ -112,6 +127,8 @@ const studentMapper = (attainments, studyRights, attainmentsToBeExluced) => stud
|
||||
abbreviatedname: `${last_name} ${first_names}`.trim(),
|
||||
studentnumber: student_number,
|
||||
email: primary_email,
|
||||
secondary_email: secondary_email,
|
||||
phone_number: phone_number,
|
||||
gender_code,
|
||||
national_student_number: oppija_id,
|
||||
birthdate: date_of_birth,
|
||||
@@ -124,6 +141,7 @@ const studentMapper = (attainments, studyRights, attainmentsToBeExluced) => stud
|
||||
home_country_sv: home_country ? home_country.name.sv : null,
|
||||
home_country_en: home_country ? home_country.name.en : null,
|
||||
sis_person_id: id,
|
||||
dissemination_info_allowed: marketingAllowed,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ const updateStudents = async personIds => {
|
||||
studyRightPrimalities,
|
||||
enrollments,
|
||||
studyplans,
|
||||
disclosures,
|
||||
] = await Promise.all([
|
||||
selectFromByIds('persons', personIds),
|
||||
selectFromByIds('studyrights', personIds, 'person_id'),
|
||||
@@ -191,6 +192,7 @@ const updateStudents = async personIds => {
|
||||
selectFromByIds('study_right_primalities', personIds, 'student_id'),
|
||||
selectFromByIds('enrolments', personIds, 'person_id'),
|
||||
selectFromByIds('plans', personIds, 'user_id'),
|
||||
selectFromByIds('disclosures', personIds, 'person_id'),
|
||||
])
|
||||
|
||||
const parsedStudyrightSnapshots = parseStudyrightSnapshots(studyrightSnapshots)
|
||||
@@ -209,10 +211,9 @@ const updateStudents = async personIds => {
|
||||
}, {})
|
||||
|
||||
const attainmentsToBeExluced = getAttainmentsToBeExcluded()
|
||||
|
||||
const mappedStudents = students
|
||||
.filter(s => s.student_number)
|
||||
.map(studentMapper(attainments, parsedStudyrightSnapshots, attainmentsToBeExluced))
|
||||
.map(studentMapper(attainments, parsedStudyrightSnapshots, attainmentsToBeExluced, disclosures))
|
||||
|
||||
await bulkCreate(Student, mappedStudents)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const updateStudyRights = async (
|
||||
groupedStudyRightSnapshots,
|
||||
personIdToStudentNumber,
|
||||
personIdToStudyRightIdToPrimality,
|
||||
personsTermRegistrations
|
||||
allTermRegistrations
|
||||
) => {
|
||||
const currentSemester = getSemesterByDate(new Date())
|
||||
|
||||
@@ -138,6 +138,10 @@ const updateStudyRights = async (
|
||||
|
||||
const formattedStudyRights = latestStudyRights.reduce((acc, studyright) => {
|
||||
const studyRightEducation = getEducation(studyright.education_id)
|
||||
|
||||
const personsTermRegistrations = allTermRegistrations.filter(
|
||||
registration => registration.study_right_id === studyright.id
|
||||
)
|
||||
const termRegistrations = flatMap(personsTermRegistrations, 'term_registrations')
|
||||
|
||||
if (!studyRightEducation) return acc
|
||||
|
||||
Reference in New Issue
Block a user