mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
316f3511c2 | ||
|
|
e36c7aa6ba | ||
|
|
ffda9af909 | ||
|
|
7b56978e7c | ||
|
|
b95d439836 | ||
|
|
1f7573a0ba | ||
|
|
0204b4890f | ||
|
|
5a19e48ff9 | ||
|
|
790386a394 | ||
|
|
e86045e8e6 | ||
|
|
b1fb0a7cc7 | ||
|
|
0086ae69d3 | ||
|
|
10d9731fab | ||
|
|
83ffd5389e | ||
|
|
fcb4328dba | ||
|
|
f196a0939d | ||
|
|
52a480d6fb | ||
|
|
0618a17830 | ||
|
|
d54b3c0782 | ||
|
|
28fa4e5310 | ||
|
|
df402c4823 | ||
|
|
9136d41bdd | ||
|
|
e1f63c2441 | ||
|
|
d797b766b1 | ||
|
|
03bed43077 | ||
|
|
6763905b2a | ||
|
|
994101732d | ||
|
|
b1c071a507 | ||
|
|
0e2bdc8d2d | ||
|
|
1cd6e2f435 | ||
|
|
0790003ae8 | ||
|
|
a77bf8b6a9 | ||
|
|
00cd57dc13 | ||
|
|
44a8f39cc1 | ||
|
|
4ecdb3237d | ||
|
|
6b544179ef | ||
|
|
a9100b78ec |
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
@@ -48,6 +48,10 @@ What different CLI options do:
|
||||
|
||||
## ⌨️ Development
|
||||
|
||||
### Architecture
|
||||
|
||||

