mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a16896e74d | ||
|
|
814599d6ea | ||
|
|
8f2dcac0f9 | ||
|
|
240659afc9 | ||
|
|
b9059e2a52 | ||
|
|
a1688f7cdd | ||
|
|
2c378bfca2 | ||
|
|
aec495b161 | ||
|
|
486bc1a194 | ||
|
|
18bcb91d09 | ||
|
|
5c4d73f555 | ||
|
|
27e8450789 | ||
|
|
56733f2445 | ||
|
|
089b00fcd6 | ||
|
|
a709fe70ba | ||
|
|
f26e4b4859 | ||
|
|
18a382167b | ||
|
|
b4be0a3e6b | ||
|
|
596ac08ad4 | ||
|
|
9c6ad0027a | ||
|
|
27ce374f35 | ||
|
|
7e6f311b62 | ||
|
|
dd77fdff9e | ||
|
|
0b7adf4888 | ||
|
|
9deee8a384 | ||
|
|
81791381a5 | ||
|
|
9806639c1b | ||
|
|
5e9a3c4b99 | ||
|
|
12f73f4dee | ||
|
|
9d27dfebe5 | ||
|
|
7bebd4c845 | ||
|
|
43d2e84b32 | ||
|
|
ee1cb395a2 | ||
|
|
4e221d8867 | ||
|
|
3aab7b74a3 | ||
|
|
92651b8bc7 | ||
|
|
08b53c34f2 | ||
|
|
8d6a311936 | ||
|
|
79b8b92b66 | ||
|
|
fa8a07481a | ||
|
|
f79d6ef0fb | ||
|
|
bb25541202 | ||
|
|
3530723bd5 | ||
|
|
f2ffaacc6c | ||
|
|
14e95fe4df | ||
|
|
f69b12a1f9 | ||
|
|
e4e527eaeb | ||
|
|
c4c8f6123b | ||
|
|
bdb71242a7 | ||
|
|
a3a36fcef1 | ||
|
|
adbcb1bb88 | ||
|
|
37504f9e97 | ||
|
|
1722bcd7a3 | ||
|
|
a770873da5 | ||
|
|
a4c3385956 | ||
|
|
9fe491e7cc | ||
|
|
42213948b7 | ||
|
|
1014348e5b | ||
|
|
dcecd66e49 | ||
|
|
4509e51f66 | ||
|
|
90841369bb | ||
|
|
c3e41c8a60 | ||
|
|
81ffbf653e | ||
|
|
b46e30c8fa | ||
|
|
3619698744 | ||
|
|
81ee6b8146 | ||
|
|
c9b27fdc64 | ||
|
|
6b50c07ed7 | ||
|
|
d2670e180c | ||
|
|
580e1b4ed0 | ||
|
|
e84e487307 | ||
|
|
d0a54c95a6 | ||
|
|
18ac10f399 | ||
|
|
263e070b13 | ||
|
|
8cf9ceac6e | ||
|
|
32005ce23f | ||
|
|
111a397811 | ||
|
|
454032368a | ||
|
|
5509cd4b9a | ||
|
|
346c9f9803 | ||
|
|
0a738df3d0 | ||
|
|
b67da1c2c4 | ||
|
|
e700586588 | ||
|
|
9bb7c1c6f1 | ||
|
|
7f70968531 | ||
|
|
9ae35e46db | ||
|
|
3d667fe1c6 | ||
|
|
62e07ce571 | ||
|
|
38cd587369 | ||
|
|
77c9801e4c | ||
|
|
d3f1e101ce | ||
|
|
10a2d2743e | ||
|
|
4d26f42f25 | ||
|
|
e8f3d9f601 | ||
|
|
6f3af6f135 | ||
|
|
3e08836d4e |
@@ -13,58 +13,127 @@ describe('Faculty overview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty basic information', () => {
|
||||
describe('Faculty basic information: admin user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.contains('td', 'H90').click()
|
||||
})
|
||||
it('Credits produced by faculty are shown', () => {
|
||||
cy.get(['data-cy=Section-CreditsProducedByTheFaculty'])
|
||||
cy.get(['data-cy=Graph-CreditsProducedByTheFaculty'])
|
||||
cy.get('[data-cy="Section-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Update statistics tab is shown', () => {
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Update statistics')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Study programme information', () => {
|
||||
describe('Faculty basic information: basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.init('/faculties')
|
||||
cy.contains('td', 'H80').click()
|
||||
})
|
||||
it('Basic information tab show all graphs and tables', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-ThesisWritersOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-StudentsOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-GraduatedOfTheFaculty"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Correct tabs are shown', () => {
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Basic information')
|
||||
// Fix these when access is gained for facultyStatistics users
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Programmes and student populations')
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Graduation times')
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Update statistics')
|
||||
})
|
||||
|
||||
it('Toggle years works', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="YearToggle"]').click()
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
})
|
||||
|
||||
it('Toggle programmes works', () => {
|
||||
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('not.exist')
|
||||
cy.get('[data-cy="ProgrammeToggle"]').click()
|
||||
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Students of the faculty infobox works', () => {
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-open-info"]').click()
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('be.visible')
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-close-info"]').click()
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Study programme information: users with access', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('td', 'H80').click()
|
||||
})
|
||||
|
||||
it('Study programme credit information is not visible in the beginning', () => {
|
||||
cy.get('table[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('Study programme credit information can be toggled', () => {
|
||||
cy.get('table[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
|
||||
cy.get('div[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
cy.get('div[data-cy="Button-Show-CreditsProducedByTheFaculty-3"]').click()
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
cy.get('div[data-cy="Button-Hide-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
// I tried to fix this without cy.wait() method without effort. Sorting the programmes
|
||||
// or something else causes rerendering of the view and yield to failing tests.
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-3"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Hide-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('Graph stays open when sorted', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Menu-CreditsProducedByTheFaculty-Total"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -223,7 +223,7 @@ describe('Population Statistics tests', () => {
|
||||
cy.init(pathToCSBach2017, 'onlyiamrights')
|
||||
})
|
||||
|
||||
it.skip('Population statistics is visible', () => {
|
||||
it('Population statistics is visible', () => {
|
||||
cy.reload()
|
||||
|
||||
cy.get('.card').within(() => {
|
||||
@@ -238,7 +238,7 @@ describe('Population Statistics tests', () => {
|
||||
cy.contains('Credit statistics')
|
||||
})
|
||||
|
||||
it.skip('Students tab is not available', () => {
|
||||
it('Students tab is not available', () => {
|
||||
cy.contains('Students (170)').should('not.exist')
|
||||
// TODO: somehow check that obfuscation works
|
||||
})
|
||||
|
||||
@@ -332,11 +332,11 @@ describe('Studyprogramme overview', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
const tableContents = [
|
||||
['2020 - 2021', 12, 5, 2, 2, 0, 2, 0, 1],
|
||||
['2019 - 2020', 86, 13, 15, 32, 11, 2, 0, 13],
|
||||
['2018 - 2019', 162, 7, 16, 35, 33, 27, 18, 26],
|
||||
['2017 - 2018', 171, 10, 22, 18, 22, 24, 27, 48],
|
||||
['Total', 431, 35, 55, 87, 66, 55, 45, 88],
|
||||
['2020 - 2021', 12, 12, 0, 0, 0, 0, 0, 0],
|
||||
['2019 - 2020', 86, 26, 34, 21, 5, 0, 0, 0],
|
||||
['2018 - 2019', 162, 10, 27, 47, 36, 26, 12, 4],
|
||||
['2017 - 2018', 171, 20, 21, 26, 22, 22, 33, 27],
|
||||
['Total', 431, 51, 64, 97, 79, 55, 51, 34],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackProgress')
|
||||
@@ -350,9 +350,9 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', 'Less than 30 credits')
|
||||
.should('contain', '30-59 credits')
|
||||
.should('contain', 'More than 180 credits')
|
||||
.should('contain', '5.8%') // The percentage for less than 30 credits in 2017-2018, to check that the graph renders
|
||||
.should('contain', '4.3%') // The percentage for less than 30 credits in 2018-2019
|
||||
.should('contain', '15.1%') // The percentage for less than 30 credits in 2019-2020
|
||||
.should('contain', '11.7%') // The percentage for less than 30 credits in 2017-2018, to check that the graph renders
|
||||
.should('contain', '6.2%') // The percentage for less than 30 credits in 2018-2019
|
||||
.should('contain', '30.2%') // The percentage for less than 30 credits in 2019-2020
|
||||
|
||||
cy.get('[data-cy=Graph-2018-AverageGraduationTimes]')
|
||||
.should('contain', '2018 - 2019')
|
||||
@@ -411,7 +411,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
// Test sorting by different columns
|
||||
cy.get('th').eq(0).click()
|
||||
cy.get('tr').eq(1).contains('Tietojenkäsittelytieteen kisälliopetus: Java-ohjelmointi')
|
||||
cy.get('tr').eq(2).contains('Tietojenkäsittelytieteen kisälliopetus: Java-ohjelmointi')
|
||||
cy.get('th').eq(1).click()
|
||||
cy.get('tr').eq(1).contains('Äidinkielinen viestintä')
|
||||
cy.get('th').eq(2).click()
|
||||
@@ -505,7 +505,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.init('/study-programme', 'onlyiamrights')
|
||||
})
|
||||
|
||||
it.skip('can access programme and correct tabs are visible', () => {
|
||||
it('can access programme and correct tabs are visible', () => {
|
||||
cy.reload()
|
||||
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
@@ -517,7 +517,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.contains('Degree Courses').should('not.exist')
|
||||
})
|
||||
|
||||
it.skip('can access basic information', () => {
|
||||
it('can access basic information', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.contains('Basic information').click()
|
||||
|
||||
@@ -528,7 +528,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
|
||||
it.skip('can access studytracks', () => {
|
||||
it('can access studytracks', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
|
||||
|
||||
Generated
+154
-213
@@ -11,12 +11,12 @@
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.10",
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/eslint-parser": "^7.17.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.16.7",
|
||||
"concurrently": "^7.3.0",
|
||||
"cypress": "^10.4.0",
|
||||
"eslint": "^8.21.0",
|
||||
"cypress": "^10.6.0",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
@@ -25,13 +25,13 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prefer-object-spread": "^1.2.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.29.3",
|
||||
"eslint-plugin-react": "^7.31.1",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"prettier": "^2.5.1",
|
||||
"stylelint": "^14.9.1",
|
||||
"stylelint": "^14.11.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"stylelint-prettier": "^2.0.0"
|
||||
@@ -72,21 +72,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.18.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz",
|
||||
"integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==",
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.13.tgz",
|
||||
"integrity": "sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.1.0",
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/generator": "^7.18.10",
|
||||
"@babel/generator": "^7.18.13",
|
||||
"@babel/helper-compilation-targets": "^7.18.9",
|
||||
"@babel/helper-module-transforms": "^7.18.9",
|
||||
"@babel/helpers": "^7.18.9",
|
||||
"@babel/parser": "^7.18.10",
|
||||
"@babel/parser": "^7.18.13",
|
||||
"@babel/template": "^7.18.10",
|
||||
"@babel/traverse": "^7.18.10",
|
||||
"@babel/types": "^7.18.10",
|
||||
"@babel/traverse": "^7.18.13",
|
||||
"@babel/types": "^7.18.13",
|
||||
"convert-source-map": "^1.7.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -120,12 +120,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.18.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.10.tgz",
|
||||
"integrity": "sha512-0+sW7e3HjQbiHbj1NeU/vN8ornohYlacAfZIaXhdoGweQqgcNy69COVciYYqEXJ/v+9OBA7Frxm4CVAuNqKeNA==",
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.13.tgz",
|
||||
"integrity": "sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.18.10",
|
||||
"@babel/types": "^7.18.13",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"jsesc": "^2.5.1"
|
||||
},
|
||||
@@ -319,9 +319,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.18.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz",
|
||||
"integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==",
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
|
||||
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -385,19 +385,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.18.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz",
|
||||
"integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==",
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.13.tgz",
|
||||
"integrity": "sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/generator": "^7.18.10",
|
||||
"@babel/generator": "^7.18.13",
|
||||
"@babel/helper-environment-visitor": "^7.18.9",
|
||||
"@babel/helper-function-name": "^7.18.9",
|
||||
"@babel/helper-hoist-variables": "^7.18.6",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||
"@babel/parser": "^7.18.11",
|
||||
"@babel/types": "^7.18.10",
|
||||
"@babel/parser": "^7.18.13",
|
||||
"@babel/types": "^7.18.13",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
@@ -406,9 +406,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.18.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
|
||||
"integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
|
||||
"integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.18.10",
|
||||
@@ -495,14 +495,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
|
||||
"integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.1.tgz",
|
||||
"integrity": "sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^9.3.2",
|
||||
"espree": "^9.4.0",
|
||||
"globals": "^13.15.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
@@ -512,6 +512,9 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||
@@ -565,6 +568,19 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/module-importer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12.22"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/object-schema": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
|
||||
@@ -1297,27 +1313,6 @@
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-regexp": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz",
|
||||
"integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-regexp": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-regexp/node_modules/is-regexp": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz",
|
||||
"integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@@ -1334,9 +1329,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/colord": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
|
||||
"integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==",
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
|
||||
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
@@ -1573,9 +1568,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.4.0.tgz",
|
||||
"integrity": "sha512-OM7F8MRE01SHQRVVzunid1ZK1m90XTxYnl+7uZfIrB4CYqUDCrZEeSyCXzIbsS6qcaijVCAhqDL60SxG8N6hew==",
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.6.0.tgz",
|
||||
"integrity": "sha512-6sOpHjostp8gcLO34p6r/Ci342lBs8S5z9/eb3ZCQ22w2cIhMWGUoGKkosabPBfKcvRS9BE4UxybBtlIs8gTQA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
@@ -2009,14 +2004,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz",
|
||||
"integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==",
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.0.tgz",
|
||||
"integrity": "sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint/eslintrc": "^1.3.0",
|
||||
"@eslint/eslintrc": "^1.3.1",
|
||||
"@humanwhocodes/config-array": "^0.10.4",
|
||||
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"ajv": "^6.10.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
@@ -2026,7 +2022,7 @@
|
||||
"eslint-scope": "^7.1.1",
|
||||
"eslint-utils": "^3.0.0",
|
||||
"eslint-visitor-keys": "^3.3.0",
|
||||
"espree": "^9.3.3",
|
||||
"espree": "^9.4.0",
|
||||
"esquery": "^1.4.0",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -2051,8 +2047,7 @@
|
||||
"regexpp": "^3.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"strip-json-comments": "^3.1.0",
|
||||
"text-table": "^0.2.0",
|
||||
"v8-compile-cache": "^2.0.3"
|
||||
"text-table": "^0.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"eslint": "bin/eslint.js"
|
||||
@@ -2299,9 +2294,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react": {
|
||||
"version": "7.30.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.1.tgz",
|
||||
"integrity": "sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==",
|
||||
"version": "7.31.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.1.tgz",
|
||||
"integrity": "sha512-j4/2xWqt/R7AZzG8CakGHA6Xa/u7iR8Q3xCxY+AUghdT92bnIDOBEefV456OeH0QvBcroVc0eyvrrLSyQGYIfg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.5",
|
||||
@@ -2639,9 +2634,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "9.3.3",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz",
|
||||
"integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==",
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
|
||||
"integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.8.0",
|
||||
@@ -2735,18 +2730,6 @@
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/execall": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz",
|
||||
"integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"clone-regexp": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/executable": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
|
||||
@@ -2835,9 +2818,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fastest-levenshtein": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.14.tgz",
|
||||
"integrity": "sha512-tFfWHjnuUfKE186Tfgr+jtaFc0mZTApEgKDOeyN+FwOqRkO/zK/3h1AiRd8u8CY53owL3CUmGr/oI9p/RdyLTA==",
|
||||
"version": "1.0.16",
|
||||
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
|
||||
"integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4.9.1"
|
||||
@@ -3046,18 +3029,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stdin": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
|
||||
"integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
@@ -5309,9 +5280,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
|
||||
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
|
||||
"version": "8.4.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
|
||||
"integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -6170,22 +6141,20 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/stylelint": {
|
||||
"version": "14.9.1",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.9.1.tgz",
|
||||
"integrity": "sha512-RdAkJdPiLqHawCSnu21nE27MjNXaVd4WcOHA4vK5GtIGjScfhNnaOuWR2wWdfKFAvcWQPOYe311iveiVKSmwsA==",
|
||||
"version": "14.11.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.11.0.tgz",
|
||||
"integrity": "sha512-OTLjLPxpvGtojEfpESWM8Ir64Z01E89xsisaBMUP/ngOx1+4VG2DPRcUyCCiin9Rd3kPXPsh/uwHd9eqnvhsYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@csstools/selector-specificity": "^2.0.1",
|
||||
"@csstools/selector-specificity": "^2.0.2",
|
||||
"balanced-match": "^2.0.0",
|
||||
"colord": "^2.9.2",
|
||||
"colord": "^2.9.3",
|
||||
"cosmiconfig": "^7.0.1",
|
||||
"css-functions-list": "^3.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"execall": "^2.0.0",
|
||||
"fast-glob": "^3.2.11",
|
||||
"fastest-levenshtein": "^1.0.12",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"file-entry-cache": "^6.0.1",
|
||||
"get-stdin": "^8.0.0",
|
||||
"global-modules": "^2.0.0",
|
||||
"globby": "^11.1.0",
|
||||
"globjoin": "^0.1.4",
|
||||
@@ -6200,7 +6169,7 @@
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss": "^8.4.16",
|
||||
"postcss-media-query-parser": "^0.2.3",
|
||||
"postcss-resolve-nested-selector": "^0.1.1",
|
||||
"postcss-safe-parser": "^6.0.0",
|
||||
@@ -6214,7 +6183,7 @@
|
||||
"svg-tags": "^1.0.0",
|
||||
"table": "^6.8.0",
|
||||
"v8-compile-cache": "^2.3.0",
|
||||
"write-file-atomic": "^4.0.1"
|
||||
"write-file-atomic": "^4.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"stylelint": "bin/stylelint.js"
|
||||
@@ -6811,16 +6780,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/write-file-atomic": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz",
|
||||
"integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
|
||||
"integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"imurmurhash": "^0.1.4",
|
||||
"signal-exit": "^3.0.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16"
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
@@ -6933,21 +6902,21 @@
|
||||
"dev": true
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.18.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz",
|
||||
"integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==",
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.13.tgz",
|
||||
"integrity": "sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@ampproject/remapping": "^2.1.0",
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/generator": "^7.18.10",
|
||||
"@babel/generator": "^7.18.13",
|
||||
"@babel/helper-compilation-targets": "^7.18.9",
|
||||
"@babel/helper-module-transforms": "^7.18.9",
|
||||
"@babel/helpers": "^7.18.9",
|
||||
"@babel/parser": "^7.18.10",
|
||||
"@babel/parser": "^7.18.13",
|
||||
"@babel/template": "^7.18.10",
|
||||
"@babel/traverse": "^7.18.10",
|
||||
"@babel/types": "^7.18.10",
|
||||
"@babel/traverse": "^7.18.13",
|
||||
"@babel/types": "^7.18.13",
|
||||
"convert-source-map": "^1.7.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -6967,12 +6936,12 @@
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.18.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.10.tgz",
|
||||
"integrity": "sha512-0+sW7e3HjQbiHbj1NeU/vN8ornohYlacAfZIaXhdoGweQqgcNy69COVciYYqEXJ/v+9OBA7Frxm4CVAuNqKeNA==",
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.13.tgz",
|
||||
"integrity": "sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.18.10",
|
||||
"@babel/types": "^7.18.13",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"jsesc": "^2.5.1"
|
||||
},
|
||||
@@ -7117,9 +7086,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.18.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz",
|
||||
"integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==",
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
|
||||
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/plugin-syntax-jsx": {
|
||||
@@ -7162,27 +7131,27 @@
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.18.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz",
|
||||
"integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==",
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.13.tgz",
|
||||
"integrity": "sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/generator": "^7.18.10",
|
||||
"@babel/generator": "^7.18.13",
|
||||
"@babel/helper-environment-visitor": "^7.18.9",
|
||||
"@babel/helper-function-name": "^7.18.9",
|
||||
"@babel/helper-hoist-variables": "^7.18.6",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||
"@babel/parser": "^7.18.11",
|
||||
"@babel/types": "^7.18.10",
|
||||
"@babel/parser": "^7.18.13",
|
||||
"@babel/types": "^7.18.13",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.18.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
|
||||
"integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
|
||||
"version": "7.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
|
||||
"integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-string-parser": "^7.18.10",
|
||||
@@ -7252,14 +7221,14 @@
|
||||
}
|
||||
},
|
||||
"@eslint/eslintrc": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
|
||||
"integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.1.tgz",
|
||||
"integrity": "sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^9.3.2",
|
||||
"espree": "^9.4.0",
|
||||
"globals": "^13.15.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
@@ -7302,6 +7271,12 @@
|
||||
"integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==",
|
||||
"dev": true
|
||||
},
|
||||
"@humanwhocodes/module-importer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
||||
"dev": true
|
||||
},
|
||||
"@humanwhocodes/object-schema": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
|
||||
@@ -7829,23 +7804,6 @@
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"clone-regexp": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz",
|
||||
"integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-regexp": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-regexp": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz",
|
||||
"integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@@ -7862,9 +7820,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"colord": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
|
||||
"integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==",
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
|
||||
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
|
||||
"dev": true
|
||||
},
|
||||
"colorette": {
|
||||
@@ -8043,9 +8001,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"cypress": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.4.0.tgz",
|
||||
"integrity": "sha512-OM7F8MRE01SHQRVVzunid1ZK1m90XTxYnl+7uZfIrB4CYqUDCrZEeSyCXzIbsS6qcaijVCAhqDL60SxG8N6hew==",
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.6.0.tgz",
|
||||
"integrity": "sha512-6sOpHjostp8gcLO34p6r/Ci342lBs8S5z9/eb3ZCQ22w2cIhMWGUoGKkosabPBfKcvRS9BE4UxybBtlIs8gTQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^2.88.10",
|
||||
@@ -8382,14 +8340,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"eslint": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz",
|
||||
"integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==",
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.0.tgz",
|
||||
"integrity": "sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint/eslintrc": "^1.3.0",
|
||||
"@eslint/eslintrc": "^1.3.1",
|
||||
"@humanwhocodes/config-array": "^0.10.4",
|
||||
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"ajv": "^6.10.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
@@ -8399,7 +8358,7 @@
|
||||
"eslint-scope": "^7.1.1",
|
||||
"eslint-utils": "^3.0.0",
|
||||
"eslint-visitor-keys": "^3.3.0",
|
||||
"espree": "^9.3.3",
|
||||
"espree": "^9.4.0",
|
||||
"esquery": "^1.4.0",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -8424,8 +8383,7 @@
|
||||
"regexpp": "^3.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"strip-json-comments": "^3.1.0",
|
||||
"text-table": "^0.2.0",
|
||||
"v8-compile-cache": "^2.0.3"
|
||||
"text-table": "^0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
@@ -8743,9 +8701,9 @@
|
||||
}
|
||||
},
|
||||
"eslint-plugin-react": {
|
||||
"version": "7.30.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.1.tgz",
|
||||
"integrity": "sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==",
|
||||
"version": "7.31.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.1.tgz",
|
||||
"integrity": "sha512-j4/2xWqt/R7AZzG8CakGHA6Xa/u7iR8Q3xCxY+AUghdT92bnIDOBEefV456OeH0QvBcroVc0eyvrrLSyQGYIfg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-includes": "^3.1.5",
|
||||
@@ -8833,9 +8791,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"espree": {
|
||||
"version": "9.3.3",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz",
|
||||
"integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==",
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
|
||||
"integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"acorn": "^8.8.0",
|
||||
@@ -8904,15 +8862,6 @@
|
||||
"strip-final-newline": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"execall": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz",
|
||||
"integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"clone-regexp": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"executable": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
|
||||
@@ -8984,9 +8933,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"fastest-levenshtein": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.14.tgz",
|
||||
"integrity": "sha512-tFfWHjnuUfKE186Tfgr+jtaFc0mZTApEgKDOeyN+FwOqRkO/zK/3h1AiRd8u8CY53owL3CUmGr/oI9p/RdyLTA==",
|
||||
"version": "1.0.16",
|
||||
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
|
||||
"integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
|
||||
"dev": true
|
||||
},
|
||||
"fastq": {
|
||||
@@ -9147,12 +9096,6 @@
|
||||
"has-symbols": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"get-stdin": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
|
||||
"integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
|
||||
"dev": true
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
@@ -10742,9 +10685,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
|
||||
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
|
||||
"version": "8.4.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
|
||||
"integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nanoid": "^3.3.4",
|
||||
@@ -11375,22 +11318,20 @@
|
||||
"dev": true
|
||||
},
|
||||
"stylelint": {
|
||||
"version": "14.9.1",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.9.1.tgz",
|
||||
"integrity": "sha512-RdAkJdPiLqHawCSnu21nE27MjNXaVd4WcOHA4vK5GtIGjScfhNnaOuWR2wWdfKFAvcWQPOYe311iveiVKSmwsA==",
|
||||
"version": "14.11.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.11.0.tgz",
|
||||
"integrity": "sha512-OTLjLPxpvGtojEfpESWM8Ir64Z01E89xsisaBMUP/ngOx1+4VG2DPRcUyCCiin9Rd3kPXPsh/uwHd9eqnvhsYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@csstools/selector-specificity": "^2.0.1",
|
||||
"@csstools/selector-specificity": "^2.0.2",
|
||||
"balanced-match": "^2.0.0",
|
||||
"colord": "^2.9.2",
|
||||
"colord": "^2.9.3",
|
||||
"cosmiconfig": "^7.0.1",
|
||||
"css-functions-list": "^3.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"execall": "^2.0.0",
|
||||
"fast-glob": "^3.2.11",
|
||||
"fastest-levenshtein": "^1.0.12",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"file-entry-cache": "^6.0.1",
|
||||
"get-stdin": "^8.0.0",
|
||||
"global-modules": "^2.0.0",
|
||||
"globby": "^11.1.0",
|
||||
"globjoin": "^0.1.4",
|
||||
@@ -11405,7 +11346,7 @@
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss": "^8.4.16",
|
||||
"postcss-media-query-parser": "^0.2.3",
|
||||
"postcss-resolve-nested-selector": "^0.1.1",
|
||||
"postcss-safe-parser": "^6.0.0",
|
||||
@@ -11419,7 +11360,7 @@
|
||||
"svg-tags": "^1.0.0",
|
||||
"table": "^6.8.0",
|
||||
"v8-compile-cache": "^2.3.0",
|
||||
"write-file-atomic": "^4.0.1"
|
||||
"write-file-atomic": "^4.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"balanced-match": {
|
||||
@@ -11861,9 +11802,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"write-file-atomic": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz",
|
||||
"integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
|
||||
"integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"imurmurhash": "^0.1.4",
|
||||
|
||||
+5
-5
@@ -29,12 +29,12 @@
|
||||
},
|
||||
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.10",
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/eslint-parser": "^7.17.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.16.7",
|
||||
"concurrently": "^7.3.0",
|
||||
"cypress": "^10.4.0",
|
||||
"eslint": "^8.21.0",
|
||||
"cypress": "^10.6.0",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
@@ -43,13 +43,13 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prefer-object-spread": "^1.2.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.29.3",
|
||||
"eslint-plugin-react": "^7.31.1",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"prettier": "^2.5.1",
|
||||
"stylelint": "^14.9.1",
|
||||
"stylelint": "^14.11.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"stylelint-prettier": "^2.0.0"
|
||||
|
||||
Generated
+30
-30
@@ -19,11 +19,11 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^5.1.1",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"moment-timezone": "^0.5.37",
|
||||
"morgan": "^1.10.0",
|
||||
"pg": "^8.7.3",
|
||||
"pg": "^8.8.0",
|
||||
"redis": "^3.1.2",
|
||||
"sequelize": "^6.17.0",
|
||||
"sequelize": "^6.21.4",
|
||||
"umzug": "^2.3.0",
|
||||
"unfuck-utf8-headers-middleware": "^1.0.1",
|
||||
"uuid": "^3.4.0",
|
||||
@@ -2209,9 +2209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.34",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
|
||||
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
|
||||
"version": "0.5.37",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.37.tgz",
|
||||
"integrity": "sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==",
|
||||
"dependencies": {
|
||||
"moment": ">= 2.9.0"
|
||||
},
|
||||
@@ -2619,14 +2619,14 @@
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.7.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz",
|
||||
"integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
|
||||
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
|
||||
"dependencies": {
|
||||
"buffer-writer": "2.0.0",
|
||||
"packet-reader": "1.0.0",
|
||||
"pg-connection-string": "^2.5.0",
|
||||
"pg-pool": "^3.5.1",
|
||||
"pg-pool": "^3.5.2",
|
||||
"pg-protocol": "^1.5.0",
|
||||
"pg-types": "^2.1.0",
|
||||
"pgpass": "1.x"
|
||||
@@ -2635,7 +2635,7 @@
|
||||
"node": ">= 8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=2.0.0"
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
@@ -2657,9 +2657,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz",
|
||||
"integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==",
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
|
||||
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
@@ -3108,9 +3108,9 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/sequelize": {
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.3.tgz",
|
||||
"integrity": "sha512-cJPrTTVCofUxaaNKoIETiXCYh2xJ+OFq5jMHJQqftp34M4kNoLpTfUMPSwYtRUeTcSh1/5HodfJXIBi7troIFA==",
|
||||
"version": "6.21.4",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.4.tgz",
|
||||
"integrity": "sha512-5A0+giRhGHerTDRMsZ54TYRB8oQPWxeVscbc4USG9wRtw2Eqik0Vk0p2EVDrhoq7tmNBh2nHpd9YMfvGdwPEJw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -5884,9 +5884,9 @@
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"moment-timezone": {
|
||||
"version": "0.5.34",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
|
||||
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
|
||||
"version": "0.5.37",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.37.tgz",
|
||||
"integrity": "sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==",
|
||||
"requires": {
|
||||
"moment": ">= 2.9.0"
|
||||
}
|
||||
@@ -6207,14 +6207,14 @@
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"pg": {
|
||||
"version": "8.7.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz",
|
||||
"integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
|
||||
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
|
||||
"requires": {
|
||||
"buffer-writer": "2.0.0",
|
||||
"packet-reader": "1.0.0",
|
||||
"pg-connection-string": "^2.5.0",
|
||||
"pg-pool": "^3.5.1",
|
||||
"pg-pool": "^3.5.2",
|
||||
"pg-protocol": "^1.5.0",
|
||||
"pg-types": "^2.1.0",
|
||||
"pgpass": "1.x"
|
||||
@@ -6231,9 +6231,9 @@
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
|
||||
},
|
||||
"pg-pool": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz",
|
||||
"integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==",
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
|
||||
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
|
||||
"requires": {}
|
||||
},
|
||||
"pg-protocol": {
|
||||
@@ -6580,9 +6580,9 @@
|
||||
}
|
||||
},
|
||||
"sequelize": {
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.3.tgz",
|
||||
"integrity": "sha512-cJPrTTVCofUxaaNKoIETiXCYh2xJ+OFq5jMHJQqftp34M4kNoLpTfUMPSwYtRUeTcSh1/5HodfJXIBi7troIFA==",
|
||||
"version": "6.21.4",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.4.tgz",
|
||||
"integrity": "sha512-5A0+giRhGHerTDRMsZ54TYRB8oQPWxeVscbc4USG9wRtw2Eqik0Vk0p2EVDrhoq7tmNBh2nHpd9YMfvGdwPEJw==",
|
||||
"requires": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/validator": "^13.7.1",
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^5.1.1",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"moment-timezone": "^0.5.37",
|
||||
"morgan": "^1.10.0",
|
||||
"pg": "^8.7.3",
|
||||
"pg": "^8.8.0",
|
||||
"redis": "^3.1.2",
|
||||
"sequelize": "^6.17.0",
|
||||
"sequelize": "^6.21.4",
|
||||
"umzug": "^2.3.0",
|
||||
"unfuck-utf8-headers-middleware": "^1.0.1",
|
||||
"uuid": "^3.4.0",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
up: queryInterface => {
|
||||
return queryInterface.bulkInsert('access_groups', [
|
||||
{
|
||||
group_code: 'facultyStatistics',
|
||||
group_info: 'grants access to faculty statistics',
|
||||
},
|
||||
])
|
||||
},
|
||||
down: queryInterface => {
|
||||
return queryInterface.bulkDelete('access_groups', [
|
||||
{
|
||||
group_code: 'facultyStatistics',
|
||||
},
|
||||
])
|
||||
},
|
||||
}
|
||||
@@ -5,6 +5,8 @@ const { refreshAssociationsInRedis } = require('./services/studyrights')
|
||||
const { getAllProgrammes } = require('./services/studyrights')
|
||||
const { updateBasicView, updateStudytrackView } = require('./services/studyprogrammeUpdates')
|
||||
const { findAndSaveTeachers } = require('./services/topteachers')
|
||||
const { faculties } = require('./services/organisations')
|
||||
const { updateFacultyOverview } = require('./services/faculty/facultyUpdates')
|
||||
const { isProduction } = require('./conf-backend')
|
||||
const { getCurrentSemester } = require('./services/semesters')
|
||||
const logger = require('./util/logger')
|
||||
@@ -16,6 +18,23 @@ const refreshStudyrightAssociations = async () => {
|
||||
logger.info('Studyright associations refreshed!')
|
||||
}
|
||||
|
||||
const refreshFaculties = async () => {
|
||||
logger.info('Refreshing faculty overview statistics for all faculties')
|
||||
const facultyList = (await faculties()).filter(f => !['Y', 'H99', 'Y01', 'H92', 'H930'].includes(f.code))
|
||||
let ready = 0
|
||||
|
||||
for (const faculty of facultyList) {
|
||||
try {
|
||||
await updateFacultyOverview(faculty.code)
|
||||
ready += 1
|
||||
} catch (e) {
|
||||
logger.error({ message: `Failed to update stats for faculty ${faculty?.code}`, meta: e })
|
||||
}
|
||||
logger.info(`${ready}/${facultyList.length} faculties done`)
|
||||
}
|
||||
logger.info('Faculty stats refreshed!')
|
||||
}
|
||||
|
||||
const refreshNewOverviews = async () => {
|
||||
logger.info('Refreshing studyprogramme and studytrack overview statistics for all programmes')
|
||||
const codes = (await getAllProgrammes())
|
||||
@@ -100,7 +119,7 @@ const startCron = () => {
|
||||
logger.info('Cronjob for refreshing stats started: runs at 3am every day.')
|
||||
// refresh 3am every day
|
||||
schedule('0 3 * * *', async () => {
|
||||
for (const func of [refreshStatistics, refreshTrends, refreshNewOverviews]) {
|
||||
for (const func of [refreshStatistics, refreshTrends, refreshNewOverviews, refreshFaculties]) {
|
||||
await func()
|
||||
}
|
||||
})
|
||||
@@ -111,4 +130,5 @@ module.exports = {
|
||||
startCron,
|
||||
refreshStatistics,
|
||||
refreshTrends,
|
||||
refreshFaculties,
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ module.exports = (app, url) => {
|
||||
app.use(url, tags)
|
||||
app.use(url, programmeModules)
|
||||
app.use(url, studyProgramme)
|
||||
app.use(url, faculties)
|
||||
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
|
||||
app.use(`${url}/updater`, auth.roles(['admin']), updater)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
app.use(`${url}/users`, auth.roles(['admin']), users)
|
||||
|
||||
@@ -1,103 +1,143 @@
|
||||
const router = require('express').Router()
|
||||
const { faculties, degreeProgrammeCodesOfFaculty } = require('../services/organisations')
|
||||
const { combineFacultyBasics } = require('../services/facultyBasics')
|
||||
const { combineFacultyCredits } = require('../services/facultyCredits')
|
||||
const { degreeProgrammesOfFaculty } = require('../services/faculty')
|
||||
const { faculties } = require('../services/organisations')
|
||||
const { combineFacultyBasics } = require('../services/faculty/facultyBasics')
|
||||
const { combineFacultyCredits } = require('../services/faculty/facultyCredits')
|
||||
const { findFacultyProgrammeCodes } = require('../services/faculty/faculty')
|
||||
const { combineFacultyThesisWriters } = require('../services/faculty/facultyThesisWriters')
|
||||
const { countGraduationTimes } = require('../services/faculty/facultyGraduationTimes')
|
||||
const { updateFacultyOverview } = require('../services/faculty/facultyUpdates')
|
||||
const {
|
||||
getFacultyProgrammes,
|
||||
setFacultyProgrammes,
|
||||
getBasicStats,
|
||||
setBasicStats,
|
||||
getCreditStats,
|
||||
setCreditStats,
|
||||
getThesisWritersStats,
|
||||
setThesisWritersStats,
|
||||
} = require('../services/faculty/facultyService')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
// Faculty uses a lot of tools designed for Study programme.
|
||||
// Some of them have been copied here and slightly edited for faculty purpose.
|
||||
|
||||
router.get('/faculties', async (req, res) => {
|
||||
const facultyList = await faculties()
|
||||
const getProgrammes = async (code, programmeFilter) => {
|
||||
const programmes = await getFacultyProgrammes(code, programmeFilter)
|
||||
if (programmes) return programmes
|
||||
let updatedProgrammes = await findFacultyProgrammeCodes(code, programmeFilter)
|
||||
if (updatedProgrammes) updatedProgrammes = await setFacultyProgrammes(code, updatedProgrammes, programmeFilter)
|
||||
|
||||
return updatedProgrammes
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
|
||||
const facultyList = (await faculties()).filter(f => !ignore.includes(f.code))
|
||||
res.json(facultyList)
|
||||
})
|
||||
|
||||
router.get('/faculties/:id/basicstats', async (req, res) => {
|
||||
router.get('/:id/basicstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
const specialGroups = req.query?.special_groups
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
let allBasics = {
|
||||
id: code,
|
||||
years: [],
|
||||
programmeNames: {},
|
||||
studentInfo: {
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
titles: ['', 'Started studying', 'Graduated', 'Transferred inside', 'Transferred away', 'Transferred to'],
|
||||
programmeTableStats: {},
|
||||
},
|
||||
graduationInfo: {
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
titles: ['', 'All graduations', 'Bachelors', 'Masters', 'Doctors', 'Others'],
|
||||
programmeTableStats: {},
|
||||
},
|
||||
status: 'DONE',
|
||||
lastUpdated: '',
|
||||
const data = await getBasicStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const wantedProgrammes = await getProgrammes(code, programmeFilter)
|
||||
if (!wantedProgrammes) return res.status(422).end()
|
||||
|
||||
//all programmes are required for correct sorting of transfers
|
||||
let allProgrammeCodes = []
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
|
||||
const allProgs = await getProgrammes(code, 'ALL_PROGRAMMES')
|
||||
allProgs?.data.forEach(prog => allProgrammeCodes.push(prog.code))
|
||||
} else {
|
||||
wantedProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
|
||||
}
|
||||
|
||||
const programmes = await degreeProgrammeCodesOfFaculty(code)
|
||||
if (programmes) {
|
||||
await combineFacultyBasics(allBasics, code, programmes, yearType)
|
||||
let updatedStats = await combineFacultyBasics(
|
||||
code,
|
||||
wantedProgrammes.data,
|
||||
yearType,
|
||||
allProgrammeCodes,
|
||||
programmeFilter,
|
||||
specialGroups
|
||||
)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
return res.json(allBasics)
|
||||
return res.json(updatedStats)
|
||||
})
|
||||
|
||||
router.get('/faculties/:id/creditstats', async (req, res) => {
|
||||
router.get('/:id/creditstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = 'SPECIAL_INCLUDED' // req.query?.special_groups
|
||||
const specialGroups = req.query?.special_groups
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
let counts = {}
|
||||
let years = []
|
||||
let allCredits = {
|
||||
id: code,
|
||||
years: [],
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
programmeTableStats: {},
|
||||
programmeNames: {},
|
||||
titles: [
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non-major faculty students credits',
|
||||
'Non-major other faculty students credits',
|
||||
'Non-degree student credits',
|
||||
],
|
||||
status: 'DONE',
|
||||
lastUpdated: '',
|
||||
const data = await getCreditStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
|
||||
let updatedStats = await combineFacultyCredits(code, programmes.data, yearType, specialGroups)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
|
||||
const programmes = await degreeProgrammesOfFaculty(code)
|
||||
return res.json(updatedStats)
|
||||
})
|
||||
|
||||
if (programmes) {
|
||||
await combineFacultyCredits(allCredits, programmes, yearType, specialGroups, counts, years)
|
||||
router.get('/:id/thesisstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
let updateStats = await combineFacultyThesisWriters(code, programmes.data, yearType, specialGroups)
|
||||
if (updateStats) {
|
||||
updateStats = await setThesisWritersStats(updateStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
let majors = []
|
||||
let facultyNonMajor = []
|
||||
let otherNonMajor = []
|
||||
let nonDegree = []
|
||||
return res.json(updateStats)
|
||||
})
|
||||
|
||||
years.forEach(year => {
|
||||
majors.push(counts[year][1])
|
||||
facultyNonMajor.push(counts[year][2])
|
||||
otherNonMajor.push(counts[year][3])
|
||||
nonDegree.push(counts[year][4])
|
||||
})
|
||||
router.get('/:id/graduationtimes', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
allCredits.graphStats = [
|
||||
{ name: 'Major students credits', data: majors },
|
||||
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
|
||||
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
|
||||
{ name: 'Non-degree credits', data: nonDegree },
|
||||
]
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
return res.json(allCredits)
|
||||
// const programmes = await getProgrammes(code, programmeFilter)
|
||||
const result = await countGraduationTimes(code, programmeFilter)
|
||||
// currently counts all programmes
|
||||
return res.json(result)
|
||||
})
|
||||
|
||||
router.get('/:id/update_basicview', async (req, res) => {
|
||||
const code = req.params.id
|
||||
if (code) {
|
||||
let result = null
|
||||
try {
|
||||
result = await updateFacultyOverview(code)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update faculty ${code} basic tab stats: ${e}`)
|
||||
}
|
||||
return res.json(result)
|
||||
}
|
||||
return res.status(422).end()
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -39,7 +39,7 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
req.body.months = 12
|
||||
}
|
||||
|
||||
const encrypted = req.body.selectedStudents[0].encryptedData
|
||||
const encrypted = req.body.selectedStudents[0]?.encryptedData
|
||||
|
||||
if (req.body.years) {
|
||||
const upperYearBound = new Date().getFullYear() + 1
|
||||
@@ -48,7 +48,11 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
if (req.body.selectedStudentsByYear) {
|
||||
req.body.selectedStudents = encrypted
|
||||
? req.body.selectedStudents
|
||||
.filter(({ encryptedData }) => req.body.selectedStudentsByYear[year].includes(encryptedData))
|
||||
.filter(
|
||||
({ encryptedData }) =>
|
||||
req.body.selectedStudentsByYear[year] &&
|
||||
req.body.selectedStudentsByYear[year].includes(encryptedData)
|
||||
)
|
||||
.map(decrypt)
|
||||
: req.body.selectedStudentsByYear[year]
|
||||
}
|
||||
@@ -77,6 +81,20 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
//These cause problems in front (where they were originally calculated)
|
||||
// Do them here for the moment
|
||||
for (const course of result.coursestatistics) {
|
||||
// course.students.notParticipated = {}
|
||||
course.students.didNotPass = {}
|
||||
|
||||
for (const student of Object.keys(course.students.all)) {
|
||||
if (
|
||||
!Object.keys(course.students.passed).includes(student) &&
|
||||
!Object.keys(course.students.retryPassed).includes(student)
|
||||
)
|
||||
course.students.didNotPass[student] = true
|
||||
}
|
||||
}
|
||||
res.json(result)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -450,7 +450,7 @@ const codeLikeTerm = code =>
|
||||
: {
|
||||
code: {
|
||||
// Starts with code or has AY/A in front of the code
|
||||
[Op.regexp]: `^(AY|ay|A|a)?${code}`,
|
||||
[Op.iRegexp]: `^(AY|ay|A|a)?${code}`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
const {
|
||||
ElementDetail,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
Student,
|
||||
Transfer,
|
||||
} = require('../models')
|
||||
const { facultyFormatStudyright, facultyFormatProgramme, formatFacultyTransfer } = require('./facultyHelpers')
|
||||
|
||||
const startedStudyrights = async (faculty, since) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
studystartdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const graduatedStudyrights = async (faculty, since) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
enddate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
graduated: 1,
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const transferredInsideFaculty = async (programmes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: programmes,
|
||||
targetcode: programmes,
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const transferredAway = async (programmes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: programmes,
|
||||
targetcode: {
|
||||
[Op.notIn]: programmes,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const transferredTo = async (programmes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: {
|
||||
[Op.notIn]: programmes,
|
||||
},
|
||||
targetcode: programmes,
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const degreeProgrammesOfFaculty = async facultyCode =>
|
||||
(
|
||||
await ProgrammeModule.findAll({
|
||||
attributes: ['code', 'name'],
|
||||
include: {
|
||||
model: Organization,
|
||||
where: {
|
||||
code: facultyCode,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(facultyFormatProgramme)
|
||||
|
||||
const getProgrammeName = async programme => {
|
||||
return await ElementDetail.findOne({
|
||||
attributes: ['name'],
|
||||
where: {
|
||||
code: programme,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
transferredInsideFaculty,
|
||||
transferredAway,
|
||||
transferredTo,
|
||||
degreeProgrammesOfFaculty,
|
||||
getProgrammeName,
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
const {
|
||||
ElementDetail,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
Semester,
|
||||
SemesterEnrollment,
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
Student,
|
||||
Transfer,
|
||||
Credit,
|
||||
Course,
|
||||
} = require('../../models')
|
||||
const {
|
||||
facultyFormatStudyright,
|
||||
facultyFormatProgramme,
|
||||
formatFacultyTransfer,
|
||||
formatFacultyThesisWriter,
|
||||
formatOrganization,
|
||||
formatAbsence,
|
||||
isNewProgramme,
|
||||
} = require('./facultyHelpers')
|
||||
|
||||
const getTransferredToAndAway = async (programmeCodes, allProgrammeCodes, since) => {
|
||||
const awayTransfers = (await transferredAway(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
|
||||
const toTransfers = (await transferredTo(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
|
||||
const transferredToOrAway = [...toTransfers, ...awayTransfers]
|
||||
return transferredToOrAway
|
||||
}
|
||||
|
||||
const getTransferredInside = async (programmeCodes, allProgrammeCodes, since) => {
|
||||
const transfersInside = (await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)).map(
|
||||
t => t.studyrightid
|
||||
)
|
||||
return transfersInside
|
||||
}
|
||||
const startedStudyrights = async (faculty, since, studyRightWhere) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
studystartdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
...studyRightWhere,
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const graduatedStudyrights = async (faculty, since, studyrightWhere) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
enddate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
graduated: 1,
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
...studyrightWhere,
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const bachelorStudyright = async id => {
|
||||
return await Studyright.findOne({
|
||||
attributes: ['studystartdate'],
|
||||
where: {
|
||||
studyrightid: id,
|
||||
extentcode: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const transferredInsideFaculty = async (programmes, allProgrammeCodes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: allProgrammeCodes,
|
||||
targetcode: programmes,
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const transferredAway = async (programmes, allProgrammeCodes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: programmes,
|
||||
targetcode: {
|
||||
[Op.notIn]: allProgrammeCodes,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const transferredTo = async (programmes, allProgrammeCodes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: {
|
||||
[Op.notIn]: allProgrammeCodes,
|
||||
},
|
||||
targetcode: programmes,
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const degreeProgrammesOfFaculty = async facultyCode =>
|
||||
(
|
||||
await ProgrammeModule.findAll({
|
||||
attributes: ['code', 'name'],
|
||||
include: {
|
||||
model: Organization,
|
||||
where: {
|
||||
code: {
|
||||
[Op.startsWith]: facultyCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(facultyFormatProgramme)
|
||||
|
||||
const facultyOgranizationId = async faculty => {
|
||||
return await Organization.findOne({
|
||||
attributes: ['id'],
|
||||
where: {
|
||||
code: faculty,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const getChildOrganizations = async facultyId =>
|
||||
(
|
||||
await Organization.findAll({
|
||||
attributes: ['id', 'name', 'code', 'parent_id'],
|
||||
where: {
|
||||
parent_id: facultyId,
|
||||
},
|
||||
})
|
||||
).map(formatOrganization)
|
||||
|
||||
const thesisWriters = async (provider, since, thesisTypes, students) =>
|
||||
(
|
||||
await Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'attainment_date', 'student_studentnumber'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['course_unit_type'],
|
||||
required: true,
|
||||
where: {
|
||||
course_unit_type: {
|
||||
[Op.in]: thesisTypes,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
model: Organization,
|
||||
attributes: [],
|
||||
required: true,
|
||||
where: {
|
||||
code: provider,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
credittypecode: 4,
|
||||
isStudyModule: {
|
||||
[Op.not]: true,
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: students.length > 0 ? students : { [Op.not]: null },
|
||||
},
|
||||
})
|
||||
).map(formatFacultyThesisWriter)
|
||||
|
||||
const statutoryAbsences = async (studentnumber, startdate, enddate) =>
|
||||
(
|
||||
await SemesterEnrollment.findAll({
|
||||
attributes: ['semestercode'],
|
||||
include: {
|
||||
model: Semester,
|
||||
attributes: ['startdate', 'enddate'],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.gte]: startdate,
|
||||
},
|
||||
enddate: {
|
||||
[Op.lte]: enddate,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
studentnumber: studentnumber,
|
||||
statutory_absence: true,
|
||||
},
|
||||
})
|
||||
).map(formatAbsence)
|
||||
|
||||
// Some programme modules are not directly associated to a faculty (organization).
|
||||
// Some have intermediate organizations, such as department, so the connection must be digged up
|
||||
const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
|
||||
let allProgrammes = []
|
||||
let allProgrammeCodes = []
|
||||
|
||||
const directAssociationProgrammes = await degreeProgrammesOfFaculty(faculty)
|
||||
|
||||
allProgrammes = allProgrammes.concat(directAssociationProgrammes)
|
||||
allProgrammeCodes = allProgrammeCodes.concat(directAssociationProgrammes.map(prog => prog.code))
|
||||
|
||||
// find faculty's organization id and its direct child organizations
|
||||
const { id } = await facultyOgranizationId(faculty)
|
||||
const facultyChildOrganizations = await getChildOrganizations(id)
|
||||
|
||||
// get programme modules that have a faculty child as organization(_id)
|
||||
for (const org of facultyChildOrganizations) {
|
||||
const childAssociationProgrammes = await degreeProgrammesOfFaculty(org.code)
|
||||
if (childAssociationProgrammes.length > 0) {
|
||||
childAssociationProgrammes.forEach(prog => {
|
||||
if (!(prog.code in allProgrammeCodes)) {
|
||||
allProgrammes = allProgrammes.concat([prog])
|
||||
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// dig deeper
|
||||
const grandChildren = await getChildOrganizations(org.id)
|
||||
if (grandChildren.length > 0) {
|
||||
for (const gcOrg of grandChildren) {
|
||||
const associatedProgrammes = await degreeProgrammesOfFaculty(gcOrg.code)
|
||||
if (associatedProgrammes.length > 0) {
|
||||
associatedProgrammes.forEach(prog => {
|
||||
if (!(prog.code in allProgrammeCodes)) {
|
||||
allProgrammes = allProgrammes.concat([prog])
|
||||
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
|
||||
allProgrammes = allProgrammes.filter(prog => isNewProgramme(prog.code))
|
||||
}
|
||||
|
||||
return allProgrammes
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
bachelorStudyright,
|
||||
transferredInsideFaculty,
|
||||
transferredAway,
|
||||
transferredTo,
|
||||
degreeProgrammesOfFaculty,
|
||||
thesisWriters,
|
||||
findFacultyProgrammeCodes,
|
||||
facultyOgranizationId,
|
||||
statutoryAbsences,
|
||||
getTransferredToAndAway,
|
||||
getTransferredInside,
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
transferredInsideFaculty,
|
||||
transferredAway,
|
||||
transferredTo,
|
||||
getTransferredToAndAway,
|
||||
getTransferredInside,
|
||||
} = require('./faculty')
|
||||
const { getStatsBasis, getYearsArray, defineYear } = require('../studyprogrammeHelpers')
|
||||
const { findRightProgramme, isNewProgramme, checkTransfers, getExtentFilter } = require('./facultyHelpers')
|
||||
|
||||
const filterDuplicateStudyrights = studyrights => {
|
||||
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
|
||||
// choose only the earlier started one, so we don't count start of masters as starting in faculty
|
||||
let rightsToCount = {}
|
||||
|
||||
studyrights.forEach(right => {
|
||||
const id = right.studyrightid.slice(0, -2)
|
||||
const start = new Date(right.studystartdate)
|
||||
if (id in rightsToCount) {
|
||||
if (new Date(rightsToCount[id].studystartdate) > start) {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
} else {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
})
|
||||
return Object.values(rightsToCount)
|
||||
}
|
||||
|
||||
const addProgramme = async (
|
||||
stats,
|
||||
programme,
|
||||
tableStats,
|
||||
transferYear,
|
||||
index,
|
||||
allBasics,
|
||||
facultyProgrammes,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
if (!(programme in stats)) {
|
||||
stats[programme] = {}
|
||||
if (includeAllSpecials) {
|
||||
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0, 0, 0]))
|
||||
} else {
|
||||
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0]))
|
||||
}
|
||||
}
|
||||
stats[programme][transferYear][index] += 1
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
const name = facultyProgrammes.filter(prog => prog.code === programme)[0]?.name
|
||||
allBasics.programmeNames[programme] = name
|
||||
}
|
||||
}
|
||||
|
||||
const getFacultyStarters = async (
|
||||
faculty,
|
||||
transfersToOrAwayStudyrights,
|
||||
insideTransfersStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
counts,
|
||||
allBasics,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const startedGraphStats = [...graphStats]
|
||||
const startedTableStats = { ...tableStats }
|
||||
const studyrightWhere = getExtentFilter(includeAllSpecials)
|
||||
const studyrights = await startedStudyrights(faculty, since, studyrightWhere)
|
||||
let filteredStudyrights = filterDuplicateStudyrights(studyrights)
|
||||
const programmeTableStats = {}
|
||||
if (!includeAllSpecials) {
|
||||
filteredStudyrights = filteredStudyrights.filter(
|
||||
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
|
||||
)
|
||||
}
|
||||
|
||||
filteredStudyrights.forEach(({ studystartdate, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'started')
|
||||
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const startYear = defineYear(studystartdate, isAcademicYear)
|
||||
|
||||
startedGraphStats[indexOf(yearsArray, startYear)] += 1
|
||||
startedTableStats[startYear] += 1
|
||||
|
||||
if (!(programme in programmeTableStats)) {
|
||||
programmeTableStats[programme] = { ...tableStats }
|
||||
}
|
||||
programmeTableStats[programme][startYear] += 1
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programme] = programmeName
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
|
||||
programmeData['started'] = programmeTableStats
|
||||
|
||||
Object.keys(startedTableStats).forEach(year => {
|
||||
counts[year] = [startedTableStats[year]]
|
||||
})
|
||||
}
|
||||
|
||||
const getFacultyGraduates = async (
|
||||
faculty,
|
||||
transfersToOrAwayStudyrights,
|
||||
insideTransfersStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
countsGraduations,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
|
||||
const graduatedTableStats = {}
|
||||
|
||||
let studyrightWhere = getExtentFilter(includeAllSpecials)
|
||||
let graduatedRights = await graduatedStudyrights(faculty, since, studyrightWhere)
|
||||
const programmeTableStats = {}
|
||||
|
||||
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
|
||||
if (!includeAllSpecials) {
|
||||
graduatedRights = graduatedRights.filter(
|
||||
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
|
||||
)
|
||||
}
|
||||
|
||||
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const endYear = defineYear(enddate, isAcademicYear)
|
||||
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][0] += 1
|
||||
|
||||
if (!(programme in programmeTableStats)) {
|
||||
programmeTableStats[programme] = {}
|
||||
Object.keys(tableStats).forEach(year => (programmeTableStats[programme][year] = [0, 0, 0, 0, 0]))
|
||||
}
|
||||
programmeTableStats[programme][endYear][0] += 1
|
||||
|
||||
if (extentcode === 1) {
|
||||
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][1] += 1
|
||||
programmeTableStats[programme][endYear][1] += 1
|
||||
} else if (extentcode === 2) {
|
||||
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][2] += 1
|
||||
programmeTableStats[programme][endYear][2] += 1
|
||||
} else if (extentcode === 4) {
|
||||
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][3] += 1
|
||||
programmeTableStats[programme][endYear][3] += 1
|
||||
} else {
|
||||
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][4] += 1
|
||||
programmeTableStats[programme][endYear][4] += 1
|
||||
}
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programme] = programmeName
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Graduated', data: graduatedGraphStats[0] })
|
||||
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Bachelors', data: graduatedGraphStats[1] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Masters', data: graduatedGraphStats[2] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Doctors', data: graduatedGraphStats[3] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Others', data: graduatedGraphStats[4] })
|
||||
|
||||
programmeData['graduated'] = programmeTableStats
|
||||
|
||||
Object.keys(graduatedTableStats).forEach(year => {
|
||||
counts[year] = counts[year].concat(graduatedTableStats[year][0])
|
||||
countsGraduations[year] = graduatedTableStats[year]
|
||||
})
|
||||
}
|
||||
|
||||
const getFacultyTransfers = async (
|
||||
programmes,
|
||||
programmeCodes,
|
||||
allProgrammeCodes,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const insideTransfers = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
|
||||
// Filter still away those that have started studying before 1.8.2017
|
||||
let transferGraphStats = []
|
||||
let transferTableStats = {}
|
||||
if (includeAllSpecials) {
|
||||
transferGraphStats = [[...graphStats], [...graphStats], [...graphStats]]
|
||||
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0, 0, 0]))
|
||||
} else {
|
||||
transferGraphStats = [[...graphStats]]
|
||||
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0]))
|
||||
}
|
||||
|
||||
const programmeTableStats = {}
|
||||
// inside transfer is counted towards target
|
||||
for (const { transferdate, targetcode } of insideTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][0] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
0,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
if (includeAllSpecials) {
|
||||
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
|
||||
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
|
||||
|
||||
for (const { transferdate, sourcecode } of awayTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][1] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
sourcecode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
1,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
|
||||
for (const { transferdate, targetcode } of toTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][2] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
2,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred inside', data: transferGraphStats[0] })
|
||||
if (includeAllSpecials) {
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
|
||||
}
|
||||
programmeData['transferred'] = programmeTableStats
|
||||
|
||||
Object.keys(transferTableStats).forEach(year => {
|
||||
counts[year] = counts[year].concat(transferTableStats[year])
|
||||
})
|
||||
}
|
||||
|
||||
const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeCodes, programmeFilter, special) => {
|
||||
let counts = {}
|
||||
let countsGraduations = {}
|
||||
let years = []
|
||||
let programmeData = {}
|
||||
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
|
||||
const includeAllSpecials = special === 'SPECIAL_INCLUDED'
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const { graphStats, tableStats } = getStatsBasis(yearsArray)
|
||||
Object.keys(tableStats).forEach(year => years.push(year))
|
||||
const wantedProgrammeCodes = programmes.map(prog => prog.code)
|
||||
|
||||
let allBasics = {
|
||||
id: faculty,
|
||||
years: [],
|
||||
programmeNames: {},
|
||||
studentInfo: {
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
titles: includeAllSpecials
|
||||
? ['', 'Started studying', 'Graduated', 'Transferred inside', 'Transferred away', 'Transferred to']
|
||||
: ['', 'Started studying', 'Graduated', 'Transferred inside'],
|
||||
programmeTableStats: {},
|
||||
},
|
||||
graduationInfo: {
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Others'],
|
||||
programmeTableStats: {},
|
||||
},
|
||||
}
|
||||
const transfersToAwayStudyrights = await getTransferredToAndAway(wantedProgrammeCodes, allProgrammeCodes, since)
|
||||
const transferInsideStudyrights = await getTransferredInside(wantedProgrammeCodes, allProgrammeCodes, since)
|
||||
// Started studying in faculty
|
||||
await getFacultyStarters(
|
||||
faculty,
|
||||
transfersToAwayStudyrights,
|
||||
transferInsideStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
counts,
|
||||
allBasics,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
)
|
||||
|
||||
// Graduated in faculty
|
||||
await getFacultyGraduates(
|
||||
faculty,
|
||||
transfersToAwayStudyrights,
|
||||
transferInsideStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
countsGraduations,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
)
|
||||
|
||||
// Transfers
|
||||
await getFacultyTransfers(
|
||||
programmes,
|
||||
wantedProgrammeCodes,
|
||||
allProgrammeCodes,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData,
|
||||
includeAllSpecials
|
||||
)
|
||||
|
||||
// combine tableStats from all categories
|
||||
allBasics.years = years
|
||||
years.forEach(year => {
|
||||
allBasics.studentInfo.tableStats.push([year, ...counts[year]])
|
||||
allBasics.graduationInfo.tableStats.push([year, ...countsGraduations[year]])
|
||||
})
|
||||
allBasics.studentInfo.tableStats.reverse()
|
||||
allBasics.graduationInfo.tableStats.reverse()
|
||||
|
||||
//combine programme level tablestats
|
||||
// all programmes are not present in all data types, check all found programmes and patch missing details with 0
|
||||
|
||||
let allCodes = Object.keys(programmeData.started).concat(
|
||||
Object.keys(programmeData.graduated),
|
||||
Object.keys(programmeData.transferred)
|
||||
)
|
||||
allCodes = [...new Set(allCodes)]
|
||||
|
||||
let studentInfo = {}
|
||||
let graduationInfo = {}
|
||||
|
||||
allCodes.forEach(code => {
|
||||
if (!(code in studentInfo)) studentInfo[code] = {}
|
||||
|
||||
for (const year of years) {
|
||||
// started
|
||||
if (code in programmeData.started) {
|
||||
studentInfo[code][year] = [year, programmeData.started[code][year]]
|
||||
} else {
|
||||
studentInfo[code][year] = [year, 0]
|
||||
}
|
||||
// graduated total
|
||||
if (code in programmeData.graduated) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([programmeData.graduated[code][year][0]])
|
||||
if (!(code in graduationInfo)) graduationInfo[code] = {}
|
||||
graduationInfo[code][year] = [year, ...programmeData.graduated[code][year]]
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0])
|
||||
}
|
||||
// trasferred
|
||||
if (code in programmeData.transferred) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(studentInfo).forEach(prog => {
|
||||
allBasics.studentInfo.programmeTableStats[prog] = [...Object.values(studentInfo[prog])].reverse()
|
||||
})
|
||||
|
||||
Object.keys(graduationInfo).forEach(prog => {
|
||||
allBasics.graduationInfo.programmeTableStats[prog] = Object.values(graduationInfo[prog]).reverse()
|
||||
})
|
||||
|
||||
return allBasics
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyBasics }
|
||||
+82
-40
@@ -1,5 +1,5 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const { mapToProviders } = require('../util/utils')
|
||||
const { mapToProviders } = require('../../util/utils')
|
||||
const {
|
||||
getStatsBasis,
|
||||
defineYear,
|
||||
@@ -8,14 +8,13 @@ const {
|
||||
getYearsArray,
|
||||
getStartDate,
|
||||
isSpecialGroupCredit,
|
||||
tableTitles,
|
||||
} = require('./studyprogrammeHelpers')
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const {
|
||||
getStudyRights,
|
||||
getCreditsForStudyProgramme,
|
||||
allTransfers,
|
||||
getCourseCodesForStudyProgramme,
|
||||
} = require('./studyprogramme')
|
||||
} = require('../studyprogramme')
|
||||
|
||||
const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes) => {
|
||||
let right = ''
|
||||
@@ -56,8 +55,10 @@ const getFacultyRegularCreditStats = async ({
|
||||
const credits = await getCreditsForStudyProgramme(courses, since)
|
||||
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
|
||||
|
||||
let studyrights = await getStudyRights(students, since)
|
||||
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid)
|
||||
let studyrights = await getStudyRights(students)
|
||||
|
||||
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid).filter(s => s !== 'undefined')
|
||||
|
||||
if (!includeAllSpecials) {
|
||||
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
|
||||
}
|
||||
@@ -96,7 +97,6 @@ const getFacultyRegularCreditStats = async ({
|
||||
nonDegree.tableStats[attainmentYear] += credits || 0
|
||||
}
|
||||
})
|
||||
|
||||
if (
|
||||
majors.graphStats.every(year => year === 0) &&
|
||||
facultyNonMajors.graphStats.every(year => year === 0) &&
|
||||
@@ -126,7 +126,6 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
|
||||
const { majors, facultyNonMajors, otherNonMajors, nonDegree } = await getFacultyRegularCreditStats(queryParameters)
|
||||
|
||||
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
|
||||
const titles = tableTitles['credits'][includeAllSpecials ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED']
|
||||
const dataFound = [majors, facultyNonMajors, otherNonMajors].some(d => d.graphStats.length) // , transferred
|
||||
|
||||
if (!dataFound) return null
|
||||
@@ -144,48 +143,44 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
|
||||
otherNonMajors.tableStats[year],
|
||||
nonDegree.tableStats[year],
|
||||
]
|
||||
: [
|
||||
year,
|
||||
majors.tableStats[year] +
|
||||
facultyNonMajors.tableStats[year] +
|
||||
otherNonMajors.tableStats[year] +
|
||||
nonDegree.tableStats[year],
|
||||
majors.tableStats[year],
|
||||
]
|
||||
: [year, majors.tableStats[year], majors.tableStats[year]]
|
||||
)
|
||||
|
||||
const graphStats = [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major faculty students credits',
|
||||
data: facultyNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major other faculty students credits 2',
|
||||
data: otherNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-degree credits',
|
||||
data: nonDegree.graphStats,
|
||||
},
|
||||
]
|
||||
const graphStats = includeAllSpecials
|
||||
? [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major faculty students credits',
|
||||
data: facultyNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major other faculty students credits 2',
|
||||
data: otherNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-degree credits',
|
||||
data: nonDegree.graphStats,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
id: studyprogramme,
|
||||
years,
|
||||
tableStats,
|
||||
graphStats,
|
||||
titles,
|
||||
}
|
||||
}
|
||||
|
||||
const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgrammes) => {
|
||||
// redis for faculty? Create faculty versions for out-commented functions
|
||||
// const data = await getCreditStats(code, yearType, specialGroups)
|
||||
// if (data) return data
|
||||
const updatedStats = await getFacultyCreditStatsForStudytrack({
|
||||
studyprogramme: code,
|
||||
facultyProgrammes,
|
||||
@@ -195,11 +190,34 @@ const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgram
|
||||
},
|
||||
})
|
||||
|
||||
// if (updatedStats) await setCreditStats(updatedStats, yearType, specialGroups)
|
||||
return updatedStats
|
||||
}
|
||||
|
||||
const combineFacultyCredits = async (allCredits, programmes, yearType, specialGroups, counts, years) => {
|
||||
const combineFacultyCredits = async (faculty, programmes, yearType, specialGroups) => {
|
||||
let counts = {}
|
||||
let years = []
|
||||
const titles =
|
||||
specialGroups === 'SPECIAL_INCLUDED'
|
||||
? [
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non-major faculty students credits',
|
||||
'Non-major other faculty students credits',
|
||||
'Non-degree student credits',
|
||||
]
|
||||
: ['', 'Total', 'Major students credits']
|
||||
|
||||
let allCredits = {
|
||||
id: faculty,
|
||||
years: [],
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
programmeTableStats: {},
|
||||
programmeNames: {},
|
||||
titles,
|
||||
}
|
||||
|
||||
const progCodes = programmes.map(p => p.code)
|
||||
for (const prog of programmes) {
|
||||
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
|
||||
@@ -228,6 +246,30 @@ const combineFacultyCredits = async (allCredits, programmes, yearType, specialGr
|
||||
|
||||
years.sort()
|
||||
allCredits.years = years
|
||||
|
||||
let majors = []
|
||||
let facultyNonMajor = []
|
||||
let otherNonMajor = []
|
||||
let nonDegree = []
|
||||
|
||||
years.forEach(year => {
|
||||
majors.push(counts[year][1])
|
||||
facultyNonMajor.push(counts[year][2])
|
||||
otherNonMajor.push(counts[year][3])
|
||||
nonDegree.push(counts[year][4])
|
||||
})
|
||||
|
||||
allCredits.graphStats =
|
||||
specialGroups === 'SPECIAL_INCLUDED'
|
||||
? [
|
||||
{ name: 'Major students credits', data: majors },
|
||||
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
|
||||
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
|
||||
{ name: 'Non-degree credits', data: nonDegree },
|
||||
]
|
||||
: [{ name: 'Major students credits', data: majors }]
|
||||
|
||||
return allCredits
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyCredits, getFacultyCreditStatsForStudytrack }
|
||||
@@ -0,0 +1,108 @@
|
||||
const moment = require('moment')
|
||||
const { graduatedStudyrights, bachelorStudyright, statutoryAbsences } = require('./faculty')
|
||||
const { findRightProgramme, isNewProgramme } = require('./facultyHelpers')
|
||||
const { getYearsArray, getYearsObject, getMean, getMedian, defineYear } = require('../studyprogrammeHelpers')
|
||||
|
||||
const findBachelorStartdate = async id => {
|
||||
const studyright = await bachelorStudyright(id)
|
||||
if (studyright) return studyright.studystartdate
|
||||
return null
|
||||
}
|
||||
const getStatutoryAbsences = async (studentnumber, startdate, enddate) => {
|
||||
const absences = await statutoryAbsences(studentnumber, startdate, enddate)
|
||||
if (absences.length) {
|
||||
const absentMonths = absences.reduce((sum, ab) => sum + moment(ab.end).diff(moment(ab.start), 'months'), 0)
|
||||
return absentMonths
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const countGraduationTimes = async (faculty, programmeFilter) => {
|
||||
const isAcademicYear = false // = yearType === 'ACADEMIC_YEAR'
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const levels = ['bachelor', 'bcMsCombo', 'master', 'doctor', 'licentiate']
|
||||
|
||||
let graduationAmounts = {}
|
||||
let graduationTimes = {}
|
||||
let medians = {}
|
||||
let means = {}
|
||||
|
||||
levels.forEach(level => {
|
||||
graduationAmounts[level] = getYearsObject({ years })
|
||||
graduationTimes[level] = getYearsObject({ years, emptyArrays: true })
|
||||
medians[level] = getYearsObject({ years, emptyArrays: true })
|
||||
means[level] = getYearsObject({ years, emptyArrays: true })
|
||||
})
|
||||
|
||||
// the max amount of months in the graph, depends on programme type
|
||||
// (times 2 to set goal time at the halfway point on graphs)
|
||||
const comparisonValues = {
|
||||
bachelor: 36 * 2,
|
||||
bcMsCombo: 60 * 2,
|
||||
master: 24 * 2,
|
||||
doctor: 48 * 2,
|
||||
// Fix differrent cases?
|
||||
licentiate: 78 * 2,
|
||||
}
|
||||
|
||||
// We count studyrights (vs. studyright_elements)
|
||||
// This way we get the whole time for a degree, even if the student was transferred to a new programme
|
||||
// E.g. started 8/2016 in old Bc, transferred to new 10/2020, graduated from new 1/2021 --> total 53 months (not 3)
|
||||
|
||||
const graduatedRights = await graduatedStudyrights(faculty, since)
|
||||
|
||||
for (const right of graduatedRights) {
|
||||
const { enddate, startdate, studyrightid, extentcode, studyrightElements, studentnumber } = right
|
||||
const { programme } = findRightProgramme(studyrightElements, 'graduated')
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
|
||||
|
||||
const graduationYear = defineYear(enddate, isAcademicYear)
|
||||
let actualStartdate = startdate
|
||||
let level = null
|
||||
|
||||
if (extentcode == 1) {
|
||||
level = 'bachelor'
|
||||
} else if (extentcode === 2) {
|
||||
if (studyrightid.slice(-2) === '-2') {
|
||||
level = 'bcMsCombo'
|
||||
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
|
||||
} else {
|
||||
level = 'master'
|
||||
}
|
||||
} else if (extentcode === 4) {
|
||||
level = 'doctor'
|
||||
} else if (extentcode === 3) {
|
||||
level = 'licentiate'
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
const absoluteTimeToGraduation = moment(enddate).diff(moment(actualStartdate), 'months')
|
||||
const statutoryAbsences = await getStatutoryAbsences(studentnumber, actualStartdate, enddate)
|
||||
const timeToGraduation = absoluteTimeToGraduation - statutoryAbsences
|
||||
|
||||
graduationAmounts[level][graduationYear] += 1
|
||||
graduationTimes[level][graduationYear] = [...graduationTimes[level][graduationYear], timeToGraduation]
|
||||
}
|
||||
|
||||
// HighCharts graph require the data to have this format (ie. actual value, "empty value")
|
||||
years.forEach(year => {
|
||||
levels.forEach(level => {
|
||||
const median = getMedian(graduationTimes[level][year])
|
||||
const mean = getMean(graduationTimes[level][year])
|
||||
medians[level][year] = [
|
||||
['', median],
|
||||
['', comparisonValues[level] - median],
|
||||
]
|
||||
means[level][year] = [
|
||||
['', mean],
|
||||
['', comparisonValues[level] - mean],
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
return { medians, means, graduationAmounts, years, levels }
|
||||
}
|
||||
|
||||
module.exports = { countGraduationTimes }
|
||||
@@ -0,0 +1,137 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
|
||||
const findRightProgramme = (studyrightElements, mode) => {
|
||||
let programme = ''
|
||||
let programmeName = ''
|
||||
let studyRightElement = null
|
||||
|
||||
if (studyrightElements) {
|
||||
if (mode === 'started') {
|
||||
studyRightElement = studyrightElements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(a.startdate) - new Date(b.startdate))[0]
|
||||
// this way round counts to old programmes, other way around to new programmes
|
||||
} else {
|
||||
// graduated
|
||||
studyRightElement = studyrightElements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]
|
||||
}
|
||||
if (studyRightElement) {
|
||||
programme = studyRightElement.code
|
||||
programmeName = studyRightElement.element_detail.name
|
||||
}
|
||||
}
|
||||
return { programme, programmeName }
|
||||
}
|
||||
|
||||
const facultyFormatStudyright = studyright => {
|
||||
const {
|
||||
studyrightid,
|
||||
studystartdate,
|
||||
enddate,
|
||||
givendate,
|
||||
graduated,
|
||||
active,
|
||||
prioritycode,
|
||||
extentcode,
|
||||
student,
|
||||
studyright_elements,
|
||||
startdate,
|
||||
} = studyright
|
||||
|
||||
return {
|
||||
studyrightid,
|
||||
studystartdate,
|
||||
startdate,
|
||||
enddate,
|
||||
givendate,
|
||||
graduated,
|
||||
active,
|
||||
prioritycode,
|
||||
extentcode,
|
||||
studentnumber: student.studentnumber,
|
||||
studyrightElements: studyright_elements,
|
||||
}
|
||||
}
|
||||
|
||||
const formatFacultyTransfer = transfer => {
|
||||
const { sourcecode, targetcode, transferdate, studyrightid } = transfer
|
||||
return {
|
||||
sourcecode,
|
||||
targetcode,
|
||||
transferdate,
|
||||
studyrightid,
|
||||
}
|
||||
}
|
||||
|
||||
const facultyFormatProgramme = programme => {
|
||||
const { code, name } = programme
|
||||
return { code, name }
|
||||
}
|
||||
|
||||
const formatFacultyThesisWriter = credit => {
|
||||
const { course_code, credits, attainment_date, student_studentnumber, course } = credit
|
||||
return {
|
||||
course_code,
|
||||
credits,
|
||||
attainment_date,
|
||||
student_studentnumber,
|
||||
courseUnitType: course.course_unit_type,
|
||||
}
|
||||
}
|
||||
|
||||
const formatOrganization = org => {
|
||||
const { id, name, code, parent_id } = org
|
||||
return { id, name, code, parentId: parent_id }
|
||||
}
|
||||
|
||||
const formatAbsence = absence => {
|
||||
const { semestercode, semester } = absence
|
||||
return { semestercode, start: semester.startdate, end: semester.enddate }
|
||||
}
|
||||
|
||||
const newProgrammes = [/^KH/, /^MH/, /^T/, /^LI/, /^K-/, /^FI/, /^00901$/, /^00910$/]
|
||||
|
||||
const isNewProgramme = code => {
|
||||
for (let i = 0; i < newProgrammes.length; i++) {
|
||||
if (newProgrammes[i].test(code)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const checkTransfers = (s, insideTransfersStudyrights, transfersToOrAwayStudyrights) => {
|
||||
if (transfersToOrAwayStudyrights.includes(s.studyrightid)) return true
|
||||
if (insideTransfersStudyrights.includes(s.studyrightid) && new Date(s.studystartdate) < new Date('2017-08-01'))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
const getExtentFilter = includeAllSpecials => {
|
||||
const filteredExtents = [16] // always filter out secondary subject students
|
||||
if (!includeAllSpecials) {
|
||||
filteredExtents.push(7, 34, 22, 99, 14, 13, 9)
|
||||
}
|
||||
let studyrightWhere = {
|
||||
extentcode: {
|
||||
[Op.notIn]: filteredExtents,
|
||||
},
|
||||
}
|
||||
return studyrightWhere
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findRightProgramme,
|
||||
facultyFormatStudyright,
|
||||
facultyFormatProgramme,
|
||||
formatFacultyTransfer,
|
||||
formatFacultyThesisWriter,
|
||||
formatOrganization,
|
||||
formatAbsence,
|
||||
isNewProgramme,
|
||||
checkTransfers,
|
||||
getExtentFilter,
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
const { redisClient } = require('../redis')
|
||||
const moment = require('moment')
|
||||
|
||||
const createRedisKeyForFacultyProgrammes = (id, programmeFilter) => `FACULTY_PROGRAMMES_${id}_${programmeFilter}`
|
||||
const createRedisKeyForBasicStats = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_BASIC_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
const createRedisKeyForCreditStats = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_CREDIT_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
// const createRedisKeyForGraduationTimeStats = (id, mode, programmeFilter) =>
|
||||
// `FACULTY_GRADUATION_TIME_STATS_${id}_${mode}__${programmeFilter}`
|
||||
const createRedisKeyForThesiswriters = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_THESIS_WRITERS_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
|
||||
const setFacultyProgrammes = async (id, data, programmeFilter) => {
|
||||
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
|
||||
const dataToRedis = {
|
||||
data: data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getFacultyProgrammes = async (id, programmeFilter) => {
|
||||
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setBasicStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getBasicStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setCreditStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getCreditStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setThesisWritersStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
const getThesisWritersStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setFacultyProgrammes,
|
||||
getFacultyProgrammes,
|
||||
setBasicStats,
|
||||
getBasicStats,
|
||||
setCreditStats,
|
||||
getCreditStats,
|
||||
setThesisWritersStats,
|
||||
getThesisWritersStats,
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const { mapToProviders } = require('../../util/utils')
|
||||
const { thesisWriters } = require('./faculty')
|
||||
const {
|
||||
getStatsBasis,
|
||||
getYearsArray,
|
||||
defineYear,
|
||||
getCorrectStudentnumbers,
|
||||
alltimeEndDate,
|
||||
alltimeStartDate,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
|
||||
const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }) => {
|
||||
const thesisTypes = [
|
||||
'urn:code:course-unit-type:bachelors-thesis',
|
||||
'urn:code:course-unit-type:masters-thesis',
|
||||
'urn:code:course-unit-type:doctors-thesis',
|
||||
'urn:code:course-unit-type:licentiate-thesis',
|
||||
]
|
||||
|
||||
let bachelors = getStatsBasis(years)
|
||||
let masters = getStatsBasis(years)
|
||||
let doctors = getStatsBasis(years)
|
||||
let licentiates = getStatsBasis(years)
|
||||
let programmeCounts = {}
|
||||
let programmeNames = {}
|
||||
|
||||
for (const { code, name } of facultyProgrammes) {
|
||||
const provider = mapToProviders([code])[0]
|
||||
const students = await getCorrectStudentnumbers({
|
||||
codes: [code],
|
||||
startDate: alltimeStartDate,
|
||||
endDate: alltimeEndDate,
|
||||
includeAllSpecials,
|
||||
})
|
||||
const thesisCourseCodes = await thesisWriters(provider, since, thesisTypes, students)
|
||||
thesisCourseCodes?.forEach(({ attainment_date, courseUnitType }) => {
|
||||
const thesisYear = defineYear(attainment_date, isAcademicYear)
|
||||
|
||||
if (!(code in programmeCounts)) {
|
||||
programmeCounts[code] = {}
|
||||
|
||||
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[code][year] = [0, 0, 0, 0, 0]))
|
||||
programmeNames[code] = name
|
||||
}
|
||||
programmeCounts[code][thesisYear][0] += 1
|
||||
|
||||
if (courseUnitType === thesisTypes[0]) {
|
||||
bachelors.graphStats[indexOf(years, thesisYear)] += 1
|
||||
bachelors.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][1] += 1
|
||||
} else if (courseUnitType === thesisTypes[1]) {
|
||||
masters.graphStats[indexOf(years, thesisYear)] += 1
|
||||
masters.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][2] += 1
|
||||
} else if (courseUnitType === thesisTypes[2]) {
|
||||
doctors.graphStats[indexOf(years, thesisYear)] += 1
|
||||
doctors.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][3] += 1
|
||||
} else if (courseUnitType === thesisTypes[3]) {
|
||||
licentiates.graphStats[indexOf(years, thesisYear)] += 1
|
||||
licentiates.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][4] += 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames }
|
||||
}
|
||||
|
||||
const getFacultyThesisWritersForStudyTrack = async (
|
||||
allThesisWriters,
|
||||
facultyProgrammes,
|
||||
isAcademicYear,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const queryParameters = { since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }
|
||||
const { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames } = await getFacultyThesisWriters(
|
||||
queryParameters
|
||||
)
|
||||
|
||||
allThesisWriters.years = years.map(year => year.toString())
|
||||
const reversedYears = years.reverse()
|
||||
allThesisWriters.tableStats = reversedYears.map(year => [
|
||||
year,
|
||||
bachelors.tableStats[year] + masters.tableStats[year] + doctors.tableStats[year] + licentiates.tableStats[year],
|
||||
bachelors.tableStats[year],
|
||||
masters.tableStats[year],
|
||||
doctors.tableStats[year],
|
||||
licentiates.tableStats[year],
|
||||
])
|
||||
allThesisWriters.graphStats.push({ name: 'Bachelors', data: bachelors.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Masters', data: masters.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Doctors', data: doctors.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Licentiates', data: licentiates.graphStats })
|
||||
|
||||
const programmes = programmeNames ? Object.keys(programmeNames) : facultyProgrammes.map(programme => programme.code)
|
||||
programmes.forEach(programmeCode => {
|
||||
reversedYears.forEach(year => {
|
||||
if (programmeCounts && programmeCode in programmeCounts) {
|
||||
if (!(programmeCode in allThesisWriters.programmeTableStats)) {
|
||||
allThesisWriters.programmeTableStats[programmeCode] = []
|
||||
}
|
||||
allThesisWriters.programmeTableStats[programmeCode].push([year, ...programmeCounts[programmeCode][year]])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
allThesisWriters.programmeNames = programmeNames
|
||||
}
|
||||
|
||||
const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType, specialGroups) => {
|
||||
let allThesisWriters = {
|
||||
id: faculty,
|
||||
years: [],
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
programmeTableStats: {},
|
||||
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Licentiates'],
|
||||
programmeNames: {},
|
||||
status: 'Done',
|
||||
lastUpdated: '',
|
||||
}
|
||||
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
|
||||
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
|
||||
|
||||
await getFacultyThesisWritersForStudyTrack(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
|
||||
|
||||
return allThesisWriters
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyThesisWriters }
|
||||
@@ -0,0 +1,113 @@
|
||||
const logger = require('../../util/logger')
|
||||
const { findFacultyProgrammeCodes } = require('./faculty')
|
||||
const { combineFacultyBasics } = require('./facultyBasics')
|
||||
const { combineFacultyCredits } = require('./facultyCredits')
|
||||
const { combineFacultyThesisWriters } = require('./facultyThesisWriters')
|
||||
const { setFacultyProgrammes, setBasicStats, setCreditStats, setThesisWritersStats } = require('./facultyService')
|
||||
|
||||
const updateFacultyOverview = async faculty => {
|
||||
const calendarNewSpecial = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
const calendarNewSpecialOut = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
|
||||
const calendarAllSpecial = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
const calendarAllSpecialOut = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
const academicNewSpecial = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
|
||||
const academicAllSpecial = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
|
||||
const academicNewSpecialOut = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
|
||||
const academicAllSpecialOut = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
|
||||
const options = [
|
||||
calendarNewSpecial,
|
||||
calendarAllSpecial,
|
||||
academicNewSpecial,
|
||||
academicAllSpecial,
|
||||
calendarNewSpecialOut,
|
||||
calendarAllSpecialOut,
|
||||
academicNewSpecialOut,
|
||||
academicAllSpecialOut,
|
||||
]
|
||||
let allProgrammes = []
|
||||
let newProgrammes = []
|
||||
let allProgrammeCodes = []
|
||||
|
||||
try {
|
||||
const all = await findFacultyProgrammeCodes(faculty, 'ALL_PROGRAMMES')
|
||||
const onlyNew = await findFacultyProgrammeCodes(faculty, 'NEW_STUDY_PROGRAMMES')
|
||||
allProgrammes = await setFacultyProgrammes(faculty, all, 'ALL_PROGRAMMES')
|
||||
newProgrammes = await setFacultyProgrammes(faculty, onlyNew, 'NEW_STUDY_PROGRAMMES')
|
||||
allProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
for (const option of options) {
|
||||
const { yearType, programmeFilter, specialGroups } = option
|
||||
try {
|
||||
const updatedStudentInfo = await combineFacultyBasics(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
yearType,
|
||||
allProgrammeCodes,
|
||||
programmeFilter,
|
||||
specialGroups
|
||||
)
|
||||
await setBasicStats(updatedStudentInfo, option.yearType, option.programmeFilter, option.specialGroups)
|
||||
|
||||
const updatedCredits = await combineFacultyCredits(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
yearType,
|
||||
specialGroups
|
||||
)
|
||||
await setCreditStats(updatedCredits, yearType, programmeFilter, specialGroups)
|
||||
|
||||
const updateThesisWriters = await combineFacultyThesisWriters(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
yearType,
|
||||
specialGroups
|
||||
)
|
||||
await setThesisWritersStats(updateThesisWriters, yearType, programmeFilter, specialGroups)
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
return 'OK'
|
||||
}
|
||||
|
||||
module.exports = { updateFacultyOverview }
|
||||
@@ -1,316 +0,0 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
transferredInsideFaculty,
|
||||
transferredAway,
|
||||
transferredTo,
|
||||
getProgrammeName,
|
||||
} = require('./faculty')
|
||||
const { getStatsBasis, getYearsArray, defineYear } = require('./studyprogrammeHelpers')
|
||||
const { findRightProgramme } = require('./facultyHelpers')
|
||||
|
||||
const filterDuplicateStudyrights = studyrights => {
|
||||
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
|
||||
// choose only the earlier started one, so we don't count start of masters as starting in faculty
|
||||
let rightsToCount = {}
|
||||
|
||||
studyrights.forEach(right => {
|
||||
const id = right.studyrightid.slice(0, -2)
|
||||
const start = new Date(right.studystartdate)
|
||||
if (id in rightsToCount) {
|
||||
if (new Date(rightsToCount[id].studystartdate) > start) {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
} else {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
})
|
||||
return Object.values(rightsToCount)
|
||||
}
|
||||
|
||||
const addProgramme = async (stats, programme, tableStats, transferYear, index, allBasics) => {
|
||||
if (!(programme in stats)) {
|
||||
stats[programme] = {}
|
||||
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0, 0, 0]))
|
||||
}
|
||||
stats[programme][transferYear][index] += 1
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
const { name } = await getProgrammeName(programme)
|
||||
allBasics.programmeNames[programme] = name
|
||||
}
|
||||
}
|
||||
|
||||
const getFacultyStarters = async (
|
||||
faculty,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
counts,
|
||||
allBasics,
|
||||
programmeData
|
||||
) => {
|
||||
const startedGraphStats = [...graphStats]
|
||||
const startedTableStats = { ...tableStats }
|
||||
const studyrights = await startedStudyrights(faculty, since)
|
||||
const filteredStudyrights = filterDuplicateStudyrights(studyrights)
|
||||
const programmeTableStats = {}
|
||||
|
||||
filteredStudyrights.forEach(({ studystartdate, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'started')
|
||||
const startYear = defineYear(studystartdate, isAcademicYear)
|
||||
startedGraphStats[indexOf(yearsArray, startYear)] += 1
|
||||
startedTableStats[startYear] += 1
|
||||
|
||||
if (!(programme in programmeTableStats)) {
|
||||
programmeTableStats[programme] = { ...tableStats }
|
||||
}
|
||||
programmeTableStats[programme][startYear] += 1
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programme] = programmeName
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
|
||||
programmeData['started'] = programmeTableStats
|
||||
|
||||
Object.keys(startedTableStats).forEach(year => {
|
||||
counts[year] = [startedTableStats[year]]
|
||||
})
|
||||
}
|
||||
|
||||
const getFacultyGraduates = async (
|
||||
faculty,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
countsGraduations,
|
||||
programmeData
|
||||
) => {
|
||||
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
|
||||
const graduatedTableStats = {}
|
||||
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
|
||||
const programmeTableStats = {}
|
||||
|
||||
const graduatedRights = await graduatedStudyrights(faculty, since)
|
||||
|
||||
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
|
||||
const endYear = defineYear(enddate, isAcademicYear)
|
||||
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][0] += 1
|
||||
|
||||
if (!(programme in programmeTableStats)) {
|
||||
programmeTableStats[programme] = {}
|
||||
Object.keys(tableStats).forEach(year => (programmeTableStats[programme][year] = [0, 0, 0, 0, 0]))
|
||||
}
|
||||
programmeTableStats[programme][endYear][0] += 1
|
||||
|
||||
if (extentcode === 1) {
|
||||
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][1] += 1
|
||||
programmeTableStats[programme][endYear][1] += 1
|
||||
} else if (extentcode === 2) {
|
||||
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][2] += 1
|
||||
programmeTableStats[programme][endYear][2] += 1
|
||||
} else if (extentcode === 4) {
|
||||
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][3] += 1
|
||||
programmeTableStats[programme][endYear][3] += 1
|
||||
} else {
|
||||
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][4] += 1
|
||||
programmeTableStats[programme][endYear][4] += 1
|
||||
}
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programme] = programmeName
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Graduated', data: graduatedGraphStats[0] })
|
||||
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'All graduations', data: graduatedGraphStats[0] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Bachelors', data: graduatedGraphStats[1] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Masters', data: graduatedGraphStats[2] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Doctors', data: graduatedGraphStats[3] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Others', data: graduatedGraphStats[4] })
|
||||
|
||||
programmeData['graduated'] = programmeTableStats
|
||||
|
||||
Object.keys(graduatedTableStats).forEach(year => {
|
||||
counts[year] = counts[year].concat(graduatedTableStats[year][0])
|
||||
countsGraduations[year] = graduatedTableStats[year]
|
||||
})
|
||||
}
|
||||
|
||||
const getFacultyTransfers = async (
|
||||
programmes,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData
|
||||
) => {
|
||||
const insideTransfers = await transferredInsideFaculty(programmes, since)
|
||||
const awayTransfers = await transferredAway(programmes, since)
|
||||
const toTransfers = await transferredTo(programmes, since)
|
||||
|
||||
const transferGraphStats = [[...graphStats], [...graphStats], [...graphStats]]
|
||||
const transferTableStats = {}
|
||||
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0, 0, 0]))
|
||||
const programmeTableStats = {}
|
||||
|
||||
// inside transfer is counted towards target
|
||||
for (const { transferdate, targetcode } of insideTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][0] += 1
|
||||
await addProgramme(programmeTableStats, targetcode, tableStats, transferYear, 0, allBasics)
|
||||
}
|
||||
for (const { transferdate, sourcecode } of awayTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][1] += 1
|
||||
await addProgramme(programmeTableStats, sourcecode, tableStats, transferYear, 1, allBasics)
|
||||
}
|
||||
|
||||
for (const { transferdate, targetcode } of toTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][2] += 1
|
||||
await addProgramme(programmeTableStats, targetcode, tableStats, transferYear, 2, allBasics)
|
||||
}
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred inside', data: transferGraphStats[0] })
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
|
||||
|
||||
programmeData['transferred'] = programmeTableStats
|
||||
|
||||
Object.keys(transferTableStats).forEach(year => {
|
||||
counts[year] = counts[year].concat(transferTableStats[year])
|
||||
})
|
||||
}
|
||||
|
||||
const combineFacultyBasics = async (allBasics, faculty, programmes, yearType) => {
|
||||
let counts = {}
|
||||
let countsGraduations = {}
|
||||
let years = []
|
||||
let programmeData = {}
|
||||
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const { graphStats, tableStats } = getStatsBasis(yearsArray)
|
||||
Object.keys(tableStats).forEach(year => years.push(year))
|
||||
|
||||
// Started studying in faculty
|
||||
await getFacultyStarters(
|
||||
faculty,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
counts,
|
||||
allBasics,
|
||||
programmeData
|
||||
)
|
||||
|
||||
// Graduated in faculty
|
||||
await getFacultyGraduates(
|
||||
faculty,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
countsGraduations,
|
||||
programmeData
|
||||
)
|
||||
|
||||
// Transfers
|
||||
await getFacultyTransfers(
|
||||
programmes,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData
|
||||
)
|
||||
|
||||
// combine tableStats from all categories
|
||||
allBasics.years = years
|
||||
years.forEach(year => {
|
||||
allBasics.studentInfo.tableStats.push([year, ...counts[year]])
|
||||
allBasics.graduationInfo.tableStats.push([year, ...countsGraduations[year]])
|
||||
})
|
||||
allBasics.studentInfo.tableStats.reverse()
|
||||
allBasics.graduationInfo.tableStats.reverse()
|
||||
|
||||
//combine programme level tablestats
|
||||
// all programmes are not present in all data types, check all found programmes and patch missing details with 0
|
||||
|
||||
let allCodes = Object.keys(programmeData.started).concat(
|
||||
Object.keys(programmeData.graduated),
|
||||
Object.keys(programmeData.transferred)
|
||||
)
|
||||
allCodes = [...new Set(allCodes)]
|
||||
|
||||
let studentInfo = {}
|
||||
let graduationInfo = {}
|
||||
|
||||
allCodes.forEach(code => {
|
||||
if (!(code in studentInfo)) studentInfo[code] = {}
|
||||
|
||||
for (const year of years) {
|
||||
// started
|
||||
if (code in programmeData.started) {
|
||||
studentInfo[code][year] = [year, programmeData.started[code][year]]
|
||||
} else {
|
||||
studentInfo[code][year] = [year, 0]
|
||||
}
|
||||
// graduated total
|
||||
if (code in programmeData.graduated) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([programmeData.graduated[code][year][0]])
|
||||
if (!(code in graduationInfo)) graduationInfo[code] = {}
|
||||
graduationInfo[code][year] = [year, ...programmeData.graduated[code][year].slice(1)]
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0])
|
||||
}
|
||||
// trasferred
|
||||
if (code in programmeData.transferred) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(studentInfo).forEach(prog => {
|
||||
allBasics.studentInfo.programmeTableStats[prog] = [...Object.values(studentInfo[prog])].reverse()
|
||||
})
|
||||
|
||||
Object.keys(graduationInfo).forEach(prog => {
|
||||
allBasics.graduationInfo.programmeTableStats[prog] = Object.values(graduationInfo[prog]).reverse()
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyBasics }
|
||||
@@ -1,69 +0,0 @@
|
||||
const findRightProgramme = (studyrightElements, mode) => {
|
||||
let programme = ''
|
||||
let programmeName = ''
|
||||
let studyRightElement = null
|
||||
|
||||
if (studyrightElements) {
|
||||
if (mode === 'started') {
|
||||
studyRightElement = studyrightElements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(a.startdate) - new Date(b.startdate))[0]
|
||||
// this way round counts to old programmes, other way around to new programmes
|
||||
} else {
|
||||
// graduated
|
||||
studyRightElement = studyrightElements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]
|
||||
}
|
||||
if (studyRightElement) {
|
||||
programme = studyRightElement.code
|
||||
programmeName = studyRightElement.element_detail.name
|
||||
}
|
||||
}
|
||||
return { programme, programmeName }
|
||||
}
|
||||
|
||||
const facultyFormatStudyright = studyright => {
|
||||
const {
|
||||
studyrightid,
|
||||
studystartdate,
|
||||
enddate,
|
||||
givendate,
|
||||
graduated,
|
||||
active,
|
||||
prioritycode,
|
||||
extentcode,
|
||||
student,
|
||||
studyright_elements,
|
||||
} = studyright
|
||||
|
||||
return {
|
||||
studyrightid,
|
||||
studystartdate,
|
||||
enddate,
|
||||
givendate,
|
||||
graduated,
|
||||
active,
|
||||
prioritycode,
|
||||
extentcode,
|
||||
studentnumber: student.studentnumber,
|
||||
studyrightElements: studyright_elements,
|
||||
}
|
||||
}
|
||||
|
||||
const formatFacultyTransfer = transfer => {
|
||||
const { sourcecode, targetcode, transferdate, studyrightid } = transfer
|
||||
return {
|
||||
sourcecode,
|
||||
targetcode,
|
||||
transferdate,
|
||||
studyrightid,
|
||||
}
|
||||
}
|
||||
|
||||
const facultyFormatProgramme = programme => {
|
||||
const { code, name } = programme
|
||||
return { code, name }
|
||||
}
|
||||
|
||||
module.exports = { findRightProgramme, facultyFormatStudyright, facultyFormatProgramme, formatFacultyTransfer }
|
||||
@@ -210,7 +210,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
raw: true,
|
||||
}),
|
||||
Enrollment.findAll({
|
||||
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber'],
|
||||
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber', 'semestercode'],
|
||||
where: {
|
||||
enrollment_date_time: {
|
||||
[Op.between]: [attainmentDateFrom, endDate],
|
||||
@@ -414,7 +414,7 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
// db startdate is formatted to utc so need to change it when querying
|
||||
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
|
||||
|
||||
const filteredExtents = [10] // always filter out secondary subject students
|
||||
const filteredExtents = [16] // always filter out secondary subject students
|
||||
let studyrightWhere = {
|
||||
extentcode: {
|
||||
[Op.notIn]: filteredExtents,
|
||||
@@ -424,7 +424,7 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
filteredExtents.push(7, 34)
|
||||
}
|
||||
if (!nondegreeStudents) {
|
||||
filteredExtents.push(33, 99, 14, 13)
|
||||
filteredExtents.push(22, 99, 14, 13)
|
||||
}
|
||||
|
||||
let studentWhere = {}
|
||||
|
||||
@@ -121,7 +121,7 @@ const getUnifyStatus = unifyCourses => {
|
||||
}
|
||||
|
||||
const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCourses = 'unifyStats') => {
|
||||
const { startdate } = await Semester.findOne({
|
||||
const { startdate, semestercode: fromSemester } = await Semester.findOne({
|
||||
where: {
|
||||
[separate ? 'semestercode' : 'yearcode']: from,
|
||||
},
|
||||
@@ -129,7 +129,7 @@ const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCo
|
||||
limit: 1,
|
||||
raw: true,
|
||||
})
|
||||
const { enddate } = await Semester.findOne({
|
||||
const { enddate, semestercode: toSemester } = await Semester.findOne({
|
||||
where: {
|
||||
[separate ? 'semestercode' : 'yearcode']: to,
|
||||
},
|
||||
@@ -164,7 +164,7 @@ const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCo
|
||||
WHERE
|
||||
e.studentnumber = s.studentnumber
|
||||
AND e.course_code IN (:coursecodes)
|
||||
AND e.enrollment_date_time BETWEEN '${startdate.toISOString()}' AND '${enddate.toISOString()}'
|
||||
AND e.semestercode BETWEEN ${fromSemester} AND ${toSemester}
|
||||
AND e.enrollment_date_time >= '2021-05-31'
|
||||
AND e.state IN ('ENROLLED', 'CONFIRMED')
|
||||
);
|
||||
|
||||
@@ -31,6 +31,19 @@ const sinceDate = since => {
|
||||
const studytrackStudents = async studentnumbers =>
|
||||
(
|
||||
await Student.findAll({
|
||||
include: {
|
||||
model: Credit,
|
||||
separate: true,
|
||||
attributes: ['credits', 'attainment_date'],
|
||||
where: {
|
||||
isStudyModule: {
|
||||
[Op.eq]: null,
|
||||
},
|
||||
credittypecode: {
|
||||
[Op.in]: [4, 9],
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: studentnumbers,
|
||||
@@ -577,6 +590,36 @@ const getOtherStudentsForProgrammeCourses = async (from, to, programmeCourses, s
|
||||
}))
|
||||
}
|
||||
|
||||
const getTransferStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
|
||||
const res = await sequelize.query(
|
||||
`
|
||||
WITH Dist AS (
|
||||
SELECT DISTINCT cr.student_studentnumber AS student, cr.credits AS credits,
|
||||
co.code AS code, co.name AS course_name FROM credit cr
|
||||
INNER JOIN course co ON cr.course_code = co.code
|
||||
WHERE cr.attainment_date BETWEEN :from AND :to
|
||||
AND (cr."isStudyModule" = false OR cr."isStudyModule" IS NULL)
|
||||
AND cr.course_code IN (:programmeCourses)
|
||||
AND cr.credittypecode = 9
|
||||
)
|
||||
SELECT COUNT(student) AS total_students, SUM(credits) AS total_credits, code, course_name
|
||||
FROM Dist
|
||||
GROUP BY dist.code, course_name;
|
||||
`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: { from, to, programmeCourses },
|
||||
}
|
||||
)
|
||||
return res.map(course => ({
|
||||
code: course.code,
|
||||
name: course.course_name,
|
||||
totalTransferStudents: parseInt(course.total_students),
|
||||
totalTransferCredits: parseInt(course.total_credits),
|
||||
type: 'transfer',
|
||||
}))
|
||||
}
|
||||
|
||||
const getStudentsWithoutStudyrightForProgrammeCourses = async (from, to, programmeCourses) => {
|
||||
const res = await sequelize.query(
|
||||
`
|
||||
@@ -716,4 +759,5 @@ module.exports = {
|
||||
getAllProgrammeCourses,
|
||||
getStudyRights,
|
||||
getCourseCodesForStudyProgramme,
|
||||
getTransferStudentsForProgrammeCourses,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const {
|
||||
getCurrentStudyYearStartDate,
|
||||
getOwnStudentsForProgrammeCourses,
|
||||
getStudentsWithoutStudyrightForProgrammeCourses,
|
||||
getTransferStudentsForProgrammeCourses,
|
||||
getOtherStudentsForProgrammeCourses,
|
||||
getAllProgrammeCourses,
|
||||
} = require('./studyprogramme')
|
||||
@@ -50,6 +51,9 @@ const makeYearlyPromises = (years, academicYear, type, programmeCourses, studypr
|
||||
case 'otherStudents':
|
||||
result = await getOtherStudentsForProgrammeCourses(from, to, programmeCourses, studyprogramme)
|
||||
break
|
||||
case 'transfer':
|
||||
result = await getTransferStudentsForProgrammeCourses(from, to, programmeCourses)
|
||||
break
|
||||
default:
|
||||
result = await getStudentsForProgrammeCourses(from, to, programmeCourses)
|
||||
}
|
||||
@@ -92,16 +96,26 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
studyprogramme
|
||||
)
|
||||
|
||||
const yearlyTransferStudentsPromises = makeYearlyPromises(
|
||||
yearRange,
|
||||
academicYear,
|
||||
'transfer',
|
||||
programmeCourses,
|
||||
studyprogramme
|
||||
)
|
||||
|
||||
const [
|
||||
yearlyStudentByCourse,
|
||||
yearlyProgrammeStudents,
|
||||
yearlyStudentsWithoutStudyright,
|
||||
yearlyOtherProgrammeStudents,
|
||||
yearlyTransferStudents,
|
||||
] = await Promise.all([
|
||||
Promise.all(yearlyStudentByCoursePromises),
|
||||
Promise.all(yearlyProgrammeStudentsPromises),
|
||||
Promise.all(yearlyStudentsWithoutStudyrightPromises),
|
||||
Promise.all(yearlyOtherProgrammeStudentsPromises),
|
||||
Promise.all(yearlyTransferStudentsPromises),
|
||||
])
|
||||
|
||||
let maxYear = 0
|
||||
@@ -110,6 +124,7 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
...yearlyProgrammeStudents.flat(),
|
||||
...yearlyStudentsWithoutStudyright.flat(),
|
||||
...yearlyOtherProgrammeStudents.flat(),
|
||||
...yearlyTransferStudents.flat(),
|
||||
].reduce((acc, curr) => {
|
||||
if (curr.year > maxYear) maxYear = curr.year
|
||||
if (!acc[curr.code]) {
|
||||
@@ -129,6 +144,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
totalOtherProgrammeCredits: 0,
|
||||
totalWithoutStudyrightStudents: 0,
|
||||
totalWithoutStudyrightCredits: 0,
|
||||
totalTransferStudents: 0,
|
||||
totalTransferCredits: 0,
|
||||
}
|
||||
}
|
||||
switch (curr.type) {
|
||||
@@ -140,16 +157,17 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
acc[curr.code]['years'][curr.year]['totalProgrammeStudents'] += curr.totalProgrammeStudents
|
||||
acc[curr.code]['years'][curr.year]['totalProgrammeCredits'] += curr.totalProgrammeCredits
|
||||
break
|
||||
|
||||
case 'otherProgramme':
|
||||
acc[curr.code]['years'][curr.year]['totalOtherProgrammeStudents'] += curr.totalOtherProgrammeStudents
|
||||
acc[curr.code]['years'][curr.year]['totalOtherProgrammeCredits'] += curr.totalOtherProgrammeCredits
|
||||
break
|
||||
|
||||
case 'noStudyright':
|
||||
acc[curr.code]['years'][curr.year]['totalWithoutStudyrightStudents'] += curr.totalWithoutStudyrightStudents
|
||||
acc[curr.code]['years'][curr.year]['totalWithoutStudyrightCredits'] += curr.totalWithoutStudyrightCredits
|
||||
break
|
||||
case 'transfer':
|
||||
acc[curr.code]['years'][curr.year]['totalTransferStudents'] += curr.totalTransferStudents
|
||||
acc[curr.code]['years'][curr.year]['totalTransferCredits'] += curr.totalTransferCredits
|
||||
}
|
||||
|
||||
return acc
|
||||
@@ -164,6 +182,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
'totalOtherProgrammeCredits',
|
||||
'totalWithoutStudyrightStudents',
|
||||
'totalWithoutStudyrightCredits',
|
||||
'totalTransferCredits',
|
||||
'totalTransferStudents',
|
||||
]
|
||||
ayCourses.forEach(ayCourse => {
|
||||
const normCode = isOpenUniCourseCode(ayCourse)[1]
|
||||
@@ -187,6 +207,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
totalOtherProgrammeCredits: 0,
|
||||
totalWithoutStudyrightStudents: 0,
|
||||
totalWithoutStudyrightCredits: 0,
|
||||
totalTransferCredits: 0,
|
||||
totalTransferStudents: 0,
|
||||
}
|
||||
} else {
|
||||
mergedCourse['years'][year] = { ...allCourses[normCode]['years'][year] }
|
||||
|
||||
@@ -76,12 +76,13 @@ const formatStudyright = studyright => {
|
||||
}
|
||||
|
||||
const formatStudent = student => {
|
||||
const { studentnumber, gender_code, home_country_en, creditcount } = student
|
||||
const { studentnumber, gender_code, home_country_en, creditcount, credits } = student
|
||||
return {
|
||||
studentnumber,
|
||||
gender_code,
|
||||
home_country_en,
|
||||
creditcount,
|
||||
credits,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +156,7 @@ const isSpecialGroupCredit = (studyrights, attainment_date, transfers) =>
|
||||
if (!studyright) return true // If there is no studyright matching the credit, is not a major student credit
|
||||
if (studyright.studystartdate > attainment_date) return true // Credits before the studyright started are not major student credits
|
||||
if (studyright.enddate && attainment_date > studyright.enddate) return true // Credits after studyright are not major student credits
|
||||
if ([7, 9, 16, 34, 33, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
|
||||
if ([7, 9, 16, 34, 22, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
|
||||
if (transfers.includes(studyright.studyrightid)) return true // Excludes both transfers in and out of the programme
|
||||
return false
|
||||
})
|
||||
@@ -349,8 +350,8 @@ const tableTitles = {
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non major students credits',
|
||||
'Non degree students credits',
|
||||
'Non-major students credits',
|
||||
'Non-degree students credits',
|
||||
'Transferred credits',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -26,11 +26,15 @@ const {
|
||||
} = require('./studyprogramme')
|
||||
const { getAcademicYearDates } = require('../util/semester')
|
||||
|
||||
const getStudentData = (students, thresholdKeys, thresholdAmounts) => {
|
||||
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) => {
|
||||
let data = { female: 0, male: 0, finnish: 0 }
|
||||
thresholdKeys.forEach(t => (data[t] = 0))
|
||||
|
||||
students.forEach(({ gender_code, home_country_en, creditcount }) => {
|
||||
students.forEach(({ gender_code, home_country_en, credits }) => {
|
||||
const creditcount = credits
|
||||
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
|
||||
.reduce((prev, curr) => (prev += curr.credits), 0)
|
||||
|
||||
data.male += gender_code === '1' ? 1 : 0
|
||||
data.female += gender_code === '2' ? 1 : 0
|
||||
data.finnish += home_country_en === 'Finland' ? 1 : 0
|
||||
@@ -121,7 +125,7 @@ const getStudytrackDataForTheYear = async ({
|
||||
// Get all the studyrights and students for the calculations
|
||||
const all = await allStudyrights(track, studentnumbers)
|
||||
const students = await studytrackStudents(studentnumbers)
|
||||
const studentData = getStudentData(students, creditThresholdKeys, creditThresholdAmounts)
|
||||
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
|
||||
const started = await startedStudyrights(track, startDate, studentnumbers)
|
||||
const enrolled = await enrolledStudents(track, startDate, studentnumbers)
|
||||
const absent = await absentStudents(track, studentnumbers)
|
||||
|
||||
@@ -43,7 +43,7 @@ const getCategoriesAndYears = async () => {
|
||||
|
||||
const creditsWithTeachersForYear = yearcode =>
|
||||
Credit.findAll({
|
||||
attributes: ['id', 'credits', 'credittypecode', 'isStudyModule'],
|
||||
attributes: ['id', 'credits', 'credittypecode', 'isStudyModule', 'is_open'],
|
||||
include: [
|
||||
{
|
||||
model: Semester,
|
||||
@@ -106,21 +106,20 @@ const findTopTeachers = async yearcode => {
|
||||
credits
|
||||
.filter(isRegularCourse)
|
||||
.map(credit => {
|
||||
const { credits, course, credittypecode } = credit
|
||||
const { credits, credittypecode, is_open } = credit
|
||||
const teachers = credit.teachers
|
||||
.filter(({ id }) => !id.includes('hy-hlo-org')) // Remove faculties from the leaderboards
|
||||
.map(({ id, name }) => ({ id, name }))
|
||||
const passed = Credit.passed(credit) || Credit.improved(credit)
|
||||
const failed = Credit.failed(credit)
|
||||
const transferred = credittypecode === 9
|
||||
const isOpenUni = course.code[0] === 'A'
|
||||
return { passed, failed, credits, teachers, isOpenUni, transferred }
|
||||
return { passed, failed, credits, teachers, is_open, transferred }
|
||||
})
|
||||
.forEach(credit => {
|
||||
const { passed, failed, credits, teachers, isOpenUni, transferred } = credit
|
||||
const { passed, failed, credits, teachers, is_open, transferred } = credit
|
||||
teachers.forEach(teacher => {
|
||||
all[teacher.id] = updatedStats(all, teacher, passed, failed, credits, transferred)
|
||||
if (isOpenUni) {
|
||||
if (is_open) {
|
||||
openuni[teacher.id] = updatedStats(openuni, teacher, passed, failed, credits, transferred)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ const { User, UserElementDetails, AccessGroup, UserFaculties, sequelizeUser } =
|
||||
const { getOrganizationAccess } = require('../util/organizationAccess')
|
||||
|
||||
const courseStatisticsGroup = 'grp-oodikone-basic-users'
|
||||
const facultyStatisticsGroup = 'grp-oodikone-users'
|
||||
const hyOneGroup = 'hy-one'
|
||||
const toskaGroup = 'grp-toska'
|
||||
|
||||
@@ -265,6 +266,8 @@ const getUser = async ({ username, name, email, iamGroups, sisId }) => {
|
||||
let newAccessGroups = []
|
||||
if (await checkStudyGuidanceGroupsAccess(sisId)) newAccessGroups.push('studyGuidanceGroups')
|
||||
if (iamGroups.includes(courseStatisticsGroup)) newAccessGroups.push('courseStatistics')
|
||||
if (iamGroups.includes(facultyStatisticsGroup) || iamGroups.some(element => /-jory$/.test(element)))
|
||||
newAccessGroups.push('facultyStatistics')
|
||||
if (iamGroups.includes(hyOneGroup) || currentAccessGroups.includes('teachers')) newAccessGroups.push('teachers')
|
||||
if (iamGroups.includes(toskaGroup) || currentAccessGroups.includes('admin')) newAccessGroups.push('admin')
|
||||
if (_.difference(newAccessGroups, currentAccessGroups).length > 0) {
|
||||
|
||||
Generated
+8
-8
@@ -6,7 +6,7 @@
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@reduxjs/toolkit": "^1.8.5",
|
||||
"@sentry/browser": "^6.18.2",
|
||||
"@sentry/tracing": "^6.18.2",
|
||||
"axios": "^0.21.4",
|
||||
@@ -18,7 +18,7 @@
|
||||
"history": "^4.10.1",
|
||||
"immer": "^9.0.12",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"moment": "^2.29.4",
|
||||
"path-to-regexp": "^0.1.7",
|
||||
"piwik-react-router": "^0.12.1",
|
||||
"prop-types": "^15.8.1",
|
||||
@@ -2841,9 +2841,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.3.tgz",
|
||||
"integrity": "sha512-lU/LDIfORmjBbyDLaqFN2JB9YmAT1BElET9y0ZszwhSBa5Ef3t6o5CrHupw5J1iOXwd+o92QfQZ8OJpwXvsssg==",
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.5.tgz",
|
||||
"integrity": "sha512-f4D5EXO7A7Xq35T0zRbWq5kJQyXzzscnHKmjnu2+37B3rwHU6mX9PYlbfXdnxcY6P/7zfmjhgan0Z+yuOfeBmA==",
|
||||
"dependencies": {
|
||||
"immer": "^9.0.7",
|
||||
"redux": "^4.1.2",
|
||||
@@ -24283,9 +24283,9 @@
|
||||
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw=="
|
||||
},
|
||||
"@reduxjs/toolkit": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.3.tgz",
|
||||
"integrity": "sha512-lU/LDIfORmjBbyDLaqFN2JB9YmAT1BElET9y0ZszwhSBa5Ef3t6o5CrHupw5J1iOXwd+o92QfQZ8OJpwXvsssg==",
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.5.tgz",
|
||||
"integrity": "sha512-f4D5EXO7A7Xq35T0zRbWq5kJQyXzzscnHKmjnu2+37B3rwHU6mX9PYlbfXdnxcY6P/7zfmjhgan0Z+yuOfeBmA==",
|
||||
"requires": {
|
||||
"immer": "^9.0.7",
|
||||
"redux": "^4.1.2",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@reduxjs/toolkit": "^1.8.5",
|
||||
"@sentry/browser": "^6.18.2",
|
||||
"@sentry/tracing": "^6.18.2",
|
||||
"axios": "^0.21.4",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
export default {
|
||||
ProgrammeToggle: `
|
||||
New study programmes: Näyttää kaikissa luvuissa ja kuvaajissa pelkästään uuden opetussuunnitelman ohjelmat.
|
||||
All programmes: Näyttää kaikissa luvuissa ja kuvaajissa kuvaajissa myös vanhan opetussuunnitelman ohjelmat.`,
|
||||
YearToggle: `
|
||||
Academic year: Näyttää tilastot akateemisille vuosille jaoteltuna, esimerkiksi 1.8.2019-31.7.2020
|
||||
Calendar year: Näyttää tilastot kalenterivuosille jaoteltuna, esimerkiksi 1.1.2019-31.12.2019
|
||||
`,
|
||||
StudentsOfTheFaculty: `
|
||||
**Started studying**: Sisältää kyseisenä vuonna tiedekunnassa aloittaneet opiskelijat (aloitetut opinto-oikeudet) riippumatta siitä, minä vuonna kyseinen opiskeluoikeus on myönnetty. Opiskelija on myös voinut myöhemmin keskeyttää opintonsa tai siirtyä pois ohjelmasta.
|
||||
Kandi-maisteri-opinto-oikeudellisista opiskelijoista lasketaan vain kandissa ts. tiedekunnassa aloitus, ei toistamiseen enää maisteriopintojen aloitusta.\n
|
||||
**Graduated**: Sisältää kyseisenä vuonna valmistuneet opiskelijat riippumatta siitä, minä vuonna opiskeluoikeus ohjelmaan on myönnetty. Jaottelu on tehty opinto-oikeuden päättymispäivän (=valmistumispäivän) perusteella.
|
||||
|
||||
**Transferred inside**: Sisältää kyseisenä vuonna tiedekunnan sisällä siirtyneet opiskelijat (opinto-oikeudet). Ohjelmakohtaisissa tilastoissa siirto on laskettu kohdeohjelman tilastoihin.
|
||||
|
||||
**Transferred away**: Sisältää kyseisenä vuonna pois tiedekunnasta siirtyneet opiskelijat (opinto-oikeudet). Sama opiskelija voi esiintyä tilastoissa useana vuonna, mikäli hän on siirtynyt tiedekuntaan ja siitä pois useasti.
|
||||
|
||||
**Transferred to**: Sisältää kyseisenä vuonna tiedekuntaan siirtyneet opiskelijat (opinto-oikeudet).
|
||||
|
||||
HUOM! Siirtyneiden määrä on erityisen suuri vuonna 2020, jolloin vanhoista koulutusohjelmista luovuttiin ja opiskelijat siirtyivät uusiin ohjelmiin.
|
||||
`,
|
||||
CreditsProducedByTheFaculty: `
|
||||
Sisältää opintopisteet suoritusvuosittain. Suoritukset on jaoteltu Sisussa näkyvän kurssin suorituspäivän mukaan.
|
||||
|
||||
**Major students credits**: Sisältää ne opintopisteet, joiden suorittajalla on suoritushetkellä ollut ensisijainen opinto-oikeus johonkin tiedekunnan koulutusohjelmaan. Sisältää myös tiedekunnan ohjelmaan siirtyneet ensisijaiset opiskelijat.
|
||||
|
||||
**Non-major faculty students credits**: Sisältää tiedekunnan koulutusohjelmien tuottamat opintopisteet, joiden suorittajalla EI ole ollut suoritushetkellä ensisijaista opinto-oikeutta kyseiseen koulutusohjelmaan,
|
||||
MUTTA hänellä on sellainen johonkin toiseen tiedekunnan ohjelmaan. Sisältää esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat.
|
||||
Sisältää myös opintopisteet, jotka opiskelija on suorittanut koulutusohjelmaan ennen opintojen aloittamista koulutusohjelmassa tai opintojen päätyttyä.
|
||||
|
||||
**Non-major other faculty students credits**: Sisältää tiedekunnan koulutusohjelmien tuottamat opintopisteet, joiden suorittajalla EI ole ollut suoritushetkellä ensisijaista opinto-oikeutta kyseiseen koulutusohjelmaan tai mihinkään muuhun kyseisen tiedekunnan koulutusohjelmaan.
|
||||
Sisältää esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat. Sisältää myös opintopisteet, jotka opiskelija on suorittanut koulutusohjelmaan ennen opintojen aloittamista koulutusohjelmassa tai opintojen päätyttyä.
|
||||
|
||||
**Non-degree student credits**: Sisältää tiedekuntaan suoritetut opintopisteet, joiden suorittajilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
|
||||
`,
|
||||
GraduatedOfTheFaculty: `Sisältää kyseisenä vuonna tiedekunnasta valmistuneet opiskelijat riippumatta siitä, minä vuonna opiskeluoikeus ohjelmaan on myönnetty. Jaottelu on tehty opinto-oikeuden päättymispäivän (=valmistumispäivän) perusteella.`,
|
||||
ThesisWritersOfTheFaculty: `Sisältää kyseisenä vuonna kandidaatintutkielman, pro gradu -tutkielman, väitöskirjan tai lisensiaatintyön hyväksytysti suorittaneet opiskelijat. Mukana ei ole mahdollisia arvosanan korotuksia.`,
|
||||
StudentToggle: `
|
||||
All studyrights: Valinnassa ovat mukana vaihto-opiskelijat, erillisoikeudella opiskelevat ja ohjelmaan siirtyneet opiskelijat sekä ohjelmasta pois siirtyneet opiskelijat.\n
|
||||
Special studyrights excluded: Valinnasta ja kaikista luvuista on poissuljettu kaikki edellä mainitut erikoisryhmät.
|
||||
`,
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import CoolDataScience from './coolDataScience'
|
||||
import CourseStatistics from './courseStatistics'
|
||||
import CoursePopulation from './coursePopulation'
|
||||
import Studyprogramme from './studyprogramme'
|
||||
import Faculty from './faculty'
|
||||
|
||||
export default {
|
||||
PopulationStatistics,
|
||||
@@ -10,4 +11,5 @@ export default {
|
||||
CourseStatistics,
|
||||
CoursePopulation,
|
||||
Studyprogramme,
|
||||
Faculty,
|
||||
}
|
||||
|
||||
@@ -80,4 +80,10 @@ export default {
|
||||
NonDegree: `
|
||||
Opiskelijat, joilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
|
||||
`,
|
||||
TransferredCourses: `
|
||||
Sisältää opiskelijat, jotka ovat suorittaneet kyseisenä vuonna hyväksiluettuja opintopisteitä tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
|
||||
`,
|
||||
TransferredCredits: `
|
||||
Sisältää opintopisteet, jotka ovat suoritettu kyseisenä vuonna, ja hyväksiluettu tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
|
||||
`,
|
||||
}
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ const countTotalStats = (formattedStats, userHasAccessToAllStats) => {
|
||||
categories: { passed, failed },
|
||||
grades: cgrades,
|
||||
enrollmentsByState: { ...acc.attempts.enrollmentsByState },
|
||||
totalEnrollments: acc.students.totalEnrollments + (curr.students.totalEnrollments || 0),
|
||||
totalEnrollments: acc.attempts.totalEnrollments + (curr.attempts.totalEnrollments || 0),
|
||||
},
|
||||
students: {
|
||||
totalEnrollments: acc.students.totalEnrollments + (curr.students.totalEnrollments || 0),
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Divider, Loader } from 'semantic-ui-react'
|
||||
|
||||
import { useGetFacultyCreditStatsQuery, useGetFacultyBasicStatsQuery } from 'redux/facultyStats'
|
||||
import { Divider, Loader, Popup, Button, Message } from 'semantic-ui-react'
|
||||
import xlsx from 'xlsx'
|
||||
import {
|
||||
useGetFacultyCreditStatsQuery,
|
||||
useGetFacultyBasicStatsQuery,
|
||||
useGetFacultyThesisStatsQuery,
|
||||
} from 'redux/facultyStats'
|
||||
import LineGraph from 'components/StudyProgramme/BasicOverview/LineGraph'
|
||||
import StackedBarChart from 'components/StudyProgramme/BasicOverview/StackedBarChart'
|
||||
import InteractiveDataTable from '../InteractiveDataView'
|
||||
@@ -10,33 +14,154 @@ import InfoBox from '../../Info/InfoBox'
|
||||
import InfotoolTips from '../../../common/InfoToolTips'
|
||||
import '../faculty.css'
|
||||
|
||||
const Overview = ({ faculty, academicYear, setAcademicYear }) => {
|
||||
const toolTips = InfotoolTips.Studyprogramme
|
||||
const Overview = ({
|
||||
faculty,
|
||||
academicYear,
|
||||
setAcademicYear,
|
||||
studyProgrammes,
|
||||
setStudyProgrammes,
|
||||
specialGroups,
|
||||
setSpecialGroups,
|
||||
language,
|
||||
}) => {
|
||||
const toolTips = InfotoolTips.Faculty
|
||||
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
|
||||
const special = 'SPECIAL_INCLUDED' // specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
|
||||
const credits = useGetFacultyCreditStatsQuery({ id: faculty?.code, yearType, specialGroups: special })
|
||||
const basics = useGetFacultyBasicStatsQuery({ id: faculty?.code, yearType, specialGroups: special })
|
||||
// basic.data.studentsInfo --> data for students of faculty table
|
||||
// basics.data.graduationInfo --> data for graduated and thesis writers table
|
||||
const studyProgrammeFilter = studyProgrammes ? 'ALL_PROGRAMMES' : 'NEW_STUDY_PROGRAMMES'
|
||||
const special = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
|
||||
const credits = useGetFacultyCreditStatsQuery({
|
||||
id: faculty?.code,
|
||||
yearType,
|
||||
studyProgrammeFilter,
|
||||
specialGroups: special,
|
||||
})
|
||||
const basics = useGetFacultyBasicStatsQuery({
|
||||
id: faculty?.code,
|
||||
yearType,
|
||||
studyProgrammeFilter,
|
||||
specialGroups: special,
|
||||
})
|
||||
const thesisWriters = useGetFacultyThesisStatsQuery({
|
||||
id: faculty?.code,
|
||||
yearType,
|
||||
studyProgrammeFilter,
|
||||
specialGroups: special,
|
||||
})
|
||||
|
||||
const getDivider = (title, toolTipText) => (
|
||||
/*
|
||||
Order of the programme keys: KH -> MH -> T -> FI -> K- -> Numbers containing letters at end -> Y- -> Numbers
|
||||
*/
|
||||
const regexValuesAll = [
|
||||
/^KH/,
|
||||
/^MH/,
|
||||
/^T/,
|
||||
/^LI/,
|
||||
/^K-/,
|
||||
/^FI/,
|
||||
/^00901$/,
|
||||
/^00910$/,
|
||||
/^\d.*a$/,
|
||||
/^Y/,
|
||||
/\d$/,
|
||||
/^\d.*e$/,
|
||||
]
|
||||
|
||||
const testKey = value => {
|
||||
for (let i = 0; i < regexValuesAll.length; i++) {
|
||||
if (regexValuesAll[i].test(value)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 6
|
||||
}
|
||||
|
||||
const sortProgrammeKeys = programmeKeys => {
|
||||
return programmeKeys.sort((a, b) => {
|
||||
if (testKey(a) - testKey(b) === 0) {
|
||||
return a.localeCompare(b)
|
||||
}
|
||||
return testKey(a) - testKey(b)
|
||||
})
|
||||
}
|
||||
const downloadCsv = (titles, tableStats, programmeStats, programmeNames, toolTipText) => {
|
||||
const headers = titles.map(title => ({ label: title === '' ? 'Year' : title, key: title === '' ? 'Year' : title }))
|
||||
const csvData = sortProgrammeKeys(Object.keys(programmeStats)).reduce(
|
||||
(results, programme) => [
|
||||
...results,
|
||||
...programmeStats[programme].map(yearRow => {
|
||||
return {
|
||||
Programme: programme,
|
||||
Name: programmeNames[programme][language]
|
||||
? programmeNames[programme][language]
|
||||
: programmeNames[programme].fi,
|
||||
...yearRow.reduce((result, value, valueIndex) => ({ ...result, [headers[valueIndex].key]: value }), {}),
|
||||
}
|
||||
}),
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const tableStatsAsCsv = tableStats.map(yearArray =>
|
||||
yearArray.reduce((result, value, yearIndex) => ({ ...result, [headers[yearIndex].key]: value }), {})
|
||||
)
|
||||
|
||||
const book = xlsx.utils.book_new()
|
||||
const tableSheet = xlsx.utils.json_to_sheet(tableStatsAsCsv)
|
||||
xlsx.utils.book_append_sheet(book, tableSheet, 'TableStats')
|
||||
const sheet = xlsx.utils.json_to_sheet(csvData)
|
||||
xlsx.utils.book_append_sheet(book, sheet, 'ProgrammeStats')
|
||||
xlsx.writeFile(book, `${faculty.code}-${toolTipText}.xlsx`)
|
||||
}
|
||||
|
||||
const getDivider = (title, toolTipText, titles, tableStats, programmeStats, programmeNames) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
</div>
|
||||
<InfoBox content="Sisältää opintopisteet suoritusvuosittain. Suoritukset on jaoteltu Sisussa näkyvän kurssin suorituspäivän mukaan. Kaikki valmistuneet -lukumäärä sisältää kaikki mahdolliset suoritetuksi merkityt opiskeluoikeudet." />
|
||||
{/* <InfoBox content={toolTips[toolTipText]} /> */}
|
||||
<InfoBox content={toolTips[toolTipText]} cypress={toolTipText} />
|
||||
<Popup
|
||||
content="Download statistics as csv"
|
||||
trigger={
|
||||
<Button
|
||||
icon="download"
|
||||
floated="right"
|
||||
onClick={() => downloadCsv(titles, tableStats, programmeStats, programmeNames, toolTipText)}
|
||||
style={{ backgroundColor: 'white', borderRadius: 0 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
const isFetchingOrLoading = credits.isLoading || credits.isFetching || basics.isLoading || basics.isFetching
|
||||
const isFetchingOrLoading =
|
||||
credits.isLoading ||
|
||||
credits.isFetching ||
|
||||
basics.isLoading ||
|
||||
basics.isFetching ||
|
||||
thesisWriters.isLoading ||
|
||||
thesisWriters.isFetching
|
||||
|
||||
const isError =
|
||||
(basics.isError && credits.isError) || (basics.isSuccess && !basics.data && credits.isSuccess && !credits.data)
|
||||
(basics.isError && credits.isError && thesisWriters.isError) ||
|
||||
(basics.isSuccess &&
|
||||
!basics.data &&
|
||||
credits.isSuccess &&
|
||||
!credits.data &&
|
||||
thesisWriters.isSuccess &&
|
||||
!thesisWriters.data)
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
let creditShortTitles = []
|
||||
let transferShortTitles = []
|
||||
if (special === 'SPECIAL_INCLUDED') {
|
||||
creditShortTitles = ['Code', 'Total', 'Major', 'Non-major', 'Non-major other', 'Non-degree']
|
||||
transferShortTitles = ['Code', 'Started', 'Graduated', 'Transferred in', 'Transferred away', 'Transferred to']
|
||||
} else {
|
||||
creditShortTitles = ['Code', 'Major']
|
||||
transferShortTitles = ['Code', 'Started', 'Graduated']
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="faculty-overview">
|
||||
<div className="toggle-container">
|
||||
@@ -48,70 +173,174 @@ const Overview = ({ faculty, academicYear, setAcademicYear }) => {
|
||||
value={academicYear}
|
||||
setValue={setAcademicYear}
|
||||
/>
|
||||
<Toggle
|
||||
cypress="ProgrammeToggle"
|
||||
toolTips={toolTips.ProgrammeToggle}
|
||||
firstLabel="New study programmes"
|
||||
secondLabel="All study programmes"
|
||||
value={studyProgrammes}
|
||||
setValue={setStudyProgrammes}
|
||||
/>
|
||||
<Toggle
|
||||
cypress="StudentToggle"
|
||||
toolTips={toolTips.StudentToggle}
|
||||
firstLabel="All studyrights"
|
||||
secondLabel="Special studyrights excluded"
|
||||
value={specialGroups}
|
||||
setValue={setSpecialGroups}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isFetchingOrLoading ? (
|
||||
<Loader active style={{ marginTop: '10em' }} />
|
||||
<Loader active style={{ marginTop: '15em' }} />
|
||||
) : (
|
||||
<>
|
||||
{studyProgrammeFilter === 'ALL_PROGRAMMES' && (
|
||||
<Message data-cy="FacultyProgrammesShownInfo">
|
||||
Please note that the data is complete only for current Bachelor, Masters and Doctoral programmes.
|
||||
Especially, credits and thesis writers contain only data for current programmes.
|
||||
</Message>
|
||||
)}
|
||||
{special === 'SPECIAL_EXCLUDED' && (
|
||||
<Message data-cy="FacultyExcludeSpecialsInfo">
|
||||
Please note that the data might be inaccurate in some fields. Checking the values is in progress.
|
||||
</Message>
|
||||
)}
|
||||
{basics.isSuccess && basics.data && (
|
||||
<>
|
||||
{getDivider('Students of the faculty', 'StudentsOfTheFaculty')}
|
||||
{getDivider(
|
||||
'Students of the faculty',
|
||||
'StudentsOfTheFaculty',
|
||||
basics?.data?.studentInfo.titles,
|
||||
basics?.data?.studentInfo.tableStats,
|
||||
basics?.data?.studentInfo.programmeTableStats,
|
||||
basics?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<LineGraph
|
||||
cypress="StudentsOfTheFaculty"
|
||||
data={{ ...basics?.data.studentInfo, years: basics.data.years }}
|
||||
/>
|
||||
<InteractiveDataTable
|
||||
cypress="StudentsOfTheFaculty"
|
||||
dataStats={basics?.data?.studentInfo.tableStats}
|
||||
dataProgrammeStats={basics?.data?.studentInfo.programmeTableStats}
|
||||
programmeNames={basics?.data?.programmeNames}
|
||||
titles={basics?.data?.studentInfo.titles}
|
||||
sliceStart={1}
|
||||
language="fi"
|
||||
/>
|
||||
<div className="graph-container-narrow">
|
||||
<LineGraph
|
||||
cypress="StudentsOfTheFaculty"
|
||||
data={{ ...basics?.data.studentInfo, years: basics.data.years }}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container-wide">
|
||||
<InteractiveDataTable
|
||||
cypress="StudentsOfTheFaculty"
|
||||
dataStats={basics?.data?.studentInfo.tableStats}
|
||||
dataProgrammeStats={basics?.data?.studentInfo.programmeTableStats}
|
||||
programmeNames={basics?.data?.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.studentInfo.programmeTableStats))}
|
||||
titles={basics?.data?.studentInfo.titles}
|
||||
sliceStart={1}
|
||||
language={language}
|
||||
yearsVisible={Array(basics?.data?.studentInfo.tableStats.length).fill(false)}
|
||||
shortNames={transferShortTitles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{credits.isSuccess && credits.data && (
|
||||
<>
|
||||
{getDivider('Credits produced by the faculty', 'CreditsProducedByTheFaculty')}
|
||||
{getDivider(
|
||||
'Credits produced by the faculty',
|
||||
'CreditsProducedByTheFaculty',
|
||||
credits?.data?.titles,
|
||||
credits?.data?.tableStats,
|
||||
credits?.data?.programmeTableStats,
|
||||
credits?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<StackedBarChart
|
||||
cypress="CreditsProducedByTheSFaculty"
|
||||
data={credits?.data?.graphStats}
|
||||
labels={credits?.data?.years}
|
||||
/>
|
||||
<InteractiveDataTable
|
||||
cypress="CreditsProducedByTheFaculty"
|
||||
dataStats={credits?.data?.tableStats}
|
||||
dataProgrammeStats={credits?.data?.programmeTableStats}
|
||||
programmeNames={credits?.data?.programmeNames}
|
||||
titles={credits?.data?.titles}
|
||||
sliceStart={2}
|
||||
language="fi"
|
||||
/>
|
||||
<div className="graph-container-narrow">
|
||||
<StackedBarChart
|
||||
cypress="CreditsProducedByTheFaculty"
|
||||
data={credits?.data?.graphStats}
|
||||
labels={credits?.data?.years}
|
||||
wideTable="narrow"
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container-wide">
|
||||
<InteractiveDataTable
|
||||
cypress="CreditsProducedByTheFaculty"
|
||||
dataStats={credits?.data?.tableStats}
|
||||
dataProgrammeStats={credits?.data?.programmeTableStats}
|
||||
programmeNames={credits?.data?.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(credits?.data?.programmeTableStats))}
|
||||
titles={credits?.data?.titles}
|
||||
extraHeight="EXTRA HEIGHT"
|
||||
sliceStart={2}
|
||||
language={language}
|
||||
yearsVisible={Array(credits?.data?.tableStats.length).fill(false)}
|
||||
shortNames={creditShortTitles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{basics.isSuccess && basics.data && (
|
||||
<>
|
||||
{getDivider('Graduated and thesis writers of the faculty', 'GraduatedAndThesisWritersOfTheFaculty')}
|
||||
{getDivider(
|
||||
'Graduated of the faculty',
|
||||
'GraduatedOfTheFaculty',
|
||||
basics?.data?.graduationInfo.titles,
|
||||
basics?.data?.graduationInfo.tableStats,
|
||||
basics?.data?.graduationInfo.programmeTableStats,
|
||||
basics?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<LineGraph
|
||||
cypress="GraduatedAndThesisWritersOfTheFaculty"
|
||||
data={{ ...basics?.data.graduationInfo, years: basics.data.years }}
|
||||
/>
|
||||
<InteractiveDataTable
|
||||
cypress="GraduatedAndThesisWritersOfTheFaculty"
|
||||
dataStats={basics?.data?.graduationInfo.tableStats}
|
||||
dataProgrammeStats={basics?.data?.graduationInfo.programmeTableStats}
|
||||
programmeNames={basics?.data?.programmeNames}
|
||||
titles={basics?.data?.graduationInfo.titles}
|
||||
sliceStart={2}
|
||||
language="fi"
|
||||
/>
|
||||
<div className="graph-container-narrow">
|
||||
<LineGraph
|
||||
cypress="GraduatedOfTheFaculty"
|
||||
data={{ ...basics?.data.graduationInfo, years: basics.data.years }}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container-wide">
|
||||
<InteractiveDataTable
|
||||
cypress="GraduatedOfTheFaculty"
|
||||
dataStats={basics?.data?.graduationInfo.tableStats}
|
||||
dataProgrammeStats={basics?.data?.graduationInfo.programmeTableStats}
|
||||
programmeNames={basics?.data?.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.graduationInfo.programmeTableStats))}
|
||||
titles={basics?.data?.graduationInfo.titles}
|
||||
sliceStart={2}
|
||||
language={language}
|
||||
yearsVisible={Array(basics?.data?.graduationInfo.tableStats.length).fill(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{thesisWriters.isSuccess && thesisWriters.data && (
|
||||
<>
|
||||
{getDivider(
|
||||
'Thesis writers of the faculty',
|
||||
'ThesisWritersOfTheFaculty',
|
||||
thesisWriters?.data?.titles,
|
||||
thesisWriters?.data.tableStats,
|
||||
thesisWriters?.data.programmeTableStats,
|
||||
thesisWriters?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<div className="graph-container-narrow">
|
||||
<LineGraph
|
||||
cypress="ThesisWritersOfTheFaculty"
|
||||
data={{ ...thesisWriters?.data, years: thesisWriters?.data.years }}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container-wide">
|
||||
<InteractiveDataTable
|
||||
cypress="ThesisWritersOfTheFaculty"
|
||||
dataStats={thesisWriters?.data.tableStats}
|
||||
dataProgrammeStats={thesisWriters?.data.programmeTableStats}
|
||||
programmeNames={thesisWriters?.data.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(thesisWriters?.data.programmeTableStats))}
|
||||
titles={thesisWriters?.data?.titles}
|
||||
sliceStart={2}
|
||||
extraHeight="EXTRA HEIGHT"
|
||||
language={language}
|
||||
yearsVisible={Array(thesisWriters?.data.tableStats.length).fill(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
+41
-2
@@ -1,7 +1,46 @@
|
||||
import React from 'react'
|
||||
// import { Divider, Loader } from 'semantic-ui-react'
|
||||
import Toggle from '../../StudyProgramme/Toggle'
|
||||
// import InfoBox from '../../Info/InfoBox'
|
||||
|
||||
const FacultyProgrammeOverview = () => {
|
||||
return <div className="programmes-overview">Faculty programmes</div>
|
||||
import InfotoolTips from '../../../common/InfoToolTips'
|
||||
import '../faculty.css'
|
||||
|
||||
// const getDivider = (title, toolTipText) => (
|
||||
// <>
|
||||
// <div className="divider">
|
||||
// <Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
// {title}
|
||||
// </Divider>
|
||||
// </div>
|
||||
// {/* <InfoBox content={toolTips[toolTipText]} /> */}
|
||||
// </>
|
||||
// )
|
||||
|
||||
const FacultyProgrammeOverview = ({ faculty, studyProgrammes, setStudyProgrammes }) => {
|
||||
const toolTipsProgramme = InfotoolTips.Faculty
|
||||
// // const [programme, setProgramme] = useState(faculty)
|
||||
// const studyProgrammeFilter = studyProgrammes ? 'ALL_PROGRAMMES' : 'NEW_STUDY_PROGRAMMES'
|
||||
|
||||
// if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="programmes-overview">
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="ProgrammeToggle"
|
||||
toolTips={toolTipsProgramme.ProgrammeToggle}
|
||||
firstLabel="New study programmes"
|
||||
secondLabel="All study programmes"
|
||||
value={studyProgrammes}
|
||||
setValue={setStudyProgrammes}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<strong> Data for {faculty?.code} upcoming</strong>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FacultyProgrammeOverview
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import { Button, Icon, Table, Label } from 'semantic-ui-react'
|
||||
|
||||
const roundValue = value => {
|
||||
return Number.isInteger(value) ? value : value.toFixed(1)
|
||||
}
|
||||
|
||||
const BasicRow = ({ icon, yearArray, cypress, yearIndex, toggleVisibility, styles }) => {
|
||||
return (
|
||||
<Table.Row style={styles} key={`Basic-row-${cypress}-${yearIndex}`}>
|
||||
{yearArray?.map((value, idx) => (
|
||||
<Table.Cell key={`random-button-cell-key-${Math.random()}`}>
|
||||
{idx === 0 ? (
|
||||
<Button
|
||||
as="div"
|
||||
onClick={toggleVisibility}
|
||||
labelPosition="right"
|
||||
style={{ backgroundColor: 'white', borderRadius: 0 }}
|
||||
data-cy={`Button-${cypress}-${yearIndex}`}
|
||||
>
|
||||
<Button icon style={{ backgroundColor: 'white', borderRadius: 0 }}>
|
||||
<Icon name={icon} />
|
||||
</Button>
|
||||
<Label as="a" style={{ backgroundColor: 'white', borderRadius: 0 }}>
|
||||
{value}
|
||||
</Label>
|
||||
</Button>
|
||||
) : (
|
||||
roundValue(value)
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default BasicRow
|
||||
+35
-10
@@ -3,13 +3,23 @@ import React from 'react'
|
||||
import Highcharts from 'highcharts'
|
||||
import ReactHighcharts from 'react-highcharts'
|
||||
|
||||
const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differenceData }) => {
|
||||
const colors = ['#7cb5ec', '#90ed7d', '#434348', '#f7a35c', '#FFF000', '#2b908f', '#f45b5b', '#91e8e1']
|
||||
|
||||
const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differenceData, extraHeight }) => {
|
||||
const transpose = matrix => {
|
||||
return matrix.reduce((prev, next) => next.map((_item, i) => (prev[i] || []).concat(next[i])), [])
|
||||
}
|
||||
|
||||
const needsExtra = extraHeight === 'EXTRA HEIGHT'
|
||||
const manyProgrammes =
|
||||
labels.includes('KH57_001') ||
|
||||
labels.includes('KH50_001') ||
|
||||
labels.includes('KH70_001') ||
|
||||
labels.includes('KH40_001') ||
|
||||
labels.includes('KH30_001') ||
|
||||
labels.includes('KH80_001')
|
||||
const dataTranspose = transpose(data)
|
||||
.map((obj, idx) => ({ name: names[idx], data: obj }))
|
||||
.map((obj, idx) => ({ name: names[idx], data: obj, color: colors[idx] }))
|
||||
.reverse()
|
||||
|
||||
const differenceArray = Object.keys(differenceData).reduce(
|
||||
@@ -29,7 +39,14 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
if (change > 0) return `+${change.toString()}`
|
||||
return change
|
||||
}
|
||||
const flexHeight = labels.length < 6 ? `${(1 / 3) * 100}%` : `${labels.length * (1 / 22) * 100}%`
|
||||
|
||||
// Point width is 24 px different multipliers adjusts the height.
|
||||
const getFlexHeight = (len, needsExtra, manyProgrammes, labels) => {
|
||||
if (labels.includes('KH74_001')) return `${len * 24 * 5}px`
|
||||
if (needsExtra && manyProgrammes) return `${len * 24 * 1.5}px`
|
||||
if (needsExtra) return `${len * 24 * 2.5}px`
|
||||
return `${len * 24 * 1.5}px`
|
||||
}
|
||||
|
||||
const getColor = change => {
|
||||
if (change > 0) return '#6ab04c'
|
||||
@@ -42,11 +59,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
chart: {
|
||||
type: 'bar',
|
||||
marginTop: 60,
|
||||
height: flexHeight,
|
||||
scrollablePlotArea: {
|
||||
minWidth: 700,
|
||||
},
|
||||
padding: 2,
|
||||
height: getFlexHeight(labels.length, needsExtra, manyProgrammes, labels),
|
||||
},
|
||||
credits: {
|
||||
text: 'oodikone | TOSKA',
|
||||
@@ -96,7 +109,9 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
backgroundColor: 'white',
|
||||
fontSize: '25px',
|
||||
formatter() {
|
||||
let tooltipString = `<b>${longLabels[this.x][language]}</b><br /><p>${this.x}</p><br />`
|
||||
let tooltipString = `<b>${
|
||||
longLabels[this.x] && longLabels[this.x][language] ? longLabels[this.x][language] : longLabels[this.x].fi
|
||||
}</b><br /><p>${this.x}</p><br />`
|
||||
const diffArray = differenceArray[this.x]
|
||||
this.points.forEach(point => {
|
||||
tooltipString += `<span style="color:${point.color}">●</span> <b>${point.series.name}: ${point.y}</b>
|
||||
@@ -104,16 +119,26 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
diffArray[point.series.name]
|
||||
)}</span>)<br />`
|
||||
})
|
||||
tooltipString += `<b>Total: ${this.points.reduce((prev, current) => prev + current.y, 0)}</b>`
|
||||
return tooltipString
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
stacking: 'normal',
|
||||
pointWidth: 25,
|
||||
pointWidth: 24,
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter() {
|
||||
if (Number.isInteger(this.y)) return `${this.y}`
|
||||
return `${this.y.toFixed(1)}`
|
||||
},
|
||||
fontSize: '24px',
|
||||
filter: {
|
||||
property: 'y',
|
||||
operator: '>',
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
import React, { useState, useImperativeHandle } from 'react'
|
||||
import { Button, Icon, Table, Label } from 'semantic-ui-react'
|
||||
|
||||
const BasicRow = ({ icon, yearArray, cypress, yearIndex, toggleVisibility, styles }) => {
|
||||
return (
|
||||
<Table.Row style={styles} key={`Basic-row-${cypress}-${yearIndex}`}>
|
||||
{yearArray?.map((value, idx) => (
|
||||
<Table.Cell key={`random-button-cell-key-${Math.random()}`}>
|
||||
{idx === 0 ? (
|
||||
<Button
|
||||
as="div"
|
||||
onClick={toggleVisibility}
|
||||
labelPosition="right"
|
||||
style={{ backgroundColor: 'white', borderRadius: 0 }}
|
||||
data-cy={`Button-${cypress}-${yearIndex}`}
|
||||
>
|
||||
<Button icon style={{ backgroundColor: 'white', borderRadius: 0 }}>
|
||||
<Icon name={icon} />
|
||||
</Button>
|
||||
<Label as="a" style={{ backgroundColor: 'white', borderRadius: 0 }}>
|
||||
{value}
|
||||
</Label>
|
||||
</Button>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
)
|
||||
}
|
||||
|
||||
const ToggleTableView = React.forwardRef((props, ref) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
const toggleVisibility = () => {
|
||||
setVisible(!visible)
|
||||
}
|
||||
useImperativeHandle(ref, () => {
|
||||
return toggleVisibility
|
||||
})
|
||||
const chartRowStyles = {
|
||||
rowSpan: 100,
|
||||
display: visible ? '' : 'none',
|
||||
}
|
||||
const basicRowStyles = {
|
||||
display: visible ? 'none' : '',
|
||||
}
|
||||
return (
|
||||
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
|
||||
<BasicRow
|
||||
icon="chevron right"
|
||||
yearArray={props.yearArray}
|
||||
cypress={`Show-${props.cypress}`}
|
||||
yearIndex={props.yearIndex}
|
||||
toggleVisibility={toggleVisibility}
|
||||
styles={basicRowStyles}
|
||||
/>
|
||||
<BasicRow
|
||||
icon="chevron down"
|
||||
yearArray={props.yearArray}
|
||||
cypress={`Hide-${props.cypress}`}
|
||||
yearIndex={props.yearIndex}
|
||||
toggleVisibility={toggleVisibility}
|
||||
styles={chartRowStyles}
|
||||
/>
|
||||
<Table.Row key={`stack-row-key-${Math.random()}`} style={chartRowStyles}>
|
||||
{props.children}
|
||||
</Table.Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
|
||||
ToggleTableView.displayName = 'ToggleTableView'
|
||||
|
||||
export default ToggleTableView
|
||||
@@ -1,20 +1,50 @@
|
||||
import React from 'react'
|
||||
import { Table } from 'semantic-ui-react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Table, Menu } from 'semantic-ui-react'
|
||||
import CollapsedStackedBar from './CollapsedStackedBar'
|
||||
import ToggleTableView from './ToggleTableView'
|
||||
import BasicRow from './BasicRow'
|
||||
|
||||
const InteractiveDataTable = ({
|
||||
cypress,
|
||||
dataStats,
|
||||
dataProgrammeStats,
|
||||
programmeNames,
|
||||
sortedKeys,
|
||||
titles,
|
||||
wideTable,
|
||||
language,
|
||||
sliceStart,
|
||||
extraHeight,
|
||||
yearsVisible,
|
||||
shortNames,
|
||||
studentsTable,
|
||||
}) => {
|
||||
if (!dataStats || !titles || !dataProgrammeStats) return null
|
||||
const [keyOrder, setkeyOrder] = useState({})
|
||||
const [sorter, setSorter] = useState('Code')
|
||||
const [sortDir, setSortDir] = useState(1)
|
||||
const [sortbyColumn, setSortByColumn] = useState(0)
|
||||
const [columnIndex, setSelectedIndex] = useState(0)
|
||||
const [visible, setVisible] = useState(yearsVisible)
|
||||
|
||||
const sortBySelectedColumn = () => {
|
||||
const keys = {}
|
||||
if (!(columnIndex === 0)) {
|
||||
const numbersOfYears = dataStats.length
|
||||
const data = Object.entries(dataProgrammeStats)
|
||||
for (let yearIdx = 0; yearIdx < numbersOfYears; yearIdx++) {
|
||||
const yearlySortedKeys = data
|
||||
.sort((a, b) => {
|
||||
if (sortDir === -1) return a[1][yearIdx][columnIndex] - b[1][yearIdx][columnIndex]
|
||||
return b[1][yearIdx][columnIndex] - a[1][yearIdx][columnIndex]
|
||||
})
|
||||
.map(yearlyProgrammes => yearlyProgrammes[0])
|
||||
keys[yearIdx] = yearlySortedKeys
|
||||
}
|
||||
}
|
||||
setkeyOrder(keys)
|
||||
setSortByColumn(columnIndex)
|
||||
}
|
||||
useEffect(sortBySelectedColumn, [columnIndex, dataStats, sortDir])
|
||||
|
||||
if (!dataStats || !titles || !dataProgrammeStats) return null
|
||||
/* Calculation works as follows
|
||||
1. Calculate difference between current and previous year: current year - previous year
|
||||
2. Calculations are done for the values starting from the index 2 (index 0: year, index 1: total)
|
||||
@@ -37,50 +67,87 @@ const InteractiveDataTable = ({
|
||||
)
|
||||
return differenceMatrix
|
||||
}
|
||||
const sortKeysAlphabeticallyAsc = programmeKeys => {
|
||||
return programmeKeys.sort((a, b) => a.localeCompare(b))
|
||||
|
||||
const handleClick = (sorterName, nameIndex) => {
|
||||
if (sorterName === sorter) setSortDir(-1 * sortDir)
|
||||
setSorter(sorterName)
|
||||
setSelectedIndex(nameIndex)
|
||||
}
|
||||
|
||||
const toggleVisibility = yearIndex => {
|
||||
const arrayToModify = [...visible]
|
||||
arrayToModify[yearIndex] = !visible[yearIndex]
|
||||
setVisible(arrayToModify)
|
||||
}
|
||||
|
||||
const sorterNames = shortNames || titles.map(title => (title === '' ? 'Code' : title))
|
||||
|
||||
const differenceToPrevYears = calculatDiffToPrevYear(dataProgrammeStats)
|
||||
const yearRef = React.createRef()
|
||||
|
||||
return (
|
||||
<div className={`table-container${wideTable ? '-wide' : ''}`}>
|
||||
<div>
|
||||
<Menu compact attached="top">
|
||||
<Menu.Item style={{ cursor: 'default', borderRadius: '1px', padding: '5px' }} active color="black">
|
||||
Sort by:
|
||||
</Menu.Item>
|
||||
{sorterNames.map((sorterName, nameIndex) => (
|
||||
<Menu.Item
|
||||
data-cy={`Menu-${cypress}-${sorterName}`}
|
||||
color={sorter === sorterName ? 'blue' : 'black'}
|
||||
key={sorterName}
|
||||
active={sorter === sorterName}
|
||||
onClick={() => handleClick(sorterName, nameIndex)}
|
||||
style={{ borderRadius: '1px', fontSize: '14px', padding: '5px' }}
|
||||
icon={sortDir === 1 ? 'triangle down' : 'triangle up'}
|
||||
content={sorterName}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
<Table data-cy={`Table-${cypress}`} celled>
|
||||
<Table.Header>
|
||||
<Table.Row key={`randow-header-row-${Math.random()}`}>
|
||||
<Table.Row key={`randow-header-row-${Math.random()}`} textAlign="center">
|
||||
{titles?.map(title => (
|
||||
<Table.HeaderCell key={title}>{title}</Table.HeaderCell>
|
||||
))}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{dataStats?.map((yearArray, yearIndex) => (
|
||||
<ToggleTableView
|
||||
yearArray={yearArray}
|
||||
cypress={cypress}
|
||||
yearIndex={yearIndex}
|
||||
ref={yearRef}
|
||||
key={`togglable-${Math.random()}`}
|
||||
>
|
||||
<Table.Cell data-cy={`Cell-${cypress}-${yearIndex}`} key={`stack-cell${Math.random()}`} colSpan={100}>
|
||||
<CollapsedStackedBar
|
||||
data={sortKeysAlphabeticallyAsc(Object.keys(dataProgrammeStats))?.map(programme =>
|
||||
dataProgrammeStats[programme][yearIndex].slice(sliceStart)
|
||||
)}
|
||||
labels={sortKeysAlphabeticallyAsc(Object.keys(dataProgrammeStats))}
|
||||
differenceData={Object.keys(differenceToPrevYears)?.reduce(
|
||||
(yearlyObject, programme) => ({
|
||||
...yearlyObject,
|
||||
[programme]: differenceToPrevYears[programme][yearIndex],
|
||||
}),
|
||||
{}
|
||||
)}
|
||||
longLabels={programmeNames}
|
||||
language={language}
|
||||
names={titles?.slice(sliceStart)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</ToggleTableView>
|
||||
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
|
||||
<BasicRow
|
||||
icon={visible[yearIndex] ? 'chevron down' : 'chevron right'}
|
||||
yearArray={yearArray}
|
||||
cypress={visible[yearIndex] ? `Hide-${cypress}` : `Show-${cypress}`}
|
||||
yearIndex={yearIndex}
|
||||
toggleVisibility={() => toggleVisibility(yearIndex)}
|
||||
/>
|
||||
<Table.Row key={`stack-row-key-${Math.random()}`} style={{ display: visible[yearIndex] ? '' : 'none' }}>
|
||||
<Table.Cell data-cy={`Cell-${cypress}-${yearIndex}`} key={`stack-cell${Math.random()}`} colSpan={100}>
|
||||
<CollapsedStackedBar
|
||||
data={
|
||||
sortbyColumn === 0
|
||||
? sortedKeys?.map(programme => dataProgrammeStats[programme][yearIndex].slice(sliceStart))
|
||||
: keyOrder[yearIndex]?.map(programme =>
|
||||
dataProgrammeStats[programme][yearIndex].slice(sliceStart)
|
||||
)
|
||||
}
|
||||
labels={sortbyColumn === 0 ? sortedKeys : keyOrder[yearIndex]}
|
||||
differenceData={Object.keys(differenceToPrevYears)?.reduce(
|
||||
(yearlyObject, programme) => ({
|
||||
...yearlyObject,
|
||||
[programme]: differenceToPrevYears[programme][yearIndex],
|
||||
}),
|
||||
{}
|
||||
)}
|
||||
longLabels={programmeNames}
|
||||
language={language}
|
||||
names={titles?.slice(sliceStart)}
|
||||
extraHeight={extraHeight}
|
||||
studentsTable={studentsTable}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from 'react'
|
||||
|
||||
const ReactHighcharts = require('react-highcharts')
|
||||
|
||||
const allTresholds = {
|
||||
bachelor: [36, 41],
|
||||
bcMsCombo: [60, 70],
|
||||
master: [24, 27],
|
||||
doctor: [48, 58],
|
||||
// Fix differrent cases?
|
||||
licentiate: [78, 88],
|
||||
}
|
||||
|
||||
const GaugeChart = ({ cypress, data, year, graduationAmount, level }) => {
|
||||
if (!data || !graduationAmount || !data.length) return null
|
||||
|
||||
const thresholdValues = allTresholds[level]
|
||||
|
||||
const getColor = data => {
|
||||
// if (!studyprogramme.includes('KH') && !studyprogramme.includes('MH')) return ['#1d44a1', '#EBECF0']
|
||||
if (data <= thresholdValues[0]) return ['#90A959', '#EBECF0']
|
||||
if (data > thresholdValues[0] && data <= thresholdValues[1]) return ['#FEE191', '#EBECF0']
|
||||
return ['#FB6962', '#EBECF0']
|
||||
}
|
||||
|
||||
if (!data) return null
|
||||
const defaultConfig = {
|
||||
chart: {
|
||||
plotBackgroundColor: null,
|
||||
plotBorderWidth: 0,
|
||||
plotShadow: false,
|
||||
width: 260,
|
||||
height: 300,
|
||||
},
|
||||
title: {
|
||||
text: `${data[0][1]} kk`,
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
y: 60,
|
||||
},
|
||||
subtitle: {
|
||||
text: `${year}<br/> n = ${graduationAmount}`,
|
||||
style: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: '15px',
|
||||
},
|
||||
},
|
||||
credits: {
|
||||
text: '',
|
||||
},
|
||||
exporting: {
|
||||
enabled: false,
|
||||
},
|
||||
accessibility: {
|
||||
point: {
|
||||
valueSuffix: '%',
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
dataLabels: {
|
||||
title: '{point.percentage:.1f}%',
|
||||
distance: -50,
|
||||
style: {
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
},
|
||||
},
|
||||
startAngle: -90,
|
||||
endAngle: 90,
|
||||
center: ['50%', '75%'],
|
||||
size: '100%',
|
||||
colors: getColor(data[0][1]),
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
name: '',
|
||||
innerSize: '40%',
|
||||
data,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-cy={`Graph-${cypress}`}>
|
||||
<ReactHighcharts config={defaultConfig} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GaugeChart
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
import { Divider, Header } from 'semantic-ui-react'
|
||||
import GaugeChart from './GaugeChart'
|
||||
|
||||
const GraduationTimes = ({ title, years, data, amounts, level }) => {
|
||||
if (!Object.values(amounts).some(a => a > 0)) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Header as="h4" floated="left">
|
||||
{title}
|
||||
</Header>
|
||||
<Divider clearing />
|
||||
{/* <Divider data-cy={`Title-${level}-degree`} clearing>
|
||||
{title}
|
||||
</Divider> */}
|
||||
</div>
|
||||
<div className="section-container-centered">
|
||||
{years.map(year => (
|
||||
<GaugeChart
|
||||
cypress={`${year}-AverageGraduationTimes`}
|
||||
key={year}
|
||||
year={year}
|
||||
data={data[year]}
|
||||
graduationAmount={amounts[year]}
|
||||
level={level}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GraduationTimes
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import { Dropdown } from 'semantic-ui-react'
|
||||
|
||||
import '../../StudyProgramme/studyprogramme.css'
|
||||
|
||||
const ProgrammeSelector = ({ programme, setProgramme, programmes }) => {
|
||||
if (!programmes) return null
|
||||
|
||||
const handleStudytrackChange = (event, { value }) => {
|
||||
event.preventDefault()
|
||||
setProgramme(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="programme-selector">
|
||||
<h4>Choose programme</h4>
|
||||
<Dropdown
|
||||
fluid
|
||||
selection
|
||||
name="programme"
|
||||
placeholder="All programmes of the faculty"
|
||||
value={programme}
|
||||
onChange={handleStudytrackChange}
|
||||
options={programmes.map(({ code, name }) => ({
|
||||
key: code,
|
||||
value: code,
|
||||
text: code === 'faculty' ? name?.fi : `${name?.fi}, ${code}`,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProgrammeSelector
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Divider, Loader } from 'semantic-ui-react'
|
||||
|
||||
import { useGetFacultyGraduationTimesQuery } from 'redux/facultyStats'
|
||||
import Toggle from '../../StudyProgramme/Toggle'
|
||||
import InfoBox from '../../Info/InfoBox'
|
||||
// import ProgrammeSelector from './ProgrammeSelector'
|
||||
import GraduationTimes from './GraduationTimes'
|
||||
import InfotoolTips from '../../../common/InfoToolTips'
|
||||
import '../faculty.css'
|
||||
|
||||
const getDivider = (title, toolTipText) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
</div>
|
||||
<InfoBox content="Keskimääräiset valmistumisajat" />
|
||||
{/* <InfoBox content={toolTips[toolTipText]} /> */}
|
||||
</>
|
||||
)
|
||||
|
||||
const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) => {
|
||||
const toolTipsProgramme = InfotoolTips.Faculty
|
||||
// const [programme, setProgramme] = useState(faculty)
|
||||
const [showMeanTime, setShowMeanTime] = useState(false)
|
||||
const studyProgrammeFilter = studyProgrammes ? 'ALL_PROGRAMMES' : 'NEW_STUDY_PROGRAMMES'
|
||||
const graduationStats = useGetFacultyGraduationTimesQuery({ id: faculty?.code, studyProgrammeFilter })
|
||||
|
||||
const isFetchingOrLoading = graduationStats.isLoading || graduationStats.isFetching
|
||||
|
||||
const isError = graduationStats.isError || (graduationStats.isSuccess && !graduationStats.data)
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
return (
|
||||
<div className="programmes-overview">
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="ProgrammeToggle"
|
||||
toolTips={toolTipsProgramme.ProgrammeToggle}
|
||||
firstLabel="New study programmes"
|
||||
secondLabel="All study programmes"
|
||||
value={studyProgrammes}
|
||||
setValue={setStudyProgrammes}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isFetchingOrLoading ? (
|
||||
<Loader active style={{ marginTop: '15em' }} />
|
||||
) : (
|
||||
<>
|
||||
{graduationStats.isSuccess && graduationStats.data && (
|
||||
<>
|
||||
{getDivider('Average graduation times', 'AverageGraduationTimes')}
|
||||
{/* <ProgrammeSelector programme={programme} setProgramme={setProgramme} programmes={graduationStats?.data} /> */}
|
||||
<Toggle
|
||||
cypress="GraduationTimeToggle"
|
||||
firstLabel="Median time"
|
||||
secondLabel="Mean time"
|
||||
value={showMeanTime}
|
||||
setValue={setShowMeanTime}
|
||||
/>
|
||||
<div>
|
||||
<GraduationTimes
|
||||
level="bachelor"
|
||||
title="Bachelor (36 kk)"
|
||||
years={graduationStats?.data?.years}
|
||||
amounts={graduationStats?.data.graduationAmounts.bachelor}
|
||||
data={showMeanTime ? graduationStats?.data?.means.bachelor : graduationStats?.data?.medians.bachelor}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="bcMsCombo"
|
||||
title="Bachelor + Master (60 kk)"
|
||||
years={graduationStats?.data?.years}
|
||||
amounts={graduationStats?.data.graduationAmounts.bcMsCombo}
|
||||
data={
|
||||
showMeanTime ? graduationStats?.data?.means?.bcMsCombo : graduationStats?.data?.medians?.bcMsCombo
|
||||
}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="master"
|
||||
title="Master (24 kk)"
|
||||
years={graduationStats?.data?.years}
|
||||
amounts={graduationStats?.data.graduationAmounts.master}
|
||||
data={showMeanTime ? graduationStats?.data?.means?.master : graduationStats?.data?.medians?.master}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="doctor"
|
||||
title="Doctor (48 kk)"
|
||||
years={graduationStats?.data?.years}
|
||||
amounts={graduationStats?.data.graduationAmounts.doctor}
|
||||
data={showMeanTime ? graduationStats?.data?.means?.doctor : graduationStats?.data?.medians?.doctor}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="licentiate"
|
||||
title="Licentiate (78 kk)"
|
||||
years={graduationStats?.data?.years}
|
||||
amounts={graduationStats?.data.graduationAmounts.licentiate}
|
||||
data={
|
||||
showMeanTime ? graduationStats?.data?.means?.licentiate : graduationStats?.data?.medians?.licentiate
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimesAndPathsView
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button, Icon, Loader } from 'semantic-ui-react'
|
||||
import { useUpdateFacultyBasicViewQuery } from '../../../redux/facultyStats'
|
||||
|
||||
const getStatusIcon = stats => {
|
||||
if (stats.isLoading) return <Loader active />
|
||||
if (stats.isSuccess) return <Icon name="check" color="green" />
|
||||
if (stats.isError) return <Icon name="close" color="red" />
|
||||
return ''
|
||||
}
|
||||
|
||||
const UpdateView = ({ faculty }) => {
|
||||
const [skipBasic, setSkipBasic] = useState(true)
|
||||
const basicTabStats = useUpdateFacultyBasicViewQuery({ id: faculty }, { skip: skipBasic })
|
||||
|
||||
return (
|
||||
<div className="update-view">
|
||||
<div className="button-container">
|
||||
<h4>Update data on Basic information -view</h4>
|
||||
<p>
|
||||
<i>(For large faculties this can take a couple of minutes)</i>
|
||||
</p>
|
||||
<Button disabled={basicTabStats.isLoading} color="blue" onClick={() => setSkipBasic(false)}>
|
||||
Update
|
||||
</Button>
|
||||
{getStatusIcon(basicTabStats)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateView
|
||||
@@ -2,30 +2,34 @@ import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { withRouter, useHistory } from 'react-router-dom'
|
||||
import { Segment, Header, Tab, Loader } from 'semantic-ui-react'
|
||||
import { useGetFacultiesQuery } from 'redux/facultyStats'
|
||||
import { useGetAuthorizedUserQuery } from 'redux/auth'
|
||||
import { getTextIn } from 'common'
|
||||
import { useTabs, useTitle } from '../../common/hooks'
|
||||
import FacultySelector from './facultySelector'
|
||||
import BasicOverview from './BasicOverview'
|
||||
import ProgrammeOverview from './FacultyProgrammeOverview'
|
||||
import TimesAndPathsView from './TimesAndPaths'
|
||||
import UpdateView from './UpdateView'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import TSA from '../../common/tsa'
|
||||
|
||||
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
|
||||
|
||||
const FacultyStatistics = props => {
|
||||
useTitle('Faculties')
|
||||
const history = useHistory()
|
||||
const { language } = useLanguage()
|
||||
const allFaculties = useGetFacultiesQuery()
|
||||
const faculties = allFaculties?.data && allFaculties.data.filter(f => !ignore.includes(f.code))
|
||||
const faculties = allFaculties?.data
|
||||
|
||||
const { match } = props
|
||||
const { facultyCode } = match.params
|
||||
const faculty = faculties && facultyCode && faculties.find(f => f.code === facultyCode)
|
||||
const facultyName = faculty && getTextIn(faculty.name, language)
|
||||
|
||||
const { isAdmin } = useGetAuthorizedUserQuery()
|
||||
const [tab, setTab] = useTabs('p_tab', 0, history)
|
||||
const [academicYear, setAcademicYear] = useState(false)
|
||||
const [studyProgrammes, setStudyProgrammes] = useState(false)
|
||||
const [specialGroups, setSpecialGroups] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!facultyName) {
|
||||
@@ -56,13 +60,55 @@ const FacultyStatistics = props => {
|
||||
const panes = [
|
||||
{
|
||||
menuItem: 'Basic information',
|
||||
render: () => <BasicOverview faculty={faculty} academicYear={academicYear} setAcademicYear={setAcademicYear} />,
|
||||
},
|
||||
{
|
||||
menuItem: 'Programmes and student populations',
|
||||
render: () => <ProgrammeOverview />,
|
||||
render: () => (
|
||||
<BasicOverview
|
||||
faculty={faculty}
|
||||
academicYear={academicYear}
|
||||
setAcademicYear={setAcademicYear}
|
||||
studyProgrammes={studyProgrammes}
|
||||
setStudyProgrammes={setStudyProgrammes}
|
||||
specialGroups={specialGroups}
|
||||
setSpecialGroups={setSpecialGroups}
|
||||
language={language}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
// REMEMBER to change cypress test 'Correct tabs are shown', when visibility of
|
||||
// Graduation times and Programmes and student Populations -tabs is changed for facultyStatistic users
|
||||
if (isAdmin) {
|
||||
panes.push({
|
||||
menuItem: 'Programmes and student populations',
|
||||
render: () => (
|
||||
<ProgrammeOverview
|
||||
faculty={faculty}
|
||||
studyProgrammes={studyProgrammes}
|
||||
setStudyProgrammes={setStudyProgrammes}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
panes.push({
|
||||
menuItem: 'Graduation times',
|
||||
render: () => (
|
||||
<TimesAndPathsView
|
||||
faculty={faculty}
|
||||
studyProgrammes={studyProgrammes}
|
||||
setStudyProgrammes={setStudyProgrammes}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
panes.push({
|
||||
menuItem: 'Update statistics',
|
||||
render: () => <UpdateView faculty={faculty?.code} />,
|
||||
})
|
||||
}
|
||||
|
||||
return panes
|
||||
}
|
||||
|
||||
@@ -81,7 +127,7 @@ const FacultyStatistics = props => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="segmentContainer">
|
||||
<div className="segmentContainer" data-cy="FacultySegmentContainer">
|
||||
<Segment className="contentSegment">
|
||||
<div align="center" style={{ padding: '30px' }}>
|
||||
<Header textAlign="center">{facultyName}</Header>
|
||||
|
||||
@@ -10,9 +10,19 @@ const filterTexts = {
|
||||
[FilterType.PASSED_AFTER_FAILURE]: { label: 'Passed After Failure' },
|
||||
[FilterType.FAILED]: { label: 'Failed' },
|
||||
[FilterType.FAILED_MANY_TIMES]: { label: 'Failed Multiple Times' },
|
||||
[FilterType.NOT_PARTICIPATED]: { label: 'Not Participated' },
|
||||
// [FilterType.NOT_PARTICIPATED]: { label: 'Not Participated' },
|
||||
[FilterType.DID_NOT_PASS]: { label: "Didn't Pass" },
|
||||
}
|
||||
// a bandaid solution to prevent oodikone crashing
|
||||
const translate = {
|
||||
ALL: 'all',
|
||||
PASSED: 'passed',
|
||||
PASSED_AFTER_FAILURE: 'retryPassed',
|
||||
FAILED: 'failed',
|
||||
FAILED_MANY_TIMES: 'failedMany',
|
||||
NOT_PARTICIPATED: 'notParticipated',
|
||||
DID_NOT_PASS: 'didNotPass',
|
||||
}
|
||||
|
||||
const CourseCard = ({ course, filterType, onChange }) => {
|
||||
const { language } = useLanguage()
|
||||
@@ -27,7 +37,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
|
||||
return (
|
||||
<>
|
||||
<Label style={{ marginTop: '0.5rem' }}>
|
||||
{getTextIn(course.name, language)}
|
||||
{getTextIn(course.course.name, language)}
|
||||
|
||||
<Dropdown
|
||||
text={filterTexts[filterType].label}
|
||||
@@ -35,7 +45,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
|
||||
fluid
|
||||
className="mini"
|
||||
button
|
||||
data-cy={`${name}-${course.code}-dropdown`}
|
||||
data-cy={`${name}-${course.course.code}-dropdown`}
|
||||
style={{ marginTop: '0.5rem' }}
|
||||
>
|
||||
<Dropdown.Menu>
|
||||
@@ -50,6 +60,8 @@ const CourseCard = ({ course, filterType, onChange }) => {
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (Object.keys(course?.students[translate[type]]).length === 0)
|
||||
return <Dropdown.Item key={label} text={label} value={type} onClick={onClick} disabled />
|
||||
return <Dropdown.Item key={label} text={label} value={type} onClick={onClick} />
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
@@ -60,7 +72,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
|
||||
size="tiny"
|
||||
onClick={clear}
|
||||
icon
|
||||
data-cy={`${name}-${course.code}-clear`}
|
||||
data-cy={`${name}-${course.course.code}-clear`}
|
||||
style={{ marginTop: '0.5rem' }}
|
||||
>
|
||||
<Icon name="close" />
|
||||
|
||||
@@ -62,7 +62,7 @@ const CourseFilterCard = ({ courseStats, options, onOptionsChange }) => {
|
||||
/>
|
||||
{Object.entries(courseFilters).map(([code]) => (
|
||||
<CourseCard
|
||||
course={courseStats[code].course}
|
||||
course={courseStats[code]}
|
||||
filterType={courseFilters[code] ?? FilterType.ALL}
|
||||
onChange={type => setCourseFilter(code, type)}
|
||||
key={`course-filter-selected-course-${code}`}
|
||||
@@ -83,9 +83,10 @@ const filterFunctions = {
|
||||
[FilterType.PASSED_AFTER_FAILURE]: createFilterFunc('retryPassed'),
|
||||
[FilterType.FAILED]: createFilterFunc('failed'),
|
||||
[FilterType.FAILED_MANY_TIMES]: createFilterFunc('failedMany'),
|
||||
[FilterType.NOT_PARTICIPATED]: createFilterFunc('all', true),
|
||||
[FilterType.DID_NOT_PASS]: ({ studentNumber }, { students }) =>
|
||||
!Object.keys(students.all).includes(studentNumber) || Object.keys(students.failed).includes(studentNumber),
|
||||
[FilterType.DID_NOT_PASS]: createFilterFunc('didNotPass'),
|
||||
// [FilterType.NOT_PARTICIPATED]: createFilterFunc('all', true),
|
||||
// [FilterType.DID_NOT_PASS]: ({ studentNumber }, { students }) =>
|
||||
// !Object.keys(students.all).includes(studentNumber) || Object.keys(students.failed).includes(studentNumber),
|
||||
}
|
||||
|
||||
const filter = createFilter({
|
||||
|
||||
@@ -4,24 +4,32 @@ import { Message, Icon, Button } from 'semantic-ui-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import './infoBox.css'
|
||||
|
||||
const InfoBox = ({ content }) => {
|
||||
const InfoBox = ({ content, cypress }) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const toggleOpen = () => setOpen(prev => !prev)
|
||||
|
||||
return open ? (
|
||||
<Message icon className="ok-infobox" color="green">
|
||||
<div className="content-container">
|
||||
<div className="content-container" data-cy={`${cypress}-info-content`}>
|
||||
<Icon name="info circle" size="huge" />
|
||||
<Message.Content className="ok-infobox content">
|
||||
<ReactMarkdown escapeHtml={false}>{content}</ReactMarkdown>
|
||||
</Message.Content>
|
||||
</div>
|
||||
<Button onClick={toggleOpen} className="ok-infobox-close">
|
||||
<Button onClick={toggleOpen} className="ok-infobox-close" data-cy={`${cypress}-close-info`}>
|
||||
Close Info
|
||||
</Button>
|
||||
</Message>
|
||||
) : (
|
||||
<Button icon labelPosition="left" className="ok-infobox-collapsed" onClick={toggleOpen} basic color="green">
|
||||
<Button
|
||||
icon
|
||||
labelPosition="left"
|
||||
className="ok-infobox-collapsed"
|
||||
onClick={toggleOpen}
|
||||
basic
|
||||
color="green"
|
||||
data-cy={`${cypress}-open-info`}
|
||||
>
|
||||
<Icon name="info circle" />
|
||||
Show Info
|
||||
</Button>
|
||||
|
||||
@@ -9,7 +9,8 @@ import LanguagePicker from '../LanguagePicker'
|
||||
import { isDev, adminerUrls } from '../../conf'
|
||||
|
||||
const allNavigationItems = {
|
||||
faculty: { path: '/faculties', key: 'faculties', label: 'Faculties', reqRights: ['admin'] },
|
||||
trends: { path: '/trends', key: 'trends', label: 'Trends' },
|
||||
faculty: { path: '/faculties', key: 'faculties', label: 'Faculties' },
|
||||
populations: {
|
||||
key: 'studyProgramme',
|
||||
label: 'Study programme',
|
||||
@@ -18,11 +19,10 @@ const allNavigationItems = {
|
||||
{ path: '/study-programme', key: 'overview', label: 'Overview' },
|
||||
],
|
||||
},
|
||||
students: { path: '/students', key: 'students', label: 'Student statistics' },
|
||||
courseStatistics: { path: '/coursestatistics', key: 'courseStatistics', label: 'Course statistics' },
|
||||
students: { path: '/students', key: 'students', label: 'Student statistics' },
|
||||
teachers: { path: '/teachers', key: 'teachers', label: 'Teachers', reqRights: ['teachers'] },
|
||||
users: { path: '/users', key: 'users', label: 'Users', reqRights: ['admin'] },
|
||||
trends: { path: '/trends', key: 'trends', label: 'Trends' },
|
||||
studyGuidanceGroups: {
|
||||
path: '/studyguidancegroups',
|
||||
key: 'studyGuidanceGroups',
|
||||
@@ -54,6 +54,8 @@ const NavigationBar = () => {
|
||||
if (!checkUserAccess(['admin', 'studyGuidanceGroups'], roles) && rights.length === 0) return
|
||||
} else if (key === 'courseStatistics') {
|
||||
if (!checkUserAccess(['courseStatistics', 'admin'], roles) && rights.length === 0) return
|
||||
} else if (key === 'faculty') {
|
||||
if (!checkUserAccess(['facultyStatistics', 'admin'], roles)) return
|
||||
}
|
||||
const { reqRights } = allNavigationItems[key]
|
||||
if (!reqRights || reqRights.every(r => roles.includes(r))) {
|
||||
|
||||
@@ -29,11 +29,11 @@ const mapCourseData = course => ({
|
||||
},
|
||||
})
|
||||
|
||||
const GradeDistribution = ({ flat }) => {
|
||||
const GradeDistribution = ({ flat, onlyIamRights }) => {
|
||||
const { modules, courseStatistics, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
const columns = useMemo(() => {
|
||||
const columns = [
|
||||
{
|
||||
key: 'name-parent',
|
||||
mergeHeader: true,
|
||||
@@ -110,9 +110,12 @@ const GradeDistribution = ({ flat }) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[onGoToCourseStatisticsClick]
|
||||
)
|
||||
]
|
||||
|
||||
if (onlyIamRights) columns[0].children.pop()
|
||||
|
||||
return columns
|
||||
}, [onGoToCourseStatisticsClick])
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (flat) {
|
||||
|
||||
@@ -23,11 +23,11 @@ const createModuleAggregateRow = ({ definition, children }) => ({
|
||||
},
|
||||
})
|
||||
|
||||
const PassFail = ({ flat }) => {
|
||||
const PassFail = ({ flat, onlyIamRights }) => {
|
||||
const { modules, courseStatistics, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
const columns = useMemo(() => {
|
||||
const columns = [
|
||||
{
|
||||
key: 'course',
|
||||
title: 'Course',
|
||||
@@ -197,9 +197,11 @@ const PassFail = ({ flat }) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[onGoToCourseStatisticsClick]
|
||||
)
|
||||
]
|
||||
if (onlyIamRights) columns[0].children[0].children.pop()
|
||||
|
||||
return columns
|
||||
}, [onGoToCourseStatisticsClick])
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (flat) {
|
||||
|
||||
+8
-6
@@ -26,11 +26,11 @@ const createModuleAggregateRow = ({ definition, children }) => ({
|
||||
},
|
||||
})
|
||||
|
||||
const PassFailEnrollments = ({ flat }) => {
|
||||
const PassFailEnrollments = ({ flat, onlyIamRights }) => {
|
||||
const { modules, courseStatistics, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
const columns = useMemo(() => {
|
||||
const columns = [
|
||||
{
|
||||
key: 'course',
|
||||
title: 'Course',
|
||||
@@ -204,9 +204,11 @@ const PassFailEnrollments = ({ flat }) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[onGoToCourseStatisticsClick]
|
||||
)
|
||||
]
|
||||
if (onlyIamRights) columns[0].children[0].children.pop()
|
||||
|
||||
return columns
|
||||
}, [onGoToCourseStatisticsClick])
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (flat) {
|
||||
|
||||
@@ -34,12 +34,12 @@ const yearColumn = (year, cumulative) => ({
|
||||
children: [semesterColumn(year, 'Fall', cumulative), semesterColumn(year, 'Spring', cumulative)],
|
||||
})
|
||||
|
||||
const PassingSemesters = () => {
|
||||
const PassingSemesters = ({ onlyIamRights }) => {
|
||||
const { modules, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
|
||||
const [cumulativeStats, setCumulativeStats] = useState(false)
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
const columns = useMemo(() => {
|
||||
const columns = [
|
||||
{
|
||||
key: 'course-name',
|
||||
mergeHeader: true,
|
||||
@@ -118,9 +118,12 @@ const PassingSemesters = () => {
|
||||
..._.range(0, 6).map(i => yearColumn(i, cumulativeStats)),
|
||||
],
|
||||
},
|
||||
],
|
||||
[cumulativeStats, onGoToCourseStatisticsClick]
|
||||
)
|
||||
]
|
||||
|
||||
if (onlyIamRights) columns[0].children.pop()
|
||||
|
||||
return columns
|
||||
}, [cumulativeStats, onGoToCourseStatisticsClick])
|
||||
|
||||
const data = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -294,7 +294,11 @@ const PopulationCourseStats = props => {
|
||||
menuItem: 'pass/fail',
|
||||
render: () => (
|
||||
<Tab.Pane className="menuTab">
|
||||
<PassFail expandedGroups={expandedGroups} toggleGroupExpansion={toggleGroupExpansion} />
|
||||
<PassFail
|
||||
expandedGroups={expandedGroups}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
onlyIamRights={props.onlyIamRights}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
),
|
||||
},
|
||||
@@ -302,7 +306,11 @@ const PopulationCourseStats = props => {
|
||||
menuItem: 'pass/fail w/ enrollments ',
|
||||
render: () => (
|
||||
<Tab.Pane className="menuTab">
|
||||
<PassFailEnrollments expandedGroups={expandedGroups} toggleGroupExpansion={toggleGroupExpansion} />
|
||||
<PassFailEnrollments
|
||||
expandedGroups={expandedGroups}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
onlyIamRights={props.onlyIamRights}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
),
|
||||
},
|
||||
@@ -310,7 +318,11 @@ const PopulationCourseStats = props => {
|
||||
menuItem: 'grades',
|
||||
render: () => (
|
||||
<Tab.Pane className="menuTab">
|
||||
<GradeDistribution expandedGroups={expandedGroups} toggleGroupExpansion={toggleGroupExpansion} />
|
||||
<GradeDistribution
|
||||
expandedGroups={expandedGroups}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
onlyIamRights={props.onlyIamRights}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
),
|
||||
},
|
||||
@@ -323,6 +335,7 @@ const PopulationCourseStats = props => {
|
||||
courseStatistics={courseStatistics}
|
||||
expandedGroups={expandedGroups}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
onlyIamRights={props.onlyIamRights}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
),
|
||||
|
||||
@@ -84,7 +84,9 @@ const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYea
|
||||
<Segment basic>
|
||||
<InfoBox content={infotooltips.PopulationStatistics.CoursesOfPopulation} />
|
||||
{renderToggleStructureButton()}
|
||||
{query.studyRights.programme && <FilterDegreeCoursesModal studyProgramme={query.studyRights.programme} />}
|
||||
{query.studyRights.programme && !onlyIamRights && (
|
||||
<FilterDegreeCoursesModal studyProgramme={query.studyRights.programme} />
|
||||
)}
|
||||
|
||||
<SegmentDimmer isLoading={pending} />
|
||||
{showByStudytrack ? (
|
||||
|
||||
@@ -143,8 +143,8 @@ export default ({ students }) => {
|
||||
studytrack: studytrack(s.studyrights).map(st => st.name)[0],
|
||||
tags: tags(s.tags),
|
||||
'start year at university': reformatDate(s.started, 'YYYY'),
|
||||
'start of studyright': getStartOfStudyright(s.studyrights),
|
||||
'started in studyright': getStartedInStudyright(s.studyrights),
|
||||
'start of studyright': reformatDate(getStartOfStudyright(s.studyrights), 'YYYY-MM-DD'),
|
||||
'started in studyright': reformatDate(getStartedInStudyright(s.studyrights), 'YYYY-MM-DD'),
|
||||
'admission type': parseInt(queryYear, 10 >= 2020) ? getAdmissionType(s.studyrights) : undefined,
|
||||
'updated at': reformatDate(s.updatedAt, 'YYYY-MM-DD hh:mm:ss'),
|
||||
'mandatory total passed': totalMandatoryPassed(s.studentNumber),
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from 'common'
|
||||
import { useGetStudyGuidanceGroupPopulationQuery } from 'redux/studyGuidanceGroups'
|
||||
import { useGetAuthorizedUserQuery } from 'redux/auth'
|
||||
import { useGetSemestersQuery } from 'redux/semesters'
|
||||
import { PRIORITYCODE_TEXTS } from '../../../constants'
|
||||
import sendEvent from '../../../common/sendEvent'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
@@ -35,6 +36,19 @@ const GeneralTab = ({
|
||||
const { useFilterSelector } = useFilters()
|
||||
const [popupStates, setPopupStates] = useState({})
|
||||
const sendAnalytics = sendEvent.populationStudents
|
||||
const { data: semesterData } = useGetSemestersQuery()
|
||||
|
||||
const fromSemester = from
|
||||
? Object.values(semesterData.semesters)
|
||||
.filter(({ startdate }) => new Date(startdate) <= new Date(from))
|
||||
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]?.semestercode
|
||||
: null
|
||||
|
||||
const toSemester = to
|
||||
? Object.values(semesterData.semesters)
|
||||
.filter(({ enddate }) => new Date(enddate) >= new Date(to))
|
||||
.sort((a, b) => new Date(a.enddate) - new Date(b.enddate))[0]?.semestercode
|
||||
: null
|
||||
|
||||
const creditDateFilterOptions = useFilterSelector(creditDateFilter.selectors.selectOptions)
|
||||
|
||||
@@ -210,9 +224,7 @@ const GeneralTab = ({
|
||||
const enrollments =
|
||||
s.enrollments
|
||||
?.filter(e => coursecode.includes(e.course_code))
|
||||
?.filter(
|
||||
e => new Date(e.enrollment_date_time) >= new Date(from) && new Date(e.enrollment_date_time) < new Date(to)
|
||||
) ?? null
|
||||
?.filter(e => e.semestercode >= fromSemester && e.semestercode <= toSemester) ?? null
|
||||
if (!enrollments || !enrollments.length) return ''
|
||||
return enrollments[0].enrollment_date_time
|
||||
}
|
||||
|
||||
@@ -62,7 +62,13 @@ const Routes = () => (
|
||||
<Route exact path={routes.feedback} component={Feedback} />
|
||||
<Route path={routes.trends} component={Trends} />
|
||||
<ProtectedRoute requireUserHasRights exact path="/populations" component={Populations} />
|
||||
<ProtectedRoute requiredRoles={['admin']} exact path="/faculties/:facultyCode?" component={FacultyStatistics} />
|
||||
<ProtectedRoute
|
||||
requiredRoles={['admin', 'facultyStatistics']}
|
||||
requireUserHasRights
|
||||
exact
|
||||
path="/faculties/:facultyCode?"
|
||||
component={FacultyStatistics}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
requireUserHasRights
|
||||
exact
|
||||
|
||||
@@ -45,7 +45,7 @@ export const getColumnValue = (ctx, column, exportMode = false) => {
|
||||
return column.getRowExportVal(ctx.item, ctx.isGroup, ctx.parents)
|
||||
}
|
||||
|
||||
if (column.getRowVal) {
|
||||
if (column && column.getRowVal) {
|
||||
return column.getRowVal(ctx.item, ctx.isGroup, ctx.parents)
|
||||
}
|
||||
|
||||
|
||||
@@ -286,6 +286,8 @@ const SingleStudyGroupViewWrapper = ({ group, language, isLoading, studyProgramm
|
||||
history.push('/studyguidancegroups')
|
||||
}
|
||||
|
||||
if (!group) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Wrapper isLoading={isLoading}>
|
||||
@@ -293,7 +295,7 @@ const SingleStudyGroupViewWrapper = ({ group, language, isLoading, studyProgramm
|
||||
<Divider />
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'baseline' }}>
|
||||
<Header size="medium" style={{ marginRight: 'auto' }}>
|
||||
{group.name && language && getTextIn(group.name, language)}
|
||||
{group && group.name && language && getTextIn(group.name, language)}
|
||||
</Header>
|
||||
{group.tags?.studyProgramme && (
|
||||
<Label tag content={studyProgrammes.find(p => p.value === group.tags.studyProgramme)?.text} color="blue" />
|
||||
@@ -316,21 +318,15 @@ const SingleStudyGuidanceGroupContainer = ({ group }) => {
|
||||
|
||||
if (!group) {
|
||||
return (
|
||||
<SingleStudyGroupViewWrapper>
|
||||
<StyledMessage>
|
||||
Couldn't find group with this id! Please check that id is correct and you have rights to this study guidance
|
||||
group.
|
||||
</StyledMessage>
|
||||
</SingleStudyGroupViewWrapper>
|
||||
<StyledMessage>
|
||||
Couldn't find group with this id! Please check that id is correct and you have rights to this study guidance
|
||||
group.
|
||||
</StyledMessage>
|
||||
)
|
||||
}
|
||||
|
||||
if (groupStudentNumbers.length === 0) {
|
||||
return (
|
||||
<SingleStudyGroupViewWrapper>
|
||||
<StyledMessage>This study guidance group doesn't contain any students.</StyledMessage>
|
||||
</SingleStudyGroupViewWrapper>
|
||||
)
|
||||
if (!groupStudentNumbers.length) {
|
||||
return <StyledMessage>This study guidance group doesn't contain any students.</StyledMessage>
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+117
-80
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Link, useHistory } from 'react-router-dom'
|
||||
import { Formik } from 'formik'
|
||||
import { Form, Button, Icon } from 'semantic-ui-react'
|
||||
import { Form, Button, Icon, Modal } from 'semantic-ui-react'
|
||||
import Datetime from 'react-datetime'
|
||||
import { getTextIn, textAndDescriptionSearch } from 'common'
|
||||
import { useChangeStudyGuidanceGroupTagsMutation } from 'redux/studyGuidanceGroups'
|
||||
@@ -40,85 +40,113 @@ const prettifyCamelCase = str => {
|
||||
const cellWrapper = { display: 'flex', gap: '8px', width: '100%' }
|
||||
const cellContent = { flexGrow: 1 }
|
||||
|
||||
const AssociateTagForm = ({ group, tagName, toggleEdit, selectFieldItems }) => {
|
||||
const EditTagModal = ({ group, tagName, toggleEdit, selectFieldItems, open }) => {
|
||||
const [changeStudyGuidanceGroupTags, { isLoading }] = useChangeStudyGuidanceGroupTagsMutation()
|
||||
const { language } = useSelector(({ settings }) => settings)
|
||||
|
||||
const onSubmit = values => {
|
||||
changeStudyGuidanceGroupTags({ groupId: group.id, tags: values })
|
||||
toggleEdit()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal onClose={toggleEdit} open={open}>
|
||||
<Formik
|
||||
initialValues={{ [tagName]: '' }}
|
||||
onSubmit={values => {
|
||||
changeStudyGuidanceGroupTags({ groupId: group.id, tags: values })
|
||||
if (group.tags?.[tagName]) toggleEdit()
|
||||
}}
|
||||
onSubmit={values => onSubmit(values)}
|
||||
validate={values => (!values[tagName] ? { [tagName]: `${tagName} is required` } : {})}
|
||||
>
|
||||
{formik => (
|
||||
<Form onSubmit={formik.handleSubmit} style={{ ...cellWrapper, alignItems: 'center' }}>
|
||||
<div style={cellContent}>
|
||||
{tagName === 'studyProgramme' ? (
|
||||
<Form.Select
|
||||
name={tagName}
|
||||
search={textAndDescriptionSearch}
|
||||
fluid
|
||||
placeholder={
|
||||
selectFieldItems.find(p => p.value === group.tags?.[tagName])?.text || 'Select study programme'
|
||||
}
|
||||
options={selectFieldItems}
|
||||
closeOnChange
|
||||
value={formik.values[tagName]}
|
||||
onChange={(_, value) => formik.setFieldValue(tagName, value?.value)}
|
||||
/>
|
||||
) : (
|
||||
<Datetime
|
||||
className="studyguidancegroupoverview__yeartagselector"
|
||||
name={tagName}
|
||||
dateFormat="YYYY"
|
||||
timeFormat={false}
|
||||
initialvalue={group.tags?.[tagName]}
|
||||
inputProps={{ readOnly: true }}
|
||||
renderYear={(props, year) => {
|
||||
const shiftBy = 2 // fix to start from 2017 instead of 2019
|
||||
const shiftedProps = {
|
||||
...props,
|
||||
key: props.key - shiftBy,
|
||||
'data-value': props['data-value'] - shiftBy,
|
||||
}
|
||||
const formattedAndShiftedYear = startYearToAcademicYear(year - shiftBy)
|
||||
return <td {...shiftedProps}> {formattedAndShiftedYear}</td>
|
||||
}}
|
||||
renderInput={({ value, ...rest }) => {
|
||||
return (
|
||||
<div>
|
||||
<input value={startYearToAcademicYear(value)} placeholder="Select year" {...rest} />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
closeOnSelect
|
||||
value={formik.values[tagName]}
|
||||
onChange={value => formik.setFieldValue(tagName, value?.format('YYYY'))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ ...cellContent, flexGrow: 0 }}>
|
||||
<Button type="submit" style={{ margin: '0' }} disabled={isLoading}>
|
||||
Add {prettifyCamelCase(tagName)}
|
||||
</Button>
|
||||
</div>
|
||||
{group.tags?.[tagName] && (
|
||||
<div style={cellContent}>
|
||||
<Button icon type="button" style={{ margin: '0' }} onClick={() => toggleEdit()}>
|
||||
Close
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
<>
|
||||
<Modal.Header>{getTextIn(group.name, language)}</Modal.Header>
|
||||
<Modal.Content>
|
||||
<AssociateTagForm group={group} tagName={tagName} selectFieldItems={selectFieldItems} formik={formik} />
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button content="Cancel" labelPosition="right" icon="trash" onClick={toggleEdit} negative />
|
||||
<Button
|
||||
content="Save"
|
||||
type="submit"
|
||||
labelPosition="right"
|
||||
icon="checkmark"
|
||||
onClick={formik.handleSubmit}
|
||||
disabled={isLoading}
|
||||
positive
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const AssociateTagForm = ({ group, tagName, selectFieldItems, formik }) => (
|
||||
<>
|
||||
<p>
|
||||
{tagName === 'studyProgramme'
|
||||
? `Edit associated study programme for this group:`
|
||||
: `Edit associated starting year for this group:`}
|
||||
</p>
|
||||
<Form onSubmit={formik.handleSubmit} style={{ ...cellWrapper, alignItems: 'center' }}>
|
||||
<div style={cellContent}>
|
||||
{tagName === 'studyProgramme' ? (
|
||||
<Form.Select
|
||||
name={tagName}
|
||||
search={textAndDescriptionSearch}
|
||||
fluid
|
||||
placeholder={
|
||||
selectFieldItems.find(p => p.value === group.tags?.[tagName])?.text || 'Select study programme'
|
||||
}
|
||||
options={selectFieldItems}
|
||||
closeOnChange
|
||||
value={formik.values[tagName]}
|
||||
onChange={(_, value) => formik.setFieldValue(tagName, value?.value)}
|
||||
/>
|
||||
) : (
|
||||
<Datetime
|
||||
className="studyguidancegroupoverview__yeartagselector"
|
||||
name={tagName}
|
||||
dateFormat="YYYY"
|
||||
timeFormat={false}
|
||||
initialvalue={group.tags?.[tagName]}
|
||||
inputProps={{ readOnly: true }}
|
||||
renderYear={(props, year) => {
|
||||
const shiftBy = 2 // fix to start from 2017 instead of 2019
|
||||
const formattedAndShiftedYear = startYearToAcademicYear(year - shiftBy)
|
||||
const shiftedProps = {
|
||||
...props,
|
||||
key: props.key - shiftBy,
|
||||
className:
|
||||
`${formik.values[tagName]}` === formattedAndShiftedYear.substring(0, 4)
|
||||
? 'rdtYear rdtActive'
|
||||
: 'rdtYear',
|
||||
'data-value': props['data-value'] - shiftBy,
|
||||
}
|
||||
return <td {...shiftedProps}> {formattedAndShiftedYear}</td>
|
||||
}}
|
||||
renderInput={({ value, ...rest }) => {
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
value={startYearToAcademicYear(value)}
|
||||
placeholder="Select year"
|
||||
style={{ maxWidth: 400 }}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
closeOnSelect
|
||||
value={formik.values[tagName]}
|
||||
onChange={value => formik.setFieldValue(tagName, value?.format('YYYY'))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
|
||||
const TagCell = ({ tagName, group, studyProgrammes }) => {
|
||||
const [showEdit, toggleEdit] = useToggle()
|
||||
const getText = () => {
|
||||
@@ -131,17 +159,26 @@ const TagCell = ({ tagName, group, studyProgrammes }) => {
|
||||
throw Error(`Wrong tagname: ${tagName}`)
|
||||
}
|
||||
}
|
||||
return group.tags?.[tagName] && !showEdit ? (
|
||||
<div style={{ ...cellWrapper, alignItems: 'baseline' }}>
|
||||
<p style={{ ...cellContent, textAlign: 'center' }}>{getText()}</p>
|
||||
<div style={{ ...cellContent, flexGrow: 0 }}>
|
||||
<Button type="button" onClick={() => toggleEdit()}>
|
||||
Edit {prettifyCamelCase(tagName)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<AssociateTagForm group={group} tagName={tagName} toggleEdit={toggleEdit} selectFieldItems={studyProgrammes} />
|
||||
return (
|
||||
<>
|
||||
<EditTagModal
|
||||
group={group}
|
||||
tagName={tagName}
|
||||
toggleEdit={toggleEdit}
|
||||
selectFieldItems={studyProgrammes}
|
||||
open={showEdit}
|
||||
/>
|
||||
{group.tags?.[tagName] ? (
|
||||
<div style={{ ...cellWrapper, alignItems: 'baseline' }}>
|
||||
<p style={{ ...cellContent, textAlign: 'center' }}>{getText()}</p>
|
||||
<div style={{ ...cellContent, flexGrow: 0 }}>
|
||||
<Button icon="pencil" onClick={() => toggleEdit()} size="tiny" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button content={`Add ${prettifyCamelCase(tagName)}`} icon="add" onClick={() => toggleEdit()} size="tiny" />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -203,7 +240,7 @@ const StudyGuidanceGroupOverview = ({ groups }) => {
|
||||
lisäominaisuuksia.{' '}
|
||||
</p>
|
||||
</StyledMessage>
|
||||
<SortableTable figure={false} columns={headers} getRowKey={group => group.id} data={groups} />
|
||||
<SortableTable figure={false} columns={headers} getRowKey={group => group.id} data={groups} singleLine={false} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import '../studyprogramme.css'
|
||||
const ReactHighcharts = require('react-highcharts')
|
||||
require('highcharts-exporting')(ReactHighcharts.Highcharts)
|
||||
|
||||
const colors = ['#003E65', '#1392c2', '#E68825', '#333737', '#036415']
|
||||
const colors = ['#7cb5ec', '#90ed7d', '#434348', '#f7a35c', '#FFF000', '#2b908f', '#f45b5b', '#91e8e1']
|
||||
|
||||
const LineGraph = ({ cypress, data }) => {
|
||||
const dataWithColors = data?.graphStats.map((series, index) => ({
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
import React from 'react'
|
||||
|
||||
const ReactHighcharts = require('react-highcharts')
|
||||
|
||||
const colors = ['#003E65', '#1392c2', '#036415', '#bdc2c7', '#E68825', '#333737', '#54997b', '#195f8a']
|
||||
const colors = ['#7cb5ec', '#90ed7d', '#434348', '#f7a35c', '#FFF000', '#2b908f', '#f45b5b', '#91e8e1']
|
||||
|
||||
const StackedBarChart = ({ cypress, data, labels, wideTable }) => {
|
||||
const dataWithColors = data?.map((series, index) => ({ ...series, color: colors[index] }))
|
||||
@@ -17,6 +18,7 @@ const StackedBarChart = ({ cypress, data, labels, wideTable }) => {
|
||||
},
|
||||
xAxis: {
|
||||
categories: labels,
|
||||
allowDecimals: false,
|
||||
},
|
||||
chart: {
|
||||
type: 'column',
|
||||
@@ -39,6 +41,10 @@ const StackedBarChart = ({ cypress, data, labels, wideTable }) => {
|
||||
stacking: 'normal',
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter() {
|
||||
if (Number.isInteger(this.y)) return `${this.y}`
|
||||
return `${this.y.toFixed(1)}`
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+18
-3
@@ -59,7 +59,6 @@ const getColumns = (language, showStudents) => {
|
||||
getRowVal: course => course.totalAllStudents,
|
||||
getRowContent: course => course.totalAllStudents,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'totalOwnProgramme',
|
||||
title: 'Major students',
|
||||
@@ -68,7 +67,6 @@ const getColumns = (language, showStudents) => {
|
||||
getRowVal: course => course.totalProgrammeStudents,
|
||||
getRowContent: course => course.totalProgrammeStudents,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'totalOtherProgramme',
|
||||
title: 'Non-major students',
|
||||
@@ -77,7 +75,6 @@ const getColumns = (language, showStudents) => {
|
||||
getRowVal: course => course.totalOtherProgrammeStudents,
|
||||
getRowContent: course => course.totalOtherProgrammeStudents,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'totalWithoutStudyright',
|
||||
title: 'Non-degree students',
|
||||
@@ -87,6 +84,15 @@ const getColumns = (language, showStudents) => {
|
||||
getRowVal: course => course.totalWithoutStudyrightStudents,
|
||||
getRowContent: course => course.totalWithoutStudyrightStudents,
|
||||
},
|
||||
{
|
||||
key: 'transfer',
|
||||
title: 'Transferred students',
|
||||
helpText: toolTips.TransferredCourses,
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalTransferStudents,
|
||||
getRowContent: course => course.totalTransferStudents,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
columns = [
|
||||
@@ -160,6 +166,15 @@ const getColumns = (language, showStudents) => {
|
||||
getRowVal: course => course.totalWithoutStudyrightCredits,
|
||||
getRowContent: course => course.totalWithoutStudyrightCredits,
|
||||
},
|
||||
{
|
||||
key: 'totalTransfer',
|
||||
title: 'Transferred credits',
|
||||
helpText: toolTips.TransferredCredits,
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalTransferCredits,
|
||||
getRowContent: course => course.totalTransferCredits,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ const ProgrammeCoursesOverview = ({ studyProgramme, academicYear, setAcademicYea
|
||||
acc.totalOtherProgrammeCredits += curr[1].totalOtherProgrammeCredits
|
||||
acc.totalWithoutStudyrightStudents += curr[1].totalWithoutStudyrightStudents
|
||||
acc.totalWithoutStudyrightCredits += curr[1].totalWithoutStudyrightCredits
|
||||
acc.totalTransferCredits += curr[1].totalTransferCredits
|
||||
acc.totalTransferStudents += curr[1].totalTransferStudents
|
||||
}
|
||||
|
||||
return acc
|
||||
@@ -76,6 +78,8 @@ const ProgrammeCoursesOverview = ({ studyProgramme, academicYear, setAcademicYea
|
||||
totalOtherProgrammeCredits: 0,
|
||||
totalWithoutStudyrightStudents: 0,
|
||||
totalWithoutStudyrightCredits: 0,
|
||||
totalTransferCredits: 0,
|
||||
totalTransferStudents: 0,
|
||||
}
|
||||
)
|
||||
return {
|
||||
|
||||
@@ -6,15 +6,33 @@ const facultystatsApi = RTKApi.injectEndpoints({
|
||||
query: () => '/faculties',
|
||||
}),
|
||||
getFacultyBasicStats: builder.query({
|
||||
query: ({ id, yearType, specialGroups }) =>
|
||||
`/faculties/${id}/basicstats?year_type=${yearType}&special_groups=${specialGroups}`,
|
||||
query: ({ id, yearType, studyProgrammeFilter, specialGroups }) =>
|
||||
`/faculties/${id}/basicstats?year_type=${yearType}&programme_filter=${studyProgrammeFilter}&special_groups=${specialGroups}`,
|
||||
}),
|
||||
getFacultyCreditStats: builder.query({
|
||||
query: ({ id, yearType, specialGroups }) =>
|
||||
`/faculties/${id}/creditstats?year_type=${yearType}&special_groups=${specialGroups}`,
|
||||
query: ({ id, yearType, studyProgrammeFilter, specialGroups }) =>
|
||||
`/faculties/${id}/creditstats?year_type=${yearType}&programme_filter=${studyProgrammeFilter}&special_groups=${specialGroups}`,
|
||||
}),
|
||||
getFacultyThesisStats: builder.query({
|
||||
query: ({ id, yearType, studyProgrammeFilter, specialGroups }) =>
|
||||
`/faculties/${id}/thesisstats?year_type=${yearType}&programme_filter=${studyProgrammeFilter}&special_groups=${specialGroups}`,
|
||||
}),
|
||||
getFacultyGraduationTimes: builder.query({
|
||||
query: ({ id, studyProgrammeFilter }) =>
|
||||
`/faculties/${id}/graduationtimes?programme_filter=${studyProgrammeFilter}`,
|
||||
}),
|
||||
updateFacultyBasicView: builder.query({
|
||||
query: ({ id }) => `/faculties/${id}/update_basicview`,
|
||||
}),
|
||||
}),
|
||||
overrideExisting: false,
|
||||
})
|
||||
|
||||
export const { useGetFacultiesQuery, useGetFacultyBasicStatsQuery, useGetFacultyCreditStatsQuery } = facultystatsApi
|
||||
export const {
|
||||
useGetFacultiesQuery,
|
||||
useGetFacultyBasicStatsQuery,
|
||||
useGetFacultyCreditStatsQuery,
|
||||
useGetFacultyThesisStatsQuery,
|
||||
useGetFacultyGraduationTimesQuery,
|
||||
useUpdateFacultyBasicViewQuery,
|
||||
} = facultystatsApi
|
||||
|
||||
Reference in New Issue
Block a user