Compare commits

...
38 Commits
Author SHA1 Message Date
Saara Satokangas d54b3c0782 Fix one test 2022-02-22 08:53:16 +02:00
Saara Satokangas 28fa4e5310 Increase the viewport-size for cypress tests to see what's wrong 2022-02-22 08:37:00 +02:00
Saara Satokangas df402c4823 Try fixing some of the tests 2022-02-22 08:20:03 +02:00
Saara Satokangas 9136d41bdd Adjust tooltips to match the new table 2022-02-21 14:39:21 +02:00
Saara Satokangas e1f63c2441 Filter our transfers also from master's 2022-02-21 14:34:15 +02:00
Saara Satokangas d797b766b1 Refactor and clean-up 2022-02-21 14:30:41 +02:00
Saara Satokangas 03bed43077 Unify following masters programme -table with the bachelor one 2022-02-21 13:53:22 +02:00
Saara Satokangas 6763905b2a Adjust texts in frontend and variable names to match the new table 2022-02-21 13:23:40 +02:00
Saara Satokangas 994101732d Define studystartdate in the programme similarly as in populations 2022-02-21 13:04:19 +02:00
Saara Satokangas b1c071a507 Add table for bachelor's programmes before the chosen masters 2022-02-21 13:03:55 +02:00
Mitja Karhusaari 0e2bdc8d2d Potential fix for problems with Courses of Population -components 2022-02-16 13:26:42 +02:00
Saara SatokangasandGitHub 1cd6e2f435 Merge pull request #3425 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/backend/follow-redirects-1.14.8
Bump follow-redirects from 1.14.3 to 1.14.8 in /services/backend
2022-02-16 09:48:12 +02:00
Saara Satokangas 0790003ae8 Fix trends-updating erroring at night 2022-02-16 08:25:30 +02:00
Saara Satokangas a77bf8b6a9 Remove old logic studyprogramme-service 2022-02-15 15:50:58 +02:00
dependabot[bot]andGitHub 00cd57dc13 Bump follow-redirects from 1.14.3 to 1.14.8 in /services/backend
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.3 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.3...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-15 12:35:10 +00:00
Saara SatokangasandGitHub 44a8f39cc1 Merge pull request #3426 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/frontend/follow-redirects-1.14.8
Bump follow-redirects from 1.14.1 to 1.14.8 in /services/frontend
2022-02-15 14:34:17 +02:00
Saara Satokangas 4ecdb3237d Clarify the test-structure 2022-02-15 13:37:37 +02:00
Saara Satokangas 6b544179ef Add more tests for studytrack-overview 2022-02-15 13:16:26 +02:00
Saara Satokangas 9e29afedad Fix tests 2022-02-15 11:53:34 +02:00
Saara Satokangas 8610bf1084 Add tests for new studyprogramme overview 2022-02-15 11:35:01 +02:00
Saara Satokangas fe49a63408 Remove old reducer-for previous bachelor programmes 2022-02-15 08:19:28 +02:00
dependabot[bot]andGitHub a9100b78ec Bump follow-redirects from 1.14.1 to 1.14.8 in /services/frontend
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.1 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.1...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-14 20:34:45 +00:00
Saara Satokangas e3fb31ef8f Remove old productivity and progress functions from redis 2022-02-14 14:48:04 +02:00
Saara Satokangas f865796771 Remove old routes for getting option-data 2022-02-14 08:58:28 +02:00
Saara Satokangas 2532b5367e Remove routes for updating and getting old studyprogramme view's data 2022-02-14 08:58:28 +02:00
Saara Satokangas eeca5c52fc Remove old studyprogramme-reducers 2022-02-14 08:58:28 +02:00
Saara Satokangas 1e940fde73 Remove old infotooltips 2022-02-14 08:58:28 +02:00
Saara Satokangas 38a2f0a9a8 Remove propTypes from studyprogramme-overview, start using useSelector 2022-02-14 08:58:20 +02:00
Saara Satokangas 991203e08e Remove old studytrackoverview, rename new views 2022-02-14 08:13:49 +02:00
Saara Satokangas 0c8698ab04 Remove old overview 2022-02-14 08:10:46 +02:00
Saara Satokangas 57e04434b0 Remove old throughput-table and producitivity-tables 2022-02-14 08:09:52 +02:00
Saara Satokangas f1fcf9661d Remove old thesis-courses-list 2022-02-14 08:09:23 +02:00
Saara Satokangas 61ab3d9849 Add some comments 2022-02-14 08:09:09 +02:00
Saara Satokangas 52db993e1f Remove old bachelor's table 2022-02-14 08:08:28 +02:00
Saara Satokangas 66d4989fee Remove mentions of old views 2022-02-14 08:07:41 +02:00
Saara Satokangas 0439efddbe Make toggle-choices permanent between tabs 2022-02-14 07:35:46 +02:00
Saara Satokangas 453511fe5d Fix test 2022-02-06 11:02:48 +02:00
Saara Satokangas bcaa0086e5 Remove column "All" from studyprogramme-overview for now
It can be a bit misleading
2022-02-06 10:47:47 +02:00
45 changed files with 554 additions and 2530 deletions
+281 -21
View File
@@ -7,19 +7,25 @@ const deleteTag = name => {
cy.contains(name).should('not.exist')
}
const getEmptyYears = () => {
const getEmptyYears = isAcademicYear => {
const thisYear = new Date().getFullYear()
const years = []
for (let year = thisYear; year > 2021; year--) {
years.push(year)
if (isAcademicYear) {
years.push(`${year} - ${year + 1}`)
} else {
years.push(year)
}
}
return years
}
describe('Studyprogramme overview', () => {
describe('when opening programme page with basic user', () => {
/* Basic information overview -tests*/
describe('Basic information -view works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme/KH50_005')
cy.init('/study-programme')
cy.viewport(1536, 960)
})
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
@@ -28,35 +34,28 @@ describe('Studyprogramme overview', () => {
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
cy.get('[data-cy=Section-ProgrammesAfterGraduation]')
cy.get('[data-cy=Section-AverageGraduationTimes]')
})
// 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('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-ProgrammesBeforeOrAfter]')
cy.get('[data-cy=Section-AverageGraduationTimes]')
})
it('Basic information contains correct students', () => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
const years = getEmptyYears()
const tableContents = [
// [Year, All, Started, Graduated, Cancelled, Transferred Away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0, 0, 0]),
[2021, 0, 0, 0, 0, 1, 0],
[2020, 12, 10, 36, 16, 3, 2],
[2019, 87, 86, 17, 14, 0, 1],
[2018, 161, 162, 1, 3, 0, 0],
[2017, 171, 173, 0, 0, 0, 0],
// [Year, Started, Graduated, Cancelled, Transferred Away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0, 0]),
[2021, 0, 0, 0, 1, 0],
[2020, 12, 36, 16, 3, 2],
[2019, 87, 17, 14, 0, 1],
[2018, 161, 1, 3, 0, 0],
[2017, 171, 0, 0, 0, 0],
]
cy.checkTableStats(tableContents, 'StudentsOfTheStudyprogramme')
})
it('Basic information contains correct credits', () => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
const years = getEmptyYears()
const tableContents = [
// [Year, Total, Major credits, Non-major credits, Transferred credits]
@@ -72,6 +71,7 @@ describe('Studyprogramme overview', () => {
})
it('Basic information contains correct thesis writers and graduates', () => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
const years = getEmptyYears()
const tableContents = [
// [Year, Graduated, Wrote thesis]
@@ -86,6 +86,266 @@ describe('Studyprogramme overview', () => {
cy.checkTableStats(tableContents, 'GraduatedAndThesisWritersOfTheProgramme')
})
it.skip('Special studyrights can be excluded and basic data changes accordingly', () => {
cy.contains('Tietojenkäsittelytieteen maisteriohjelma').click()
cy.get('[data-cy=StudentToggle]').click()
const years = getEmptyYears()
const studentTableContents = [
// [Year, Started, Graduated]
...years.map(year => [year, 0, 0]),
[2021, 0, 0],
[2020, 22, 0],
[2019, 12, 0],
[2018, 0, 0],
[2017, 0, 0],
]
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
const creditTableContents = [
// [Year, Total, Major students credits, Transferred credits]
...years.map(year => [year, 0, 0, 0]),
[2021, 0, 0, 0],
[2020, 117.5, 0, 7.5],
[2019, 67, 0, 0],
[2018, 0, 0, 0],
[2017, 0, 0, 0],
]
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
const graduatedTableContents = [
// [Year, Graduated, Wrote thesis]
...years.map(year => [year, 0, 0]),
[2021, 0, 0],
[2020, 0, 0],
[2019, 0, 0],
[2018, 0, 0],
[2017, 0, 0],
]
cy.checkTableStats(graduatedTableContents, 'GraduatedAndThesisWritersOfTheProgramme')
cy.get('[data-cy=StudentToggle]').click()
})
it('Year can be changed to academic year, and data changes accordingly', () => {
cy.contains('Tietojenkäsittelytieteen maisteriohjelma').click()
cy.get('[data-cy=YearToggle]').click()
const isAcademicYear = true
const years = getEmptyYears(isAcademicYear)
const studentTableContents = [
// [Year, Started, Graduated, Cancelled, Transferred away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0, 0]),
['2021 - 2022', 0, 0, 0, 0, 0],
['2020 - 2021', 5, 0, 1, 0, 0],
['2019 - 2020', 27, 0, 1, 0, 0],
['2018 - 2019', 4, 0, 0, 0, 0],
['2017 - 2018', 0, 0, 0, 0, 0],
]
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
const creditTableContents = [
// [Year, Total, Major students credits, Non-major students 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, 0, 331, 7.5],
['2018 - 2019', 173, 0, 173, 0],
['2017 - 2018', 42, 0, 42, 0],
]
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
cy.get('[data-cy=YearToggle]').click()
})
it('Basic information graphs render', () => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('[data-cy=Graph-StudentsOfTheStudyprogramme')
.should('contain', 'Started')
.should('contain', 'Graduated')
.should('contain', 'Transferred away')
cy.get('[data-cy=Graph-CreditsProducedByTheStudyprogramme')
.should('contain', 'Major students credits')
.should('contain', 'Non-major students credits')
.should('contain', 'Transferred credits')
.should('contain', 788)
.should('contain', 1820)
cy.get('[data-cy=Graph-GraduatedAndThesisWritersOfTheProgramme')
.should('contain', 'Graduated students')
.should('contain', 'Wrote thesis')
.should('contain', 1)
.should('contain', 5)
.should('contain', 17)
.should('contain', 25)
.should('contain', 36)
.should('contain', 35)
cy.get('[data-cy=Graph-2019-AverageGraduationTimes]')
.should('contain', '2019')
.should('contain', 'n = 17')
.should('contain', '22 kk')
cy.get('[data-cy=Graph-ProgrammesBeforeOrAfter')
.should('contain', 'Tietojenkäsittelytieteen maisteriohjelma')
.should('contain', 'Datatieteen maisteriohjelma')
.should('contain', 'Matematiikan ja tilastotieteen maisteriohjelma')
.should('contain', 1)
.should('contain', 4)
.should('contain', 13)
})
})
/* Studytrack overview -tests*/
describe('Basic information -view works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
})
// 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('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]')
})
it('Students of the studyprogramme are shown correctly', () => {
cy.contains('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,
'100.0 %',
10,
'83.3 %',
0,
'0 %',
0,
'0 %',
0,
'0 %',
0,
'0 %',
10,
'83.3 %',
2,
'16.7 %',
12,
'100.0 %',
],
[
'2019 - 2020',
86,
'100.0 %',
86,
'100.0 %',
0,
'0 %',
0,
'0 %',
1,
'1.2 %',
1,
'1.2 %',
65,
'75.6 %',
21,
'24.4 %',
86,
'100.0 %',
],
[
'2018 - 2019',
162,
'100.0 %',
161,
'99.4 %',
0,
'0 %',
0,
'0 %',
11,
'6.8 %',
11,
'6.8 %',
118,
'72.8 %',
44,
'27.2 %',
161,
'99.4 %',
],
[
'2017 - 2018',
171,
'100.0 %',
171,
'100.0 %',
0,
'0 %',
0,
'0 %',
21,
'12.3 %',
42,
'24.6 %',
135,
'78.9 %',
36,
'21.1 %',
166,
'97.1 %',
],
]
cy.checkTableStats(tableContents, 'StudytrackOverview')
})
it('Student progress data is shown correctly', () => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
const tableContents = [
['2020 - 2021', 12, 5, 2, 2, 0, 2, 1],
['2019 - 2020', 86, 13, 15, 32, 11, 2, 13],
['2018 - 2019', 162, 7, 16, 35, 33, 27, 44],
['2017 - 2018', 171, 10, 22, 18, 22, 24, 75],
]
cy.checkTableStats(tableContents, 'StudytrackProgress')
})
it('Studytrack overview graphs render', () => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
cy.get('[data-cy=Graph-StudytrackProgress]')
.should('contain', 'Less than 30 credits')
.should('contain', '30-59 credits')
.should('contain', 'More than 150 credits')
.should('contain', '5.8%')
.should('contain', '4.3%')
.should('contain', '15.1%')
cy.get('[data-cy=Graph-2018-AverageGraduationTimes]')
.should('contain', '2018 - 2019')
.should('contain', 'n = 11 / 162')
.should('contain', '22 kk')
})
})
/* Tag-tests*/
describe('Basic information -view works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
})
it('can create and delete tags for population', () => {
const name = `tag-${new Date().getTime()}`
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
+1 -1
View File
@@ -106,7 +106,7 @@ Cypress.Commands.add('selectStudyProgramme', name => {
})
Cypress.Commands.add('checkTableStats', (correctStats, tableName) => {
cy.get(`[data-cy=${tableName}]>tbody`).within(() => {
cy.get(`[data-cy=Table-${tableName}]>tbody`).within(() => {
correctStats.forEach((values, trIndex) => {
cy.get('tr')
.eq(trIndex)
+3 -3
View File
@@ -1003,9 +1003,9 @@
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"follow-redirects": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz",
"integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw=="
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
},
"for-in": {
"version": "1.0.2",
@@ -1,23 +1,7 @@
const router = require('express').Router()
const MandatoryCourses = require('../services/mandatoryCourses')
const {
productivityStatsForStudytrack,
throughputStatsForStudytrack,
optionData,
} = require('../services/studyprogramme')
const { findProgrammeTheses, createThesisCourse, deleteThesisCourse } = require('../services/thesis')
const {
getProductivity,
setProductivity,
getThroughput,
setThroughput,
patchProductivity,
patchThroughput,
} = require('../services/analyticsService')
const { getAllProgrammes, getAllElementDetails } = require('../services/studyrights')
const logger = require('../util/logger')
const programmeStatsSince = new Date('2017-07-31')
router.get('/elementdetails/all', async (req, res) => {
const elementdetails = await getAllElementDetails()
@@ -29,22 +13,6 @@ router.get('/studyprogrammes', async (req, res) => {
res.json(studyrights)
})
router.get('/v2/studyprogrammes/:id/optiondata', async (req, res) => {
const code = req.params.id
let level
if (code.includes('MH')) {
level = 'MSC'
} else if (code.includes('KH')) {
level = 'BSC'
} else {
return res.json([])
}
const data = await optionData(new Date('2017-07-31'), new Date(), code, level)
res.json(data)
})
router.get('/v2/studyprogrammes/:id/mandatory_courses', async (req, res) => {
if (req.params.id) {
const codes = await MandatoryCourses.byStudyprogramme(req.params.id)
@@ -54,109 +22,6 @@ router.get('/v2/studyprogrammes/:id/mandatory_courses', async (req, res) => {
}
})
router.get('/v2/studyprogrammes/:id/productivity', async (req, res) => {
const code = req.params.id
if (code) {
let data = null
try {
data = await getProductivity(code)
} catch (e) {
logger.error(`Failed to get code ${code} productivity`)
}
if (!data) {
try {
const stats = await productivityStatsForStudytrack(code, programmeStatsSince)
data = await setProductivity(stats)
} catch (e) {
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
}
}
return res.json(data)
} else {
res.status(422).end()
}
})
router.get('/v2/studyprogrammes/productivity/recalculate', async (req, res) => {
const code = req.query.code
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
res.status(200).end()
try {
if (code.includes('MH') || code.includes('KH')) {
const data = await productivityStatsForStudytrack(code, new Date('2017-07-31'))
await setProductivity(data)
} else {
const data = await productivityStatsForStudytrack(code, new Date('2000-07-31'))
await setProductivity(data)
}
} catch (e) {
try {
await patchProductivity({
[code]: { status: 'RECALCULATION ERRORED' },
})
} catch (e) {
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
return
}
logger.error(`Failed to update productivity stats for code: ${code}, reason: ${e.message}`)
}
logger.info(`Produdtiviy stats recalculation for studyprogramme ${code} done`)
})
router.get('/v2/studyprogrammes/:id/throughput', async (req, res) => {
const code = req.params.id
if (code) {
const thesisPromise = findProgrammeTheses(req.params.id)
let data = null
try {
data = await getThroughput(code)
} catch (e) {
logger.error(`Failed to get code ${code} throughput`)
}
if (!data) {
try {
const result = await throughputStatsForStudytrack(req.params.id, programmeStatsSince.getFullYear())
data = await setThroughput(result)
} catch (e) {
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
}
}
return res.json({ ...data, thesis: await thesisPromise })
} else {
res.status(422).end()
}
})
router.get('/v2/studyprogrammes/throughput/recalculate', async (req, res) => {
const code = req.query.code
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
res.status(200).end()
try {
if (code.includes('MH') || code.includes('KH')) {
const data = await throughputStatsForStudytrack(code, 2017)
await setThroughput(data)
} else {
const data = await throughputStatsForStudytrack(code, 2000)
await setThroughput(data)
}
} catch (e) {
try {
await patchThroughput({ [code]: { status: 'RECALCULATION ERRORED' } })
} catch (e) {
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
return
}
logger.error(`Failed to update throughput stats for code: ${code}, reason: ${e.message}`)
}
logger.info(`Throughput stats recalculation for studyprogramme ${code} done`)
})
router.get('/v2/studyprogrammes/:id/thesis', async (req, res) => {
const { id } = req.params
if (id) {
@@ -1,8 +1,6 @@
const { redisClient } = require('../services/redis')
const moment = require('moment')
const createRedisKeyForProductivity = id => `PRODUCTIVITY_${id}`
const createRedisKeyForThroughput = id => `THROUGHPUT_${id}`
const createRedisKeyForBasicStats = (id, yearType, specialGroups) => `BASIC_STATS_${id}_${yearType}_${specialGroups}`
const createRedisKeyForCreditStats = (id, yearType, specialGroups) => `CREDIT_STATS_${id}_${yearType}_${specialGroups}`
const createRedisKeyForGraduationStats = (id, yearType, specialGroups) =>
@@ -10,89 +8,6 @@ const createRedisKeyForGraduationStats = (id, yearType, specialGroups) =>
const createRedisKeyForStudytrackStats = (id, graduated, specialGroups) =>
`STUDYTRACK_STATS_${id}_${graduated}_${specialGroups}`
const getProductivity = async id => {
const redisKey = createRedisKeyForProductivity(id)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return {
[id]: JSON.parse(dataFromRedis),
}
}
const setProductivity = async data => {
const { id } = data
const redisKey = createRedisKeyForProductivity(id)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return {
[id]: dataToRedis,
}
}
const patchProductivity = async data => {
const { id } = data
const redisKey = createRedisKeyForProductivity(id)
const dataFromRedis = JSON.parse(await redisClient.getAsync(redisKey))
const patchedData = {
...dataFromRedis,
...data,
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(patchedData))
if (setOperationStatus !== 'OK') return null
return {
[id]: patchedData,
}
}
const getThroughput = async id => {
const redisKey = createRedisKeyForThroughput(id)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return {
[id]: JSON.parse(dataFromRedis),
}
}
const setThroughput = async data => {
const { id, data: dataToSet } = data
const redisKey = createRedisKeyForThroughput(id)
const dataToRedis = {
data: dataToSet.years,
totals: dataToSet.totals,
stTotals: dataToSet.stTotals,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return {
[id]: dataToRedis,
}
}
const patchThroughput = async data => {
const [id, dataToPatch] = Object.entries(data)[0]
const redisKey = createRedisKeyForThroughput(id)
const { status } = dataToPatch
const dataFromRedis = JSON.parse(await redisClient.getAsync(redisKey))
const patchedData = {
...dataFromRedis,
status,
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(patchedData))
if (setOperationStatus !== 'OK') return null
return {
[id]: patchedData,
}
}
const getBasicStats = async (id, yearType, specialGroups) => {
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
@@ -174,12 +89,6 @@ const setStudytrackStats = async (data, graduated, specialGroups) => {
}
module.exports = {
getProductivity,
setProductivity,
patchProductivity,
getThroughput,
setThroughput,
patchThroughput,
getBasicStats,
setBasicStats,
getCreditStats,
@@ -203,13 +203,18 @@ const graduatedStudyRights = async (studytrack, since, studentnumbers) =>
})
).map(formatStudyright)
const followingStudyrights = async (since, studentnumbers) =>
const followingStudyrights = async (since, programmes, studentnumbers) =>
(
await Studyright.findAll({
include: [
{
model: StudyrightElement,
required: true,
where: {
code: {
[Op.in]: programmes.map(p => p.code),
},
},
include: [
{
model: ElementDetail,
@@ -234,6 +239,40 @@ const followingStudyrights = async (since, studentnumbers) =>
})
).map(formatStudyright)
const previousStudyrights = async (since, programmes, studentnumbers) =>
(
await Studyright.findAll({
include: [
{
model: StudyrightElement,
required: true,
where: {
code: {
[Op.in]: programmes.map(p => p.code),
},
},
include: [
{
model: ElementDetail,
attributes: ['name'],
},
],
attributes: ['code'],
},
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
where: {
extentcode: 1,
student_studentnumber: whereStudents(studentnumbers),
},
order: [[StudyrightElement, 'startdate', 'DESC']],
})
).map(formatStudyright)
const transfersAway = async (studytrack, since) =>
(
await Transfer.findAll({
@@ -391,6 +430,7 @@ module.exports = {
allStudyrights,
startedStudyrights,
graduatedStudyRights,
previousStudyrights,
followingStudyrights,
transfersAway,
transfersTo,
@@ -1,902 +0,0 @@
const sequelize = require('sequelize')
const { Op } = sequelize
const moment = require('moment')
const { flatMap } = require('lodash')
const {
Credit,
Student,
Course,
Organization,
Studyright,
StudyrightElement,
ElementDetail,
Transfer,
} = require('../models')
const { getAssociations, getAllProgrammes } = require('./studyrights')
const { ThesisCourse, ThesisTypeEnums } = require('../models/models_kone')
const { studentnumbersWithAllStudyrightElements } = require('./populations')
const { semesterStart, semesterEnd } = require('../util/semester')
const { mapToProviders } = require('../util/utils')
const formatCredit = credit => {
const { id, credits, attainment_date } = credit
const year = attainment_date && attainment_date.getFullYear()
return { id, year, credits }
}
const getTransferredCredits = async (provider, since) =>
await Credit.findAll({
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'credittypecode'],
include: {
model: Course,
attributes: ['code'],
required: true,
where: {
is_study_module: false,
},
include: {
model: Organization,
attributes: [],
required: true,
where: {
code: provider,
},
},
},
where: {
credittypecode: {
[Op.eq]: [9],
},
attainment_date: {
[Op.gte]: since,
},
},
})
const getCreditsForMajors = async (provider, since, studentnumbers) =>
await Credit.findAll({
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'student_studentnumber'],
include: {
model: Course,
attributes: ['code'],
required: true,
where: {
is_study_module: false,
},
include: {
model: Organization,
attributes: [],
required: true,
where: {
code: provider,
},
},
},
where: {
credittypecode: {
[Op.notIn]: [10, 9, 7],
},
isStudyModule: {
[Op.not]: true,
},
attainment_date: {
[Op.gte]: since,
},
student_studentnumber: {
[Op.in]: studentnumbers,
},
},
})
const getCreditsForProvider = async (provider, since) =>
(
await Credit.findAll({
attributes: ['id', 'course_code', 'credits', 'attainment_date'],
include: {
model: Course,
attributes: ['code'],
required: true,
where: {
is_study_module: false,
},
include: {
model: Organization,
attributes: [],
required: true,
where: {
code: provider,
},
},
},
where: {
credittypecode: {
[Op.notIn]: [10, 9, 7],
},
isStudyModule: {
[Op.not]: true,
},
attainment_date: {
[Op.gte]: since,
},
},
})
).map(formatCredit)
const productivityStatsFromCredits = credits => {
const stats = {}
credits.forEach(({ year, credits: creds }) => {
const stat = stats[year] || (stats[year] = { credits: 0, year })
stat.credits += creds
})
return stats
}
const productivityStatsForProvider = async (providercode, since) => {
const credits = await getCreditsForProvider(providercode, since)
return productivityStatsFromCredits(credits)
}
const formatGraduatedStudyright = ({ studyrightid, enddate, studystartdate }) => {
const year = enddate && enddate.getFullYear()
const timeToGraduation = moment(enddate).diff(moment(studystartdate), 'months')
return { studyrightid, year, timeToGraduation }
}
const findGraduated = async (studytrack, since) =>
(
await Studyright.findAll({
include: {
model: StudyrightElement,
attributes: [],
required: true,
include: {
model: ElementDetail,
attributes: [],
required: true,
where: {
code: studytrack,
},
},
},
where: {
graduated: 1,
enddate: {
[Op.gte]: since,
},
},
})
).map(formatGraduatedStudyright)
const graduatedStatsFromStudyrights = studyrights => {
const stats = {}
let graduationTimes = []
studyrights.forEach(({ year, timeToGraduation }) => {
const graduated = stats[year] ? stats[year].graduated : 0
stats[year] = {
graduated: graduated + 1,
timesToGraduation: stats[year]
? [...stats[year].timesToGraduation, timeToGraduation || 0]
: [timeToGraduation || 0],
}
graduationTimes = [...graduationTimes, timeToGraduation || 0]
})
return stats
}
const graduatedStatsForStudytrack = async (studytrack, since) => {
const studyrights = await findGraduated(studytrack, since)
return graduatedStatsFromStudyrights(studyrights)
}
const findProgrammeThesisCredits = async code => {
const thesiscourses = await ThesisCourse.findAll({
model: ThesisCourse,
where: {
programmeCode: code,
},
})
const courseCodeToThesisCourse = thesiscourses.reduce((acc, tc) => {
acc[tc.courseCode] = tc
return acc
}, {})
const credits = await Credit.findAll({
include: {
model: Course,
required: true,
where: {
code: {
[Op.in]: thesiscourses.map(tc => tc.courseCode),
},
},
},
where: {
credittypecode: {
[Op.eq]: 4,
},
},
})
return credits.map(credit => {
const { id, course, attainment_date } = credit
const { code } = course
const year = attainment_date && attainment_date.getFullYear()
return { id, code, type: courseCodeToThesisCourse[code].thesisType, year }
})
}
const thesisProductivityFromCredits = credits => {
const stats = {}
credits.forEach(({ type, year }) => {
const yearstat = stats[year] || (stats[year] = { mThesis: 0, bThesis: 0 })
if (type === ThesisTypeEnums.MASTER) {
yearstat.mThesis++
} else if (type === ThesisTypeEnums.BACHELOR) {
yearstat.bThesis++
} else {
return
}
})
return stats
}
const thesisProductivityForStudytrack = async code => {
const credits = await findProgrammeThesisCredits(code)
return thesisProductivityFromCredits(credits)
}
const combineStatistics = (creditStats, studyrightStats, thesisStats, creditsForMajors, transferredCredits) => {
const allYears = [
...new Set(flatMap([creditStats, studyrightStats, thesisStats, creditsForMajors, transferredCredits], Object.keys)),
]
const stats = {}
allYears.forEach(year => {
const thesis = thesisStats[year] || {}
stats[year] = {}
stats[year] = creditStats[year] || { credits: 0, year }
stats[year].graduated = studyrightStats[year] ? studyrightStats[year].graduated : 0
// stats[year].medianGraduationTime = studyrightStats[year] ? studyrightStats[year].medianGraduationTime : 0
stats[year].bThesis = thesis.bThesis || 0
stats[year].mThesis = thesis.mThesis || 0
stats[year].creditsForMajors = creditsForMajors[year] || 0
stats[year].transferredCredits = transferredCredits[year] || 0
})
return Object.values(stats)
}
const productivityStatsForStudytrack = async (studytrack, since) => {
const providercode = mapToProviders([studytrack])[0]
const year = 1950
const startDate = `${year}-${semesterStart['FALL']}`
const endDate = `${moment(new Date(), 'YYYY').add(1, 'years').format('YYYY')}-${semesterEnd['SPRING']}`
// This includes ALL students: exchange students, the ones that have transferred to program, the ones
// with non-degree studyright and the ones that have cancelled their studyright
const studentnumbers = await studentnumbersWithAllStudyrightElements(
[studytrack],
startDate,
endDate,
true, // exchange students
true, // cancelled students
true, // non-degree students
true // transferred to students
)
const promises = [
graduatedStatsForStudytrack(studytrack, since),
productivityStatsForProvider(providercode, since),
thesisProductivityForStudytrack(studytrack),
productivityCreditsFromStudyprogrammeStudents(studytrack, since, studentnumbers),
transferredCreditsForProductivity(studytrack, since),
]
const [studyrightStats, creditStats, thesisStats, creditsForMajors, transferredCredits] = await Promise.all(promises)
return {
id: studytrack,
status: null,
data: combineStatistics(creditStats, studyrightStats, thesisStats, creditsForMajors, transferredCredits),
}
}
const creditsAfter = (studentnumbers, startDate) => {
return Promise.all(
studentnumbers.map(student =>
Credit.sum('credits', {
where: {
student_studentnumber: {
[Op.eq]: student,
},
attainment_date: {
[Op.gte]: startDate,
},
isStudyModule: {
[Op.not]: true,
},
credittypecode: {
[Op.notIn]: [10, 9, 7],
},
},
})
)
)
}
const thesesFromClass = async (studentnumbers, startDate, code) => {
const thesiscourses = await ThesisCourse.findAll({
where: {
programmeCode: code,
},
})
const credits = await Credit.findAll({
include: {
model: Course,
required: true,
where: {
code: {
[Op.in]: thesiscourses.map(tc => tc.courseCode),
},
},
},
where: {
credittypecode: {
[Op.ne]: 10,
},
attainment_date: {
[Op.gte]: startDate,
},
student_studentnumber: {
[Op.in]: studentnumbers,
},
},
})
const courseCodeToThesisCourse = thesiscourses.reduce((acc, tc) => {
acc[tc.courseCode] = tc
return acc
}, {})
const courseCodes = credits.map(c => c.course.code)
const theses = courseCodes.reduce((acc, code) => {
const thesisType = courseCodeToThesisCourse[code].thesisType
acc[thesisType] = acc[thesisType] ? acc[thesisType] + 1 : 1
return acc
}, {})
return theses
}
const graduationsFromClass = async (studentnumbers, studytrack) => {
return await Studyright.findAll({
include: {
model: StudyrightElement,
attributes: [],
required: true,
where: {
code: {
[Op.eq]: studytrack,
},
},
},
where: {
student_studentnumber: {
[Op.in]: studentnumbers,
},
graduated: {
[Op.eq]: 1,
},
},
})
}
const genderCodeToValue = code => {
if (code === '1') return 'Male'
if (code === '2') return 'Female'
if (code === '3') return 'Other'
if (code === '0') return 'Unknown'
return 'Unknown'
}
const gendersFromClass = async studentnumbers => {
return (
await Student.findAll({
attributes: [[sequelize.fn('count', sequelize.col('gender_code')), 'count'], 'gender_code'],
where: {
studentnumber: {
[Op.in]: studentnumbers,
},
},
group: ['gender_code'],
raw: true,
})
).reduce((acc, curr) => {
acc[genderCodeToValue(curr.gender_code)] = curr.count
return acc
}, {})
}
const nationalitiesFromClass = async studentnumbers => {
return (
await Student.findAll({
where: {
studentnumber: {
[Op.in]: studentnumbers,
},
},
attributes: ['home_country_en'],
})
).reduce((acc, { home_country_en }) => {
const country = home_country_en || 'Unknown'
if (!acc[country]) acc[country] = 0
acc[country] += 1
return acc
}, {})
}
const tranferredToStudyprogram = async (studentnumbers, startDate, studytrack, endDate) => {
return await Transfer.count({
where: {
studentnumber: {
[Op.in]: studentnumbers,
},
transferdate: {
[Op.between]: [startDate, endDate],
},
targetcode: studytrack,
},
distinct: true,
col: 'studentnumber',
})
}
const formatCreditsForProductivity = credits => {
return credits.map(formatCredit).reduce(function (acc, curr) {
var key = curr['year']
if (!acc[key]) {
acc[key] = []
}
acc[key] = Number(acc[key]) + Number(curr.credits)
return acc
}, {})
}
const productivityCreditsFromStudyprogrammeStudents = async (studytrack, startDate, studentnumbers) => {
const providercode = mapToProviders([studytrack])[0]
const credits = await getCreditsForMajors(providercode, startDate, studentnumbers)
const formattedStudentCredits = formatCreditsForProductivity(credits)
return formattedStudentCredits
}
const transferredCreditsForProductivity = async (studytrack, since) => {
const providercode = mapToProviders([studytrack])[0]
const credits = await getTransferredCredits(providercode, since)
const formattedCredits = formatCreditsForProductivity(credits)
return formattedCredits
}
const cancelledStudyright = async (studentnumbers, startDate, studytrack, endDate) => {
return await Studyright.count({
include: {
model: StudyrightElement,
attributes: [],
required: true,
where: {
code: {
[Op.eq]: studytrack,
},
},
},
where: {
student_studentnumber: {
[Op.in]: studentnumbers,
},
startdate: {
[Op.between]: [startDate, endDate],
},
canceldate: {
[Op.not]: null,
},
},
})
}
const startedStudyright = async (studentnumbers, startDate, studytrack, endDate) => {
return await Studyright.count({
include: {
model: StudyrightElement,
attributes: [],
required: true,
where: {
code: {
[Op.eq]: studytrack,
},
},
},
where: {
student_studentnumber: {
[Op.in]: studentnumbers,
},
studystartdate: {
[Op.between]: [startDate, endDate],
},
},
})
}
const optionData = async (startDate, endDate, code, level) => {
const programmes = await getAllProgrammes()
// This includes ALL students: exchange students, the ones that have transferred to program, the ones
// with non-degree studyright and the ones that have cancelled their studyright
const students = await studentnumbersWithAllStudyrightElements(
[code],
startDate,
endDate,
true, // exchange students
true, // cancelled students
true, // non-degree students
true // transferred to students
)
let graduated
let currentExtent
let optionExtent
if (level === 'BSC') {
graduated = { graduated: 1 }
currentExtent = 1
optionExtent = 2
} else if (level === 'MSC') {
graduated = {}
currentExtent = 2
optionExtent = 1
} else {
throw new Error('Invalid study level ' + level)
}
const currentStudyrights = await Studyright.findAll({
include: [
{
model: StudyrightElement,
where: {
code: code,
},
attributes: ['startdate'],
},
],
where: {
...graduated,
extentcode: currentExtent,
student_studentnumber: {
[Op.in]: students,
},
},
attributes: ['studentStudentnumber', 'givendate', 'studystartdate'],
})
const currentStudyrightsMap = currentStudyrights
.filter(b => b.studystartdate)
.reduce((obj, studyright) => {
const acualDate = new Date(Math.max(+studyright.studystartdate, +studyright.studyright_elements[0].startdate))
obj[studyright.studentStudentnumber] = { givendate: studyright.givendate, startdate: acualDate }
return obj
}, {})
const options = await Studyright.findAll({
include: [
{
model: StudyrightElement,
where: {
studentnumber: {
[Op.in]: students,
},
code: {
[Op.in]: programmes.map(p => p.code),
},
},
include: [
{
model: ElementDetail,
attributes: ['name'],
},
],
attributes: ['code'],
},
],
where: {
extentcode: optionExtent,
student_studentnumber: {
[Op.in]: students,
},
},
order: [[StudyrightElement, 'startdate', 'DESC']],
attributes: ['studentStudentnumber', 'startdate', 'givendate'],
})
const data = {}
const years = new Set()
options
.filter(m => m.studentStudentnumber in currentStudyrightsMap)
.filter(m => m.givendate.getTime() === currentStudyrightsMap[m.studentStudentnumber].givendate.getTime())
.forEach(b => {
const date = currentStudyrightsMap[b.studentStudentnumber].startdate
const year =
date.getMonth() > 6 || (date.getMonth() === 6 && date.getDate() == 31)
? date.getFullYear()
: date.getFullYear() - 1
years.add(year)
const code = b.studyright_elements[0].code
if (!data[code]) {
data[code] = {}
data[code].name = b.studyright_elements[0].element_detail.name
data[code].total = 0
}
if (!data[code][year]) {
data[code][year] = 0
}
data[code][year] += 1
data[code].total += 1
})
const dataAsArray = Object.keys(data).map(code => {
const { name, total } = data[code]
const output = { code, name, total }
years.forEach(y => (output[y] = data[code][y] || 0))
return output
})
return { data: dataAsArray, years: Array.from(years).sort() }
}
const statsForClass = async (studentnumbers, startDate, studyprogramme, endDate) => {
return Promise.all([
creditsAfter(studentnumbers, startDate),
graduationsFromClass(studentnumbers, studyprogramme),
thesesFromClass(studentnumbers, startDate, studyprogramme),
gendersFromClass(studentnumbers),
tranferredToStudyprogram(studentnumbers, startDate, studyprogramme, endDate),
nationalitiesFromClass(studentnumbers),
cancelledStudyright(studentnumbers, startDate, studyprogramme, endDate),
startedStudyright(studentnumbers, startDate, studyprogramme, endDate),
])
}
const getYears = since => {
const years = []
for (let i = since; i <= new Date().getFullYear(); i++) {
years.push(i)
}
return years
}
const throughputStatsForStudytrack = async (studyprogramme, since) => {
const associations = await getAssociations()
const studyprogrammeYears = associations.programmes[studyprogramme]
? associations.programmes[studyprogramme].enrollmentStartYears
: {}
const totals = {
credits: {
mte30: 0,
mte60: 0,
mte90: 0,
mte120: 0,
mte150: 0,
},
genders: {},
thesisM: 0,
thesisB: 0,
students: 0,
graduated: 0,
inTargetTime: 0,
transferred: 0,
nationalities: {},
cancelled: 0,
started: 0,
}
const stTotals = {}
const years = getYears(since)
// studyprogramme starts with K if bachelors and M if masters
const graduationTimeLimit = studyprogramme[0] === 'K' ? 36 : 24
const median = values => {
if (values.length === 0) return 0
values.sort((a, b) => a - b)
var half = Math.floor(values.length / 2)
if (values.length % 2) return values[half]
return (values[half - 1] + values[half]) / 2.0
}
let allGraduationTimes = []
const arr = await Promise.all(
years.map(async year => {
const startDate = `${year}-${semesterStart['FALL']}`
const endDate = `${moment(year, 'YYYY').add(1, 'years').format('YYYY')}-${semesterEnd['SPRING']}`
const studytracks = studyprogrammeYears[year] ? Object.keys(studyprogrammeYears[year].studyTracks) : []
const studytrackdata = await studytracks.reduce(async (acc, curr) => {
const previousData = await acc
const studentnumbers = await studentnumbersWithAllStudyrightElements(
[studyprogramme, curr],
startDate,
endDate,
true, // exchange students
true, // cancelled students
true, // non-degree students
true // transferred to students
)
const creditsForStudyprogramme = await productivityCreditsFromStudyprogrammeStudents(
studyprogramme,
startDate,
studentnumbers
)
const [credits, graduated, theses, genders, transferredTo, nationalities, cancelled, started] =
await statsForClass(studentnumbers, startDate, studyprogramme, endDate)
delete genders[null]
const creditValues = credits.reduce(
(acc, curr) => {
acc.mte30 = curr >= 30 ? acc.mte30 + 1 : acc.mte30
acc.mte60 = curr >= 60 ? acc.mte60 + 1 : acc.mte60
acc.mte90 = curr >= 90 ? acc.mte90 + 1 : acc.mte90
acc.mte120 = curr >= 120 ? acc.mte120 + 1 : acc.mte120
acc.mte150 = curr >= 150 ? acc.mte150 + 1 : acc.mte150
return acc
},
{ mte30: 0, mte60: 0, mte90: 0, mte120: 0, mte150: 0 }
)
if (!stTotals[curr]) {
stTotals[curr] = {
credits: {
mte30: 0,
mte60: 0,
mte90: 0,
mte120: 0,
mte150: 0,
},
genders: {},
thesisM: 0,
thesisB: 0,
students: 0,
graduated: 0,
inTargetTime: 0,
transferred: 0,
nationalities: {},
cancelled: 0,
started: 0,
}
}
Object.keys(stTotals[curr].credits).forEach(key => {
stTotals[curr].credits[key] += creditValues[key]
})
Object.keys(genders).forEach(genderKey => {
stTotals[curr].genders[genderKey] = stTotals[curr].genders[genderKey]
? stTotals[curr].genders[genderKey] + Number(genders[genderKey])
: Number(genders[genderKey])
})
Object.keys(nationalities).forEach(nationality => {
if (!stTotals[curr].nationalities[nationality]) stTotals[curr].nationalities[nationality] = 0
stTotals[curr].nationalities[nationality] += nationalities[nationality]
})
const graduationTimes = graduated.map(g => moment(g.enddate).diff(g.studystartdate, 'months'))
const inTargetTime = graduationTimes.filter(time => time <= graduationTimeLimit).length
allGraduationTimes = [...allGraduationTimes, ...graduationTimes]
stTotals[curr].thesisM = theses.MASTER ? stTotals[curr].thesisM + theses.MASTER : stTotals[curr].thesisM
stTotals[curr].thesisB = theses.BACHELOR ? stTotals[curr].thesisB + theses.BACHELOR : stTotals[curr].thesisB
stTotals[curr].students = stTotals[curr].students + credits.length
stTotals[curr].graduated = stTotals[curr].graduated + graduated.length
stTotals[curr].medianGraduationTime = median(allGraduationTimes)
stTotals[curr].inTargetTime = stTotals[curr].inTargetTime + inTargetTime
stTotals[curr].transferred = stTotals[curr].transferred + transferredTo
stTotals[curr].cancelled += cancelled
stTotals[curr].started += started
return {
...previousData,
[curr]: {
year: `${year}-${year + 1}`,
credits: credits.map(cr => (cr === null ? 0 : cr)),
creditsForStudyprogramme: creditsForStudyprogramme,
graduated: graduated.length,
medianGraduationTime: median(graduationTimes),
inTargetTime,
thesisM: theses.MASTER || 0,
thesisB: theses.BACHELOR || 0,
genders,
creditValues,
transferred: transferredTo,
nationalities,
cancelled,
started,
},
}
}, {})
const studentnumbers = await studentnumbersWithAllStudyrightElements(
[studyprogramme],
startDate,
endDate,
true, // exchange students
true, // cancelled students
true, // non-degree students
true // transferred to students
)
const creditsForStudyprogramme = await productivityCreditsFromStudyprogrammeStudents(
studyprogramme,
startDate,
studentnumbers
)
const [credits, graduated, theses, genders, transferredTo, nationalities, cancelled, started] =
await statsForClass(studentnumbers, startDate, studyprogramme, endDate)
// theres so much shit in the data that transefferFrom doesnt rly mean anything
delete genders[null]
const creditValues = credits.reduce(
(acc, curr) => {
acc.mte30 = curr >= 30 ? acc.mte30 + 1 : acc.mte30
acc.mte60 = curr >= 60 ? acc.mte60 + 1 : acc.mte60
acc.mte90 = curr >= 90 ? acc.mte90 + 1 : acc.mte90
acc.mte120 = curr >= 120 ? acc.mte120 + 1 : acc.mte120
acc.mte150 = curr >= 150 ? acc.mte150 + 1 : acc.mte150
return acc
},
{ mte30: 0, mte60: 0, mte90: 0, mte120: 0, mte150: 0 }
)
Object.keys(totals.credits).forEach(key => {
totals.credits[key] += creditValues[key]
})
Object.keys(genders).forEach(genderKey => {
totals.genders[genderKey] = totals.genders[genderKey]
? totals.genders[genderKey] + Number(genders[genderKey])
: Number(genders[genderKey])
})
Object.keys(nationalities).forEach(nationality => {
if (!totals.nationalities[nationality]) totals.nationalities[nationality] = 0
totals.nationalities[nationality] += nationalities[nationality]
})
const graduationTimes = graduated.map(g => moment(g.enddate).diff(g.studystartdate, 'months'))
const inTargetTime = graduationTimes.filter(time => time <= graduationTimeLimit).length
allGraduationTimes = [...allGraduationTimes, ...graduationTimes]
totals.thesisM = theses.MASTER ? totals.thesisM + theses.MASTER : totals.thesisM
totals.thesisB = theses.BACHELOR ? totals.thesisB + theses.BACHELOR : totals.thesisB
totals.students = totals.students + credits.length
totals.graduated = totals.graduated + graduated.length
totals.medianGraduationTime = median(allGraduationTimes)
totals.inTargetTime = totals.inTargetTime + inTargetTime
totals.transferred = totals.transferred + transferredTo
totals.cancelled += cancelled
totals.started += started
return {
year: `${year}-${year + 1}`,
credits: credits.map(cr => (cr === null ? 0 : cr)),
creditsForStudyprogramme: creditsForStudyprogramme,
graduated: graduated.length,
medianGraduationTime: median(graduationTimes),
inTargetTime,
thesisM: theses.MASTER || 0,
thesisB: theses.BACHELOR || 0,
genders,
creditValues,
transferred: transferredTo,
nationalities,
cancelled,
started,
studytrackdata,
}
})
)
return { id: studyprogramme, status: null, data: { years: arr, totals, stTotals } }
}
module.exports = {
getCreditsForProvider,
productivityStatsFromCredits,
productivityStatsForProvider,
findGraduated,
graduatedStatsFromStudyrights,
combineStatistics,
productivityStatsForStudytrack,
throughputStatsForStudytrack,
findProgrammeThesisCredits,
thesisProductivityFromCredits,
thesisProductivityForStudytrack,
optionData,
}
@@ -16,27 +16,6 @@ const {
} = require('./newStudyprogramme')
const { getYearStartAndEndDates } = require('../util/semester')
const getAllStats = async ({ studyprogramme, years, isAcademicYear, includeAllSpecials }) => {
const { graphStats, tableStats } = getStatsBasis(years)
await Promise.all(
years.map(async year => {
const { startDate, endDate } = getYearStartAndEndDates(year, isAcademicYear)
const studentnumbersOfTheYear = await getCorrectStudentnumbers({
codes: [studyprogramme],
startDate,
endDate,
includeAllSpecials,
})
const all = await allStudyrights(studyprogramme, null, studentnumbersOfTheYear)
graphStats[indexOf(years, year)] = all.length
tableStats[year] += all.length
})
)
return { graphStats, tableStats }
}
const getStartedStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const { graphStats, tableStats } = getStatsBasis(years)
@@ -133,7 +112,6 @@ const getBasicStatsForStudytrack = async ({ studyprogramme, yearType, specialGro
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials }
const all = await getAllStats(queryParameters)
const started = await getStartedStats(queryParameters)
const graduated = await getGraduatedStats(queryParameters)
const cancelled = await getCancelledStats(queryParameters)
@@ -146,14 +124,13 @@ const getBasicStatsForStudytrack = async ({ studyprogramme, yearType, specialGro
includeAllSpecials
? [
year,
all.tableStats[year],
started.tableStats[year],
graduated.tableStats[year],
cancelled.tableStats[year],
transferredAway.tableStats[year],
transferredTo.tableStats[year],
]
: [year, all.tableStats[year], started.tableStats[year], graduated.tableStats[year]]
: [year, started.tableStats[year], graduated.tableStats[year]]
)
return {
@@ -69,6 +69,8 @@ const getTransferredCreditStats = async ({ studyprogramme, since, years, isAcade
return { graphStats, tableStats }
}
// Fetches all credits for the studytrack and combines them into the statistics for the table
// and graph in the studyprogramme overview
const getCreditStatsForStudytrack = async ({ studyprogramme, yearType, specialGroups }) => {
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
@@ -16,12 +16,14 @@ const {
} = require('./studyprogrammeHelpers')
const {
graduatedStudyRights,
transfersAway,
transfersTo,
getThesisCredits,
followingStudyrights,
previousStudyrights,
allStudyrights,
allTransfers,
} = require('./newStudyprogramme')
const { getYearStartAndEndDates } = require('../util/semester')
const { getAllProgrammes } = require('./studyrights')
const getGraduatedStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const { graphStats, tableStats } = getStatsBasis(years)
@@ -123,38 +125,97 @@ const getGraduationTimeStats = async ({ studyprogramme, since, years, isAcademic
return { medians, means, graduationAmounts, totalAmounts }
}
const getProgrammesAfterGraduation = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const graduated = await graduatedStudyRights(studyprogramme, since)
const graduatedStudents = graduated.map(s => s.studentnumber)
let studyrightsAfterThisProgramme = await followingStudyrights(since, graduatedStudents)
const studyprogrammeCodes = uniqBy(studyrightsAfterThisProgramme, 'code').map(s => ({ code: s.code, name: s.name }))
// Creates (studyrightid, transfer)-map out of programmes transfers
const getTransferStudyrightMap = async (studyprogramme, since) => {
const transfers = await allTransfers(studyprogramme, since)
const studyrightTransferMap = new Map()
transfers.forEach(t => studyrightTransferMap.set(t.studyrightid, t))
return studyrightTransferMap
}
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])])
const graphStats = Object.values(stats)
.filter(p => years.map(year => p[year]).find(started => started !== 0)) // Filter out programmes with no-one started between the selected years
.map(p => ({ name: p.name['fi'], code: p.code, data: years.map(year => p[year]) }))
return { tableStats, graphStats }
}
const getProgrammesBeforeStarting = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const all = await allStudyrights(studyprogramme)
const allStudents = all.map(s => s.studentnumber)
// Get studyrights for bachelor studyprogrammes. This excludes studytracks.
const programmes = await getAllProgrammes()
let studyrightsBeforeMasters = await previousStudyrights(since, programmes, allStudents)
const studyprogrammeCodes = uniqBy(studyrightsBeforeMasters, 'code').map(s => ({ code: s.code, name: s.name }))
const stats = {}
studyprogrammeCodes.forEach(c => (stats[c.code] = { ...c, ...getYearsObject(years) }))
if (!includeAllSpecials) {
const transferredAway = await transfersAway(studyprogramme, since)
const transferredTo = await transfersTo(studyprogramme, since)
const transfers = [...transferredAway, ...transferredTo].map(s => s.studyrightid)
studyrightsAfterThisProgramme.filter(s => !transfers.includes(s.studyrightid) && !s.canceldate)
}
// Map transfers to the Master's programme
const transfers = await getTransferStudyrightMap(studyprogramme, since)
studyrightsAfterThisProgramme.forEach(({ studystartdate, code }) => {
const startYear = defineYear(studystartdate, isAcademicYear)
if (years.includes(startYear) && code) {
studyrightsBeforeMasters.forEach(({ studentnumber, code, givendate }) => {
const masterStudyright = all.find(s => s.studentnumber === studentnumber)
const transfer = transfers.get(masterStudyright.studyrightid)
// If special studyrights are excluded, transfers to the master's programme should be excluded
if (!includeAllSpecials && transfers.get(masterStudyright.studyrightid)) {
return
}
// If all studyrights are included, and transfer exists, define the start year in the programme, based on when the student transferred to it
// Otherwise, the year is defined based on when the student started in the chosen master's programme
const programmeStartDate = includeAllSpecials && transfer ? transfer.transferdate : masterStudyright?.studystartdate
const startYear = defineYear(programmeStartDate, isAcademicYear)
// The master studyright should have been given on the same date as the bachelor's
if (years.includes(startYear) && code && masterStudyright.givendate.getTime() === givendate.getTime()) {
stats[code][startYear] += 1
}
})
const tableStats = Object.values(stats)
.map(p => [p.code, p.name['fi'], ...years.map(year => p[year])])
.filter(p => p[0].includes('MH'))
const graphAndTableStats = formatStats(stats, years)
return graphAndTableStats
}
const graphStats = Object.values(stats)
.map(p => ({ name: p.name['fi'], code: p.code, data: years.map(year => p[year]) }))
.filter(p => p.code.includes('MH'))
const getProgrammesAfterGraduation = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const graduated = await graduatedStudyRights(studyprogramme, since)
const graduatedStudents = graduated.map(s => s.studentnumber)
return { tableStats, graphStats }
// Get studyrights for masters studyprogramme. This excludes studytracks.
const programmes = await getAllProgrammes()
let studyrightsAfterThisProgramme = await followingStudyrights(since, programmes, graduatedStudents)
const studyprogrammeCodes = uniqBy(studyrightsAfterThisProgramme, 'code').map(s => ({ code: s.code, name: s.name }))
const stats = {}
studyprogrammeCodes.forEach(c => (stats[c.code] = { ...c, ...getYearsObject(years) }))
const transfers = await getTransferStudyrightMap(studyprogramme, since)
studyrightsAfterThisProgramme.forEach(({ studentnumber, studystartdate, code, givendate }) => {
const bachelorStudyright = graduated.find(s => s.studentnumber === studentnumber)
// If special studyrights are excluded, transfers to the bachelor programme should be excluded
if (!includeAllSpecials && transfers.get(bachelorStudyright.studyrightid)) {
return
}
// If all studyrights are included, and transfer exists, define the start year in the programme, based on when the student transferred to it
// Otherwise, the year is defined based on when the student started in the chosen master's programme
const startYear = defineYear(studystartdate, isAcademicYear)
// The master studyright should have been given on the same date as the bachelor's
if (years.includes(startYear) && code && bachelorStudyright.givendate.getTime() === givendate.getTime()) {
stats[code][startYear] += 1
}
})
const graphAndTableStats = formatStats(stats, years)
return graphAndTableStats
}
const getGraduationStatsForStudytrack = async ({ studyprogramme, yearType, specialGroups }) => {
@@ -167,12 +228,14 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, yearType, speci
const thesis = await getThesisStats(queryParameters)
const graduated = await getGraduatedStats(queryParameters)
const graduationTimeStats = await getGraduationTimeStats(queryParameters)
const programmesAfterGraduation = await getProgrammesAfterGraduation(queryParameters)
const programmesBeforeOrAfter = studyprogramme.includes('KH')
? await getProgrammesAfterGraduation(queryParameters)
: await getProgrammesBeforeStarting(queryParameters)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const titles = ['', 'Graduated', 'Wrote thesis']
const programmesAfterTitles = ['Code', 'Programme', ...years]
const programmesBeforeOrAfterTitles = ['Code', 'Programme', ...years]
const tableStats = reversedYears.map(year => [year, graduated.tableStats[year], thesis.tableStats[year]])
return {
@@ -194,9 +257,9 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, yearType, speci
graduationMeanTime: graduationTimeStats.means,
graduationAmounts: graduationTimeStats.graduationAmounts,
totalAmounts: graduationTimeStats.totalAmounts,
programmesAfterTableStats: programmesAfterGraduation.tableStats,
programmesAfterGraphStats: programmesAfterGraduation.graphStats,
programmesAfterTitles,
programmesBeforeOrAfterTableStats: programmesBeforeOrAfter.tableStats,
programmesBeforeOrAfterGraphStats: programmesBeforeOrAfter.graphStats,
programmesBeforeOrAfterTitles,
}
}
@@ -28,11 +28,19 @@ const getCorrectStudentnumbers = async ({ codes, startDate, endDate, includeAllS
return studentnumbers
}
const defineStartDate = (studyright_elements, studystartdate) => {
if (studyright_elements?.length && studyright_elements[0].startdate > studystartdate) {
return studyright_elements[0].startdate
}
return studystartdate
}
const formatStudyright = studyright => {
const {
studyrightid,
studystartdate,
enddate,
givendate,
canceldate,
graduated,
prioritycode,
@@ -43,8 +51,9 @@ const formatStudyright = studyright => {
return {
studyrightid,
studystartdate,
studystartdate: defineStartDate(studyright_elements, studystartdate),
enddate,
givendate,
canceldate,
graduated,
prioritycode,
@@ -69,10 +78,10 @@ const formatStudent = student => {
}
const formatTransfer = transfer => {
const { sourcecode, transfercode, transferdate, studyrightid } = transfer
const { sourcecode, targetcode, transferdate, studyrightid } = transfer
return {
sourcecode,
transfercode,
targetcode,
transferdate,
studyrightid,
}
@@ -258,8 +267,8 @@ const getCreditThresholds = studyprogramme => {
const tableTitles = {
basics: {
SPECIAL_EXCLUDED: ['', 'All', 'Started studying', 'Graduated'],
SPECIAL_INCLUDED: ['', 'All', 'Started studying', 'Graduated', 'Cancelled', 'Transferred away', 'Transferred to'],
SPECIAL_EXCLUDED: ['', 'Started studying', 'Graduated'],
SPECIAL_INCLUDED: ['', 'Started studying', 'Graduated', 'Cancelled', 'Transferred away', 'Transferred to'],
},
credits: {
SPECIAL_EXCLUDED: ['', 'Total', 'Major students credits', 'Transferred credits'],
@@ -93,7 +93,9 @@ const calculateStatusGraduated = async (unixMillis, showByYear) => {
// init faculty
if (!acc[curr.faculty_code]) {
const org = orgs.find(o => o.code === curr.faculty_code)
acc[curr.faculty_code] = { name: org.name, yearly: {}, drill: {} }
if (org) {
acc[curr.faculty_code] = { name: org.name, yearly: {}, drill: {} }
}
}
if (!acc[curr.faculty_code]['yearly'][data.year])
acc[curr.faculty_code]['yearly'][data.year] = { acc: 0, total: 0 }
+3 -3
View File
@@ -6837,9 +6837,9 @@
}
},
"follow-redirects": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
"integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg=="
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
},
"for-in": {
"version": "1.0.2",
@@ -1,5 +1,4 @@
import PopulationStatistics from './populationStatistics'
import PopulationOverview from './populationOverview'
import CoolDataScience from './coolDataScience'
import CourseStatistics from './courseStatistics'
import CoursePopulation from './coursePopulation'
@@ -7,7 +6,6 @@ import Studyprogramme from './studyprogramme'
export default {
PopulationStatistics,
PopulationOverview,
CoolDataScience,
CourseStatistics,
CoursePopulation,
@@ -1,24 +0,0 @@
export default {
PopulationProgress: `Tämä koontisivu sisältää koulutusohjelman kaikkien opiskelijoiden perustiedot lukuvuosittain. Luvut perustuvat opinto-oikeuksiin kyseisessä ohjelmassa.
Population progress:
Vuosittain ohjelmassa aloittaneet, ohjelman keskeyttäneet / peruneet sekä ohjelmaan siirtyneet opiskelijat.
Huomattavaa on, että sama opiskelija voi olla kahdessa luokassa, jos hän on esimerkiksi aloittanut opinnot jo, mutta keskeyttänyt ne lukuvuoden aikana.
Tarkemmat tiedot saat esille klikkaamalla nuolta kunkin lukuvuoden kohdalla.`,
YearlyProductivity: `
Yearly productivity:
Koulutusohjelman kunakin kalenterivuonna tuottamat opintopisteet, valmistuneiden määrän, ohjelman pääaineopiskelijoiden
opintopisteet sekä muiden kuin pääaineopiskelijoiden opintopisteet. Mukana on myös kuhunkin koulutusohjelmaan hyväksiluetut opintopisteet.
Vanhojen koulutusohjelmien lakkauttaminen ja opiskelijoiden siirtäminen uusiin ohjelmiin voi aiheuttaa tavallista suuremmat opintopistemäärät lukuvuodelle 2020-2021.`,
Year: `Lukuvuosi`,
Students: `Opiskelijat, jotka ovat koulutusohjelmassa aloittaneet, opinto-oikeutensa peruneet sekä koulutusohjelmaan tai siitä pois siirtyneet.`,
Started: `Koulutusohjelmassa kunakin lukuvuonna aloittaneet opiskelijat. EI sisällä ohjelmaan siirtyneitä opiskelijoita.`,
Cancelled: `Koulutusohjelmaan oikeuttavan opinto-oikeutensa kunakin lukuvuonna peruttaneet`,
Graduated: `Tiettynä lukuvuonna aloittaneista opiskelijoista on valmistunut`,
Transferred: `Koulutusohjelmaan kyseisenä lukuvuonna siirtyneet opiskelijat.
Vanhojen koulutusohjelmien lakkauttaminen aiheuttaa tavallista suuremmat siirtoluvut lukuvuodelle 2020-2021.`,
Credits: `Kunakin vuonna aloittaneista opiskelijoista ne, jotka ovat ylittäneet tietyn opintopisterajapyykin`,
Thesis: `Loppututkielman kirjoittaneiden määrä`,
}
@@ -48,10 +48,12 @@ export default {
**Mean time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina. Luku sisältää mahdolliset poissaolokuukaudet.
n = **valmistuneet opiskelijat / kaikki ohjelmassa kyseisenä vuonna alkaneet opinto-oikeudet**. Kandiohjelmissa tavoiteaika on 36 kk, maisteriohjelmissa 24 kk`,
ProgrammesAfterGraduation: `
Sisältää maisteriohjelmat, joissa tämän kandiohjelman opiskelijat ovat aloittaneet valmistuttuaan kandeiksi. Vuosijaottelu on tehty maisteriohjelmassa aloittamisen perusteella.
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.
Mukana ovat **vain valmistuneet opiskelijat ja vain maisteriohjelmat**.
Mukana ovat **vain valmistuneet opiskelijat ja vain maisteriohjelmat.**
**Mikäli valittuna on maisteriohjelma:** Sisältää kandiohjelmat, joissa tämän ohjelman opiskelijat ovat opiskelleet ennen tuloaan maisteriohjelmaan. Vuosijaottelu on tehty maisteriohjelmassa aloittamisen perusteella.
`,
StudytrackOverview: `
Kuvaa koulutusohjelman kyseiseen ohjelmaan opinto-oikeudella varustettujen opiskelijoiden etenemistä opintopisteissä mitattuna. Esimerkiksi 2020-2021 aloittaneiden opintopistejakaumaa.
@@ -195,7 +195,7 @@ const PopulationCourseStats = props => {
}))
.sort((a, b) => a.module.order - b.module.order)
)
}, [state.studentAmountLimit, state.codeFilter, state.nameFilter, mandatoryCourses])
}, [state.studentAmountLimit, props.courses.coursestatistics, state.codeFilter, state.nameFilter, mandatoryCourses])
const onFilterChange = (e, field) => {
const {
@@ -1,114 +0,0 @@
import React, { useState, useEffect } from 'react'
import { Table, Header, Grid, Segment } from 'semantic-ui-react'
import { shape, any, arrayOf, bool, oneOfType, array } from 'prop-types'
const DIRECTIONS = {
ASC: 'ascending', // true
DESC: 'descending', // false
}
function sortBy(column, data) {
const sortByString = column === 'name' || column === 'code'
return data.concat().sort((a, b) => {
const keyA = column === 'name' ? a[column].fi : a[column]
const keyB = column === 'name' ? b[column].fi : b[column]
return sortByString ? keyB.localeCompare(keyA) : keyB - keyA
})
}
const BachelorsTable = ({ bachelors, loading }) => {
const [bachelorsData, setBachelorsData] = useState({})
const [sortColumn, setSortColumn] = useState(null)
const [direction, setDirection] = useState(false)
useEffect(() => setBachelorsData(bachelors), [bachelors])
useEffect(() => {
if (!bachelorsData || !bachelorsData.data) return
const data = sortBy(sortColumn, bachelorsData.data)
if (direction) data.reverse()
setBachelorsData({ ...bachelorsData, data })
}, [direction, sortColumn])
const getSorted = column => {
if (column !== sortColumn) return null
return direction ? DIRECTIONS.ASC : DIRECTIONS.DESC
}
const setSorted = column => {
if (column === sortColumn) setDirection(!direction)
else {
setSortColumn(column)
setDirection(false)
}
}
if (!loading && (!bachelorsData.data || !bachelorsData.data.length)) return null
return (
<>
<Header>
<Grid columns={2}>
<Grid.Row>
<Grid.Column>Study programme options</Grid.Column>
</Grid.Row>
</Grid>
</Header>
{bachelorsData && bachelorsData.years && `Statistics from ${bachelorsData.years.length} years`}
<Segment basic loading={loading} style={{ overflowX: 'auto' }}>
<Table celled structured compact striped selectable sortable>
{bachelorsData && bachelorsData.data ? (
<>
<Table.Header>
<Table.Row>
<Table.HeaderCell sorted={getSorted('name')} onClick={() => setSorted('name')}>
Name
</Table.HeaderCell>
<Table.HeaderCell sorted={getSorted('code')} onClick={() => setSorted('code')}>
Code
</Table.HeaderCell>
{bachelorsData.years.map(year => (
<Table.HeaderCell sorted={getSorted(year)} onClick={() => setSorted(year)} key={year}>
{year}
</Table.HeaderCell>
))}
<Table.HeaderCell sorted={getSorted('total')} onClick={() => setSorted('total')}>
Total
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{bachelorsData.data.map(item => (
<Table.Row key={item.code}>
<Table.Cell>{item.name.fi}</Table.Cell>
<Table.Cell>{item.code}</Table.Cell>
{bachelorsData.years.map(year => (
<Table.Cell key={year}>{item[year]}</Table.Cell>
))}
<Table.Cell>{item.total}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</>
) : null}
</Table>
</Segment>
</>
)
}
BachelorsTable.propTypes = {
bachelors: oneOfType([
shape({
data: arrayOf(any),
years: arrayOf(any),
}),
array,
]),
loading: bool.isRequired,
}
BachelorsTable.defaultProps = {
bachelors: null,
}
export default BachelorsTable
@@ -4,7 +4,7 @@ const ReactHighcharts = require('react-highcharts')
const colors = ['#003E65', '#1392c2', '#036415']
const BarChart = ({ data }) => {
const BarChart = ({ cypress, data }) => {
const dataWithColors = data?.graphStats?.map((series, index) => ({ ...series, color: colors[index] }))
const defaultConfig = {
@@ -51,7 +51,7 @@ const BarChart = ({ data }) => {
if (!data) return <></>
return (
<div className="graph-container">
<div className="graph-container" data-cy={`Graph-${cypress}`}>
<ReactHighcharts config={defaultConfig} />
</div>
)
@@ -6,7 +6,7 @@ const DataTable = ({ data, cypress, titles, wideTable }) => {
return (
<div className={`table-container${wideTable ? '-wide' : ''}`}>
<Table data-cy={cypress} celled>
<Table data-cy={`Table-${cypress}`} celled>
<Table.Header>
<Table.Row>
{titles?.map(title => (
@@ -2,7 +2,7 @@ import React from 'react'
const ReactHighcharts = require('react-highcharts')
const GaugeChart = ({ data, year, graduationAmount, studyprogramme }) => {
const GaugeChart = ({ cypress, data, year, graduationAmount, studyprogramme }) => {
if (!data || !graduationAmount || !data.length) return null
const thresholdValues = studyprogramme.includes('KH') ? [36, 41] : [24, 27]
@@ -76,7 +76,11 @@ const GaugeChart = ({ data, year, graduationAmount, studyprogramme }) => {
],
}
return <ReactHighcharts config={defaultConfig} />
return (
<div data-cy={`Graph-${cypress}`}>
<ReactHighcharts config={defaultConfig} />
</div>
)
}
export default GaugeChart
@@ -7,7 +7,7 @@ require('highcharts-exporting')(ReactHighcharts.Highcharts)
const colors = ['#003E65', '#1392c2', '#E68825', '#333737', '#036415']
const LineGraph = ({ data }) => {
const LineGraph = ({ cypress, data }) => {
const dataWithColors = data?.graphStats.map((series, index) => ({
...series,
color: colors[index],
@@ -49,7 +49,7 @@ const LineGraph = ({ data }) => {
if (!data) return <></>
return (
<div className="graph-container">
<div className="graph-container" data-cy={`Graph-${cypress}`}>
<ReactHighcharts config={defaultConfig} />
</div>
)
@@ -4,7 +4,7 @@ const ReactHighcharts = require('react-highcharts')
const colors = ['#003E65', '#1392c2', '#036415', '#bdc2c7', '#E68825', '#333737', '#54997b', '#195f8a']
const StackedBarChart = ({ data, labels, wideTable }) => {
const StackedBarChart = ({ cypress, data, labels, wideTable }) => {
const dataWithColors = data?.map((series, index) => ({ ...series, color: colors[index] }))
const defaultConfig = {
@@ -55,7 +55,7 @@ const StackedBarChart = ({ data, labels, wideTable }) => {
if (!data) return <></>
return (
<div className={`graph-container${wideTable ? '-narrow' : ''}`}>
<div className={`graph-container${wideTable ? '-narrow' : ''}`} data-cy={`Graph-${cypress}`}>
<ReactHighcharts config={defaultConfig} />
</div>
)
@@ -13,10 +13,10 @@ import InfoBox from '../../Info/InfoBox'
import InfotoolTips from '../../../common/InfoToolTips'
import '../studyprogramme.css'
const getRadioButton = (toolTip, firstLabel, secondLabel, value, setValue) => (
const getRadioButton = (cypress, toolTip, firstLabel, secondLabel, value, setValue) => (
<div className="radio-toggle">
<label className="toggle-label">{firstLabel}</label>
<Radio toggle checked={value} onChange={() => setValue(!value)} />
<Radio data-cy={cypress} toggle checked={value} onChange={() => setValue(!value)} />
{toolTip ? (
<WithHelpTooltip tooltip={{ short: toolTip }}>
<label className="toggle-label">{secondLabel}</label>
@@ -27,9 +27,7 @@ const getRadioButton = (toolTip, firstLabel, secondLabel, value, setValue) => (
</div>
)
const Overview = ({ studyprogramme }) => {
const [academicYear, setAcademicYear] = useState(false)
const [specialGroups, setSpecialGroups] = useState(false)
const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYear, setAcademicYear }) => {
const [showMeanTime, setShowMeanTime] = useState(true)
const toolTips = InfotoolTips.Studyprogramme
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
@@ -71,8 +69,16 @@ const Overview = ({ studyprogramme }) => {
return (
<div className="studyprogramme-overview">
<div className="toggle-container">
{getRadioButton(toolTips.YearToggle, 'Calendar year ', 'Academic year', academicYear, setAcademicYear)}
{getRadioButton(
'YearToggle',
toolTips.YearToggle,
'Calendar year ',
'Academic year',
academicYear,
setAcademicYear
)}
{getRadioButton(
'StudentToggle',
toolTips.StudentToggle,
'All studyrights included',
'Special studyrights excluded',
@@ -94,7 +100,7 @@ const Overview = ({ studyprogramme }) => {
<>
{getDivider('Students of the studyprogramme', 'StudentsOfTheStudyprogramme')}
<div className="section-container">
<LineGraph data={basics?.data} />
<LineGraph cypress="StudentsOfTheStudyprogramme" data={basics?.data} />
<DataTable
cypress="StudentsOfTheStudyprogramme"
data={basics?.data?.tableStats}
@@ -107,7 +113,11 @@ const Overview = ({ studyprogramme }) => {
<>
{getDivider('Credits produced by the studyprogramme', 'CreditsProducedByTheStudyprogramme')}
<div className="section-container">
<StackedBarChart data={credits?.data?.graphStats} labels={credits?.data?.years} />
<StackedBarChart
cypress="CreditsProducedByTheStudyprogramme"
data={credits?.data?.graphStats}
labels={credits?.data?.years}
/>
<DataTable
cypress="CreditsProducedByTheStudyprogramme"
data={credits?.data?.tableStats}
@@ -120,7 +130,7 @@ const Overview = ({ studyprogramme }) => {
<>
{getDivider('Graduated and thesis writers of the programme', 'GraduatedAndThesisWritersOfTheProgramme')}
<div className="section-container">
<BarChart data={graduations?.data} />
<BarChart cypress="GraduatedAndThesisWritersOfTheProgramme" data={graduations?.data} />
<DataTable
cypress="GraduatedAndThesisWritersOfTheProgramme"
data={graduations?.data?.tableStats}
@@ -128,10 +138,11 @@ const Overview = ({ studyprogramme }) => {
/>
</div>
{getDivider('Average graduation times', 'AverageGraduationTimes')}
{getRadioButton(null, 'Mean time', 'Median time', showMeanTime, setShowMeanTime)}
{getRadioButton('GraduationTimeToggle', null, 'Mean time', 'Median time', showMeanTime, setShowMeanTime)}
<div className="section-container-centered">
{graduations?.data?.years.map(year => (
<GaugeChart
cypress={`${year}-AverageGraduationTimes`}
key={year}
year={year}
data={
@@ -145,23 +156,27 @@ const Overview = ({ studyprogramme }) => {
/>
))}
</div>
{studyprogramme.includes('KH') && (
<>
{getDivider('Primary master programme studies after this programme', 'ProgrammesAfterGraduation')}
<div className="section-container">
<StackedBarChart
wideTable
data={graduations?.data?.programmesAfterGraphStats}
labels={graduations?.data?.years}
/>
<DataTable
wideTable
data={graduations?.data?.programmesAfterTableStats}
titles={graduations?.data?.programmesAfterTitles}
/>
</div>
</>
)}
<>
{getDivider(
studyprogramme.includes('KH')
? 'Primary master programme studies after this programme'
: 'Primary bachelor programme studies before this programme',
'ProgrammesBeforeOrAfter'
)}
<div className="section-container">
<StackedBarChart
cypress="ProgrammesBeforeOrAfter"
wideTable
data={graduations?.data?.programmesBeforeOrAfterGraphStats}
labels={graduations?.data?.years}
/>
<DataTable
wideTable
data={graduations?.data?.programmesBeforeOrAfterTableStats}
titles={graduations?.data?.programmesBeforeOrAfterTitles}
/>
</div>
</>
</>
)}
</>
@@ -1,62 +0,0 @@
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import ProductivityTable from '../ProductivityTable'
import ThroughputTable from '../ThroughputTable'
import BachelorsTable from '../BachelorsTable'
import { getProductivity } from '../../../redux/productivity'
import { getThroughput } from '../../../redux/throughput'
import { getBachelors } from '../../../redux/studyProgrammeBachelors'
import { isNewHYStudyProgramme } from '../../../common'
const Overview = props => {
const {
productivity,
throughput,
bachelors,
studyprogramme,
dispatchGetProductivity,
dispatchGetThroughput,
dispatchGetBachelors,
history,
} = props
useEffect(() => {
dispatchGetProductivity(studyprogramme)
dispatchGetThroughput(studyprogramme)
dispatchGetBachelors(studyprogramme)
}, [])
return (
<>
<ThroughputTable
throughput={throughput.data[studyprogramme]}
thesis={throughput.data.thesis}
loading={throughput.pending}
error={throughput.error}
studyprogramme={studyprogramme}
history={history}
newProgramme={isNewHYStudyProgramme(studyprogramme)}
/>
<ProductivityTable
productivity={productivity.data[studyprogramme]}
thesis={throughput.data.thesis}
loading={productivity.pending}
error={productivity.error}
showCredits={isNewHYStudyProgramme(studyprogramme)}
newProgramme={isNewHYStudyProgramme(studyprogramme)}
/>
<BachelorsTable bachelors={bachelors.data} loading={throughput.pending} />
</>
)
}
const mapStateToProps = ({ studyProgrammeProductivity, studyProgrammeThroughput, studyProgrammeBachelors }) => ({
productivity: studyProgrammeProductivity,
throughput: studyProgrammeThroughput,
bachelors: studyProgrammeBachelors,
})
export default connect(mapStateToProps, {
dispatchGetProductivity: getProductivity,
dispatchGetThroughput: getThroughput,
dispatchGetBachelors: getBachelors,
})(Overview)
@@ -1,162 +0,0 @@
import React, { useState, useEffect } from 'react'
import moment from 'moment'
import { Table, Header, Grid, Label, Segment, Dropdown } from 'semantic-ui-react'
import { connect } from 'react-redux'
import { shape, number, arrayOf, bool, string, oneOfType } from 'prop-types'
import { getProductivity } from '../../../redux/productivity'
import InfoBox from '../../Info/InfoBox'
import infotooltips from '../../../common/InfoToolTips'
import './productivityTable.css'
const ProductivityTable = ({ productivity, thesis, loading, error, showCredits, newProgramme }) => {
const [selectedYear, setYear] = useState(newProgramme ? 2017 : null)
const years = productivity
? productivity.data
.map(stats => ({ key: stats.year, text: stats.year, value: stats.year }))
.sort((year1, year2) => Number(year2.value) - Number(year1.value))
: []
useEffect(() => {
if (!selectedYear && years.length > 5) {
setYear(years[5].value)
} else if (!selectedYear && years.length > 1) {
setYear(years[years.length - 1].value)
}
}, [years])
if (error) return <h1>Oh no so error {error}</h1>
let thesisTypes = []
if (thesis) {
thesisTypes = thesis.map(t => t.thesisType)
}
const headerList = [
'Year',
'Credits',
thesisTypes.includes('MASTER') && 'Masters Thesis',
thesisTypes.includes('BACHELOR') && 'Bachelors Thesis',
'Graduated',
'Credits for studyprogramme majors',
'Credits for non major students',
'Hyväksiluettu (not included in Credits column)',
].filter(_ => _)
const handleChange = (event, { value }) => {
event.preventDefault()
setYear(value)
}
const creditCell = content =>
showCredits ? (
<Table.Cell>{content}</Table.Cell>
) : (
<Table.Cell
className="productivity-table__not-available"
title="Credits productivity statistics not available for old programmes"
>
Not available
</Table.Cell>
)
return (
<>
<div style={{ marginTop: '2em' }}>
<InfoBox content={infotooltips.PopulationOverview.YearlyProductivity} />
</div>
<Header>
<Grid columns={2}>
<Grid.Row>
<Grid.Column>
Yearly productivity
{productivity && (
<Header.Subheader data-cy="productivityUpdateStatus">
{`Last updated ${
productivity.lastUpdated
? moment(productivity.lastUpdated).format('HH:mm:ss MM-DD-YYYY')
: 'unknown'
}`}
{productivity.status === 'RECALCULATING' && (
<Label content="Recalculating! Refresh page in a few minutes" color="red" />
)}
</Header.Subheader>
)}
</Grid.Column>
</Grid.Row>
</Grid>
</Header>
<div>Statistics from selected year onwards</div>
{years.length > 4 ? <Dropdown options={years} onChange={handleChange} selection value={selectedYear} /> : null}
<Segment basic loading={loading} style={{ overflowX: 'auto' }}>
<Table structured celled compact striped selectable className="fixed-header">
<Table.Header>
<Table.Row>
{headerList.map(header => (
<Table.HeaderCell key={header}>{header}</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{productivity && productivity.data
? productivity.data
.filter(stats => Number(stats.year) >= Number(selectedYear))
.sort((year1, year2) => year2.year - year1.year)
.map(year => (
<Table.Row key={year.year}>
<Table.Cell>{year.year}</Table.Cell>
{creditCell(year.credits.toFixed(2))}
{thesisTypes.includes('BACHELOR') && <Table.Cell>{year.bThesis}</Table.Cell>}
{thesisTypes.includes('MASTER') && <Table.Cell>{year.mThesis}</Table.Cell>}
<Table.Cell>{year.graduated}</Table.Cell>
{creditCell(year.creditsForMajors.toFixed(2))}
{creditCell((year.credits - year.creditsForMajors).toFixed(2))}
{creditCell(year.transferredCredits.toFixed(2))}
</Table.Row>
))
: null}
</Table.Body>
</Table>
</Segment>
</>
)
}
ProductivityTable.propTypes = {
productivity: shape({
lastUpdated: string,
status: string,
data: arrayOf(
shape({
year: oneOfType([number, string]),
credits: number,
mThesis: number,
bThesis: number,
graduated: number,
creditsForMajors: number,
transferredCredits: number,
})
),
}),
thesis: arrayOf(
shape({
programmeCode: string,
courseCode: string,
thesisType: string,
createdAt: string,
updatedAt: string,
})
),
loading: bool.isRequired,
error: bool.isRequired,
showCredits: bool.isRequired,
newProgramme: bool.isRequired,
}
ProductivityTable.defaultProps = {
productivity: null,
thesis: undefined,
}
export default connect(null, {
dispatchGetProductivity: getProductivity,
})(ProductivityTable)
@@ -1,3 +0,0 @@
.productivity-table__not-available {
color: rgba(40, 40, 40, 0.3);
}
@@ -1,265 +0,0 @@
import React, { useEffect, useState } from 'react'
import { string, func, shape, bool } from 'prop-types'
import { connect } from 'react-redux'
import { uniqBy } from 'lodash'
import { Dropdown, Tab, Message, Button } from 'semantic-ui-react'
import ThroughputTable from '../ThroughputTable'
import { getProductivity } from '../../../redux/productivity'
import { getThroughput } from '../../../redux/throughput'
import { isNewHYStudyProgramme, textAndDescriptionSearch, getTextIn } from '../../../common'
import ProtoC from '../../CoolDataScience/ProtoC'
const Overview = ({
language,
throughput,
studyprogramme,
dispatchGetProductivity,
dispatchGetThroughput,
history,
studyprogrammes,
}) => {
const [selectedTrack, setTrack] = useState('')
const [selectedYear, setYear] = useState('')
const [throughputData, setData] = useState(null)
const [studytrackOptions, setStudytrackOptions] = useState([])
const [yearOptions, setYearOptions] = useState([])
const [activeIndex, setIndex] = useState(0)
useEffect(() => {
dispatchGetProductivity(studyprogramme)
dispatchGetThroughput(studyprogramme)
}, [])
// set dropdown options
useEffect(() => {
if (throughput.data[studyprogramme]) {
const years = throughput.data[studyprogramme].data.map(data => data.year)
const filteredStudytracks = years.reduce((acc, curr) => {
if (studyprogrammes[studyprogramme].enrollmentStartYears[curr.slice(0, 4)])
acc.push(...Object.values(studyprogrammes[studyprogramme].enrollmentStartYears[curr.slice(0, 4)].studyTracks))
return uniqBy(acc, 'code')
}, [])
const yearDropdownOptions = years.map(year => ({
key: year,
text: year,
value: year,
}))
const dropdownOptions = filteredStudytracks.map(st => ({
key: st.code,
text: getTextIn(st.name, language),
description: st.code,
value: st.code,
}))
if (yearDropdownOptions.length > 0) {
setYear(yearDropdownOptions[0].value)
}
setYearOptions(yearDropdownOptions)
setStudytrackOptions(dropdownOptions)
}
}, [throughput, studyprogrammes])
// mankel throughput data after selecting studytrack
useEffect(() => {
if (throughput.data[studyprogramme] && selectedTrack) {
if (throughput.data[studyprogramme].data[0].studytrackdata) {
const selectedData = throughput.data[studyprogramme].data.reduce((acc, curr) => {
if (curr.studytrackdata[selectedTrack]) acc.push(curr.studytrackdata[selectedTrack])
return acc
}, [])
const newData = {
data: selectedData,
lastUpdated: throughput.data[studyprogramme].lastUpdated,
status: throughput.data[studyprogramme].status,
totals: throughput.data[studyprogramme].stTotals[selectedTrack],
}
setData(newData)
}
}
}, [selectedTrack, activeIndex])
// mankel throughput data after selecting a year
useEffect(() => {
if (throughput.data[studyprogramme] && selectedYear) {
if (throughput.data[studyprogramme].data[0].studytrackdata) {
const selectedYearData = throughput.data[studyprogramme].data.find(data => data.year === selectedYear)
const filteredStudytracks = Object.values(studyprogrammes[studyprogramme].enrollmentStartYears).reduce(
(acc, curr) => {
acc.push(...Object.values(curr.studyTracks))
return uniqBy(acc, 'code')
},
[]
)
const studytrackYearData = Object.keys(selectedYearData.studytrackdata).reduce((acc, curr) => {
const studytrack = filteredStudytracks.find(st => st.code === curr)
if (studytrack) {
const newStudyTrackObject = {
...selectedYearData.studytrackdata[curr],
// oh pls no, pls fix asap
// need to fix logic in throughputtable component so that we can name this better
year: `${getTextIn(studytrack.name, language)}, ${curr}`,
}
acc.push(newStudyTrackObject)
}
return acc
}, [])
const newData = {
data: studytrackYearData,
lastUpdated: throughput.data[studyprogramme].lastUpdated,
status: throughput.data[studyprogramme].status,
totals: {
...selectedYearData,
credits: selectedYearData.creditValues,
genders: {
Male: Number(selectedYearData.genders.Male) || 0,
Female: Number(selectedYearData.genders.Female) || 0,
Other: Number(selectedYearData.genders.Other) || 0,
},
students:
(Number(selectedYearData.genders.Male) || 0) +
(Number(selectedYearData.genders.Female) || 0) +
(Number(selectedYearData.genders.Other) || 0),
},
}
setData(newData)
}
}
}, [selectedYear, activeIndex])
const handleStudytrackChange = (event, { value }) => {
event.preventDefault()
setTrack(value)
}
const handleYearChange = (event, { value }) => {
event.preventDefault()
setYear(value)
}
const renderSelection = year => {
if (year && yearOptions.length < 10)
return (
<>
<h3>Year selection</h3>
<Button.Group>
{yearOptions.map(option => (
<Button
key={option.key}
content={option.text}
style={{ borderRadius: '1px' }}
onClick={e => handleYearChange(e, option)}
active={option.value === selectedYear}
color={option.value === selectedYear ? 'blue' : 'black'}
basic={option.value !== selectedYear}
/>
))}
</Button.Group>
</>
)
return (
<>
<h3>{year ? 'Year' : 'Studytrack'} selection</h3>
<Dropdown
options={year ? yearOptions : studytrackOptions}
onChange={year ? handleYearChange : handleStudytrackChange}
defaultValue={year ? yearOptions[0].value : studytrackOptions[0].value}
selection
placeholder={`Select ${year ? 'year' : ' studytrack'}`}
search={textAndDescriptionSearch}
fluid
/>
</>
)
}
const renderTable = year => {
if (!throughputData || throughputData.data.length < 1) {
return (
<>
{renderSelection(year)}
<h2>No {year ? 'year' : 'studytrack'} selected or no data available for selection</h2>
</>
)
}
return (
<>
{renderSelection(year)}
<ThroughputTable
throughput={throughputData}
thesis={throughput.data.thesis}
loading={throughput.pending}
error={throughput.error}
studyprogramme={studyprogramme}
studytrack={selectedTrack}
history={history}
newProgramme={isNewHYStudyProgramme(studyprogramme)}
isStudytrackView={year}
/>
</>
)
}
const handleTabChange = (event, { activeIndex }) => {
setData(null)
if (activeIndex === 1) {
setTrack(studytrackOptions[0].value)
setYear('')
} else if (activeIndex === 0) {
setYear(yearOptions[0].value)
setTrack('')
}
setIndex(activeIndex)
}
const panes = [
{
menuItem: 'Year specific',
render: () => renderTable(true),
},
{
menuItem: 'Studytrack specific',
render: () => renderTable(false),
},
]
if (studyprogramme.includes('KH'))
panes.push({
menuItem: 'Studytrack Graph',
render: () => <ProtoC programme={studyprogramme} />,
})
return (
<>
<Message
content="Here you can see statistics on studytrack level. In year specific tab you can compare statistics for all studytracks within selected year.
In studytrack specific tab you can compare different years from one selected studytrack."
/>
<Tab panes={panes} activeIndex={activeIndex} onTabChange={handleTabChange} />
</>
)
}
Overview.propTypes = {
studyprogramme: string.isRequired,
dispatchGetProductivity: func.isRequired,
dispatchGetThroughput: func.isRequired,
history: shape({}).isRequired,
studyprogrammes: shape({}).isRequired,
language: string.isRequired,
throughput: shape({
error: bool,
pending: bool,
data: shape({}),
}).isRequired // eslint-disable-line
}
const mapStateToProps = ({ studyProgrammeThroughput, populationProgrammes, settings }) => ({
throughput: studyProgrammeThroughput,
studyprogrammes: populationProgrammes.data.programmes || {},
language: settings.language,
})
export default connect(mapStateToProps, {
dispatchGetProductivity: getProductivity,
dispatchGetThroughput: getThroughput,
})(Overview)
@@ -5,7 +5,7 @@ const ReactHighcharts = require('react-highcharts')
const colors = ['#333737', '#bdc2c7', '#1392c2', '#195f8a', '#54997b', '#036415']
const BarChart = ({ data, track }) => {
const BarChart = ({ cypress, data, track }) => {
if (!data || !data.creditGraphStats || !data.creditGraphStats[track])
return <NoDataMessage message="No progress data for the studytrack found. Try with another studytrack" />
@@ -64,7 +64,7 @@ const BarChart = ({ data, track }) => {
}
return (
<div className="graph-container">
<div className="graph-container" data-cy={`Graph-${cypress}`}>
<ReactHighcharts config={defaultConfig} />
</div>
)
@@ -18,7 +18,7 @@ const DataTable = ({ data, titles, track }) => {
return (
<div className="table-container">
<Table compact celled>
<Table data-cy="Table-StudytrackProgress" compact celled>
<Table.Header>
<Table.Row>
{titles?.map(title => (
@@ -2,7 +2,7 @@ import React from 'react'
const ReactHighcharts = require('react-highcharts')
const GaugeChart = ({ data, year, graduationAmount, totalAmount, studyprogramme }) => {
const GaugeChart = ({ cypress, data, year, graduationAmount, totalAmount, studyprogramme }) => {
if (!data || !graduationAmount || !data.length) return null
const thresholdValues = studyprogramme.includes('KH') ? [36, 41] : [24, 27]
@@ -76,7 +76,11 @@ const GaugeChart = ({ data, year, graduationAmount, totalAmount, studyprogramme
],
}
return <ReactHighcharts config={defaultConfig} />
return (
<div data-cy={`Graph-${cypress}`}>
<ReactHighcharts config={defaultConfig} />
</div>
)
}
export default GaugeChart
@@ -152,7 +152,7 @@ const StudytrackDataTable = ({
return (
<div className="datatable">
{getRadioButton(null, 'Show percentages', 'Hide percentages', hidePercentages, setHidePercentages)}
<Table celled>
<Table data-cy="Table-StudytrackOverview" celled>
<Table.Header>
<Table.Row>
{titles.map((title, index) => (
@@ -23,11 +23,9 @@ const getRadioButton = (toolTip, firstLabel, secondLabel, value, setValue) => (
</div>
)
const StudytrackOverview = ({ studyprogramme }) => {
const StudytrackOverview = ({ studyprogramme, specialGroups, setSpecialGroups, graduated, setGraduated }) => {
const toolTips = InfotoolTips.Studyprogramme
const [showMeanTime, setShowMeanTime] = useState(true)
const [specialGroups, setSpecialGroups] = useState(false)
const [graduated, setGraduated] = useState(false)
const [track, setTrack] = useState(studyprogramme)
const special = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
const grad = graduated ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
@@ -97,8 +95,9 @@ const StudytrackOverview = ({ studyprogramme }) => {
'StudytrackProgress'
)}
<div className="section-container">
<BarChart data={stats?.data} track={track || studyprogramme} />
<BarChart cypress="StudytrackProgress" data={stats?.data} track={track || studyprogramme} />
<BasicDataTable
cypress="StudytrackProgress"
data={stats?.data?.creditTableStats}
track={track || studyprogramme}
titles={stats?.data?.creditTableTitles}
@@ -111,6 +110,7 @@ const StudytrackOverview = ({ studyprogramme }) => {
<div className="section-container-centered">
{stats?.data?.years.map(year => (
<GaugeChart
cypress={`${year.slice(0, 4)}-AverageGraduationTimes`}
key={year}
year={year}
data={stats?.data?.graduationMedianTime[track][year]}
@@ -1,56 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import { Segment } from 'semantic-ui-react'
import { getCourseSearchResults } from '../../../selectors/courses'
import useLanguage from '../../LanguagePicker/useLanguage'
import SortableTable from '../../SortableTable'
const SearchResult = ({ courses, getCourseActions }) => {
const { language } = useLanguage()
if (!courses || courses.length === 0) return <Segment content="No results" />
return (
<SortableTable
getRowKey={c => c.code}
tableProps={{ celled: true }}
columns={[
{
key: 'code',
title: 'Code',
getRowVal: c => c.code,
headerProps: { width: 2, textAlign: 'center' },
},
{
key: 'name',
title: 'Name',
getRowVal: c => c.name[language],
headerProps: { width: 12, textAlign: 'left' },
},
{
key: 'actions',
title: 'Action',
getRowContent: getCourseActions,
headerProps: { width: 2, textAlign: 'center' },
},
]}
data={courses}
/>
)
}
SearchResult.defaultProps = {
courses: [],
}
const mapStateToProps = state => {
const { thesisCourses } = state
const { data } = thesisCourses || null
const thesisCourseCodes = data.map(c => c.courseCode)
const searchResults = getCourseSearchResults(state).courses
const filteredResults = searchResults.filter(c => !thesisCourseCodes.includes(c.code))
return {
courses: filteredResults,
}
}
export default connect(mapStateToProps)(SearchResult)
@@ -1,67 +0,0 @@
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { Dimmer, Loader, Segment, Button } from 'semantic-ui-react'
import { getThesisCourses, deleteThesisCourse } from '../../../redux/thesisCourses'
import SortableTable from '../../SortableTable'
const ThesisCourseList = ({ studyprogramme, getThesisCourses, pending, data, deleteThesisCourse }) => {
useEffect(() => {
getThesisCourses(studyprogramme)
}, [studyprogramme])
const handleDelete = course => () => {
deleteThesisCourse(studyprogramme, course)
}
return (
<>
<Dimmer inverted active={pending}>
<Loader />
</Dimmer>
{data.length === 0 ? (
<Segment content="No thesis courses" />
) : (
<SortableTable
tableProps={{ celled: true }}
data={data}
getRowKey={c => c.courseCode}
columns={[
{
key: 'code',
title: 'Code',
headerProps: { width: 2 },
getRowVal: c => c.courseCode,
},
{
key: 'type',
title: 'Type',
headerProps: { width: 13 },
getRowVal: c => c.thesisType,
},
{
key: 'action',
title: 'Remove',
headerProps: { width: 1, textAlign: 'center', onClick: null, sorted: null },
cellProps: { textAlign: 'center' },
getRowContent: c => <Button basic icon="trash" onClick={handleDelete(c.courseCode)} />,
},
]}
/>
)}
</>
)
}
ThesisCourseList.defaultProps = {
data: [],
}
const mapStateToProps = ({ thesisCourses }) => {
const { pending, data } = thesisCourses
return {
pending,
data,
}
}
export default connect(mapStateToProps, { getThesisCourses, deleteThesisCourse })(ThesisCourseList)
@@ -1,39 +0,0 @@
import React from 'react'
import { Button, Message } from 'semantic-ui-react'
import { connect } from 'react-redux'
import CourseSearchForm from '../../CourseSearchForm'
import ThesisCourseList from './ThesisCourseList'
import SearchResult from './SearchResult'
import Expandable from '../../Expandable'
import { createNewThesisCourse, getThesisCourses } from '../../../redux/thesisCourses'
const ThesisCourses = ({ studyprogramme, createThesis, getTheses }) => {
const handleClick = (course, thesis) => () => {
createThesis(studyprogramme, course, thesis).then(() => {
getTheses(studyprogramme)
})
}
return (
<>
<Message content="The set of courses used for calculating thesis productivity statistics" />
<Expandable fluid title="Add thesis course">
<CourseSearchForm />
<SearchResult
getCourseActions={({ code }) => (
<Button.Group basic fluid width="3">
<Button content="Bachelors" onClick={handleClick(code, 'BACHELOR')} />
<Button.Or />
<Button content="Masters" onClick={handleClick(code, 'MASTER')} />
</Button.Group>
)}
/>
</Expandable>
<ThesisCourseList studyprogramme={studyprogramme} />
</>
)
}
export default connect(null, {
createThesis: createNewThesisCourse,
getTheses: getThesisCourses,
})(ThesisCourses)
@@ -1,390 +0,0 @@
import React, { useState } from 'react'
import moment from 'moment'
import { Header, Table, Icon, Label, Segment, Dropdown, Button, Modal, Popup } from 'semantic-ui-react'
import { Link } from 'react-router-dom'
import { flatten, uniq, range } from 'lodash'
import { useGetAuthorizedUserQuery } from 'redux/auth'
import { isDev } from '../../../conf'
import InfoBox from '../../Info/InfoBox'
import infotooltips from '../../../common/InfoToolTips'
const getMonths = year => {
const end = moment()
const lastDayOfMonth = moment(end).endOf('month')
const start = `${year}-08-01`
return Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(start))).asMonths())
}
const PopulationStatisticsLink = ({ studytrack, studyprogramme, year: yearLabel, children }) => {
const year = Number(yearLabel.slice(0, 4))
const months = Math.ceil(moment.duration(moment().diff(`${year}-08-01`)).asMonths())
const href = studytrack
? `/populations?months=${months}&semesters=FALL&semesters=` +
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"studyTrack"%3A"${studytrack}"%7D&year=${year}`
: `/populations?months=${months}&semesters=FALL&semesters=` +
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=${year}`
return (
<Link title={`Population statistics of class ${yearLabel}`} to={href}>
{children}
</Link>
)
}
const TotalPopulationLink = ({ confirm, years, studyprogramme, studytrack, children }) => {
const confirmWrapper = e => {
if (confirm) {
// eslint-disable-next-line no-alert
const c = window.confirm(
`Are you sure you want to see a combined population of ${years.length} different populations?`
)
if (!c) e.preventDefault()
}
}
const yearsString = years.map(year => year.value).join('&years=')
const months = getMonths(Math.min(...years.map(year => Number(year.value))))
const href = studytrack
? `/populations?months=${months}&semesters=FALL&semesters=` +
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"studyTrack"%3A"${studytrack}"%7D&year=${years[0].value}&years=${yearsString}`
: `/populations?months=${months}&semesters=FALL&semesters=` +
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=${years[0].value}&years=${yearsString}`
return (
<Link onClick={confirmWrapper} title="Population statistics of all years" to={href}>
{children}
</Link>
)
}
const ThroughputTable = ({
throughput,
thesis,
loading,
error,
studyprogramme,
studytrack,
history,
newProgramme,
isStudytrackView,
}) => {
const [lowerYear, setLower] = useState(null)
const [upperYear, setUpper] = useState(null)
const [yearDifference, setDifference] = useState(null)
const { isAdmin } = useGetAuthorizedUserQuery()
const data = throughput && throughput.data ? throughput.data.filter(year => year.credits.length > 0) : []
const years =
data && !isStudytrackView
? data
.map(stats => ({
key: stats.year.substring(0, 4),
text: stats.year,
value: stats.year.substring(0, 4),
}))
.sort((year1, year2) => Number(year2.value) - Number(year1.value))
: []
if (error) return <h1>Oh no so error {error}</h1>
// toggle only on dev env and with admin user
const GRADUATED_FEATURE_TOGGLED_ON = isAdmin && isDev
const genders = data.length > 0 ? uniq(flatten(data.map(year => Object.keys(year.genders)))) : []
genders.sort()
const renderGenders = genders.length > 0
const calculateTotalNationalities = () =>
data.length > 0 ? Object.values(throughput.totals.nationalities).reduce((res, curr) => res + curr, 0) : 0
const renderRatioOfFinns = calculateTotalNationalities() > 0
let thesisTypes = []
if (thesis) {
thesisTypes = thesis.map(t => t.thesisType)
}
const handleLowerBoundChange = (event, { value }) => {
event.preventDefault()
if (Number(value) > Number(upperYear)) {
setUpper(value)
setLower(value)
setDifference(1)
} else if (Number(upperYear) - Number(value) > 5) {
setUpper(String(Number(value) + 5))
setLower(value)
setDifference(6)
} else {
setLower(value)
setDifference(Number(upperYear) - Number(value) + 1)
}
}
const handleUpperBoundChange = (event, { value }) => {
event.preventDefault()
if (Number(value) < Number(lowerYear)) {
setUpper(value)
setLower(value)
setDifference(1)
} else if (Number(value) - Number(lowerYear) > 5) {
setLower(String(Number(value) - 5))
setUpper(value)
setDifference(6)
} else {
setUpper(value)
setDifference(Number(value) - Number(lowerYear) + 1)
}
}
const pushQueryToUrl = event => {
event.preventDefault()
const yearRange = range(Number(lowerYear), Number(upperYear) + 1)
const months = getMonths(Number(lowerYear))
const yearsString = yearRange.join('&years=')
if (yearRange.length > 1) {
history.push(
`/populations?months=${months}&semesters=FALL&semesters=` +
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=${lowerYear}&years=${yearsString}`
)
} else {
history.push(
`/populations?months=${months}&semesters=FALL&semesters=` +
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=${lowerYear}&years[]=${yearsString}`
)
}
}
const headerCellPopUp = (title, cell) => {
return (
<Popup trigger={cell} wide="very" position="left center">
<Popup.Content>{infotooltips.PopulationOverview[title]}</Popup.Content>
</Popup>
)
}
const renderStudentsHeader = () => {
let colSpan = 4
let rowSpan = 1
if (renderGenders) colSpan += genders.length
if (renderRatioOfFinns) colSpan += 1
if (!renderGenders && !renderRatioOfFinns) rowSpan += 1
return headerCellPopUp(
'Students',
<Table.HeaderCell colSpan={colSpan} rowSpan={rowSpan}>
Students
</Table.HeaderCell>
)
}
const ratioOfFinnsIn = year => {
const total = Object.values(year.nationalities).reduce((res, curr) => res + curr, 0)
return (
<Table.Cell>
{`${year.nationalities.Finland || 0} (${Math.floor((year.nationalities.Finland / total) * 100) || 0}%)`}
</Table.Cell>
)
}
return (
<>
<div style={{ marginTop: '2em' }}>
<InfoBox content={infotooltips.PopulationOverview.PopulationProgress} />
</div>
<Header>
{studytrack ? `Population progress for selected studytrack` : 'Population progress'}
{throughput && (
<Header.Subheader data-cy="throughputUpdateStatus">
{`Last updated ${
throughput.lastUpdated ? moment(throughput.lastUpdated).format('HH:mm:ss MM-DD-YYYY') : 'unknown'
}`}
{throughput.status === 'RECALCULATING' && (
<Label content="Recalculating! Refresh page in a few minutes" color="red" />
)}
</Header.Subheader>
)}
</Header>
<Segment basic loading={loading} style={{ overflowX: 'auto' }}>
<Table celled structured compact striped selectable className="fixed-header">
<Table.Header>
<Table.Row>
<Table.HeaderCell rowSpan="2">{isStudytrackView ? 'Studytrack' : 'Year'}</Table.HeaderCell>
{renderStudentsHeader()}
{headerCellPopUp(
'Graduated',
<Table.HeaderCell colSpan={GRADUATED_FEATURE_TOGGLED_ON ? '3' : '1'}>Graduated</Table.HeaderCell>
)}
{headerCellPopUp('Credits', <Table.HeaderCell colSpan="5">Credits</Table.HeaderCell>)}
{(thesisTypes.includes('BACHELOR') || thesisTypes.includes('MASTER')) &&
headerCellPopUp('Thesis', <Table.HeaderCell colSpan={thesisTypes.length}>Thesis</Table.HeaderCell>)}
</Table.Row>
<Table.Row>
{renderGenders || renderRatioOfFinns ? <Table.HeaderCell content="All" /> : null}
{genders.map(gender => (
<Table.HeaderCell key={gender} content={gender} />
))}
{renderRatioOfFinns ? <Table.HeaderCell content="Finnish" /> : null}
{headerCellPopUp(
'Started',
<Table.HeaderCell rowSpan="2" colSpan="1">
Started
</Table.HeaderCell>
)}
{headerCellPopUp(
'Transferred',
<Table.HeaderCell rowSpan="2" colSpan="1">
Transferred to
</Table.HeaderCell>
)}
{headerCellPopUp(
'Cancelled',
<Table.HeaderCell rowSpan="2" colSpan="1">
Cancelled
</Table.HeaderCell>
)}
<Table.HeaderCell>Graduated overall</Table.HeaderCell>
{GRADUATED_FEATURE_TOGGLED_ON && (
<>
<Table.HeaderCell>Graduated in time</Table.HeaderCell>
<Table.HeaderCell>Graduation median time</Table.HeaderCell>
</>
)}
<Table.HeaderCell content="≥ 30" />
<Table.HeaderCell content="≥ 60" />
<Table.HeaderCell content="≥ 90" />
<Table.HeaderCell content="≥ 120" />
<Table.HeaderCell content="≥ 150" />
{thesisTypes.includes('MASTER') && <Table.HeaderCell content="Master" />}
{thesisTypes.includes('BACHELOR') && <Table.HeaderCell content="Bachelor" />}
</Table.Row>
</Table.Header>
<Table.Body>
{data
.sort((year1, year2) => Number(year2.year.slice(0, 4)) - Number(year1.year.slice(0, 4)))
.map(year => (
<Table.Row key={year.year}>
<Table.Cell>
{year.year}
{!isStudytrackView && (
<PopulationStatisticsLink
studytrack={studytrack}
studyprogramme={studyprogramme}
year={year.year}
>
<Icon name="level up alternate" />
</PopulationStatisticsLink>
)}
</Table.Cell>
<Table.Cell>{year.credits.length}</Table.Cell>
{genders.map(gender => (
<Table.Cell key={`${year.year} gender:${gender}`}>
{`${year.genders[gender] || 0} (${
Math.floor((year.genders[gender] / year.credits.length) * 100) || 0
}%)`}
</Table.Cell>
))}
{renderRatioOfFinns && ratioOfFinnsIn(year)}
<Table.Cell>{year.started}</Table.Cell>
<Table.Cell>{year.transferred}</Table.Cell>
<Table.Cell>{year.cancelled}</Table.Cell>
<Table.Cell>{year.graduated}</Table.Cell>
{GRADUATED_FEATURE_TOGGLED_ON && (
<>
<Table.Cell>{year.inTargetTime}</Table.Cell>
<Table.Cell>{year.medianGraduationTime ? `${year.medianGraduationTime} months` : '∞'}</Table.Cell>
</>
)}
{Object.keys(year.creditValues).map(creditKey => (
<Table.Cell key={`${year.year} credit:${creditKey}`}>{year.creditValues[creditKey]}</Table.Cell>
))}
{thesisTypes.includes('MASTER') ? <Table.Cell>{year.thesisM}</Table.Cell> : null}
{thesisTypes.includes('BACHELOR') ? <Table.Cell>{year.thesisB}</Table.Cell> : null}
</Table.Row>
))}
</Table.Body>
{throughput && throughput.totals ? (
<Table.Footer>
<Table.Row>
<Table.HeaderCell style={{ fontWeight: 'bold' }}>
All{' '}
{newProgramme && !isStudytrackView && years.length > 0 && years.length < 5 ? (
<TotalPopulationLink confirm studyprogramme={studyprogramme} studytrack={studytrack} years={years}>
<Icon name="level up alternate" />
</TotalPopulationLink>
) : null}
</Table.HeaderCell>
<Table.HeaderCell>{throughput.totals.students}</Table.HeaderCell>
{Object.keys(throughput.totals.genders)
.sort()
.map(genderKey => (
<Table.HeaderCell key={`${genderKey}total`}>
{`${throughput.totals.genders[genderKey]} (${Math.floor(
(throughput.totals.genders[genderKey] / throughput.totals.students) * 100
)}%)`}
</Table.HeaderCell>
))}
{renderRatioOfFinns ? (
<Table.HeaderCell>
{`${throughput.totals.nationalities.Finland || 0} (${
Math.floor((throughput.totals.nationalities.Finland / calculateTotalNationalities()) * 100) || 0
}%)`}
</Table.HeaderCell>
) : null}
<Table.HeaderCell>{throughput.totals.started}</Table.HeaderCell>
<Table.HeaderCell>{throughput.totals.transferred}</Table.HeaderCell>
<Table.HeaderCell>{throughput.totals.cancelled}</Table.HeaderCell>
<Table.HeaderCell>{throughput.totals.graduated}</Table.HeaderCell>
{GRADUATED_FEATURE_TOGGLED_ON && (
<>
<Table.HeaderCell>{throughput.totals.inTargetTime}</Table.HeaderCell>
<Table.HeaderCell>
{throughput.totals.medianGraduationTime
? `${throughput.totals.medianGraduationTime} months`
: '∞'}
</Table.HeaderCell>
</>
)}
{Object.keys(throughput.totals.credits).map(creditKey => (
<Table.HeaderCell key={`${creditKey}total`}>{throughput.totals.credits[creditKey]}</Table.HeaderCell>
))}
{thesisTypes.includes('MASTER') ? (
<Table.HeaderCell>{throughput.totals.thesisM}</Table.HeaderCell>
) : null}
{thesisTypes.includes('BACHELOR') ? (
<Table.HeaderCell>{throughput.totals.thesisB}</Table.HeaderCell>
) : null}
</Table.Row>
</Table.Footer>
) : null}
</Table>
</Segment>
{years.length > 4 ? (
<>
Statistics from:
<Dropdown selection options={years} onChange={handleLowerBoundChange} value={lowerYear} />
to:
<Dropdown selection options={years} onChange={handleUpperBoundChange} value={upperYear} />
<Modal
trigger={<Button disabled={!lowerYear || !upperYear}>Show combined population</Button>}
header={`Are you sure you want to see a combined population of ${yearDifference} different populations?`}
actions={[
'Cancel',
{
key: 'fetch',
content: 'Show population',
positive: true,
onClick: event => pushQueryToUrl(event),
},
]}
/>
</>
) : null}
</>
)
}
export default ThroughputTable
@@ -1,35 +1,38 @@
import React, { useCallback, useEffect } from 'react'
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
import React, { useCallback, useEffect, useState } from 'react'
import { useHistory, withRouter } from 'react-router-dom'
import { connect, useDispatch, useSelector } from 'react-redux'
import { Header, Segment, Tab } from 'semantic-ui-react'
import { isEqual } from 'lodash'
import { useGetAuthorizedUserQuery } from 'redux/auth'
import DegreeCoursesTable from './DegreeCourses'
import StudyProgrammeSelector from './StudyProgrammeSelector'
import NewOverview from './NewOverview'
import NewStudytrackOverview from './NewStudytrackOverview'
import NewUpdateView from './NewUpdateView'
import BasicOverview from './BasicOverview'
import StudytrackOverview from './StudytrackOverview'
import UpdateView from './UpdateView'
import '../PopulationQueryCard/populationQueryCard.css'
import { getTextIn } from '../../common'
import { useTabs, useTitle } from '../../common/hooks'
import TSA from '../../common/tsa'
import Tags from './Tags'
import { getThroughput } from '../../redux/throughput'
import { getProductivity } from '../../redux/productivity'
import { getProgrammes } from '../../redux/populationProgrammes'
import useLanguage from '../LanguagePicker/useLanguage'
const StudyProgramme = props => {
const dispatch = useDispatch()
const history = useHistory()
const programmes = useSelector(state => state.populationProgrammes?.data?.programmes)
const { language } = useLanguage()
const { isAdmin } = useGetAuthorizedUserQuery()
const [tab, setTab] = useTabs('p_tab', 0, props.history)
const [tab, setTab] = useTabs('p_tab', 0, history)
const [academicYear, setAcademicYear] = useState(false)
const [specialGroups, setSpecialGroups] = useState(false)
const [graduated, setGraduated] = useState(false)
useTitle('Study programmes')
useEffect(() => {
props.getProgrammesDispatch()
dispatch(getProgrammes())
}, [])
const getPanes = () => {
@@ -38,16 +41,34 @@ const StudyProgramme = props => {
const panes = []
panes.push({
menuItem: 'Basic information (NEW)',
render: () => <NewOverview studyprogramme={studyProgrammeId} history={props.history} />,
render: () => (
<BasicOverview
studyprogramme={studyProgrammeId}
history={history}
specialGroups={specialGroups}
setSpecialGroups={setSpecialGroups}
academicYear={academicYear}
setAcademicYear={setAcademicYear}
/>
),
})
panes.push({
menuItem: 'Studytracks and student populations (NEW)',
render: () => <NewStudytrackOverview studyprogramme={studyProgrammeId} history={props.history} />,
render: () => (
<StudytrackOverview
studyprogramme={studyProgrammeId}
history={history}
specialGroups={specialGroups}
setSpecialGroups={setSpecialGroups}
graduated={graduated}
setGraduated={setGraduated}
/>
),
})
if (isAdmin) {
panes.push({
menuItem: 'Update statistics',
render: () => <NewUpdateView studyprogramme={studyProgrammeId} />,
render: () => <UpdateView studyprogramme={studyProgrammeId} />,
})
}
panes.push({
@@ -63,12 +84,12 @@ const StudyProgramme = props => {
const handleSelect = useCallback(
programme => {
props.history.push(`/study-programme/${programme}`, { selected: programme })
history.push(`/study-programme/${programme}`, { selected: programme })
},
[props.history]
[history]
)
const { match, programmes } = props
const { match } = props
const { studyProgrammeId } = match.params
const programmeName = programmes?.[studyProgrammeId] && getTextIn(programmes?.[studyProgrammeId].name, language)
const panes = getPanes()
@@ -112,22 +133,4 @@ const StudyProgramme = props => {
)
}
const mapStateToProps = ({ populationProgrammes }) => {
const programmes = populationProgrammes.data ? populationProgrammes.data.programmes : {}
return {
programmes,
}
}
export default connect(
mapStateToProps,
{
getThroughputDispatch: getThroughput,
getProductivityDispatch: getProductivity,
getProgrammesDispatch: getProgrammes,
},
null,
{
areStatePropsEqual: isEqual,
}
)(withRouter(StudyProgramme))
export default connect()(withRouter(StudyProgramme))
-6
View File
@@ -9,9 +9,6 @@ import populationSelectedStudentCourses from './populationSelectedStudentCourses
import populationProgrammes from './populationProgrammes'
import populationProgrammesUnfiltered from './populationProgrammesUnfiltered'
import populationMandatoryCourses from './populationMandatoryCourses'
import studyProgrammeProductivity from './productivity'
import studyProgrammeThroughput from './throughput'
import studyProgrammeBachelors from './studyProgrammeBachelors'
import faculties from './faculties'
import units from './units'
import elementdetails from './elementdetails'
@@ -53,9 +50,6 @@ const store = configureStore({
populationProgrammes,
populationProgrammesUnfiltered,
populationMandatoryCourses,
studyProgrammeProductivity,
studyProgrammeThroughput,
studyProgrammeBachelors,
faculties,
units,
students,
@@ -1,13 +0,0 @@
import { callController } from '../apiConnection/index'
import listreducer from './common/listreducer'
const prefix = 'STUDYPROGRAMME_PRODUCTIVITY_'
export const getProductivity = studyprogrammeId => {
const route = `v2/studyprogrammes/${studyprogrammeId}/productivity`
return callController(route, prefix, [], 'get')
}
const reducer = listreducer(prefix, null, false)
export default reducer
@@ -1,13 +0,0 @@
import { callController } from '../apiConnection/index'
import listreducer from './common/listreducer'
const prefix = 'GET_STUDYPROGRAMME_BACHELORS_'
export const getBachelors = studyprogrammeId => {
const route = `v2/studyprogrammes/${studyprogrammeId}/optiondata`
return callController(route, prefix, [], 'get')
}
const reducer = listreducer(prefix, null, true)
export default reducer
-13
View File
@@ -1,13 +0,0 @@
import { callController } from '../apiConnection/index'
import listreducer from './common/listreducer'
const prefix = 'GET_STUDYPROGRAMME_THROUGHPUT_'
export const getThroughput = studyprogrammeId => {
const route = `v2/studyprogrammes/${studyprogrammeId}/throughput`
return callController(route, prefix, [], 'get')
}
const reducer = listreducer(prefix, null, false)
export default reducer