|
||||
|
||||
### Basics
|
||||
|
||||
The development environment is entirely configured in the `docker-compose.yml` file located in this repository. The file defines the services for oodikone's two main components: **oodikone** and **updater**.
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('Course Statistics tests', () => {
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietokantojen perusteet')
|
||||
|
||||
cy.contains('Tietokantojen perusteet')
|
||||
cy.contains('Avoin yo: Tietokantojen perusteet')
|
||||
cy.contains('Tietokantojen perusteet')
|
||||
cy.contains('A581328')
|
||||
cy.contains('AYTKT10004')
|
||||
cy.contains('TKT10004, 581328')
|
||||
@@ -105,46 +105,39 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
|
||||
cy.contains('TKT10004, 581328 Tietokantojen perusteet')
|
||||
cy.contains('TKT10004, 581328, AYTKT10004, A581328 Tietokantojen perusteet')
|
||||
cy.get('.right').click()
|
||||
cy.contains('No results')
|
||||
})
|
||||
|
||||
it('Provider organization toggle works', () => {
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('Search for courses')
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietokantojen perusteet')
|
||||
cy.contains('td', /^AYTKT10004/).click()
|
||||
|
||||
cy.contains('td', /^TKT10004/).click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
cy.contains('AYTKT10004 Avoin yo: Tietokantojen perusteet')
|
||||
|
||||
cy.contains('TKT10004, 581328, AYTKT10004, A581328 Tietokantojen perusteet')
|
||||
cy.get('[data-cy=unify_radio_reqular]').click()
|
||||
cy.contains('TKT10004, 581328 Tietokantojen perusteet')
|
||||
})
|
||||
|
||||
it('Searching course by name displays right courses, 10 credit courses', () => {
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('Search for courses')
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
|
||||
|
||||
cy.contains('Tietorakenteet ja algoritmit')
|
||||
cy.contains('Avoin yo: Tietorakenteet ja algoritmit')
|
||||
cy.contains('AYTKT20001')
|
||||
cy.contains('TKT20001, 58131')
|
||||
cy.contains('td', /^TKT20001/).click()
|
||||
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
|
||||
cy.contains('TKT20001, 58131 Tietorakenteet ja algoritmit')
|
||||
cy.contains('TKT20001, 58131, AYTKT20001 Tietorakenteet ja algoritmit')
|
||||
cy.get('.right').click()
|
||||
cy.contains('No results')
|
||||
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
|
||||
cy.contains('td', /^AYTKT20001/).click()
|
||||
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
cy.contains('AYTKT20001 Avoin yo: Tietorakenteet ja algoritmit')
|
||||
|
||||
cy.get('.right').click()
|
||||
cy.contains('No results')
|
||||
cy.get("input[name='unifyOpenUniCourses']").parent().click()
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
|
||||
cy.contains('td', 'TKT20001, 58131, AYTKT20001').click()
|
||||
|
||||
@@ -159,12 +152,11 @@ describe('Course Statistics tests', () => {
|
||||
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
|
||||
cy.contains('tr', 'TKT20003').click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
|
||||
cy.contains('Käyttöjärjestelmät')
|
||||
cy.contains('TKT20003')
|
||||
|
||||
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
|
||||
cy.contains('Population of course Käyttöjärjestelmät 2019-2020')
|
||||
cy.contains('Population of course Unified with open uni: Käyttöjärjestelmät 2019-2020')
|
||||
cy.contains('TKT20003')
|
||||
|
||||
cy.contains('Students (127)').click()
|
||||
@@ -174,12 +166,12 @@ describe('Course Statistics tests', () => {
|
||||
|
||||
it('Population of course shows grades for each student', () => {
|
||||
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
|
||||
cy.contains(/^TKT20001$/).click()
|
||||
cy.contains(/^TKT20001, AYTKT20001$/).click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('TKT20001, 58131 Tietorakenteet ja algoritmit')
|
||||
cy.contains('TKT20001, 58131, AYTKT20001 Tietorakenteet ja algoritmit')
|
||||
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
|
||||
cy.contains('Population of course Tietorakenteet ja algoritmit 2018-2019')
|
||||
cy.contains('Students (197)').click()
|
||||
cy.contains('Population of course Unified with open uni: Tietorakenteet ja algoritmit 2018-2019')
|
||||
cy.contains('Students (204)').click()
|
||||
cy.contains('td', '010262566').siblings().contains('4')
|
||||
cy.contains('td', '010674989').siblings().contains('1')
|
||||
})
|
||||
@@ -189,10 +181,11 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains(/^TKT20003$/).click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('TKT20003, 582219 Käyttöjärjestelmät')
|
||||
cy.get('[data-cy=unify_radio_reqular]').click()
|
||||
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
|
||||
cy.contains('Population of course Käyttöjärjestelmät 2019-2020')
|
||||
cy.contains('Language distribution').click()
|
||||
cy.contains('td', 'finnish').siblings().contains('107')
|
||||
cy.contains('td', 'finnish').siblings().contains('122')
|
||||
cy.contains('td', 'english').siblings().contains('5')
|
||||
})
|
||||
|
||||
@@ -200,7 +193,6 @@ describe('Course Statistics tests', () => {
|
||||
beforeEach(() => {
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('Search for courses')
|
||||
cy.get("input[name='unifyOpenUniCourses']").parent().click()
|
||||
cy.get("input[placeholder='Search by a course code']").type('TKT10002')
|
||||
cy.contains('td', 'TKT10002, AYTKT10002').click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
@@ -215,6 +207,16 @@ describe('Course Statistics tests', () => {
|
||||
['Total', 511, 486, 25],
|
||||
]
|
||||
|
||||
const attemptsTableContentsOpen = [
|
||||
// [time, passed, failed, passrate]
|
||||
['Total', 175, 175, 0],
|
||||
]
|
||||
|
||||
const attemptsTableContentsReqular = [
|
||||
// [time, passed, failed, passrate]
|
||||
['Total', 312, 298, 14],
|
||||
]
|
||||
|
||||
const gradesTableContents = [
|
||||
// [time, attempts, 0, 1, 2, 3, 4, 5, other passed]
|
||||
['Total', 511, 25, 11, 17, 21, 59, 365, 13],
|
||||
@@ -271,6 +273,42 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Toggling course provider changes stats correctly', () => {
|
||||
cy.get('[data-cy=unify_radio_reqular]').click()
|
||||
cy.get("div[name='toYear']").within(() => {
|
||||
cy.get("div[role='option']").first().should('have.text', '2020-2021')
|
||||
cy.contains("div[role='option']", yearRange.to).should('have.class', 'selected')
|
||||
cy.get("div[role='option']").last().should('have.text', yearRange.from)
|
||||
cy.get("div[role='option']").should('have.length', 21)
|
||||
})
|
||||
cy.contains('#CourseStatPanes a.item', 'Table').click()
|
||||
cy.contains('#CourseStatPanes a.item', 'Attempts').click()
|
||||
cy.get('#CourseStatPanes table>tbody').within(() => {
|
||||
attemptsTableContentsReqular.forEach((values, trIndex) => {
|
||||
cy.get('tr')
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
cy.get('[data-cy=unify_radio_open]').click()
|
||||
cy.get('#CourseStatPanes table>tbody').within(() => {
|
||||
attemptsTableContentsOpen.forEach((values, trIndex) => {
|
||||
cy.get('tr')
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
cy.get('[data-cy=unify_radio_unify]').click()
|
||||
})
|
||||
|
||||
it('after changing time range shows same stats', () => {
|
||||
const newYearRange = { from: '2016-2017', to: '2019-2020' }
|
||||
cy.get("div[name='fromYear']")
|
||||
@@ -302,7 +340,7 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.only('Some features of Course Statistics are hidden for courseStatistics-users without other rights', () => {
|
||||
it('Some features of Course Statistics are hidden for courseStatistics-users without other rights', () => {
|
||||
cy.init('/coursestatistics', 'onlycoursestatistics')
|
||||
cy.get('[data-cy=navbar-courseStatistics]').click()
|
||||
cy.get('[data-cy=course-code-input]').type('TKT20003')
|
||||
@@ -316,10 +354,10 @@ describe('Course Statistics tests', () => {
|
||||
|
||||
const attemptsTableContents = [
|
||||
// [time, passed, failed, passrate]
|
||||
['Total*', 288, 213, 75, '73.96 %'],
|
||||
['Total', 288, 213, 75, '73.96 %'],
|
||||
// ['Total', 295, 216, 79, '73.22 %'],
|
||||
['2020-2021', '5 or less students', 'NA', 'NA', 'NA'],
|
||||
//['2020-2021', 5, 2, 3, '40.00 %'],
|
||||
// ['2020-2021', 5, 2, 3, '40.00 %'],
|
||||
['2019-2020', 164, 121, 43, '73.78 %'],
|
||||
['2018-2019', 85, 60, 25, '70.59 %'],
|
||||
['2017-2018', 39, 32, 7, '82.05 %'],
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('Population Statistics tests', () => {
|
||||
cy.get('[data-cy=coursestats-link-TKT10002]').click()
|
||||
cy.wait('@coursePage')
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('TKT10002, 581325 Ohjelmoinnin perusteet')
|
||||
cy.contains('TKT10002, 581325, AYTKT10002, A581325 Ohjelmoinnin perusteet')
|
||||
})
|
||||
|
||||
it("Empty 'tags' tab has a link to the page where tags can be created", () => {
|
||||
|
||||
@@ -21,20 +21,20 @@ const getEmptyYears = isAcademicYear => {
|
||||
}
|
||||
|
||||
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')
|
||||
cy.viewport(1536, 960)
|
||||
})
|
||||
|
||||
/* Basic information overview -tests*/
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Basic information -tab loads', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
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-ProgrammesBeforeOrAfter]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
|
||||
@@ -45,10 +45,10 @@ describe('Studyprogramme overview', () => {
|
||||
// [Year, Started, Graduated, Cancelled, Transferred Away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
[2021, 0, 0, 0, 1, 0],
|
||||
[2020, 10, 36, 16, 3, 2],
|
||||
[2019, 86, 17, 14, 0, 1],
|
||||
[2018, 162, 1, 3, 0, 0],
|
||||
[2017, 173, 0, 0, 0, 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')
|
||||
@@ -86,7 +86,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.checkTableStats(tableContents, 'GraduatedAndThesisWritersOfTheProgramme')
|
||||
})
|
||||
|
||||
it('Special studyrights can be excluded and basic data changes accordingly', () => {
|
||||
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()
|
||||
@@ -125,6 +125,7 @@ describe('Studyprogramme overview', () => {
|
||||
]
|
||||
|
||||
cy.checkTableStats(graduatedTableContents, 'GraduatedAndThesisWritersOfTheProgramme')
|
||||
cy.get('[data-cy=StudentToggle]').click()
|
||||
})
|
||||
|
||||
it('Year can be changed to academic year, and data changes accordingly', () => {
|
||||
@@ -155,6 +156,7 @@ describe('Studyprogramme overview', () => {
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
cy.get('[data-cy=YearToggle]').click()
|
||||
})
|
||||
|
||||
it('Basic information graphs render', () => {
|
||||
@@ -182,17 +184,26 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', 36)
|
||||
.should('contain', 35)
|
||||
|
||||
cy.get('[data-cy=Graph-ProgrammesAfterGraduation')
|
||||
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', 'Kielellisen diversiteetin ja digitaalisten ihmistieteiden maisteriohjelma')
|
||||
.should('contain', 1)
|
||||
.should('contain', 4)
|
||||
.should('contain', 13)
|
||||
})
|
||||
})
|
||||
|
||||
/* Studytrack overview -tests*/
|
||||
/* 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', () => {
|
||||
@@ -310,7 +321,30 @@ describe('Studyprogramme overview', () => {
|
||||
cy.checkTableStats(tableContents, 'StudytrackProgress')
|
||||
})
|
||||
|
||||
/* Tag-tests*/
|
||||
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()}`
|
||||
|
||||
Generated
+3
-3
@@ -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",
|
||||
|
||||
@@ -59,10 +59,12 @@ const findOneByCode = code => {
|
||||
})
|
||||
}
|
||||
|
||||
const creditsForCourses = async (codes, anonymizationSalt, unifyOpenUniCourses) => {
|
||||
const creditsForCourses = async (codes, anonymizationSalt, unification) => {
|
||||
let is_open = false
|
||||
|
||||
if (unifyOpenUniCourses) {
|
||||
if (unification === 'open') is_open = true
|
||||
|
||||
if (unification === 'unify') {
|
||||
is_open = {
|
||||
[Op.in]: [false, true],
|
||||
}
|
||||
@@ -162,23 +164,19 @@ const allCodeAlternatives = async code => {
|
||||
return sortMainCode([...course.substitutions, code])
|
||||
}
|
||||
|
||||
const yearlyStatsOfNew = async (coursecode, separate, unifyOpenUniCourses, anonymizationSalt) => {
|
||||
const yearlyStatsOfNew = async (coursecode, separate, unification, anonymizationSalt) => {
|
||||
const courseForSubs = await Course.findOne({
|
||||
where: { code: coursecode },
|
||||
})
|
||||
|
||||
let codes = courseForSubs.substitutions ? sortMainCode([...courseForSubs.substitutions, coursecode]) : [coursecode]
|
||||
|
||||
if (isOpenUniCourseCode(coursecode) && !unifyOpenUniCourses) {
|
||||
codes = [coursecode]
|
||||
}
|
||||
|
||||
if (!isOpenUniCourseCode(coursecode) && !unifyOpenUniCourses) {
|
||||
codes = codes.filter(code => !code.match(/^[A][0-9]|^AY/))
|
||||
if (unification === 'reqular') {
|
||||
codes = codes.filter(course => !isOpenUniCourseCode(course))
|
||||
}
|
||||
|
||||
const [credits, course] = await Promise.all([
|
||||
creditsForCourses(codes, anonymizationSalt, unifyOpenUniCourses),
|
||||
creditsForCourses(codes, anonymizationSalt, unification),
|
||||
Course.findOne({
|
||||
where: {
|
||||
code: coursecode,
|
||||
@@ -234,6 +232,7 @@ const yearlyStatsOfNew = async (coursecode, separate, unifyOpenUniCourses, anony
|
||||
counter.markCreditToStudentCategories(studentnumber, passed, attainment_date, groupcode)
|
||||
}
|
||||
const statistics = counter.getFinalStatistics(anonymizationSalt)
|
||||
|
||||
return {
|
||||
...statistics,
|
||||
coursecode,
|
||||
@@ -292,10 +291,19 @@ const maxYearsToCreatePopulationFrom = async coursecodes => {
|
||||
}
|
||||
|
||||
const courseYearlyStats = async (coursecodes, separate, unifyOpenUniCourses, anonymizationSalt) => {
|
||||
const stats = await Promise.all(
|
||||
coursecodes.map(code => yearlyStatsOfNew(code, separate, unifyOpenUniCourses, anonymizationSalt))
|
||||
const [unify, reqular, open] = ['unify', 'reqular', 'open']
|
||||
|
||||
const statsReqular = await Promise.all(
|
||||
coursecodes.map(async code => {
|
||||
const unifyStats = await yearlyStatsOfNew(code, separate, unify, anonymizationSalt)
|
||||
const reqularStats = await yearlyStatsOfNew(code, separate, reqular, anonymizationSalt)
|
||||
const openStats = await yearlyStatsOfNew(code, separate, open, anonymizationSalt)
|
||||
|
||||
return { unifyStats, reqularStats, openStats }
|
||||
})
|
||||
)
|
||||
return stats
|
||||
|
||||
return statsReqular
|
||||
}
|
||||
|
||||
const nameLikeTerm = name => {
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
const moment = require('moment')
|
||||
const sequelize = require('sequelize')
|
||||
const { Semester } = require('../models')
|
||||
const Op = sequelize.Op
|
||||
@@ -28,8 +29,8 @@ const getSemestersAndYears = async () => {
|
||||
acc.years[yearcode] = {
|
||||
yearcode,
|
||||
yearname,
|
||||
startdate: Math.min(acc.years[yearcode].startdate, startdate),
|
||||
enddate: Math.max(acc.years[yearcode].enddate, enddate),
|
||||
startdate: moment.min(moment(acc.years[yearcode].startdate), moment(startdate)),
|
||||
enddate: moment.max(moment(acc.years[yearcode].enddate), moment(enddate)),
|
||||
}
|
||||
}
|
||||
return acc
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Generated
+3
-3
@@ -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",
|
||||
|
||||
@@ -4,11 +4,11 @@ export default {
|
||||
Calendar year: Näyttää tilastot kalenterivuosille jaoteltuna, esimerkiksi 1.1.2019-31.12.2019
|
||||
`,
|
||||
StudentToggle: `
|
||||
All studyrights included: Valinnassa ovat mukana vaihto-opiskelijat, erillisoikeudella opiskelevat, opintonsa keskeyttäneet opiskelijat, ohjelmaan siirtyneet opiskelijat sekä ohjelmasta pois siirtyneet opiskelijat.
|
||||
All studyrights: Valinnassa ovat mukana vaihto-opiskelijat, erillisoikeudella opiskelevat, opintonsa keskeyttäneet opiskelijat, ohjelmaan siirtyneet opiskelijat sekä ohjelmasta pois siirtyneet opiskelijat.
|
||||
|
||||
Special studyrights excluded: Valinnasta ja kaikista luvuista on poissuljettu kaikki edellä mainitut erikoisryhmät.
|
||||
`,
|
||||
GradToggle: `
|
||||
GraduatedToggle: `
|
||||
Graduated included: Valinnassa ovat mukana ohjelmasta jo valmistuneet opiskelijat.
|
||||
|
||||
Graduated excluded: Valinnasta on poistettu ohjelmasta jo valmistuneet opiskelijat.
|
||||
@@ -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.
|
||||
|
||||
@@ -101,6 +101,22 @@ export const getTextIn = (texts, language) => {
|
||||
return null
|
||||
}
|
||||
|
||||
export const getUnifyTextIn = unify => {
|
||||
switch (unify) {
|
||||
case 'reqularStats':
|
||||
return ''
|
||||
|
||||
case 'openStats':
|
||||
return 'Open University: '
|
||||
|
||||
case 'unifyStats':
|
||||
return 'Unified with open uni: '
|
||||
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export const cancelablePromise = promise => {
|
||||
let hasCanceled = false
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import CoursePopulationCreditGainTable from './CoursePopulationCreditGainTable'
|
||||
import CustomPopulationProgrammeDist from '../CustomPopulation/CustomPopulationProgrammeDist'
|
||||
import CustomPopulationCourses from '../CustomPopulation/CustomPopulationCourses'
|
||||
import ProgressBar from '../ProgressBar'
|
||||
import { getStudentToTargetCourseDateMap, getTextIn } from '../../common'
|
||||
import { getStudentToTargetCourseDateMap, getTextIn, getUnifyTextIn } from '../../common'
|
||||
import { useProgress, useTitle } from '../../common/hooks'
|
||||
import infotooltips from '../../common/InfoToolTips'
|
||||
import {
|
||||
@@ -47,9 +47,9 @@ const CoursePopulation = ({
|
||||
getSemestersDispatch,
|
||||
semesters,
|
||||
getFacultiesDispatch,
|
||||
unifyOpen,
|
||||
}) => {
|
||||
const { language } = useLanguage()
|
||||
|
||||
const [codes, setCodes] = useState([])
|
||||
const [headerYears, setYears] = useState('')
|
||||
const [dateFrom, setDateFrom] = useState(null)
|
||||
@@ -124,7 +124,8 @@ const CoursePopulation = ({
|
||||
}
|
||||
}, [semesters])
|
||||
|
||||
const header = courseData ? `${getTextIn(courseData.name, language)} ${headerYears}` : null
|
||||
const avoin = getUnifyTextIn(unifyOpen)
|
||||
const header = courseData ? `${avoin} ${getTextIn(courseData.name, language)} ${headerYears}` : null
|
||||
|
||||
const subHeader = codes.join(', ')
|
||||
|
||||
@@ -364,11 +365,12 @@ const CustomPopulationCoursesWrapper = props => {
|
||||
return <CustomPopulationCourses {...props} courses={courseStatistics} />
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ singleCourseStats, populations, semesters }) => {
|
||||
const mapStateToProps = ({ singleCourseStats, populations, semesters, courseSearch }) => {
|
||||
return {
|
||||
studentData: populations.data,
|
||||
pending: populations.pending,
|
||||
courseData: singleCourseStats.stats || {},
|
||||
courseData: singleCourseStats.stats?.[courseSearch.openOrReqular],
|
||||
unifyOpen: courseSearch.openOrReqular,
|
||||
semesters: semesters.data,
|
||||
elementDetails: populations?.data?.elementdetails?.data,
|
||||
}
|
||||
|
||||
+1
-2
@@ -70,9 +70,8 @@ const getGradeColumns = (notThesisGrades, addHTAndTT) => {
|
||||
|
||||
const AttemptsTable = ({
|
||||
data: { stats, name },
|
||||
settings: { showGrades },
|
||||
settings: { showGrades, separate },
|
||||
alternatives,
|
||||
separate,
|
||||
isRelative,
|
||||
userHasAccessToAllStats,
|
||||
headerVisible = false,
|
||||
|
||||
+3
-6
@@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'
|
||||
import { Header, Icon, Item } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import { uniq } from 'lodash'
|
||||
import { shape, string, number, oneOfType, arrayOf, bool } from 'prop-types'
|
||||
import { string, object, arrayOf, bool } from 'prop-types'
|
||||
import SortableTable from '../../../../SortableTable'
|
||||
import { defineCellColor } from '../util'
|
||||
|
||||
@@ -92,9 +92,8 @@ const getColumns = (showDetails, userHasAccessToAllStats, alternatives, separate
|
||||
|
||||
const StudentTable = ({
|
||||
data: { name, stats },
|
||||
settings: { showDetails },
|
||||
settings: { showDetails, separate },
|
||||
alternatives,
|
||||
separate,
|
||||
userHasAccessToAllStats,
|
||||
headerVisible = false,
|
||||
}) => {
|
||||
@@ -125,12 +124,10 @@ const StudentTable = ({
|
||||
}
|
||||
|
||||
StudentTable.propTypes = {
|
||||
stats: arrayOf(shape({})).isRequired,
|
||||
name: oneOfType([number, string]).isRequired,
|
||||
data: object.isRequired,
|
||||
alternatives: arrayOf(string).isRequired,
|
||||
separate: bool,
|
||||
userHasAccessToAllStats: bool.isRequired,
|
||||
headerVisible: bool.isRequired,
|
||||
}
|
||||
|
||||
StudentTable.defaultProps = {
|
||||
|
||||
@@ -1,60 +1,109 @@
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Menu, Radio } from 'semantic-ui-react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { Menu, Radio, Form } from 'semantic-ui-react'
|
||||
import { viewModeNames } from './util'
|
||||
import HelpButton from '../HelpButton'
|
||||
import StudentTable from './Tables/student'
|
||||
import AttemptsTable from './Tables/attempts'
|
||||
import { toggleOpenAndReqularCourses } from '../../../../redux/coursesearch'
|
||||
import selectors from '../../../../selectors/courseStats'
|
||||
|
||||
export const TablesSettings = ({ value, onChange }) => {
|
||||
const { viewMode, showDetails, showGrades } = value
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const openOrReqular = useSelector(state => state.courseSearch.openOrReqular)
|
||||
const toggleUnifyRadioValue = (event, { value }) => {
|
||||
dispatch(toggleOpenAndReqularCourses(value))
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu secondary style={{ marginBottom: 0 }}>
|
||||
{Object.entries(viewModeNames).map(([key, name]) => (
|
||||
<Menu.Item
|
||||
active={viewMode === key}
|
||||
data-cy={`viewMode-${name}`}
|
||||
name={name}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
...value,
|
||||
viewMode: key,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Show details"
|
||||
disabled={viewMode !== 'STUDENT'}
|
||||
checked={showDetails}
|
||||
onChange={() => onChange({ ...value, showDetails: !showDetails })}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Show grades"
|
||||
data-cy="gradeToggle"
|
||||
disabled={viewMode !== 'ATTEMPTS'}
|
||||
checked={showGrades}
|
||||
onChange={() => onChange({ ...value, showGrades: !showGrades })}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<HelpButton tab="Tables" viewMode={viewMode} />
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
<div>
|
||||
<Menu secondary>
|
||||
{Object.entries(viewModeNames).map(([key, name]) => (
|
||||
<Menu.Item
|
||||
key={key}
|
||||
active={viewMode === key}
|
||||
data-cy={`viewMode-${name}`}
|
||||
name={name}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
...value,
|
||||
viewMode: key,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Show details"
|
||||
disabled={viewMode !== 'STUDENT'}
|
||||
checked={showDetails}
|
||||
onChange={() => onChange({ ...value, showDetails: !showDetails })}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Show grades"
|
||||
data-cy="gradeToggle"
|
||||
disabled={viewMode !== 'ATTEMPTS'}
|
||||
checked={showGrades}
|
||||
onChange={() => onChange({ ...value, showGrades: !showGrades })}
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item>
|
||||
<HelpButton tab="Tables" viewMode={viewMode} />
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
<Menu secondary style={{ marginLeft: '0.5rem' }}>
|
||||
<Form>
|
||||
<Form.Group>
|
||||
<Form.Field>
|
||||
<b>Provider organization:</b>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Radio
|
||||
label="university"
|
||||
name="radioGroup"
|
||||
value="reqularStats"
|
||||
checked={openOrReqular === 'reqularStats'}
|
||||
onChange={toggleUnifyRadioValue}
|
||||
data-cy="unify_radio_reqular"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Radio
|
||||
label="open university"
|
||||
name="radioGroup"
|
||||
value="openStats"
|
||||
checked={openOrReqular === 'openStats'}
|
||||
onChange={toggleUnifyRadioValue}
|
||||
data-cy="unify_radio_open"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Radio
|
||||
label="unify"
|
||||
name="radioGroup"
|
||||
value="unifyStats"
|
||||
checked={openOrReqular === 'unifyStats'}
|
||||
onChange={toggleUnifyRadioValue}
|
||||
data-cy="unify_radio_unify"
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Tables = props => {
|
||||
const alternatives = useSelector(state => state.courseStats.data[state.singleCourseStats.selectedCourse].alternatives)
|
||||
|
||||
const alternatives = useSelector(selectors.getCourseAlternatives)
|
||||
const viewModes = { ATTEMPTS: AttemptsTable, STUDENT: StudentTable }
|
||||
|
||||
const Content = viewModes[props.settings.viewMode]
|
||||
|
||||
return <Content {...props} alternatives={alternatives} />
|
||||
|
||||
@@ -51,137 +51,29 @@ const PaneContent = ({ component: Component, settings: SettingsComponent, initia
|
||||
)
|
||||
}
|
||||
|
||||
const ResultTabs = ({ primary, comparison, history }) => {
|
||||
const ResultTabs = ({ primary, comparison, history, separate }) => {
|
||||
const [tab, setTab] = useTabs('cs_tab', 0, history)
|
||||
// const [viewMode, setViewMode] = useState(viewModeNames.STUDENT)
|
||||
// const [selectedView, setSelectedView] = useState(false)
|
||||
// const [isRelative, setIsRelative] = useState(false)
|
||||
// const [showGrades, setShowGrades] = useState(false)
|
||||
const { userHasAccessToAllStats } = primary
|
||||
|
||||
/* const handleModeChange = newViewMode => {
|
||||
sendAnalytics(`Current view mode '${newViewMode}'`, 'Course statistics')
|
||||
setViewMode(newViewMode)
|
||||
} */
|
||||
|
||||
/* useEffect(() => {
|
||||
const newViewMode = selectedView ? viewModeNames.ATTEMPTS : viewModeNames.STUDENT
|
||||
handleModeChange(newViewMode)
|
||||
}, [selectedView]) */
|
||||
|
||||
const handleTabChange = (...params) => {
|
||||
const { activeIndex } = params[1]
|
||||
const currentTab = params[1].panes[activeIndex]
|
||||
sendAnalytics(`Current tab '${currentTab.menuItem.content}'`, 'Course statistics')
|
||||
setTab(...params)
|
||||
// setViewMode(resetViewMode ? viewMode : viewModeNames.ATTEMPTS)
|
||||
}
|
||||
|
||||
/* const getRadioButton = (firstLabel, secondLabel, value, setValue) => (
|
||||
<div className="toggleContainer">
|
||||
<label className="toggleLabel">{firstLabel}</label>
|
||||
<Radio toggle data-cy="gradeToggle" checked={value} onChange={() => setValue(!value)} />
|
||||
<label className="toggleLabel">{secondLabel}</label>
|
||||
</div>
|
||||
) */
|
||||
|
||||
/* const renderViewModeSelector = () => {
|
||||
const isTogglePane = tab !== 0
|
||||
const getButtonMenu = () => (
|
||||
<Menu secondary>
|
||||
{Object.values(viewModeNames).map(name => (
|
||||
<Menu.Item key={name} name={name} active={viewMode === name} onClick={() => handleModeChange(name)} />
|
||||
))}
|
||||
{viewMode === 'Attempts' && getRadioButton('Totals', 'Grade distribution', showGrades, setShowGrades)}
|
||||
{viewMode === 'Attempts' && showGrades && getRadioButton('Absolute', 'Relative', isRelative, setIsRelative)}
|
||||
</Menu>
|
||||
)
|
||||
|
||||
const getToggle = () => {
|
||||
return (
|
||||
<div className="chartToggleContainer">
|
||||
{tab === 1 && getRadioButton('Student', 'Attempts', selectedView, setSelectedView)}
|
||||
{(tab === 2 || comparison) && getRadioButton('Absolute', 'Relative', isRelative, setIsRelative)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Remove "false" and activate infoboxes when the texts for them are ready
|
||||
return (
|
||||
<div className="modeSelectorContainer">
|
||||
{isTogglePane ? getToggle() : getButtonMenu()}
|
||||
<InfoBox content={infotooltips.CourseStatistics[tab][viewMode]} />
|
||||
</div>
|
||||
)
|
||||
} */
|
||||
|
||||
/* const getPanes = () => {
|
||||
return [
|
||||
{
|
||||
menuItem: { key: 'Table', icon: 'table', content: 'Table' },
|
||||
render: () => (
|
||||
<Tables
|
||||
separate={separate}
|
||||
comparison={comparison}
|
||||
primary={primary}
|
||||
viewMode={viewMode}
|
||||
isRelative={isRelative}
|
||||
showGrades={showGrades}
|
||||
userHasAccessToAllStats={userHasAccessToAllStats}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
menuItem: { key: 'pass', icon: 'balance', content: 'Pass rate chart' },
|
||||
render: () => (
|
||||
<PassRate
|
||||
comparison={comparison}
|
||||
primary={primary}
|
||||
viewMode={viewMode}
|
||||
isRelative={isRelative && !!comparison}
|
||||
userHasAccessToAllStats={userHasAccessToAllStats}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
menuItem: { key: 'grade', icon: 'chart bar', content: 'Grade distribution chart' },
|
||||
render: () => (
|
||||
<Distribution
|
||||
comparison={comparison}
|
||||
primary={primary}
|
||||
isRelative={isRelative}
|
||||
userHasAccessToAllStats={userHasAccessToAllStats}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return paneMenuItems.map(p => {
|
||||
const { menuItem, renderFn } = p
|
||||
return {
|
||||
menuItem,
|
||||
render: () => (
|
||||
<Grid padded="vertically" columns="equal">
|
||||
<Grid.Row className="modeSelectorRow">{renderViewModeSelector()}</Grid.Row>
|
||||
{renderFn()}
|
||||
</Grid>
|
||||
),
|
||||
}
|
||||
})
|
||||
} */
|
||||
|
||||
const paneTypes = [
|
||||
{
|
||||
label: 'Tables',
|
||||
icon: 'table',
|
||||
initialSettings: { showDetails: false, viewMode: 'STUDENT' },
|
||||
initialSettings: { showDetails: false, viewMode: 'STUDENT', separate },
|
||||
settings: TablesSettings,
|
||||
component: Tables,
|
||||
},
|
||||
{
|
||||
label: 'Pass rate chart',
|
||||
icon: 'balance',
|
||||
initialSettings: { viewMode: 'STUDENT' },
|
||||
initialSettings: { viewMode: 'STUDENT', separate },
|
||||
settings: PassRateSettings,
|
||||
component: PassRate,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect, Fragment } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Segment, Header, Form } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import qs from 'query-string'
|
||||
import { sortBy } from 'lodash'
|
||||
import { func, arrayOf, shape, bool } from 'prop-types'
|
||||
import { clearCourses, findCoursesV2, toggleUnifyOpenUniCourses } from '../../../redux/coursesearch'
|
||||
import { func, shape } from 'prop-types'
|
||||
import { clearCourses, findCoursesV2 } from '../../../redux/coursesearch'
|
||||
import { getCourseStats, clearCourseStats } from '../../../redux/coursestats'
|
||||
import { getCourseSearchResults } from '../../../selectors/courses'
|
||||
import { useSearchHistory, usePrevious } from '../../../common/hooks'
|
||||
@@ -15,7 +15,6 @@ import AutoSubmitSearchInput from '../../AutoSubmitSearchInput'
|
||||
import CourseTable from '../CourseTable'
|
||||
import SearchHistory from '../../SearchHistory'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
import filterCourseSearchResults from './searchFormUtils'
|
||||
|
||||
const sendAnalytics = (action, name, value) => TSA.Matomo.sendEvent('Course statistics search', action, name, value)
|
||||
|
||||
@@ -55,13 +54,18 @@ const useTSASearchResultsHook = (coursesLoading, courseName, courseCode, matchin
|
||||
|
||||
const SearchForm = props => {
|
||||
const { language } = useLanguage()
|
||||
const dispatch = useDispatch()
|
||||
const isLoading = useSelector(state => state.courseStats.pending)
|
||||
const coursesLoading = useSelector(state => state.courseSearch.pending)
|
||||
const matchingCourses = useSelector(getCourseSearchResults)
|
||||
const [state, setState] = useState({
|
||||
...INITIAL,
|
||||
})
|
||||
const [searchHistory, addItemToSearchHistory, updateItemInSearchHistory] = useSearchHistory('courseSearch', 6)
|
||||
|
||||
const { courseName, courseCode, selectedCourses, separate } = state
|
||||
const { coursesLoading, isLoading, matchingCourses, unifyOpenUniCourses } = props
|
||||
|
||||
// const { coursesLoading, isLoading, matchingCourses /* unifyOpenUniCourses */ } = props
|
||||
|
||||
const parseQueryFromUrl = () => {
|
||||
const { location } = props
|
||||
@@ -71,7 +75,7 @@ const SearchForm = props => {
|
||||
...rest,
|
||||
courseCodes: JSON.parse(courseCodes),
|
||||
separate: JSON.parse(separate),
|
||||
unifyOpenUniCourses: JSON.parse(unifyOpenUniCourses || false),
|
||||
// unifyOpenUniCourses: JSON.parse(unifyOpenUniCourses || false),
|
||||
}
|
||||
return query
|
||||
}
|
||||
@@ -79,14 +83,14 @@ const SearchForm = props => {
|
||||
const fetchStatisticsFromUrlParams = () => {
|
||||
const query = parseQueryFromUrl()
|
||||
setState({ ...state, ...query })
|
||||
props.getCourseStats(query, props.onProgress)
|
||||
dispatch(getCourseStats(query, props.onProgress))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const { location } = props
|
||||
if (!location.search) {
|
||||
props.clearCourses()
|
||||
props.clearCourseStats()
|
||||
dispatch(clearCourses())
|
||||
dispatch(clearCourseStats())
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -143,7 +147,6 @@ const SearchForm = props => {
|
||||
const params = {
|
||||
courseCodes: codes,
|
||||
separate,
|
||||
unifyOpenUniCourses: props.unifyOpenUniCourses,
|
||||
}
|
||||
const searchHistoryText = codes.map(code => `${getTextIn(selectedCourses[code].name, language)} ${code}`)
|
||||
addItemToSearchHistory({
|
||||
@@ -158,10 +161,10 @@ const SearchForm = props => {
|
||||
const isValidCode = validateInputLength(courseCode, 2)
|
||||
|
||||
if (isValidName || isValidCode) {
|
||||
return props.findCoursesV2({ name: courseName, code: courseCode })
|
||||
return dispatch(findCoursesV2({ name: courseName, code: courseCode }))
|
||||
}
|
||||
if (courseName.length < 5 && courseCode.length < 2) {
|
||||
props.clearCourses()
|
||||
dispatch(clearCourses())
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
@@ -172,14 +175,6 @@ const SearchForm = props => {
|
||||
sendAnalytics('Toggle Separate stats for spring & fall', `${newValue}`)
|
||||
}
|
||||
|
||||
const onToggleUnifyOpenUniCoursesCheckbox = () => {
|
||||
const newValue = !unifyOpenUniCourses
|
||||
sendAnalytics('Toggle Unify open university courses', `${newValue}`)
|
||||
|
||||
setState({ ...state, selectedCourses: {} })
|
||||
props.toggleUnifyOpenUniCourses()
|
||||
}
|
||||
|
||||
const courses = matchingCourses.filter(c => !selectedCourses[c.code])
|
||||
|
||||
const disabled = isLoading || Object.keys(selectedCourses).length === 0
|
||||
@@ -235,16 +230,6 @@ const SearchForm = props => {
|
||||
data-cy="course-code-input"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field width={4} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<label>Unify open university courses:</label>
|
||||
<span style={{ flex: 1, display: 'flex', alignItems: 'center' }}>
|
||||
<Form.Checkbox
|
||||
name="unifyOpenUniCourses"
|
||||
onChange={onToggleUnifyOpenUniCoursesCheckbox}
|
||||
checked={unifyOpenUniCourses}
|
||||
/>
|
||||
</span>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
<div style={!noSelectedCourses ? searchBoxStyle : null}>
|
||||
<CourseTable
|
||||
@@ -314,38 +299,9 @@ SearchForm.defaultProps = {
|
||||
}
|
||||
|
||||
SearchForm.propTypes = {
|
||||
findCoursesV2: func.isRequired,
|
||||
getCourseStats: func.isRequired,
|
||||
clearCourses: func.isRequired,
|
||||
clearCourseStats: func.isRequired,
|
||||
matchingCourses: arrayOf(shape({})).isRequired,
|
||||
isLoading: bool.isRequired,
|
||||
coursesLoading: bool.isRequired,
|
||||
history: shape({}).isRequired,
|
||||
location: shape({}).isRequired,
|
||||
unifyOpenUniCourses: bool.isRequired,
|
||||
toggleUnifyOpenUniCourses: func.isRequired,
|
||||
onProgress: func,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { courses } = getCourseSearchResults(state)
|
||||
const { pending: courseStatsPending } = state.courseStats
|
||||
const { unifyOpenUniCourses } = state.courseSearch
|
||||
return {
|
||||
matchingCourses: filterCourseSearchResults(courses, unifyOpenUniCourses),
|
||||
isLoading: courseStatsPending,
|
||||
coursesLoading: state.courseSearch.pending,
|
||||
unifyOpenUniCourses,
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, {
|
||||
getCourseStats,
|
||||
clearCourses,
|
||||
findCoursesV2,
|
||||
clearCourseStats,
|
||||
toggleUnifyOpenUniCourses,
|
||||
})(SearchForm)
|
||||
)
|
||||
export default withRouter(SearchForm)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { orderBy } from 'lodash'
|
||||
// import { orderBy } from 'lodash'
|
||||
|
||||
// handle special case where the course's actual code starts with "A" so it is mistakenly taken as Open Uni course
|
||||
const isAvoin = course => !!course.code.match(/^[A][0-9]|^AY/)
|
||||
// const isAvoin = course => !!course.code.match(/^[A][0-9]|^AY/)
|
||||
|
||||
const sortAlternatives = alternatives => {
|
||||
/* const sortAlternatives = alternatives => {
|
||||
const result = orderBy(
|
||||
alternatives,
|
||||
[
|
||||
@@ -20,17 +20,18 @@ const sortAlternatives = alternatives => {
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
} */
|
||||
|
||||
// If special case of Open Uni course that starts with A let's just sort them length, that
|
||||
// should be good enough in this case.
|
||||
const getAlternatives = course => sortAlternatives(course.alternatives)
|
||||
|
||||
const filterCourseSearchResults = (courses, unifyOpenUniCourses) => {
|
||||
/* const getAlternatives = course => sortAlternatives(course.alternatives)
|
||||
|
||||
const filterCourseSearchResults = courses => {
|
||||
const mergedCourses = {}
|
||||
const sortedCourses = sortAlternatives(courses)
|
||||
sortedCourses.forEach(course => {
|
||||
const groupId = isAvoin(course) && !unifyOpenUniCourses ? course.code : course.subsId
|
||||
const groupId = course.subsId
|
||||
|
||||
if (!(course.max_attainment_date && course.min_attainment_date)) {
|
||||
return
|
||||
@@ -64,6 +65,4 @@ const filterCourseSearchResults = (courses, unifyOpenUniCourses) => {
|
||||
}))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default filterCourseSearchResults
|
||||
} */
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Segment, Label, Header, Divider, Form } from 'semantic-ui-react'
|
||||
import { shape, arrayOf, oneOfType, number, string, bool } from 'prop-types'
|
||||
import { oneOfType, number, string, bool } from 'prop-types'
|
||||
import SingleCourseStats from '../SingleCourseStats'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
import selectors from '../../../selectors/courseStats'
|
||||
import { getTextIn } from '../../../common'
|
||||
|
||||
const SingleCourseTab = ({ selected, stats, courses, userHasAccessToAllStats }) => {
|
||||
const SingleCourseTab = ({ selected, userHasAccessToAllStats }) => {
|
||||
const [selection, setSelection] = useState(selected)
|
||||
const { language } = useLanguage()
|
||||
const stats = useSelector(selectors.getCourseStats)
|
||||
const courses = useSelector(selectors.getCourses).map(({ code, name }) => ({
|
||||
key: code,
|
||||
value: code,
|
||||
text: <Header content={<>{name}</>} />,
|
||||
content: name,
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
setSelection(selected)
|
||||
@@ -50,20 +57,8 @@ const SingleCourseTab = ({ selected, stats, courses, userHasAccessToAllStats })
|
||||
}
|
||||
|
||||
SingleCourseTab.propTypes = {
|
||||
stats: shape({}).isRequired,
|
||||
courses: arrayOf(shape({})).isRequired,
|
||||
selected: oneOfType([number, string]).isRequired,
|
||||
userHasAccessToAllStats: bool.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
stats: selectors.getCourseStats(state),
|
||||
courses: selectors.getCourses(state).map(({ code, name }) => ({
|
||||
key: code,
|
||||
value: code,
|
||||
text: <Header content={<>{name}</>} />,
|
||||
content: name,
|
||||
})),
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, {})(SingleCourseTab)
|
||||
export default SingleCourseTab
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -76,7 +76,11 @@ const GaugeChart = ({ cypress, data, year, graduationAmount, studyprogramme }) =
|
||||
],
|
||||
}
|
||||
|
||||
return <ReactHighcharts data-cy={`Graph-${cypress}`} config={defaultConfig} />
|
||||
return (
|
||||
<div data-cy={`Graph-${cypress}`}>
|
||||
<ReactHighcharts config={defaultConfig} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GaugeChart
|
||||
|
||||
@@ -1,32 +1,18 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Divider, Loader, Message, Radio } from 'semantic-ui-react'
|
||||
import { Divider, Loader, Message } from 'semantic-ui-react'
|
||||
|
||||
import { useGetBasicStatsQuery, useGetCreditStatsQuery, useGetGraduationStatsQuery } from 'redux/studyProgramme'
|
||||
import WithHelpTooltip from '../../Info/InfoWithHelpTooltip'
|
||||
import LineGraph from './LineGraph'
|
||||
import StackedBarChart from './StackedBarChart'
|
||||
import BarChart from './BarChart'
|
||||
import GaugeChart from './GaugeChart'
|
||||
import DataTable from './DataTable'
|
||||
import Toggle from '../Toggle'
|
||||
import InfoBox from '../../Info/InfoBox'
|
||||
|
||||
import InfotoolTips from '../../../common/InfoToolTips'
|
||||
import '../studyprogramme.css'
|
||||
|
||||
const getRadioButton = (cypress, toolTip, firstLabel, secondLabel, value, setValue) => (
|
||||
<div className="radio-toggle">
|
||||
<label className="toggle-label">{firstLabel}</label>
|
||||
<Radio data-cy={cypress} toggle checked={value} onChange={() => setValue(!value)} />
|
||||
{toolTip ? (
|
||||
<WithHelpTooltip tooltip={{ short: toolTip }}>
|
||||
<label className="toggle-label">{secondLabel}</label>
|
||||
</WithHelpTooltip>
|
||||
) : (
|
||||
<label className="toggle-label">{secondLabel}</label>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYear, setAcademicYear }) => {
|
||||
const [showMeanTime, setShowMeanTime] = useState(true)
|
||||
const toolTips = InfotoolTips.Studyprogramme
|
||||
@@ -69,22 +55,22 @@ const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYea
|
||||
return (
|
||||
<div className="studyprogramme-overview">
|
||||
<div className="toggle-container">
|
||||
{getRadioButton(
|
||||
'YearToggle',
|
||||
toolTips.YearToggle,
|
||||
'Calendar year ',
|
||||
'Academic year',
|
||||
academicYear,
|
||||
setAcademicYear
|
||||
)}
|
||||
{getRadioButton(
|
||||
'StudentToggle',
|
||||
toolTips.StudentToggle,
|
||||
'All studyrights included',
|
||||
'Special studyrights excluded',
|
||||
specialGroups,
|
||||
setSpecialGroups
|
||||
)}
|
||||
<Toggle
|
||||
cypress="YearToggle"
|
||||
toolTips={toolTips.YearToggle}
|
||||
firstLabel="Calendar year"
|
||||
secondLabel="Academic year"
|
||||
value={academicYear}
|
||||
setValue={setAcademicYear}
|
||||
/>
|
||||
<Toggle
|
||||
cypress="StudentToggle"
|
||||
toolTips={toolTips.StudentToggle}
|
||||
firstLabel="All studyrights"
|
||||
secondLabel="Special studyrights excluded"
|
||||
value={specialGroups}
|
||||
setValue={setSpecialGroups}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isFetchingOrLoading ? (
|
||||
@@ -138,10 +124,17 @@ const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYea
|
||||
/>
|
||||
</div>
|
||||
{getDivider('Average graduation times', 'AverageGraduationTimes')}
|
||||
{getRadioButton('GraduationTimeToggle', null, 'Mean time', 'Median time', showMeanTime, setShowMeanTime)}
|
||||
<Toggle
|
||||
cypress="GraduationTimeToggle"
|
||||
firstLabel="Mean time"
|
||||
secondLabel="Median time"
|
||||
value={showMeanTime}
|
||||
setValue={setShowMeanTime}
|
||||
/>
|
||||
<div className="section-container-centered">
|
||||
{graduations?.data?.years.map(year => (
|
||||
<GaugeChart
|
||||
cypress={`${year}-AverageGraduationTimes`}
|
||||
key={year}
|
||||
year={year}
|
||||
data={
|
||||
@@ -155,24 +148,27 @@ const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYea
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{studyprogramme.includes('KH') && (
|
||||
<>
|
||||
{getDivider('Primary master programme studies after this programme', 'ProgrammesAfterGraduation')}
|
||||
<div className="section-container">
|
||||
<StackedBarChart
|
||||
cypress="ProgrammesAfterGraduation"
|
||||
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>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
+8
-10
@@ -1,8 +1,9 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Icon, Radio, Table } from 'semantic-ui-react'
|
||||
import { Icon, Table } from 'semantic-ui-react'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
import PopulationLink from './PopulationLink'
|
||||
import Toggle from '../Toggle'
|
||||
|
||||
const getKey = year => `${year}-${Math.random()}`
|
||||
|
||||
@@ -124,14 +125,6 @@ const sortMainDataByYear = data => {
|
||||
return sortedData
|
||||
}
|
||||
|
||||
const getRadioButton = (toolTip, firstLabel, secondLabel, value, setValue) => (
|
||||
<div className="radio-toggle">
|
||||
<label className="toggle-label">{firstLabel}</label>
|
||||
<Radio toggle checked={value} onChange={() => setValue(!value)} />
|
||||
<label className="toggle-label">{secondLabel}</label>
|
||||
</div>
|
||||
)
|
||||
|
||||
const StudytrackDataTable = ({
|
||||
studyprogramme,
|
||||
dataOfAllTracks,
|
||||
@@ -151,7 +144,12 @@ const StudytrackDataTable = ({
|
||||
|
||||
return (
|
||||
<div className="datatable">
|
||||
{getRadioButton(null, 'Show percentages', 'Hide percentages', hidePercentages, setHidePercentages)}
|
||||
<Toggle
|
||||
firstLabel="Show percentages"
|
||||
secondLabel="Hide percentages"
|
||||
value={hidePercentages}
|
||||
setValue={setHidePercentages}
|
||||
/>
|
||||
<Table data-cy="Table-StudytrackOverview" celled>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Divider, Loader, Radio } from 'semantic-ui-react'
|
||||
import { Divider, Loader } from 'semantic-ui-react'
|
||||
|
||||
import { useGetStudytrackStatsQuery } from 'redux/studyProgramme'
|
||||
import WithHelpTooltip from '../../Info/InfoWithHelpTooltip'
|
||||
import InfoBox from '../../Info/InfoBox'
|
||||
import BarChart from './BarChart'
|
||||
import BasicDataTable from './BasicDataTable'
|
||||
import GaugeChart from './GaugeChart'
|
||||
import StudytrackDataTable from './StudytrackDataTable'
|
||||
import StudytrackSelector from './StudytrackSelector'
|
||||
import Toggle from '../Toggle'
|
||||
|
||||
import InfotoolTips from '../../../common/InfoToolTips'
|
||||
import '../studyprogramme.css'
|
||||
|
||||
const getRadioButton = (toolTip, firstLabel, secondLabel, value, setValue) => (
|
||||
<div className="radio-toggle">
|
||||
<label className="toggle-label">{firstLabel}</label>
|
||||
<Radio toggle checked={value} onChange={() => setValue(!value)} />
|
||||
<WithHelpTooltip tooltip={{ short: toolTip }}>
|
||||
<label className="toggle-label">{secondLabel}</label>
|
||||
</WithHelpTooltip>
|
||||
</div>
|
||||
)
|
||||
|
||||
const StudytrackOverview = ({ studyprogramme, specialGroups, setSpecialGroups, graduated, setGraduated }) => {
|
||||
const toolTips = InfotoolTips.Studyprogramme
|
||||
const [showMeanTime, setShowMeanTime] = useState(true)
|
||||
@@ -60,14 +50,22 @@ const StudytrackOverview = ({ studyprogramme, specialGroups, setSpecialGroups, g
|
||||
<>
|
||||
<StudytrackSelector track={track} setTrack={setTrack} studytracks={stats?.data?.studytrackOptions} />
|
||||
<div className="toggle-container">
|
||||
{getRadioButton(
|
||||
toolTips.StudentToggle,
|
||||
'All studyrights included',
|
||||
'Special studyrights excluded',
|
||||
specialGroups,
|
||||
setSpecialGroups
|
||||
)}
|
||||
{getRadioButton(toolTips.GradToggle, 'Graduated included', 'Graduated excluded', graduated, setGraduated)}
|
||||
<Toggle
|
||||
cypress="StudentToggle"
|
||||
toolTips={toolTips.StudentToggle}
|
||||
firstLabel="All studyrights"
|
||||
secondLabel="Special studyrights excluded"
|
||||
value={specialGroups}
|
||||
setValue={setSpecialGroups}
|
||||
/>
|
||||
<Toggle
|
||||
cypress="GraduatedToggle"
|
||||
toolTips={toolTips.GraduatedToggle}
|
||||
firstLabel="Graduated included"
|
||||
secondLabel="Graduated excluded"
|
||||
value={graduated}
|
||||
setValue={setGraduated}
|
||||
/>
|
||||
</div>
|
||||
{getDivider(
|
||||
`Students of ${
|
||||
@@ -95,7 +93,7 @@ const StudytrackOverview = ({ studyprogramme, specialGroups, setSpecialGroups, g
|
||||
'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}
|
||||
@@ -106,10 +104,16 @@ const StudytrackOverview = ({ studyprogramme, specialGroups, setSpecialGroups, g
|
||||
{stats?.isSuccess && stats?.data?.includeGraduated && (
|
||||
<>
|
||||
{getDivider('Average graduation times', 'AverageGraduationTimes')}
|
||||
{getRadioButton(null, 'Mean time', 'Median time', showMeanTime, setShowMeanTime)}
|
||||
<Toggle
|
||||
firstLabel="Mean time"
|
||||
secondLabel="Median time"
|
||||
value={showMeanTime}
|
||||
setValue={setShowMeanTime}
|
||||
/>
|
||||
<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]}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import { Radio } from 'semantic-ui-react'
|
||||
|
||||
import WithHelpTooltip from '../Info/InfoWithHelpTooltip'
|
||||
|
||||
const Toggle = ({ cypress, toolTips, firstLabel, secondLabel, value, setValue }) => (
|
||||
<div className="radio-toggle">
|
||||
<label className={`toggle-label${value ? '-checked' : ''}`}>{firstLabel}</label>
|
||||
<Radio data-cy={cypress} toggle checked={value} onChange={() => setValue(!value)} />
|
||||
{toolTips ? (
|
||||
<WithHelpTooltip tooltip={{ short: toolTips }}>
|
||||
<label className={`toggle-label${value ? '' : '-checked'}`}>{secondLabel}</label>
|
||||
</WithHelpTooltip>
|
||||
) : (
|
||||
<label className={`toggle-label${value ? '' : '-checked'}`}>{secondLabel}</label>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Toggle
|
||||
@@ -39,6 +39,22 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.studytrack-overview .toggle-label-checked,
|
||||
.studyprogramme-overview .toggle-label-checked {
|
||||
cursor: pointer;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
font-weight: bold;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.studytrack-overview .ui.toggle.checkbox input:checked ~ label::before,
|
||||
.studyprogramme-overview .ui.toggle.checkbox input:checked ~ label::before,
|
||||
.studytrack-overview .ui.toggle.checkbox input:focus:checked ~ label::before,
|
||||
.studyprogramme-overview .ui.toggle.checkbox input:focus:checked ~ label::before {
|
||||
background-color: rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.studyprogramme-overview .divider {
|
||||
margin-top: 4em;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// import { actions } from './common/itemreducer'
|
||||
import { callController } from '../apiConnection/index'
|
||||
|
||||
const prefix = 'GET_COURSE_SEARCH_RESULT_'
|
||||
|
||||
// const storeActions = actions(prefix)
|
||||
|
||||
// export const clearCourses = storeActions.clear
|
||||
export const clearCourses = () => ({ type: 'CLEAR_SEARCH_RESULTS' })
|
||||
|
||||
export const findCourses = ({ name, type }, language = 'fi') => {
|
||||
@@ -20,11 +16,14 @@ export const findCoursesV2 = ({ name, code }) => {
|
||||
return callController(route, prefix, [], 'get', params, params)
|
||||
}
|
||||
|
||||
export const toggleUnifyOpenUniCourses = () => ({
|
||||
type: 'TOGGLE_UNIFY_OPEN_UNI_COURSES',
|
||||
})
|
||||
export const toggleOpenAndReqularCourses = showType => {
|
||||
return {
|
||||
type: 'TOGGLE_OPEN_AND_REQULAR_COURSES',
|
||||
showType,
|
||||
}
|
||||
}
|
||||
|
||||
const reducer = (state = { data: {}, pending: false, unifyOpenUniCourses: false }, action) => {
|
||||
const reducer = (state = { data: {}, pending: false, openOrReqular: 'unifyStats' }, action) => {
|
||||
switch (action.type) {
|
||||
case 'GET_COURSE_SEARCH_RESULT_ATTEMPT':
|
||||
return {
|
||||
@@ -48,16 +47,19 @@ const reducer = (state = { data: {}, pending: false, unifyOpenUniCourses: false
|
||||
? action.response
|
||||
: state.data,
|
||||
}
|
||||
case 'TOGGLE_UNIFY_OPEN_UNI_COURSES':
|
||||
|
||||
case 'TOGGLE_OPEN_AND_REQULAR_COURSES':
|
||||
return {
|
||||
...state,
|
||||
unifyOpenUniCourses: !state.unifyOpenUniCourses,
|
||||
openOrReqular: action.showType,
|
||||
}
|
||||
|
||||
case 'CLEAR_SEARCH_RESULTS':
|
||||
return {
|
||||
...state,
|
||||
data: {},
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const getCourseStats = ({ courseCodes, separate, unifyOpenUniCourses }, o
|
||||
const responseToObj = coursestats => {
|
||||
const data = {}
|
||||
coursestats.forEach(stat => {
|
||||
data[stat.coursecode] = stat
|
||||
data[stat.unifyStats.coursecode] = stat
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@ import { createSelector } from '@reduxjs/toolkit'
|
||||
import { sortBy, flatten } from 'lodash'
|
||||
|
||||
const courseStatsSelector = state => state.courseStats.data
|
||||
const openOrReqularSelector = state => state.courseSearch.openOrReqular
|
||||
const singleCourseStatsSelector = state => state.singleCourseStats
|
||||
|
||||
const getCourseStats = createSelector([courseStatsSelector], courseStats => {
|
||||
const getCourseStats = createSelector([courseStatsSelector, openOrReqularSelector], (courseStats, openOrReqular) => {
|
||||
const stats = {}
|
||||
|
||||
Object.entries(courseStats).forEach(entry => {
|
||||
const [coursecode, data] = entry
|
||||
const [coursecode] = entry
|
||||
const data = entry[1][openOrReqular]
|
||||
const { statistics } = data
|
||||
stats[coursecode] = {
|
||||
...data,
|
||||
@@ -16,6 +20,13 @@ const getCourseStats = createSelector([courseStatsSelector], courseStats => {
|
||||
return stats
|
||||
})
|
||||
|
||||
const getCourseAlternatives = createSelector(
|
||||
[courseStatsSelector, openOrReqularSelector, singleCourseStatsSelector],
|
||||
(courseStats, openOrReqular, singleCourseStats) => {
|
||||
return courseStats[singleCourseStats.selectedCourse][openOrReqular].alternatives
|
||||
}
|
||||
)
|
||||
|
||||
const selectedCourseSelector = state => state.singleCourseStats.selectedCourse
|
||||
|
||||
const getQueryInfo = createSelector([getCourseStats], stats => {
|
||||
@@ -185,6 +196,7 @@ const getCourses = createSelector(getCourseStats, stats =>
|
||||
export default {
|
||||
getCourseStats,
|
||||
getCourses,
|
||||
getCourseAlternatives,
|
||||
getAllStudyProgrammes,
|
||||
summaryStatistics,
|
||||
ALL,
|
||||
|
||||
@@ -1,9 +1,73 @@
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import { orderBy } from 'lodash'
|
||||
import { byDateDesc, reformatDate } from '../common'
|
||||
|
||||
const getData = obj => obj
|
||||
const sortAlternatives = alternatives => {
|
||||
const result = orderBy(
|
||||
alternatives,
|
||||
[
|
||||
a => {
|
||||
if (a.code.match(/^A/)) return 4 // open university codes come last
|
||||
if (a.code.match(/^\d/)) return 2 // old numeric codes come second
|
||||
if (a.code.match(/^[A-Za-z]/)) return 1 // new letter based codes come first
|
||||
return 3 // unknown, comes before open uni?
|
||||
},
|
||||
a => a.latestInstanceDate || new Date(),
|
||||
'code',
|
||||
],
|
||||
['asc', 'desc', 'desc']
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// If special case of Open Uni course that starts with A let's just sort them length, that
|
||||
// should be good enough in this case.
|
||||
const getAlternatives = course => sortAlternatives(course.alternatives)
|
||||
|
||||
const filterCourseSearchResults = (courses /* unifyOpenUniCourses */) => {
|
||||
const mergedCourses = {}
|
||||
const sortedCourses = sortAlternatives(courses)
|
||||
sortedCourses.forEach(course => {
|
||||
const groupId = /* isAvoin(course) && !unifyOpenUniCourses ? course.code : */ course.subsId
|
||||
|
||||
if (!(course.max_attainment_date && course.min_attainment_date)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!mergedCourses[groupId]) {
|
||||
mergedCourses[groupId] = {
|
||||
...course,
|
||||
alternatives: [{ code: course.code, latestInstanceDate: new Date(course.latest_instance_date) }],
|
||||
min_attainment_date: new Date(course.min_attainment_date),
|
||||
max_attainment_date: new Date(course.max_attainment_date),
|
||||
}
|
||||
} else {
|
||||
const mergedCourse = mergedCourses[groupId]
|
||||
mergedCourse.min_attainment_date = new Date(
|
||||
Math.min(mergedCourse.min_attainment_date, new Date(course.min_attainment_date))
|
||||
)
|
||||
mergedCourse.max_attainment_date = new Date(
|
||||
Math.max(mergedCourse.max_attainment_date, new Date(course.max_attainment_date))
|
||||
)
|
||||
mergedCourse.alternatives.push({
|
||||
code: course.code,
|
||||
latestInstanceDate: new Date(course.latest_instance_date),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const result = Object.values(mergedCourses).map(course => ({
|
||||
...course,
|
||||
alternatives: getAlternatives(course),
|
||||
}))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const getData = obj => obj
|
||||
const getInstanceData = obj => obj.data
|
||||
const getCourseSearchSelector = state => state.courseSearch.data.courses
|
||||
|
||||
export const sortInstances = courseInstances => {
|
||||
if (courseInstances) {
|
||||
@@ -31,11 +95,9 @@ export const makeSortCourseInstances = () => createSelector(getInstanceData, sor
|
||||
|
||||
export const makeSortCourses = () => createSelector(getData, sortCourses)
|
||||
|
||||
export const getCourseSearchResults = state =>
|
||||
state.courseSearch.data.courses
|
||||
? {
|
||||
courses: state.courseSearch.data.courses,
|
||||
}
|
||||
: {
|
||||
courses: [],
|
||||
}
|
||||
export const getCourseSearchResults = createSelector(getCourseSearchSelector, courses => {
|
||||
if (!courses) {
|
||||
return []
|
||||
}
|
||||
return filterCourseSearchResults(courses)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user