mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d552a5811 | ||
|
|
624a103f08 | ||
|
|
9f56554ed9 | ||
|
|
0d1bc3c519 | ||
|
|
aa1d21bc09 | ||
|
|
eaf5d7574e | ||
|
|
9db14a839f | ||
|
|
b55d649622 | ||
|
|
47c792e1f1 | ||
|
|
fffa3d3ee1 | ||
|
|
c9407ff4b0 | ||
|
|
1b43d4fc39 | ||
|
|
c0ce396d4d | ||
|
|
f15ffa95f5 | ||
|
|
765d2f771a | ||
|
|
e1271d7d1f | ||
|
|
7f632bde34 | ||
|
|
9dd47d002d | ||
|
|
16a07ace39 | ||
|
|
ac756cb35b | ||
|
|
b9059eb058 | ||
|
|
1f967a1f2c | ||
|
|
e8ef944a9d | ||
|
|
0e9ea20235 | ||
|
|
8d4f1c7794 | ||
|
|
6f3e4b8f64 | ||
|
|
316ed2af2f | ||
|
|
2f753d976b | ||
|
|
9b6aa96531 | ||
|
|
229a64aa45 | ||
|
|
6456b84777 | ||
|
|
9d34c34de7 | ||
|
|
2573976ea4 | ||
|
|
389b71cfbf | ||
|
|
c0ee74b7f8 | ||
|
|
e3fbf91b99 | ||
|
|
3d1f7cc655 | ||
|
|
069bd97729 | ||
|
|
96fded285b | ||
|
|
9c172c2f97 | ||
|
|
d37b037cc6 | ||
|
|
a55eef0f34 | ||
|
|
01ec83d909 | ||
|
|
81e8a0c77e | ||
|
|
7782f4f863 | ||
|
|
80f52355bb | ||
|
|
28cf6c855d | ||
|
|
6d446a1c05 | ||
|
|
d8214e7432 | ||
|
|
a8b04d56d6 | ||
|
|
0fc7bfda51 | ||
|
|
5614a6eb3f | ||
|
|
dfde4adf87 | ||
|
|
32a8c7c261 | ||
|
|
b7390912f4 | ||
|
|
6af7a1a5d0 | ||
|
|
e24b63da74 | ||
|
|
bb5bb8d04e | ||
|
|
a91fb5d7b7 | ||
|
|
5369e75b61 | ||
|
|
7a43433947 | ||
|
|
656870028e | ||
|
|
1315d7ca81 | ||
|
|
3b1f002ac8 | ||
|
|
00116544bf | ||
|
|
7bbccc9a81 | ||
|
|
c18187d094 | ||
|
|
7688a22c53 | ||
|
|
49df0a0d21 | ||
|
|
449e8976d9 | ||
|
|
6871502cb9 | ||
|
|
b72bedc49f | ||
|
|
9f300ccab0 | ||
|
|
cb8aa00f6f | ||
|
|
4a05f136cc | ||
|
|
50840a3fe2 | ||
|
|
ac440ff843 | ||
|
|
b6d28c6cff | ||
|
|
f79539c2a6 | ||
|
|
c46173cc98 | ||
|
|
a13c0e4628 | ||
|
|
a4887a30bc | ||
|
|
bf3adf446f | ||
|
|
a03d1c9707 | ||
|
|
2d9fe72481 | ||
|
|
15ef2f156e | ||
|
|
5470d53a19 | ||
|
|
65eedf1de8 | ||
|
|
1a9e51534b | ||
|
|
0d7d059541 | ||
|
|
2c76703085 | ||
|
|
81c45c383f | ||
|
|
9bb456e258 | ||
|
|
c9641bb118 | ||
|
|
06d129b685 | ||
|
|
b78446a11a | ||
|
|
3511b947c4 | ||
|
|
a338b2f4d5 | ||
|
|
3de3bbab43 |
Binary file not shown.
|
Before Width: | Height: | Size: 106 KiB |
@@ -50,7 +50,11 @@ What different CLI options do:
|
||||
|
||||
### Architecture
|
||||
|
||||

|
||||

|
||||
|
||||
### 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
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}) */
|
||||
})
|
||||
*/
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
@@ -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 |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -125,7 +125,7 @@ const codes = {
|
||||
T922106: 'foodhealth',
|
||||
T923101: 'papu',
|
||||
T923102: 'geodoc',
|
||||
T923103: 'atm',
|
||||
T923103: 'atm-dp',
|
||||
T923104: 'chems',
|
||||
T923105: 'domast',
|
||||
T923106: 'matrena',
|
||||
|
||||
+33
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
+42
-36
@@ -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>
|
||||
|
||||
+96
-29
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
+14
-2
@@ -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',
|
||||
|
||||
+48
-9
@@ -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}
|
||||
|
||||
+18
-7
@@ -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
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
-4
@@ -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}
|
||||
|
||||
+19
@@ -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;
|
||||
}
|
||||
+18
-1
@@ -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 => (
|
||||
|
||||
+2
@@ -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']
|
||||
}
|
||||
|
||||
+23
-1
@@ -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} />
|
||||
}
|
||||
|
||||
|
||||
+25
-7
@@ -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
|
||||
Reference in New Issue
Block a user