Compare commits

...
99 Commits
Author SHA1 Message Date
outisa 3d552a5811 Fix typos 2023-02-03 22:19:14 +02:00
outisa 624a103f08 Add moar wating time 2023-02-03 21:26:19 +02:00
outisa 9f56554ed9 Fix criteria to show correctly after change 2023-02-03 19:31:28 +02:00
outisa 0d1bc3c519 Add betterment for placeholder credit criteria 2023-02-03 19:02:04 +02:00
outisa aa1d21bc09 Modify infotext 2023-02-03 18:13:00 +02:00
outisa eaf5d7574e Add frontend functionality to change criteria for studyprogramme 2023-02-03 17:53:39 +02:00
outisa 9db14a839f Add backend functionality to change criteria for studyprogrammes 2023-02-03 17:51:32 +02:00
outisa b55d649622 Create table for atudy programme criteria 2023-02-03 17:47:50 +02:00
Miko Keskimäki 47c792e1f1 Revert "Dependabot updates"
This reverts commit 2c76703085.
2023-01-30 18:55:29 +02:00
Miko Keskimäki fffa3d3ee1 Maybe fix package updates breaking dev env setup 2023-01-30 18:41:32 +02:00
Laura Sinikallio c9407ff4b0 Improve students table column layout 2023-01-26 12:55:48 +02:00
outisa 1b43d4fc39 [Faculty] Add (new in faculty) clarifiaction for started studying 2023-01-25 16:54:19 +02:00
Laura Sinikallio c0ce396d4d Try fixing last of duplicateprogramme id issue 2023-01-20 16:34:59 +02:00
Laura Sinikallio f15ffa95f5 Update infoboxes 2023-01-20 15:40:17 +02:00
Laura Sinikallio 765d2f771a Fix lint issue 2023-01-20 15:19:20 +02:00
Laura Sinikallio e1271d7d1f Adjust table header texts 2023-01-20 14:55:54 +02:00
Laura Sinikallio 7f632bde34 Enable line break in long column headers 2023-01-20 14:44:45 +02:00
Laura Sinikallio 9dd47d002d Make column name react to HOPS filter 2023-01-20 13:54:01 +02:00
Miko Keskimäki 16a07ace39 [skip ci] Update architecture diagram 2023-01-20 09:02:00 +02:00
outisa ac756cb35b Add message box for bachelor-master credits 2023-01-19 17:44:24 +02:00
outisa b9059eb058 [Faculty] Separate programmes with different colors 2023-01-19 16:55:08 +02:00
Miko Keskimäki 1f967a1f2c [skip ci] Update Oodikone architecture diagram 2023-01-19 16:21:54 +02:00
outisa e8ef944a9d Bring total population link back 2023-01-19 13:06:48 +02:00
outisa 0e9ea20235 [Study programme] Remove unnecessary code 2023-01-19 11:05:45 +02:00
outisa 8d4f1c7794 [studyprogramme] Fix master titles and remove unnecessary code 2023-01-19 10:46:46 +02:00
Laura Sinikallio 6f3e4b8f64 Update infobox 2023-01-19 10:45:29 +02:00
Laura Sinikallio 316ed2af2f Adjust students table column order 2023-01-19 10:37:23 +02:00
Laura Sinikallio 2f753d976b Adjust students table visibility 2023-01-19 10:24:21 +02:00
iPegii 9b6aa96531 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-19 10:05:08 +02:00
iPegii 229a64aa45 Update text for "hyväksiluku"-icon 2023-01-19 10:04:51 +02:00
Miko Keskimäki 6456b84777 Get kosu and jory info from Jami, grant kosus full access to their programmes 2023-01-18 16:46:35 +02:00
Laura Sinikallio 9d34c34de7 Unify spelling, remove notification asterisk 2023-01-18 14:51:58 +02:00
Laura Sinikallio 2573976ea4 Update students table excel to match previous table changes 2023-01-18 14:04:25 +02:00
Laura Sinikallio 389b71cfbf Students table: Add start of studyright column and touch up started in programme column 2023-01-18 13:22:55 +02:00
Laura Sinikallio c0ee74b7f8 Population filters - Add passive to enrollment status filter 2023-01-18 10:07:40 +02:00
iPegii e3fbf91b99 fix frontend lint 2023-01-17 22:03:40 +02:00
iPegii 3d1f7cc655 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-17 20:50:46 +02:00
iPegii 069bd97729 [student page] show different icon for courses 'approved' 2023-01-17 20:50:37 +02:00
outisa 96fded285b [study programme] Fix MH90_001 progress table and graph 2023-01-17 20:40:56 +02:00
iPegii 9c172c2f97 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-17 20:39:48 +02:00
iPegii d37b037cc6 Column sticky on 'courses of class' 2023-01-17 20:39:35 +02:00
outisa a55eef0f34 [studyprogramme] Change credit limits for master programme 2023-01-17 19:35:26 +02:00
iPegii 01ec83d909 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-17 19:26:01 +02:00
iPegii 81e8a0c77e [pop statistics - student] courses, column sticky 2023-01-17 19:25:33 +02:00
Miko Keskimäki 7782f4f863 Move iam logic from oodikone to jami special groups 2023-01-17 15:15:52 +02:00
outisa 80f52355bb [Studyprogramme] Remove LIC/DOC thesis writers 2023-01-17 10:53:19 +02:00
outisa 28cf6c855d [Faculty] Improve thesis writers infotext 2023-01-16 21:17:46 +02:00
outisa 6d446a1c05 Chance column name Finnish to Finland 2023-01-16 21:16:55 +02:00
outisa d8214e7432 Fix test 2023-01-16 21:15:42 +02:00
outisa a8b04d56d6 [Faculty-progress] Show academic year if data available 2023-01-16 21:14:43 +02:00
Miko Keskimäki 0fc7bfda51 Remove specialGroup data for now
Messes up current IAM logic and not yet used for anything
2023-01-16 16:57:17 +02:00
iPegii 5614a6eb3f [Custom pop] Accept student number w/o leading zero 2023-01-16 15:23:29 +02:00
iPegii dfde4adf87 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-12 20:27:05 +02:00
iPegii 32a8c7c261 [class statistics] Show also grade for openUni 2023-01-12 20:26:56 +02:00
Laura Sinikallio b7390912f4 Fix typo 2023-01-12 10:27:20 +02:00
Laura Sinikallio 6af7a1a5d0 Add some documentation 2023-01-12 10:25:18 +02:00
Laura Sinikallio e24b63da74 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-11 16:31:30 +02:00
Miko Keskimäki bb5bb8d04e [skip ci] Minor setup script fix 2023-01-11 16:13:26 +02:00
Laura Sinikallio a91fb5d7b7 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-11 15:46:42 +02:00
Miko Keskimäki 5369e75b61 Update CI docker-compose 2023-01-11 14:40:46 +02:00
Laura Sinikallio 7a43433947 Handle duplicate programme name ATM 2023-01-11 14:37:12 +02:00
Miko Keskimäki 656870028e Get iams for login as from jami 2023-01-11 14:16:52 +02:00
Miko Keskimäki 1315d7ca81 Get iam groups for user table from jami 2023-01-11 13:53:31 +02:00
Miko Keskimäki 3b1f002ac8 Refactor jamiClient, test connection 2023-01-11 13:49:38 +02:00
Miko Keskimäki 00116544bf Update setup scripts 2023-01-11 13:12:22 +02:00
Miko Keskimäki 7bbccc9a81 Add jami-db to dev environment 2023-01-11 12:01:29 +02:00
outisa c18187d094 [Faculty] Modify sorting actually work in all use cases 2023-01-10 23:44:26 +02:00
outisa 7688a22c53 [Faculty programmes] Remove licentiates from doctoral view 2023-01-10 19:45:48 +02:00
outisa 49df0a0d21 [Faculty studentstats] Do not show programmes that have no data 2023-01-10 19:39:40 +02:00
outisa 449e8976d9 [Faculty basics] Remove doctors and licentiates from thesiswriters 2023-01-10 19:29:31 +02:00
outisa 6871502cb9 [Faculty progess] Modify info text 2023-01-10 19:24:50 +02:00
iPegii b72bedc49f Fix hover text when course code match 2023-01-10 10:44:00 +02:00
iPegii 9f300ccab0 [users] fix crashing on programme search 2023-01-09 18:29:59 +02:00
iPegii cb8aa00f6f [student statistics] show most recent in search 2023-01-09 17:41:40 +02:00
iPegii 4a05f136cc remove "new"-icon from Study Programme 2023-01-09 15:39:40 +02:00
iPegii 50840a3fe2 [Class statistics] improve term on credits column 2023-01-09 15:33:28 +02:00
iPegii ac440ff843 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-09 15:08:37 +02:00
iPegii b6d28c6cff improve colunm order in [class statistics] student table 2023-01-09 15:08:21 +02:00
Laura Sinikallio f79539c2a6 Adjust chart size on small dataset 2023-01-09 14:09:17 +02:00
Laura Sinikallio c46173cc98 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-09 14:07:52 +02:00
Laura Sinikallio a13c0e4628 Update info text 2023-01-09 13:51:24 +02:00
outisa a4887a30bc Fix progress tab faculty tests 2023-01-09 13:51:13 +02:00
Laura Sinikallio bf3adf446f Update tests 2023-01-09 13:44:30 +02:00
Laura Sinikallio a03d1c9707 Faculty - switch grad times default display 2023-01-09 13:27:43 +02:00
Laura Sinikallio 2d9fe72481 Update tests 2023-01-09 13:21:06 +02:00
Laura Sinikallio 15ef2f156e Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-09 13:20:07 +02:00
outisa 5470d53a19 [Faculty] Show coming academic year in studentstats only if data available 2023-01-09 13:13:50 +02:00
outisa 65eedf1de8 [Open Uni] Handle invalid dates better 2023-01-09 13:04:55 +02:00
Laura Sinikallio 1a9e51534b Studyprogramme - switch grad times default display 2023-01-09 13:03:19 +02:00
Miko Keskimäki 0d7d059541 Fix lint error 2023-01-09 12:04:34 +02:00
Miko Keskimäki 2c76703085 Dependabot updates 2023-01-09 11:52:13 +02:00
outisa 81c45c383f Fix test 2023-01-09 11:01:19 +02:00
outisa 9bb456e258 [Faculty - progress tab] Modify charts to be more compact 2023-01-09 10:45:00 +02:00
outisa c9641bb118 [Faculty - programmes tab] Add hover for programmenames and table titles 2023-01-08 13:11:51 +02:00
outisa 06d129b685 Handle cases when programmelist is empty 2023-01-05 19:33:35 +02:00
outisa b78446a11a [faculty basic view] Improve sorting 2023-01-05 19:09:53 +02:00
outisa 3511b947c4 [Faculty - programmes tab] Fix sorting 2023-01-05 16:18:23 +02:00
iPegii a338b2f4d5 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-05 10:22:26 +02:00
iPegii 3de3bbab43 improve hover text on study programme 2023-01-05 10:22:13 +02:00
75 changed files with 1679 additions and 693 deletions
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

+5 -1
View File
@@ -50,7 +50,11 @@ What different CLI options do:
### Architecture
![Oodikone architecture](Oodikone.png)
![Oodikone architecture](./documentation/Oodikone.png)
### Documentation
Folder `documentation` currently contains some notes on sis-db schema and is meant as a place for storing useful information on Oodikone to make life easier for future developers. Feel free to add anything relevant!
### Basics
+26 -19
View File
@@ -152,6 +152,12 @@ describe('Faculty overview', () => {
})
it('Graphs display data', () => {
cy.get('[data-cy="Section-bachelor"]').within(() => {
cy.get('div[class="faculty-breakdown-graph"]')
})
cy.get('[data-cy="GraduationTimeToggle"]').click()
cy.get('[data-cy="Section-bachelor"]').within(() => {
cy.get('div[class="faculty-graph"]')
cy.contains('1 graduated').should('have.length', 1)
@@ -194,6 +200,7 @@ describe('Faculty overview', () => {
})
it('Graduation times grouping and time types can be toggled', () => {
cy.get('[data-cy="GraduationTimeToggle"]').click()
cy.get('[data-cy="GroupByToggle"]').click()
cy.get('[data-cy="Section-bachelor"]').should('not.exist')
cy.get('[data-cy="Section-master"]').should('be.visible')
@@ -226,12 +233,12 @@ describe('Faculty overview', () => {
it('User can view studentdata table without %', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'NA')
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', '0 %')
})
it('User can view studentdata table with %', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="HidePercentagesToggle"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', 'NA')
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', '0 %')
})
it('Percentage Graphs exists', () => {
@@ -240,8 +247,8 @@ describe('Faculty overview', () => {
cy.get('[data-cy="Graph-FacultyMastersProgress"]').should('be.visible')
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').should('be.visible')
})
// Following two tests just test that nothig goes broken after toggling
// No data for testing that the data actual changes as supposed
// Following two tests just test that nothing goes broken after toggling
// No data for testing that data actual change as supposed
it('Graduations can be excluded', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="StudentToggle"]').click()
@@ -262,24 +269,24 @@ describe('Faculty overview', () => {
})
it('Progress tables can be toggled', () => {
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('not.be.visible')
cy.get('[data-cy="Button-Show-FacultyBachelorsProgressTable-0"]').click()
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('be.visible')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-3"]').should('not.be.visible')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-5"]').should('not.be.visible')
cy.get('[data-cy="Button-Hide-FacultyBachelorsProgressTable-0"]').click()
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('not.be.visible')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-6"]').should('not.be.visible')
cy.get('[data-cy="Button-Show-FacultyBachelorsProgressTable-6"]').click()
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('not.exist')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-1"]').should('not.exist')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-2"]').should('not.exist')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-3"]').should('not.exist')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-4"]').should('not.exist')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-6"]').should('be.visible')
cy.get('[data-cy="Button-Hide-FacultyBachelorsProgressTable-6"]').click()
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-6"]').should('not.be.visible')
})
it('Sudent statstable can be toggled', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT - KH50_005')
cy.get('[data-cy="Button-FacultyStudentStatsTable-0"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', 'TKT - KH50_005')
cy.get('[data-cy="Button-FacultyStudentStatsTable-0"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT - KH50_005')
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
cy.get('[data-cy="Button-FacultyStudentStatsTable-3"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', 'TKT')
cy.get('[data-cy="Button-FacultyStudentStatsTable-3"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
})
it('Progress infobox can be toggled', () => {
+4 -3
View File
@@ -217,8 +217,8 @@ describe('Population Statistics tests', () => {
})
})
// Fails in CI, not worth the effort to fix
/* describe('when using IAM user', () => {
/*
describe('when using IAM user', () => {
beforeEach(() => {
cy.init(pathToCSBach2017, 'onlyiamrights')
})
@@ -242,5 +242,6 @@ describe('Population Statistics tests', () => {
cy.contains('Students (170)').should('not.exist')
// TODO: somehow check that obfuscation works
})
}) */
})
*/
})
+20 -3
View File
@@ -181,6 +181,8 @@ describe('Studyprogramme overview', () => {
.should('contain', 25)
.should('contain', 35)
cy.get('[data-cy=graduation-times-graph-breakdown]')
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graph]').within(() => {
cy.contains('Graduation year')
cy.contains('2019')
@@ -206,6 +208,11 @@ describe('Studyprogramme overview', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(5000)
cy.contains('a', 'Kasvatustieteiden maisteriohjelma').click({ force: true })
cy.get('[data-cy=graduation-times-graph-breakdownMaster]')
cy.get('[data-cy=graduation-times-graph-breakdownBachelor]')
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graphMaster]').within(() => {
cy.contains('Graduation year')
cy.contains('2020')
@@ -229,6 +236,8 @@ describe('Studyprogramme overview', () => {
describe('Studytrack overview works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and class statistics').click()
})
@@ -274,6 +283,9 @@ describe('Studyprogramme overview', () => {
.should('contain', '5.6%') // The percentage for 15-29 credits in 2018-2019
.should('contain', '12.8%') // The percentage for less than 15 credits in 2019-2020
cy.get('[data-cy=graduation-times-graph-breakdown]')
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graph]').within(() => {
cy.contains('Start year')
cy.contains('2019 - 2020')
@@ -288,6 +300,8 @@ describe('Studyprogramme overview', () => {
describe('Programme courses works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Programme courses').click()
})
@@ -364,6 +378,8 @@ describe('Studyprogramme overview', () => {
describe('Basic information -view works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
})
@@ -424,8 +440,8 @@ describe('Studyprogramme overview', () => {
})
})
// Fails in CI and not worth the effor to fix
/* describe('IAM user', () => {
/*
describe('IAM user', () => {
beforeEach(() => {
cy.init('/study-programme', 'onlyiamrights')
@@ -459,5 +475,6 @@ describe('Studyprogramme overview', () => {
cy.get('[data-cy=Section-StudytrackProgress]')
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
})
}) */
})
*/
})
+15 -1
View File
@@ -66,12 +66,26 @@ services:
<<: *defaults
image: toska/jami
environment:
- NODE_ENV=production
- PORT=3003
- NODE_ENV=production
- POSTGRES_HOST=jami-db
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DATABASE=postgres
ports:
- 3003:3003
container_name: jami
jami-db:
<<: *defaults
image: postgres:13.2
environment:
- PGDATA=/data
- POSTGRES_PASSWORD=postgres
container_name: jami-db
ports:
- 5433:5432
nginx:
image: nginx:latest
container_name: nginx
+18 -1
View File
@@ -172,12 +172,28 @@ services:
<<: *defaults
image: toska/jami
environment:
- NODE_ENV=development
- PORT=3003
- NODE_ENV=dev
- POSTGRES_HOST=jami-db
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DATABASE=postgres
ports:
- 3003:3003
container_name: jami
jami-db:
<<: *defaults
image: postgres:13.2
environment:
- PGDATA=/data
- POSTGRES_PASSWORD=postgres
container_name: jami-db
ports:
- 5433:5432
volumes:
- jami-data:/data
volumes:
backend-nodemod:
frontend-nodemod:
@@ -188,3 +204,4 @@ volumes:
sis-updater-scheduler-nodemod:
sis-updater-worker-nodemod:
user-db-data:
jami-data:
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 72 KiB

+48
View File
@@ -0,0 +1,48 @@
# Mystery numbers
Sis data uses some numeric classifiers that are not self-explanatory and (most) previously needed to be deciphered from existing code.
Some mystery numbers are gathered here for convenience. Feel free to add anything that could be useful. Remember to use the spelling used in code to enable project wide search.
## extentcode
(from **studyright**)
These are actually defined in db table studyright_extents. Tells studyright level or type, eg. bachelor's degree or non-degree studies.
## prioritycode
(from **studyright**)
1: Primary
2: Secondary
5: Rescinded (Seemingly not in use anymore)
6: Option
30: Graduated
## type
(from **element_details**)
20: A study programme
30: Module/Studytrack/kokonaisuus? Eg. Neurology
## enrollmenttype
(from **semester_enrollments**)
1: Present
2: Absent
3: Inactive (neither 1 or 2 has been declared)
## credittypecode
(from **credit**)
Defined in credit_types db table. Completed/Improved/Transferred/Failed
+28
View File
@@ -10,6 +10,7 @@
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
DUMP_DIR="$PROJECT_ROOT/.databasedumps"
USER_DATA_FILE="$DUMP_DIR/hyuserdata"
DOCKER_COMPOSE=$PROJECT_ROOT/docker-compose.yml
## Following the naming convention in docker-compose, these are names for database
## services. The real data databases inside service will have names suffixed by "-real".
@@ -100,6 +101,28 @@ reset_databases() {
successmsg "Database setup finished"
}
reset_jami_data() {
local database="jami-db"
local pannu_url="svm-85.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/jami.sql.gz"
local dump_destination="$DUMP_DIR/$database.sqz"
infomsg "Downloading Jami data"
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkki.cs.helsinki.fi" "$username@$pannu_url" "$dump_destination"
infomsg "Removing database and related volume"
docker volume rm oodikone_jami-data || warningmsg "This is okay, continuing"
docker-compose -f "$DOCKER_COMPOSE" down "$database" || warningmsg "This is okay, continuing"
infomsg "Starting postgres in the background"
docker-compose -f "$DOCKER_COMPOSE" up -d "$database"
retry docker-compose -f "$DOCKER_COMPOSE" exec "$database" pg_isready --dbname="postgres"
infomsg "Populating Jami"
docker exec -i "$database" /bin/bash -c "gunzip | psql -U postgres" < "$dump_destination"
docker-compose stop "$database"
msg ""
}
reset_all_real_data() {
infomsg "Downloading real data dumps, asking for pannu password when needed"
for i in ${!DATABASES[*]}; do
@@ -108,6 +131,7 @@ reset_all_real_data() {
download_real_dump "$database" "$url"
done
reset_databases "${DATABASES[@]}"
reset_jami_data
}
reset_single_database() {
@@ -119,6 +143,7 @@ reset_single_database() {
"sis-db"
"sis-importer-db"
"user-db"
"jami-db"
"Return to main menu."
)
@@ -144,6 +169,9 @@ reset_single_database() {
local database=$USER_DB_NAME
local url=$USER_DB_REAL_DUMP_URL
break 2;;
"jami-db")
reset_jami_data
break 2;;
"Return to main menu.")
break 2;;
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
+1 -1
View File
@@ -125,7 +125,7 @@ const codes = {
T922106: 'foodhealth',
T923101: 'papu',
T923102: 'geodoc',
T923103: 'atm',
T923103: 'atm-dp',
T923104: 'chems',
T923105: 'domast',
T923106: 'matrena',
@@ -0,0 +1,33 @@
const { INTEGER, STRING, ARRAY, DATE } = require('sequelize')
module.exports = {
up: async queryInterface => {
await queryInterface.createTable('progress_criteria', {
code: {
type: STRING,
primaryKey: true,
},
courses_year_one: {
type: ARRAY(STRING),
},
courses_year_two: {
type: ARRAY(STRING),
},
courses_year_three: {
type: ARRAY(STRING),
},
credits_year_one: {
type: INTEGER,
},
credits_year_two: {
type: INTEGER,
},
credits_year_three: {
type: INTEGER,
},
created_at: DATE,
updated_at: DATE,
})
},
down: () => {},
}
@@ -28,7 +28,8 @@ const currentUserMiddleware = async (req, _res, next) => {
}
const iamGroups = parseIamGroups(hygroupcn)
const iamRights = Object.keys(await getOrganizationAccess({ iamGroups }))
const { access = {}, specialGroup = {} } = await getOrganizationAccess({ iamGroups })
const iamRights = Object.keys(access)
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
logger.error({ message: 'User does not have required iam group', meta: { iamGroups, iamRights } })
@@ -41,6 +42,7 @@ const currentUserMiddleware = async (req, _res, next) => {
email,
iamGroups,
iamRights,
specialGroup,
sisId,
})
@@ -1,3 +1,4 @@
const { STRING } = require('sequelize')
const Sequelize = require('sequelize')
const { sequelizeKone } = require('../database/connection')
@@ -143,6 +144,35 @@ const OpenUniPopulationSearch = sequelizeKone.define(
{ underscored: true, timestamps: true }
)
const StudyProgrammeCriteria = sequelizeKone.define(
'progress_criteria',
{
code: {
primaryKey: true,
type: STRING,
},
coursesYearOne: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
coursesYearTwo: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
coursesYearThree: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
creditsYearOne: {
type: Sequelize.INTEGER,
},
creditsYearTwo: {
type: Sequelize.INTEGER,
},
creditsYearThree: {
type: Sequelize.INTEGER,
},
},
{ underscored: true, timestamps: true }
)
const StudyGuidanceGroupTag = sequelizeKone.define(
'study_guidance_group_tags',
{
@@ -188,4 +218,5 @@ module.exports = {
sequelizeKone,
ExcludedCourse,
StudyGuidanceGroupTag,
StudyProgrammeCriteria,
}
+2
View File
@@ -31,6 +31,7 @@ const programmeModules = require('./routes/programmeModules')
const studyGuidanceGroups = require('./routes/studyGuidanceGroups')
const studyProgramme = require('./routes/studyProgramme')
const customOpenUniSearch = require('./routes/customOpenUniSearch')
const studyProgrammeCriteria = require('./routes/studyProgrammeCriteria')
const initializeSentry = require('./util/sentry')
const errorMiddleware = require('./middleware/errorMiddleware')
@@ -58,6 +59,7 @@ module.exports = (app, url) => {
app.use(url, tags)
app.use(url, programmeModules)
app.use(url, studyProgramme)
app.use(`${url}/programmecriteria`, studyProgrammeCriteria)
app.use(`${url}/openunisearch`, auth.roles(['openUniSearch']), customOpenUniSearch)
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
app.use(`${url}/updater`, auth.roles(['admin']), updater)
@@ -1,5 +1,5 @@
const router = require('express').Router()
var moment = require('moment-timezone')
const moment = require('moment-timezone')
const { getCustomOpenUniCourses } = require('../services/openUni/openUniStats')
const {
getOpenUniSearches,
@@ -0,0 +1,30 @@
const {
getCriteria,
saveYearlyCourseCriteria,
saveYearlyCreditCriteria,
} = require('../services/studyProgrammeCriteria')
const router = require('express').Router()
router.get('/', async (req, res) => {
const studyProgramme = req.query?.programmecode
if (!studyProgramme) return res.status(422).end()
const studyProgrammeCriteria = await getCriteria(studyProgramme)
return res.json(studyProgrammeCriteria)
})
router.post('/courses', async (req, res) => {
const { code, courses, year } = req.body
if (!code || !courses || !year) return res.status(400).end()
const studyProgrammeCriteria = await saveYearlyCourseCriteria(code, courses, year)
return res.json(studyProgrammeCriteria)
})
router.post('/credits', async (req, res) => {
const { code, credits } = req.body
if (!code || !credits) return res.status(400).end()
const studyProgrammeCriteria = await saveYearlyCreditCriteria(code, credits)
return res.json(studyProgrammeCriteria)
})
module.exports = router
@@ -111,7 +111,7 @@ const getFacultyStarters = async (
}
})
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
allBasics.studentInfo.graphStats.push({ name: 'Started studying (new in faculty)', data: startedGraphStats })
programmeData['started'] = programmeTableStats
Object.keys(startedTableStats).forEach(year => {
@@ -155,6 +155,7 @@ const getFacultyGraduates = async (
if (keys.includes(programme)) {
programmeId = codes[programme].toUpperCase()
}
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
const endYear = defineYear(enddate, isAcademicYear)
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
@@ -21,7 +21,8 @@ const getSortedProgIds = progs => {
for (const prog of progs) {
if (Object.keys(codes).includes(prog)) {
programmes = [...programmes, { name: codes[prog].toUpperCase(), code: prog }]
const name = prog === 'T923103' ? codes[prog].toUpperCase() + '-DP' : codes[prog].toUpperCase()
programmes = [...programmes, { name: name, code: prog }]
} else {
programmes = [...programmes, { name: prog, code: prog }]
}
@@ -135,6 +135,9 @@ const mapCodesToIds = data => {
for (const prog of data) {
if (keys.includes(prog.code)) {
prog.progId = codes[prog.code].toUpperCase()
if (prog.code === 'T923103') {
prog.progId = prog.progId + '-DP'
}
} else {
prog.progId = prog.code
}
@@ -10,35 +10,21 @@ const {
getYearsArray,
tableTitles,
getOnlyMasterCreditGraphStats,
getOnlyMasterThresholds,
getBcMsThresholds,
getVetenaryCreditGraphStats,
} = require('../studyprogrammeHelpers')
const { studytrackStudents } = require('../studyprogramme')
const { getStudyRightsByExtent, getTransferredInside, getTransferredToAndAway } = require('./faculty')
const { checkTransfers } = require('./facultyHelpers')
const getStudentData = (
startDate,
students,
thresholdKeys,
thresholdAmounts,
limits,
limitKeys,
prog,
earlyStudyRights = []
) => {
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, limits = [], limitKeys = [], prog) => {
let data = {}
let programmeData = {}
let start = startDate
thresholdKeys.forEach(t => (data[t] = 0))
limitKeys.forEach(t => (programmeData[t] = 0))
students.forEach(({ studentnumber, credits }) => {
if (earlyStudyRights.length > 0) {
const bcStudyright = earlyStudyRights.filter(sr => sr.studentnumber === studentnumber)
start = bcStudyright.length > 0 ? bcStudyright[0].startdate : startDate
}
students.forEach(({ credits }) => {
const creditcount = credits
.filter(credit => moment(credit.attainment_date).isSameOrAfter(start))
.filter(credit => moment(credit.attainment_date).isSameOrAfter(startDate))
.reduce((prev, curr) => prev + curr.credits, 0)
data[thresholdKeys[0]] += creditcount < thresholdAmounts[0] ? 1 : 0
data[thresholdKeys[1]] += creditcount >= thresholdAmounts[0] && creditcount < thresholdAmounts[1] ? 1 : 0
@@ -139,10 +125,7 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
yearlyBachelorTitles: [],
yearlyBcMsTitles: [],
yearlyMasterTitles: [],
programmeNames: programmes.reduce(
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
{}
),
programmeNames: {},
bachelorsProgStats: {},
bcMsProgStats: {},
mastersProgStats: {},
@@ -160,12 +143,15 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
let bachelorlimits = []
let masterlimits = []
let bcmslimits = []
const bachelorProgress = {}
const masterProgress = {}
const bcmsProgress = {}
const doctoralProgress = {}
for (const year of reversedYears) {
if (year === 'Total') continue
const { startDate, endDate } = getAcademicYearDates(year, since)
const lastDayOfMonth = moment().endOf('month')
let months = Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(startDate))).asMonths())
if (months >= 36) {
bachelorlimits = createLimits(36, 0)
} else {
@@ -177,18 +163,22 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
masterlimits = createLimits(months, 0)
}
if (months >= 72 && faculty === 'H90') {
bcmslimits = createLimits(72, 180)
bcmslimits = createLimits(72, 0)
} else if (months >= 60) {
bcmslimits = createLimits(60, 180)
bcmslimits = createLimits(60, 0)
} else {
bcmslimits = createLimits(months, 180)
bcmslimits = createLimits(months, 0)
}
progressStats.yearlyBachelorTitles = [...progressStats.yearlyBachelorTitles, createYearlyTitles(0, bachelorlimits)]
progressStats.yearlyMasterTitles = [...progressStats.yearlyMasterTitles, createYearlyTitles(0, masterlimits)]
progressStats.yearlyBcMsTitles = [...progressStats.yearlyBcMsTitles, createYearlyTitles(180, bcmslimits)]
progressStats.yearlyBcMsTitles = [...progressStats.yearlyBcMsTitles, createYearlyTitles(0, bcmslimits)]
const programmeCodes = programmes.map(prog => prog.code)
for (const { progId, code } of programmes) {
for (const { progId, code, name } of programmes) {
let extentcodes = [1, 2, 3, 4]
if (includeAllSpecials) {
extentcodes = [...extentcodes, ...[7, 9, 34, 22, 99, 14, 13]]
}
const startDateWhere = {
startdate: {
[Op.and]: {
@@ -197,23 +187,18 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
},
},
}
let extentcodes = []
if (includeAllSpecials) {
extentcodes = [7, 9, 34, 22, 99, 14, 13]
}
let studyrights = await getStudyRightsByExtent(faculty, {}, startDateWhere, code, extentcodes, graduationStatus)
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code)
if (!creditThresholdKeys) return
if (!creditThresholdKeys) continue
if (!(progId in progressStats.programmeNames)) {
progressStats.programmeNames[progId] = { code: code, ...name }
}
if (!includeAllSpecials) {
studyrights = await filterOutTransfers(studyrights, programmeCodes, allProgrammeCodes, since)
}
if (code.includes('KH')) {
extentcodes.push(1)
let studyrights = await getStudyRightsByExtent(faculty, {}, startDateWhere, code, extentcodes, graduationStatus)
if (!includeAllSpecials) {
studyrights = await filterOutTransfers(studyrights, programmeCodes, allProgrammeCodes, since)
}
const all = studyrights
.filter(sr => sr.studyrightElements.some(element => element.code === code))
.map(sr => sr.studentnumber)
const students = await studytrackStudents(all)
const allBachelors = studyrights.filter(sr => sr.extentcode === 1).map(sr => sr.studentnumber)
const students = await studytrackStudents(allBachelors)
const { data: studentData, progData } = getStudentData(
startDate,
students,
@@ -224,12 +209,12 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
'KH'
)
if (!(progId in progressStats.bachelorsProgStats)) {
progressStats.bachelorsProgStats[progId] = new Array(reversedYears.length - 1)
if (!(progId in bachelorProgress)) {
bachelorProgress[progId] = new Array(reversedYears.length - 1)
}
progressStats.bachelorsTableStats[indexOf(reversedYears, year)][1] += students.length || 0
progressStats.bachelorsTableStats[indexOf(reversedYears, 'Total')][1] += students.length || 0
progressStats.bachelorsProgStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => progData[key])
bachelorProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => progData[key])
for (const key of Object.keys(studentData)) {
progressStats.bachelorsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
studentData[key] || 0
@@ -239,16 +224,12 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
progressStats.bachelorsGraphStats[key].data[indexOf(yearsArray, 'Total')] += studentData[key] || 0
}
} else if (code.includes('MH')) {
extentcodes.push(2)
let all = await getStudyRightsByExtent(faculty, startDateWhere, {}, code, extentcodes, graduationStatus)
if (!includeAllSpecials) {
all = await filterOutTransfers(all, programmeCodes, allProgrammeCodes, since)
}
const allMsStudents = all.filter(sr => sr.studyrightid.slice(-2) !== '-2').map(sr => sr.studentnumber)
const allBcMsStudents = all.filter(sr => sr.studyrightid.slice(-2) === '-2').map(sr => sr.studentnumber)
const studentsWithBachelor = all
.filter(sr => sr.studyrightid.slice(-2) === '-2')
.map(sr => ({ studentnumber: sr.studentnumber, startdate: sr.startdate }))
const allMsStudents = studyrights
.filter(sr => sr.extentcode === 2 && sr.studyrightid.slice(-2) !== '-2')
.map(sr => sr.studentnumber)
const allBcMsStudents = studyrights
.filter(sr => sr.extentcode === 2 && sr.studyrightid.slice(-2) === '-2')
.map(sr => sr.studentnumber)
const bcMsStudents = await studytrackStudents(allBcMsStudents)
const msStudents = await studytrackStudents(allMsStudents)
@@ -256,85 +237,85 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
progressStats.mastersTableStats[indexOf(reversedYears, 'Total')][1] += msStudents.length || 0
progressStats.bcMsTableStats[indexOf(reversedYears, year)][1] += bcMsStudents.length || 0
progressStats.bcMsTableStats[indexOf(reversedYears, 'Total')][1] += bcMsStudents.length || 0
const { creditThresholdKeysBcMs, creditThresholdAmountsBcMs } = getBcMsThresholds()
const { data: bcMsStudentdata, progData: bcMsProgdata } = getStudentData(
startDate,
bcMsStudents,
creditThresholdKeys,
creditThresholdAmounts,
creditThresholdKeysBcMs,
creditThresholdAmountsBcMs,
bcmslimits,
limitKeys,
'MH',
studentsWithBachelor
'MH'
)
const { msOnlyCreditThresholdKeys, msOnlyCreditThresholdAmount } = getOnlyMasterThresholds()
const { data: msStudentdata, progData: msProgdata } = getStudentData(
startDate,
msStudents,
msOnlyCreditThresholdKeys,
msOnlyCreditThresholdAmount,
creditThresholdKeys,
creditThresholdAmounts,
masterlimits,
limitKeys,
'MH'
)
if (!(progId in progressStats.mastersProgStats)) {
progressStats.mastersProgStats[progId] = new Array(reversedYears.length - 1)
if (!(progId in masterProgress)) {
masterProgress[progId] = new Array(reversedYears.length - 1)
bcmsProgress[progId] = new Array(reversedYears.length - 1)
}
if (!(progId in progressStats.bcMsProgStats)) {
progressStats.bcMsProgStats[progId] = new Array(reversedYears.length - 1)
}
progressStats.mastersProgStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => msProgdata[key])
progressStats.bcMsProgStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => bcMsProgdata[key])
masterProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => msProgdata[key])
bcmsProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => bcMsProgdata[key])
for (const key of Object.keys(msStudentdata)) {
progressStats.mastersTableStats[indexOf(reversedYears, year)][indexOf(msOnlyCreditThresholdKeys, key) + 2] +=
progressStats.mastersTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
msStudentdata[key] || 0
progressStats.mastersTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeys, key) + 2] +=
msStudentdata[key] || 0
progressStats.mastersTableStats[indexOf(reversedYears, 'Total')][
indexOf(msOnlyCreditThresholdKeys, key) + 2
] += msStudentdata[key] || 0
progressStats.mastersGraphStats[key].data[indexOf(yearsArray, year)] += msStudentdata[key] || 0
progressStats.mastersGraphStats[key].data[indexOf(yearsArray, 'Total')] += msStudentdata[key] || 0
}
for (const key of Object.keys(bcMsStudentdata)) {
progressStats.bcMsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
progressStats.bcMsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeysBcMs, key) + 2] +=
bcMsStudentdata[key] || 0
progressStats.bcMsTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeysBcMs, key) + 2] +=
bcMsStudentdata[key] || 0
progressStats.bcMsGraphStats[key].data[indexOf(yearsArray, year)] += bcMsStudentdata[key] || 0
progressStats.bcMsTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeys, key) + 2] +=
bcMsStudentdata[key] || 0
progressStats.bcMsGraphStats[key].data[indexOf(yearsArray, 'Total')] += bcMsStudentdata[key] || 0
}
} else {
extentcodes.push(4)
let studyrights = await getStudyRightsByExtent(faculty, {}, startDateWhere, code, extentcodes, graduationStatus)
if (!includeAllSpecials) {
studyrights = await filterOutTransfers(studyrights, programmeCodes, allProgrammeCodes, since)
}
const all = studyrights
.filter(sr => sr.studyrightElements.some(element => element.code === code))
.map(sr => sr.studentnumber)
const students = await studytrackStudents(all)
const { data } = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts, [], [], 'T')
const all = studyrights.filter(sr => sr.extentcode === 4 || sr.extentcode === 3).map(sr => sr.studentnumber)
const doctoralStudents = await studytrackStudents(all)
const { data } = getStudentData(startDate, doctoralStudents, creditThresholdKeys, creditThresholdAmounts, 'T')
progressStats.doctoralTableStats[indexOf(reversedYears, year)][1] += students.length || 0
progressStats.doctoralTableStats[indexOf(reversedYears, 'Total')][1] += students.length || 0
if (!(progId in progressStats.doctoralProgStats)) {
progressStats.doctoralProgStats[progId] = new Array(reversedYears.length - 1)
progressStats.doctoralTableStats[indexOf(reversedYears, year)][1] += doctoralStudents.length || 0
progressStats.doctoralTableStats[indexOf(reversedYears, 'Total')][1] += doctoralStudents.length || 0
if (!(progId in doctoralProgress)) {
doctoralProgress[progId] = new Array(reversedYears.length - 1)
}
progressStats.doctoralProgStats[progId][indexOf(reversedYears, year)] = creditThresholdKeys.map(
key => data[key]
)
doctoralProgress[progId][indexOf(reversedYears, year)] = creditThresholdKeys.map(key => data[key])
for (const key of Object.keys(data)) {
progressStats.doctoralTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
data[key] || 0
progressStats.doctoralGraphStats[key].data[indexOf(yearsArray, year)] += data[key] || 0
progressStats.doctoralTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeys, key) + 2] +=
data[key] || 0
progressStats.doctoralGraphStats[key].data[indexOf(yearsArray, year)] += data[key] || 0
progressStats.doctoralGraphStats[key].data[indexOf(yearsArray, 'Total')] += data[key] || 0
}
}
}
}
progressStats.bachelorsProgStats = Object.keys(bachelorProgress)
.filter(prog => !bachelorProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: bachelorProgress[prog] }), {})
progressStats.mastersProgStats = Object.keys(masterProgress)
.filter(prog => !masterProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: masterProgress[prog] }), {})
progressStats.bcMsProgStats = Object.keys(bcmsProgress)
.filter(prog => !bcmsProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: bcmsProgress[prog] }), {})
progressStats.doctoralProgStats = Object.keys(doctoralProgress)
.filter(prog => !doctoralProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: doctoralProgress[prog] }), {})
return progressStats
}
module.exports = { combineFacultyStudentProgress }
@@ -88,6 +88,7 @@ const getFacultyDataForYear = async ({
facultyExtra,
allTotals,
progTotals,
programmeNames,
}) => {
const { includeAllSpecials, includeGraduated } = settings
const { startDate, endDate } = getAcademicYearDates(year, since)
@@ -102,7 +103,7 @@ const getFacultyDataForYear = async ({
}
const totals = emptyTotals()
for (const { progId, code } of programmes) {
for (const { progId, code, name } of programmes) {
let elementStart = {}
let studyrightStart = {}
const start = moment(`${year.slice(0, 4)}-08-01`, 'YYYY-MM-DD')
@@ -156,11 +157,18 @@ const getFacultyDataForYear = async ({
s.semesters.filter(semester => semester.semestercode === currentSemester.semestercode)[0]?.enrollmenttype === 1
)
// Count stats for the programme
if (students.length === 0) continue
if (!(progId in programmeTableStats)) {
programmeTableStats[progId] = years.reduce((resultObj, yearKey) => {
return { ...resultObj, [yearKey]: [] }
}, {})
progTotals[progId] = emptyTotals()
}
if (!(progId in programmeNames)) {
programmeNames[progId] = { code: code, ...name }
progTotals[progId] = emptyTotals()
}
const yearTotals = {
total: students.length,
allStarted: started.length,
@@ -226,8 +234,9 @@ const combineFacultyStudents = async (code, programmes, allProgrammeCodes, speci
const facultyTableStats = getYearsObject({ years, emptyArrays: true })
const programmeTableStats = {}
const facultyExtra = years.reduce((acc, year) => ({ ...acc, [year]: {} }), {})
const progTotals = programmes.reduce((acc, row) => ({ ...acc, [row.progId]: emptyTotals() }), {})
const progTotals = {}
const allTotals = emptyTotals()
const programmeNames = {}
for (const year of years.reverse()) {
if (year === 'Total') continue
const queryParams = {
@@ -242,15 +251,19 @@ const combineFacultyStudents = async (code, programmes, allProgrammeCodes, speci
facultyExtra,
allTotals,
progTotals,
programmeNames,
}
const totals = await getFacultyDataForYear(queryParams)
addTotals(facultyTableStats, year, totals)
if (totals.total !== 0) {
addTotals(facultyTableStats, year, totals)
}
}
addTotals(facultyTableStats, 'Total', allTotals)
Object.keys(progTotals).forEach(programme =>
addTotalsProgramme(programmeTableStats, programme, 'Total', progTotals[programme])
)
const studentsData = {
id: code,
years: years,
@@ -258,10 +271,7 @@ const combineFacultyStudents = async (code, programmes, allProgrammeCodes, speci
facultyTableStatsExtra: facultyExtra,
programmeStats: programmeTableStats,
titles: tableTitles['programmes'],
programmeNames: programmes.reduce(
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
{}
),
programmeNames: programmeNames,
}
return studentsData
}
@@ -11,22 +11,15 @@ const {
} = 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',
]
const thesisTypes = ['urn:code:course-unit-type:bachelors-thesis', 'urn:code:course-unit-type:masters-thesis']
let bachelors = getStatsBasis(years)
let masters = getStatsBasis(years)
let doctors = getStatsBasis(years)
let licentiates = getStatsBasis(years)
let programmeCounts = {}
let programmeNames = {}
for (const { progId, code, name } of facultyProgrammes) {
if (code === 'MH70_008_2') continue
if (code === 'MH70_008_2' || code.startsWith('LIS') || code.startsWith('T')) continue
const provider = mapToProviders([code])[0]
const students = await getCorrectStudentnumbers({
codes: [code],
@@ -42,7 +35,7 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
if (!(progId in programmeCounts)) {
programmeCounts[progId] = {}
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0, 0, 0]))
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0]))
programmeNames[progId] = { ...name, code: code }
}
programmeCounts[progId][thesisYear][0] += 1
@@ -55,19 +48,11 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
masters.graphStats[indexOf(years, thesisYear)] += 1
masters.tableStats[thesisYear] += 1
programmeCounts[progId][thesisYear][2] += 1
} else if (courseUnitType === thesisTypes[2]) {
doctors.graphStats[indexOf(years, thesisYear)] += 1
doctors.tableStats[thesisYear] += 1
programmeCounts[progId][thesisYear][3] += 1
} else if (courseUnitType === thesisTypes[3]) {
licentiates.graphStats[indexOf(years, thesisYear)] += 1
licentiates.tableStats[thesisYear] += 1
programmeCounts[progId][thesisYear][4] += 1
}
})
}
return { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames }
return { bachelors, masters, programmeCounts, programmeNames }
}
const getFacultyThesisWritersForProgrammes = async (
@@ -79,24 +64,18 @@ const getFacultyThesisWritersForProgrammes = async (
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
)
const { bachelors, masters, 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],
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(programmeId => {
@@ -120,7 +99,7 @@ const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType,
tableStats: [],
graphStats: [],
programmeTableStats: {},
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Licentiates'],
titles: ['', 'All', 'Bachelors', 'Masters'],
programmeNames: {},
status: 'Done',
lastUpdated: '',
@@ -9,7 +9,6 @@ const {
} = require('./openUniHelpers')
const { Op } = require('sequelize')
// 1. iteration: time is hardcoded. Check hyväksytty grades
const getCredits = async (courseCodes, startdate) =>
(
await Credit.findAll({
@@ -0,0 +1,122 @@
const { StudyProgrammeCriteria } = require('../models/models_kone')
const logger = require('../util/logger')
const getCriteriaByStudyProgramme = code =>
StudyProgrammeCriteria.findOne({
where: {
code: code,
},
})
const formattedData = updatedData => {
const criteria = {
courses: {
yearOne: updatedData.coursesYearOne,
yearTwo: updatedData.coursesYearTwo,
yearThree: updatedData.coursesYearThree,
},
credits: {
yearOne: updatedData.creditsYearOne,
yearTwo: updatedData.creditsYearTwo,
yearThree: updatedData.creditsYearThree,
},
}
return criteria
}
const createCriteria = async (studyProgramme, coYear1, coYear2, coYear3, crYear1, crYear2, crYear3) => {
const newProgrammeCriteria = {
code: studyProgramme,
coursesYearOne: coYear1,
coursesYearTwo: coYear2,
coursesYearThree: coYear3,
creditsYearOne: crYear1,
creditsYearTwo: crYear2,
creditsYearThree: crYear3,
}
try {
const createdData = await StudyProgrammeCriteria.create(newProgrammeCriteria)
const criteria = formattedData(createdData)
return criteria
} catch (error) {
logger.error(`Create criteria failed: ${error}`)
}
}
const saveYearlyCreditCriteria = async (studyProgramme, credits) => {
const studyProgrammeToUpdate = await getCriteriaByStudyProgramme(studyProgramme)
if (!studyProgrammeToUpdate) {
return await createCriteria(
studyProgramme,
[],
[],
[],
parseInt(credits.year1),
parseInt(credits.year2),
parseInt(credits.year3)
)
}
const yearlyCredits = {
creditsYearOne: parseInt(credits.year1),
creditsYearTwo: parseInt(credits.year2),
creditsYearThree: parseInt(credits.year3),
}
try {
const updatedData = await studyProgrammeToUpdate.update({ ...yearlyCredits })
const criteria = formattedData(updatedData)
return criteria
} catch (error) {
logger.error(`Update yearly credit criteria failed: ${error}`)
}
}
const saveYearlyCourseCriteria = async (studyProgramme, courses, year) => {
const studyProgrammeToUpdate = await getCriteriaByStudyProgramme(studyProgramme)
if (!studyProgrammeToUpdate) {
const courseObj = { year1: [], year2: [], year3: [] }
if (year === 1) {
courseObj.year1 = courses
} else if (year === 2) {
courseObj.year2 = courses
} else {
courseObj.year3 = courses
}
return await createCriteria(studyProgramme, courseObj.year1, courseObj.year2, courseObj.year3, 0, 0, 0)
}
try {
let updatedData = {}
if (year === 1) updatedData = await studyProgrammeToUpdate.update({ coursesYearOne: courses })
if (year === 2) updatedData = await studyProgrammeToUpdate.update({ coursesYearTwo: courses })
if (year === 3) updatedData = await studyProgrammeToUpdate.update({ coursesYearThree: courses })
const criteria = formattedData(updatedData)
return criteria
} catch (error) {
logger.error(`Update yearly credit criteria failed: ${error}`)
}
}
const getCriteria = async studyProgramme => {
const studyProgrammeCriteria = await getCriteriaByStudyProgramme(studyProgramme)
const criteria = {
courses: {
yearOne: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearOne : [],
yearTwo: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearTwo : [],
yearThree: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearThree : [],
},
credits: {
yearOne: studyProgrammeCriteria ? studyProgrammeCriteria.creditsYearOne : 0,
yearTwo: studyProgrammeCriteria ? studyProgrammeCriteria.creditsYearTwo : 0,
yearThree: studyProgrammeCriteria ? studyProgrammeCriteria.creditsYearThree : 0,
},
}
return criteria
}
module.exports = {
getCriteria,
saveYearlyCourseCriteria,
saveYearlyCreditCriteria,
}
@@ -260,25 +260,39 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) =>
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const titles = ['', 'Graduated', 'Wrote thesis']
const titles =
studyprogramme.includes('LIS') || studyprogramme.includes('T')
? ['', 'Graduated']
: ['', 'Graduated', 'Wrote thesis']
const programmesBeforeOrAfterTitles = ['Code', 'Id', 'Programme', ...years]
const tableStats = reversedYears.map(year => [year, graduated.tableStats[year], thesis.tableStats[year]])
const tableStats =
studyprogramme.includes('LIS') || studyprogramme.includes('T')
? reversedYears.map(year => [year, graduated.tableStats[year]])
: reversedYears.map(year => [year, graduated.tableStats[year], thesis.tableStats[year]])
return {
id: studyprogramme,
years,
tableStats,
titles,
graphStats: [
{
name: 'Graduated students',
data: graduated.graphStats,
},
{
name: 'Wrote thesis',
data: thesis.graphStats,
},
],
graphStats:
studyprogramme.includes('LIS') || studyprogramme.includes('T')
? [
{
name: 'Graduated students',
data: graduated.graphStats,
},
]
: [
{
name: 'Graduated students',
data: graduated.graphStats,
},
{
name: 'Wrote thesis',
data: thesis.graphStats,
},
],
graduationTimes: graduationTimeStats.times,
doCombo: graduationTimeStats.doCombo,
comboTimes: graduationTimeStats.comboTimes,
@@ -315,6 +315,7 @@ const getVetenaryCreditGraphStats = years => ({
data: getEmptyArray(years.length),
},
})
const getOnlyMasterCreditGraphStats = years => ({
lte15: {
name: 'Less than 15 credits',
@@ -365,10 +366,11 @@ const getDoctoralCreditGraphStats = years => ({
},
})
const getCreditGraphStats = (studyprogramme, years) => {
const getCreditGraphStats = (studyprogramme, years, studyprog = false) => {
if (studyprogramme.includes('KH')) return getBachelorCreditGraphStats(years)
if (studyprogramme === 'MH90_001') return getVetenaryCreditGraphStats(years)
if (studyprogramme.includes('MH')) return getMasterCreditGraphStats(years)
if (studyprogramme === 'MH90_001')
return studyprog ? getBachelorCreditGraphStats(years) : getVetenaryCreditGraphStats(years)
if (studyprogramme.includes('MH')) return getOnlyMasterCreditGraphStats(years)
return getDoctoralCreditGraphStats(years)
}
@@ -382,18 +384,20 @@ const onlyMasterCreditAmounts = [15, 30, 60, 90, 120, 120]
const doctoralCreditAmounts = [10, 20, 30, 40, 40]
const vetenaryCreditThresholds = ['lte210', 'lte240', 'lte270', 'lte300', 'lte330', 'lte360', 'mte360']
const vetenaryCreditAmounts = [210, 240, 270, 300, 330, 360, 360]
const getOnlyMasterThresholds = () => {
return { msOnlyCreditThresholdKeys: onlyMasterCreditThresholds, msOnlyCreditThresholdAmount: onlyMasterCreditAmounts }
const getBcMsThresholds = () => {
return { creditThresholdKeysBcMs: masterCreditThresholds, creditThresholdAmountsBcMs: masterCreditAmounts }
}
const getCreditThresholds = studyprogramme => {
const getCreditThresholds = (studyprogramme, studyProg = false) => {
if (studyprogramme === 'MH90_001') {
return { creditThresholdKeys: vetenaryCreditThresholds, creditThresholdAmounts: vetenaryCreditAmounts }
return studyProg
? { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
: { creditThresholdKeys: vetenaryCreditThresholds, creditThresholdAmounts: vetenaryCreditAmounts }
}
if (studyprogramme.includes('KH')) {
return { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
}
if (studyprogramme.includes('MH')) {
return { creditThresholdKeys: masterCreditThresholds, creditThresholdAmounts: masterCreditAmounts }
return { creditThresholdKeys: onlyMasterCreditThresholds, creditThresholdAmounts: onlyMasterCreditAmounts }
}
return { creditThresholdKeys: doctoralCreditThresholds, creditThresholdAmounts: doctoralCreditAmounts }
}
@@ -460,7 +464,7 @@ const tableTitles = {
'Graduated',
'Men',
'Women',
'Finnish',
'Finland',
],
programmes: [
'',
@@ -473,14 +477,14 @@ const tableTitles = {
'Men',
'Women',
'Other/Unknown',
'Finnish',
'Finland',
'Other',
],
}
const getCreditProgressTableTitles = studyprogramme => {
if (studyprogramme.includes('KH')) return tableTitles.creditProgress.bachelor
if (studyprogramme.includes('MH')) return tableTitles.creditProgress.master
if (studyprogramme.includes('MH')) return tableTitles.creditProgress.masterOnly
return tableTitles.creditProgress.doctoral
}
@@ -552,7 +556,7 @@ module.exports = {
getCreditThresholds,
tableTitles,
getCreditProgressTableTitles,
getOnlyMasterThresholds,
getBcMsThresholds,
isNonMajorCredit,
mapCodesToIds,
getId,
@@ -30,7 +30,6 @@ const { countTimeCategories } = require('./graduationHelpers')
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, credits }) => {
const creditcount = credits
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
@@ -84,6 +83,29 @@ const getGraduationTimeStats = async ({ year, graduated, track, graduationAmount
]
}
const getStats = (all, started, enrolled, absent, inactive, graduated, studentData) => {
return [
all.length,
getPercentage(all.length, all.length),
started.length,
getPercentage(started.length, all.length),
enrolled.length,
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
]
}
// Goes through the programme and all its studytracks for the said year and adds the wanted stats to the data objects
const getStudytrackDataForTheYear = async ({
studyprogramme,
@@ -174,7 +196,6 @@ const getStudytrackDataForTheYear = async ({
threshold => (creditGraphStats[track][threshold].data[indexOf(years, 'Total')] += studentData[threshold])
)
// Count stats for the credit progress table for the year per track
creditTableStats[track] = [
[year, all.length, ...creditThresholdKeys.map(threshold => studentData[threshold])],
...creditTableStats[track],
@@ -184,32 +205,12 @@ const getStudytrackDataForTheYear = async ({
(a, b) => a.map((colVal, idx) => colVal + (b[idx] || 0)),
Array(creditThresholdKeys.length + 2).fill(0)
)
creditTableStats[track] = [[year, all.length, ...totalCredits.slice(2)], ...creditTableStats[track]]
creditTableStats[track] = [[year, ...totalCredits.slice(1)], ...creditTableStats[track]]
}
// Count stats for the main studytrack table grouped by tracks
mainStatsByTrack[track] = [
[
year,
all.length,
getPercentage(all.length, all.length),
started.length,
getPercentage(started.length, all.length),
enrolled.length,
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
],
[year, ...getStats(all, started, enrolled, absent, inactive, graduated, studentData)],
...mainStatsByTrack[track],
]
@@ -217,24 +218,7 @@ const getStudytrackDataForTheYear = async ({
mainStatsByYear[year] = [
[
studytrackNames[track]?.name['fi'] ? `${studytrackNames[track]?.name['fi']}, ${track}` : year,
all.length,
getPercentage(all.length, all.length),
started.length,
getPercentage(started.length, all.length),
enrolled.length,
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
...getStats(all, started, enrolled, absent, inactive, graduated, studentData),
],
...mainStatsByYear[year],
]
@@ -288,7 +272,7 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
const totals = {}
studytracks.forEach(async track => {
mainStatsByTrack[track] = []
creditGraphStats[track] = getCreditGraphStats(studyprogramme, years)
creditGraphStats[track] = getCreditGraphStats(studyprogramme, years, true)
creditTableStats[track] = []
graduationAmounts[track] = getYearsObject({ years })
totalAmounts[track] = getYearsObject({ years })
@@ -331,7 +315,7 @@ const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings })
const studytrackNames = associations.studyTracks
const data = getEmptyStatsObjects(years, studytracks, studyprogramme)
const { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(studyprogramme)
const { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(studyprogramme, true)
const yearsReversed = [...years].reverse()
@@ -358,7 +342,10 @@ const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings })
studytrackOptions,
includeGraduated: settings.graduated,
populationTitles: tableTitles['studytracks'],
creditTableTitles: getCreditProgressTableTitles(studyprogramme),
creditTableTitles:
studyprogramme === 'MH90_001'
? tableTitles['creditProgress']['bachelor']
: getCreditProgressTableTitles(studyprogramme),
}
}
+31 -23
View File
@@ -8,12 +8,10 @@ const { checkStudyGuidanceGroupsAccess, getAllStudentsUserHasInGroups } = requir
const _ = require('lodash')
const { User, UserElementDetails, AccessGroup, UserFaculties, sequelizeUser } = require('../models/models_user')
const { getOrganizationAccess } = require('../util/organizationAccess')
const { getUserIams, getAllUserAccess } = require('../util/jami')
const courseStatisticsGroup = 'grp-oodikone-basic-users'
const facultyStatisticsGroup = 'grp-oodikone-users'
const hyOneGroup = 'hy-one'
const toskaGroup = 'grp-toska'
const openUniGroup = 'hy-ypa-opa-dojo'
// Max 25 users can be stored in the cache
const userDataCache = new LRU({
@@ -170,28 +168,34 @@ const getAccessGroups = async () => await AccessGroup.findAll()
const enrichProgrammesFromFaculties = faculties =>
facultiesAndProgrammesForTrends.filter(f => faculties.includes(f.faculty_code)).map(f => f.programme_code)
const findAll = async () =>
(
await User.findAll({
include: userIncludes,
})
).map(user => ({
const findAll = async () => {
const allUsers = await User.findAll({
include: userIncludes,
})
const userAccess = await getAllUserAccess()
const formattedUsers = allUsers.map(user => ({
...user.get(),
is_enabled: true,
iam_groups: userAccess.find(({ id }) => id === user.sisu_person_id)?.iamGroups || [],
elementdetails: _.uniqBy([
...user.programme.map(p => p.elementDetailCode),
...enrichProgrammesFromFaculties(user.faculty.map(f => f.faculty_code)),
]),
}))
const formatUser = async userFromDb => {
return formattedUsers
}
const formatUser = async (userFromDb, extraRights) => {
const [accessGroups, programmes, faculties] = [
['accessgroup', 'group_code'],
['programme', 'elementDetailCode'],
['faculty', 'faculty_code'],
].map(([field, code]) => userFromDb[field].map(entry => entry[code]))
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)])
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)]).concat(extraRights)
const {
dataValues: {
@@ -232,15 +236,18 @@ const getMockedUser = async ({ userToMock, mockedBy }) => {
}
const userFromDb = await byUsername(userToMock)
const formattedUser = await formatUser(userFromDb)
userFromDb.iamGroups = await getUserIams(userFromDb.sisu_person_id)
const iamRights = Object.keys(await getOrganizationAccess(userFromDb)).filter(
programmeCode => !formattedUser.rights.includes(programmeCode)
)
const { access = {}, specialGroup = {} } = await getOrganizationAccess(userFromDb)
const iamRights = Object.keys(access)
const formattedUser = await formatUser(userFromDb, specialGroup.kosu ? iamRights : [])
const toReturn = {
...formattedUser,
iamRights,
specialGroup,
mockedBy,
}
userDataCache.set(userToMock, toReturn)
@@ -248,7 +255,7 @@ const getMockedUser = async ({ userToMock, mockedBy }) => {
return toReturn
}
const getUser = async ({ username, name, email, iamGroups, iamRights, sisId }) => {
const getUser = async ({ username, name, email, iamGroups, iamRights, specialGroup, sisId }) => {
if (userDataCache.has(username)) {
return userDataCache.get(username)
}
@@ -262,23 +269,24 @@ const getUser = async ({ username, name, email, iamGroups, iamRights, sisId }) =
email,
language,
sisu_person_id: sisId,
iam_groups: iamGroups,
last_login: new Date(),
})
const { kosu, jory, hyOne, superAdmin, openUni } = specialGroup
const userFromDb = await byUsername(username)
const formattedUser = await formatUser(userFromDb)
const formattedUser = await formatUser(userFromDb, kosu ? iamRights : [])
const { roles: currentAccessGroups } = formattedUser
// Modify accessgroups based on current groups and iamGroups
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 (iamGroups.includes(openUniGroup)) newAccessGroups.push('openUniSearch')
if (jory || iamGroups.includes(facultyStatisticsGroup)) newAccessGroups.push('facultyStatistics')
if (hyOne || currentAccessGroups.includes('teachers')) newAccessGroups.push('teachers')
if (superAdmin || currentAccessGroups.includes('admin')) newAccessGroups.push('admin')
if (openUni) newAccessGroups.push('openUniSearch')
// In the difference method "the order and references of result values are determined by the first array." https://lodash.com/docs/4.17.15#difference
// Both directions needs to be checked in order to update roles when the access should no longer exists.
if (
+79
View File
@@ -0,0 +1,79 @@
const axios = require('axios')
const Sentry = require('@sentry/node')
const { importerToken, jamiUrl } = require('../conf-backend')
const logger = require('./logger')
const jamiClient = axios.create({
baseURL: jamiUrl,
params: {
token: importerToken,
},
})
const getUserIamAccess = async (user, attempt = 1) => {
if (user.iamGroups.length === 0) return {}
const { id, iamGroups } = user
try {
const { data: iamAccess } = await jamiClient.post('/', {
userId: id,
iamGroups,
})
const { specialGroup } = iamAccess
delete iamAccess.specialGroup
return { iamAccess, specialGroup }
} catch (error) {
if (attempt > 3) {
logger.error('[Jami] error: ', error)
Sentry.captureException(error)
return {}
}
return getUserIamAccess(user, attempt + 1)
}
}
const getUserIams = async userId => {
try {
const { data } = await jamiClient.get(`/${userId}`)
return data.iamGroups
} catch (error) {
if (error.response.status !== 404) {
logger.error('[Jami] error: ', error)
Sentry.captureException(error)
}
return []
}
}
const getAllUserAccess = async () => {
const { data } = await jamiClient.get('/all-access')
return data
}
const testJami = async () => {
try {
await jamiClient.get('/ping', { timeout: 4000 })
logger.info('JAMI connected')
} catch (error) {
logger.error(error)
logger.warn('JAMI not responding :(')
logger.info('Are you sure you are using the latest JAMI image?')
}
}
testJami()
module.exports = {
getUserIamAccess,
getUserIams,
getAllUserAccess,
}
-22
View File
@@ -1,22 +0,0 @@
const axios = require('axios')
const { importerToken, jamiUrl } = require('../conf-backend')
const logger = require('./logger')
const jamiClient = axios.create({
params: {
token: importerToken,
},
baseURL: jamiUrl,
})
const getJamiClient = () => {
if (!importerToken) {
logger.error("Importer token not set, can't return client!")
return null
}
return jamiClient
}
module.exports = {
getJamiClient,
}
@@ -1,46 +1,21 @@
/* eslint-disable no-unused-vars */
const _ = require('lodash')
const Sentry = require('@sentry/node')
const logger = require('./logger')
const { getJamiClient } = require('../util/jamiClient')
const jamiClient = getJamiClient()
const getIAMAccessFromJami = async (user, attempt = 1) => {
const { id, iamGroups } = user
try {
const { data: iamAccess } = await jamiClient.post('/', {
userId: id,
iamGroups,
})
return iamAccess
} catch (error) {
if (attempt > 3) {
logger.error(error)
Sentry.captureException(error)
return {}
}
return getIAMAccessFromJami(user, attempt + 1)
}
}
const { getUserIamAccess } = require('./jami')
const getAccessFromIAMs = async user => {
if (user.iamGroups.length === 0) return {}
const { iamAccess, specialGroup } = await getUserIamAccess(user)
const access = {}
if (_.isObject(iamAccess)) {
Object.keys(iamAccess).forEach(code => {
access[code] = iamAccess[code]
})
}
const iamAccess = await getIAMAccessFromJami(user)
if (!_.isObject(iamAccess)) return access
Object.keys(iamAccess).forEach(code => {
access[code] = iamAccess[code]
})
return access
return { access, specialGroup }
}
const getOrganizationAccess = async user => {
@@ -34,7 +34,7 @@ export default {
**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.`,
ThesisWritersOfTheFaculty: `Sisältää kyseisenä vuonna kandidaatintutkielman tai pro gradu -tutkielman 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.
@@ -55,25 +55,27 @@ export default {
Valmistumisajoista on **vähennetty lakisääteiset poissaolot**.
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
Vihreä **vaakapalkki** merkitsee, että keskimääräinen valmistumisaika on alle tai yhtä kuin tavoiteaika, keltainen palkki merkitsee korkeintaan vuoden ylitystä
ja punainen yli vuoden tavoiteajan ylitystä.
Palkissa oleva lukumäärä kertoo **montako opiskelijaa** kuhunkin valmistuneiden ryhmään kuuluu.
Tarkastellessa valmistumisaikoja aloitusvuosittain palkissa näkyy myös **kuinka suuri osa** kyseisenä vuonna tiedekuntaan/opinto-ohjelmaan opinto-oikeuden saaneista on valmistunut.
Palkissa oleva lukumäärä kertoo **montako opiskelijaa** kuhunkin valmistuneiden ryhmään kuuluu.
Vihreä **vaakapalkki** kuvaa tavoiteajassa (tai alle) valmistuneista, keltainen palkki korkeintaan vuodella tavoiteajan ylittäneitä
ja punainen yli vuodella tavoiteajan ylittäneitä.
Säädöt:
**Breakdown**: Näyttää, kuinka moni opiskelija valmistui ajallaan, korkeintaan vuoden myöhässä tavoiteajasta tai tätä myöhemmin.
**Median time**: Näyttää kyseisenä vuonna/lukuvuonna valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
**Graduation year**: Ryhmittelee tulokset valmistumisvuosittain. Näyttää siis vuonna xxxx valmistuneiden opiskelijoiden keskimääräiset valmistumisajat.
Luvut ovat lopullisia (kuluva vuosi poislukien), sillä edeltäville vuosille ei tule uusia valmistumisia.
**Starting year**. Ryhmittelee tulokset opiskelijoiden opinto-oikeuden alkamisvuosittain. Näyttää siis vuonna xxxx aloittaneiden opiskelijoiden keskimääräiset valmistumisajat. Luvut voivat elää, koska osa vuonna xxxx
aloittaneiden opiskelijoiden opinnoista voi olla edelleen kesken.
**Median time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
Mediaaninäkymässä vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
**Breakdown**: Näyttää, kuinka moni opiskelija valmistui ajallaan, korkeintaan vuoden myöhässä tavoiteajasta tai tätä myöhemmin.`,
Tarkastellessa valmistumisaikoja aloitusvuosittain palkissa näkyy myös **kuinka suuri osa** kyseisenä vuonna tiedekuntaan/opinto-ohjelmaan opinto-oikeuden saaneista on valmistunut.
`,
// Faculty Student population
GraduatedToggle: `
@@ -85,7 +87,7 @@ export default {
Kuvaa tiedekunnan ohjelmissa opiskelevien opiskelijoden tilastoja. Esimerkiksi 2020-2021 aloittaneiden opintopistejakaumaa.
HUOM!
- **All** Sisältää kaikki opiskelijat, joiden opinto-oikeudella on aloituspäivä kyseisenä vuonna, riippumatta siitä, onko opiskelija aloittanut opiskelemaan ko. vuonna.
- **All** Sisältää opiskelijat, jotka siirtyneet ohjelmaan tai siitä ulos sekä aloittaneet ohjelmassa
- **Currently enrolled** Sisältää kaikki tällä hetkellä läsnäolevat olevat opiskelija.
- **Absent** Sisältää kaikki lakisääteisistä syistä poissaolevat opiskelijat.
- **Started studying** Sisältää kaikki opiskelijat, jotka ovat aloittaneet opiskelemaan koulutusohjelmassa kyseisenä lukuvuonna.
@@ -93,22 +95,18 @@ export default {
joissa opiskelija on laiminlyönyt ilmoittautumisen tälle lukukaudelle.
Mukana ovat oletusarvoisesti myös vaihto-opiskelijat, ohjelmaan ja siitä pois siirtyneet opiskelijat sekä erillisopinto-oikeudella opiskelevat.
Taulukko näyttää myös näistä opiskelijoista miesten, naisten ja suomalaisten osuudet sekä valmistuneiden määrän.
Yläosan valikosta on mahdollista valita tarkasteluun yhden opinto-ohjelman opiskelijat tietylle vuodelle. Sekä opiskelijoiden yleistiedot että edistyminen opintopisteittäin
kuvaavat tällöin kyseisen opintosuunnan opiskelijoita.`,
Taulukko näyttää myös näistä opiskelijoista miesten, naisten ja suomalaisten osuudet sekä valmistuneiden määrän. `,
StudentProgress: `
Kuvaa tiedekuntaan kuuluvien eri ohjemien opinto-oikeudella varustettujen opiskelijoiden etenemistä opintopisteissä mitattuna. Esimerkiksi 2021-2022 aloittaneiden opintopistejakaumaa.
- Bachelor: Kandidaatin opinto-oikeudella varustettujen opiskelijoiden eteneminen
- Bachelor + Master: Kandi+maisteriopinto-oikeudella opiskelleiden ja siitä maisteriksi ohjelmiin siirtyneiden etenenminen
- Bachelor + Master: Kandi+maisteriopinto-oikeudella opiskelleiden ja siitä maisteriksi ohjelmiin siirtyneiden etenenminen. Aloitusvuodeksi on merkitty kandiohjelman aloitusvuosi.
Opinto-ohjelmaksi on merkitty maisteriohjelma, johon opiskelija on siirtynyt kandiksi valmistumisen jälkeen.
- Master: Pelkällä maisterintutkinnon opinto-oikeudella varustettujen opiskelijoiden eteneminen
- Doctor: Tohtorin ja lisensiaatin opinto-oikeudella varustettujen opiskelijoiden eteneminen
- Doctor: Tohtorin opinto-oikeudella varustettujen opiskelijoiden eteneminen
HUOM! Mukana ovat myös muuhun kuin valittuun koulutusohjelmaan tai opintosuuntaan tehdyt opintopisteet.
HUOM! Mukana ovat myös muuhun kuin valittuun koulutusohjelmaan tai opintosuuntaan tehdyt opintopisteet. Opintopisteet on laskettu opinto-oikeuden alkamisesta, eikä tällä hetkellä ota huomioon mahdollisia hyväksilukuja.
Mukana ovat oletusarvoisesti myös vaihto-opiskelijat, erillisopinto-oikeudella opiskelevat ja ohjelmaan siirtyneet.
Mikäli yläosan valikosta valittuna on jokin tietty opintosuunta, myös nämä graafit ja taulukot kuvaavat kyseisen opintosuunnan opiskelijoita.
`,
}
@@ -1,8 +1,8 @@
export default {
enrollmentStatus: {
label: 'Filter students present or absent',
label: 'Filter students present, absent or passive',
short:
'Suodata opiskelijoita sen mukaan ovatko he ilmoittautuneet läsnä- tai poissaoleviksi kyseiselle lukukaudelle',
'Suodata opiskelijoita sen mukaan ovatko he joko ilmoittautuneet läsnä- tai poissaoleviksi tai jääneet passiivisiksi kyseiselle lukukaudelle. Huom. osa passiivisista opiskelijoista on jo valmistunut ko. ohjelmasta.',
},
studyrightStatus: {
label: 'Rajaa opiskelijoita opinto-oikeuden statuksen mukaan',
@@ -66,8 +66,8 @@ export default {
**Taulukot**
- **pass/fail** - oletusnäkymä, joka sisältää kurssisuoritukset, hylätyt sekä yritykset. Taulukon tiedot voi järjestää sarakkeita (n, after retry, percentage, many times, per student, passed ja attempted) klikkaamalla.
- **pass/fail w/ enrollments** - näkymä, joka sisältää suoritustiedot ilmoittautumisten kanssa. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
- **pass/fail** - näkymä sisältää kurssisuoritukset, hylätyt sekä yritykset sekä ilmoittautumistiedot. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
- **grades** - arvosanajakauma.
- **when passed** - kurssisuorituksen ajankohta. Huom.! *Passed*-sarakkeen kokonaissumma ei välttämättä täsmää puolivuosittaisten lukumäärien summan kanssa johtuen tuplakirjauksista Oodissa.
`,
@@ -83,31 +83,64 @@ export default {
Tägien ja opintosuuntien valinnat ovat siirtyneet tästä omiksi filtereikseen.
Voit tehdä valinnat siirryttyäsi tarkastelemaan ohjelman tilastoja.
`,
Students: `**Students**
StudentsGuidanceGroups: `**Students**
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä henkilösymbolia, siirryt kyseisen opiskelijan "student statistic" -näkymään.
Klikkaamalla "Sisu"-nuolta, siirryt tarkastelemaan opiskelijan tietoja Sisu-järjestelmässä.
**General**
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä nuolta, siirtyy "student statistic" näkymään valitun opiskelijan osalta.
Valikoituja selvennyksiä:
- **Credits**
- **All** listaa opiskelijan suoritetut opintopisteet valitun tarkastelujakson ajalta.
- **Since ... ** listaa opiskelijan suoritetut opintopisteet määritellystä ajankohdasta alkaen
- **Start of studyright**: Opiskelijan valittuun ohjelmaan liittyvän opinto-oikeuden alkupäivä
- **Started in programme**: Opiskelijan valitussa ohjelmassa aloituspäivämäärä. Kandi+maisteriopiskelijoille kandiksi valmistumispäivämäärä +1.
- **credits since start of studyright:** listaa opiskelijan suoritetut opintopisteet valitun tarkastelujakson / populaation ajalta.
- **all credits:** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
- **transferred from:** opiskelijan vanha koulutusohjelma, josta opiskelija on siirtynyt uuteen.
- **studytracks:** opintosuunta
`,
StudentsClass: `**Students**
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
**Mandatory Courses**
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä henkilösymbolia, siirryt kyseisen opiskelijan "student statistic" -näkymään.
Klikkaamalla "Sisu"-nuolta, siirryt tarkastelemaan opiskelijan tietoja Sisu-järjestelmässä.
**General**
Valikoituja selvennyksiä:
- **Credits**
- **All** listaa opiskelijan suoritetut opintopisteet valitun tarkastelujakson ajalta
- **HOPS** listaa opiskelijan kaikki HOPSiin sijoitut opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
- **Since start in programme** listaa opiskelijan ohjelmassa aloittamisen jälkeen suoritut opintopisteet
- **Transferred from:** opiskelijan vanha koulutusohjelma, josta opiskelija on siirtynyt uuteen.
- **Start of studyright**: Opiskelijan valittuun ohjelmaan liittyvän opinto-oikeuden alkupäivä
- **Started in programme**: Opiskelijan ko. ohjelmassa aloituspäivämäärä. Kandi+maisteriopiskelijoille kandiksi valmistumispäivämäärä +1
- **Semesters**: Läsnäololukukausien lukumäärä
**Courses**
Tällä välilehdellä näkyy, mitä pakollisiksi määriteltyjä kursseja opiskelija on suorittanut.
Kurssit voi määrittää kohdassa "Study program" > "Overview" > haluttu koulutusohjelma. Scrollatessa kurssitaulukkoa alaspäin, kurssin nimen saa näkyviin viemällä kursorin halutun solun kohdalle.
**Tags**
Opiskelijoille voi luoda tag:eja ryhmittelyä varten kohdassa "Study Program" > "Overview" > haluttu koulutusohjelma.
**Download**
`,
StudentsCustom: `**Students**
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
Klikkaamalla tätä harmaata kuvaketta, saa ladattua .xlsx tiedoston, joka sisältää kaikki "General" ja "Mandatory Courses" välilehtien tiedot.
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä henkilösymbolia, siirryt kyseisen opiskelijan "student statistic" -näkymään.
Klikkaamalla "Sisu"-nuolta, siirryt tarkastelemaan opiskelijan tietoja Sisu-järjestelmässä.
**All credits** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
`,
Filters: {
Add: `Harmaata ”add” painiketta klikkaamalla aukeaa filters- eli suodatinvalikko,
@@ -41,30 +41,30 @@ export default {
**Graduated**: Sisältää kyseisenä vuonna valmistuneet opiskelijat riippumatta siitä, minä vuonna opiskeluoikeus ohjelmaan on myönnetty. Jaottelu on tehty opinto-oikeuden päättymispäivän (=valmistumispäivän) perusteella.
`,
AverageGraduationTimes: `
**Median time**: Näyttää kyseisenä kalenteri- tai lukuvuotena valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
Yksittäinen palkki kertoo, kuinka moni opiskelija on valmistunut kyseisenä vuonna/lukuvuonna.
Vihreä **vaakapalkki** kuvaa tavoiteajassa (tai alle) valmistuneita, keltainen palkki korkeintaan vuoden tavoiteajasta myöhässä valmistuneita
ja punainen yli vuoden tavoiteajan ylittäneitä.
**Breakdown**: Näyttää, kuinka moni kyseisenä kalenteri- tai lukuvuotena valmistuneista opiskelijoista valmistui ajallaan, korkeintaan vuoden myöhässä tavoiteajasta tai tätä myöhemmin.
Valmistumisajoista on vähennetty lakisääteiset poissaolot.
Yksittäinen palkki kertoo, kuinka moni opiskelija on valmistunut kyseisenä vuonna.
**Median time**: Näyttää kyseisenä kalenteri- tai lukuvuotena valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
Vihreä **vaakapalkki** merkitsee, että keskimääräinen valmistumisaika on alle tai yhtä kuin tavoiteaika, keltainen palkki merkitsee korkeintaan vuoden ylitystä
ja punainen yli vuoden tavoiteajan ylitystä.`,
Valmistumisajoista on vähennetty lakisääteiset poissaolot.`,
AverageGraduationTimesStudytracks: `
Yksittäinen palkki kertoo, kuinka moni kyseisenä **lukukautena aloittaneista** opiskelijoista on valmistunut .
Yksittäinen palkki kertoo, kuinka moni kyseisenä **lukuvuotena ALOITTANEISTA** opiskelijoista on valmistunut.
**Median time**: Näyttää valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina sekä heidän prosentuaalinen osuutensa koko vuosikurssista.
Vihreä **vaakapalkki** kuvaa tavoiteajassa (tai alle) valmistuneita, keltainen palkki korkeintaan vuoden tavoiteajasta myöhässä valmistuneita
ja punainen yli vuoden tavoiteajan ylittäneitä.
**Breakdown**: Näyttää, kuinka moni kyseisenä lukukautena aloittaneista, jo valmistuneista, opiskelijoista on valmistui ajallaan, korkeintaan vuoden myöhässä tavoiteajasta tai tätä myöhemmin.
Valmistumisajoista on vähennetty lakisääteiset poissaolot.
**Breakdown**: Näyttää, kuinka moni kyseisenä lukuvuotena aloittaneista, jo valmistuneista, opiskelijoista on valmistui ajallaan, korkeintaan vuoden myöhässä tavoiteajasta tai tätä myöhemmin.
**Median time**: Näyttää valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina sekä heidän prosentuaalisen osuutensa koko vuosikurssista.
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
Vihreä **vaakapalkki** merkitsee, että keskimääräinen valmistumisaika on alle tai yhtä kuin tavoiteaika, keltainen palkki merkitsee korkeintaan vuoden ylitystä
ja punainen yli vuoden tavoiteajan ylitystä.`,
Valmistumisajoista on vähennetty lakisääteiset poissaolot.`,
ProgrammesBeforeOrAfter: `
**Mikäli valittuna on kandiohjelma:** Sisältää maisteriohjelmat, joissa tämän kandiohjelman opiskelijat ovat aloittaneet valmistuttuaan kandeiksi. Vuosijaottelu on tehty maisteriohjelmassa aloittamisen perusteella.
@@ -92,10 +92,19 @@ const CustomOpenUniSearch = ({ setValues, savedSearches, location, history }) =>
const courseList = input.split(/[\s,]+/).map(code => code.trim().toUpperCase())
const query = {
courseCode: courseList,
startdate: moment(startdate).format('DD-MM-YYYY'),
enddate: moment(enddate).format('DD-MM-YYYY'),
startdate: moment(startdate).isValid()
? moment(startdate).format('DD-MM-YYYY')
: moment('01-08-2017', 'DD-MM-YYYY').format('DD-MM-YYYY'),
enddate: moment(enddate).isValid() ? moment(enddate).format('DD-MM-YYYY') : moment().format('DD-MM-YYYY'),
}
setValues({ courseList, startdate: startdate.toISOString(), enddate: enddate.toISOString() })
setValues({
courseList,
startdate: moment(startdate).isValid()
? moment(startdate).toISOString()
: moment('01-08-2017', 'DD-MM-YYYY').toISOString(),
enddate: moment(enddate).isValid() ? moment(enddate).toISOString() : moment().toISOString(),
})
pushQueryToUrl(query)
handleClose()
}
@@ -87,7 +87,7 @@ const CustomPopulationSearch = ({
const onClicker = e => {
e.preventDefault()
const studentnumbers = input.match(/[0-9]+/g)
const studentnumbers = input.match(/[0-9]+/g).map(sNumber => (sNumber.length === 8 ? `0${sNumber}` : sNumber))
getCustomPopulationDispatch({ studentnumberlist: studentnumbers, onProgress })
getCustomPopulationCoursesByStudentnumbers({ studentnumberlist: studentnumbers })
selectCustomPopulationSearchDispatch(selectedSearchId || null)
@@ -128,8 +128,21 @@ const Overview = ({
transferShortTitles = ['Code', 'Started', 'Graduated']
}
const options = {
KH: 'Bachelors',
MH: 'Masters',
T: 'Doctors and Licentiates',
LIS: 'Doctors and Licentiates',
OTHER: 'Other',
}
const getChartPlotLinePlaces = programmeKeys => {
const plotLinePlaces = []
if (programmeKeys.length === 0) return []
let key = programmeKeys[0][1].slice(0, 2)
if (!['KH', 'MH', 'T', 'LIS'].includes(key)) {
key = 'OTHER'
}
const plotLinePlaces = [[0, options[key]]]
for (let i = 0; i < programmeKeys.length - 1; i++) {
if (
(programmeKeys[i][1].startsWith('KH') && programmeKeys[i + 1][1].startsWith('MH')) ||
@@ -143,11 +156,31 @@ const Overview = ({
programmeKeys[i][1].startsWith('MH')) &&
programmeKeys[i + 1][1].startsWith('K-'))
) {
plotLinePlaces.push(i)
let key = programmeKeys[i + 1][1].slice(0, 2)
if (!['KH', 'MH'].includes(key)) {
const keyT = programmeKeys[i + 1][1].slice(0, 1)
const keyLis = programmeKeys[i + 1][1].slice(0, 3)
if (keyT === 'T') {
key = keyT
} else if (keyLis === 'LIS') {
key = keyLis
} else {
key = 'OTHER'
}
}
if (
!programmeKeys[i + 1][1].includes(faculty.code) &&
(programmeKeys[i + 1][1].startsWith('MH') || programmeKeys[i + 1][1].startsWith('KH'))
) {
plotLinePlaces.push([i + 1, `${options[key]} secondary`])
} else {
plotLinePlaces.push([i + 1, options[key]])
}
}
}
return plotLinePlaces
}
return (
<div className="faculty-overview">
<div className="toggle-container">
@@ -13,6 +13,7 @@ const FacultyProgressTable = ({
language,
cypress,
progressTitles,
needsExtra = 'NO EXTRA',
}) => {
const [visible, setVisible] = useState(progressYearsVisible)
if (!data) return null
@@ -21,6 +22,7 @@ const FacultyProgressTable = ({
arrayToModify[yearIndex] = !visible[yearIndex]
setVisible(arrayToModify)
}
const lenOfYearArray = data.length - 1
return (
<div>
@@ -34,43 +36,47 @@ const FacultyProgressTable = ({
</Table.Header>
<Table.Body>
{data?.map((yearArray, yearIndex) => (
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
{yearIndex < lenOfYearArray ? (
<BasicRow
icon={visible[yearIndex] ? 'angle down' : 'angle right'}
yearArray={yearArray}
cypress={visible[yearIndex] ? `Hide-${cypress}` : `Show-${cypress}`}
yearIndex={yearIndex}
toggleVisibility={() => toggleVisibility(yearIndex)}
/>
) : (
<Table.Row>
{yearArray?.map(value => (
<Table.Cell key={`last-row-${Math.random()}`}>{value}</Table.Cell>
))}
</Table.Row>
)}
{yearIndex < lenOfYearArray ? (
<Table.Row
key={`${cypress}-row-${Math.random()}`}
style={{ display: visible[yearIndex] ? '' : 'none' }}
>
<Table.Cell data-cy={`Cell-${cypress}-${yearIndex}`} colSpan={100}>
<ProgrammeProgressChart
data={sortedKeys?.map(programme => programmeStats[programme][yearIndex])}
longLabels={programmeNames}
labels={sortedKeys}
language={language}
names={progressTitles ? progressTitles[yearIndex] : titles?.slice(2)}
{data?.map(
(yearArray, yearIndex) =>
yearArray?.slice(1).some(value => value !== 0) && (
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
{yearIndex < lenOfYearArray ? (
<BasicRow
icon={visible[yearIndex] ? 'angle down' : 'angle right'}
yearArray={yearArray}
cypress={visible[yearIndex] ? `Hide-${cypress}` : `Show-${cypress}`}
yearIndex={yearIndex}
toggleVisibility={() => toggleVisibility(yearIndex)}
/>
</Table.Cell>
</Table.Row>
) : (
<></>
)}
</React.Fragment>
))}
) : (
<Table.Row>
{yearArray?.map(value => (
<Table.Cell key={`last-row-${Math.random()}`}>{value}</Table.Cell>
))}
</Table.Row>
)}
{yearIndex < lenOfYearArray ? (
<Table.Row
key={`${cypress}-row-${Math.random()}`}
style={{ display: visible[yearIndex] ? '' : 'none' }}
>
<Table.Cell data-cy={`Cell-${cypress}-${yearIndex}`} colSpan={100}>
<ProgrammeProgressChart
data={sortedKeys?.map(programme => programmeStats[programme][yearIndex])}
longLabels={programmeNames}
labels={sortedKeys}
language={language}
names={progressTitles ? progressTitles[yearIndex] : titles?.slice(2)}
needsExtra={needsExtra === 'EXTRA HEIGHT'}
/>
</Table.Cell>
</Table.Row>
) : (
<></>
)}
</React.Fragment>
)
)}
</Table.Body>
</Table>
</div>
@@ -1,7 +1,7 @@
/* eslint-disable react/no-array-index-key */
import PopulationLink from 'components/StudyProgramme/StudytrackOverview/PopulationLink'
import React, { useState } from 'react'
import { Table, Button, Icon, Label } from 'semantic-ui-react'
import { Table, Button, Icon, Label, Popup } from 'semantic-ui-react'
import Toggle from '../../StudyProgramme/Toggle'
const getStyle = idx => {
@@ -10,10 +10,29 @@ const getStyle = idx => {
if (idx === 18) return { borderLeftWidth: 'thick' }
return {}
}
const backgroundColors = { KH: '#ffffff', MH: '#f2f6f7', T: '#e7edee' }
const backgroundColorsDarks = { KH: '#f9f9f9', MH: '#eeeeee', T: '#dddddd' }
const getTitlePopup = idx => {
if (idx === 0) return <p>All</p>
if ([1, 2].includes(idx)) return <p>Started studying</p>
if ([3, 4].includes(idx)) return <p>Currently enrolled</p>
if ([5, 6].includes(idx)) return <p>Absent</p>
if ([7, 8].includes(idx)) return <p>Inactive</p>
if ([9, 10].includes(idx)) return <p>Graduated</p>
if ([11, 12].includes(idx)) return <p>Men</p>
if ([13, 14].includes(idx)) return <p>Women</p>
if ([15, 16].includes(idx)) return <p>Other/Unknown</p>
if ([17, 18].includes(idx)) return <p>Finnish</p>
return <p>Other</p>
}
const FacultyStudentDataTable = ({
tableStats,
programmeStats,
programmeNames,
tableLinePlaces,
titles,
years,
sortedKeys,
@@ -29,6 +48,52 @@ const FacultyStudentDataTable = ({
setVisible(arrayToModify)
}
const getRowStyle = (idx, dark = false) => {
if (tableLinePlaces.length >= 3 && tableLinePlaces[2][0] <= idx)
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[2][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[2][1]] }
if (tableLinePlaces.length >= 3 && tableLinePlaces[2][0] > idx && tableLinePlaces[1][0] <= idx)
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[1][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[1][1]] }
if (tableLinePlaces.length >= 2 && tableLinePlaces[1][0] > idx && tableLinePlaces[0][0] <= idx)
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[0][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[0][1]] }
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[0][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[0][1]] }
}
const calendarYears = years.reduce((all, year) => {
if (year === 'Total') return all
return all.concat(Number(year.slice(0, 4)))
}, [])
const getRows = (idx, programme, year) => {
return programmeStats[programme][year].map((value, valIdx) => {
if (!showPercentages && typeof value === 'string' && (value.includes('%') || value.includes('NA'))) return null
return (
<Popup
content={getTitlePopup(valIdx)}
trigger={
<Table.Cell
key={`${year}-${programme}-color-${valIdx}`}
style={
getStyle(valIdx + 1)?.backgroundColor
? { borderLeftWidth: getStyle(valIdx + 1).borderLeftWidth, ...getRowStyle(idx, true) }
: { ...getStyle(valIdx + 1), ...getRowStyle(idx) }
}
>
{value}
</Table.Cell>
}
/>
)
})
}
return (
<div className="datatable">
<Toggle
@@ -40,7 +105,7 @@ const FacultyStudentDataTable = ({
/>
<Table data-cy={cypress} celled structured>
<Table.Header>
<Table.Row>
<Table.Row key="FirstHeader">
<Table.HeaderCell colSpan={!showPercentages ? 3 : 4} />
<Table.HeaderCell colSpan={!showPercentages ? 4 : 8} style={{ borderLeftWidth: 'thick' }}>
Status
@@ -52,9 +117,7 @@ const FacultyStudentDataTable = ({
Country
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Header>
<Table.Row>
<Table.Row key="secondHeader">
{titles.map((title, index) => (
<Table.HeaderCell
key={title}
@@ -80,6 +143,7 @@ const FacultyStudentDataTable = ({
return (
<Table.Cell key={`${year}-faculty-cell}`}>
<Button
key={`${year}-studentsTableButton}`}
as="div"
onClick={() => toggleVisibility(yearIndex)}
labelPosition="right"
@@ -108,36 +172,39 @@ const FacultyStudentDataTable = ({
})}
</Table.Row>
{yearsVisible[yearIndex] &&
sortedKeys.map(programme => {
return (
sortedKeys.map((programme, idx) => {
return programmeStats[programme][year].length === 0 ? null : (
<Table.Row className="regular-row" key={`${year}-regular-row-${programme}`}>
<Table.Cell textAlign="left" style={{ paddingLeft: '50px' }} key={`${year}-${programme}`}>
<b>
{programme} - {programmeNames[programme].code}
<br />
{programmeNames[programme][language]
? programmeNames[programme][language]
: programmeNames[programme].fi}
</b>
<Table.Cell
textAlign="left"
key={`${year}-${programme}`}
style={{ paddingLeft: '50px', ...getRowStyle(idx) }}
>
<Popup
content={
programmeNames[programme][language] ? (
<p>
{programmeNames[programme].code} - {programmeNames[programme][language]}
</p>
) : (
<p>
{programmeNames[programme].code} - {programmeNames[programme].fi}
</p>
)
}
trigger={<b>{programme}</b>}
/>
{(requiredRights.rights?.includes(programmeNames[programme].code) ||
requiredRights.isAdmin ||
requiredRights.IAMrights?.includes(programmeNames[programme].code)) && (
<PopulationLink studyprogramme={programmeNames[programme].code} year={year} />
<PopulationLink
studyprogramme={programmeNames[programme].code}
year={year}
years={calendarYears}
/>
)}
</Table.Cell>
{programmeStats[programme][year].map((value, valIdx) => {
if (
!showPercentages &&
typeof value === 'string' &&
(value.includes('%') || value.includes('NA'))
)
return null
return (
<Table.Cell key={`${year}-${programme}-color-${valIdx}`} style={getStyle(valIdx + 1)}>
{value}
</Table.Cell>
)
})}
{getRows(idx, programme, year)}
</Table.Row>
)
})}
@@ -8,8 +8,19 @@ const getColors = len => {
if (len < 8) return ['#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
return ['#e66067', '#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
}
const ProgrammeProgressChart = ({ data, labels, longLabels, names, language }) => {
// Point width is 24 px different multipliers adjusts the height.
const getFlexHeight = (len, needsExtra) => {
if (len > 7 && needsExtra) return `${len * 24 * 1.5}px`
if (len > 5 && !needsExtra) return `${len * 24 * 1.5}px`
if (needsExtra && len <= 2) return `${len * 24 * 6}px`
if (needsExtra && len <= 4) return `${len * 24 * 3}px`
if (needsExtra) return `${len * 24 * 2}px`
if (len < 2) return `${len * 24 * 5}px`
if (len < 3) return `${len * 24 * 4}px`
if (len <= 4) return `${len * 24 * 3}px`
return `${len * 24}px`
}
const ProgrammeProgressChart = ({ data, labels, longLabels, names, language, needsExtra }) => {
if (!data || data.length === 0)
return (
<>
@@ -30,6 +41,7 @@ const ProgrammeProgressChart = ({ data, labels, longLabels, names, language }) =
chart: {
type: 'bar',
marginTop: 60,
height: getFlexHeight(labels.length, needsExtra),
},
credits: {
text: 'oodikone | TOSKA',
@@ -1,5 +1,5 @@
import React from 'react'
import { Divider, Loader } from 'semantic-ui-react'
import { Divider, Loader, Message } from 'semantic-ui-react'
import { useGetFacultyProgressStatsQuery, useGetFacultyStudentStatsQuery } from 'redux/facultyStats'
import FacultyProgressTable from './FacultyProgressTable'
import FacultyBarChart from './FacultyBarChart'
@@ -20,7 +20,9 @@ const getDivider = (title, toolTipText, content, cypress) => (
{content === 'no-infobox' ? null : <InfoBox content={content} cypress={cypress} />}
</>
)
const getKey = (programmeKeys, idx) => {
return programmeKeys[idx][1].startsWith('T') ? 'T' : programmeKeys[idx][1].slice(0, 2)
}
const FacultyProgrammeOverview = ({
faculty,
language,
@@ -57,6 +59,22 @@ const FacultyProgrammeOverview = ({
(studentStats.isSuccess && !studentStats.data)
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
const getTableLinePlaces = programmeKeys => {
if (programmeKeys.length === 0) return []
const key = getKey(programmeKeys, 0)
const plotLinePlaces = [[0, key]]
for (let i = 0; i < programmeKeys.length - 1; i++) {
if (
(programmeKeys[i][1].startsWith('KH') && programmeKeys[i + 1][1].startsWith('MH')) ||
(programmeKeys[i][1].startsWith('MH') && programmeKeys[i + 1][1].startsWith('T'))
) {
const key = getKey(programmeKeys, i + 1)
plotLinePlaces.push([i + 1, key])
}
}
return plotLinePlaces
}
return (
<div className="faculty-overview">
<div className="toggle-container">
@@ -99,9 +117,19 @@ const FacultyProgrammeOverview = ({
sortedKeys={sortProgrammeKeys(
Object.keys(studentStats?.data.programmeStats).map(obj => [
obj,
studentStats?.data?.programmeNames[obj].code,
])
studentStats?.data?.programmeNames[obj]?.code,
]),
faculty.code
).map(listObj => listObj[0])}
tableLinePlaces={getTableLinePlaces(
sortProgrammeKeys(
Object.keys(studentStats?.data?.programmeStats).map(obj => [
obj,
studentStats?.data?.programmeNames[obj].code,
]),
faculty.code
)
)}
language={language}
cypress="FacultyStudentStatsTable"
requiredRights={requiredRights}
@@ -139,7 +167,8 @@ const FacultyProgrammeOverview = ({
Object.keys(progressStats?.data.bachelorsProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
])
]),
faculty.code
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
@@ -150,6 +179,11 @@ const FacultyProgrammeOverview = ({
</div>
</div>
{getDivider('Bachelor + Master', 'ProgressOfBachelorMaster', 'no-infobox')}
<Message data-cy="FacultyProgrammesShownInfo">
The starting year is the studyright start in the bachelor programme. The credits are comouted by the
start date of the bachelor programme and at the moment, they do not include any transferred credits.
Thus, in these statistics some students have less credits than in reality.
</Message>
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
@@ -170,13 +204,15 @@ const FacultyProgrammeOverview = ({
Object.keys(progressStats?.data.bcMsProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
])
]),
faculty.code
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorMasterProgressTable"
progressTitles={progressStats?.data.yearlyBcMsTitles}
needsExtra={faculty.code === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
/>
</div>
</div>
@@ -203,19 +239,21 @@ const FacultyProgrammeOverview = ({
Object.keys(progressStats?.data.mastersProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
])
]),
faculty.code
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyMastersProgressTable"
progressTitles={progressStats?.data.yearlyMasterTitles}
needsExtra={faculty.code === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
/>
</div>
</div>
</>
)}
{getDivider('Doctor and licentiate', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
{getDivider('Doctor', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
@@ -236,7 +274,8 @@ const FacultyProgrammeOverview = ({
Object.keys(progressStats?.data.doctoralProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
])
]),
faculty.code
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
@@ -1,6 +1,5 @@
/* eslint-disable react/no-this-in-sfc */
import React from 'react'
import Highcharts from 'highcharts'
import ReactHighcharts from 'react-highcharts'
const colors = ['#7cb5ec', '#90ed7d', '#434348', '#f7a35c', '#FFF000', '#2b908f', '#f45b5b', '#91e8e1']
@@ -18,7 +17,7 @@ const CollapsedStackedBar = ({
const transpose = matrix => {
return matrix.reduce((prev, next) => next.map((_item, i) => (prev[i] || []).concat(next[i])), [])
}
if (names[0] === 'Started studying') names[0] += ' (new in faculty)'
const needsExtra = extraHeight === 'EXTRA HEIGHT'
const dataTranspose = transpose(data)
.map((obj, idx) => ({ name: names[idx], data: obj, color: colors[idx] }))
@@ -45,9 +44,21 @@ const CollapsedStackedBar = ({
const chartPlotLinePlaces = plotLinePlaces
? plotLinePlaces.map(val => ({
color: '#90A959',
width: 2,
value: val + 0.5,
dashStyle: 'shortDash',
width: 1,
value: val[0] - 0.5,
dashStyle: 'solid',
label: {
text: val[1],
style: {
color: 'black',
fontWeight: 'bold',
fontSize: 14,
position: 'absolute',
},
align: 'right',
x: 0,
y: 5,
},
}))
: []
@@ -102,7 +113,7 @@ const CollapsedStackedBar = ({
stackLabels: {
enabled: true,
style: {
color: (Highcharts.defaultOptions.title.style && Highcharts.defaultOptions.title.style.color) || 'gray',
color: 'gray',
},
fontSize: '24px',
},
@@ -115,7 +126,7 @@ const CollapsedStackedBar = ({
verticalAlign: 'top',
y: -10,
floating: true,
backgroundColor: Highcharts.defaultOptions.legend.backgroundColor || 'white',
backgroundColor: 'white',
borderColor: '#CCC',
borderWidth: 1,
shadow: false,
@@ -29,14 +29,32 @@ const InteractiveDataTable = ({
const keys = {}
if (!(columnIndex === 0)) {
const numbersOfYears = dataStats.length
const data = Object.entries(dataProgrammeStats)
const data = sortedKeys?.map(programme => [programme, dataProgrammeStats[programme]])
const groupIndices = plotLinePlaces.length > 0 ? plotLinePlaces.map(val => val[0]) : []
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])
let yearlySortedKeys = []
if (groupIndices.length > 0) {
for (let i = 0; i < groupIndices.length; i++) {
const ending = i === groupIndices.length - 1 ? data.length : groupIndices[i + 1]
yearlySortedKeys = [
...yearlySortedKeys,
...data
.slice(groupIndices[i], ending)
.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]),
]
}
} else {
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
}
}
@@ -24,6 +24,9 @@ const BarChart = ({ data, handleClick, facultyGraph = true, year = null, label,
}
const getHeight = () => {
if (data.length === 1) {
return 250
}
const t = data.length > 8 ? 80 : 110
return data.length * t + 100
}
@@ -113,7 +113,7 @@ const GraduationTimes = ({
label,
levelProgrammeData,
programmeNames,
showBreakdown,
showMedian,
classSizes,
groupBy,
goalExceptions,
@@ -136,7 +136,7 @@ const GraduationTimes = ({
return (
<div className={`graduation-times-${level}`} data-cy={`Section-${level}`}>
<Divider horizontal>{title}</Divider>
{showBreakdown ? (
{!showMedian ? (
<BreakdownDislay
handleClick={handleClick}
data={data}
@@ -10,7 +10,7 @@ import '../faculty.css'
const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) => {
const toolTips = InfotoolTips.Faculty
const [showBreakdown, setShowBreakdown] = useState(false)
const [showMedian, setShowMedian] = useState(false)
const [groupByStartYear, setGroupByStartYear] = useState(false)
const studyProgrammeFilter = studyProgrammes ? 'ALL_PROGRAMMES' : 'NEW_STUDY_PROGRAMMES'
const graduationStats = useGetFacultyGraduationTimesQuery({ id: faculty?.code, studyProgrammeFilter })
@@ -23,7 +23,7 @@ const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) =>
const programmeData = graduationStats?.data?.[groupBy].programmes.medians
const programmeNames = graduationStats?.data?.programmeNames
const classSizes = graduationStats?.data?.classSizes
const commonProps = { label, programmeNames, showBreakdown, classSizes, goalExceptions }
const commonProps = { label, programmeNames, showMedian, classSizes, goalExceptions }
const isFetchingOrLoading = graduationStats.isLoading || graduationStats.isFetching
@@ -63,6 +63,13 @@ const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) =>
<>
{getDivider('Average graduation times', 'AverageGraduationTimes')}
<div className="toggle-container">
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Breakdown"
secondLabel="Median times"
value={showMedian}
setValue={setShowMedian}
/>
<Toggle
cypress="GroupByToggle"
firstLabel="Group by: Graduation year"
@@ -70,13 +77,6 @@ const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) =>
value={groupByStartYear}
setValue={setGroupByStartYear}
/>
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Show: Median times"
secondLabel="Breakdown"
value={showBreakdown}
setValue={setShowBreakdown}
/>
</div>
<div>
<GraduationTimes
@@ -32,13 +32,10 @@ const sortProgrammeKeys = (programmeKeys, faculty) => {
return programmeKeys.sort((a, b) => {
if (a[1].includes(faculty) && !b[1].includes(faculty)) return -1
if (!a[1].includes(faculty) && b[1].includes(faculty)) return 1
if (a[1].startsWith('T') && !b[1].includes(faculty)) return -1
if (!a[1].includes(faculty) && b[1].startsWith('T')) return 1
if (a[1].startsWith('LIS') && !b[1].includes(faculty)) return -1
if (!a[1].includes(faculty) && b[1].startsWith('LIS')) return 1
if (testKey(a[1]) - testKey(b[1]) === 0 && testKey(a[0]) - testKey(b[0]) === 0) {
return a[1].localeCompare(b[1])
}
if (a[1].startsWith('T') && !b[1].includes(faculty) && !b[1].includes('T')) return -1
if (!a[1].includes(faculty) && b[1].startsWith('T') && !a[1].includes('T')) return 1
if (a[1].startsWith('LIS') && !b[1].includes(faculty) && !b[1].includes('LIS')) return -1
if (!a[1].includes(faculty) && b[1].startsWith('LIS') && !a[1].includes('LIS')) return 1
if (testKey(a[1]) - testKey(b[1]) === 0) {
return a[0].localeCompare(b[0])
}
@@ -8,6 +8,7 @@ import createFilter from './createFilter'
const STATUS_OPTIONS = [
{ key: 'enrl-status-present', text: 'Present', value: 1 },
{ key: 'enrl-status-absent', text: 'Absent', value: 2 },
{ key: 'enrl-status-inactive', text: 'Passive', value: 3 },
]
const EnrollmentStatusFilterCard = ({ options, onOptionsChange, allSemesters, language, semesterCodes }) => {
@@ -179,7 +179,7 @@ const StudentNumberFilterCard = ({ options, onOptionsChange, withoutSelf }) => {
export default createFilter({
key: 'studentNumber',
title: 'Student number *',
title: 'Student Number',
defaultOptions: {
allowlist: [],
@@ -49,7 +49,7 @@ const StudyrightStatusFilterCard = ({ options, onOptionsChange }) => {
export default createFilter({
key: 'studyrightStatusFilter',
title: 'Studyright status',
title: 'Studyright Status',
info: filterInfo.studyrightStatus,
@@ -6,6 +6,7 @@ import { Link } from 'react-router-dom'
import CourseFilterToggle from '../CourseFilterToggle'
import { UsePopulationCourseContext } from '../PopulationCourseContext'
import { getTextIn } from '../../../common'
import './passFailEnrollments.css'
const createModuleAggregateRow = ({ definition, children }) => ({
label_code: definition.module.code,
@@ -93,7 +94,7 @@ const PassFailEnrollments = ({ flat, onlyIamRights }) => {
children: [
{
key: 'total',
title: 'Total students',
title: 'Total\nstudents',
helpText: 'Total count of students, including enrolled students with no grade.',
cellStyle: { textAlign: 'right' },
filterType: 'range',
@@ -115,7 +116,7 @@ const PassFailEnrollments = ({ flat, onlyIamRights }) => {
},
{
key: 'totalEnrolledNoGrade',
title: 'Enrolled no grade',
title: 'Enrolled, \nno grade',
helpText: 'Students with a valid enrollment and no passing or failing grade.',
cellStyle: { textAlign: 'right' },
filterType: 'range',
@@ -159,14 +160,14 @@ const PassFailEnrollments = ({ flat, onlyIamRights }) => {
},
{
key: 'passed-after-retry',
title: 'Passed after retry',
title: 'Passed\nafter retry',
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: row => row.stats?.retryPassed ?? 0,
},
{
key: 'failed-many',
title: 'Failed multiple times',
title: 'Failed\nmultiple times',
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: row => row.stats?.failedMany ?? 0,
@@ -234,6 +235,7 @@ const PassFailEnrollments = ({ flat, onlyIamRights }) => {
return (
<>
<SortableTable
tableId="pass-and-fail-statistics-of-courses"
title="Pass and fail statistics of courses with course enrollment details"
data={data}
columns={columns}
@@ -0,0 +1,19 @@
table#pass-and-fail-statistics-of-courses tbody tr td:nth-child(n + 1) {
z-index: -1;
}
table#pass-and-fail-statistics-of-courses tbody tr:nth-child(2n) td:nth-child(2) {
position: sticky;
left: 0;
background: rgb(242 242 242) !important;
overflow: hidden;
z-index: 0;
}
table#pass-and-fail-statistics-of-courses tbody tr:nth-child(2n + 1) td:nth-child(2) {
position: sticky;
left: 0;
background: white !important;
overflow: hidden;
z-index: 0;
}
@@ -1,9 +1,14 @@
import moment from 'moment/moment'
import React from 'react'
import { Table } from 'semantic-ui-react'
import studyPlanFilter from 'components/FilterView/filters/hops'
import CollapsibleCreditRow from './CollapsibleCreditRow'
import useFilters from '../../FilterView/useFilters'
const CreditsGainedTable = ({ filteredStudents, type, year, creditDateFilterOptions }) => {
const { useFilterSelector } = useFilters()
const studyPlanFilterIsActive = useFilterSelector(studyPlanFilter.selectors.isActive)
if (!filteredStudents || !filteredStudents.length || !type) return null
let creditList = []
const studentCount = (min, max = Infinity) =>
@@ -79,6 +84,18 @@ const CreditsGainedTable = ({ filteredStudents, type, year, creditDateFilterOpti
]
const monthString = months === 1 ? 'Month' : 'Months'
const CreditsHeader = () => {
if (studyPlanFilterIsActive) {
return <p>All credits in studyplan</p>
}
return (
<p>
Gredits Gained Between <br /> {title} <br /> ({months} {monthString})
</p>
)
}
return (
<div className="credits-gained-table" data-cy={`credits-gained-table-${type}`}>
<h3>{type}</h3>
@@ -87,7 +104,7 @@ const CreditsGainedTable = ({ filteredStudents, type, year, creditDateFilterOpti
<Table.Row key={`credits-gained-table-${type}`}>
<Table.HeaderCell collapsing />
<Table.HeaderCell key={`${title}-${type}`}>
Gredits Gained Between <br /> {title} <br /> ({months} {monthString})
<CreditsHeader />
</Table.HeaderCell>
<Table.HeaderCell key={`credits-number-of-students-${type}`}>
Number of Students
@@ -189,17 +189,19 @@ const PopulationSearchHistory = props => {
</div>
</div>
<div style={{ marginLeft: '100px' }}>
<Form.Group>
<Form.Field>
<Form.Radio
data-cy="advanced-toggle"
toggle
checked={showAdvancedSettings}
onClick={() => setShowAdvancedSettings(!showAdvancedSettings)}
label="Advanced settings"
/>
</Form.Field>
</Form.Group>
{populations.query.year !== 'All' && (
<Form.Group>
<Form.Field>
<Form.Radio
data-cy="advanced-toggle"
toggle
checked={showAdvancedSettings}
onClick={() => setShowAdvancedSettings(!showAdvancedSettings)}
label="Advanced settings"
/>
</Form.Field>
</Form.Group>
)}
<div>{renderAdvancedSettingsSelector()}</div>
<div>{showAdvancedSettings ? <InfoBox content={Advanced} /> : <FilterActiveNote />}</div>
</div>
@@ -1,4 +1,4 @@
import { sortBy } from 'lodash'
import { flatten, sortBy } from 'lodash'
import React from 'react'
import { useSelector } from 'react-redux'
import { Dropdown } from 'semantic-ui-react'
@@ -73,6 +73,18 @@ export default ({ students }) => {
return studyright?.startdate ? studyright.startdate : ''
}
const getStartedInProgramme = studyrights => {
const code = queryStudyrights[0]
if (!code || !studyrights) return ''
const element = flatten(
studyrights.reduce((acc, curr) => {
acc.push(curr.studyright_elements)
return acc
}, [])
).filter(e => e.code === code)
return new Date(Math.max(new Date(element[0]?.startdate), new Date(getStartOfStudyright(studyrights))))
}
const getAdmissionType = studyrights => {
const studyright = findCorrectStudyright(studyrights)
return studyright && studyright.admission_type ? studyright.admission_type : 'Ei valintatapaa'
@@ -138,6 +150,7 @@ export default ({ students }) => {
tags: tags(s.tags),
'start year at university': reformatDate(s.started, 'YYYY'),
'start of studyright': reformatDate(getStartOfStudyright(s.studyrights), 'YYYY-MM-DD'),
'started in programme': reformatDate(getStartedInProgramme(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, codes),
@@ -30,6 +30,10 @@ import {
hopsFilter,
} from '../FilterView/filters'
const getYearText = year => {
if (year === 'All') return ''
return `${year} - ${Number(year) + 1}`
}
const PopulationStatistics = () => {
const location = useLocation()
const { language } = useLanguage()
@@ -96,9 +100,7 @@ const PopulationStatistics = () => {
}, [samples, programmeCode])
const title =
location.search === ''
? 'Class statistics'
: `${getTextIn(programmeName, language)} ${query?.year} - ${Number(query?.year) + 1}`
location.search === '' ? 'Class statistics' : `${getTextIn(programmeName, language)} ${getYearText(query?.year)}`
return (
<FilterView
@@ -119,6 +121,9 @@ const PopulationStatistics = () => {
<div className="segmentContainer" style={{ flexGrow: 1 }}>
<Header className="segmentTitle" size="large" align="center">
{title}
{location.search !== '' && query?.studyRights?.studyTrack && query?.studyRights?.studyTrack !== '' && (
<Header.Subheader> studytrack {query?.studyRights?.studyTrack}</Header.Subheader>
)}
{location.search !== '' && (
<Header.Subheader>
studytime {query?.months} months, class size {students.length} students
@@ -163,13 +163,28 @@ const GeneralTab = ({
}
const studentToStudyrightStartMap = selectedStudents.reduce((res, sn) => {
const currentStudyright = students[sn].studyrights.find(studyright =>
studyright.studyright_elements.some(e => e.code === programmeCode)
)
if (currentStudyright?.studyrightid && currentStudyright.studyrightid.slice(-2) === '-2') {
const bachelorId = currentStudyright.studyrightid.replace(/-2$/, '-1')
const bacherlorStudyright = students[sn].studyrights.find(studyright => studyright.studyrightid === bachelorId)
res[sn] = bacherlorStudyright?.startdate || null
} else {
res[sn] = currentStudyright?.startdate || null
}
return res
}, {})
const studentToProgrammeStartMap = selectedStudents.reduce((res, sn) => {
const targetStudyright = _.flatten(
students[sn].studyrights.reduce((acc, curr) => {
acc.push(curr.studyright_elements)
return acc
}, [])
).filter(e => e.code === programmeCode)
res[sn] = targetStudyright[0] ? targetStudyright[0].startdate : null
// clean up odd bachelor start dates, (givendate)
res[sn] = new Date(Math.max(new Date(targetStudyright[0]?.startdate), new Date(studentToStudyrightStartMap[sn])))
return res
}, {})
@@ -236,7 +251,7 @@ const GeneralTab = ({
const shouldShowAdmissionType = parseInt(query?.year, 10) >= 2020 || parseInt(group?.tags?.year, 10) >= 2020
let creditColumnTitle = 'Since Start of Studyright'
let creditColumnTitle = 'Since start\nin programme'
if (creditDateFilterOptions) {
const { startDate, endDate } = creditDateFilterOptions
@@ -277,7 +292,7 @@ const GeneralTab = ({
getRowVal: s => {
const credits = getStudentTotalCredits({
...s,
courses: s.courses.filter(c => new Date(c.date) >= new Date(studentToStudyrightStartMap[s.studentNumber])),
courses: s.courses.filter(c => new Date(c.date) >= studentToProgrammeStartMap[s.studentNumber]),
})
return credits
},
@@ -374,42 +389,23 @@ const GeneralTab = ({
return grade
},
},
transferredFrom: {
key: 'transferredFrom',
title: 'Transferred From',
getRowVal: s => (s.transferredStudyright ? transferFrom(s) : ''),
},
studyTrack: containsStudyTracks && {
key: 'studyTrack',
title: 'Study Track',
getRowVal: s => studytrack(s.studyrights).map(st => st.name)[0],
},
priority: {
key: 'priority',
title: 'Priority',
getRowVal: s => priorityText(s.studyrights),
},
extent: {
key: 'extent',
title: 'Extent',
getRowVal: s => extentCodes(s.studyrights),
},
semesterEnrollments: {
key: 'semesterEnrollments',
title: 'Semesters',
helpText: 'Number of present semesters',
getRowVal: s => semesterEnrollments(s.semesterenrollments),
},
tags: {
key: 'tags',
title: 'Tags',
getRowVal: s => (!s.obfuscated ? tags(s.tags) : ''),
studyrightStart: {
key: 'studyrightStart',
title: 'Start of\nstudyright',
filterType: 'date',
getRowVal: s => studentToStudyrightStartMap[s.studentNumber], // new Date(studentToStudyrightStartMap[s.studentNumber]),
formatValue: value => reformatDate(new Date(value), 'YYYY-MM-DD'),
},
studyStartDate: {
key: 'studyStartDate',
title: 'Start of studyright',
title: 'Started in\nprogramme',
filterType: 'date',
getRowVal: s => new Date(studentToStudyrightStartMap[s.studentNumber]),
getRowVal: s => studentToProgrammeStartMap[s.studentNumber],
formatValue: value => reformatDate(new Date(value), 'YYYY-MM-DD'),
},
endDate: {
@@ -423,6 +419,21 @@ const GeneralTab = ({
? reformatDate(studentToStudyrightEndMap[s.studentNumber], 'YYYY-MM-DD')
: '',
},
semesterEnrollments: {
key: 'semesterEnrollments',
title: 'Semesters\npresent',
getRowVal: s => semesterEnrollments(s.semesterenrollments),
},
tags: {
key: 'tags',
title: 'Tags',
getRowVal: s => (!s.obfuscated ? tags(s.tags) : ''),
},
transferredFrom: {
key: 'transferredFrom',
title: 'Transferred\nfrom',
getRowVal: s => (s.transferredStudyright ? transferFrom(s) : ''),
},
admissionType: shouldShowAdmissionType && {
key: 'admissionType',
title: 'Admission type',
@@ -471,6 +482,16 @@ const GeneralTab = ({
title: cleanedQueryStudyrights.some(code => code.startsWith('MH')) ? 'Bachelor' : 'Master',
getRowVal: s => (s.option ? getTextIn(s.option.name, language) : ''),
},
priority: {
key: 'priority',
title: 'Priority',
getRowVal: s => priorityText(s.studyrights),
},
extent: {
key: 'extent',
title: 'Extent',
getRowVal: s => extentCodes(s.studyrights),
},
email: {
mergeHeader: true,
merge: true,
@@ -583,7 +604,8 @@ const GeneralTabContainer = ({ studyGuidanceGroup, variant, ...props }) => {
const getStudyGuidanceGroupColumns = () => {
const cols = ['programme', 'startYear']
if (studyGuidanceGroup?.tags?.year) cols.push('credits.startYear')
if (studyGuidanceGroup?.tags?.studyProgramme) cols.push('studyStartDate', 'studyStartDateActual', 'endDate')
if (studyGuidanceGroup?.tags?.studyProgramme)
cols.push('studyrightStart', 'studyStartDate', 'studyStartDateActual', 'endDate')
if (studyGuidanceGroup?.tags?.studyProgramme && studyGuidanceGroup?.tags?.year) {
cols.push('admissionType')
}
@@ -600,6 +622,7 @@ const GeneralTabContainer = ({ studyGuidanceGroup, variant, ...props }) => {
'priority',
'extent',
'semesterEnrollments',
'studyrightStart',
'studyStartDate',
'studyStartDateActual',
'endDate',
@@ -611,7 +634,7 @@ const GeneralTabContainer = ({ studyGuidanceGroup, variant, ...props }) => {
const baseColumns = ['credits', 'credits.all', 'studentnumber-parent', 'tags', 'updatedAt', 'option']
const nameColumnsToAdd = namesVisible ? ['email', 'lastname', 'firstname'] : []
const adminColumnsToFilter = isAdmin ? [] : ['priority', 'extent', 'studyStartDate', 'updatedAt']
const adminColumnsToFilter = isAdmin ? [] : ['priority', 'extent', 'updatedAt']
const columnKeysToInclude = _.chain(baseColumns)
.union(columnsByVariant[variant])
@@ -89,57 +89,7 @@ const CoursesTable = ({ students }) => {
)
const columns = useMemo(() => {
const nameColumns = [
{
key: 'studentnumber-parent',
mergeHeader: true,
// merge: true,
title: 'Student Number',
children: [
{
key: 'studentnumber',
title: 'Student Number',
cellProps: { title: 'student number' },
getRowVal: s => (s.total ? '*' : s.studentNumber),
getRowContent: s => (s.total ? 'Summary:' : s.studentNumber),
child: true,
},
{
key: 'icon',
title: 'Icon',
export: false,
getRowVal: s =>
!s.total && (
<Item
as={Link}
to={`/students/${s.studentNumber}`}
onClick={() => {
sendAnalytics('Student details button clicked', 'Mandatory courses table')
}}
>
<Icon name="user outline" />
</Item>
),
cellProps: { className: 'iconCell' },
child: true,
},
],
},
{
key: 'totalpassed',
title: 'Total Passed',
filterType: 'range',
vertical: true,
getRowVal: s =>
s.total
? Object.values(s)
.filter(isNumber)
.reduce((acc, e) => acc + e, 0)
: totalMandatoryPassed(s.studentNumber),
cellProps: { title: 'total passed' },
child: true,
},
]
const nameColumns = []
if (namesVisible) {
nameColumns.push(
@@ -156,17 +106,51 @@ const CoursesTable = ({ students }) => {
getRowVal: s => (s.total ? null : s.firstnames),
cellProps: { title: 'first names' },
child: true,
},
{
key: 'email',
title: 'Email',
getRowVal: s => (s.total ? null : s.email),
cellProps: { title: 'emails' },
child: true,
}
)
}
nameColumns.push(
{
key: 'studentnumber-parent',
title: 'Student Number',
cellProps: { title: 'student number', className: 'studentNumber' },
getRowVal: s => (s.total ? '*' : s.studentNumber),
getRowContent: s =>
s.total ? (
'Summary:'
) : (
<div>
<span>{s.studentNumber}</span>
<Item
as={Link}
to={`/students/${s.studentNumber}`}
onClick={() => {
sendAnalytics('Student details button clicked', 'Mandatory courses table')
}}
>
<Icon style={{ borderLeft: '1em' }} name="user outline" />
</Item>
</div>
),
child: true,
},
{
key: 'totalpassed',
title: 'Total Passed',
filterType: 'range',
vertical: true,
getRowVal: s =>
s.total
? Object.values(s)
.filter(isNumber)
.reduce((acc, e) => acc + e, 0)
: totalMandatoryPassed(s.studentNumber),
cellProps: { title: 'total passed' },
child: true,
}
)
const mandatoryCourseLabels = []
const labelToMandatoryCourses = mandatoryCourses.reduce((acc, e) => {
@@ -239,12 +223,24 @@ const CoursesTable = ({ students }) => {
textTitle: m.code,
vertical: true,
forceToolsMode: 'dangling',
cellProps: {
title: `${m.code}, ${getTextIn(m.name)}`,
style: {
verticalAlign: 'middle',
textAlign: 'center',
},
cellProps: s => {
let grade = s.courses
? s.courses.find(course => course.course_code === m.code || course.course_code === `AY${m.code}`)
?.grade
: null
grade =
s.courses && !grade && hasPassedMandatory(s.studentNumber, m.code)
? s.courses.find(course => course.course_code === m.label_code)?.grade
: grade
const gradeText = grade ? `\nGrade: ${grade}` : ''
const studentCode = s.studentNumber ? `\nStudent number: ${s.studentNumber}` : ``
return {
title: `${m.code}, ${getTextIn(m.name)}${studentCode} ${gradeText}`,
style: {
verticalAlign: 'middle',
textAlign: 'center',
},
}
},
headerProps: { title: `${m.code}, ${getTextIn(m.name)}` },
getRowVal: s => {
@@ -272,6 +268,14 @@ const CoursesTable = ({ students }) => {
}))
)
columns.push({
key: 'email',
title: 'Email',
getRowVal: s => (s.total ? null : s.email),
cellProps: { title: 'emails' },
child: true,
})
return columns
}, [namesVisible, mandatoryCourses, getTextIn])
@@ -298,6 +302,7 @@ const CoursesTable = ({ students }) => {
<div style={{ maxHeight: '80vh', width: '100%' }}>
{mandatoryCourses.length > 0 && (
<SortableTable
tableId="course-of-population-students"
title={`Courses of population's students`}
getRowKey={s => (s.total ? 'totals' : s.studentNumber)}
tableProps={{
@@ -501,7 +506,7 @@ const PopulationStudentsContainer = ({ ...props }) => {
const contentByVariant = {
population: {
panesToInclude: ['General', 'Courses', 'Tags'],
infotoolTipContent: infotoolTips.PopulationStatistics.Students,
infotoolTipContent: infotoolTips.PopulationStatistics.StudentsClass,
},
coursePopulation: {
panesToInclude: ['General'],
@@ -509,11 +514,11 @@ const PopulationStudentsContainer = ({ ...props }) => {
},
customPopulation: {
panesToInclude: ['General'],
infotoolTipContent: infotoolTips.PopulationStatistics.Students,
infotoolTipContent: infotoolTips.PopulationStatistics.StudentsCustom,
},
studyGuidanceGroupPopulation: {
panesToInclude: ['General'],
infotoolTipContent: infotoolTips.PopulationStatistics.Students,
infotoolTipContent: infotoolTips.PopulationStatistics.StudentsGuidanceGroups,
},
}
@@ -19,3 +19,35 @@
white-space: initial;
padding: 1px;
}
table#course-of-population-students td.studentNumber:not(:first-child) {
padding: 0 !important;
}
table#course-of-population-students .studentNumber div span {
padding-left: 0.1em;
}
table#course-of-population-students .studentNumber div a {
padding-left: 0.7em;
}
table#course-of-population-students tbody tr td:nth-child(n + 1) {
z-index: -1;
}
table#course-of-population-students tbody tr:nth-child(2n) td:first-child {
position: sticky;
left: 0;
background: rgb(242 242 242) !important;
overflow: hidden;
z-index: 0;
}
table#course-of-population-students tbody tr:nth-child(2n + 1) td:first-child {
position: sticky;
left: 0;
background: white !important;
overflow: hidden;
z-index: 0;
}
@@ -24,7 +24,13 @@ const DefaultColumnFilterComponent = ({ column, options, dispatch }) => {
return []
}
const t = _.uniq(values)
.filter(value => search === '' || `${value.toLowerCase()}`.indexOf(search.toLowerCase()) > -1)
.filter(
value =>
search === '' ||
(typeof value === 'string' &&
typeof search === 'string' &&
`${value.toLowerCase()}`.indexOf(search.toLowerCase()) > -1)
)
.sort((a, b) => {
if (typeof a === 'number' && typeof b === 'number') {
return a - b
@@ -446,7 +446,7 @@ const ColumnHeaderContent = React.memo(({ column, colSpan, state, dispatch, rowS
}}
>
<SizeMeasurer onSizeChange={onTitleSizeChange} style={{ display: 'inline-flex', alignItems: 'center' }}>
{column.title}
<p style={{ whiteSpace: 'pre' }}>{column.title}</p>
{column.helpText && !forcedTitleWidth && (
<Icon
name="question circle outline"
@@ -575,7 +575,7 @@ const createHeaders = (columns, columnDepth, dispatch) => {
const [column] = stack.splice(0, 1)
if (column.parent?.mergeHeader) {
continue; // eslint-disable-line
continue // eslint-disable-line
}
const currentDepth = column.depth ?? 0
@@ -692,7 +692,8 @@ const tableStateReducer = (...args) =>
state.expandedGroups.push(group)
}
},
}[type]())
// eslint-disable-next-line prettier/prettier
})[type]()
})
const injectParentPointers = columns => {
@@ -801,6 +802,7 @@ const extractColumnKeys = columns => {
}
const SortableTable = ({
tableId,
columns: pColumns,
title,
data,
@@ -921,7 +923,7 @@ const SortableTable = ({
}
const content = (
<table className={classNames.join(' ')} style={tableStyles}>
<table className={classNames.join(' ')} id={tableId} style={tableStyles}>
<thead style={{ position: 'sticky', top: 0, zIndex: 1 }}>{headers}</thead>
<tbody>
{sortedData.map(item => (
@@ -26,6 +26,8 @@ const CourseParticipationTable = ({ student, language, clearCourseStats, studyri
if (isStudyModuleCredit) {
icon = <Icon name="certificate" color="purple" />
} else if (c.credittypecode === 9) {
icon = <Icon name="file alternative outline" title="Credit transfer" color="green" />
} else if (passed) {
icon = <Icon name="check circle outline" color="green" />
} else {
@@ -91,10 +91,36 @@ const StudentSearch = ({
// so that the loading spinner doesn't go on top of the search box
return <div style={{ margin: 100 }} />
}
// let studentsSorted = students
if (students.length <= 0) {
return <div>No search results or search term is not accurate enough</div>
}
const studentsSorted = students.sort((a, b) => {
let aDateSplit = a.started.split('.')
let bDateSplit = b.started.split('.')
let aDate
let bDate
if (aDateSplit[0] === 'Unavailable') {
aDateSplit = null
} else {
aDate = new Date(aDateSplit[2], aDateSplit[1], aDateSplit[0])
}
if (bDateSplit[0] === 'Unavailable') {
bDateSplit = null
} else {
bDate = new Date(bDateSplit[2], bDateSplit[1], bDateSplit[0])
}
if (aDate && bDate) {
return bDate - aDate
}
if (aDate && !bDate) {
return -1
}
return 0
})
const columns = [
{
key: 'studentnumber',
@@ -224,14 +250,13 @@ const StudentSearch = ({
columns.splice(0, 0, ...nameColumns)
}
return (
<SortableTable
figure={false}
getRowKey={s => s.studentNumber}
tableProps={{ celled: false }}
columns={columns}
data={students.slice(0, 200)}
data={studentsSorted.slice(0, 200)}
chunkifyBy="studentNumber"
/>
)
@@ -17,7 +17,7 @@ import '../studyprogramme.css'
const isNewProgramme = code => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code)
const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYear, setAcademicYear }) => {
const [showBreakdown, setShowBreakdown] = useState(false)
const [showMedian, setShowMedian] = useState(false)
const toolTips = InfotoolTips.Studyprogramme
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
const special = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
@@ -160,14 +160,14 @@ const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYea
<div className="toggle-container">
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Median time"
secondLabel="Breakdown"
value={showBreakdown}
setValue={setShowBreakdown}
firstLabel="Breakdown"
secondLabel="Median time"
value={showMedian}
setValue={setShowMedian}
/>
</div>
<div className={`section-container${doCombo ? '' : '-centered'}`}>
{showBreakdown ? displayBreakdown() : displayMedian()}
{showMedian ? displayMedian() : displayBreakdown()}
</div>
{graduations?.data?.programmesBeforeOrAfterGraphStats && (
<>
@@ -92,7 +92,7 @@ const BarChart = ({ data, title, byStartYear = false }) => {
}
return (
<div className={`graduation-times-graph${title.split(' ')[0]}`}>
<div data-cy={`graduation-times-graph-breakdown${title.split(' ')[0]}`}>
<ReactHighcharts config={config} />
</div>
)
@@ -1,7 +1,11 @@
import React, { useEffect, useState } from 'react'
import { connect, useSelector, useDispatch } from 'react-redux'
import { string, func } from 'prop-types'
import { Button, Label, Table, Icon } from 'semantic-ui-react'
import { Button, Label, Table, Icon, Dropdown, Container, Message, Form } from 'semantic-ui-react'
import {
useAddProgressCriteriaCourseMutation,
useAddProgressCriteriaCreditsMutation,
} from 'redux/programmeProgressCriteria'
import { getTextIn } from '../../../common'
import { GetMandatoryCourseLabels } from '../../../redux/mandatoryCourseLabels'
import {
@@ -14,17 +18,35 @@ import sendEvent from '../../../common/sendEvent'
const sendAnalytics = sendEvent.degreeCourses
const DegreeCourses = ({ studyProgramme, setExclusion, removeExclusion }) => {
const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, removeExclusion }) => {
const { language } = useLanguage()
const dispatch = useDispatch()
const mandatoryCourses = useSelector(({ populationMandatoryCourses }) => populationMandatoryCourses.data)
const [visible, setVisible] = useState({})
const [modules, setModules] = useState([])
const [creditsLimit1, setCreditsLimit1] = useState('')
const [creditsLimit2, setCreditsLimit2] = useState('')
const [creditsLimit3, setCreditsLimit3] = useState('')
const [addProgressCriteriaCourse, { data: courseData }] = useAddProgressCriteriaCourseMutation()
const [addProgressCriteriaCredits, { data: creditsData }] = useAddProgressCriteriaCreditsMutation()
useEffect(() => {
dispatch(getMandatoryCourses(studyProgramme, true))
}, [])
useEffect(() => {
if (courseData) {
setCriteria(courseData)
}
}, [courseData])
useEffect(() => {
if (creditsData) {
setCriteria(creditsData)
}
}, [creditsData])
useEffect(() => {
if (!mandatoryCourses || !mandatoryCourses.length) return
const modules = {}
@@ -51,6 +73,14 @@ const DegreeCourses = ({ studyProgramme, setExclusion, removeExclusion }) => {
}
}, [modules])
const getYear = criterionYear => {
let year = 'yearOne'
if (criterionYear !== 1) {
year = criterionYear === 2 ? 'yearTwo' : 'yearThree'
}
return year
}
const setExclusionButton = course => (
<Button
onClick={() => {
@@ -72,6 +102,63 @@ const DegreeCourses = ({ studyProgramme, setExclusion, removeExclusion }) => {
</Button>
)
const setCriteriaButton = (course, criterionYear) => (
<Button
onClick={() => {
const year = getYear(criterionYear)
const courses = criteria.courses ? [...criteria.courses[year], course.code] : [course.code]
addProgressCriteriaCourse({ programmeCode: studyProgramme, courses, year: criterionYear })
}}
>
{`Set criterion for year ${criterionYear}`}
</Button>
)
const deleteCriteriaButton = (course, criterionYear) => (
<Button
onClick={() => {
const year = getYear(criterionYear)
const filteredCourses = criteria?.courses[year]?.filter(courseCode => courseCode !== course.code)
addProgressCriteriaCourse({ programmeCode: studyProgramme, courses: filteredCourses, year: criterionYear })
}}
>
{`Remove criterion for year ${criterionYear}`}
</Button>
)
const labelDropdown = course => {
return (
<Dropdown text="Modify labels" className="link item">
<Dropdown.Menu>
{course.visible.visibility ? setExclusionButton(course) : deleteButton(course)}
<Dropdown.Divider />
{criteria?.courses?.yearOne?.includes(course.code)
? deleteCriteriaButton(course, 1)
: setCriteriaButton(course, 1)}
<Dropdown.Divider />
{criteria?.courses?.yearTwo?.includes(course.code)
? deleteCriteriaButton(course, 2)
: setCriteriaButton(course, 2)}
<Dropdown.Divider />
{criteria?.courses?.yearThree?.includes(course.code)
? deleteCriteriaButton(course, 3)
: setCriteriaButton(course, 3)}
</Dropdown.Menu>
</Dropdown>
)
}
const setCreditsLimitCriteria = () => {
const credits = {
year1: creditsLimit1 === '' ? criteria.credits.yearOne : creditsLimit1,
year2: creditsLimit2 === '' ? criteria.credits.yearTwo : creditsLimit2,
year3: creditsLimit3 === '' ? criteria.credits.yearThree : creditsLimit3,
}
addProgressCriteriaCredits({ programmeCode: studyProgramme, credits })
setCreditsLimit1('')
setCreditsLimit2('')
setCreditsLimit3('')
}
const excludeAll = code => {
const module = modules.find(({ module }) => module === code)
setExclusion(
@@ -142,56 +229,111 @@ const DegreeCourses = ({ studyProgramme, setExclusion, removeExclusion }) => {
}
return (
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Code</Table.HeaderCell>
<Table.HeaderCell>Label</Table.HeaderCell>
<Table.HeaderCell>Set visibility</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{modules.map(({ module, courses }) => (
<>
<Table.Row key={module}>
<Table.Cell style={{ cursor: 'pointer' }} onClick={() => toggleVisible(module)}>
<Icon name={visible[module] ? 'angle down' : 'angle right'} />
<b>{courses[0] && courses[0].label_name ? getTextIn(courses[0].label_name, language) : module}</b>
</Table.Cell>
<Table.Cell>{module}</Table.Cell>
<Table.Cell>
<Label
content={calculateModuleVisibility(module)}
color={moduleVisibilityColor(calculateModuleVisibility(module))}
<>
{studyProgramme.includes('KH') && (
<>
<Message style={{ fontSize: '16px' }}>
<Message.Header>Change visibility of degree courses and select criteria for academic years</Message.Header>
<p>
Here you can change visibility of degree courses as and set course and credits criteria. Each academic
year have their own course and credits criteria. The progress of the students by these criteria will be
shown in class statistics view. These statistics will be available at the latest at Friday on 10.2..
</p>
</Message>
<Container>
<h5>Credit criteria</h5>
<Form>
<Form.Group widths="equal">
<Form.Input
type="number"
fluid
label={`First year (12 months) last set: ${criteria?.credits?.yearOne}`}
onChange={e => setCreditsLimit1(e.target.value)}
/>
</Table.Cell>
<Table.Cell>
{calculateModuleVisibility(module) === 'hidden' ? showAllButton(module) : hideAllButton(module)}
</Table.Cell>
</Table.Row>
{visible[module] &&
courses
.sort((a, b) => a.code.localeCompare(b.code))
.map(course => (
<Table.Row key={`${module}/${course.code}`}>
<Table.Cell>{getTextIn(course.name, language)}</Table.Cell>
<Table.Cell>{course.code}</Table.Cell>
<Table.Cell>
<Label
content={course.visible.visibility ? 'visible' : 'hidden'}
color={course.visible.visibility ? 'green' : 'red'}
/>
</Table.Cell>
<Table.Cell>
{course.visible.visibility ? setExclusionButton(course) : deleteButton(course)}
</Table.Cell>
</Table.Row>
))}
</>
))}
</Table.Body>
</Table>
<Form.Input
type="number"
fluid
label={`Second year (24 months) last set: ${criteria?.credits?.yearTwo}`}
onChange={e => setCreditsLimit2(e.target.value)}
/>
<Form.Input
type="number"
fluid
label={`Third year (36 months) last set: ${criteria?.credits?.yearThree}`}
onChange={e => setCreditsLimit3(e.target.value)}
/>
</Form.Group>
<Form.Button content="Save credit changes" onClick={setCreditsLimitCriteria} />
</Form>
</Container>
</>
)}
<Table>
<Table.Header>
<Table.Row key="HeaderRow">
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Code</Table.HeaderCell>
<Table.HeaderCell>Visibility Label</Table.HeaderCell>
{studyProgramme.includes('KH') && <Table.HeaderCell>Criterion Labels</Table.HeaderCell>}
<Table.HeaderCell>{studyProgramme.includes('KH') ? 'Set Labels' : 'Set Visibility'}</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{modules.map(({ module, courses }) => (
<React.Fragment key={`fragment-${module}`}>
<Table.Row key={module}>
<Table.Cell style={{ cursor: 'pointer' }} onClick={() => toggleVisible(module)}>
<Icon name={visible[module] ? 'angle down' : 'angle right'} />
<b>{courses[0] && courses[0].label_name ? getTextIn(courses[0].label_name, language) : module}</b>
</Table.Cell>
<Table.Cell>{module}</Table.Cell>
<Table.Cell>
<Label
content={calculateModuleVisibility(module)}
color={moduleVisibilityColor(calculateModuleVisibility(module))}
/>
</Table.Cell>
{studyProgramme.includes('KH') && <Table.Cell />}
<Table.Cell>
{calculateModuleVisibility(module) === 'hidden' ? showAllButton(module) : hideAllButton(module)}
</Table.Cell>
</Table.Row>
{visible[module] &&
courses
.sort((a, b) => a.code.localeCompare(b.code))
.map(course => (
<Table.Row key={`${module}/${course.code}`}>
<Table.Cell>{getTextIn(course.name, language)}</Table.Cell>
<Table.Cell>{course.code}</Table.Cell>
<Table.Cell>
<Label
content={course.visible.visibility ? 'visible' : 'hidden'}
color={course.visible.visibility ? 'green' : 'red'}
/>
</Table.Cell>
{studyProgramme.includes('KH') && (
<Table.Cell>
{criteria?.courses?.yearOne?.includes(course.code) && <Label content="year 1" color="blue" />}
{criteria?.courses?.yearTwo?.includes(course.code) && <Label content="year 2" color="blue" />}
{criteria?.courses?.yearThree?.includes(course.code) && (
<Label content="year 3" color="blue" />
)}
</Table.Cell>
)}
{studyProgramme.includes('KH') ? (
<Table.Cell>{labelDropdown(course)}</Table.Cell>
) : (
<Table.Cell>
{course.visible.visibility ? setExclusionButton(course) : deleteButton(course)}
</Table.Cell>
)}
</Table.Row>
))}
</React.Fragment>
))}
</Table.Body>
</Table>
</>
)
}
@@ -5,7 +5,7 @@ const ReactHighcharts = require('react-highcharts')
const getColors = track => {
if (!track) return ['#e66067', '#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
if (track.includes('KH'))
if (track.includes('KH') || track === 'MH90_001')
return ['#e66067', '#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
return ['#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
}
@@ -3,6 +3,13 @@ import { Icon } from 'semantic-ui-react'
import { Link } from 'react-router-dom'
import moment from 'moment'
const getMonths = year => {
const end = moment()
const lastDayOfMonth = moment(end).endOf('month')
const start = `${year}-08-01`
return Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(start))).asMonths())
}
const PopulationStatisticsLink = ({ studytrack, studyprogramme, year }) => {
const startYear = Number(year.slice(0, 4))
const months = Math.ceil(moment.duration(moment().diff(`${startYear}-08-01`)).asMonths())
@@ -18,8 +25,23 @@ const PopulationStatisticsLink = ({ studytrack, studyprogramme, year }) => {
)
}
const TotalPopulationLink = ({ studytrack, studyprogramme, years }) => {
const yearsString = years.join('&years=')
const months = getMonths(Math.min(...years.map(year => Number(year))))
const href = studytrack
? `/populations?months=${months}&semesters=FALL&semesters=` +
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"studyTrack"%3A"${studytrack}"%7D&year=All&years=${yearsString}`
: `/populations?months=${months}&semesters=FALL&semesters=` +
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=All&years=${yearsString}`
return (
<Link title="Population statistics of all years" to={href}>
<Icon name="level up alternate" />
</Link>
)
}
const PopulationLink = ({ year, ...rest }) => {
if (year === 'Total') return null
if (year === 'Total') return <TotalPopulationLink {...rest} />
return <PopulationStatisticsLink {...rest} year={year} />
}
@@ -11,17 +11,17 @@ const shouldBeHidden = (showPercentages, value) => !showPercentages && typeof va
const getCellClass = value => (value === 'Total' ? 'total-row-cell' : '')
const getFirstCell = ({ yearlyData, year, show, studyprogramme }) => {
const getFirstCell = ({ yearlyData, year, show, studyprogramme, calendarYears }) => {
return (
<Table.Cell key={getKey(year)} className={getCellClass(year)}>
{yearlyData.length > 1 && <Icon name={`${show ? 'angle down' : 'angle right'}`} />}
{year}
<PopulationLink studyprogramme={studyprogramme} year={year} />
<PopulationLink studyprogramme={studyprogramme} year={year} years={calendarYears} />
</Table.Cell>
)
}
const getSingleTrackRow = ({ row, studyprogramme, code, showPercentages }) => {
const getSingleTrackRow = ({ row, studyprogramme, code, showPercentages, calendarYears }) => {
return (
<Table.Row key={getKey(row[0])} className="regular-row">
{row.map((value, index) => (
@@ -29,7 +29,9 @@ const getSingleTrackRow = ({ row, studyprogramme, code, showPercentages }) => {
{shouldBeHidden(showPercentages, value) ? null : (
<Table.Cell textAlign="left" className={getCellClass(row[0])} key={getKey(row[0])}>
{value}
{index === 0 && <PopulationLink studyprogramme={studyprogramme} year={row[0]} studytrack={code} />}
{index === 0 && (
<PopulationLink studyprogramme={studyprogramme} year={row[0]} studytrack={code} years={calendarYears} />
)}
</Table.Cell>
)}
</>
@@ -38,7 +40,17 @@ const getSingleTrackRow = ({ row, studyprogramme, code, showPercentages }) => {
)
}
const getRow = ({ yearlyData, row, show, setShow, studyprogramme, studytracks, showPercentages, years }) => {
const getRow = ({
yearlyData,
row,
show,
setShow,
studyprogramme,
studytracks,
showPercentages,
years,
calendarYears,
}) => {
const year = yearlyData && yearlyData[0] && yearlyData[0][0]
// Get row for the studyprogramme
@@ -47,7 +59,7 @@ const getRow = ({ yearlyData, row, show, setShow, studyprogramme, studytracks, s
<Table.Row key={getKey(row[0])} className="header-row" onClick={() => setShow(!show)}>
{row.map((value, index) =>
index === 0 ? (
getFirstCell({ yearlyData, year: row[0], show, studyprogramme })
getFirstCell({ yearlyData, year: row[0], show, studyprogramme, calendarYears })
) : (
<>
{shouldBeHidden(showPercentages, value) ? null : (
@@ -73,6 +85,7 @@ const getRow = ({ yearlyData, row, show, setShow, studyprogramme, studytracks, s
<PopulationLink
studyprogramme={studyprogramme}
year={year}
years={calendarYears}
studytrack={_.findKey(studytracks, s => s === value.split(',')[0])}
/>
</Table.Cell>
@@ -147,6 +160,10 @@ const StudytrackDataTable = ({
const sortedMainStats = sortMainDataByYear(Object.values(dataOfAllTracks))
const sortedTrackStats = sortTrackDataByYear(dataOfSingleTrack)
const calendarYears = years.reduce((all, year) => {
if (year === 'Total') return all
return all.concat(Number(year.slice(0, 4)))
}, [])
return (
<div className="datatable">
@@ -175,7 +192,7 @@ const StudytrackDataTable = ({
<Table.Body>
{singleTrack
? sortedTrackStats.map(row =>
getSingleTrackRow({ row, studyprogramme, code: singleTrack, showPercentages, years })
getSingleTrackRow({ row, studyprogramme, code: singleTrack, showPercentages, years, calendarYears })
)
: sortedMainStats?.map(yearlyData =>
yearlyData.map(row =>
@@ -188,6 +205,7 @@ const StudytrackDataTable = ({
studytracks,
showPercentages,
years,
calendarYears,
})
)
)}
@@ -16,7 +16,7 @@ import '../studyprogramme.css'
const StudytrackOverview = ({ studyprogramme, specialGroups, setSpecialGroups, graduated, setGraduated }) => {
const toolTips = InfotoolTips.Studyprogramme
const [showBreakdown, setShowBreakdown] = useState(false)
const [showMedian, setShowMedian] = useState(false)
const [track, setTrack] = useState(studyprogramme)
const special = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
const grad = graduated ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
@@ -114,13 +114,14 @@ const StudytrackOverview = ({ studyprogramme, specialGroups, setSpecialGroups, g
<>
{getDivider('Average graduation times', 'AverageGraduationTimesStudytracks')}
<Toggle
firstLabel="Median time"
secondLabel="Breakdown"
value={showBreakdown}
setValue={setShowBreakdown}
cypress="GraduationTimeToggle"
firstLabel="Breakdown"
secondLabel="Median times"
value={showMedian}
setValue={setShowMedian}
/>
<div className="section-container-centered">
{!showBreakdown ? (
{showMedian ? (
<MedianTimeBarChart
data={stats?.data?.graduationTimes[track].medians}
goal={stats?.data?.graduationTimes.goal}
@@ -1,8 +1,9 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useHistory, withRouter } from 'react-router-dom'
import { connect, useDispatch, useSelector } from 'react-redux'
import { Header, Segment, Tab, Label, Menu } from 'semantic-ui-react'
import { Header, Segment, Tab, Menu } from 'semantic-ui-react'
import { useGetAuthorizedUserQuery } from 'redux/auth'
import { useGetProgressCriteriaQuery } from 'redux/programmeProgressCriteria'
import DegreeCoursesTable from './DegreeCourses'
import StudyProgrammeSelector from './StudyProgrammeSelector'
import BasicOverview from './BasicOverview'
@@ -23,19 +24,30 @@ const StudyProgramme = props => {
const dispatch = useDispatch()
const history = useHistory()
const programmes = useSelector(state => state.populationProgrammes?.data?.programmes)
const progressCriteria = useGetProgressCriteriaQuery({ programmeCode: props.match.params.studyProgrammeId })
const { language } = useLanguage()
const { isAdmin, rights } = useGetAuthorizedUserQuery()
const [tab, setTab] = useTabs('p_tab', 0, history)
const [academicYear, setAcademicYear] = useState(false)
const [specialGroups, setSpecialGroups] = useState(false)
const [graduated, setGraduated] = useState(false)
const emptyCriteria = {
courses: { yearOne: [], yearTwo: [], yearThree: [] },
credits: { yearOne: 0, yearTwo: 0, yearThree: 0 },
}
const [criteria, setCriteria] = useState(progressCriteria?.data ? progressCriteria.data : emptyCriteria)
useTitle('Study programmes')
useEffect(() => {
dispatch(getProgrammes())
}, [])
useEffect(() => {
if (progressCriteria.data) {
setCriteria(progressCriteria.data)
}
}, [progressCriteria.data])
const getPanes = () => {
const { match } = props
const { studyProgrammeId } = match.params
@@ -67,21 +79,9 @@ const StudyProgramme = props => {
),
})
if (isAdmin) {
panes.push({
menuItem: 'Update statistics',
render: () => <UpdateView studyprogramme={studyProgrammeId} />,
})
}
if (isAdmin || rights.includes(studyProgrammeId)) {
panes.push({
menuItem: (
<Menu.Item key="Programme courses">
Programme courses
<Label color="green">New!</Label>
</Menu.Item>
),
menuItem: <Menu.Item key="Programme courses">Programme courses</Menu.Item>,
render: () => (
<ProgrammeCoursesOverview
academicYear={academicYear}
@@ -92,13 +92,21 @@ const StudyProgramme = props => {
})
panes.push({
menuItem: 'Degree Courses',
render: () => <DegreeCoursesTable studyProgramme={studyProgrammeId} />,
render: () => (
<DegreeCoursesTable studyProgramme={studyProgrammeId} criteria={criteria} setCriteria={setCriteria} />
),
})
panes.push({
menuItem: 'Tags',
render: () => <Tags studyprogramme={studyProgrammeId} />,
})
}
if (isAdmin) {
panes.push({
menuItem: 'Update statistics',
render: () => <UpdateView studyprogramme={studyProgrammeId} />,
})
}
return panes
}
@@ -140,7 +148,6 @@ const StudyProgramme = props => {
</Segment>
</div>
)
return (
<div className="segmentContainer">
<Segment className="contentSegment">
@@ -0,0 +1,37 @@
import { RTKApi } from 'apiConnection'
const programmeProgressCriteria = RTKApi.injectEndpoints({
endpoints: builder => ({
getProgressCriteria: builder.query({
query: ({ programmeCode }) => `/programmecriteria?programmecode=${programmeCode}`,
}),
addProgressCriteriaCourse: builder.mutation({
query: ({ programmeCode, courses, year }) => ({
url: '/programmecriteria/courses',
method: 'POST',
body: {
code: programmeCode,
courses,
year,
},
}),
}),
addProgressCriteriaCredits: builder.mutation({
query: ({ programmeCode, credits }) => ({
url: '/programmecriteria/credits',
method: 'POST',
body: {
code: programmeCode,
credits,
},
}),
}),
}),
overrideExisting: false,
})
export const {
useGetProgressCriteriaQuery,
useAddProgressCriteriaCourseMutation,
useAddProgressCriteriaCreditsMutation,
} = programmeProgressCriteria