Compare commits

...
362 Commits
Author SHA1 Message Date
ShootingStar91 4f00b09052 [General Tab] Add diamond to second graduation crown 2023-06-09 12:59:21 +03:00
ShootingStar91 4f1e6bc7f1 [SortableTable] Replace all instead of one newline in titles 2023-06-09 12:38:12 +03:00
ShootingStar91 b38c1ca65d [General tab] Fix filter bug 2023-06-08 18:16:56 +03:00
ShootingStar91 5c0b2454a1 Add option to priority code texts 2023-06-08 15:49:10 +03:00
ShootingStar91 487d45ee38 [General tab] Major refactor to smaller files
Also small fixes: Fixed studyprogramme column bug in course statistics. Fixed email copy button triggering column sorting. Made copy all emails icon larger.
2023-06-08 15:46:10 +03:00
outisa 804eb072b9 [General tab] Add hops for combined prog 2023-06-07 17:27:46 +03:00
ShootingStar91 3222e71444 [General tab] Fix semesters present & add range filter 2023-06-07 16:52:52 +03:00
ShootingStar91 fcbf2ed7ae [Studyguidance] When passed tab fixed 2023-06-07 16:52:52 +03:00
ShootingStar91 fda3081df8 [General tab] Add phone number to export 2023-06-07 16:52:52 +03:00
outisa f0821ed78d [Updater (frontend)] Remove old unused code 2023-06-07 15:55:03 +03:00
outisa 05cd3221e4 Add studyright status filter for combined programmes 2023-06-07 14:38:44 +03:00
outisa 1165875599 Hops filter betterments 2023-06-07 14:36:34 +03:00
outisa 14bc6dc4e8 Graduated from programme filter betterments 2023-06-07 14:36:01 +03:00
Miko Keskimäki 886adcd5fb Fix wrong tag 2023-06-07 14:07:00 +03:00
Miko Keskimäki f2c01e6c6a Temporary openshift production workflow 2023-06-07 14:04:49 +03:00
Miko Keskimäki f7aff532a8 Add production tag to updater workflow 2023-06-07 13:36:19 +03:00
ShootingStar91 0d13d75a82 [General tab] Semesters present: Hovertext mentions graduation type 2023-06-07 12:34:46 +03:00
ShootingStar91 758884f3ee [General tab] Fix new column bug 2023-06-07 12:34:46 +03:00
ShootingStar91 7bceadcd2a [General tab] Add semester present amount column to excel for sorting purposes 2023-06-07 12:34:46 +03:00
Miko Keskimäki e83ae489ad Don't run oodikone release action when only updater is updated 2023-06-07 12:31:29 +03:00
Miko Keskimäki 160dff6e89 Updater scheduler logging 2023-06-07 12:31:29 +03:00
outisa f6efe102c7 Fix typo 2023-06-07 12:30:39 +03:00
outisa ac06cb95b6 [Study prog - search by class] Fix fetching data 2023-06-07 12:22:17 +03:00
outisa 33d7972835 [Custom pop] Remove studytrack filter for now (no prog. code for it) 2023-06-07 11:43:32 +03:00
outisa 197dbb90b6 Improve saving study programme stats to redis 2023-06-07 11:41:47 +03:00
ShootingStar91 8c6ad1da30 [General tab] Move start year column before semesters present so it wont get pushed far 2023-06-07 10:32:20 +03:00
ShootingStar91 149d4485d2 [General tab] Max 7 years of semesters present shown, or starting from associated year 2023-06-07 10:29:24 +03:00
ShootingStar91 e7fbf9c44b [General tab] Study programme column improvements
Show large plus-icon when more programmes. Show more info about programmes on hover and excel: graduated or inactive program. When program associated, prioritize that instead of newest.
2023-06-07 10:19:16 +03:00
Miko Keskimäki b5dc1ea51c Fix custom basepath for staging 2023-06-07 09:57:19 +03:00
outisa 4447ea66e9 [class stats graduated filter] Add possibility to fetch graduated for bachelor and master 2023-06-06 18:17:29 +03:00
outisa be8f5de412 [Class stats - hops filter] Add possibility to chose hops for combined programme 2023-06-06 17:29:20 +03:00
outisa bbda9e3914 Minor faculty refactoring 2023-06-06 17:19:53 +03:00
Miko Keskimäki c04ad4f02d Workflow fix 2023-06-06 12:29:22 +03:00
Miko KeskimäkiandGitHub b70dc1abd9 Merge pull request #4243 from UniversityOfHelsinkiCS/openshift-updater
Openshift Updater changes
2023-06-06 12:26:42 +03:00
Miko Keskimäki a7038e3ada Simplify workflow 2023-06-06 12:25:50 +03:00
Miko Keskimäki 519ad889c7 Bring old updater workflow back temporarily 2023-06-06 12:23:37 +03:00
Miko Keskimäki e45620b541 Updater staging workflow 2023-06-06 10:30:00 +03:00
Miko Keskimäki a814909a27 Pass correct CI value to updater 2023-06-06 10:18:17 +03:00
Miko Keskimäki 7723cb03a3 Update db connection 2023-06-06 10:06:27 +03:00
Miko Keskimäki d23923cc66 Remove development targets from ci docker-compose 2023-06-06 09:48:12 +03:00
Miko Keskimäki 674349bce5 Use correct variable names for login 2023-06-06 09:36:08 +03:00
Miko Keskimäki b517457c16 Login to Toska registry for tests 2023-06-06 09:32:07 +03:00
Miko Keskimäki adc975e46e Run updater action 2023-06-06 09:26:00 +03:00
Miko Keskimäki 5c4a7b85a6 Correct branch name 2023-06-06 09:24:44 +03:00
Miko Keskimäki 94c63f9429 New updater workflow 2023-06-06 09:24:00 +03:00
Miko Keskimäki 4bd37de38e Update docker-compose 2023-06-06 09:06:42 +03:00
Miko Keskimäki 4b13dfeae5 Update updater dockerfiles for openshift 2023-06-06 09:01:06 +03:00
outisa 1a7a625e81 [FacultyStudentDataTable] Add other countries popup 2023-06-05 17:27:17 +03:00
outisa 2cd9077088 [Courses tab] Add courses for combined programme 2023-06-05 15:58:52 +03:00
outisa d45b57bc67 [CourseTable] Possible fix 'reading properties of undefined' 2023-06-05 14:17:54 +03:00
outisa bc1eb1fe48 [Study track student table] countries Popup betterments 2023-06-05 14:02:05 +03:00
ShootingStar91 a8c9c0dd15 [General tab] Semesters present: Add larger gaps between years 2023-06-05 13:51:59 +03:00
ShootingStar91 5b14afc587 [General tab] Adjust titles 2023-06-05 13:51:59 +03:00
ShootingStar91 56eef7742c [General tab] Change three dots indicator to a plus-icon 2023-06-05 13:51:59 +03:00
outisa 2ebc864a24 [studytrack student table] Add better info to hover 2023-06-05 10:38:57 +03:00
outisa 0c0dcb697c [Credit stats: Distribution development] Add second graph for combined programme 2023-06-05 10:20:09 +03:00
outisa decf88bd52 Fix test 2023-06-04 21:53:31 +03:00
outisa 872e9b1849 [Studytrack data table] Show other countries stats 2023-06-04 21:16:01 +03:00
outisa 3f03cbe63d [courses of stats/population] Remove limit courses filter where it should not exist 2023-06-02 11:44:43 +03:00
ShootingStar91 4f630ed168 Disable clicking graph if user has no rights to see student 2023-06-01 13:06:11 +03:00
ShootingStar91 bb83a0f09a [General tab] Fix crashing bug 2023-06-01 13:00:59 +03:00
ShootingStar91 8d1f41ecf7 Fix studyguidance crashing better this time 2023-06-01 09:10:59 +03:00
ShootingStar91 5769a7041c Fix studyguidance group hops-filter crash 2023-05-31 21:37:25 +03:00
outisa 18088ab0e9 Fix test 2023-05-31 20:35:01 +03:00
outisa db133cc6ec [Studytrack table] Apply style also when tracks shown 2023-05-31 19:39:47 +03:00
outisa b741404678 Add students stats table betterments 2023-05-31 19:30:53 +03:00
outisa 1e68713472 [Studytrack student table] Infotext betterment 2023-05-31 16:51:17 +03:00
outisa e04a8b5fe2 [studytrack stats table] Compute values correctly for combined prog 2023-05-31 16:24:09 +03:00
ShootingStar91 997dddc298 Try to fix flaky test 2023-05-31 15:51:38 +03:00
ShootingStar91 783d720a0a Remove commented code 2023-05-31 15:30:36 +03:00
outisa 4f36bb6a55 [Studytrack stats] Try again to fix numbers 2023-05-31 15:20:14 +03:00
ShootingStar91 a577caaf46 [General tab] Semesters present sorting now works correctly 2023-05-31 15:18:04 +03:00
ShootingStar91 5196f3158e Fixes & refactoring related to exported name columns & sticky column fixed 2023-05-31 13:34:57 +03:00
ShootingStar91 646617be61 [General tab] Add infotooltip about graduation crown 2023-05-31 12:59:29 +03:00
ShootingStar91 b1030a4076 [General tab] Add grey outline to crown 2023-05-31 12:20:55 +03:00
ShootingStar91 5120517754 Try to fix crashing bug in course statistics general tab 2023-05-31 10:23:10 +03:00
ShootingStar91 fd5d755a29 [General tab] Add crown to semester traffic lights on graduation, improve excel version 2023-05-30 20:56:54 +03:00
outisa 88ba467ef7 [studytrack stats - graduated] Change bc+ms start to be bc start year 2023-05-30 15:20:28 +03:00
outisa c951e49aa3 Remove piece of unused code 2023-05-30 15:14:49 +03:00
outisa caafdb3139 Remove old unused mandatory courses related code 2023-05-30 09:48:45 +03:00
outisa aad66cf9e4 Fix visibility toggle for degree courses 2023-05-29 20:54:38 +03:00
outisa 32d91b916e Get name correctly for combined programmes 2023-05-29 20:12:37 +03:00
outisa d6f35c38f8 Fix 2023-05-29 19:43:23 +03:00
outisa 87823b5bde Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-05-29 19:15:26 +03:00
outisa efe8f802a6 Show total class stats for combined programmes 2023-05-29 19:14:59 +03:00
ShootingStar91 8463cdc327 Add wait to completed courses test 2023-05-29 18:44:48 +03:00
ShootingStar91 11cdaf997d Add script 'npm run flushredis' 2023-05-29 18:44:48 +03:00
outisa db025d68af [Search by class] Make possible to search combined programme 2023-05-29 18:19:47 +03:00
outisa fb65fe89c3 Unify naming of the variables 2023-05-29 18:17:36 +03:00
outisa c01b5415a0 Get name correctly for combined programmes 2023-05-29 18:08:30 +03:00
ShootingStar91 8c9c091e87 [General tab] Fixed semester enrollment feature 2023-05-29 18:00:28 +03:00
ShootingStar91 deb08e886f Backend: Remove dead code getMaxYearcode 2023-05-29 15:25:39 +03:00
ShootingStar91 7715e5964a [General tab] Remove double email columns if show names selected 2023-05-29 15:14:53 +03:00
ShootingStar91 7a226c2895 [General tab] Semester enrollments shown individually 2023-05-29 14:57:27 +03:00
outisa 861badff3d [Programme courses] Show bc and licentiate course stats for combined prog 2023-05-28 13:02:53 +03:00
outisa c7639859b0 [Programme courses] Make long name short 2023-05-28 12:57:01 +03:00
outisa c2c3d4495c [studyprogrammes] Fix nightly update of credits 2023-05-26 18:22:11 +03:00
outisa 92c05650fd [Study programme] Fix programme courses fetching, if no courses present 2023-05-26 17:27:17 +03:00
ShootingStar91 0fc4e57798 [General tab] Add indicator when more than 1 programmes 2023-05-26 14:49:49 +03:00
ShootingStar91 2be9712b19 Fix infotooltips 2023-05-26 14:48:38 +03:00
ShootingStar91 dab251084e [SortableTable] Export to CSV renamed to Excel 2023-05-26 14:47:59 +03:00
ShootingStar91 e4337d0cf2 [Credit accumulation graph] Add second marker for later graduations in combined views 2023-05-26 14:11:09 +03:00
ShootingStar91 1272a342e6 Add secondary email to common exports (most tables) 2023-05-26 12:30:19 +03:00
outisa 3ee3baf548 Fix ojelma to ohjelma 2023-05-26 10:18:13 +03:00
outisa 761612878a Fix numbers in tests 2023-05-25 22:59:44 +03:00
outisa 9453877c00 [Faculty/study programme stats] Fix (some) numbers 2023-05-25 22:20:05 +03:00
outisa 4c5b571df7 [Study track stats] Fix graduated table values 2023-05-25 19:19:56 +03:00
outisa 39a646c12d [Study tracks tab] Divide ms and bc+ms graduation graphs 2023-05-25 18:24:26 +03:00
ShootingStar91 66039b184e Add missing sisu links, add names & emails to csv exports 2023-05-25 18:09:31 +03:00
ShootingStar91 4eab18ea31 [Completed courses] Unify variablenames & remove unnecessary getRowContent 2023-05-25 16:32:50 +03:00
ShootingStar91 0b9fe19272 [SortableTable] Replace newlines with spaces for export 2023-05-25 15:58:13 +03:00
ShootingStar91 9a1668e132 [General tab] Fix titles and date formats for excel 2023-05-25 15:26:31 +03:00
ShootingStar91 720fda4725 Fix hidden studentnumbers 2023-05-25 15:25:06 +03:00
ShootingStar91 b3904a56c7 Refactor some studentnumber columns to use component 2023-05-25 13:50:14 +03:00
ShootingStar91 c035404b89 Refactor student info cell (studentnumber, link, sisulink) into one component 2023-05-25 13:50:14 +03:00
ShootingStar91 b36b98e6b2 Refactor other common components into common folder 2023-05-25 13:50:14 +03:00
outisa f70d237163 Acual test fix 2023-05-25 13:40:54 +03:00
outisa 2a6bedccfb Fix test 2023-05-25 13:28:32 +03:00
outisa 25ea34fbd4 [Study programme] Add graduation graph title betterments 2023-05-25 12:49:16 +03:00
ShootingStar91 2f2e86d975 [Progress table] Added sisu link 2023-05-25 11:56:00 +03:00
ShootingStar91 7da0a4de0d Refactor sisu links as a component in new common folder 2023-05-25 11:56:00 +03:00
ShootingStar91 3bffb3d934 [Completed courses] Fix sisu link not working 2023-05-25 11:56:00 +03:00
ShootingStar91 123a86077b [Filters] Improve studentnumber-filter tooltip text 2023-05-25 11:56:00 +03:00
outisa 6c2c216797 Fix misspelling of licentiate 2023-05-25 00:03:35 +03:00
outisa bfaad2af95 [Studyprogramme] Update studytrack view for combined programmes 2023-05-24 23:49:37 +03:00
outisa 9faa9d0ef4 Fetch stats correctly for tilannekuvalomake views 2023-05-24 21:37:12 +03:00
ShootingStar91 4e917800c9 [Study guidance groups] Add HOPS-column 2023-05-24 15:42:32 +03:00
ShootingStar91 146166e5ed [Study guidance groups] Add semesters present column 2023-05-24 14:32:08 +03:00
ShootingStar91 9e49edb33a [Study guidance groups] Remove default creditdatefilter 2023-05-24 14:15:45 +03:00
outisa a681cf3b36 Progress table betterments 2023-05-23 09:26:21 +03:00
outisa 7af8e848f9 Fix calculating months 2023-05-23 09:25:38 +03:00
outisa ca79c82ddd Fix the fix 2023-05-19 17:35:38 +03:00
outisa 46b7659e73 Add possibility to choose only bachelor programme 2023-05-19 16:45:10 +03:00
outisa 26f44079e2 Title improvements 2023-05-19 16:44:01 +03:00
outisa 03da43bfe1 [Studyprogramme-basic tab] Add credits produced for licenciate 2023-05-19 16:19:56 +03:00
outisa 251f5526c0 Fix tests 2023-05-19 16:18:18 +03:00
outisa 764e09b34a [studyprogramme-graduation times] Add missing titles 2023-05-19 15:00:06 +03:00
outisa 933f332ea3 [Studyprogramme-basic view] Add separated graduation time graph for licenciate 2023-05-19 14:40:29 +03:00
outisa 6facbbdb36 [Studytrack stats] Fix and refactor table titles 2023-05-19 12:43:36 +03:00
outisa 23e2622c3a Refactor combined programme information spreading and credit limits 2023-05-19 11:03:39 +03:00
ShootingStar91 1fab787ed9 [Class statistics] Courses-tab summary row now ignores sorting 2023-05-18 18:10:07 +03:00
ShootingStar91 db86775288 [Study guidance] HOPS filter shown only when programme is associated with population 2023-05-18 14:02:26 +03:00
ShootingStar91 288403c90c [General tab] Table shows daterange column 2023-05-17 15:48:26 +03:00
ShootingStar91 93b8bef0a5 [Custom population] Remove DataExport 2023-05-17 15:22:23 +03:00
ShootingStar91 e5880373b1 Fix resilient bug, also round credits 2023-05-17 14:42:52 +03:00
ShootingStar91 d47985349a Giving faculty rights adds rights to newer programmes 2023-05-16 17:36:25 +03:00
ShootingStar91 7e177d6252 [General tab] Fix sisu link 2023-05-16 12:10:32 +03:00
outisa f224903e7c [Study programme - basic view] Separate graduated students in the tables 2023-05-15 19:01:19 +03:00
outisa feff8bed08 [Degree courses] Remove hardcoded combined programme value 2023-05-15 09:11:08 +03:00
outisa 59287b9622 [StudyProgramme] Add possibility to show combined programmes 2023-05-15 00:40:55 +03:00
outisa f62635076b Change ignore filters back to true 2023-05-15 00:36:37 +03:00
outisa eab18870ea [Degree courses] Refactor view to show combined programmes 2023-05-13 16:54:42 +03:00
outisa cf5078b7a3 Adjust function 2023-05-12 23:53:31 +03:00
ShootingStar91 3e14a7a2e4 [General Tab] Programme-column shows more programmes by hover and csv 2023-05-12 16:35:49 +03:00
outisa 81a22c571a [custom population] Fix data export 2023-05-11 16:36:06 +03:00
outisa 297d697e75 [Dataexport] Fix bug 2023-05-11 14:38:48 +03:00
ShootingStar91 7b180d63a1 [Course statistics] Add column 'started in programme' 2023-05-11 10:34:47 +03:00
ShootingStar91 12520a9665 [SortableTable] Sort order made to ignore case 2023-05-11 10:07:19 +03:00
Outi SavolainenandGitHub f403be88b3 Try to trigger workflow 2023-05-11 07:57:21 +03:00
outisa 0c82f32d20 Add possibility to get mandatorycourses to combined programme 2023-05-10 19:59:55 +03:00
outisa 61fb4da81c Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-05-10 18:44:49 +03:00
outisa 0d5daf423f Change sisu link to newer one 2023-05-10 18:44:37 +03:00
Miko Keskimäki 42b41ecece Maybe fix sentry 2023-05-10 14:06:07 +03:00
outisa b21b098ff4 Fix population total link 2023-05-10 13:15:16 +03:00
outisa 9be3c4e852 Fix (?) Filters tests 2023-05-09 16:09:59 +03:00
outisa ca67096bc5 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-05-09 15:15:25 +03:00
outisa 5d5ed09b98 Add faculty fun 2023-05-09 15:14:58 +03:00
Miko Keskimäki 5e2ed40ac5 Backend needed for db actions 2023-05-09 12:37:28 +03:00
Miko Keskimäki 2e2c7c4ad4 Bring back test db build actions 2023-05-09 12:19:48 +03:00
Miko Keskimäki db64cb8ae2 CI used staging images... 2023-05-09 11:39:37 +03:00
Miko Keskimäki 7422a55799 Build images for each test individually 2023-05-09 11:27:32 +03:00
Miko Keskimäki 59f29de345 Re-enable cache 2023-05-09 11:03:55 +03:00
Miko Keskimäki b9a10d8d51 Skip caching test images 2023-05-09 10:57:59 +03:00
ShootingStar91 391ffeefe0 Change cache clearing button texts 2023-05-09 09:40:58 +03:00
ShootingStar91 6a2f6fc50c Try to fix the test again 2023-05-08 17:39:12 +03:00
ShootingStar91 29c38ee38e Add wait after clicking update-tab 2023-05-08 16:24:50 +03:00
ShootingStar91 8d4ec802ec Clear redis from UI in tests 2023-05-08 15:25:03 +03:00
ShootingStar91 99dfb4d26f [Completed courses] Fix transfers not showing & add student links 2023-05-08 13:43:12 +03:00
ShootingStar91 1eb4e9a46f Attempt to flush redis for CI 2023-05-08 12:18:24 +03:00
ShootingStar91 e89d4709c1 Try to force build ci yml 2023-05-08 10:54:24 +03:00
Miko Keskimäki e226bdcdca Add softa-url to staging action 2023-05-08 10:22:51 +03:00
ShootingStar91 575cddfd10 Skip the troubled tests 2023-05-08 09:56:22 +03:00
ShootingStar91 4e37801352 Attempt to fix tests of studyprogramme 2023-05-08 09:22:18 +03:00
Miko Keskimäki 2fb270a070 Remove test failure report, deployment failure message is enough 2023-05-05 08:44:58 +03:00
Miko Keskimäki c894f858ca Bring back slack test failure, deployment notifications 2023-05-05 08:25:59 +03:00
Miko Keskimäki f5b04beda0 Fix wrong base path in prod deploment 2023-05-05 08:25:16 +03:00
outisa c20a4e4727 Fix bug 2023-05-04 19:11:05 +03:00
ShootingStar91 3081752d8f [Degree Courses] Fixed exploded modal UI 2023-05-04 18:49:11 +03:00
Miko Keskimäki 19fe7fd817 Remove target: production for updated Dockerfiles 2023-05-04 18:01:55 +03:00
ShootingStar91 99b3f7756c [Progress tab] Infobox margin issue fixed 2023-05-04 17:47:02 +03:00
ShootingStar91 51475be0db [Class statistics] Credits gained: info improved 2023-05-04 17:29:32 +03:00
ShootingStar91 a8eb68e317 Next academic year only shown after 1.8.: affects many views 2023-05-04 16:23:35 +03:00
ShootingStar91 8281564fef [Student statistics] Transfer text made nicer 2023-05-04 14:25:58 +03:00
ShootingStar91 4373065fdd [Studyprogramme Overview] Fixed title for doctors 2023-05-04 14:24:58 +03:00
Miko Keskimäki 8dbbf00b51 Remove openshift branch from staging action 2023-05-04 13:42:08 +03:00
Outi SavolainenandGitHub 584a1b474a Merge pull request #4195 from UniversityOfHelsinkiCS/openshift
Update configuration to work with OpenShift
2023-05-04 13:40:09 +03:00
ShootingStar91 8151c4f463 [General tab] Emails in CSV fixes 2023-05-04 12:44:05 +03:00
Miko Keskimäki bb818ee9ac Run workflow when files updated 2023-05-04 12:16:25 +03:00
Miko Keskimäki cd3b9bb843 Test new staging workflow 2023-05-04 12:14:25 +03:00
Miko Keskimäki aaa012894c Don't run sentry in staging, update staging workflow 2023-05-04 12:12:59 +03:00
ShootingStar91 4d06eb7558 Fix typo lenght to length 2023-05-04 11:17:59 +03:00
ShootingStar91 b563b29a36 [Credit accumulation] Graduations are shown as diamond-icon 2023-05-04 10:48:36 +03:00
outisa 617e234798 [class stats] Small fixes 2023-05-03 18:44:08 +03:00
Miko Keskimäki 754d525f87 Update environment configuration 2023-05-03 15:28:04 +03:00
Miko Keskimäki a0e6d342b9 Configurable Jami url 2023-05-03 14:37:27 +03:00
Miko Keskimäki 60f500c9e7 Configurable importer url 2023-05-03 14:12:17 +03:00
Miko Keskimäki dc9ce89aa7 More env variables 2023-05-03 13:37:24 +03:00
Miko Keskimäki 8a1fb2725b Pass built_at to frontend, update staging 2023-05-03 13:33:19 +03:00
Miko Keskimäki 0ca6bca243 Maybe now 2023-05-03 12:50:10 +03:00
Miko Keskimäki 7457bc412b Again 2023-05-03 12:48:13 +03:00
Miko Keskimäki 3d2e8fb1ca Try again 2023-05-03 12:47:41 +03:00
Miko Keskimäki 1d980795c6 Update image build and release 2023-05-03 12:27:38 +03:00
Miko Keskimäki 6bc6a28127 Define secrets as inputs 2023-05-03 12:11:50 +03:00
Miko Keskimäki a2f84931cd Pass toska registry secrets to workflow 2023-05-03 12:04:32 +03:00
Miko Keskimäki 1f1bae8557 Add missing build jobs 2023-05-03 11:59:15 +03:00
Miko Keskimäki c714c61269 Workflow refactor 2023-05-03 11:53:30 +03:00
Miko Keskimäki e85deff947 Delete unnecessary steps, update image names 2023-05-03 11:37:45 +03:00
Miko Keskimäki 8c87135f58 Update openshift workflow 2023-05-03 11:09:56 +03:00
Miko Keskimäki 7eac6c686e Remove extra backend package installs 2023-05-03 10:11:04 +03:00
Miko Keskimäki 11172f7d77 Update temporary workflow 2023-05-03 10:07:34 +03:00
Miko Keskimäki be29486cdd Update frontend, backend Dockerfiles to work with openshift 2023-05-03 10:04:49 +03:00
Miko Keskimäki 04cbec19a5 Separate backend and frontend dev setups to separate Dockerfiles 2023-05-03 09:50:48 +03:00
Miko Keskimäki 7c3cfb56b5 Fix iams not being saved to Jami 2023-05-03 09:10:04 +03:00
outisa 090305be3b [Class stats] Unify bachelor and master (eläinlääkkis) 2023-05-02 18:39:26 +03:00
ShootingStar91 c1e0d34670 [Programme courses] Move help texts to infobox 2023-05-02 13:51:58 +03:00
ShootingStar91 9cfc51382d [Pass/fail table] Help texts moved to infobox 2023-05-02 12:33:25 +03:00
ShootingStar91 3f179d3460 [Study Programmes] Admins have toggle to filter out old programmes 2023-05-02 12:09:40 +03:00
outisa 0ad0d73d18 [Progress tab] Add StudyGuidanceGroups betterments 2023-05-02 11:38:29 +03:00
outisa 7952b3703f Fix stupidness 2023-05-02 11:36:19 +03:00
outisa e9902a0bbe [studyGuidangeGroups]Remove tags tab from the view 2023-05-02 10:50:26 +03:00
ShootingStar91 b225e03bd5 Fix test 2023-04-28 18:54:37 +03:00
ShootingStar91 374cef4a1a [Study Programme] Programme courses tab: Shorten long names 2023-04-28 17:44:38 +03:00
ShootingStar91 6dff16f683 [Workflow] Add completed courses tests to workflow 2023-04-28 16:57:13 +03:00
ShootingStar91 73a1d50ec1 [Study Programme] Credit distribution development: target credits adjusted to age of class 2023-04-28 16:49:11 +03:00
outisa b473b393f0 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-04-27 21:46:34 +03:00
outisa 218f09ea65 [StudyGuidanceGroups] Add moar tabs in general view 2023-04-27 21:44:46 +03:00
outisa 5948a34c6e Adjust message box rows 2023-04-27 21:36:08 +03:00
outisa 6db0e982d9 [Stats-progress tab] Make view look better 2023-04-27 21:21:40 +03:00
ShootingStar91 205637c4db [Completed courses] Minor fix 2023-04-27 12:59:01 +03:00
ShootingStar91 7ac397969a [CustomPopulation] Invalid or forbidden studentnumbers explicitly mentioned 2023-04-27 12:59:01 +03:00
outisa 9fc927daff [Student stats-progress tab] Make semester enrollment column readable 2023-04-27 12:54:11 +03:00
outisa 72635b53ac [Openuni] Add minor impovement 2023-04-27 12:32:06 +03:00
outisa 980a7a6703 [Student stats] Change passive to inactive 2023-04-27 12:31:03 +03:00
outisa 80709a129c [courses of pop/class tab] Add limit number of students filter 2023-04-25 23:07:40 +03:00
outisa 26c7f3e00b [Courses of Pop] Remove old pass/fail table 2023-04-25 20:41:27 +03:00
outisa cc60c9f415 [Courses of class/pop] Add info box betterments 2023-04-25 20:38:50 +03:00
ShootingStar91 7f8f40a193 [Completed courses] Added tests & minor improvements 2023-04-25 16:40:49 +03:00
ShootingStar91 1749503aa4 [Completed courses] Also invalid student numbers are mentioned 2023-04-25 16:40:49 +03:00
Laura Sinikallio da55bfefac Open evalution overview for wider access 2023-04-25 12:48:07 +03:00
outisa 0c2f197460 [StudyGuidanceGroups] Add credit filter 2023-04-24 23:05:35 +03:00
ShootingStar91 6503971915 Fix broken tests 2023-04-20 15:26:31 +03:00
ShootingStar91 c30624d319 [FilterView] Fixed semester sort order 2023-04-20 14:41:31 +03:00
outisa a4c798ea25 [OpenUni] Remove dissemination_info_allowed related functionality 2023-04-20 12:41:09 +03:00
outisa bf175e9140 [Updater] Remove dissemination_info_allowed column related code 2023-04-20 12:39:12 +03:00
outisa 70f4440974 [Student stats -progress tab] Add betterments to colors 2023-04-20 12:29:49 +03:00
ShootingStar91 cde5dd5ced [Completed courses] Emails shown only if names are, all shown in csv 2023-04-20 12:05:23 +03:00
ShootingStar91 35777287b2 [SortableTable] Fixed export column selection refresh 2023-04-20 11:43:02 +03:00
ShootingStar91 3ec010f347 [Completed courses] Removed 'No completion' from CSV 2023-04-19 14:54:50 +03:00
ShootingStar91 36792ac667 [NavitagionBar] Fix minor rights bug 2023-04-19 14:30:19 +03:00
ShootingStar91 d4f187ce92 [Completed courses] Fix rights & CSV columns 2023-04-19 13:02:18 +03:00
outisa 0af1e0d82e [Student stats] Remove programme from studystartline 2023-04-18 20:42:16 +03:00
outisa 74803f7af4 [Student stats] Add programmes to charts 2023-04-18 20:16:19 +03:00
outisa 4817393665 Add better info 2023-04-18 19:19:45 +03:00
outisa 80de8768e3 [class stats - progress] Add betterments 2023-04-18 18:56:55 +03:00
ShootingStar91 3f696acf99 [Completed courses] Notification if missing rights to some students 2023-04-18 18:37:05 +03:00
ShootingStar91 831cab9e2b [Completed courses] Fix url in rtk api 2023-04-18 17:57:11 +03:00
ShootingStar91 94b0c0f241 [Completed courses] Saving course list, improvements, bugfixes 2023-04-18 17:21:39 +03:00
ShootingStar91 5d51096557 [Completed courses] Course substitutions are taken into account 2023-04-18 13:16:48 +03:00
ShootingStar91 be90065bd4 [Completed courses] Improve text 2023-04-18 10:06:08 +03:00
outisa 9501055406 Explain status better 2023-04-14 18:35:21 +03:00
outisa 66685ee0ff [Student Stats] Add grade for graduation 2023-04-14 18:03:36 +03:00
outisa 84cb0ca7a1 [Student statistics] Make Bachelor Honours accordion better 2023-04-14 17:56:15 +03:00
outisa c16e85d688 [Student statistics] Add padding to studyright column 2023-04-14 17:40:54 +03:00
outisa 0781f2a9b0 [class stats-progress view] Show dates and semester enrollments better 2023-04-14 16:58:02 +03:00
outisa 0689e83cbd Remove red from the console 2023-04-14 16:56:03 +03:00
outisa 6e407fc196 [OpenUniSearch] Fix dates to have year first 2023-04-14 16:54:58 +03:00
ShootingStar91 4d6abef0b9 [Completed courses] Renamed from prerequisites, many improvements 2023-04-14 16:39:08 +03:00
outisa 1f1e2d45fa [Studyprogramme - Degree courses] Add better info 2023-04-14 12:54:26 +03:00
outisa 3028cdb6ec [Studyprogramme - Degree courses] Add better info 2023-04-14 12:52:22 +03:00
outisa ed71a0ce3b Fix typos 2023-04-12 21:28:36 +03:00
outisa f3c290a5ee [class stats - Progress]Adjust months when criteria shown 2023-04-12 21:25:09 +03:00
outisa 7ab8236e14 [Degree courses] Fix criteria months 2023-04-12 21:20:42 +03:00
outisa a884069e77 [Class stats - Progress]Add possibility to add criteria for 6 years 2023-04-12 20:42:01 +03:00
outisa 1d8a06c477 Mute linting error 2023-04-12 20:38:20 +03:00
ShootingStar91 88ba4af98c [Prerequisites tool] Many fixes regarding roles & rights 2023-04-12 18:46:02 +03:00
ShootingStar91 4ed75331ff [Prerequisites tool] Initial version 2023-04-12 17:08:36 +03:00
outisa 63c2d9756f Skip one of the age filter test for now 2023-04-12 00:35:51 +03:00
outisa 2592adfd18 Fix bug if criteria undefined 2023-04-11 18:33:36 +03:00
outisa d8976adb46 [class stats Progress tab] Handle stats better 2023-04-11 17:32:03 +03:00
outisa f47f4bca29 [Updater]Change stupidness back to greatness 2023-04-05 11:02:39 +03:00
Kangas Arttu a5d824b927 [OpenUni] Fixed text about button 2023-04-04 15:15:36 +03:00
Miko Keskimäki e20e1137ae Don't fetch studentsUserCanAccess for users table 2023-04-04 14:02:46 +03:00
outisa 11792b0397 [Updater] Modify error logging in bulkcreate 2023-04-03 20:45:31 +03:00
outisa 3cb2e370aa [Student stats] Show qualified for honours correct programmes 2023-04-02 14:04:29 +03:00
outisa db5b0a29cd [Student stats] Make bachelorhonours modules collapsible 2023-04-02 13:40:26 +03:00
outisa 79f9a59116 [Student statistics] Add Betterments 2023-04-02 13:20:13 +03:00
outisa fdba5dc6a6 [Updater] Make credit error more readable 2023-04-02 13:18:45 +03:00
outisa d0e8a9632d [Updater] Fix logger error 2023-04-02 12:34:00 +03:00
outisa 16b7debbb5 [Frontend] Take Hops and studyright field changes into action 2023-04-01 23:16:25 +03:00
outisa 6081bb3e78 [Backend] Add HOPS and Studyright field changes 2023-04-01 23:12:44 +03:00
outisa de2823f3b5 [Updater] Possible fix for updating HOPS 2023-04-01 22:41:42 +03:00
outisa 07e2dabe3f Make course tables more readable when compared 2023-03-27 21:27:30 +03:00
outisa 1cccecc7f5 [class stats - pop courses] add criteria management to manage courses shown 2023-03-27 20:31:46 +03:00
outisa 37a50731aa [Faculty] Fix bug showing progress info eläinlääkkis 2023-03-27 14:42:06 +03:00
outisa eba296f5fd Fix typo 2023-03-27 14:39:03 +03:00
outisa fbe0b0af3a Show messages when updater usedin FE 2023-03-25 19:59:52 +02:00
outisa ea4d299f16 Reduce red from the console 2023-03-24 13:49:53 +02:00
iPegii f2f9bdd9e8 Show transfers on student statistics 2023-03-24 12:18:08 +02:00
outisa 4b86ed252f Add bachelor column to excel 2023-03-23 15:03:04 +02:00
outisa f535a5dbdb Fix admission filter to work with 'Ei valintatapaa' 2023-03-23 14:34:22 +02:00
outisa ffe6425999 Fix excel to show credits correctly in since studystart column 2023-03-23 14:17:11 +02:00
outisa 256997633a Fix test 2023-03-22 18:06:28 +02:00
outisa 1d3fef6c8d Reduce red from console 2023-03-22 17:45:30 +02:00
outisa 428e46084c [Study programme-Programme courses] Add new stats columns 2023-03-22 17:27:33 +02:00
outisa 09f4814f80 Remove red from the dev console 2023-03-22 14:22:56 +02:00
outisa a220050a53 Possible fix to Sentry error 2023-03-22 13:58:47 +02:00
outisa 115674636f [class stats-progress table] Add more columns csv 2023-03-20 18:52:20 +02:00
outisa f901fec6a3 Infotext betterment 2023-03-20 10:18:47 +02:00
Laura Sinikallio a77ba0e1e1 Edit info text 2023-03-19 20:40:31 +02:00
Laura Sinikallio 434183bac0 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-03-19 20:33:38 +02:00
Laura Sinikallio 7993c2b45a Edit info text 2023-03-19 20:33:03 +02:00
Laura Sinikallio 8551a76fc9 Add info to evaluation programme view 2023-03-19 20:29:58 +02:00
Laura Sinikallio b25f9ba55f Add info to evaluation faculty view 2023-03-19 20:12:08 +02:00
outisa f28276e086 Clean warnings from the console 2023-03-19 20:11:45 +02:00
outisa 6327ca7e2c [Class stats - Progress table] Add infotexts 2023-03-19 20:01:49 +02:00
Laura Sinikallio 625f2962eb Clean warnings 2023-03-19 19:44:37 +02:00
Laura Sinikallio f85e9f0a5d Fetch and display programme name 2023-03-19 19:29:35 +02:00
Laura Sinikallio d4d67b239e Move toggle 2023-03-19 19:03:35 +02:00
Laura Sinikallio 83d17a3026 Make custom endpoint for tilannekuvalomake programme view 2023-03-19 19:02:28 +02:00
Laura Sinikallio e3f8fdb4ae Add redirection rules 2023-03-17 18:59:19 +02:00
Laura Sinikallio b7051ccd41 Add header placeholder 2023-03-17 18:56:28 +02:00
Laura Sinikallio b0a0be7969 Add faculty view for tilannekuvalomake 2023-03-17 18:51:37 +02:00
Laura Sinikallio 896cb46ad1 Add redirection for malformed url 2023-03-17 17:11:56 +02:00
Laura Sinikallio 17f7289b1c Create new view for Tilannekuvalomake 2023-03-17 16:53:41 +02:00
outisa a019dd9672 Fix minor bug 2023-03-16 21:13:10 +02:00
outisa 881aaf93b4 [Class stats] Progress table betterments 2023-03-16 17:06:28 +02:00
outisa 5c973d4f8c Change icon 2023-03-16 16:57:42 +02:00
outisa f9e8bf6d31 Add consistency to icons in different views 2023-03-16 10:49:42 +02:00
outisa a2726d4894 [Class stats - progress table] Show checked credits criteria correctly 2023-03-15 18:43:05 +02:00
outisa 11377da7bd [Class stats - progress table] Make csv better 2023-03-15 18:38:18 +02:00
outisa 8c4515b158 [Class stats-Progress table] Add hidden fields csv 2023-03-15 18:13:46 +02:00
outisa db3cd52d69 Refactor fetching courses 2023-03-12 20:08:13 +02:00
outisa b4c8bf0f4a Fix checking the passed courses for criteria 2023-03-09 20:51:29 +02:00
outisa 384afb1e5f Fix keeping track from enrollmentNoGrade 2023-03-09 20:35:13 +02:00
outisa 090d327405 Cypress, please make my day 2023-03-09 19:10:04 +02:00
outisa 30117e60b3 Fix message text 2023-03-09 16:44:23 +02:00
outisa 9a4fec0974 [Faculty basic] Remove any effects from special excluded toggle for credits 2023-03-09 16:41:43 +02:00
outisa 37114c5570 Add betterments for progress table 2023-02-24 17:06:29 +02:00
outisa 63b7c5876d Fix full screen view, if nav bar visible 2023-02-24 14:37:03 +02:00
outisa 9790c829c6 [Tags] Change button color from positive to green 2023-02-23 17:37:52 +02:00
outisa 082aaa4022 [Courses filter] Remove Failed manyt times and passed after retry options 2023-02-23 17:14:59 +02:00
outisa 3906efbcca Add colors for buttons 2023-02-23 16:51:00 +02:00
outisa 9d575d6c88 [Degree courses] Make button betterments 2023-02-23 16:39:52 +02:00
outisa 4d4a288719 Reduce red from the console 2023-02-23 16:21:08 +02:00
outisa a39f522501 [Class statistics] Add first draft for student progress table 2023-02-23 16:19:46 +02:00
outisa 32293eb050 [Courses filter] Add enrolled, no grade option 2023-02-17 15:29:30 +02:00
outisa ed56d32cce Remove unused code 2023-02-17 15:22:38 +02:00
Miko Keskimäki f4b04d3283 Don't reset studyGuidanceGroups roles each time users table is rendered 2023-02-17 14:54:30 +02:00
outisa 3a6e5132f3 Fix filter backend code not to crash studyguidance groups 2023-02-14 19:00:16 +02:00
outisa 694e537baa Fix parts of course filter 2023-02-14 18:49:37 +02:00
Miko Keskimäki 372ac53a46 Update studyGuidanceGroups only when user logs in
Is not dependent on IAMs so doesn't actually need to be rerun for users table. Also takes a lot of time and leads to timeout problems
2023-02-08 20:31:15 +02:00
Miko Keskimäki 5d9bbe368c Recalculate user access roles for users table 2023-02-08 19:49:04 +02:00
217 changed files with 8478 additions and 3764 deletions
@@ -15,24 +15,17 @@ inputs:
runs:
using: composite
steps:
- name: 'Download images'
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built images'
shell: bash
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
- name: 'Login to toska Docker registry'
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ inputs.username }}
password: ${{ inputs.password }}
- name: 'Run services, pull rest of the needed images'
shell: bash
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
run: docker-compose --file docker-compose.ci.yml up --detach
- name: 'Run cypress'
uses: cypress-io/github-action@v4
with:
@@ -40,12 +33,14 @@ runs:
spec: ${{ inputs.spec }}
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v3
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
shell: bash
if: failure()
+66
View File
@@ -0,0 +1,66 @@
name: 'Release to production'
on:
push:
branches:
- master # Switch to on release
jobs:
release_frontend:
name: 'Build and release frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build frontend image
id: build-image
uses: redhat-actions/buildah-build@v2
with:
image: oodikone-frontend
tags: production ${{ github.sha }}
context: services/frontend
containerfiles: |
services/frontend/Dockerfile
build-args: |
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
SENTRY_ENVIRONMENT=production
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-image.outputs.image }}
tags: ${{ steps.build-image.outputs.tags }}
registry: quay.io/toska
username: toska+github
password: ${{ secrets.QUAY_IO_TOKEN }}
release_backend:
name: 'Build and release backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build backend image
id: build-image
uses: redhat-actions/buildah-build@v2
with:
image: oodikone-backend
tags: production ${{ github.sha }}
context: services/backend
containerfiles: |
services/backend/Dockerfile
build-args: |
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
SENTRY_ENVIRONMENT=production
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-image.outputs.image }}
tags: ${{ steps.build-image.outputs.tags }}
registry: quay.io/toska
username: toska+github
password: ${{ secrets.QUAY_IO_TOKEN }}
+1 -3
View File
@@ -35,7 +35,6 @@ jobs:
context: services/backend
push: true
tags: toska/oodikone2-backend:latest
target: production
build_and_release_frontend:
name: 'Build and release frontend'
@@ -71,11 +70,10 @@ jobs:
context: services/frontend
push: true
tags: toska/oodikone2-frontend:latest
target: production
- name: 'Create a container from built image '
run: docker create --name frontend toska/oodikone2-frontend:latest
- name: 'Copy assets from the container'
run: docker cp frontend:/usr/src/app/build ./build
run: docker cp frontend:/opt/app-root/src/build ./build
- name: 'Create Sentry release'
uses: getsentry/action-release@v1
env:
+124
View File
@@ -0,0 +1,124 @@
name: 'Test and release to staging'
on:
push:
branches:
- master
paths:
- '.github/workflows/test.yaml'
- '.github/workflows/staging.yaml'
- '.github/actions/**'
- 'cypress/**'
- 'cypress.config.json'
- 'services/**'
- '!services/backend/src/rapodiff.js'
- 'docker-compose.ci.yml'
- 'package*.json'
- 'updater/sis-updater-worker/src/db/migrations/**'
jobs:
test:
uses: ./.github/workflows/test.yaml
secrets:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
release_frontend:
name: 'Build and release frontend'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- name: Build frontend image
id: build-image
uses: redhat-actions/buildah-build@v2
with:
image: oodikone-frontend
tags: staging ${{ github.sha }}
context: services/frontend
containerfiles: |
services/frontend/Dockerfile
build-args: |
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
BASE_PATH=/oodikone
GIT_SHA=${{ github.sha }}
STAGING=true
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-image.outputs.image }}
tags: ${{ steps.build-image.outputs.tags }}
registry: quay.io/toska
username: toska+github
password: ${{ secrets.QUAY_IO_TOKEN }}
release_backend:
name: 'Build and release backend'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- name: Build backend image
id: build-image
uses: redhat-actions/buildah-build@v2
with:
image: oodikone-backend
tags: staging ${{ github.sha }}
context: services/backend
containerfiles: |
services/backend/Dockerfile
build-args: |
BASE_PATH=/oodikone
GIT_SHA=${{ github.sha }}
STAGING=true
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-image.outputs.image }}
tags: ${{ steps.build-image.outputs.tags }}
registry: quay.io/toska
username: toska+github
password: ${{ secrets.QUAY_IO_TOKEN }}
report_success:
name: 'Report deployment success to oodikone notifications'
runs-on: ubuntu-latest
needs:
- release_frontend
- release_backend
if: success()
steps:
- uses: actions/checkout@v3
- name: 'Send notification to slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment
softa-url: https://toska-staging.cs.helsinki.fi/oodikone
deployment-target: staging
report_failure:
name: 'Report deployment failure to oodikone notifications'
runs-on: ubuntu-latest
needs:
- release_frontend
- release_backend
if: failure()
steps:
- uses: actions/checkout@v3
- name: 'Send notification to slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment-failure
deployment-target: staging
@@ -23,7 +23,6 @@ jobs:
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=production
context: updater/sis-updater-worker/
target: production
push: false
outputs: type=docker,dest=/tmp/worker.tar
tags: toska/sis-updater-worker:latest
@@ -47,7 +46,6 @@ jobs:
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=production
context: updater/sis-updater-scheduler/
target: production
push: false
outputs: type=docker,dest=/tmp/scheduler.tar
tags: toska/sis-updater-scheduler:latest
@@ -58,61 +56,12 @@ jobs:
with:
path: /tmp/scheduler.tar
test_updater:
name: 'Test updater'
runs-on: ubuntu-latest
release_updater:
name: 'Release updater'
needs:
- build_worker
- build_scheduler
steps:
- uses: actions/checkout@v3
- name: 'Login to toska docker registry'
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Download built images'
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load worker image'
run: docker load --input /tmp/artifact/worker.tar
- name: 'Load scheduler image'
run: docker load --input /tmp/artifact/scheduler.tar
- name: 'Start worker'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker
- name: 'Run hourly jobs'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
SCHEDULE_IMMEDIATE: hourly
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run daily jobs'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
SCHEDULE_IMMEDIATE: daily
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run weekly jobs'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
SCHEDULE_IMMEDIATE: weekly
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run prepurge'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
SCHEDULE_IMMEDIATE: prepurge
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run purge'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
SCHEDULE_IMMEDIATE: purge
EXIT_AFTER_IMMEDIATES: yes
release_updater:
name: 'Release updater'
runs-on: ubuntu-latest
needs:
- test_updater
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
@@ -129,7 +78,6 @@ jobs:
context: updater/sis-updater-scheduler/
push: true
tags: toska/sis-updater-scheduler:latest
target: production
- name: 'Build and push worker'
uses: docker/build-push-action@v3
with:
@@ -138,7 +86,6 @@ jobs:
context: updater/sis-updater-worker/
push: true
tags: toska/sis-updater-worker:latest
target: production
- name: 'Create Sentry release'
uses: getsentry/action-release@v1
env:
@@ -158,5 +105,4 @@ jobs:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment
softa-url: https://oodikone.cs.helsinki.fi
deployment-target: production
softa-to-deploy: sis-updater
@@ -1,25 +1,17 @@
name: 'Test oodikone and release to staging'
name: 'Lint and test'
on:
push:
branches:
- master
paths:
- '.github/workflows/test-oodikone.yaml'
- '.github/actions/**'
- 'cypress/**'
- 'cypress.config.json'
- 'services/**'
- '!services/backend/src/rapodiff.js'
- 'docker-compose.ci.yml'
- 'docker-compose.test.yml'
- 'nginx.conf'
- 'package*.json'
- 'updater/sis-updater-worker/src/db/migrations/**'
workflow_call:
secrets:
username:
required: true
description: 'Toska registry username'
password:
required: true
description: 'Toska registry password'
jobs:
# === Static tests / linting ===
lint_backend:
name: 'Lint backend'
runs-on: ubuntu-latest
@@ -48,7 +40,7 @@ jobs:
'eslint --ext .jsx,.js services/frontend' \
'stylelint services/frontend/src/**/*.css'
# === Build images ===
# === Build database images ===
build_backend:
name: 'Build backend'
runs-on: ubuntu-latest
@@ -61,50 +53,15 @@ jobs:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=staging
cache-from: |
type=registry,ref=toska/oodikone2-backend:staging
type=registry,ref=toska/oodikone2-backend:latest
cache-to: type=inline
context: services/backend
push: false
tags: toska/oodikone2-backend:staging
target: production
tags: oodikone-backend:staging
outputs: type=docker,dest=/tmp/backend.tar
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v3
with:
path: /tmp/backend.tar
build_frontend:
name: 'Build frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Get current time'
id: get_current_time
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
- name: 'Build image and export it'
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=staging
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
cache-from: |
type=registry,ref=toska/oodikone2-frontend:staging
type=registry,ref=toska/oodikone2-frontend:latest
cache-to: type=inline
context: services/frontend
push: false
tags: toska/oodikone2-frontend:staging
target: production
outputs: type=docker,dest=/tmp/frontend.tar
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v3
with:
path: /tmp/frontend.tar
build_and_push_anon_sis_db:
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
runs-on: ubuntu-latest
@@ -116,13 +73,13 @@ jobs:
filters: |
migrations:
- 'updater/sis-updater-worker/src/db/migrations/**'
- '.github/workflows/test-oodikone.yaml'
- '.github/workflows/test.yaml'
- name: 'Login to toska docker registry'
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: 'Let sis-updater-worker run migrations, then push new version of image to toska hub'
if: steps.check_migrations.outputs.migrations == 'true'
run: |
@@ -143,7 +100,7 @@ jobs:
filters: |
migrations:
- 'services/backend/src/database/migrations_kone/**'
- '.github/workflows/test-oodikone.yaml'
- '.github/workflows/test.yaml'
- name: 'Download images'
uses: actions/download-artifact@v3
with:
@@ -154,8 +111,8 @@ jobs:
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: 'Let backend run migrations, then push new version of image to toska hub'
if: steps.check_migrations.outputs.migrations == 'true'
run: |
@@ -176,7 +133,7 @@ jobs:
filters: |
migrations:
- 'services/backend/src/database/migrations_user/**'
- '.github/workflows/test-oodikone.yaml'
- '.github/workflows/test.yaml'
- name: 'Download images'
uses: actions/download-artifact@v3
with:
@@ -187,8 +144,8 @@ jobs:
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: 'Let backend run migrations, then push new version of image to toska hub'
if: steps.check_migrations.outputs.migrations == 'true'
run: |
@@ -200,8 +157,6 @@ jobs:
test_course_statistics:
name: 'Test course statistics'
needs:
- build_backend
- build_frontend
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -211,15 +166,13 @@ jobs:
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: cypress/e2e/Course_statistics.js
test_custom_population:
name: 'Test custom population'
needs:
- build_backend
- build_frontend
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -229,51 +182,45 @@ jobs:
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: cypress/e2e/Custom_population.js
test_population_statistics:
name: 'Test population statistics'
runs-on: ubuntu-latest
needs:
- build_backend
- build_frontend
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: cypress/e2e/Population_statistics.js
test_filters:
name: 'Test filters'
runs-on: ubuntu-latest
needs:
- build_backend
- build_frontend
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: cypress/e2e/Filters.js
test_student_statistics:
name: 'Test student statistics'
needs:
- build_backend
- build_frontend
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -283,15 +230,29 @@ jobs:
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: cypress/e2e/Student_statistics.js
test_completed_courses:
name: 'Test completed courses'
needs:
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: cypress/e2e/Completed_courses.js
test_studyprogramme_overview:
name: 'Test studyprogramme overview'
needs:
- build_backend
- build_frontend
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -301,15 +262,13 @@ jobs:
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: cypress/e2e/Studyprogramme_overview.js
test_teachers_and_users:
name: 'Test teachers and users'
needs:
- build_backend
- build_frontend
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -319,8 +278,8 @@ jobs:
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: |
cypress/e2e/Teachers.js
cypress/e2e/Users.js
@@ -328,8 +287,6 @@ jobs:
test_faculty_statistics:
name: 'Test faculty statistics'
needs:
- build_backend
- build_frontend
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -339,15 +296,13 @@ jobs:
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: cypress/e2e/Faculty_statistics.js
test_open_uni_search:
name: 'Test open uni search'
needs:
- build_backend
- build_frontend
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
@@ -357,39 +312,30 @@ jobs:
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: cypress/e2e/Openuni.js
# === Other tests ===
test_service_exit_statuses:
name: 'Check services run correctly'
needs:
- build_backend
- build_frontend
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 'Download images'
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
run: docker-compose --file docker-compose.ci.yml up --detach
- name: 'Check that exit statuses are not errors'
run: |
docker ps -a
@@ -397,101 +343,7 @@ jobs:
echo 'Some process had nonzero exit code';
exit 1;
fi
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
# === Release ===
release_images:
name: 'Release built images'
runs-on: ubuntu-latest
needs:
- lint_backend
- lint_frontend
- test_course_statistics
- test_custom_population
- test_filters
- test_population_statistics
- test_student_statistics
- test_studyprogramme_overview
- test_teachers_and_users
- test_faculty_statistics
- test_open_uni_search
- test_service_exit_statuses
steps:
- uses: actions/checkout@v3
- name: 'Download images'
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to DockerHub'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 'Create a container from frontend'
run: docker create --name frontend toska/oodikone2-frontend:staging
- name: 'Copy assets from the container'
run: docker cp frontend:/usr/src/app/build ./build
- name: 'Create Sentry release'
uses: getsentry/action-release@v1
env:
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: toska
SENTRY_PROJECT: oodikone
with:
environment: staging
set_commits: 'skip'
sourcemaps: './build/static/js'
url_prefix: '~/static/js'
version: ${{ github.sha }}
- name: 'Push to docker hub'
run: |
docker push toska/oodikone2-backend:staging
docker push toska/oodikone2-frontend:staging
- name: 'Send deployment success notification to slack'
if: success()
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment
softa-url: https://oodikone-staging.cs.helsinki.fi/
deployment-target: staging
- name: 'Send deployment failure notification to slack'
if: failure()
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment-failure
deployment-target: staging
report_test_failure:
runs-on: ubuntu-latest
name: 'Report test failure to oodikone notifications'
needs:
- lint_backend
- lint_frontend
- test_course_statistics
- test_custom_population
- test_filters
- test_population_statistics
- test_student_statistics
- test_studyprogramme_overview
- test_teachers_and_users
- test_faculty_statistics
- test_open_uni_search
- test_service_exit_statuses
if: failure()
steps:
- uses: actions/checkout@v3
- name: 'Send notification to slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: test-failure
+168
View File
@@ -0,0 +1,168 @@
name: 'Test and release Updater'
on:
push:
branches:
- master
paths:
- '.github/workflows/updater.yaml'
- 'updater/sis-updater-worker/**'
- 'updater/sis-updater-scheduler/**'
jobs:
build_worker:
name: 'Build Updater worker'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Build the image'
uses: docker/build-push-action@v3
with:
context: updater/sis-updater-worker/
push: false
outputs: type=docker,dest=/tmp/worker.tar
tags: toska/sis-updater-worker:latest
- name: 'Store built image'
uses: actions/upload-artifact@v3
with:
path: /tmp/worker.tar
build_scheduler:
name: 'Build Updater scheduler'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Build the image'
uses: docker/build-push-action@v3
with:
context: updater/sis-updater-scheduler/
push: false
outputs: type=docker,dest=/tmp/scheduler.tar
tags: toska/sis-updater-scheduler:latest
- name: 'Store built image'
uses: actions/upload-artifact@v3
with:
path: /tmp/scheduler.tar
test_updater:
name: 'Test Updater'
runs-on: ubuntu-latest
needs:
- build_worker
- build_scheduler
steps:
- uses: actions/checkout@v3
- name: 'Login to toska docker registry'
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Download built images'
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load worker image'
run: docker load --input /tmp/artifact/worker.tar
- name: 'Load scheduler image'
run: docker load --input /tmp/artifact/scheduler.tar
- name: 'Start worker'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker
- name: 'Run hourly jobs'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
SCHEDULE_IMMEDIATE: hourly
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run daily jobs'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
SCHEDULE_IMMEDIATE: daily
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run weekly jobs'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
SCHEDULE_IMMEDIATE: weekly
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run prepurge'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
SCHEDULE_IMMEDIATE: prepurge
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run purge'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
SCHEDULE_IMMEDIATE: purge
EXIT_AFTER_IMMEDIATES: yes
release_worker:
name: 'Build and release Updater worker'
runs-on: ubuntu-latest
needs: test_updater
steps:
- uses: actions/checkout@v3
- name: Build worker image
id: build-image
uses: redhat-actions/buildah-build@v2
with:
image: updater-worker
tags: staging production ${{ github.sha }}
context: updater/sis-updater-worker
containerfiles: |
updater/sis-updater-worker/Dockerfile
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-image.outputs.image }}
tags: ${{ steps.build-image.outputs.tags }}
registry: quay.io/toska
username: toska+github
password: ${{ secrets.QUAY_IO_TOKEN }}
release_scheduler:
name: 'Build and release Updater scheduler'
runs-on: ubuntu-latest
needs: test_updater
steps:
- uses: actions/checkout@v3
- name: Build scheduler image
id: build-image
uses: redhat-actions/buildah-build@v2
with:
image: updater-scheduler
tags: staging production ${{ github.sha }}
context: updater/sis-updater-scheduler
containerfiles: |
updater/sis-updater-scheduler/Dockerfile
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-image.outputs.image }}
tags: ${{ steps.build-image.outputs.tags }}
registry: quay.io/toska
username: toska+github
password: ${{ secrets.QUAY_IO_TOKEN }}
slack_notification:
name: 'Send deployment notification to Slack'
runs-on: ubuntu-latest
needs:
- release_worker
- release_scheduler
steps:
- name: 'Send notification to slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment
softa-url: https://toska-staging.cs.helsinki.fi/oodikone
deployment-target: production
softa-to-deploy: sis-updater
+1 -1
View File
@@ -7,7 +7,7 @@ module.exports = {
'{services,updater}/**/*.{js,jsx}': files => `eslint --fix ${files.join(' ')}`,
'*.{js,json,md,yml,yaml,html}': files => `prettier --write ${files.join(' ')}`,
'*.css': files => `stylelint --fix ${files.join(' ')}`,
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.7.0 hadolint ${relativeFilePaths(files)}`,
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.12.0 hadolint ${relativeFilePaths(files)}`,
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck:v0.7.2 ${relativeFilePaths(files)} -x`,
'.github/{workflows,actions}/*': files => `npm run actionlint ${relativeFilePaths(files)}`,
'docker-compose*': files => {
+1
View File
@@ -30,6 +30,7 @@ npm run cli
_Please use a terminal at least 80 characters wide, the CLI is a bit rudimentary_ 😊
What different CLI options do:
1. Set up oodikone from scratch:
+154
View File
@@ -0,0 +1,154 @@
/// <reference types="Cypress" />
const openCompletedCoursesModal = () => {
cy.get('button').contains('Search completed courses of students').click()
}
const openSearch = () => {
cy.init('/completedcoursessearch')
hasLanded()
}
const selectSavedCourselist = name => {
cy.get('[data-cy="history-search"]').children().eq(0).type(name).type('{enter}')
}
const hasLanded = () => {
cy.contains('Search completed courses')
cy.contains(
'Here you can search by a list of student and course numbers to see whether students have completed certain courses yet'
)
}
const deleteAllSearches = () => {
cy.contains('Saved courselists')
.get('.dropdown')
.then(d => {
const searchItems = d.find('div[role=option] > span[class=text]')
for (let i = 0; i < searchItems.length; i++) {
if (searchItems[i].textContent.includes('TEST-')) {
cy.get('[data-cy="history-search"]').children().eq(0).type(searchItems[i].textContent).type('{enter}')
cy.get('button').contains('Delete').click()
}
}
})
}
describe('When search modal is opened', () => {
after(() => {
cy.init('/completedcoursessearch')
hasLanded()
cy.url().should('include', '/completedcoursessearch')
cy.contains('Search completed courses of students')
cy.get('[data-cy="open-completed-courses-modal-button"]').click()
deleteAllSearches()
})
it('Modal opens correctly', () => {
cy.init('/completedcoursessearch')
hasLanded()
openCompletedCoursesModal()
cy.contains('Search completed courses of students')
cy.contains('Insert one or more student numbers, separated by a space, a newline, or a comma.')
cy.contains('Insert one or more courses, separated by a space, a newline, or a comma.')
cy.contains('Insert name for this course list if you wish to save it')
})
describe('When a search is executed with invalid input', () => {
it('Returns empty table and notification', () => {
cy.init('/completedcoursessearch')
hasLanded()
openCompletedCoursesModal()
cy.get('[data-cy="student-no-input"]').type('1')
cy.get('[data-cy="course-list-input"]').type('1')
cy.get('[data-cy="completed-courses-search-button"]').click()
cy.get('[data-cy="rights-notification"]').should(
'contain.text',
'The following students information could not be displayed'
)
cy.get('[data-cy="rights-notification"]').should('contain.text', '1')
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', 'Completed courses search')
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', 'Student Number')
cy.get('[data-cy="completed-courses-table-div"]').should('not.contain', '1')
})
})
describe('When a search with correct data is executed', () => {
before(() => {
cy.init('/completedcoursessearch')
hasLanded()
openCompletedCoursesModal()
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
cy.get('[data-cy="student-no-input"]').type(studentSet1.join('\n'))
cy.get('[data-cy="course-list-input"]').type(coursesSet1.join('\n'))
cy.get('[data-cy="completed-courses-search-button"]').click()
})
})
it('Pushes the query to url', () => {
cy.url().should(
'include',
'/completedcoursessearch?courseList=TKT10001&courseList=TKT10002&courseList=TKT10003&courseList=TKT10004&studentList=010450938&studentList=010589388&studentList=010614509'
)
})
it('Finds correct students and courses', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(5000)
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[0])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[1])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[2])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[0])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[1])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[2])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[3])
})
})
})
})
const courseListNames = ['TEST-courselist1', 'TEST-courselist2']
describe('Courselist saving-related functions work', () => {
beforeEach(() => {
openSearch()
openCompletedCoursesModal()
cy.get('[data-cy="course-list-input"]').type('CSM14204\nTKT10004')
cy.get('[data-cy="search-name"]').type(courseListNames[0])
cy.get('button').contains('Save').click()
})
it('Course list can be saved', () => {
selectSavedCourselist(courseListNames[0])
openSearch()
openCompletedCoursesModal()
selectSavedCourselist(courseListNames[0])
cy.get('[data-cy="course-list-input"]').contains('TKT10004').contains('CSM14204')
})
it('Course list can be deleted', () => {
openSearch()
openCompletedCoursesModal()
cy.get('[data-cy="search-name"]').type(courseListNames[1])
cy.get('button').contains('Save').click()
openSearch()
openCompletedCoursesModal()
selectSavedCourselist(courseListNames[1])
cy.get('button').contains('Delete').click()
cy.get('[data-cy="history-search"]')
.type(courseListNames[1])
.type('{enter}')
.should('not.contain', courseListNames[1])
})
it('Course list can be updated', () => {
openSearch()
openCompletedCoursesModal()
selectSavedCourselist(courseListNames[0])
cy.get('[data-cy="course-list-input"]').type(`,TKT10001`)
cy.get('button').contains('Save').click()
openSearch()
openCompletedCoursesModal()
selectSavedCourselist(courseListNames[0])
cy.get('[data-cy="course-list-input"]').contains(`TKT10001`)
})
})
+10 -4
View File
@@ -1,3 +1,4 @@
/* eslint-disable cypress/no-unnecessary-waiting */
/// <reference types="Cypress" />
describe('Course Statistics tests', () => {
@@ -166,12 +167,17 @@ describe('Course Statistics tests', () => {
it('Population of course shows grades for each student', () => {
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
cy.contains(/^TKT20001, AYTKT20001$/).click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.wait(2000)
cy.contains(/^TKT20001, AYTKT20001$/).click({ force: true })
cy.wait(2000)
cy.contains('Fetch statistics').should('be.enabled').click({ force: true })
cy.wait(2000)
cy.contains('TKT20001, 58131, AYTKT20001 Tietorakenteet ja algoritmit')
cy.get('tbody >:nth-child(3) > :nth-child(2) .level').click()
cy.get('tbody >:nth-child(3) > :nth-child(2) .level').click({ force: true })
cy.wait(2000)
cy.contains('Population of course Tietorakenteet ja algoritmit 2018-2019 (open and normal)')
cy.contains('Students (204)').click()
cy.contains('Students (204)').click({ force: true })
cy.wait(2000)
cy.contains('td', '010262566').siblings().contains('4')
cy.contains('td', '010674989').siblings().contains('1')
})
+11 -12
View File
@@ -269,23 +269,22 @@ describe('Faculty overview', () => {
})
it('Progress tables can be toggled', () => {
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')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-5"]').should('not.be.visible')
cy.get('[data-cy="Button-Show-FacultyBachelorsProgressTable-5"]').click()
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('not.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-5"]').should('be.visible')
cy.get('[data-cy="Button-Hide-FacultyBachelorsProgressTable-5"]').click()
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-5"]').should('not.be.visible')
})
it('Sudent statstable can be toggled', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
cy.get('[data-cy="Button-FacultyStudentStatsTable-3"]').click()
cy.get('[data-cy="Button-FacultyStudentStatsTable-2"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', 'TKT')
cy.get('[data-cy="Button-FacultyStudentStatsTable-3"]').click()
cy.get('[data-cy="Button-FacultyStudentStatsTable-2"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
})
+10 -3
View File
@@ -1,3 +1,4 @@
/* eslint-disable cypress/no-unnecessary-waiting */
/// <reference types="Cypress" />
// const baseUrl = Cypress.config().baseUrl
@@ -20,6 +21,7 @@ const clearSingleDropdownSelection = dataCyAttribute => {
}
const testRangeFilter = (parentEl, min, max, expected) => {
cy.clock(MOCKED_DATE, ['Date'])
cy.cs(parentEl)
.cs('range-selector-min')
.find('input')
@@ -49,8 +51,6 @@ const createRunTestStepWithPreAndPostPartsFunction = amountWithoutFiltering => {
const card = cy.cs(`${filterName}-filter-card`)
card.invoke('attr', 'data-open').then(open => {
console.log('open', open)
const getHeader = () => cy.cs(`${filterName}-header`)
if (open === 'false') {
@@ -110,6 +110,7 @@ describe('Population Statistics', () => {
checkFilteringResult(defaultAmountOfStudents - graduated)
getCard().cs('option-all').click()
})
cy.cs('reset-all-filters').click()
})
it('Transfer filter works', () => {
@@ -121,12 +122,13 @@ describe('Population Statistics', () => {
checkFilteringResult(defaultAmountOfStudents + transferred)
cy.cs('TransferredToProgramme-filter-card').cs('option-havenot').click()
})
cy.cs('reset-all-filters').click()
})
it('Enrollment filter works', () => {
runTestStepWithPreAndPostParts('EnrollmentStatus', () => {
cy.cs('enrollmentStatusFilter-status').selectFromDropdown(0)
cy.cs('enrollmentStatusFilter-semesters').selectFromDropdown(0)
cy.cs('enrollmentStatusFilter-semesters').selectFromDropdown(5)
checkFilteringResult(148)
clearSemanticUIMultipleDropDownSelection('enrollmentStatusFilter-semesters')
})
@@ -136,6 +138,7 @@ describe('Population Statistics', () => {
runTestStepWithPreAndPostParts('CreditsEarned', () => {
testRangeFilter('CreditsEarned-filter-card', 50, 150, 118)
})
cy.cs('reset-all-filters').click()
})
it('Age filter works', () => {
@@ -146,6 +149,7 @@ describe('Population Statistics', () => {
})
it('Gender filter works', () => {
cy.cs('reset-all-filters').click()
runTestStepWithPreAndPostParts('Gender', () => {
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
checkFilteringResult(42)
@@ -263,12 +267,14 @@ describe('Course Statistics', () => {
it('Age filter works', () => {
cy.clock(MOCKED_DATE, ['Date'])
cy.cs('reset-all-filters').click()
runTestStepWithPreAndPostParts('Age', () => {
testRangeFilter('Age-filter-card', 20, 40, 33)
})
})
it('Gender filter works', () => {
cy.cs('reset-all-filters').click()
runTestStepWithPreAndPostParts('Gender', () => {
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
checkFilteringResult(27)
@@ -325,6 +331,7 @@ describe('Custom Population Statistics', () => {
runTestStepWithPreAndPostParts('Age', () => {
testRangeFilter('Age-filter-card', 20, 30, 1)
})
cy.cs('reset-all-filters').click()
})
it('Programme defaults to "Active Study Right" mode', () => {
+1 -1
View File
@@ -78,7 +78,7 @@ describe('Student Statistics tests', () => {
cy.contains('td a', student.studentnumber).click()
cy.get('[data-cy=sisulink] > a')
.should('have.attr', 'href')
.and('include', 'https://sis-helsinki.funidata.fi/tutor/role/staff/student/hy-hlo-84830887/basic/basic-info')
.and('include', 'https://sisu.helsinki.fi/tutor/role/staff/student/hy-hlo-84830887/basic/basic-info')
})
it('Searching with bad inputs doesnt yield results', () => {
+55 -36
View File
@@ -8,9 +8,11 @@ const deleteTag = name => {
}
const getEmptyYears = isAcademicYear => {
const thisYear = new Date().getFullYear()
const today = new Date()
const latestYear = isAcademicYear && today.getMonth() < 7 ? today.getFullYear() - 1 : today.getFullYear()
const years = []
for (let year = thisYear; year > 2021; year--) {
for (let year = latestYear; year > 2021; year--) {
if (isAcademicYear) {
years.push(`${year} - ${year + 1}`)
} else {
@@ -21,6 +23,20 @@ const getEmptyYears = isAcademicYear => {
}
describe('Studyprogramme overview', () => {
before(() => {
cy.init('/study-programme', 'admin')
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click({ force: true })
cy.get('.attached').contains('Update statistics').click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(5000)
cy.get('button').eq(0).click({ force: true })
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.get('button').eq(1).click({ force: true })
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
})
/* Basic information overview -tests*/
describe('Basic information -view works for basic user', () => {
beforeEach(() => {
@@ -85,17 +101,19 @@ describe('Studyprogramme overview', () => {
cy.checkTableStats(tableContents, 'GraduatedAndThesisWritersOfTheProgramme')
})
it.skip('Special studyrights can be excluded and basic data changes accordingly', () => {
it('Special studyrights can be excluded and basic data changes accordingly', () => {
cy.get('[data-cy=StudentToggle]').click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(15000)
const years = getEmptyYears()
const studentTableContents = [
// [Year, Started, Graduated]
...years.map(year => [year, 0, 0]),
[2021, 0, 0],
[2020, 22, 0],
[2019, 12, 0],
[2018, 0, 0],
[2017, 0, 0],
[2020, 10, 35],
[2019, 85, 17],
[2018, 159, 1],
[2017, 170, 0],
]
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
@@ -104,10 +122,10 @@ describe('Studyprogramme overview', () => {
// [Year, Total, Major students credits, Transferred credits]
...years.map(year => [year, 0, 0, 0]),
[2021, 0, 0, 0],
[2020, 117.5, 0, 7.5],
[2019, 67, 0, 0],
[2018, 0, 0, 0],
[2017, 0, 0, 0],
[2020, 4999, 4675, 167],
[2019, 6182, 5699, 349],
[2018, 4884, 4537, 272],
[2017, 1988, 1600, 388],
]
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
@@ -116,9 +134,9 @@ describe('Studyprogramme overview', () => {
// [Year, Graduated, Wrote thesis]
...years.map(year => [year, 0, 0]),
[2021, 0, 0],
[2020, 0, 0],
[2019, 0, 0],
[2018, 0, 0],
[2020, 35, 35],
[2019, 17, 25],
[2018, 1, 5],
[2017, 0, 0],
]
@@ -127,8 +145,9 @@ describe('Studyprogramme overview', () => {
})
it('Year can be changed to academic year, and data changes accordingly', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.get('[data-cy=YearToggle]').click({ force: true })
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(15000)
const isAcademicYear = true
const years = getEmptyYears(isAcademicYear)
const studentTableContents = [
@@ -169,8 +188,8 @@ describe('Studyprogramme overview', () => {
.should('contain', 'Major students credits')
.should('contain', 'Non-major students credits')
.should('contain', 'Transferred credits')
.should('contain', 852)
.should('contain', 577)
.should('contain', 7737)
.should('contain', 6584)
cy.get('[data-cy=Graph-GraduatedAndThesisWritersOfTheProgramme')
.should('contain', 'Graduated students')
@@ -181,9 +200,9 @@ describe('Studyprogramme overview', () => {
.should('contain', 25)
.should('contain', 35)
cy.get('[data-cy=graduation-times-graph-breakdown]')
cy.get('[data-cy=graduation-times-graph-breakdownBachelor]')
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graph]').within(() => {
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
cy.contains('Graduation year')
cy.contains('2019')
cy.contains('17 graduated').trigger('mouseover')
@@ -251,12 +270,12 @@ describe('Studyprogramme overview', () => {
it('Students of the studyprogramme are shown correctly', () => {
const tableContents = [
// [Year, All, Started studying, Currently enrolled, Absent, Cancelled, Graduated, Men, Women, Finnish]
['2020 - 2021', 12, 10, 0, 0, 0, 0, 10, 2, 12],
['2019 - 2020', 86, 86, 0, 0, 1, 1, 65, 21, 86],
['2018 - 2019', 162, 161, 0, 0, 0, 11, 118, 44, 161],
['2017 - 2018', 171, 171, 0, 0, 4, 42, 135, 36, 166],
['Total', 431, 428, 0, 0, 5, 54, 328, 103, 425],
// [Year, All, Started studying, Currently enrolled, Absent, Inactive, Graduated, Men, Women, Finnish]
['2020 - 2021', 12, 10, 0, 0, 0, 0, 10, 2, 0, 12, 0],
['2019 - 2020', 86, 85, 0, 0, 0, 1, 65, 21, 0, 86, 0],
['2018 - 2019', 162, 159, 0, 0, 0, 11, 118, 44, 0, 161, 1],
['2017 - 2018', 171, 170, 0, 0, 0, 42, 135, 36, 0, 166, 5],
['Total', 431, 424, 0, 0, 0, 54, 328, 103, 0, 425, 6],
]
cy.checkTableStats(tableContents, 'StudytrackOverview')
@@ -265,10 +284,10 @@ describe('Studyprogramme overview', () => {
it('Student progress data is shown correctly', () => {
const tableContents = [
['2020 - 2021', 12, 12, 0, 0, 0, 0, 0, 0, 0],
['2019 - 2020', 86, 11, 15, 35, 20, 5, 0, 0, 0],
['2018 - 2019', 162, 3, 9, 28, 50, 33, 25, 11, 3],
['2017 - 2018', 171, 7, 13, 22, 32, 18, 23, 32, 24],
['Total', 431, 33, 37, 85, 102, 56, 48, 43, 27],
['2019 - 2020', 86, 10, 16, 34, 21, 5, 0, 0, 0],
['2018 - 2019', 162, 2, 8, 27, 47, 36, 26, 12, 4],
['2017 - 2018', 171, 5, 15, 21, 26, 22, 22, 33, 27],
['Total', 431, 29, 39, 82, 94, 63, 48, 45, 31],
]
cy.checkTableStats(tableContents, 'StudytrackProgress')
@@ -279,14 +298,14 @@ describe('Studyprogramme overview', () => {
.should('contain', 'Less than 15 credits')
.should('contain', '30-59 credits')
.should('contain', '180 or more credits')
.should('contain', '4.1%') // The percentage for less than 15 credits in 2017-2018, to check that the graph renders
.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
.should('contain', '2.9%') // The percentage for less than 15 credits in 2017-2018, to check that the graph renders
.should('contain', '4.9%') // The percentage for 15-29 credits in 2018-2019
.should('contain', '11.6%') // The percentage for less than 15 credits in 2019-2020
cy.get('[data-cy=graduation-times-graph-breakdown]')
cy.get('[data-cy=graduation-times-graph-breakdownBachelor]')
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graph]').within(() => {
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
cy.contains('Start year')
cy.contains('2019 - 2020')
cy.contains('42 graduated').trigger('mouseover')
@@ -353,7 +372,7 @@ describe('Studyprogramme overview', () => {
cy.get('[data-cy=CoursesSortableTable]').within(() => {
// Test sorting by different columns
cy.get('th').eq(0).click()
cy.get('tr').eq(2).contains('Tietojenkäsittelytieteen kisälliopetus: Java-ohjelmointi')
cy.get('tr').eq(2).contains('Tietojenkäsittelytieteen kisälliopetus')
cy.get('th').eq(1).click()
cy.get('tr').eq(1).contains('Äidinkielinen viestintä')
cy.get('th').eq(2).click()
@@ -369,7 +388,7 @@ describe('Studyprogramme overview', () => {
cy.get('[data-cy=creditsStudentsToggle]').first().click()
cy.get('[data-cy=CoursesSortableTable]').within(() => {
cy.get('tr').eq(1).contains('267')
cy.get('tr').eq(2).contains('267')
})
})
})
@@ -0,0 +1,4 @@
{
"studentSet1": ["010450938", "010589388", "010614509"],
"coursesSet1": ["TKT10001", "TKT10002", "TKT10003", "TKT10004"]
}
+10 -6
View File
@@ -19,12 +19,16 @@ services:
SECRET_TOKEN: dev
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
image: toska/oodikone2-backend:staging
IMPORTER_DB_API_URL: 'https://importer.cs.helsinki.fi/api/importer'
JAMI_URL: 'http://jami:3003/'
build:
context: services/backend
frontend:
<<: *defaults
container_name: frontend
image: toska/oodikone2-frontend:staging
build:
context: services/frontend
kone-db:
<<: *defaults
@@ -37,8 +41,8 @@ services:
redis:
<<: *defaults
command: 'redis-server --appendonly yes'
container_name: redis
command: >
sh -c 'redis-server --appendonly yes && redis-cli flushall'
image: redis
ports:
- 6379:6379
@@ -108,7 +112,6 @@ services:
sis-updater-worker:
<<: *defaults
build:
target: development
context: updater/sis-updater-worker
depends_on:
- redis
@@ -116,6 +119,7 @@ services:
- sis-importer-db
- sis-updater-nats
environment:
CI: 'true'
DB_URL: postgres://postgres@sis-db:5432/sis-db
REDIS_URI: //redis:6379
SIS_IMPORTER_DATABASE: sis-importer-db
@@ -130,7 +134,6 @@ services:
sis-updater-scheduler:
<<: *defaults
build:
target: development
context: updater/sis-updater-scheduler
container_name: sis-updater-scheduler
depends_on:
@@ -138,6 +141,7 @@ services:
- sis-importer-db
- sis-updater-nats
environment:
CI: 'true'
REDIS_URI: //redis:6379
SECRET_TOKEN: dev
SIS_IMPORTER_DATABASE: sis-importer-db
+11 -9
View File
@@ -4,7 +4,7 @@ x-defaults: &defaults
restart: unless-stopped
x-build_defaults: &build_defaults
target: development
dockerfile: dev.Dockerfile
services:
adminer:
@@ -39,13 +39,15 @@ services:
SECRET_TOKEN: dev
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
IMPORTER_DB_API_URL: 'https://importer.cs.helsinki.fi/api/importer'
JAMI_URL: 'http://jami:3003/'
ports:
- 8080:8080
profiles:
- oodikone
volumes:
- backend-nodemod:/usr/src/app/node_modules
- ./services/backend/:/usr/src/app
- backend-nodemod:/opt/app-root/src/node_modules
- ./services/backend/:/opt/app-root/src
frontend:
<<: *defaults
@@ -58,8 +60,8 @@ services:
profiles:
- oodikone
volumes:
- frontend-nodemod:/usr/src/app/node_modules
- ./services/frontend/:/usr/src/app
- frontend-nodemod:/opt/app-root/src/node_modules
- ./services/frontend/:/opt/app-root/src
kone-db:
<<: *defaults
@@ -131,8 +133,8 @@ services:
profiles:
- updater
volumes:
- sis-updater-scheduler-nodemod:/usr/src/app/node_modules
- ./updater/sis-updater-scheduler/:/usr/src/app
- sis-updater-scheduler-nodemod:/opt/app-root/src/node_modules
- ./updater/sis-updater-scheduler/:/opt/app-root/src
sis-updater-worker:
<<: *defaults
@@ -156,8 +158,8 @@ services:
profiles:
- updater
volumes:
- sis-updater-worker-nodemod:/usr/src/app/node_modules
- ./updater/sis-updater-worker/:/usr/src/app
- sis-updater-worker-nodemod:/opt/app-root/src/node_modules
- ./updater/sis-updater-worker/:/opt/app-root/src
user-db:
<<: *defaults
+2 -1
View File
@@ -16,8 +16,9 @@
"both": "./run.sh both anon up --detach",
"both:real": "./run.sh both real up --detach",
"docker:down": "./run.sh both anon down --remove-orphans; ./run.sh both real down --remove-orphans",
"flushredis": "docker exec -it redis redis-cli flushall",
"prepare": "husky install",
"rapodiff": "docker exec backend node /usr/src/app/src/rapodiff.js"
"rapodiff": "docker exec backend node /opt/app-root/src/src/rapodiff.js"
},
"repository": {
"type": "git",
+18 -14
View File
@@ -1,23 +1,27 @@
FROM node:14.17-alpine3.13 as base
FROM registry.access.redhat.com/ubi8/nodejs-14-minimal
ENV TZ="Europe/Helsinki"
WORKDIR /opt/app-root/src
WORKDIR /usr/src/app
EXPOSE 8080
FROM base as development
ENV NODE_ENV=development
COPY ./package* /usr/src/app/
RUN npm ci
COPY . /usr/src/app/
CMD ["npm", "run", "dev"]
FROM base as production
ENV NODE_ENV=production
ARG BASE_PATH
ENV PUBLIC_URL=$BASE_PATH
ARG STAGING
ENV REACT_APP_STAGING=$STAGING
ARG SENTRY_ENVIRONMENT
ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
ARG SENTRY_RELEASE
ENV SENTRY_RELEASE=$SENTRY_RELEASE
COPY --chown=node:node ./package* /usr/src/app/
RUN npm ci --production && apk add --no-cache vim curl nano
COPY --chown=node:node . /usr/src/app/
USER node
COPY ./package* ./
RUN npm ci --omit-dev
COPY . .
CMD ["npm", "start"]
+15
View File
@@ -0,0 +1,15 @@
FROM node:14.17-alpine3.13
ENV TZ="Europe/Helsinki"
WORKDIR /opt/app-root/src
EXPOSE 8080
ENV NODE_ENV=development
COPY ./package* ./
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]
+1 -1
View File
@@ -4,7 +4,7 @@
"scripts": {
"start": "node --max_old_space_size=4096 index.js",
"dev": "nodemon --max_old_space_size=4096 index.js",
"rapodiff": "node /usr/src/app/src/rapodiff.js"
"rapodiff": "node /opt/app-root/src/src/rapodiff.js"
},
"dependencies": {
"@sentry/node": "^5.30.0",
+5 -3
View File
@@ -1,7 +1,7 @@
// Node env to use
const isStaging = process.env.NODE_ENV === 'staging'
const isDev = process.env.NODE_ENV === 'development'
const isProduction = process.env.NODE_ENV === 'production'
const isStaging = process.env.REACT_APP_STAGING === 'true'
const isProduction = !isStaging && process.env.NODE_ENV === 'production'
// Sentry
const sentryRelease = process.env.SENTRY_RELEASE || ''
@@ -17,9 +17,10 @@ const requiredGroup = isStaging
const pateToken = process.env.PATE_API_TOKEN || ''
// Jami
const jamiUrl = isDev || runningInCI ? 'http://jami:3003/' : 'https://importer.cs.helsinki.fi/api/auth'
const jamiUrl = process.env.JAMI_URL || ''
// Importer client
const importerUrl = process.env.IMPORTER_DB_API_URL || ''
const importerToken = process.env.IMPORTER_DB_API_TOKEN || ''
// Networking: Urls & ports
@@ -86,5 +87,6 @@ module.exports = {
baseUrl,
frontUrl,
backendPort,
importerUrl,
importerToken,
}
@@ -0,0 +1,23 @@
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('progress_criteria', 'courses_year_four', {
type: Sequelize.ARRAY(Sequelize.STRING),
})
await queryInterface.addColumn('progress_criteria', 'courses_year_five', {
type: Sequelize.ARRAY(Sequelize.STRING),
})
await queryInterface.addColumn('progress_criteria', 'courses_year_six', {
type: Sequelize.ARRAY(Sequelize.STRING),
})
await queryInterface.addColumn('progress_criteria', 'credits_year_four', {
type: Sequelize.INTEGER,
})
await queryInterface.addColumn('progress_criteria', 'credits_year_five', {
type: Sequelize.INTEGER,
})
await queryInterface.addColumn('progress_criteria', 'credits_year_six', {
type: Sequelize.INTEGER,
})
},
down: async () => {},
}
+16 -2
View File
@@ -6,6 +6,7 @@ const { getAllProgrammes } = require('./services/studyrights')
const { updateBasicView, updateStudytrackView } = require('./services/studyprogrammeUpdates')
const { findAndSaveTeachers } = require('./services/topteachers')
const { faculties } = require('./services/organisations')
const { combinedStudyprogrammes } = require('./services/studyprogrammeHelpers')
const { updateFacultyOverview, updateFacultyProgressOverview } = require('./services/faculty/facultyUpdates')
const { isProduction } = require('./conf-backend')
const { getCurrentSemester } = require('./services/semesters')
@@ -50,13 +51,26 @@ const refreshNewOverviews = async () => {
)
let ready = 0
for (const code of codes) {
let combinedProgramme = ''
// Update only the bachelor programme
try {
await updateBasicView(code)
await updateStudytrackView(code)
await updateBasicView(code, combinedProgramme)
await updateStudytrackView(code, combinedProgramme)
ready += 1
} catch (e) {
logger.error({ message: `Failed to update overview stats for programme ${code}`, meta: e })
}
combinedProgramme = combinedStudyprogrammes[code] || ''
// If bachelor programme is combined with master programme, then update the combination.
if (combinedProgramme) {
try {
await updateBasicView(code, combinedProgramme)
await updateStudytrackView(code, combinedProgramme)
ready += 1
} catch (e) {
logger.error({ message: `Failed to update overview stats for programme ${code}`, meta: e })
}
}
logger.info(`${ready}/${codes.length} programmes done`)
}
logger.info('Studyprogramme and studytrack overview stats refreshed!')
@@ -28,7 +28,7 @@ const currentUserMiddleware = async (req, _res, next) => {
}
const iamGroups = parseIamGroups(hygroupcn)
const { access = {}, specialGroup = {} } = await getOrganizationAccess({ iamGroups })
const { access = {}, specialGroup = {} } = await getOrganizationAccess({ id: sisId, iamGroups })
const iamRights = Object.keys(access)
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
+3
View File
@@ -67,6 +67,9 @@ Studyright.belongsTo(Organization, { foreignKey: 'facultyCode', targetKey: 'code
Studyplan.belongsTo(Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
Student.hasMany(Studyplan, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
Studyplan.belongsTo(Studyright, { foreignKey: 'studyrightid', targetKey: 'studyrightid' })
Studyright.hasMany(Studyplan, { foreignKey: 'studyrightid', sourceKey: 'studyrightid' })
Studyright.belongsTo(Student, { foreignKey: 'studentStudentnumber', targetKey: 'studentnumber' })
Student.hasMany(Studyright, { foreignKey: 'studentStudentnumber', sourceKey: 'studentnumber' })
+21 -42
View File
@@ -33,18 +33,6 @@ const ExcludedCourse = sequelizeKone.define(
}
)
const MandatoryCourse = sequelizeKone.define('mandatory_courses', {
course_code: {
type: Sequelize.STRING,
},
studyprogramme_id: {
type: Sequelize.STRING,
},
label: {
type: Sequelize.BIGINT,
},
})
const TagStudent = sequelizeKone.define(
'tag_student',
{
@@ -88,23 +76,6 @@ const Tag = sequelizeKone.define(
}
)
const MandatoryCourseLabels = sequelizeKone.define('mandatory_course_labels', {
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
studyprogramme_id: {
type: Sequelize.STRING,
},
label: {
type: Sequelize.STRING,
},
orderNumber: {
type: Sequelize.INTEGER,
},
})
const CustomPopulationSearch = sequelizeKone.define('custom_population_searches', {
id: {
primaryKey: true,
@@ -122,6 +93,7 @@ const CustomPopulationSearch = sequelizeKone.define('custom_population_searches'
},
})
// also used in completed courses search feature
const OpenUniPopulationSearch = sequelizeKone.define(
'open_uni_population_searches',
{
@@ -144,7 +116,7 @@ const OpenUniPopulationSearch = sequelizeKone.define(
{ underscored: true, timestamps: true }
)
const StudyProgrammeCriteria = sequelizeKone.define(
const ProgressCriteria = sequelizeKone.define(
'progress_criteria',
{
code: {
@@ -160,6 +132,15 @@ const StudyProgrammeCriteria = sequelizeKone.define(
coursesYearThree: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
coursesYearFour: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
coursesYearFive: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
coursesYearSix: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
creditsYearOne: {
type: Sequelize.INTEGER,
},
@@ -169,6 +150,15 @@ const StudyProgrammeCriteria = sequelizeKone.define(
creditsYearThree: {
type: Sequelize.INTEGER,
},
creditsYearFour: {
type: Sequelize.INTEGER,
},
creditsYearFive: {
type: Sequelize.INTEGER,
},
creditsYearSix: {
type: Sequelize.INTEGER,
},
},
{ underscored: true, timestamps: true }
)
@@ -195,28 +185,17 @@ const StudyGuidanceGroupTag = sequelizeKone.define(
{ underscored: true, timestamps: true }
)
MandatoryCourse.belongsTo(MandatoryCourseLabels, {
foreignKey: 'label',
sourceKey: 'id',
})
MandatoryCourseLabels.hasMany(MandatoryCourse, {
foreignKey: 'label',
sourceKey: 'id',
})
TagStudent.belongsTo(Tag, { foreignKey: 'tag_id', sourceKey: 'tag_id' })
Tag.hasMany(TagStudent, { foreignKey: 'tag_id', sourceKey: 'tag_id' })
module.exports = {
MandatoryCourse,
TagStudent,
Tag,
ThesisTypeEnums,
MandatoryCourseLabels,
CustomPopulationSearch,
OpenUniPopulationSearch,
sequelizeKone,
ExcludedCourse,
StudyGuidanceGroupTag,
StudyProgrammeCriteria,
ProgressCriteria,
}
+1 -2
View File
@@ -1,4 +1,4 @@
const { Model, STRING, DATE, INTEGER, BOOLEAN } = require('sequelize')
const { Model, STRING, DATE, INTEGER } = require('sequelize')
const { dbConnections } = require('../database/connection')
class Student extends Model {}
@@ -27,7 +27,6 @@ Student.init(
home_country_en: { type: STRING },
gender_code: { type: STRING },
sis_person_id: { type: STRING },
dissemination_info_allowed: { type: BOOLEAN },
createdAt: {
type: DATE,
},
+13
View File
@@ -20,6 +20,16 @@ Studyplan.init(
onDelete: 'cascade',
allowNull: false,
},
studyrightid: {
type: STRING,
references: {
model: 'studyright',
key: 'studyrightid',
},
onUpdate: 'cascade',
onDelete: 'cascade',
allowNull: false,
},
programme_code: {
type: STRING,
allowNull: false,
@@ -30,6 +40,9 @@ Studyplan.init(
completed_credits: {
type: INTEGER,
},
sisu_id: {
type: STRING,
},
createdAt: {
type: DATE,
},
+4 -2
View File
@@ -1,4 +1,4 @@
const { Model, STRING, DATE, INTEGER } = require('sequelize')
const { Model, STRING, DATE, INTEGER, BOOLEAN } = require('sequelize')
const { dbConnections } = require('../database/connection')
class Studyright extends Model {}
@@ -27,7 +27,9 @@ Studyright.init(
active: {
type: INTEGER,
},
// irtisanomisperuste
cancelled: {
type: BOOLEAN,
},
studentStudentnumber: {
type: STRING,
references: {
+2 -6
View File
@@ -7,7 +7,6 @@ const matomoInit = require('./routes/matomo-init')
const currentUserMiddleware = require('./middleware/currentUserMiddleware')
const accessLogger = require('./middleware/accessLogger')
const auth = require('./middleware/auth')
const courses = require('./routes/courses')
const students = require('./routes/students')
const population = require('./routes/population')
@@ -19,8 +18,6 @@ const teachers = require('./routes/teachers')
const providers = require('./routes/providers')
const faculties = require('./routes/faculties')
const semesters = require('./routes/semesters')
const mandatoryCourses = require('./routes/mandatorycourses')
const mandatoryCourseLabels = require('./routes/mandatorycourselabels')
const feedback = require('./routes/feedback')
const tags = require('./routes/tags')
const updater = require('./routes/updater')
@@ -33,7 +30,7 @@ const studyProgramme = require('./routes/studyProgramme')
const customOpenUniSearch = require('./routes/customOpenUniSearch')
const studyProgrammeCriteria = require('./routes/studyProgrammeCriteria')
const initializeSentry = require('./util/sentry')
const completedCoursesSearch = require('./routes/completedCoursesSearch')
const errorMiddleware = require('./middleware/errorMiddleware')
module.exports = (app, url) => {
@@ -61,13 +58,12 @@ module.exports = (app, url) => {
app.use(url, studyProgramme)
app.use(`${url}/programmecriteria`, studyProgrammeCriteria)
app.use(`${url}/openunisearch`, auth.roles(['openUniSearch']), customOpenUniSearch)
app.use(`${url}/completedcoursessearch`, completedCoursesSearch)
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
app.use(`${url}/updater`, auth.roles(['admin']), updater)
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
app.use(`${url}/users`, auth.roles(['admin']), users)
app.use(`${url}/feedback`, feedback)
app.use(`${url}/mandatory_courses`, mandatoryCourses)
app.use(`${url}/mandatory-course-labels`, mandatoryCourseLabels)
app.use(`${url}/tsa`, tsaAnalytics)
app.use(`${url}/custom-population-search`, customPopulationSearch)
app.use(`${url}/cool-data-science`, trends)
@@ -0,0 +1,82 @@
const router = require('express').Router()
const { getCompletedCourses } = require('../services/completedCoursesSearch')
const _ = require('lodash')
const {
getOpenUniSearches,
createNewSearch,
deleteSearch,
updateSearch,
} = require('../services/openUni/openUniManageSearches')
router.get('/', async (req, res) => {
const studentNumbers = JSON.parse(req.query?.studentlist) || []
const courseCodes = JSON.parse(req.query?.courselist) || []
const {
user: { isAdmin, studentsUserCanAccess },
} = req
if (!Array.isArray(studentNumbers)) return res.status(400).json({ error: 'Student numbers must be of type array' })
if (!Array.isArray(courseCodes)) return res.status(400).json({ error: 'Courses must be of type array' })
const filteredStudentNumbers = isAdmin ? studentNumbers : _.intersection(studentNumbers, studentsUserCanAccess)
const completedCourses = await getCompletedCourses(filteredStudentNumbers, courseCodes)
const discardedStudentNumbers = _.difference(
studentNumbers,
completedCourses.students.map(s => s.studentNumber)
)
return res.json({ discardedStudentNumbers, ...completedCourses })
})
router.get('/searches', async (req, res) => {
const userId = req.user.id
const foundSearches = await getOpenUniSearches(userId)
return res.json(foundSearches)
})
router.post('/searches', async (req, res) => {
const courseCodes = req.body?.courselist || []
const userId = req.user.id
const name = req.body?.name
if (!name) return res.status(400).json({ error: 'Courselist name missing' })
if (courseCodes && !Array.isArray(courseCodes))
return res.status(400).json({ error: 'Course codes must be type of array' })
const createdSearch = await createNewSearch(userId, name, courseCodes)
if (!createdSearch) return res.status(400).json({ error: 'Failed to create courselist' })
return res.status(201).json({
id: createdSearch.id,
userId: createdSearch.userId,
name: createdSearch.name,
courseList: createdSearch.courseCodes,
updatedAt: createdSearch.updatedAt,
})
})
router.put('/searches/:id', async (req, res) => {
const id = req.params?.id
const courseCodes = req.body?.courselist || []
const userId = req.user.id
if (!id || !userId) return res.status(422).end()
const updatedSearch = await updateSearch(userId, id, courseCodes)
if (!updatedSearch) return res.status(404).json({ error: 'Courselist could not be found' })
return res.json({
id: updatedSearch.id,
userId: updatedSearch.userId,
name: updatedSearch.name,
courseList: updatedSearch.courseCodes,
updatedAt: updatedSearch.updatedAt,
})
})
router.delete('/searches/:id', async (req, res) => {
const id = req.params?.id
const userId = req.user.id
if (!id || !userId) return res.status(422).end()
const deletedSearch = await deleteSearch(userId, id)
if (!deletedSearch) return res.status(404).json({ error: 'Courselist could not be found' })
return res.json(id)
})
module.exports = router
@@ -1,5 +1,4 @@
const router = require('express').Router()
const MandatoryCourses = require('../services/mandatoryCourses')
const { getAllProgrammes, getAllElementDetails } = require('../services/studyrights')
router.get('/elementdetails/all', async (req, res) => {
@@ -12,13 +11,4 @@ router.get('/studyprogrammes', async (req, res) => {
res.json(studyrights)
})
router.get('/v2/studyprogrammes/:id/mandatory_courses', async (req, res) => {
if (req.params.id) {
const codes = await MandatoryCourses.byStudyprogramme(req.params.id)
res.json(codes)
} else {
res.status(422).end()
}
})
module.exports = router
+3 -4
View File
@@ -84,11 +84,10 @@ router.get('/:id/basicstats', async (req, res) => {
router.get('/:id/creditstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
const programmeFilter = req.query?.programme_filter
if (!code) return res.status(422).end()
const data = await getCreditStats(code, yearType, programmeFilter, specialGroups)
const data = await getCreditStats(code, yearType, programmeFilter)
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
@@ -97,9 +96,9 @@ router.get('/:id/creditstats', async (req, res) => {
// list of all programmes is also needed in classification of credits
const allProgrammes = programmeFilter === 'ALL_PROGRAMMES' ? programmes : await getProgrammes(code, 'ALL_PROGRAMMES')
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType, specialGroups)
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType)
if (updatedStats) {
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter)
}
return res.json(updatedStats)
@@ -1,71 +0,0 @@
const router = require('express').Router()
const MandatoryCourseLabels = require('../services/mandatoryCourseLabels')
const logger = require('../util/logger')
router.get('/:programme/labels', async (req, res) => {
const { programme } = req.params
if (programme) {
try {
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
res.status(200).json(labels)
} catch (err) {
logger.error(err)
res.status(400).json(err.message)
}
} else {
res.status(422).end()
}
})
router.delete('/:programme/labels', async (req, res) => {
const { programme } = req.params
const { label } = req.body
if (programme && label) {
try {
await MandatoryCourseLabels.destroy(programme, label)
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
res.status(200).json(labels)
} catch (err) {
logger.error(err)
res.status(400).json(err.message)
}
} else {
res.status(422).end()
}
})
router.post('/:programme/move', async (req, res) => {
const { programme } = req.params
const { label, direction } = req.body
if (programme && label) {
try {
await MandatoryCourseLabels.move(programme, label, direction)
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
res.status(200).json(labels)
} catch (err) {
logger.error(err)
res.status(400).json(err.message)
}
} else {
res.status(422).end()
}
})
router.post('/:programme/labels', async (req, res) => {
const { programme } = req.params
const { label } = req.body
if (programme && label) {
try {
await MandatoryCourseLabels.create(programme, label)
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
res.status(200).json(labels)
} catch (err) {
logger.error(err)
res.status(400).json(err.message)
}
} else {
res.status(422).end()
}
})
module.exports = router
@@ -1,36 +0,0 @@
const router = require('express').Router()
const MandatoryCourses = require('../services/mandatoryCourses')
router.delete('/:programme', async (req, res) => {
const { programme } = req.params
const { course } = req.body
if (!programme || !course) return res.status(400).end()
await MandatoryCourses.remove(programme, course)
res.status(200).json(course)
})
router.post('/:programme', async (req, res) => {
const { programme } = req.params
const { course } = req.body
if (!programme || !course) return res.status(400).end()
await MandatoryCourses.create(programme, course)
const newCourse = await MandatoryCourses.find(programme, course)
if (!newCourse) return res.status(400).end()
res.status(201).json({ name: newCourse.course.name, code: newCourse.course_code, label: newCourse.label })
})
router.post('/:programme/label/:course', async (req, res) => {
const { programme, course } = req.params
const { label } = req.body
if (!programme || !course) return res.status(400).end()
await MandatoryCourses.updateLabel(programme, course, label)
const updatedCourse = await MandatoryCourses.find(programme, course)
if (!updatedCourse) return res.status(400).end()
res.status(200).json({
name: updatedCourse.course.name,
code: updatedCourse.course_code,
label: updatedCourse.mandatory_course_label,
})
})
module.exports = router
+10 -21
View File
@@ -82,20 +82,6 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
return
}
//These cause problems in front (where they were originally calculated)
// Do them here for the moment
for (const course of result.coursestatistics) {
// course.students.notParticipated = {}
course.students.didNotPass = {}
for (const student of Object.keys(course.students.all)) {
if (
!Object.keys(course.students.passed).includes(student) &&
!Object.keys(course.students.retryPassed).includes(student)
)
course.students.didNotPass[student] = true
}
}
res.json(result)
}
})
@@ -223,7 +209,7 @@ router.get('/v3/populationstatistics', async (req, res) => {
return
}
if (req.query.months == null) {
if (req.query.months === null) {
req.query.months = 12
}
@@ -234,7 +220,7 @@ router.get('/v3/populationstatistics', async (req, res) => {
const newMonths = (upperYearBound - Number(year)) * 12
const populationStudents = Population.optimizedStatisticsOf({
...req.query,
studyRights,
studyRights: { programme: studyRights.programme },
year,
months: newMonths,
})
@@ -253,7 +239,11 @@ router.get('/v3/populationstatistics', async (req, res) => {
res.json(filterPersonalTags(result, user.id))
} else {
const result = await Population.optimizedStatisticsOf({ ...req.query, studyRights })
const result = await Population.optimizedStatisticsOf({
...req.query,
studyRights: { programme: studyRights.programme },
})
if (result.error) {
Sentry.captureException(new Error(result.error))
res.status(400).end()
@@ -403,17 +393,16 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
})
router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
const { studentnumberlist } = req.body
const { studentnumberlist, tags } = req.body
const {
user: { isAdmin, id, studentsUserCanAccess },
} = req
const filteredStudentNumbers = isAdmin ? studentnumberlist : _.intersection(studentnumberlist, studentsUserCanAccess)
const result = await Population.optimizedStatisticsOf(
{
year: 1900,
studyRights: [],
year: tags?.year || 1900,
studyRights: tags?.studyProgramme ? [tags.studyProgramme] : [],
semesters: ['FALL', 'SPRING'],
months: 10000,
},
@@ -30,8 +30,8 @@ router.delete('/v3/programme_modules', async (req, res) => {
})
router.post('/v3/programme_modules/:programmecode/', async (req, res) => {
const { programmecode, coursecodes } = req.body
await addExcludedCourses(programmecode, coursecodes)
const { programmecode, excludeFromProgramme, coursecodes } = req.body
await addExcludedCourses(excludeFromProgramme, coursecodes)
const result = await byProgrammeCode(programmecode)
if (!result) {
res.status(400).end()
+91 -10
View File
@@ -15,20 +15,22 @@ const {
setStudytrackStats,
} = require('../services/analyticsService')
const { updateBasicView, updateStudytrackView } = require('../services/studyprogrammeUpdates')
const { getProgrammeName } = require('../services/studyprogramme')
const logger = require('../util/logger')
router.get('/v2/studyprogrammes/:id/basicstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
const combinedProgramme = req.query?.combined_programme
if (!code) return res.status(422).end()
const data = await getBasicStats(code, yearType, specialGroups)
const data = await getBasicStats(code, combinedProgramme, yearType, specialGroups)
if (data) return res.json(data)
const updated = await getBasicStatsForStudytrack({
studyprogramme: req.params.id,
combinedProgramme,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
@@ -42,13 +44,15 @@ router.get('/v2/studyprogrammes/:id/creditstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
const combinedProgramme = req.query?.combined_programme
if (!code) return res.status(422).end()
const data = await getCreditStats(code, yearType, specialGroups)
const data = await getCreditStats(code, combinedProgramme, yearType, specialGroups)
if (data) return res.json(data)
const updatedStats = await getCreditStatsForStudytrack({
studyprogramme: req.params.id,
combinedProgramme,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
@@ -62,13 +66,15 @@ router.get('/v2/studyprogrammes/:id/graduationstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
const combinedProgramme = req.query?.combined_programme
if (!code) return res.status(422).end()
const data = await getGraduationStats(code, yearType, specialGroups)
const data = await getGraduationStats(code, combinedProgramme, yearType, specialGroups)
if (data) return res.json(data)
const updatedStats = await getGraduationStatsForStudytrack({
studyprogramme: req.params.id,
combinedProgramme,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
@@ -81,11 +87,12 @@ router.get('/v2/studyprogrammes/:id/graduationstats', async (req, res) => {
router.get('/v2/studyprogrammes/:id/coursestats', async (req, res) => {
const code = req.params.id
const showByYear = req.query.academicyear
const combinedProgramme = req.query?.combined_programme
const date = new Date()
date.setHours(23, 59, 59, 999)
try {
const data = await getStudyprogrammeCoursesForStudytrack(date.getTime(), code, showByYear)
const data = await getStudyprogrammeCoursesForStudytrack(date.getTime(), code, showByYear, combinedProgramme)
return res.json(data)
} catch (e) {
logger.error(`Failed to get code ${code} programme courses stats: ${e}`)
@@ -96,14 +103,16 @@ router.get('/v2/studyprogrammes/:id/studytrackstats', async (req, res) => {
const code = req.params.id
const graduated = req.query?.graduated
const specialGroups = req.query?.special_groups
const combinedProgramme = req.query?.combined_programme
if (!code) return res.status(422).end()
const data = await getStudytrackStats(code, graduated, specialGroups)
const data = await getStudytrackStats(code, combinedProgramme, graduated, specialGroups)
if (data) return res.json(data)
const updated = await getStudytrackStatsForStudyprogramme({
studyprogramme: code,
combinedProgramme,
settings: {
graduated: graduated === 'GRADUATED_INCLUDED',
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
@@ -115,12 +124,13 @@ router.get('/v2/studyprogrammes/:id/studytrackstats', async (req, res) => {
router.get('/v2/studyprogrammes/:id/update_basicview', async (req, res) => {
const code = req.params.id
const combinedProgramme = req.query?.combined_programme
if (code) {
let result = null
try {
result = await updateBasicView(code)
result = await updateBasicView(code, combinedProgramme)
} catch (e) {
logger.error(`Failed to update code ${code} basic stats: ${e}`)
logger.error(`Failed to update code ${code} - ${combinedProgramme || 'not combined'} basic stats: ${e}`)
}
return res.json(result)
} else {
@@ -130,12 +140,13 @@ router.get('/v2/studyprogrammes/:id/update_basicview', async (req, res) => {
router.get('/v2/studyprogrammes/:id/update_studytrackview', async (req, res) => {
const code = req.params.id
const combinedProgramme = req.query?.combined_programme
if (code) {
let result = null
try {
result = await updateStudytrackView(code)
result = await updateStudytrackView(code, combinedProgramme)
} catch (e) {
logger.error(`Failed to update code ${code} studytrack stats: ${e}`)
logger.error(`Failed to update code ${code} - ${combinedProgramme || 'not combined'} studytrack stats: ${e}`)
}
return res.json(result)
} else {
@@ -143,4 +154,74 @@ router.get('/v2/studyprogrammes/:id/update_studytrackview', async (req, res) =>
}
})
router.get('/v2/studyprogrammes/:id/evaluationstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
const graduated = req.query?.graduated
if (!code) return res.status(422).end()
// Statistics for Tilannekuvalomake view
const combinedProgramme = ''
let gradData = await getGraduationStats(code, combinedProgramme, yearType, specialGroups)
if (!gradData) {
const updatedStats = await getGraduationStatsForStudytrack({
studyprogramme: req.params.id,
combinedProgramme,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
})
if (updatedStats) {
await setGraduationStats(updatedStats, yearType, specialGroups)
gradData = updatedStats
}
}
let progressData = await getStudytrackStats(code, combinedProgramme, graduated, specialGroups)
if (!progressData) {
const updated = await getStudytrackStatsForStudyprogramme({
studyprogramme: code,
combinedProgramme,
settings: {
graduated: graduated === 'GRADUATED_INCLUDED',
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
},
})
if (updated) {
await setStudytrackStats(updated, graduated, specialGroups)
progressData = updated
}
}
const programmeName = await getProgrammeName(code)
delete gradData.tableStats
delete gradData.graphStats
delete gradData.titles
const data = {
id: code,
programmeName: programmeName?.name,
status: gradData?.status,
lastUpdated: gradData.lastUpdated,
graduations: gradData,
progress: {
creditTableStats: {},
creditTableTitles: progressData?.creditTableTitles,
graphData: {
creditGraphStats: {},
id: progressData?.id,
years: progressData?.years,
},
},
}
data.progress.creditTableStats[code] = progressData?.creditTableStats[code]
data.progress.graphData.creditGraphStats[code] = progressData?.creditGraphStats[code]
return res.json(data)
})
module.exports = router
@@ -8,7 +8,7 @@ const router = require('express').Router()
router.get('/', async (req, res) => {
const studyProgramme = req.query?.programmecode
if (!studyProgramme) return res.status(422).end()
if (studyProgramme !== '' && !studyProgramme) return res.status(422).end()
const studyProgrammeCriteria = await getCriteria(studyProgramme)
return res.json(studyProgrammeCriteria)
})
@@ -10,9 +10,10 @@ const createRedisKeyForGraduationStats = (id, yearType, specialGroups) =>
const createRedisKeyForStudytrackStats = (id, graduated, specialGroups) =>
`STUDYTRACK_STATS_${id}_${graduated}_${specialGroups}`
const getBasicStats = async (id, yearType, specialGroups) => {
const getBasicStats = async (id, combinedProgramme, yearType, specialGroups) => {
if (!isUpdatedNewProgramme(id)) return null
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForBasicStats(searchkey, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
@@ -32,9 +33,10 @@ const setBasicStats = async (data, yearType, specialGroups) => {
return dataToRedis
}
const getCreditStats = async (id, yearType, specialGroups) => {
const getCreditStats = async (id, combinedProgramme, yearType, specialGroups) => {
if (!isUpdatedNewProgramme(id)) return null
const redisKey = createRedisKeyForCreditStats(id, yearType, specialGroups)
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForCreditStats(searchkey, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
@@ -54,9 +56,10 @@ const setCreditStats = async (data, yearType, specialGroups) => {
return dataToRedis
}
const getGraduationStats = async (id, yearType, specialGroups) => {
const getGraduationStats = async (id, combinedProgramme, yearType, specialGroups) => {
if (!isUpdatedNewProgramme(id)) return null
const redisKey = createRedisKeyForGraduationStats(id, yearType, specialGroups)
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForGraduationStats(searchkey, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
@@ -76,9 +79,10 @@ const setGraduationStats = async (data, yearType, specialGroups) => {
return dataToRedis
}
const getStudytrackStats = async (id, graduated, specialGroups) => {
const getStudytrackStats = async (id, combinedProgramme, graduated, specialGroups) => {
if (!isUpdatedNewProgramme(id)) return null
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups)
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForStudytrackStats(searchkey, graduated, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
@@ -0,0 +1,164 @@
const { Credit, Enrollment, Student, Course } = require('../models')
const { Op } = require('sequelize')
const _ = require('lodash')
const getCompletedCourses = async (studentNumbers, courseCodes) => {
let courses = await Course.findAll({
attributes: ['code', 'name', 'substitutions'],
where: {
code: {
[Op.in]: courseCodes,
},
},
})
let fullCourseCodes = [
...courseCodes,
...new Set([...courses.reduce((acc, course) => [...acc, course.code, ...course.substitutions], [])]),
]
let credits = await Credit.findAll({
attributes: ['course_code', 'student_studentnumber', 'credittypecode', 'attainment_date'],
where: {
course_code: {
[Op.in]: fullCourseCodes,
},
student_studentnumber: {
[Op.in]: studentNumbers,
},
},
})
let enrollments = await Enrollment.findAll({
attributes: ['course_code', 'enrollment_date_time', 'studentnumber', 'state'],
where: {
course_code: {
[Op.in]: fullCourseCodes,
},
studentnumber: {
[Op.in]: studentNumbers,
},
state: {
[Op.eq]: 'ENROLLED',
},
},
})
const studentInfo = await Student.findAll({
attributes: ['studentnumber', 'firstnames', 'lastname', 'email', 'sis_person_id'],
where: {
studentnumber: {
[Op.in]: studentNumbers,
},
},
})
courses = courses.map(course => {
const { code, name, substitutions } = course
return { code, name, substitutions }
})
credits = credits.map(credit => {
const { course_code, student_studentnumber, credittypecode, attainment_date } = credit
const originalCode = courseCodes.includes(course_code)
? null
: courses.find(course => course.substitutions.includes(course_code))?.code
return {
courseCode: originalCode ? originalCode : course_code,
substitution: originalCode ? course_code : null,
studentNumber: student_studentnumber,
creditType: credittypecode,
date: attainment_date,
}
})
enrollments = enrollments.map(enrollment => {
const originalCode = courses.find(course => course.substitutions.includes(enrollment.course_code))?.code
return {
courseCode: originalCode ? originalCode : enrollment.course_code,
substitution: originalCode ? enrollment.course_code : null,
studentNumber: enrollment.studentnumber,
date: enrollment.enrollment_date_time,
}
})
const studentCredits = {}
studentInfo.forEach(s => {
if (!studentCredits[s.studentnumber]) {
studentCredits[s.studentnumber] = { credits: [], enrollments: [] }
}
studentCredits[s.studentnumber].firstnames = s.firstnames
studentCredits[s.studentnumber].lastname = s.lastname
studentCredits[s.studentnumber].email = s.email
studentCredits[s.studentnumber].sis_person_id = s.sis_person_id
})
credits.forEach(credit => {
if (credit.creditType === 10) {
return
}
const previous = studentCredits[credit.studentNumber].credits?.find(c => credit.courseCode === c.courseCode)
if (previous && previous.date > credit.date) {
return
} else if (previous) {
studentCredits[credit.studentNumber].credits = studentCredits[credit.studentNumber].credits.filter(
c => credit.courseCode !== c.courseCode
)
}
studentCredits[credit.studentNumber].credits.push({
date: credit.date,
courseCode: credit.courseCode,
creditType: credit.creditType,
substitution: credit.substitution,
})
})
enrollments.forEach(enrollment => {
if (
credits.find(
credit => credit.courseCode === enrollment.courseCode && credit.studentNumber === enrollment.studentNumber
)
) {
return
}
studentCredits[enrollment.studentNumber].enrollments.push({
date: enrollment.date,
courseCode: enrollment.courseCode,
state: enrollment.state,
})
})
const students = Object.keys(studentCredits).reduce(
(acc, student) => [
...acc,
{
studentNumber: student,
sis_person_id: studentCredits[student].sis_person_id,
credits: studentCredits[student].credits,
enrollments: studentCredits[student].enrollments,
firstnames: studentCredits[student].firstnames,
lastname: studentCredits[student].lastname,
email: studentCredits[student].email,
},
],
[]
)
students.forEach(student => {
courseCodes.forEach(courseCode => {
let [hits, rest] = _.partition(student.enrollments, e => e.courseCode === courseCode)
if (hits?.length === 0) return
let latest = hits[0]
for (let hit of hits) {
if (hit > latest) {
latest = hit
}
}
student.enrollments = [latest, ...rest]
})
})
return { students, courses }
}
module.exports = { getCompletedCourses }
@@ -26,7 +26,9 @@ class CourseStatsCounter {
failedMany: {},
improvedPassedGrade: {},
markedToSemester: {},
enrolledNoGrade: {},
}
this.stats = {
students: 0,
passed: 0,
@@ -38,10 +40,10 @@ class CourseStatsCounter {
percentage: undefined,
passedOfPopulation: undefined,
triedOfPopulation: undefined,
perStudent: undefined,
passingSemesters: this.initializePassingSemesters(),
}
this.enrollments = {
enrolledNoGrade: new Set(),
semesters: this.initializePassingSemesters({}),
}
this.grades = {}
@@ -57,7 +59,6 @@ class CourseStatsCounter {
passingSemesters[fall[i]] = initialValue
passingSemesters[spring[i]] = initialValue
}
return passingSemesters
}
@@ -184,11 +185,20 @@ class CourseStatsCounter {
getFinalStats() {
const stats = this.stats
const students = this.students
const allStudents = Object.keys(students.all)
const totalEnrolledNoGrade = [...(this.enrollments.ENROLLED || [])].filter(
student => !allStudents.includes(student)
).length
const studs = this.enrollments
? Object.keys(this.enrollments)
.filter(key => key !== 'ENROLLED' && key !== 'semesters')
.reduce((acc, key) => [...acc, ...[...this.enrollments[key]].map(studentnumber => studentnumber)], [])
: []
const allStudents = Object.keys(students.all).map(student => student)
const fileredEnrolledNoGrade = this.enrollments['ENROLLED']
? [...this.enrollments['ENROLLED']]
.filter(student => !studs.includes(student) && !allStudents.includes(student))
.reduce((acc, student) => ({ ...acc, [student]: true }), {})
: {}
students.all = { ...students.all, ...fileredEnrolledNoGrade }
students.enrolledNoGrade = fileredEnrolledNoGrade
stats.students = lengthOf(students.all)
stats.passed = lengthOf(students.passed)
stats.failed = lengthOf(students.failed)
@@ -200,10 +210,9 @@ class CourseStatsCounter {
stats.triedOfPopulation = percentageOf(stats.students, this.studentsInTotal)
stats.perStudent = stats.attempts / (stats.passed + stats.failed)
stats.passingSemestersCumulative = this.getPassingSemestersCumulative()
stats.totalStudents = stats.students + totalEnrolledNoGrade
stats.totalEnrolledNoGrade = totalEnrolledNoGrade
stats.totalStudents = stats.students
stats.totalEnrolledNoGrade = lengthOf(fileredEnrolledNoGrade)
stats.percentageWithEnrollments = percentageOf(stats.passed, stats.totalStudents)
return {
stats,
students,
@@ -7,14 +7,8 @@ const {
isNonMajorCredit,
getYearsArray,
getStartDate,
isSpecialGroupCredit,
} = require('../studyprogrammeHelpers')
const {
getStudyRights,
getCreditsForStudyProgramme,
allTransfers,
getCourseCodesForStudyProgramme,
} = require('../studyprogramme')
const { getStudyRights, getCreditsForStudyProgramme, getCourseCodesForStudyProgramme } = require('../studyprogramme')
const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes) => {
let right = ''
@@ -42,25 +36,14 @@ const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes
// Fetches all the credits for the studyprogramme and divides them into major-students, non-major faculty students,
// non-major other faculty students adn non-degree student credits
// Division is done on the basis that whether the student had a primary studyright to the programme on the attainment_date
const getFacultyRegularCreditStats = async ({
studyprogramme,
since,
years,
isAcademicYear,
includeAllSpecials,
facultyProgrammes,
}) => {
const getFacultyRegularCreditStats = async ({ studyprogramme, since, years, isAcademicYear, facultyProgrammes }) => {
const providercode = mapToProviders([studyprogramme])[0]
const courses = await getCourseCodesForStudyProgramme(providercode)
// console.log(JSON.stringify(courses))
const credits = await getCreditsForStudyProgramme(courses, since)
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
let studyrights = await getStudyRights(students)
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid).filter(s => s !== 'undefined')
if (!includeAllSpecials) {
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
}
let majors = getStatsBasis(years)
let allNonMajors = getStatsBasis(years)
let facultyNonMajors = getStatsBasis(years)
@@ -71,10 +54,6 @@ const getFacultyRegularCreditStats = async ({
const studentStudyrights = studyrights.filter(studyright => studyright.studentnumber === student_studentnumber)
const attainmentYear = defineYear(attainment_date, isAcademicYear)
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainment_date, transfers)) {
return
}
if (isMajorStudentCredit(studentStudyrights, attainment_date, studyprogramme)) {
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
majors.tableStats[attainmentYear] += credits || 0
@@ -116,12 +95,11 @@ const getFacultyRegularCreditStats = async ({
// Fetches all credits for the studytrack and combines them into statistics
// These are for the faculty view which differs from study programme overview
// (no transfers, non majors split to in- and outside-faculty)
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, settings }) => {
const { isAcademicYear, includeAllSpecials } = settings
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, isAcademicYear }) => {
const since = getStartDate(studyprogramme, isAcademicYear)
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials, facultyProgrammes }
const queryParameters = { studyprogramme, since, years, isAcademicYear, facultyProgrammes }
const { majors, facultyNonMajors, otherNonMajors, nonDegree } = await getFacultyRegularCreditStats(queryParameters)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
@@ -129,47 +107,36 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
if (!dataFound) return null
const tableStats = reversedYears.map(year =>
includeAllSpecials
? [
year,
majors.tableStats[year] +
facultyNonMajors.tableStats[year] +
otherNonMajors.tableStats[year] +
nonDegree.tableStats[year],
majors.tableStats[year],
facultyNonMajors.tableStats[year],
otherNonMajors.tableStats[year],
nonDegree.tableStats[year],
]
: [year, majors.tableStats[year], majors.tableStats[year]]
)
const tableStats = reversedYears.map(year => [
year,
majors.tableStats[year] +
facultyNonMajors.tableStats[year] +
otherNonMajors.tableStats[year] +
nonDegree.tableStats[year],
majors.tableStats[year],
facultyNonMajors.tableStats[year],
otherNonMajors.tableStats[year],
nonDegree.tableStats[year],
])
const graphStats = includeAllSpecials
? [
{
name: 'Major students credits',
data: majors.graphStats,
},
{
name: 'Non-major faculty students credits',
data: facultyNonMajors.graphStats,
},
{
name: 'Non-major other faculty students credits 2',
data: otherNonMajors.graphStats,
},
{
name: 'Non-degree credits',
data: nonDegree.graphStats,
},
]
: [
{
name: 'Major students credits',
data: majors.graphStats,
},
]
const graphStats = [
{
name: 'Major students credits',
data: majors.graphStats,
},
{
name: 'Non-major faculty students credits',
data: facultyNonMajors.graphStats,
},
{
name: 'Non-major other faculty students credits 2',
data: otherNonMajors.graphStats,
},
{
name: 'Non-degree credits',
data: nonDegree.graphStats,
},
]
return {
id: studyprogramme,
@@ -179,33 +146,27 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
}
}
const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgrammes) => {
const getProgrammeCredits = async (code, yearType, facultyProgrammes) => {
const updatedStats = await getFacultyCreditStatsForStudytrack({
studyprogramme: code,
facultyProgrammes,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
isAcademicYear: yearType === 'ACADEMIC_YEAR',
})
return updatedStats
}
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType, specialGroups) => {
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType) => {
let counts = {}
let years = []
const titles =
specialGroups === 'SPECIAL_INCLUDED'
? [
'',
'Total',
'Major students credits',
'Non-major faculty students credits',
'Non-major other faculty students credits',
'Non-degree student credits',
]
: ['', 'Total', 'Major students credits']
const titles = [
'',
'Total',
'Major students credits',
'Non-major faculty students credits',
'Non-major other faculty students credits',
'Non-degree student credits',
]
let allCredits = {
id: faculty,
@@ -220,7 +181,7 @@ const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearTyp
const progCodes = allProgrammes.map(p => p.code)
for (const prog of programmes) {
if (prog.code === 'MH70_008_2') continue
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
const data = await getProgrammeCredits(prog.code, yearType, progCodes)
if (data) {
allCredits.programmeNames[prog.progId] = { code: prog.code, ...prog.name }
allCredits.programmeTableStats[prog.progId] = data.tableStats
@@ -260,16 +221,12 @@ const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearTyp
nonDegree.push(counts[year][4])
})
allCredits.graphStats =
specialGroups === 'SPECIAL_INCLUDED'
? [
{ name: 'Major students credits', data: majors },
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
{ name: 'Non-degree credits', data: nonDegree },
]
: [{ name: 'Major students credits', data: majors }]
allCredits.graphStats = [
{ name: 'Major students credits', data: majors },
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
{ name: 'Non-degree credits', data: nonDegree },
]
return allCredits
}
@@ -54,9 +54,9 @@ const getBasicStats = async (id, yearType, programmeFilter, specialGroups) => {
return JSON.parse(dataFromRedis)
}
const setCreditStats = async (data, yearType, programmeFilter, specialGroups) => {
const setCreditStats = async (data, yearType, programmeFilter) => {
const { id } = data
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter)
const dataToRedis = {
...data,
status: 'DONE',
@@ -6,11 +6,10 @@ const {
getCreditThresholds,
getBachelorCreditGraphStats,
getMasterCreditGraphStats,
getBachelorMasterCreditGraphStats,
getDoctoralCreditGraphStats,
getYearsArray,
tableTitles,
getOnlyMasterCreditGraphStats,
getBcMsThresholds,
getVetenaryCreditGraphStats,
} = require('../studyprogrammeHelpers')
const { studytrackStudents } = require('../studyprogramme')
@@ -115,12 +114,14 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
mastersTableStats: new Array(yearsArray.length),
doctoralTableStats: new Array(yearsArray.length),
bachelorsGraphStats: getBachelorCreditGraphStats(yearsArray),
bcMsGraphStats: faculty === 'H90' ? getVetenaryCreditGraphStats(yearsArray) : getMasterCreditGraphStats(yearsArray),
mastersGraphStats: getOnlyMasterCreditGraphStats(yearsArray),
bcMsGraphStats:
faculty === 'H90' ? getVetenaryCreditGraphStats(yearsArray) : getBachelorMasterCreditGraphStats(yearsArray),
mastersGraphStats:
faculty === 'H90' ? getBachelorCreditGraphStats(yearsArray) : getMasterCreditGraphStats(yearsArray),
doctoralGraphStats: getDoctoralCreditGraphStats(yearsArray),
bachelorTitles: tableTitles.creditProgress.bachelor,
bcMsTitles: faculty === 'H90' ? titlesVet : tableTitles.creditProgress.master,
mastersTitles: tableTitles.creditProgress.masterOnly,
bcMsTitles: faculty === 'H90' ? titlesVet : tableTitles.creditProgress.bachelorMaster,
mastersTitles: faculty === 'H90' ? tableTitles.creditProgress.bachelor : tableTitles.creditProgress.master,
doctoralTitles: tableTitles.creditProgress.doctoral,
yearlyBachelorTitles: [],
yearlyBcMsTitles: [],
@@ -139,6 +140,7 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
progressStats.mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0]
progressStats.doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0]
})
const limitKeys = ['zero', 't5', 't4', 't3', 't2', 't1']
let bachelorlimits = []
let masterlimits = []
@@ -188,7 +190,9 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
},
}
let studyrights = await getStudyRightsByExtent(faculty, {}, startDateWhere, code, extentcodes, graduationStatus)
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code)
// Get credit threshold values: 'combined study programme' is false and 'only master studyright' is true
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code, false, true)
if (!creditThresholdKeys) continue
if (!(progId in progressStats.programmeNames)) {
progressStats.programmeNames[progId] = { code: code, ...name }
@@ -237,7 +241,11 @@ 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()
// Get credit threshold values both 'combined study programme' and 'only master studyright' are false
const { creditThresholdKeys: creditThresholdKeysBcMs, creditThresholdAmounts: creditThresholdAmountsBcMs } =
getCreditThresholds(code, false, false)
const { data: bcMsStudentdata, progData: bcMsProgdata } = getStudentData(
startDate,
bcMsStudents,
@@ -247,7 +255,6 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
limitKeys,
'MH'
)
const { data: msStudentdata, progData: msProgdata } = getStudentData(
startDate,
msStudents,
@@ -261,7 +268,6 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
masterProgress[progId] = new Array(reversedYears.length - 1)
bcmsProgress[progId] = new Array(reversedYears.length - 1)
}
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)) {
@@ -3,9 +3,9 @@ const { Op } = require('sequelize')
const { getStartDate, getYearsArray, getPercentage, getYearsObject, tableTitles } = require('../studyprogrammeHelpers')
const { getAcademicYearDates } = require('../../util/semester')
const { getStudyRightsByExtent, getStudentsByStudentnumbers, getTransfers } = require('./faculty')
const { getCurrentSemester } = require('../semesters')
const { checkTransfers } = require('./facultyHelpers')
const { inactiveStudyrights, absentStudents } = require('../studyprogramme')
const { inactiveStudyrights, absentStudents, enrolledStudents } = require('../studyprogramme')
const { graduatedStudyRights } = require('../studyprogramme')
const emptyTotals = () => {
return {
total: 0,
@@ -65,12 +65,14 @@ const getStudentData = (students, facultyExtra, year, code) => {
facultyExtra['Total'][code].unknownG += gender_code === '0' ? 1 : 0
data.finnish += home_country_en === 'Finland' ? 1 : 0
data.otherCountries += home_country_en !== 'Finland' ? 1 : 0
if (!Object.keys(facultyExtra[year][code].countries).includes(home_country_en))
if (!Object.keys(facultyExtra[year][code].countries).includes(home_country_en) && home_country_en !== 'Finland')
facultyExtra[year][code].countries[home_country_en] = 0
if (!Object.keys(facultyExtra['Total'][code].countries).includes(home_country_en))
if (!Object.keys(facultyExtra['Total'][code].countries).includes(home_country_en) && home_country_en !== 'Finland')
facultyExtra['Total'][code].countries[home_country_en] = 0
facultyExtra[year][code].countries[home_country_en] += 1
facultyExtra['Total'][code].countries[home_country_en] += 1
if (home_country_en !== 'Finland') {
facultyExtra[year][code].countries[home_country_en] += 1
facultyExtra['Total'][code].countries[home_country_en] += 1
}
})
return data
}
@@ -139,23 +141,18 @@ const getFacultyDataForYear = async ({
studyrights = allStudyrights.filter(s => !checkTransfers(s, allTransfers, allTransfers))
if (includeAllSpecials) {
allStudents = [...new Set([...allTransfers.map(sr => sr.studentnumber).filter(smbr => smbr !== null)])]
allStudents = [...new Set([...allTransfers.map(sr => sr.studentnumber).filter(student => student !== null)])]
}
// Get all the studyrights and students for the calculations
const currentSemester = await getCurrentSemester()
const studentNbrs = studyrights.map(sr => sr.studentnumber)
allStudents = [...studentNbrs, ...allStudents]
const students = await getStudentsByStudentnumbers(allStudents)
const studentData = getStudentData(students, facultyExtra, year, code)
const started = await getStudentsByStudentnumbers(studentNbrs)
const inactive = await inactiveStudyrights(code, studentNbrs)
const graduatedStudents = studyrights.filter(sr => sr.graduated === 1)
const absent = await absentStudents(code, studentNbrs)
const enrolled = students.filter(
s =>
s.semesters.filter(semester => semester.semestercode === currentSemester.semestercode)[0]?.enrollmenttype === 1
)
const inactive = await inactiveStudyrights(code, allStudents)
const graduatedStudents = await graduatedStudyRights(code, start, allStudents)
const absent = await absentStudents(code, allStudents)
const enrolled = await enrolledStudents(code, allStudents)
// Count stats for the programme
if (students.length === 0) continue
if (!(progId in programmeTableStats)) {
@@ -254,9 +251,7 @@ const combineFacultyStudents = async (code, programmes, allProgrammeCodes, speci
programmeNames,
}
const totals = await getFacultyDataForYear(queryParams)
if (totals.total !== 0) {
addTotals(facultyTableStats, year, totals)
}
addTotals(facultyTableStats, year, totals)
}
addTotals(facultyTableStats, 'Total', allTotals)
@@ -270,7 +265,7 @@ const combineFacultyStudents = async (code, programmes, allProgrammeCodes, speci
facultyTableStats: facultyTableStats,
facultyTableStatsExtra: facultyExtra,
programmeStats: programmeTableStats,
titles: tableTitles['programmes'],
titles: [...tableTitles.studytracksStart, ...tableTitles.studytracksBasic, ...tableTitles.studytracksEnd],
programmeNames: programmeNames,
}
return studentsData
@@ -100,13 +100,12 @@ const updateFacultyOverview = async (faculty, statsType) => {
)
await setBasicStats(updatedStudentInfo, option.yearType, option.programmeFilter, option.specialGroups)
}
if (statsType === 'ALL' || statsType === 'CREDITS') {
if ((statsType === 'ALL' || statsType === 'CREDITS') && specialGroups !== 'SPECIAL_EXCLUDED') {
const updatedCredits = await combineFacultyCredits(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
allProgrammes.data,
yearType,
specialGroups
yearType
)
await setCreditStats(updatedCredits, yearType, programmeFilter, specialGroups)
}
@@ -1,64 +0,0 @@
const Sequelize = require('sequelize')
const _ = require('lodash')
const { MandatoryCourseLabels, sequelizeKone } = require('../models/models_kone')
const { Op } = Sequelize
const create = async (studyprogramme_id, { label }) => {
const labels = await labelsByStudyprogramme(studyprogramme_id)
await MandatoryCourseLabels.create({ studyprogramme_id, label, orderNumber: labels.length + 1 })
}
const destroy = (studyprogramme_id, { id }) => {
return MandatoryCourseLabels.destroy({ where: { studyprogramme_id, id } })
}
const move = async (studyprogramme_id, { id }, direction) => {
const labels = await labelsByStudyprogramme(studyprogramme_id).map(e => e.get())
const orderedlabels = _.sortBy(labels, ['orderNumber'])
const indexToMove = orderedlabels.findIndex(e => e.id === id)
const indexToSwap = direction === 'up' ? indexToMove - 1 : indexToMove + 1
if (indexToSwap < 0 || indexToSwap >= orderedlabels.length) return null
;[orderedlabels[indexToMove], orderedlabels[indexToSwap]] = [orderedlabels[indexToSwap], orderedlabels[indexToMove]]
const newOrder = orderedlabels.map((e, index) => ({ ...e, orderNumber: index }))
await sequelizeKone.transaction(async transaction => {
for (let e of newOrder) {
await MandatoryCourseLabels.update(
{ orderNumber: e.orderNumber },
{ where: { studyprogramme_id, id: e.id }, transaction }
)
}
})
}
const find = (studyprogramme_id, { id }) => {
return MandatoryCourseLabels.findOne({
attributes: ['id', 'label', 'orderNumber'],
where: {
studyprogramme_id: {
[Op.eq]: studyprogramme_id,
},
id: {
[Op.eq]: id,
},
},
})
}
const labelsByStudyprogramme = studyProgrammeId => {
return MandatoryCourseLabels.findAll({
attributes: ['id', 'label', 'orderNumber'],
where: {
studyprogramme_id: {
[Op.eq]: studyProgrammeId,
},
},
})
}
module.exports = {
labelsByStudyprogramme,
create,
destroy,
move,
find,
}
@@ -1,108 +0,0 @@
const Sequelize = require('sequelize')
const { Course } = require('../models')
const { MandatoryCourse, MandatoryCourseLabels } = require('../models/models_kone')
const { Op } = Sequelize
const byStudyprogramme = async studyProgrammeId => {
const mandatoryCourses = await MandatoryCourse.findAll({
attributes: ['course_code'],
include: [
{
model: MandatoryCourseLabels,
attributes: ['id', 'label', 'orderNumber'],
},
],
where: {
studyprogramme_id: {
[Op.eq]: studyProgrammeId,
},
},
})
const courses = await Course.findAll({
model: Course,
where: {
code: {
[Op.in]: mandatoryCourses.map(c => c.course_code),
},
},
})
const courseCodeToCourse = courses.reduce((acc, c) => {
acc[c.code] = c
return acc
}, {})
return mandatoryCourses.map(mc => ({
name: courseCodeToCourse[mc.course_code].name,
code: mc.course_code,
label: mc.mandatory_course_label,
}))
}
const create = (studyProgrammeId, code) => {
return MandatoryCourse.create({
studyprogramme_id: studyProgrammeId,
course_code: code,
})
}
const find = async (studyProgrammeId, code) => {
const mandatoryCourse = await MandatoryCourse.findOne({
attributes: ['course_code', 'label'],
include: [
{
model: MandatoryCourseLabels,
attributes: ['id', 'label', 'orderNumber'],
},
],
where: {
studyprogramme_id: {
[Op.eq]: studyProgrammeId,
},
course_code: {
[Op.eq]: code,
},
},
})
if (!mandatoryCourse) return null
const course = await Course.findOne({
where: {
code: mandatoryCourse.course_code,
},
})
if (!course) return null
return { ...mandatoryCourse.get(), course: course.get() }
}
const remove = (studyProgrammeId, code) => {
return MandatoryCourse.destroy({
where: {
studyprogramme_id: studyProgrammeId,
course_code: code,
},
})
}
const updateLabel = (studyProgrammeId, code, { id }) => {
return MandatoryCourse.update(
{
label: id,
},
{
where: {
course_code: code,
studyprogramme_id: studyProgrammeId,
},
}
)
}
module.exports = {
create,
find,
remove,
byStudyprogramme,
updateLabel,
}
@@ -9,12 +9,11 @@ const mapOpenCredits = credit => {
}
const mapStundentInfo = student => {
const { email, secondary_email, studentnumber, dissemination_info_allowed } = student
const { email, secondary_email, studentnumber } = student
return {
studentnumber,
email,
secondary_email,
dissemination_info_allowed,
}
}
const mapOpenEnrollments = enrollment => {
@@ -51,15 +51,17 @@ const getCustomOpenUniCourses = async (courseCodes, startdate, enddate) => {
}
}
for (const { studentnumber, email, secondary_email, dissemination_info_allowed } of studentInfo) {
for (const { studentnumber, email, secondary_email } of studentInfo) {
// Check if the student has existing studyright: if yes, then stop here
if (!uniqueStudentsWithCurrentStudyRight.includes(studentnumber)) {
if (!(studentnumber in studentStats)) {
studentStats[studentnumber] = {
courseInfo: courseCodes.reduce((acc, code) => ({ ...acc, [code]: getEmptyCourseInfo() }), {}),
courseInfo: courseCodes.reduce(
(acc, code) => ({ ...acc, [code.replace('AY', '')]: getEmptyCourseInfo() }),
{}
),
email: email,
secondaryEmail: secondary_email,
disseminationInfoAllowed: dissemination_info_allowed,
totals: { passed: 0, failed: 0, unfinished: 0 },
}
}
+41 -1
View File
@@ -10,6 +10,7 @@ const facultiesAndProgrammesInUserService = [
['H10', 'KH10_001'],
['H20', 'KH20_001'],
['H20', 'MH20_001'],
['H20', 'MH20_003'],
['H20', 'MH20_002'],
['H30', 'KH30_001'],
['H30', 'KH30_002'],
@@ -48,6 +49,7 @@ const facultiesAndProgrammesInUserService = [
['H50', 'KH50_005'],
['H50', 'KH50_006'],
['H50', 'KH50_007'],
['H50', 'KH50_008'],
['H50', 'MH50_001'],
['H50', 'MH50_002'],
['H50', 'MH50_003'],
@@ -61,8 +63,10 @@ const facultiesAndProgrammesInUserService = [
['H50', 'MH50_011'],
['H50', 'MH50_012'],
['H50', 'MH50_013'],
['H50', 'MH50_014'],
['H55', 'KH55_001'],
['H55', 'MH55_001'],
['H55', 'MH55_002'],
['H57', 'KH57_001'],
['H57', 'KH57_002'],
['H57', 'KH57_003'],
@@ -76,6 +80,7 @@ const facultiesAndProgrammesInUserService = [
['H57', 'MH80_007'],
['H60', 'KH60_001'],
['H60', 'MH60_001'],
['H60', 'MH60_002'],
['H70', 'KH40_001'],
['H70', 'KH50_001'],
['H70', 'KH70_001'],
@@ -94,6 +99,7 @@ const facultiesAndProgrammesInUserService = [
['H70', 'MH70_008'],
['H70', 'MH70_009'],
['H70', 'MH70_010'],
['H70', 'MH70_011'],
['H74', 'KH74_001'],
['H80', 'KH57_002'],
['H80', 'KH80_001'],
@@ -110,7 +116,42 @@ const facultiesAndProgrammesInUserService = [
['H80', 'MH80_005'],
['H80', 'MH80_006'],
['H80', 'MH80_007'],
['H80', 'MH80_008'],
['H90', 'KH90_001'],
['H90', 'MH90_001'],
['H92', 'T920101'],
['H92', 'T920102'],
['H92', 'T920103'],
['H92', 'T920104'],
['H92', 'T920105'],
['H92', 'T920106'],
['H92', 'T920107'],
['H92', 'T920108'],
['H92', 'T920109'],
['H92', 'T920110'],
['H92', 'T920111'],
['H92', 'T921101'],
['H92', 'T921102'],
['H92', 'T921103'],
['H92', 'T921104'],
['H92', 'T921105'],
['H92', 'T921106'],
['H92', 'T921107'],
['H92', 'T921108'],
['H92', 'T921109'],
['H92', 'T922101'],
['H92', 'T922102'],
['H92', 'T922103'],
['H92', 'T922104'],
['H92', 'T922105'],
['H92', 'T922106'],
['H92', 'T923101'],
['H92', 'T923102'],
['H92', 'T923103'],
['H92', 'T923104'],
['H92', 'T923105'],
['H92', 'T923106'],
['H92', 'T923107'],
['H30', '447000'],
['H30', '447001'],
['H30', '447002'],
@@ -143,7 +184,6 @@ const facultiesAndProgrammesInUserService = [
['H60', '0005'],
['H60', '7900'],
['H60', '0152 '],
['H90', 'MH90_001'],
]
const facultiesAndProgrammesForTrends = facultiesAndProgrammesInUserService.map(f => ({
+269 -38
View File
@@ -25,6 +25,8 @@ const { CourseStatsCounter } = require('./course_stats_counter')
const { getPassingSemester, semesterEnd, semesterStart } = require('../util/semester')
const { getAllProgrammes } = require('./studyrights')
const { encrypt } = require('../services/encrypt')
const { getCriteria } = require('./studyProgrammeCriteria')
const { getCurrentSemester } = require('./semesters')
const enrolmentDates = () => {
const query = 'SELECT DISTINCT s.dateOfUniversityEnrollment as date FROM Student s'
@@ -38,6 +40,61 @@ const universityEnrolmentDates = async () => {
.filter(d => d)
.sort()
}
// Progress tab related helper functions.
const createEmptyCriteriaYear = (criteria, year) => {
return {
credits: false,
totalSatisfied: 0,
coursesSatisfied:
criteria?.courses && criteria?.courses[year]
? criteria.courses[year].reduce((acc, course) => {
acc[course] = null
return acc
}, {})
: {},
}
}
const getCreditAmount = (course, hops, courseCode, startDate, addition) => {
return moment(course.date).isBetween(moment(startDate), moment(startDate).add(addition, 'year')) &&
hops.length > 0 &&
(hops[0].included_courses.includes(courseCode) || hops[0].included_courses.includes(course.course_code))
? course.credits
: 0
}
const updateCourseByYear = (criteria, criteriaYear, course, criteriaChecked, yearToAdd, correctCode) => {
if (
criteria?.courses &&
criteria?.courses[criteriaYear] &&
(criteria.courses[criteriaYear].includes(course.course_code) ||
criteria.courses[criteriaYear].some(
c => criteria.allCourses[c] && criteria.allCourses[c].includes(course.course_code)
))
) {
if (
!criteriaChecked[yearToAdd].coursesSatisfied[correctCode] ||
new Date(criteriaChecked[yearToAdd].coursesSatisfied[correctCode]) > new Date(course.date)
) {
criteriaChecked[yearToAdd].coursesSatisfied[correctCode] = course.date
}
}
}
const updateCreditcriteriaInfo = (criteria, criteriaYear, criteriaChecked, yearToAdd, academicYears, academicYear) => {
if (criteria.courses) {
if (criteriaChecked?.[yearToAdd]) {
criteriaChecked[yearToAdd].totalSatisfied +=
Object.keys(criteriaChecked?.[yearToAdd].coursesSatisfied).filter(
course => criteriaChecked?.[yearToAdd].coursesSatisfied[course] !== null
).length || 0
}
if (academicYears[academicYear] >= criteria?.credits[criteriaYear] && criteria?.credits[criteriaYear] > 0) {
criteriaChecked[yearToAdd].credits = true
criteriaChecked[yearToAdd].totalSatisfied += 1
}
}
}
const formatStudentForPopulationStatistics = (
{
@@ -48,6 +105,7 @@ const formatStudentForPopulationStatistics = (
creditcount,
abbreviatedname,
email,
secondary_email,
phone_number,
studyrights,
studyplans,
@@ -68,7 +126,10 @@ const formatStudentForPopulationStatistics = (
credits,
startDate,
startDateMoment,
endDateMoment
endDateMoment,
criteria,
code,
currentSemester
) => {
const toCourse = ({ grade, attainment_date, credits, course_code, credittypecode, isStudyModule, language }) => {
const attainment_date_normailized =
@@ -86,10 +147,58 @@ const formatStudentForPopulationStatistics = (
language,
}
}
const criteriaCoursesBySubstitutions = criteria?.allCourses
? Object.keys(criteria.allCourses).reduce((acc, code) => {
acc[code] = code
criteria.allCourses[code].map(subst => (acc[subst] = code))
return acc
}, {})
: {}
const toProgressCriteria = () => {
const criteriaChecked = {
year1: createEmptyCriteriaYear(criteria, 'yearOne'),
year2: createEmptyCriteriaYear(criteria, 'yearTwo'),
year3: createEmptyCriteriaYear(criteria, 'yearThree'),
year4: createEmptyCriteriaYear(criteria, 'yearFour'),
year5: createEmptyCriteriaYear(criteria, 'yearFive'),
year6: createEmptyCriteriaYear(criteria, 'yearSix'),
}
const correctStudyplan = studyplans ? studyplans.filter(plan => plan.programme_code === code) : []
const academicYears = { first: 0, second: 0, third: 0, fourth: 0, fifth: 0, sixth: 0 }
if (criteria.courses || criteria.credits) {
const courses = credits[studentnumber] ? credits[studentnumber].map(toCourse) : []
courses.forEach(course => {
if (course.passed) {
const correctCode = criteriaCoursesBySubstitutions[course.course_code]
updateCourseByYear(criteria, 'yearOne', course, criteriaChecked, 'year1', correctCode)
updateCourseByYear(criteria, 'yearTwo', course, criteriaChecked, 'year2', correctCode)
updateCourseByYear(criteria, 'yearThree', course, criteriaChecked, 'year3', correctCode)
updateCourseByYear(criteria, 'yearFour', course, criteriaChecked, 'year4', correctCode)
updateCourseByYear(criteria, 'yearFive', course, criteriaChecked, 'year5', correctCode)
updateCourseByYear(criteria, 'yearSix', course, criteriaChecked, 'year6', correctCode)
academicYears.first += getCreditAmount(course, correctStudyplan, correctCode, startDate, 1)
academicYears.second += getCreditAmount(course, correctStudyplan, correctCode, startDate, 2)
academicYears.third += getCreditAmount(course, correctStudyplan, correctCode, startDate, 3)
academicYears.fourth += getCreditAmount(course, correctStudyplan, correctCode, startDate, 4)
academicYears.fifth += getCreditAmount(course, correctStudyplan, correctCode, startDate, 5)
academicYears.sixth += getCreditAmount(course, correctStudyplan, correctCode, startDate, 6)
}
})
}
updateCreditcriteriaInfo(criteria, 'yearOne', criteriaChecked, 'year1', academicYears, 'first')
updateCreditcriteriaInfo(criteria, 'yearTwo', criteriaChecked, 'year2', academicYears, 'second')
updateCreditcriteriaInfo(criteria, 'yearThree', criteriaChecked, 'year3', academicYears, 'third')
updateCreditcriteriaInfo(criteria, 'yearFour', criteriaChecked, 'year4', academicYears, 'fourth')
updateCreditcriteriaInfo(criteria, 'yearFive', criteriaChecked, 'year5', academicYears, 'fifth')
updateCreditcriteriaInfo(criteria, 'yearSix', criteriaChecked, 'year6', academicYears, 'sixth')
return criteriaChecked
}
studyrights = studyrights || []
const started = dateofuniversityenrollment
return {
firstnames,
lastname,
@@ -104,8 +213,13 @@ const formatStudentForPopulationStatistics = (
gender_code,
gender: { gender_en, gender_fi, gender_sv },
email,
secondaryEmail: secondary_email,
phoneNumber: phone_number,
semesterenrollments: semester_enrollments || [],
semesterenrollments: semester_enrollments
? semester_enrollments
.sort((a, b) => a.semestercode - b.semestercode)
.filter(enrollment => enrollment.semestercode <= currentSemester + (currentSemester % 2))
: [],
updatedAt: updatedAt || createdAt,
tags: tags || [],
studyrightStart: startDate,
@@ -114,6 +228,7 @@ const formatStudentForPopulationStatistics = (
birthdate,
studyplans,
sis_person_id,
criteriaProgress: toProgressCriteria(),
}
}
@@ -233,6 +348,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'creditcount',
'abbreviatedname',
'email',
'secondary_email',
'phone_number',
'updatedAt',
'gender_code',
@@ -261,6 +377,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'faculty_code',
'studystartdate',
'admission_type',
'cancelled',
],
separate: true,
include: [
@@ -291,7 +408,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
},
{
model: Studyplan,
attributes: ['included_courses', 'programme_code', 'completed_credits'],
attributes: ['included_courses', 'programme_code', 'completed_credits', 'studyrightid'],
separate: true,
},
],
@@ -412,7 +529,6 @@ const studentnumbersWithAllStudyrightElements = async (
graduatedStudents
) => {
// eslint-disable-line
// db startdate is formatted to utc so need to change it when querying
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
@@ -569,7 +685,7 @@ const studentnumbersWithAllStudyrightElements = async (
}
// fetch students that have transferred to the programme and filter out these studentnumbers
if (transferredToStudents) {
if (!transferredToStudents) {
const transfersTo = (
await Transfer.findAll({
attributes: ['studentnumber'],
@@ -591,7 +707,7 @@ const studentnumbersWithAllStudyrightElements = async (
}
// fetch students that have graduated from the programme and filter out these studentnumbers
if (graduatedStudents) {
if (!graduatedStudents) {
const graduated = (
await Student.findAll({
attributes: ['studentnumber'],
@@ -749,10 +865,14 @@ const formatStudentsForApi = async (
startDate,
endDate,
{ studyRights },
optionData
optionData,
criteria,
code
) => {
const startDateMoment = moment(startDate)
const endDateMoment = moment(endDate)
const currentSemester = (await getCurrentSemester()).semestercode
elementdetails = elementdetails.reduce(
(acc, e) => {
acc.data[e.code] = e
@@ -783,8 +903,19 @@ const formatStudentsForApi = async (
})
if (student.studentnumber in optionData) student.option = optionData[student.studentnumber]
else student.option = null
stats.students.push(
formatStudentForPopulationStatistics(student, enrollments, credits, startDate, startDateMoment, endDateMoment)
formatStudentForPopulationStatistics(
student,
enrollments,
credits,
startDate,
startDateMoment,
endDateMoment,
criteria,
code,
currentSemester
)
)
return stats
},
@@ -866,18 +997,22 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
endDate,
exchangeStudents,
nondegreeStudents,
transferredStudents
transferredStudents,
null,
!transferredStudents,
true
)
// wtf
// plz
const code = studyRights[0] || ''
let optionData = {}
let criteria = {}
if (code.includes('MH')) {
optionData = await getOptionsForStudents(studentnumbers, code, 'MSC')
} else if (code.includes('KH')) {
optionData = await getOptionsForStudents(studentnumbers, code, 'BSC')
}
if (code.includes('KH') || ['MH30_001', 'MH30_003'].includes(code)) {
criteria = await getCriteria(code)
}
const { students, enrollments, credits, extents, semesters, elementdetails, courses } =
await getStudentsIncludeCoursesBetween(
studentnumbers,
@@ -892,7 +1027,9 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
startDate,
endDate,
formattedQueryParams,
optionData
optionData,
criteria,
code
)
return formattedStudents
@@ -902,10 +1039,52 @@ const getSubstitutions = async codes => {
const courses = await Course.findAll({ where: { code: codes }, attributes: ['code', 'substitutions'], raw: true })
return [...new Set(courses.map(({ code, substitutions }) => [code, ...substitutions]).flat())]
}
// This duplicate code is added here to ensure that we get the enrollments in cases no credits found for the selected students.
const findCourseEnrollments = async (studentnumbers, beforeDate, courses = [], studentCountLimit = 0) => {
const courseCodes = courses.length > 0 ? await getSubstitutions(courses) : ['DUMMY']
const res = await sequelize.query(
`
SELECT DISTINCT ON (course.id)
course.code,
course.name,
course.coursetypecode,
course.substitutions,
course.main_course_code,
course_types.name AS course_type,
enrollment.data AS enrollments
FROM course
INNER JOIN course_types ON course_types.coursetypecode = course.coursetypecode
LEFT JOIN (
SELECT
course_id,
ARRAY_AGG(JSON_BUILD_OBJECT(
'studentnumber', studentnumber,
'state', state,
'enrollment_date_time', enrollment_date_time
)) AS data
FROM enrollment
WHERE enrollment.studentnumber IN (:studentnumbers) AND enrollment.enrollment_date_time < :beforeDate
GROUP BY enrollment.course_id
HAVING COUNT(DISTINCT enrollment.studentnumber) >= :studentCountLimit
) enrollment ON enrollment.course_id = course.id
WHERE :skipCourseCodeFilter OR course.code IN (:courseCodes)
-- GROUP BY 1, 2, 3, 4, 5, 6
`,
{
replacements: {
studentnumbers: studentnumbers.length > 0 ? studentnumbers : ['DUMMY'],
beforeDate,
studentCountLimit,
courseCodes,
skipCourseCodeFilter: courses.length === 0,
},
type: sequelize.QueryTypes.SELECT,
}
)
return res
}
const findCourses = async (studentnumbers, beforeDate, courses = [], studentCountLimit = 0) => {
const courseCodes = courses.length > 0 ? await getSubstitutions(courses) : ['DUMMY']
const res = await sequelize.query(
`
SELECT DISTINCT ON (course.id)
@@ -999,7 +1178,9 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
exchangeStudents,
nondegreeStudents,
transferredStudents,
query.tag
query.tag,
!transferredStudents,
true
)
const disallowedRequest = checkThatSelectedStudentsAreUnderRequestedStudyright(
query.selectedStudents,
@@ -1023,21 +1204,31 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
exchangeStudents,
nondegreeStudents,
transferredStudents,
tag
tag,
!transferredStudents,
true
))
const allstudents = studentnumbers.reduce((numbers, num) => {
numbers[num] = true
return numbers
}, {})
const courses = await findCourses(
studentnumbers,
dateMonthsFromNow(startDate, months),
courseCodes,
query.studentCountLimit
)
return [allstudents, courses]
const foundCourseCodes = Object.keys(keyBy(courses, 'code'))
const filteredCourseCodes = courseCodes?.filter(code => !foundCourseCodes.includes(code))
const courseEnrollements = await findCourseEnrollments(
studentnumbers,
dateMonthsFromNow(startDate, months),
filteredCourseCodes,
query.studentCountLimit
)
return [allstudents, courses, courseEnrollements]
} else {
const { months, startDate } = params
const beforeDate = months && startDate ? dateMonthsFromNow(startDate, months) : new Date()
@@ -1047,7 +1238,16 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
return numbers
}, {})
const courses = await findCourses(studentnumberlist, beforeDate, courseCodes, query.studentCountLimit)
return [allstudents, courses]
const foundCourseCodes = Object.keys(keyBy(courses, 'code'))
const filteredCourseCodes = courseCodes?.filter(code => !foundCourseCodes.includes(code))
const courseEnrollements = await findCourseEnrollments(
studentnumberlist,
dateMonthsFromNow(startDate, months),
filteredCourseCodes,
query.studentCountLimit
)
return [allstudents, courses, courseEnrollements]
}
}
@@ -1067,24 +1267,52 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
}
const params = parseQueryParams(query)
const [[allstudents, courses], error] = await Promise.all([
const allStudentsByYears = query?.selectedStudentsByYear
? Object.keys(query.selectedStudentsByYear).reduce(
(res, year) => [...res, ...query.selectedStudentsByYear[year]],
[]
)
: []
// To fix failed and enrolled, no grade filter options some not so clean and nice solutions were added
// Get the data with actual 1. courses and filtered students. 2. all students by year, if provided.
const [[allstudents, courses, courseEnrollements], [, allCourses], error] = await Promise.all([
getStudentsAndCourses(query.selectedStudents, studentnumberlist, query.courses),
getStudentsAndCourses(allStudentsByYears, null, query.courses),
isValidRequest(query, params),
])
if (error) return error
if (error) return error
// Get the substitution codes for the fetch data by selscted students
const substitutionCodes = Object.entries(courses).reduce(
(res, [, obj]) => [...res, ...(obj?.substitutions || [])],
[]
)
const codes = Object.keys(keyBy(courses, 'code')).map(code => code)
// Filter substitution courses for fetched courses -> this avoid the situation in which there is only
// courses with old course codes. Frontend NEEDS in most cases the current course.
const substitutionCourses = allCourses.filter(
obj => substitutionCodes.includes(obj.code) && !codes.includes(obj.code)
)
const bottlenecks = {
disciplines: {},
coursetypes: {},
}
const stats = {}
const startYear = parseInt(query.year, 10)
const allstudentslength = Object.keys(allstudents).length
const coursesByCode = keyBy(courses, 'code')
let coursesToLoop = courses.concat(substitutionCourses)
const codesList = coursesToLoop.map(course => course.code)
// This fix problem when Enrolled no grade is chosen. The sql query for fetching
// credits do not fetch enrollments if no credits found for selected students.
// This and other sql query ensures that enrollments are added.
const coursesOnlyWithEnrollments = courseEnrollements.filter(
course => !codesList.includes(course.code) && course.enrollments
)
coursesToLoop = coursesToLoop.concat(coursesOnlyWithEnrollments)
for (const course of courses) {
const coursesByCode = keyBy(coursesToLoop, 'code')
for (const course of coursesToLoop) {
let { course_type } = course
let maincourse = course
@@ -1099,30 +1327,33 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
if (!stats[maincourse.code]) {
stats[maincourse.code] = new CourseStatsCounter(maincourse.code, maincourse.name, allstudentslength)
}
const coursestats = stats[maincourse.code]
coursestats.addCourseType(course_type.coursetypecode, course_type.name)
coursestats.addCourseSubstitutions(course.substitutions)
bottlenecks.coursetypes[course_type.coursetypecode] = course_type.name
course.credits.forEach(credit => {
const { studentnumber, passingGrade, improvedGrade, failingGrade, grade, date } = parseCreditInfo(credit)
const semester = getPassingSemester(startYear, date)
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
})
if (course.enrollments) {
course.enrollments.forEach(({ studentnumber, state, enrollment_date_time }) => {
const semester = getPassingSemester(startYear, enrollment_date_time)
coursestats.markEnrollment(studentnumber, state, semester)
if ((query?.selectedStudents && query?.selectedStudents.includes(studentnumber)) || !query?.selectedStudents) {
const semester = getPassingSemester(startYear, enrollment_date_time)
coursestats.markEnrollment(studentnumber, state, semester, enrollment_date_time)
}
})
}
if (course.credits) {
course.credits.forEach(credit => {
const { studentnumber, passingGrade, improvedGrade, failingGrade, grade, date } = parseCreditInfo(credit)
if ((query?.selectedStudents && query?.selectedStudents.includes(studentnumber)) || !query?.selectedStudents) {
const semester = getPassingSemester(startYear, date)
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
}
})
}
stats[maincourse.code] = coursestats
}
bottlenecks.coursestatistics = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
const allStats = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
bottlenecks.coursestatistics = allStats.filter(course => course.stats.students > 0)
bottlenecks.allStudents = allstudentslength
if (encryptdata) encryptStudentnumbers(bottlenecks)
@@ -2,6 +2,7 @@ const Sequelize = require('sequelize')
const { ExcludedCourse } = require('../models/models_kone')
const { Op } = Sequelize
const logger = require('../util/logger')
const { combinedStudyprogrammes } = require('./studyprogrammeHelpers.js')
const { dbConnections } = require('../database/connection')
@@ -27,6 +28,21 @@ const recursivelyGetModuleAndChildren = async (code, type) => {
}
}
// Why only finnish is used???
const labelProgammes = (filteredProgrammes, excludedProgrammes) => {
return filteredProgrammes.map(module => {
const label = {
id: module.label_name.fi,
label: `${module.label_code}\n${module.label_name.fi}`,
orderNumber: module.module_order,
}
// check if course is excluded, and hide if it is
const foundCourse = excludedProgrammes.find(course => course.course_code === module.code)
const visible = { visibility: !foundCourse, id: foundCourse ? foundCourse.id : null }
return { ...module, label, visible }
})
}
const byProgrammeCode = async code => {
const result = await recursivelyGetModuleAndChildren(code, 'course')
if (!result.length) return []
@@ -45,27 +61,52 @@ const byProgrammeCode = async code => {
)
// this labels the modules to match the old system in frontend
const labeled = filtered.map(module => {
const label = {
id: module.label_name.fi,
label: `${module.label_code}\n${module.label_name.fi}`,
orderNumber: module.module_order,
const labeled = labelProgammes(filtered, excluded)
// Some studyprogrammes (e.g. eläinlääkis) are combined as bachelor-master
const isCombined = Object.keys(combinedStudyprogrammes).includes(code)
let secondProgrammelabeled = []
if (isCombined) {
const secondProgCode = combinedStudyprogrammes[code]
const secondProgrammeResult = await recursivelyGetModuleAndChildren(secondProgCode, 'course')
// Now do the same things for the second set of results as for the first
if (secondProgrammeResult.length) {
const excludedSecondProgrammes = await ExcludedCourse.findAll({
where: {
programme_code: {
[Op.eq]: secondProgCode,
},
},
})
// filter out possible duplicates
const secondFiltered = secondProgrammeResult.filter(
(course, index, array) =>
array.findIndex(c => c.id == course.id && c.code == course.code && c.label_code == course.label_code) == index
)
// this labels the modules to match the old system in frontend
secondProgrammelabeled = labelProgammes(secondFiltered, excludedSecondProgrammes)
}
// check if course is excluded, and hide if it is
const foundCourse = excluded.find(course => course.course_code === module.code)
const visible = { visibility: !foundCourse, id: foundCourse ? foundCourse.id : null }
return { ...module, label, visible }
})
return labeled
}
// The second results are for combined programme views
return { defaultProgrammeCourses: labeled, secondProgrammeCourses: secondProgrammelabeled }
}
const addExcludedCourses = async (programmecode, coursecodes) => {
return ExcludedCourse.bulkCreate(coursecodes.map(c => ({ programme_code: programmecode, course_code: c })))
}
const modulesByProgrammeCode = async code => {
return await recursivelyGetModuleAndChildren(code, 'module')
const programmeResults = await recursivelyGetModuleAndChildren(code, 'module')
let secondProgrammeResults = []
if (Object.keys(combinedStudyprogrammes).includes(code)) {
const masterProgCode = combinedStudyprogrammes[code]
secondProgrammeResults = await recursivelyGetModuleAndChildren(masterProgCode, 'module')
}
// The second results are for combined programme views
return { defaultProgrammeResults: programmeResults, secondProgrammeResults: secondProgrammeResults }
}
const removeExcludedCourses = async ids => {
@@ -43,16 +43,7 @@ const getSemestersAndYears = async () => {
return result
}
const getMaxYearcode = async () => {
const aa = await Semester.findAll({
attributes: [[sequelize.fn('max', sequelize.col('yearcode')), 'maxYearCode']],
raw: true,
})
return aa[0].maxYearCode
}
module.exports = {
getSemestersAndYears,
getMaxYearcode,
getCurrentSemester,
}
+33 -1
View File
@@ -13,6 +13,7 @@ const {
SemesterEnrollment,
Semester,
Studyplan,
Transfer,
} = require('../models')
const { TagStudent, Tag } = require('../models/models_kone')
const Op = Sequelize.Op
@@ -60,7 +61,10 @@ const byId = async id => {
},
{
model: Studyplan,
attributes: ['included_courses', 'programme_code', 'completed_credits'],
attributes: ['included_courses', 'programme_code', 'completed_credits', 'studyrightid'],
},
{
model: Transfer,
},
],
}),
@@ -347,6 +351,20 @@ const studentnumberLike = terms => {
const bySearchTerm = async searchterm => {
const terms = splitByEmptySpace(searchterm)
const matches = await Student.findAll({
include: {
model: Studyright,
include: {
model: StudyrightElement,
include: {
model: ElementDetail,
where: {
type: {
[Op.in]: [10, 20, 30],
},
},
},
},
},
where: {
[Op.or]: [nameLike(terms), studentnumberLike(terms)],
},
@@ -357,6 +375,20 @@ const bySearchTerm = async searchterm => {
const bySearchTermAndStudentNumbers = async (searchterm, studentnumbers) => {
const terms = splitByEmptySpace(searchterm)
const matches = await Student.findAll({
include: {
model: Studyright,
include: {
model: StudyrightElement,
include: {
model: ElementDetail,
where: {
type: {
[Op.in]: [10, 20, 30],
},
},
},
},
},
where: {
[Op.and]: {
[Op.or]: [nameLike(terms), studentnumberLike(terms)],
@@ -1,43 +1,66 @@
const { StudyProgrammeCriteria } = require('../models/models_kone')
const { ProgressCriteria } = require('../models/models_kone')
const { Course } = require('../models')
const logger = require('../util/logger')
const getCriteriaByStudyProgramme = code =>
StudyProgrammeCriteria.findOne({
const getCriteriaByStudyProgramme = async code => {
if (code === '') return null
return await ProgressCriteria.findOne({
where: {
code: code,
},
})
}
const getSubstitutions = async codes => {
if (codes === []) return []
const courses = await Course.findAll({ where: { code: codes }, attributes: ['code', 'substitutions'], raw: true })
return courses.reduce((acc, { code, substitutions }) => ({ ...acc, [code]: substitutions }), {})
}
const formattedData = async data => {
const yearOne = data.coursesYearOne ? data.coursesYearOne : []
const yearTwo = data.coursesYearTwo ? data.coursesYearTwo : []
const yearThree = data.coursesYearThree ? data.coursesYearThree : []
const yearFour = data.coursesYearFour ? data.coursesYearFour : []
const yearFive = data.coursesYearFive ? data.coursesYearFive : []
const yearSix = data.coursesYearSix ? data.coursesYearSix : []
const courseCodes = [...yearOne, ...yearTwo, ...yearThree, ...yearFour, ...yearFive, ...yearSix]
const allCourses = await getSubstitutions(courseCodes)
const formattedData = updatedData => {
const criteria = {
courses: {
yearOne: updatedData.coursesYearOne,
yearTwo: updatedData.coursesYearTwo,
yearThree: updatedData.coursesYearThree,
},
courses: { yearOne, yearTwo, yearThree, yearFour, yearFive, yearSix },
allCourses,
credits: {
yearOne: updatedData.creditsYearOne,
yearTwo: updatedData.creditsYearTwo,
yearThree: updatedData.creditsYearThree,
yearOne: data.creditsYearOne ? data.creditsYearOne : 0,
yearTwo: data.creditsYearTwo ? data.creditsYearTwo : 0,
yearThree: data.creditsYearThree ? data.creditsYearThree : 0,
yearFour: data.creditsYearFour ? data.creditsYearFour : 0,
yearFive: data.creditsYearFive ? data.creditsYearFive : 0,
yearSix: data.creditsYearSix ? data.creditsYearSix : 0,
},
}
return criteria
}
const createCriteria = async (studyProgramme, coYear1, coYear2, coYear3, crYear1, crYear2, crYear3) => {
const createCriteria = async (studyProgramme, courseObj, creditsObj) => {
const newProgrammeCriteria = {
code: studyProgramme,
coursesYearOne: coYear1,
coursesYearTwo: coYear2,
coursesYearThree: coYear3,
creditsYearOne: crYear1,
creditsYearTwo: crYear2,
creditsYearThree: crYear3,
coursesYearOne: courseObj.year1,
coursesYearTwo: courseObj.year2,
coursesYearThree: courseObj.year3,
coursesYearFour: courseObj.year4,
coursesYearFive: courseObj.year5,
coursesYearSix: courseObj.year6,
creditsYearOne: creditsObj.year1,
creditsYearTwo: creditsObj.year2,
creditsYearThree: creditsObj.year3,
creditsYearFour: creditsObj.year4,
creditsYearFive: creditsObj.year5,
creditsYearSix: creditsObj.year6,
}
try {
const createdData = await StudyProgrammeCriteria.create(newProgrammeCriteria)
const criteria = formattedData(createdData)
return criteria
const createdData = await ProgressCriteria.create(newProgrammeCriteria)
return await formattedData(createdData)
} catch (error) {
logger.error(`Create criteria failed: ${error}`)
}
@@ -46,26 +69,28 @@ const createCriteria = async (studyProgramme, coYear1, coYear2, coYear3, crYear1
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 courseObj = { year1: [], year2: [], year3: [], year4: [], year5: [], year6: [] }
const creditsObj = {
year1: parseInt(credits.year1),
year2: parseInt(credits.year2),
year3: parseInt(credits.year3),
year4: parseInt(credits.year4),
year5: parseInt(credits.year5),
year6: parseInt(credits.year6),
}
return await createCriteria(studyProgramme, courseObj, creditsObj)
}
const yearlyCredits = {
creditsYearOne: parseInt(credits.year1),
creditsYearTwo: parseInt(credits.year2),
creditsYearThree: parseInt(credits.year3),
creditsYearFour: parseInt(credits.year4),
creditsYearFive: parseInt(credits.year5),
creditsYearSix: parseInt(credits.year6),
}
try {
const updatedData = await studyProgrammeToUpdate.update({ ...yearlyCredits })
const criteria = formattedData(updatedData)
return criteria
return await formattedData(updatedData)
} catch (error) {
logger.error(`Update yearly credit criteria failed: ${error}`)
}
@@ -73,17 +98,23 @@ const saveYearlyCreditCriteria = async (studyProgramme, credits) => {
const saveYearlyCourseCriteria = async (studyProgramme, courses, year) => {
const studyProgrammeToUpdate = await getCriteriaByStudyProgramme(studyProgramme)
if (!studyProgrammeToUpdate) {
const courseObj = { year1: [], year2: [], year3: [] }
const courseObj = { year1: [], year2: [], year3: [], year4: [], year5: [], year6: [] }
const creditObj = { year1: 0, year2: 0, year3: 0, year4: 0, year5: 0, year6: 0 }
if (year === 1) {
courseObj.year1 = courses
} else if (year === 2) {
courseObj.year2 = courses
} else {
} else if (year === 3) {
courseObj.year3 = courses
} else if (year === 4) {
courseObj.year4 = courses
} else if (year === 5) {
courseObj.year5 = courses
} else {
courseObj.year6 = courses
}
return await createCriteria(studyProgramme, courseObj.year1, courseObj.year2, courseObj.year3, 0, 0, 0)
return await createCriteria(studyProgramme, courseObj, creditObj)
}
try {
@@ -91,8 +122,10 @@ const saveYearlyCourseCriteria = async (studyProgramme, courses, year) => {
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
if (year === 4) updatedData = await studyProgrammeToUpdate.update({ coursesYearFour: courses })
if (year === 5) updatedData = await studyProgrammeToUpdate.update({ coursesYearFive: courses })
if (year === 6) updatedData = await studyProgrammeToUpdate.update({ coursesYearSix: courses })
return await formattedData(updatedData)
} catch (error) {
logger.error(`Update yearly credit criteria failed: ${error}`)
}
@@ -101,18 +134,11 @@ const saveYearlyCourseCriteria = async (studyProgramme, courses, year) => {
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,
},
courses: { yearOne: [], yearTwo: [], yearThree: [], yearFour: [], yearFive: [], yearSix: [] },
allCourses: [],
credits: { yearOne: 0, yearTwo: 0, yearThree: 0, yearFour: 0, yearFive: 0, yearSix: 0 },
}
return criteria
return studyProgrammeCriteria ? await formattedData(studyProgrammeCriteria) : criteria
}
module.exports = {
+202 -36
View File
@@ -12,13 +12,16 @@ const {
Studyright,
StudyrightElement,
ElementDetail,
ProgrammeModule,
Transfer,
Student,
SemesterEnrollment,
Semester,
Enrollment,
} = require('../models')
const { formatStudyright, formatStudent, formatTransfer } = require('./studyprogrammeHelpers')
const { getCurrentSemester } = require('./semesters')
const logger = require('../util/logger')
const whereStudents = studentnumbers => {
return studentnumbers ? studentnumbers : { [Op.not]: null }
@@ -50,7 +53,7 @@ const studytrackStudents = async studentnumbers =>
})
).map(formatStudent)
const enrolledStudents = async (studytrack, since, studentnumbers) => {
const enrolledStudents = async (studytrack, studentnumbers) => {
const currentSemester = await getCurrentSemester()
const students = await Student.findAll({
@@ -68,14 +71,17 @@ const enrolledStudents = async (studytrack, since, studentnumbers) => {
},
],
attributes: ['studyrightid'],
where: {
graduated: 0,
active: 1,
},
},
{
model: SemesterEnrollment,
attributes: ['semestercode'],
attributes: ['semestercode', 'enrollmenttype'],
include: [
{
model: Semester,
required: true,
where: {
semestercode: currentSemester.semestercode,
},
@@ -113,6 +119,10 @@ const absentStudents = async (studytrack, studentnumbers) => {
},
],
attributes: ['studyrightid'],
where: {
graduated: 0,
active: 1,
},
},
{
model: SemesterEnrollment,
@@ -190,6 +200,38 @@ const startedStudyrights = async (studytrack, since, studentnumbers) =>
})
).map(formatStudyright)
const graduatedStudyRightsByStartDate = async (studytrack, since, academicEnddate) =>
(
await Studyright.findAll({
include: [
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
where: {
code: studytrack,
},
},
},
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
where: {
graduated: 1,
startdate: {
[Op.between]: [since, academicEnddate],
},
student_studentnumber: {
[Op.not]: null,
},
},
})
).map(formatStudyright)
const graduatedStudyRights = async (studytrack, since, studentnumbers) =>
(
await Studyright.findAll({
@@ -220,37 +262,46 @@ const graduatedStudyRights = async (studytrack, since, studentnumbers) =>
).map(formatStudyright)
const inactiveStudyrights = async (studytrack, studentnumbers) => {
const now = moment(new Date())
const inactiveOrExpired = (
await Studyright.findAll({
include: [
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
const currentSemester = await getCurrentSemester()
const students = await Student.findAll({
attributes: ['studentnumber'],
include: [
{
model: Studyright,
required: true,
include: [
{
model: StudyrightElement,
required: true,
where: {
code: studytrack,
},
},
],
attributes: ['studyrightid', 'enddate'],
where: {
graduated: 0,
active: 0,
},
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
where: {
student_studentnumber: whereStudents(studentnumbers),
graduated: 0,
},
})
)
.filter(s => s.active === 0 || (s.enddate && moment(s.enddate).isBefore(now)))
.map(formatStudyright)
{
model: SemesterEnrollment,
attributes: ['semestercode', 'enrollmenttype'],
},
],
where: {
studentnumber: {
[Op.in]: studentnumbers,
},
},
})
return inactiveOrExpired
return students.filter(
student =>
student.studyrights[0].enddate <= new Date() ||
!student.semester_enrollments.find(enrollment => enrollment.semestercode === currentSemester) ||
student.semester_enrollments.find(enrollment => enrollment.semestercode === currentSemester).enrollmenttype === 3
)
}
const followingStudyrights = async (since, programmes, studentnumbers) =>
@@ -356,7 +407,7 @@ const allTransfers = async (studytrack, since) => {
const getProgrammesStudyrights = async studyprogramme =>
(
await Studyright.findAll({
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode', 'cancelled'],
include: [
{
model: StudyrightElement,
@@ -379,7 +430,16 @@ const getProgrammesStudyrights = async studyprogramme =>
const getStudyRights = async students =>
(
await Studyright.findAll({
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
attributes: [
'studyrightid',
'startdate',
'studystartdate',
'enddate',
'graduated',
'prioritycode',
'extentcode',
'cancelled',
],
where: {
studentStudentnumber: students,
},
@@ -415,9 +475,7 @@ const getCreditsForStudyProgramme = async (codes, since) => {
},
},
where: {
credittypecode: {
[Op.notIn]: [10, 9, 7],
},
credittypecode: 4,
isStudyModule: {
[Op.not]: true,
},
@@ -473,6 +531,7 @@ const getAllProgrammeCourses = async providerCode => {
}
const getStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
if (!programmeCourses.length) return []
try {
const res = await sequelize.query(
`
@@ -498,17 +557,110 @@ const getStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
code: course.code,
name: course.course_name,
year: course.year,
totalAllStudents: parseInt(course.total_students),
totalPassed: parseInt(course.total_students),
totalAllcredits: parseInt(course.total_credits),
type: 'total',
type: 'passed',
}))
} catch (e) {
// eslint-disable-next-line no-console
console.log(e)
logger.log(e)
}
}
const getNotCompletedForProgrammeCourses = async (from, to, programmeCourses) => {
try {
const enrollmentsCourses = await Enrollment.findAll({
attributes: ['studentnumber', 'course_code'],
where: {
course_code: {
[Op.in]: programmeCourses,
},
studentnumber: {
[Op.ne]: null,
},
enrollment_date_time: {
[Op.between]: [from, to],
},
state: 'ENROLLED',
},
})
const allEnrollments = {}
for (const { studentnumber, course_code } of enrollmentsCourses) {
if (!(course_code in allEnrollments)) {
allEnrollments[course_code] = []
}
allEnrollments[course_code].push(studentnumber)
}
const credits = await Credit.findAll({
attributes: ['id', 'course_code', 'student_studentnumber', 'credittypecode', 'attainment_date'],
include: {
model: Course,
attributes: ['code', 'name'],
required: true,
where: {
is_study_module: false,
code: programmeCourses,
},
},
where: {
credittypecode: [4, 7, 10],
isStudyModule: {
[Op.not]: true,
},
attainment_date: {
[Op.between]: [from, to],
},
},
})
const creditsObj = credits.map(credit => {
return {
code: credit.course_code,
studentnumber: credit.student_studentnumber,
credittype: credit.credittypecode,
courseName: credit.course.name,
year: moment(credit.attainment_date).year(),
}
})
const passedByCourseCodes = {}
const notCompletedByCourseCodes = {}
const courses = {}
for (const course of creditsObj) {
if (!(course.code in courses)) {
courses[course.code] = {
code: course.code,
name: course.courseName,
year: course.year,
}
passedByCourseCodes[course.code] = []
notCompletedByCourseCodes[course.code] = []
}
if ([4, 7].includes(course.credittype)) {
passedByCourseCodes[course.code].push(course.studentnumber)
}
if (
(allEnrollments[course.code]?.includes(course.studentnumber) || course.credittype === 10) &&
!passedByCourseCodes[course.code].includes(course.studentnumber)
) {
notCompletedByCourseCodes[course.code].push(course.studentnumber)
}
}
return Object.keys(courses)
.reduce((acc, val) => [...acc, { ...courses[val] }], [])
.map(course => ({
code: course.code,
name: course.name,
year: course.year,
totalNotCompleted: [...new Set(notCompletedByCourseCodes[course.code])].length,
type: 'notCompleted',
}))
} catch (e) {
logger.log(e)
}
}
const getOwnStudentsForProgrammeCourses = async (from, to, programmeCourses, studyprogramme) => {
if (!programmeCourses.length) return []
const res = await sequelize.query(
`
WITH Dist AS (
@@ -534,7 +686,6 @@ const getOwnStudentsForProgrammeCourses = async (from, to, programmeCourses, stu
}
)
return res.map(course => {
// console.log('course: ', course)
const res = {
code: course.code,
name: course.course_name,
@@ -547,6 +698,7 @@ const getOwnStudentsForProgrammeCourses = async (from, to, programmeCourses, stu
}
const getOtherStudentsForProgrammeCourses = async (from, to, programmeCourses, studyprogramme) => {
if (!programmeCourses.length) return []
const res = await sequelize.query(
`
WITH Dist AS (
@@ -589,6 +741,7 @@ const getOtherStudentsForProgrammeCourses = async (from, to, programmeCourses, s
}
const getTransferStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
if (!programmeCourses.length) return []
const res = await sequelize.query(
`
WITH Dist AS (
@@ -619,6 +772,7 @@ const getTransferStudentsForProgrammeCourses = async (from, to, programmeCourses
}
const getStudentsWithoutStudyrightForProgrammeCourses = async (from, to, programmeCourses) => {
if (!programmeCourses.length) return []
const res = await sequelize.query(
`
WITH Dist AS (
@@ -732,6 +886,15 @@ const getCurrentStudyYearStartDate = _.memoize(
)
)
const getProgrammeName = async code => {
return ProgrammeModule.findOne({
attributes: ['name'],
where: {
code,
},
})
}
module.exports = {
studytrackStudents,
enrolledStudents,
@@ -739,6 +902,7 @@ module.exports = {
allStudyrights,
startedStudyrights,
graduatedStudyRights,
graduatedStudyRightsByStartDate,
inactiveStudyrights,
previousStudyrights,
followingStudyrights,
@@ -758,4 +922,6 @@ module.exports = {
getStudyRights,
getCourseCodesForStudyProgramme,
getTransferStudentsForProgrammeCourses,
getProgrammeName,
getNotCompletedForProgrammeCourses,
}
@@ -38,7 +38,7 @@ const getStartedStats = async ({ studyprogramme, since, years, isAcademicYear, i
const getGraduatedStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const { graphStats, tableStats } = getStatsBasis(years)
if (!studyprogramme) return { graphStats, tableStats }
const studentnumbersOfTheYear = await getCorrectStudentnumbers({
codes: [studyprogramme],
startDate: alltimeStartDate,
@@ -62,8 +62,9 @@ const getGraduatedStats = async ({ studyprogramme, since, years, isAcademicYear,
return { graphStats, tableStats }
}
const getTransferredAwayStats = async ({ studyprogramme, since, years, isAcademicYear }) => {
const getTransferredAwayStats = async ({ studyprogramme, since, years, isAcademicYear, combinedProgramme }) => {
const studyrights = await transfersAway(studyprogramme, since)
const secondStudyrights = combinedProgramme ? await transfersAway(combinedProgramme, since) : []
const { graphStats, tableStats } = getStatsBasis(years)
studyrights.forEach(({ transferdate }) => {
@@ -72,11 +73,18 @@ const getTransferredAwayStats = async ({ studyprogramme, since, years, isAcademi
tableStats[transferYear] += 1
})
secondStudyrights.forEach(({ transferdate }) => {
const transferYear = defineYear(transferdate, isAcademicYear)
graphStats[indexOf(years, transferYear)] += 1
tableStats[transferYear] += 1
})
return { graphStats, tableStats }
}
const getTransferredToStats = async ({ studyprogramme, since, years, isAcademicYear }) => {
const getTransferredToStats = async ({ studyprogramme, since, years, isAcademicYear, combinedProgramme }) => {
const studyrights = await transfersTo(studyprogramme, since)
const secondStudyrights = combinedProgramme ? await transfersTo(combinedProgramme, since) : []
const { graphStats, tableStats } = getStatsBasis(years)
studyrights.forEach(({ transferdate }) => {
@@ -85,66 +93,163 @@ const getTransferredToStats = async ({ studyprogramme, since, years, isAcademicY
tableStats[transferYear] += 1
})
secondStudyrights.forEach(({ transferdate }) => {
const transferYear = defineYear(transferdate, isAcademicYear)
graphStats[indexOf(years, transferYear)] += 1
tableStats[transferYear] += 1
})
return { graphStats, tableStats }
}
const getBasicStatsForStudytrack = async ({ studyprogramme, settings }) => {
const initializeGraphStats = (
includeAllSpecials,
combinedProgramme,
started,
startedSecondProg,
graduated,
transferredAway,
transferredTo,
graduatedSecondProg
) => {
let basicTable = []
if (combinedProgramme) {
basicTable = [
{
name: 'Started bachelor',
data: started.graphStats,
},
{
name: 'Started licentiate',
data: startedSecondProg.graphStats,
},
{
name: 'Graduated bachelor',
data: graduated.graphStats,
},
{
name: 'Graduated licentiate',
data: graduatedSecondProg.graphStats,
},
]
} else {
basicTable = [
{
name: 'Started',
data: started.graphStats,
},
{
name: 'Graduated',
data: graduated.graphStats,
},
]
}
if (includeAllSpecials) {
basicTable.push({
name: 'Transferred away',
data: transferredAway.graphStats,
})
basicTable.push({
name: 'Transferred To',
data: transferredTo.graphStats,
})
}
return basicTable
}
const initializeTableStats = (
year,
combinedProgramme,
includeAllSpecials,
started,
startedSecondProg,
graduated,
graduatedSecondProg,
transferredAway,
transferredTo
) => {
if (includeAllSpecials && combinedProgramme) {
return [
year,
started.tableStats[year],
startedSecondProg.tableStats[year],
graduated.tableStats[year],
graduatedSecondProg.tableStats[year],
transferredAway.tableStats[year],
transferredTo.tableStats[year],
]
}
if (combinedProgramme) {
return [
year,
started.tableStats[year],
startedSecondProg.tableStats[year],
graduated.tableStats[year],
graduatedSecondProg.tableStats[year],
]
}
if (includeAllSpecials) {
return [
year,
started.tableStats[year],
graduated.tableStats[year],
transferredAway.tableStats[year],
transferredTo.tableStats[year],
]
}
return [year, started.tableStats[year], graduated.tableStats[year]]
}
const getBasicStatsForStudytrack = async ({ studyprogramme, combinedProgramme, settings }) => {
const { includeAllSpecials, isAcademicYear } = settings
const since = getStartDate(studyprogramme, isAcademicYear)
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials }
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials, combinedProgramme }
const queryParametersCombinedProg = {
studyprogramme: combinedProgramme,
since,
years,
isAcademicYear,
includeAllSpecials,
}
const started = await getStartedStats(queryParameters)
const startedSecondProg = await getStartedStats(queryParametersCombinedProg)
const graduated = await getGraduatedStats(queryParameters)
const graduatedSecondProg = await getGraduatedStats(queryParametersCombinedProg)
const transferredAway = await getTransferredAwayStats(queryParameters)
const transferredTo = await getTransferredToStats(queryParameters)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const titles = tableTitles['basics'][includeAllSpecials ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED']
const key = includeAllSpecials ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED'
const titles = tableTitles['basics'][combinedProgramme ? `${key}_COMBINED_PROGRAMME` : key]
const tableStats = reversedYears.map(year =>
includeAllSpecials
? [
year,
started.tableStats[year],
graduated.tableStats[year],
transferredAway.tableStats[year],
transferredTo.tableStats[year],
]
: [year, started.tableStats[year], graduated.tableStats[year]]
initializeTableStats(
year,
combinedProgramme,
includeAllSpecials,
started,
startedSecondProg,
graduated,
graduatedSecondProg,
transferredAway,
transferredTo
)
)
return {
id: studyprogramme,
id: combinedProgramme ? `${studyprogramme}-${combinedProgramme}` : studyprogramme,
years,
graphStats: includeAllSpecials
? [
{
name: 'Started',
data: started.graphStats,
},
{
name: 'Graduated',
data: graduated.graphStats,
},
{
name: 'Transferred away',
data: transferredAway.graphStats,
},
{
name: 'Transferred To',
data: transferredTo.graphStats,
},
]
: [
{
name: 'Started',
data: started.graphStats,
},
{
name: 'Graduated',
data: graduated.graphStats,
},
],
graphStats: initializeGraphStats(
includeAllSpecials,
combinedProgramme,
started,
startedSecondProg,
graduated,
transferredAway,
transferredTo,
graduatedSecondProg
),
tableStats,
titles,
}
@@ -4,6 +4,7 @@ const {
getStudentsForProgrammeCourses,
getCurrentStudyYearStartDate,
getOwnStudentsForProgrammeCourses,
getNotCompletedForProgrammeCourses,
getStudentsWithoutStudyrightForProgrammeCourses,
getTransferStudentsForProgrammeCourses,
getOtherStudentsForProgrammeCourses,
@@ -39,9 +40,12 @@ const makeYearlyPromises = (years, academicYear, type, programmeCourses, studypr
let result = null
switch (type) {
case 'allStudents':
case 'passed':
result = await getStudentsForProgrammeCourses(from, to, programmeCourses)
break
case 'notCompleted':
result = await getNotCompletedForProgrammeCourses(from, to, programmeCourses)
break
case 'ownStudents':
result = await getOwnStudentsForProgrammeCourses(from, to, programmeCourses, studyprogramme)
break
@@ -68,13 +72,21 @@ const makeYearlyPromises = (years, academicYear, type, programmeCourses, studypr
)
}
const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme, academicYear) => {
const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme, academicYear, combinedProgramme) => {
const startDate = academicYear == 'true' ? await getCurrentStudyYearStartDate(unixMillis) : getCurrentYearStartDate()
const startYear = startDate.getFullYear()
const yearRange = _.range(2017, startYear + 1)
const programmeCourses = await getAllStudyprogrammeCourses(studyprogramme)
const mainProgrammeCourses = await getAllStudyprogrammeCourses(studyprogramme)
const secondProgrammeCourses = combinedProgramme ? await getAllStudyprogrammeCourses(combinedProgramme) : []
const programmeCourses = [...mainProgrammeCourses, ...secondProgrammeCourses]
const yearlyStudentByCoursePromises = makeYearlyPromises(yearRange, academicYear, 'allStudents', programmeCourses)
const yearlyPassedStudentByCoursePromises = makeYearlyPromises(yearRange, academicYear, 'passed', programmeCourses)
const yearlyNotCompletedStudentByCoursePromises = makeYearlyPromises(
yearRange,
academicYear,
'notCompleted',
programmeCourses
)
const yearlyProgrammeStudentsPromises = makeYearlyPromises(
yearRange,
academicYear,
@@ -105,22 +117,24 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
)
const [
yearlyStudentByCourse,
yearlyPassedStudentByCourse,
yearlyNotCompletedStudentByCourse,
yearlyProgrammeStudents,
yearlyStudentsWithoutStudyright,
yearlyOtherProgrammeStudents,
yearlyTransferStudents,
] = await Promise.all([
Promise.all(yearlyStudentByCoursePromises),
Promise.all(yearlyPassedStudentByCoursePromises),
Promise.all(yearlyNotCompletedStudentByCoursePromises),
Promise.all(yearlyProgrammeStudentsPromises),
Promise.all(yearlyStudentsWithoutStudyrightPromises),
Promise.all(yearlyOtherProgrammeStudentsPromises),
Promise.all(yearlyTransferStudentsPromises),
])
let maxYear = 0
const allCourses = [
...yearlyStudentByCourse.flat(),
...yearlyPassedStudentByCourse.flat(),
...yearlyNotCompletedStudentByCourse.flat(),
...yearlyProgrammeStudents.flat(),
...yearlyStudentsWithoutStudyright.flat(),
...yearlyOtherProgrammeStudents.flat(),
@@ -134,9 +148,12 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
years: {},
}
}
if (!acc[curr.code]['years'][curr.year]) {
acc[curr.code]['years'][curr.year] = {
totalAllStudents: 0,
totalPassed: 0,
totalNotCompleted: 0,
totalAllCredits: 0,
totalProgrammeStudents: 0,
totalProgrammeCredits: 0,
@@ -149,10 +166,16 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
}
}
switch (curr.type) {
case 'total':
acc[curr.code]['years'][curr.year]['totalAllStudents'] += curr.totalAllStudents
case 'passed':
acc[curr.code]['years'][curr.year]['totalPassed'] += curr.totalPassed
acc[curr.code]['years'][curr.year]['totalAllStudents'] += acc[curr.code]['years'][curr.year]['totalPassed']
acc[curr.code]['years'][curr.year]['totalAllCredits'] += curr.totalAllcredits
break
case 'notCompleted':
acc[curr.code]['years'][curr.year]['totalNotCompleted'] += curr.totalNotCompleted
acc[curr.code]['years'][curr.year]['totalAllStudents'] +=
acc[curr.code]['years'][curr.year]['totalNotCompleted']
break
case 'ownProgramme':
acc[curr.code]['years'][curr.year]['totalProgrammeStudents'] += curr.totalProgrammeStudents
acc[curr.code]['years'][curr.year]['totalProgrammeCredits'] += curr.totalProgrammeCredits
@@ -175,6 +198,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
const ayCourses = Object.keys(allCourses).filter(c => c.startsWith('AY'))
const properties = [
'totalAllStudents',
'totalPassed',
'totalNotCompleted',
'totalAllCredits',
'totalProgrammeStudents',
'totalProgrammeCredits',
@@ -200,6 +225,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
if (!allCourses[normCode]['years'][year]) {
mergedCourse['years'][year] = {
totalAllStudents: 0,
totalPassed: 0,
totalNotCompleted: 0,
totalAllCredits: 0,
totalProgrammeStudents: 0,
totalProgrammeCredits: 0,
@@ -19,97 +19,28 @@ const {
getCourseCodesForStudyProgramme,
} = require('./studyprogramme')
// Fetches all the credits for the studyprogramme and divides them into major-students and non-major students credits
// Division is done on the basis that whether the student had a primary studyright to the programme on the attainment_date
// If special groups are excluded, the transfer students are filtered away from the major-students as well
const getRegularCreditStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const providercode = mapToProviders([studyprogramme])[0]
const courses = await getCourseCodesForStudyProgramme(providercode)
const credits = await getCreditsForStudyProgramme(courses, since)
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
//let studyrights = await getProgrammesStudyrights(studyprogramme, since)
let studyrights = await getStudyRights(students, since)
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid)
if (!includeAllSpecials) {
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
}
let majors = getStatsBasis(years)
let nonMajors = getStatsBasis(years)
let nonDegree = getStatsBasis(years)
credits.forEach(({ student_studentnumber, attainment_date, credits }) => {
const studentStudyrights = studyrights.filter(studyright => studyright.studentnumber === student_studentnumber)
const attainmentYear = defineYear(attainment_date, isAcademicYear)
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainment_date, transfers)) {
return
}
if (isMajorStudentCredit(studentStudyrights, attainment_date, studyprogramme)) {
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
majors.tableStats[attainmentYear] += credits || 0
} else if (isNonMajorCredit(studentStudyrights, attainment_date, studyprogramme)) {
nonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
nonMajors.tableStats[attainmentYear] += credits || 0
} else {
nonDegree.graphStats[indexOf(years, attainmentYear)] += credits || 0
nonDegree.tableStats[attainmentYear] += credits || 0
}
})
if (
majors.graphStats.every(year => year === 0) &&
nonMajors.graphStats.every(year => year === 0) &&
nonDegree.graphStats.every(year => year === 0)
) {
return {
majors: { graphStats: [], tableStats: {} },
nonMajors: { graphStats: [], tableStats: {} },
nonDegree: { graphStats: [], tablestats: {} },
}
}
return { majors, nonMajors, nonDegree }
const createGraphStats = (majors, nonMajors, nonDegree, transferred) => {
return [
{
name: 'Major students credits',
data: majors.graphStats,
},
{
name: 'Non-major students credits',
data: nonMajors.graphStats,
},
{
name: 'Non-degree credits',
data: nonDegree.graphStats,
},
{
name: 'Transferred credits',
data: transferred.graphStats,
},
]
}
// Fetches all the credits with the type "transferred / hyväksiluettu" and divides them by year
const getTransferredCreditStats = async ({ studyprogramme, since, years, isAcademicYear }) => {
const providercode = mapToProviders([studyprogramme])[0]
const credits = await getTransferredCredits(providercode, since)
const { graphStats, tableStats } = getStatsBasis(years)
credits.forEach(({ attainment_date, credits }) => {
const attainmentYear = defineYear(attainment_date, isAcademicYear)
graphStats[indexOf(years, attainmentYear)] += credits || 0
tableStats[attainmentYear] += credits || 0
})
if (graphStats.every(year => year === 0)) {
return { graphStats: [], tableStats: {} }
}
return { graphStats, tableStats }
}
// Fetches all credits for the studytrack and combines them into the statistics for the table
// and graph in the studyprogramme overview
const getCreditStatsForStudytrack = async ({ studyprogramme, settings }) => {
const { isAcademicYear, includeAllSpecials } = settings
const since = getStartDate(studyprogramme, isAcademicYear)
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials }
const { majors, nonMajors, nonDegree } = await getRegularCreditStats(queryParameters)
const transferred = await getTransferredCreditStats(queryParameters)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const titles = tableTitles['credits'][includeAllSpecials ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED']
const dataFound = [majors, nonMajors, transferred].some(d => d.graphStats.length)
if (!dataFound) return null
const createTableStats = (reversedYears, includeAllSpecials, majors, nonMajors, transferred, nonDegree) => {
const tableStats = reversedYears.map(year =>
includeAllSpecials
? [
@@ -133,31 +64,117 @@ const getCreditStatsForStudytrack = async ({ studyprogramme, settings }) => {
transferred.tableStats[year],
]
)
return tableStats
}
const graphStats = [
{
name: 'Major students credits',
data: majors.graphStats,
},
{
name: 'Non-major students credits',
data: nonMajors.graphStats,
},
{
name: 'Non-degree credits',
data: nonDegree.graphStats,
},
{
name: 'Transferred credits',
data: transferred.graphStats,
},
]
// Fetches all the credits for the studyprogramme and divides them into major-students and non-major students credits
// Division is done on the basis that whether the student had a primary studyright to the programme on the attainment_date
// If special groups are excluded, the transfer students are filtered away from the major-students as well
const getRegularCreditStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const providercode = mapToProviders([studyprogramme])[0]
const courses = await getCourseCodesForStudyProgramme(providercode)
const credits = await getCreditsForStudyProgramme(courses, since)
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
return {
id: studyprogramme,
//let studyrights = await getProgrammesStudyrights(studyprogramme, since)
let studyrights = await getStudyRights(students, since)
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid)
if (!includeAllSpecials) {
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
}
let majors = getStatsBasis(years)
let nonMajors = getStatsBasis(years)
let nonDegree = getStatsBasis(years)
credits.forEach(({ student_studentnumber, attainment_date, credits }) => {
const studentStudyrights = studyrights.filter(studyright => studyright.studentnumber === student_studentnumber)
const attainmentYear = defineYear(attainment_date, isAcademicYear)
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainment_date, transfers)) {
return
}
if (isMajorStudentCredit(studentStudyrights, attainment_date, studyprogramme)) {
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
majors.tableStats[attainmentYear] += credits || 0
} else if (isNonMajorCredit(studentStudyrights, attainment_date, studyprogramme)) {
nonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
nonMajors.tableStats[attainmentYear] += credits || 0
} else {
nonDegree.graphStats[indexOf(years, attainmentYear)] += credits || 0
nonDegree.tableStats[attainmentYear] += credits || 0
}
})
return { majors, nonMajors, nonDegree }
}
// Fetches all the credits with the type "transferred / hyväksiluettu" and divides them by year
const getTransferredCreditStats = async ({ studyprogramme, since, years, isAcademicYear }) => {
const providercode = mapToProviders([studyprogramme])[0]
const credits = await getTransferredCredits(providercode, since)
const { graphStats, tableStats } = getStatsBasis(years)
credits.forEach(({ attainment_date, credits }) => {
const attainmentYear = defineYear(attainment_date, isAcademicYear)
graphStats[indexOf(years, attainmentYear)] += credits || 0
tableStats[attainmentYear] += credits || 0
})
return { graphStats, tableStats }
}
// Fetches all credits for the studytrack and combines them into the statistics for the table
// and graph in the studyprogramme overview
const getCreditStatsForStudytrack = async ({ studyprogramme, combinedProgramme, settings }) => {
const { isAcademicYear, includeAllSpecials } = settings
const since = getStartDate(studyprogramme, isAcademicYear)
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials }
const queryParametersSecondProg = {
studyprogramme: combinedProgramme,
since,
years,
tableStats,
graphStats,
isAcademicYear,
includeAllSpecials,
}
const { majors, nonMajors, nonDegree } = await getRegularCreditStats(queryParameters)
const transferred = await getTransferredCreditStats(queryParameters)
const {
majors: majorsSecondProg,
nonMajors: nonMajorsSecondProg,
nonDegree: nonDegreeSecondProg,
} = await getRegularCreditStats(queryParametersSecondProg)
const transferredSecondProg = await getTransferredCreditStats(queryParametersSecondProg)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const titles = tableTitles['credits'][includeAllSpecials ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED']
const dataFound = [majors, nonMajors, transferred].some(d => d.graphStats.length)
if (!dataFound) return null
const dataFoundSecondProg = [majorsSecondProg, nonMajorsSecondProg, transferredSecondProg].some(
d => d.graphStats.length
)
return {
id: combinedProgramme ? `${studyprogramme}-${combinedProgramme}` : studyprogramme,
years,
tableStats: createTableStats(reversedYears, includeAllSpecials, majors, nonMajors, transferred, nonDegree),
graphStats: createGraphStats(majors, nonMajors, nonDegree, transferred),
tableStatsSecondProg: dataFoundSecondProg
? createTableStats(
reversedYears,
includeAllSpecials,
majorsSecondProg,
nonMajorsSecondProg,
transferredSecondProg,
nonDegreeSecondProg
)
: [],
graphStatsSecondProg: dataFoundSecondProg
? createGraphStats(majorsSecondProg, nonMajorsSecondProg, nonDegreeSecondProg, transferredSecondProg)
: [],
titles,
}
}
@@ -50,7 +50,7 @@ const addGraduation = async (studyright, isAcademicYear, amounts, times) => {
const getGraduatedStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const { graphStats, tableStats } = getStatsBasis(years)
if (!studyprogramme) return { graphStats, tableStats }
const studentnumbers = await getCorrectStudentnumbers({
codes: [studyprogramme],
startDate: alltimeStartDate,
@@ -75,9 +75,10 @@ const getGraduatedStats = async ({ studyprogramme, since, years, isAcademicYear,
}
const getThesisStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const { graphStats, tableStats } = getStatsBasis(years)
if (!studyprogramme) return { graphStats, tableStats }
const providercode = mapToProviders([studyprogramme])[0]
const thesisType = getThesisType(studyprogramme)
const { graphStats, tableStats } = getStatsBasis(years)
const studentnumbers = await getCorrectStudentnumbers({
codes: [studyprogramme],
@@ -100,6 +101,7 @@ const getThesisStats = async ({ studyprogramme, since, years, isAcademicYear, in
const getGraduationTimeStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
let graduationAmounts = getYearsObject({ years })
let graduationTimes = getYearsObject({ years, emptyArrays: true })
if (!studyprogramme) return { times: { medians: [], goal: 0 }, doCombo: false, comboTimes: { medians: [], goal: 0 } }
// for bc+ms combo
const doCombo = studyprogramme.startsWith('MH') && !['MH30_001', 'MH30_003'].includes(studyprogramme)
let graduationAmountsCombo = getYearsObject({ years })
@@ -244,17 +246,26 @@ const getProgrammesBeforeOrAfter = async (studyprogramme, queryParameters) => {
return null
}
const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) => {
const getGraduationStatsForStudytrack = async ({ studyprogramme, combinedProgramme, settings }) => {
const { isAcademicYear, includeAllSpecials } = settings
const since = getStartDate(studyprogramme, isAcademicYear)
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials }
const combinedQueryParameters = {
studyprogramme: combinedProgramme,
since,
years,
isAcademicYear,
includeAllSpecials,
}
const thesis = await getThesisStats(queryParameters)
const thesisSecondProgramme = await getThesisStats(combinedQueryParameters)
const graduated = await getGraduatedStats(queryParameters)
const graduatedSecondProgramme = await getGraduatedStats(combinedQueryParameters)
const graduationTimeStats = await getGraduationTimeStats(queryParameters)
const graduationTimeStatsSecondProg = await getGraduationTimeStats(combinedQueryParameters)
const programmesBeforeOrAfter = await getProgrammesBeforeOrAfter(studyprogramme, queryParameters)
@@ -264,17 +275,64 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) =>
studyprogramme.includes('LIS') || studyprogramme.includes('T')
? ['', 'Graduated']
: ['', 'Graduated', 'Wrote thesis']
const combinedTitles = [
'',
'Graduated bachelor',
'Wrote thesis bachelor',
'Graduated licentiate',
'Wrote thesis licentiate',
]
const programmesBeforeOrAfterTitles = ['Code', 'Id', 'Programme', ...years]
const tableStatsDefault = combinedProgramme
? reversedYears.map(year => [
year,
graduated.tableStats[year],
thesis.tableStats[year],
graduatedSecondProgramme.tableStats[year],
thesisSecondProgramme.tableStats[year],
])
: 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]])
: tableStatsDefault
const graphStats = combinedProgramme
? [
{
name: 'Graduated bachelor',
data: graduated.graphStats,
},
{
name: 'Wrote thesis bachelor',
data: thesis.graphStats,
},
{
name: 'Graduated licentiate',
data: graduatedSecondProgramme.graphStats,
},
{
name: 'Wrote thesis licentiate',
data: thesisSecondProgramme.graphStats,
},
]
: [
{
name: 'Graduated students',
data: graduated.graphStats,
},
{
name: 'Wrote thesis',
data: thesis.graphStats,
},
]
return {
id: studyprogramme,
id: combinedProgramme ? `${studyprogramme}-${combinedProgramme}` : studyprogramme,
years,
tableStats,
titles,
titles: combinedProgramme ? combinedTitles : titles,
graphStats:
studyprogramme.includes('LIS') || studyprogramme.includes('T')
? [
@@ -283,19 +341,11 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) =>
data: graduated.graphStats,
},
]
: [
{
name: 'Graduated students',
data: graduated.graphStats,
},
{
name: 'Wrote thesis',
data: thesis.graphStats,
},
],
: graphStats,
graduationTimes: graduationTimeStats.times,
doCombo: graduationTimeStats.doCombo,
comboTimes: graduationTimeStats.comboTimes,
graduationTimesSecondProgramme: graduationTimeStatsSecondProg.comboTimes,
programmesBeforeOrAfterTableStats: programmesBeforeOrAfter?.tableStats,
programmesBeforeOrAfterGraphStats: programmesBeforeOrAfter?.graphStats,
programmesBeforeOrAfterTitles,
@@ -7,8 +7,8 @@ const getCorrectStudentnumbers = async ({ codes, startDate, endDate, includeAllS
const exchangeStudents = includeAllSpecials
const nondegreeStudents = includeAllSpecials
const transferredOutStudents = includeAllSpecials
const transferredToStudents = !includeAllSpecials
const graduatedStudents = !includeGraduated
const transferredToStudents = includeAllSpecials
const graduatedStudents = includeGraduated
studentnumbers = await studentnumbersWithAllStudyrightElements(
codes,
@@ -54,6 +54,7 @@ const formatStudyright = studyright => {
extentcode,
student,
studyright_elements,
cancelled,
} = studyright
return {
@@ -69,6 +70,7 @@ const formatStudyright = studyright => {
studentnumber: student.studentnumber,
code: resolveStudyRightCode(studyright_elements),
studyrightElements: studyright_elements,
cancelled,
name:
studyright_elements?.length && studyright_elements[0].element_detail && studyright_elements[0].element_detail.name
? studyright_elements[0].element_detail.name
@@ -101,7 +103,9 @@ const getYearsArray = (since, isAcademicYear, yearsCombined) => {
const years = []
const allYears = 'Total'
if (yearsCombined) years.push(allYears)
for (let i = since; i <= new Date().getFullYear(); i++) {
const today = new Date()
const until = isAcademicYear && today.getMonth() < 7 ? today.getFullYear() - 1 : today.getFullYear()
for (let i = since; i <= until; i++) {
const year = isAcademicYear ? `${i} - ${i + 1}` : i
years.push(year)
}
@@ -152,16 +156,15 @@ const isNonMajorCredit = (studyrights, attainment_date) =>
)
})
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) => {
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) =>
studyrights.some(studyright => {
if (!studyright) return true // If there is no studyright matching the credit, is not a major student credit
if (studyright.studystartdate > attainment_date) return true // Credits before the studyright started are not major student credits
if (studyright.enddate && attainment_date > studyright.enddate) return true // Credits after studyright are not major student credits
if (studyright.studystartdate && new Date(studyright.studystartdate) > new Date(attainment_date)) return true // Credits before the studyright started are not major student credits
if (studyright.enddate && new Date(attainment_date) > new Date(studyright.enddate)) return true // Credits after studyright are not major student credits
if ([7, 9, 16, 34, 22, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
if (transfers.includes(studyright.studyrightid)) return true // Excludes both transfers in and out of the programme
return false
})
}
const getMedian = values => {
if (values.length === 0) return 0
@@ -189,6 +192,9 @@ const getStartDate = (studyprogramme, isAcademicYear) => {
const alltimeStartDate = new Date('1900-01-01')
const alltimeEndDate = new Date()
// In the array programmes should be [bachelorCode, masterCode]
const combinedStudyprogrammes = { KH90_001: 'MH90_001' }
// There are 9 course_unit_types
// 1. urn:code:course-unit-type:regular
// 2. urn:code:course-unit-type:bachelors-thesis
@@ -255,7 +261,7 @@ const getBachelorCreditGraphStats = years => ({
},
})
const getMasterCreditGraphStats = years => ({
const getBachelorMasterCreditGraphStats = years => ({
lte200: {
name: 'Less than 200 credits',
data: getEmptyArray(years.length),
@@ -285,6 +291,7 @@ const getMasterCreditGraphStats = years => ({
data: getEmptyArray(years.length),
},
})
const getVetenaryCreditGraphStats = years => ({
lte210: {
name: 'Less than 210 credits',
@@ -316,7 +323,42 @@ const getVetenaryCreditGraphStats = years => ({
},
})
const getOnlyMasterCreditGraphStats = years => ({
const getCombinedVetenaryCreditGraphStats = years => ({
lte30: {
name: 'Less than 30 credits',
data: getEmptyArray(years.length),
},
lte60: {
name: '30-59 credits',
data: getEmptyArray(years.length),
},
lte120: {
name: '60-179 credits',
data: getEmptyArray(years.length),
},
lte180: {
name: '180-239 credits',
data: getEmptyArray(years.length),
},
lte240: {
name: '240-299 credits',
data: getEmptyArray(years.length),
},
lte300: {
name: '300-359 credits',
data: getEmptyArray(years.length),
},
lte360: {
name: '300-359 credits',
data: getEmptyArray(years.length),
},
mte360: {
name: '360 or more credits',
data: getEmptyArray(years.length),
},
})
const getMasterCreditGraphStats = years => ({
lte15: {
name: 'Less than 15 credits',
data: getEmptyArray(years.length),
@@ -366,30 +408,34 @@ const getDoctoralCreditGraphStats = years => ({
},
})
const getCreditGraphStats = (studyprogramme, years, studyprog = false) => {
const getCreditGraphStats = (studyprogramme, years, combinedVetenary, masterStudyRightOnly = false) => {
if (combinedVetenary) return getCombinedVetenaryCreditGraphStats(years)
if (studyprogramme.includes('KH')) return getBachelorCreditGraphStats(years)
if (studyprogramme === 'MH90_001')
return studyprog ? getBachelorCreditGraphStats(years) : getVetenaryCreditGraphStats(years)
if (studyprogramme.includes('MH')) return getOnlyMasterCreditGraphStats(years)
return masterStudyRightOnly ? getBachelorCreditGraphStats(years) : getVetenaryCreditGraphStats(years)
if (studyprogramme.includes('MH'))
return masterStudyRightOnly ? getMasterCreditGraphStats(years) : getBachelorMasterCreditGraphStats(years)
return getDoctoralCreditGraphStats(years)
}
const bachelorCreditThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'lte120', 'lte150', 'lte180', 'mte180']
const masterCreditThresholds = ['lte200', 'lte220', 'lte240', 'lte260', 'lte280', 'lte300', 'mte300']
const onlyMasterCreditThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'lte120', 'mte120']
const bachelorMasterCreditThresholds = ['lte200', 'lte220', 'lte240', 'lte260', 'lte280', 'lte300', 'mte300']
const masterCreditThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'lte120', 'mte120']
const doctoralCreditThresholds = ['lte10', 'lte20', 'lte30', 'lte40', 'mte40']
const bachelorCreditAmounts = [15, 30, 60, 90, 120, 150, 180, 180]
const masterCreditAmounts = [200, 220, 240, 260, 280, 300, 300]
const onlyMasterCreditAmounts = [15, 30, 60, 90, 120, 120]
const bachelorMasterCreditAmounts = [200, 220, 240, 260, 280, 300, 300]
const masterCreditAmounts = [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 getBcMsThresholds = () => {
return { creditThresholdKeysBcMs: masterCreditThresholds, creditThresholdAmountsBcMs: masterCreditAmounts }
}
const getCreditThresholds = (studyprogramme, studyProg = false) => {
const combinedVetenaryThresholds = ['lte30', 'lte60', 'lte120', 'lte180', 'lte240', 'lte300', 'lte360', 'mte360']
const combinedVetenaryAmounts = [30, 60, 120, 180, 240, 300, 360, 360]
const getCreditThresholds = (studyprogramme, combinedVetenary, masterStudyRightOnly = false) => {
if (combinedVetenary)
return { creditThresholdKeys: combinedVetenaryThresholds, creditThresholdAmounts: combinedVetenaryAmounts }
if (studyprogramme === 'MH90_001') {
return studyProg
return masterStudyRightOnly
? { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
: { creditThresholdKeys: vetenaryCreditThresholds, creditThresholdAmounts: vetenaryCreditAmounts }
}
@@ -397,7 +443,9 @@ const getCreditThresholds = (studyprogramme, studyProg = false) => {
return { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
}
if (studyprogramme.includes('MH')) {
return { creditThresholdKeys: onlyMasterCreditThresholds, creditThresholdAmounts: onlyMasterCreditAmounts }
return masterStudyRightOnly
? { creditThresholdKeys: masterCreditThresholds, creditThresholdAmounts: masterCreditAmounts }
: { creditThresholdKeys: bachelorMasterCreditThresholds, creditThresholdAmounts: bachelorMasterCreditAmounts }
}
return { creditThresholdKeys: doctoralCreditThresholds, creditThresholdAmounts: doctoralCreditAmounts }
}
@@ -406,6 +454,22 @@ const tableTitles = {
basics: {
SPECIAL_EXCLUDED: ['', 'Started studying', 'Graduated'],
SPECIAL_INCLUDED: ['', 'Started studying', 'Graduated', 'Transferred away', 'Transferred to'],
SPECIAL_EXCLUDED_COMBINED_PROGRAMME: [
'',
'Started studying bachelor',
'Started studying licentiate',
'Graduated bachelor',
'Graduated licentiate',
],
SPECIAL_INCLUDED_COMBINED_PROGRAMME: [
'',
'Started studying bachelor',
'Started studying licentiate',
'Graduated bachelor',
'Graduated licentiate',
'Transferred away',
'Transferred to',
],
},
credits: {
SPECIAL_EXCLUDED: ['', 'Total', 'Major students credits', 'Transferred credits'],
@@ -431,7 +495,7 @@ const tableTitles = {
'150-179 credits',
'180 ≤ credits',
],
master: [
bachelorMaster: [
'',
'All',
'< 200 credits',
@@ -442,7 +506,7 @@ const tableTitles = {
'280-299 credits',
'300 ≤ credits',
],
masterOnly: [
master: [
'',
'All',
'< 15 credits',
@@ -452,39 +516,34 @@ const tableTitles = {
'90-119 credits',
'120 ≤ credits',
],
combinedVetenary: [
'',
'All',
'< 30 credits',
'30-59 credits',
'60-119 credits',
'120-179 credits',
'180-239 credits',
'240-299 credits',
'300-359 credits',
'360 ≤ credits',
],
doctoral: ['', 'All', '< 10 credits', '10-19 credits', '20-29 credits', '30-39 credits', '40 ≤ credits'],
},
studytracks: [
'',
'All',
'Started studying',
'Currently enrolled',
'Absent',
'Inactive',
'Graduated',
'Men',
'Women',
'Finland',
],
programmes: [
'',
'All',
'Started studying',
'Currently enrolled',
'Absent',
'Inactive',
'Graduated',
'Men',
'Women',
'Other/Unknown',
'Finland',
'Other',
],
studytracksStart: ['', 'All', 'Started studying', 'Currently enrolled', 'Absent', 'Inactive'],
studytracksBasic: ['Graduated'],
studytracksCombined: {
licentiate: ['Graduated Bachelor', 'Graduated Licentiate'],
master: ['Graduated Bachelor', 'Graduated master'],
},
studytracksEnd: ['Men', 'Women', 'Other/\nUnknown', 'Finland', 'Other'],
}
const getCreditProgressTableTitles = studyprogramme => {
const getCreditProgressTableTitles = (studyprogramme, combinedVetenaryProgramme, masterProgrammeOnly = false) => {
if (combinedVetenaryProgramme) return tableTitles.creditProgress.combinedVetenary
if (studyprogramme.includes('KH')) return tableTitles.creditProgress.bachelor
if (studyprogramme.includes('MH')) return tableTitles.creditProgress.masterOnly
if (studyprogramme.includes('MH'))
return masterProgrammeOnly ? tableTitles.creditProgress.master : tableTitles.creditProgress.bachelorMaster
return tableTitles.creditProgress.doctoral
}
@@ -508,10 +567,12 @@ const getId = code => {
}
const getGoal = programme => {
if (!programme) return 0
if (programme.startsWith('KH') || programme.endsWith('-ba')) {
return 36
}
if (programme.startsWith('MH') || programme.endsWith('-ma')) {
if (programme === 'MH90_001') return 36 // vetenary programme's licentiate is 36 months.
if (['MH30_004', '420420-ma'].includes(programme)) {
return 24 + 6
}
@@ -545,18 +606,18 @@ module.exports = {
getStartDate,
alltimeStartDate,
alltimeEndDate,
combinedStudyprogrammes,
getThesisType,
getPercentage,
getBachelorCreditGraphStats,
getBachelorMasterCreditGraphStats,
getMasterCreditGraphStats,
getVetenaryCreditGraphStats,
getDoctoralCreditGraphStats,
getOnlyMasterCreditGraphStats,
getCreditGraphStats,
getCreditThresholds,
tableTitles,
getCreditProgressTableTitles,
getBcMsThresholds,
isNonMajorCredit,
mapCodesToIds,
getId,
@@ -10,7 +10,7 @@ const {
} = require('../services/analyticsService')
const logger = require('../util/logger')
const updateBasicView = async code => {
const updateBasicView = async (code, combinedProgramme) => {
const specialCalendar = {
yearType: 'CALENDAR_YEAR',
specialGroups: 'SPECIAL_INCLUDED',
@@ -34,6 +34,7 @@ const updateBasicView = async code => {
try {
const basicStats = await getBasicStatsForStudytrack({
studyprogramme: code,
combinedProgramme,
settings: {
isAcademicYear: option.yearType === 'ACADEMIC_YEAR',
includeAllSpecials: option.specialGroups === 'SPECIAL_INCLUDED',
@@ -43,6 +44,7 @@ const updateBasicView = async code => {
const creditStats = await getCreditStatsForStudytrack({
studyprogramme: code,
combinedProgramme,
settings: {
isAcademicYear: option.yearType === 'ACADEMIC_YEAR',
includeAllSpecials: option.specialGroups === 'SPECIAL_INCLUDED',
@@ -52,6 +54,7 @@ const updateBasicView = async code => {
const graduationStats = await getGraduationStatsForStudytrack({
studyprogramme: code,
combinedProgramme,
settings: {
isAcademicYear: option.yearType === 'ACADEMIC_YEAR',
includeAllSpecials: option.specialGroups === 'SPECIAL_INCLUDED',
@@ -65,7 +68,7 @@ const updateBasicView = async code => {
return 'OK'
}
const updateStudytrackView = async code => {
const updateStudytrackView = async (code, combinedProgramme) => {
const specialGraduated = {
graduated: 'GRADUATED_INCLUDED',
specialGroups: 'SPECIAL_INCLUDED',
@@ -94,6 +97,7 @@ const updateStudytrackView = async code => {
try {
const stats = await getStudytrackStatsForStudyprogramme({
studyprogramme: code,
combinedProgramme,
settings: {
specialGroups: option.specialGroups === 'SPECIAL_INCLUDED',
graduated: option.graduated === 'GRADUATED_INCLUDED',
@@ -168,6 +168,7 @@ const associatedStudyrightElements = async (offset, limit) => {
},
limit,
offset,
subQuery: false, // Do not include sub queries to count towards the limit
})
const groupings = studyrights.map(({ studyright_elements: sres }) =>
sres.map(sre => ({
+254 -48
View File
@@ -22,13 +22,15 @@ const {
inactiveStudyrights,
enrolledStudents,
absentStudents,
graduatedStudyRightsByStartDate,
graduatedStudyRights,
} = require('./studyprogramme')
const { getAcademicYearDates } = require('../util/semester')
const { countTimeCategories } = require('./graduationHelpers')
const { countTimeCategories, getStatutoryAbsences } = require('./graduationHelpers')
const getUnique = studentnumbers => [...new Set(studentnumbers)]
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) => {
let data = { female: 0, male: 0, finnish: 0 }
let data = { female: 0, male: 0, otherUnkown: 0, finnish: 0, otherCountries: 0, otherCountriesCounts: {} }
thresholdKeys.forEach(t => (data[t] = 0))
students.forEach(({ gender_code, home_country_en, credits }) => {
const creditcount = credits
@@ -37,7 +39,15 @@ const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) =>
data.male += gender_code === '1' ? 1 : 0
data.female += gender_code === '2' ? 1 : 0
data.otherUnkown += ['0', '3'].includes(gender_code) ? 1 : 0
data.finnish += home_country_en === 'Finland' ? 1 : 0
data.otherCountries += home_country_en !== 'Finland' ? 1 : 0
if (home_country_en !== 'Finland') {
if (!(home_country_en in data.otherCountriesCounts)) {
data.otherCountriesCounts[home_country_en] = 0
}
data.otherCountriesCounts[home_country_en] += 1
}
data[thresholdKeys[0]] += creditcount < thresholdAmounts[0] ? 1 : 0
data[thresholdKeys[1]] += creditcount >= thresholdAmounts[0] && creditcount < thresholdAmounts[1] ? 1 : 0
data[thresholdKeys[2]] += creditcount >= thresholdAmounts[1] && creditcount < thresholdAmounts[2] ? 1 : 0
@@ -60,33 +70,90 @@ const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) =>
})
return data
}
// type: either Bc + Ms combo or Bc/Ms/T/anything else
const addGraduation = async ({ studentnumber, startdate, enddate, graduationAmounts, times, track, type, year }) => {
const totalTimeToGraduation = moment(enddate).diff(moment(startdate), 'months')
const statutoryAbsences = await getStatutoryAbsences(studentnumber, startdate, enddate)
const timeToGraduation = totalTimeToGraduation - statutoryAbsences
graduationAmounts[track][type][year] += 1
times[type] = [...times[type], timeToGraduation]
}
const getGraduationTimeStats = async ({ year, graduated, track, graduationAmounts, graduationTimes, classSize }) => {
const getGraduationTimeStats = async ({
year,
graduated,
track,
graduationAmounts,
graduationTimes,
classSize,
doCombo,
}) => {
if (year === 'Total') {
return
}
// Count how long each student took to graduate
let times = []
graduated.forEach(({ enddate, startdate }) => {
const timeToGraduation = moment(enddate).diff(moment(startdate), 'months')
graduationAmounts[track][year] += 1
times = [...times, timeToGraduation]
})
// Separate Bc + Ms and other
const times = { combo: [], basic: [] }
for (const { enddate, startdate, studyrightid, studentnumber } of graduated) {
if (doCombo && studyrightid.slice(-2) === '-2') {
await addGraduation({
studentnumber,
startdate,
enddate,
graduationAmounts,
times,
track,
type: 'combo',
year,
})
} else {
await addGraduation({
studentnumber,
startdate,
enddate,
graduationAmounts,
times,
track,
type: 'basic',
year,
})
}
}
const median = getMedian(times)
const statistics = countTimeCategories(times, graduationTimes.goal)
const median = getMedian(times.basic)
const medianCombo = getMedian(times.combo)
const statistics = countTimeCategories(times.basic, graduationTimes.goals.basic)
const statisticsCombo = countTimeCategories(times.combo, graduationTimes.goals.combo)
graduationTimes[track].medians.combo = [
...graduationTimes[track].medians.combo,
{
y: medianCombo,
amount: graduationAmounts[track]['combo'][year],
name: year,
statistics: statisticsCombo,
classSize,
},
]
graduationTimes[track].medians = [
...graduationTimes[track].medians,
{ y: median, amount: graduationAmounts[track][year], name: year, statistics, classSize },
graduationTimes[track].medians.basic = [
...graduationTimes[track].medians.basic,
{ y: median, amount: graduationAmounts[track]['basic'][year], name: year, statistics, classSize },
]
}
const getStats = (all, started, enrolled, absent, inactive, graduated, studentData) => {
return [
const getStats = (
all,
started,
enrolled,
absent,
inactive,
graduated,
graduatedSecondProg,
studentData,
combinedProgramme
) => {
const beginTablestats = [
all.length,
getPercentage(all.length, all.length),
started.length,
getPercentage(started.length, all.length),
enrolled.length,
@@ -97,18 +164,29 @@ const getStats = (all, started, enrolled, absent, inactive, graduated, studentDa
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
]
const combinedTableStats = combinedProgramme
? [graduatedSecondProg.length, getPercentage(graduatedSecondProg.length, all.length)]
: []
const endTableStats = [
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.otherUnkown,
getPercentage(studentData.otherUnkown, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
studentData.otherCountries,
getPercentage(studentData.otherCountries, all.length),
]
return [...beginTablestats, ...combinedTableStats, ...endTableStats]
}
// 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,
combinedProgramme,
since,
settings,
studytracks,
@@ -118,6 +196,7 @@ const getStudytrackDataForTheYear = async ({
data,
creditThresholdKeys,
creditThresholdAmounts,
doCombo,
}) => {
const { specialGroups: includeAllSpecials, graduated: includeGraduated } = settings
const { startDate, endDate } = getAcademicYearDates(year, since)
@@ -129,9 +208,12 @@ const getStudytrackDataForTheYear = async ({
creditTableStats,
graduationAmounts,
graduationTimes,
graduationTimesSecondProg,
graduationAmountsSecondProg,
totalAmounts,
emptyTracks,
totals,
otherCountriesCount,
} = data
await Promise.all(
@@ -144,6 +226,13 @@ const getStudytrackDataForTheYear = async ({
includeAllSpecials,
includeGraduated,
})
const startedStudentnumbers = await getCorrectStudentnumbers({
codes,
startDate,
endDate,
includeAllSpecials: !includeAllSpecials,
includeGraduated,
})
const students = await studytrackStudents(studentnumbers)
let all = []
@@ -153,24 +242,54 @@ const getStudytrackDataForTheYear = async ({
let absent = []
let inactive = []
let graduated = []
let graduatedSecondProg = []
let graduatedByStartdate = []
let graduatedByStartSecondProg = []
// Get all the studyrights and students for the calculations
if (year !== 'Total') {
all = await allStudyrights(track, studentnumbers)
studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
started = await startedStudyrights(track, startDate, studentnumbers)
enrolled = await enrolledStudents(track, startDate, studentnumbers)
otherCountriesCount[track][year] = studentData.otherCountriesCounts
started = await startedStudyrights(track, startDate, startedStudentnumbers)
enrolled = await enrolledStudents(track, studentnumbers)
absent = await absentStudents(track, studentnumbers)
inactive = await inactiveStudyrights(track, studentnumbers)
graduated = await graduatedStudyRights(track, startDate, studentnumbers)
const enrolledSecondProgramme = combinedProgramme
? await enrolledStudents(combinedProgramme, studentnumbers)
: []
const absentSecondProgramme = combinedProgramme ? await absentStudents(combinedProgramme, studentnumbers) : []
const inactiveSecondProgramme = combinedProgramme
? await inactiveStudyrights(combinedProgramme, studentnumbers)
: []
// Studentnumbers are fetched based on studystartdate, if it is greater than startdate
// Thus, computing the bc+ms graduated by startdate based on these studentnumbers does not work.
const academicEnddate = `${year.slice(-4)}-07-31T23:59:59`
graduatedByStartdate = await graduatedStudyRightsByStartDate(track, startDate, academicEnddate)
if (combinedProgramme) {
enrolled = getUnique([...enrolled, ...enrolledSecondProgramme].map(student => student.studentnumber))
absent = getUnique([...absent, ...absentSecondProgramme].map(student => student.studentnumber))
inactive = getUnique([...inactive, ...inactiveSecondProgramme].map(student => student.studentnumber))
graduatedSecondProg = await graduatedStudyRights(combinedProgramme, startDate, studentnumbers)
graduatedByStartSecondProg = await graduatedStudyRightsByStartDate(
combinedProgramme,
startDate,
academicEnddate
)
}
totals[track].all = [...totals[track].all, ...all]
totals[track].studentData.male += studentData.male
totals[track].studentData.female += studentData.female
totals[track].studentData.otherUnkown += studentData.otherUnkown
totals[track].studentData.finnish += studentData.finnish
totals[track].studentData.otherCountries += studentData.otherCountries
totals[track].started = [...totals[track].started, ...started]
totals[track].enrolled = [...totals[track].enrolled, ...enrolled]
totals[track].absent = [...totals[track].absent, ...absent]
totals[track].inactive = [...totals[track].inactive, ...inactive]
totals[track].graduated = [...totals[track].graduated, ...graduated]
totals[track].graduatedSecondProg = [...totals[track].graduatedSecondProg, ...graduatedSecondProg]
} else {
all = totals[track].all
studentData = totals[track].studentData
@@ -179,6 +298,20 @@ const getStudytrackDataForTheYear = async ({
absent = totals[track].absent
inactive = totals[track].inactive
graduated = totals[track].graduated
graduatedSecondProg = totals[track].graduatedSecondProg
const countryStatsFromYears = years.reduce((acc, year) => {
if (year === 'Total') return acc
const countries = otherCountriesCount[track][year]
Object.keys(countries).forEach(country => {
if (!(country in acc)) {
acc[country] = 0
}
acc[country] += countries[country]
})
return acc
}, {})
otherCountriesCount[track]['Total'] = countryStatsFromYears
}
// If the track has no stats for that year, it should be removed from the table and dropdown options
@@ -189,12 +322,11 @@ const getStudytrackDataForTheYear = async ({
if (year !== 'Total') {
// Count stats for creditgraph for the year per track
creditThresholdKeys.forEach(
threshold => (creditGraphStats[track][threshold].data[indexOf(years, year)] += studentData[threshold])
)
creditThresholdKeys.forEach(
threshold => (creditGraphStats[track][threshold].data[indexOf(years, 'Total')] += studentData[threshold])
)
creditThresholdKeys.forEach(threshold => {
creditGraphStats[track][threshold].data[indexOf(years, year)] += studentData[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])],
@@ -207,10 +339,22 @@ const getStudytrackDataForTheYear = async ({
)
creditTableStats[track] = [[year, ...totalCredits.slice(1)], ...creditTableStats[track]]
}
// Count stats for the main studytrack table grouped by tracks
mainStatsByTrack[track] = [
[year, ...getStats(all, started, enrolled, absent, inactive, graduated, studentData)],
[
year,
...getStats(
all,
started,
enrolled,
absent,
inactive,
graduated,
graduatedSecondProg,
studentData,
combinedProgramme
),
],
...mainStatsByTrack[track],
]
@@ -218,30 +362,62 @@ const getStudytrackDataForTheYear = async ({
mainStatsByYear[year] = [
[
studytrackNames[track]?.name['fi'] ? `${studytrackNames[track]?.name['fi']}, ${track}` : year,
...getStats(all, started, enrolled, absent, inactive, graduated, studentData),
...getStats(
all,
started,
enrolled,
absent,
inactive,
graduated,
graduatedSecondProg,
studentData,
combinedProgramme
),
],
...mainStatsByYear[year],
]
// Count stats for the graduation time charts grouped by year
if (!(track in graduationTimes)) {
graduationTimes[track] = { medians: [] }
graduationTimes[track] = { medians: { basic: [], combo: [] } }
}
totalAmounts[track][year] = all.length
await getGraduationTimeStats({
year,
graduated,
graduated: graduatedByStartdate,
track,
graduationAmounts,
graduationTimes,
classSize: totalAmounts[track][year],
doCombo,
})
if (combinedProgramme) {
await getGraduationTimeStats({
year,
graduated: graduatedByStartSecondProg,
track: combinedProgramme,
graduationAmounts: graduationAmountsSecondProg,
graduationTimes: graduationTimesSecondProg,
classSize: totalAmounts[studyprogramme][year],
doCombo: true,
})
}
})
)
Object.keys(graduationTimes).forEach(track => {
if (track !== 'goal') {
const sortedMedians = graduationTimes[track].medians.sort((a, b) => b.name.localeCompare(a.name))
graduationTimes[track].medians = sortedMedians
if (track !== 'goals') {
const sortedMedians = graduationTimes[track].medians.basic.sort((a, b) => b.name.localeCompare(a.name))
const sortedMediansCombo = graduationTimes[track].medians.combo.sort((a, b) => b.name.localeCompare(a.name))
graduationTimes[track].medians.basic = sortedMedians
graduationTimes[track].medians.combo = sortedMediansCombo
}
})
Object.keys(graduationTimesSecondProg).forEach(track => {
if (track !== 'goals') {
const sortedMedians = graduationTimesSecondProg[track].medians.combo.sort((a, b) => b.name.localeCompare(a.name))
graduationTimesSecondProg[track].medians.combo = sortedMedians
}
})
}
@@ -251,7 +427,7 @@ const getStudytrackDataForTheYear = async ({
const getStudytrackOptions = (studyprogramme, studytrackNames, studytracks, emptyTracks, years) => {
const names = { [studyprogramme]: 'All students of the programme' }
studytracks.forEach(track => {
const trackName = studytrackNames[track]?.name['fi']
const trackName = studytrackNames[track]?.name
if (trackName && emptyTracks.get(track) !== years.length) {
names[track] = trackName
}
@@ -260,21 +436,34 @@ const getStudytrackOptions = (studyprogramme, studytrackNames, studytracks, empt
}
// Creates empty objects for each statistic type, which are then updated with the studytrack data
const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
const getEmptyStatsObjects = (years, studytracks, studyprogramme, combinedProgramme) => {
const mainStatsByYear = getYearsObject({ years, emptyArrays: true })
const mainStatsByTrack = {}
const creditGraphStats = {}
const creditTableStats = {}
const graduationAmounts = {}
const graduationTimes = { goal: getGoal(studyprogramme) }
const goal = getGoal(studyprogramme)
const goalSecondProg = getGoal(combinedProgramme)
const graduationTimes = { goals: { basic: goal, combo: goal + 36 } }
const graduationTimesSecondProg = {
[combinedProgramme]: { medians: { basic: [], combo: [] } },
goals: { basic: goal, combo: goal + goalSecondProg },
}
const otherCountriesCount = {}
const graduationAmountsSecondProg = {
[combinedProgramme]: { basic: getYearsObject({ years }), combo: getYearsObject({ years }) },
}
const totalAmounts = {}
const emptyTracks = new Map()
const totals = {}
studytracks.forEach(async track => {
otherCountriesCount[track] = {}
mainStatsByTrack[track] = []
creditGraphStats[track] = getCreditGraphStats(studyprogramme, years, true)
creditGraphStats[track] = getCreditGraphStats(studyprogramme, years, combinedProgramme, true)
creditTableStats[track] = []
graduationAmounts[track] = getYearsObject({ years })
graduationAmounts[track] = { basic: getYearsObject({ years }), combo: getYearsObject({ years }) }
totalAmounts[track] = getYearsObject({ years })
totals[track] = {
all: [],
@@ -283,7 +472,8 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
absent: [],
inactive: [],
graduated: [],
studentData: { male: 0, female: 0, finnish: 0 },
graduatedSecondProg: [],
studentData: { male: 0, female: 0, otherUnkown: 0, finnish: 0, otherCountries: 0 },
}
})
@@ -294,14 +484,18 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
creditTableStats,
graduationAmounts,
graduationTimes,
graduationAmountsSecondProg,
graduationTimesSecondProg,
totalAmounts,
emptyTracks,
totals,
otherCountriesCount,
}
}
// Combines all the data for the Populations and Studytracks -view
const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings }) => {
// At the moment combined programme is thought to have only one track, the programme itself
const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, combinedProgramme, settings }) => {
const isAcademicYear = true
const includeYearsCombined = true
const since = getStartDate(studyprogramme, isAcademicYear)
@@ -313,15 +507,20 @@ const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings })
: [studyprogramme]
const studytrackNames = associations.studyTracks
const data = getEmptyStatsObjects(years, studytracks, studyprogramme, combinedProgramme)
const onlyMasterStudyrights = true
const data = getEmptyStatsObjects(years, studytracks, studyprogramme)
const { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(studyprogramme, true)
const { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(
studyprogramme,
combinedProgramme,
onlyMasterStudyrights
)
const doCombo = studyprogramme.startsWith('MH') && !['MH30_001', 'MH30_003'].includes(studyprogramme)
const yearsReversed = [...years].reverse()
for (const year of yearsReversed) {
await getStudytrackDataForTheYear({
studyprogramme,
combinedProgramme,
since,
settings,
studytracks,
@@ -331,21 +530,28 @@ const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings })
data,
creditThresholdKeys,
creditThresholdAmounts,
doCombo,
})
}
const getCorrectCombinedTitles = () => {
if (['MH90_001'].includes(combinedProgramme)) return tableTitles.studytracksCombined.licentiate
return tableTitles.studytracksCombined.master
}
const studytrackOptions = getStudytrackOptions(studyprogramme, studytrackNames, studytracks, data.emptyTracks, years)
const graduatedTitles = combinedProgramme ? getCorrectCombinedTitles() : tableTitles.studytracksBasic
return {
id: studyprogramme,
id: combinedProgramme ? `${studyprogramme}-${combinedProgramme}` : studyprogramme,
years: years,
...data,
doCombo,
studytrackOptions,
includeGraduated: settings.graduated,
populationTitles: tableTitles['studytracks'],
populationTitles: [...tableTitles.studytracksStart, ...graduatedTitles, ...tableTitles.studytracksEnd],
creditTableTitles:
studyprogramme === 'MH90_001'
? tableTitles['creditProgress']['bachelor']
: getCreditProgressTableTitles(studyprogramme),
: getCreditProgressTableTitles(studyprogramme, combinedProgramme, true),
}
}
@@ -346,7 +346,7 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
if (!regexresult) return code
return regexresult[1]
}
// console.log('yearly open uni acc credits: ', yearlyOpenUniAccCredits)
const fromAyToNormalCourses = [
...new Set(_.flatten(yearlyOpenUniAccCredits).map(c => unifyOpenUniversity(c.code))),
...new Set(_.flatten(yearlyOpenUniTotalCredits).map(c => unifyOpenUniversity(c.code))),
+66 -44
View File
@@ -164,6 +164,44 @@ const updateUser = async (username, fields) => {
const getAccessGroups = async () => await AccessGroup.findAll()
const updateAccessGroups = async (username, iamGroups = [], specialGroup = {}, sisId) => {
const { jory, hyOne, superAdmin, openUni } = specialGroup
const userFromDb = await byUsername(username)
const formattedUser = await formatUser(userFromDb, [], Boolean(sisId))
const { roles: currentAccessGroups } = formattedUser
// Modify accessgroups based on current groups and iamGroups
let newAccessGroups = []
if (currentAccessGroups.includes('studyGuidanceGroups') || (sisId && (await checkStudyGuidanceGroupsAccess(sisId))))
newAccessGroups.push('studyGuidanceGroups')
if (iamGroups.includes(courseStatisticsGroup)) newAccessGroups.push('courseStatistics')
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')
const accessGroups = await AccessGroup.findAll({
where: {
group_code: {
[Op.in]: newAccessGroups,
},
},
})
// 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 (
_.difference(newAccessGroups, currentAccessGroups).length > 0 ||
_.difference(currentAccessGroups, newAccessGroups).length > 0
) {
userFromDb.setAccessgroup(accessGroups.map(({ id }) => id))
}
return { newAccessGroups, accessGroups }
}
// newer logic
const enrichProgrammesFromFaculties = faculties =>
facultiesAndProgrammesForTrends.filter(f => faculties.includes(f.faculty_code)).map(f => f.programme_code)
@@ -175,20 +213,29 @@ const findAll = async () => {
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 formattedUsers = await Promise.all(
allUsers.map(async user => {
const { iamGroups, specialGroup } = userAccess.find(({ id }) => id === user.sisu_person_id) || {}
const { accessGroups } = await updateAccessGroups(user.username, iamGroups, specialGroup)
return {
...user.get(),
is_enabled: true,
iam_groups: iamGroups || [],
elementdetails: _.uniqBy([
...user.programme.map(p => p.elementDetailCode),
...enrichProgrammesFromFaculties(user.faculty.map(f => f.faculty_code)),
]),
accessgroup: accessGroups,
}
})
)
return formattedUsers
}
const formatUser = async (userFromDb, extraRights) => {
const formatUser = async (userFromDb, extraRights, getStudentAccess = true) => {
const [accessGroups, programmes, faculties] = [
['accessgroup', 'group_code'],
['programme', 'elementDetailCode'],
@@ -209,9 +256,13 @@ const formatUser = async (userFromDb, extraRights) => {
},
} = userFromDb
const studentsUserCanAccess = _.uniqBy(
(await Promise.all([getStudentnumbersByElementdetails(rights), getAllStudentsUserHasInGroups(sisPersonId)])).flat()
)
const studentsUserCanAccess = getStudentAccess
? _.uniqBy(
(
await Promise.all([getStudentnumbersByElementdetails(rights), getAllStudentsUserHasInGroups(sisPersonId)])
).flat()
)
: []
// attribute naming is a bit confusing, but it's used so widely in oodikone, that it's not probably worth changing
return {
@@ -272,39 +323,10 @@ const getUser = async ({ username, name, email, iamGroups, iamRights, specialGro
last_login: new Date(),
})
const { kosu, jory, hyOne, superAdmin, openUni } = specialGroup
const userFromDb = await byUsername(username)
const formattedUser = await formatUser(userFromDb, kosu ? iamRights : [])
const { roles: currentAccessGroups } = formattedUser
const formattedUser = await formatUser(userFromDb, specialGroup.kosu ? iamRights : [])
// 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 (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 (
_.difference(newAccessGroups, currentAccessGroups).length > 0 ||
_.difference(currentAccessGroups, newAccessGroups).length > 0
) {
userFromDb.setAccessgroup(
(
await AccessGroup.findAll({
where: {
group_code: {
[Op.in]: newAccessGroups,
},
},
})
).map(({ id }) => id)
)
}
const { newAccessGroups } = await updateAccessGroups(username, iamGroups, specialGroup, sisId)
if (isNewUser) await sendNotificationAboutNewUser({ userId: username, userFullName: name })
+2 -2
View File
@@ -1,12 +1,12 @@
const axios = require('axios')
const { importerToken } = require('../conf-backend')
const { importerUrl, importerToken } = require('../conf-backend')
const logger = require('./logger')
const importerClient = axios.create({
headers: {
token: importerToken,
},
baseURL: 'https://importer.cs.helsinki.fi/api/importer',
baseURL: importerUrl,
})
const getImporterClient = () => {
+2 -2
View File
@@ -1,10 +1,10 @@
const Sentry = require('@sentry/node')
const Tracing = require('@sentry/tracing')
const { sentryRelease, sentryEnvironment, runningInCI } = require('../conf-backend')
const { sentryRelease, sentryEnvironment, runningInCI, isStaging } = require('../conf-backend')
const initializeSentry = app => {
if (!sentryRelease || !sentryEnvironment || runningInCI) return
if (!sentryRelease || !sentryEnvironment || runningInCI || isStaging) return
Sentry.init({
dsn: 'https://020b79f0cbb14aad94cc9d69a1ea9d52@sentry.cs.helsinki.fi/2',
+26 -19
View File
@@ -1,25 +1,32 @@
FROM node:14.17-alpine3.13 as base
FROM registry.access.redhat.com/ubi8/nodejs-14-minimal
WORKDIR /usr/src/app
ENV TZ="Europe/Helsinki"
FROM base as development
COPY ./package* /usr/src/app/
RUN npm ci
COPY . /usr/src/app/
EXPOSE 3000
CMD ["npm", "start"]
WORKDIR /opt/app-root/src
EXPOSE 5000
ENV NODE_ENV=production
ARG BASE_PATH
ENV PUBLIC_URL=$BASE_PATH
FROM base as production
ARG SENTRY_ENVIRONMENT
ENV REACT_APP_SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
ARG SENTRY_RELEASE
ENV REACT_APP_SENTRY_RELEASE=$SENTRY_RELEASE
ARG BUILT_AT
ENV REACT_APP_BUILT_AT=$BUILT_AT
COPY --chown=node:node ./package* /usr/src/app/
RUN npm ci --production
COPY --chown=node:node . /usr/src/app/
RUN npm run build && npm install -g serve@12.0.0 && chown -R node:node /usr/src/app/build
USER node
EXPOSE 5000
ARG STAGING
ENV REACT_APP_STAGING=$STAGING
ARG SENTRY_ENVIRONMENT
ENV REACT_APP_SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
ARG SENTRY_RELEASE
ENV REACT_APP_SENTRY_RELEASE=$SENTRY_RELEASE
COPY ./package* ./
RUN npm ci --omit-dev
COPY . .
RUN npm run build && npm install -g serve@12.0.0
CMD ["serve", "--single", "--listen", "5000", "build"]
+13
View File
@@ -0,0 +1,13 @@
FROM node:14.17-alpine3.13
ENV TZ="Europe/Helsinki"
WORKDIR /opt/app-root/src
EXPOSE 3000
COPY ./package* ./
RUN npm ci
COPY . .
CMD ["npm", "start"]
@@ -1,12 +1,11 @@
export default {
Students: `Students
Harmaata _Show_-painiketta klikkaamalla saa listan kaikista populaation opiskelijoista. (Huom. suodattimet vaikuttavat tähän listaan!) Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
Students: `
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
- **Student Number:** Klikkaamalla opiskelijanumeron vieressä olevaa sinistä nuolta, siirtyy "student statistic" -näkymään valitun opiskelijan osalta.
- **All Credits:** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
- **Tags:** Opiskelijoille voi luoda tageja ryhmittelyä varten kohdassa _Study Program > Overview > haluttu koulutusohjelma_.
- **Study Programme:** Opiskelijan koulutusohjelma.
- **Study Programme**-sarake näyttää opiskelijan uusimman opinto-oikeuden sillä hetkellä, kun hän ilmoittautui valitulle kurssille. Jos opiskelijalla on ollut silloin opinto-oikeuksia muihinkin koulutusohjelmiin, näkyy ohjelman nimen perässä plus-merkki. Laittamalla hiiren kyseisen solun kohdalle, kaikki koulutusohjelmat tulevat näkyviin. Ne näkyvät myös Excel-tiedostossa aikajärjestyksessä uusimmasta vanhimpaan.
- **Start Year at Uni:** Aloitusvuosi yliopistolla.
- **Tags:** Opiskelijoille voi luoda tageja ryhmittelyä varten kohdassa _Study Program > Overview > haluttu koulutusohjelma_.
`,
}
@@ -21,6 +21,6 @@ export default {
studentNumber: {
label: null,
short:
'(ent. cherry-picker) Rajaa yksittäisiä opiskelijoita pois tilastoista (blocklist) tai näytä vain määritellyt opiskelijat (allowlist). Vain täsmälliset opiskelijanumerot toimivat.',
'Rajaa yksittäisiä opiskelijoita pois tilastoista (blocklist) tai näytä vain määritellyt opiskelijat (allowlist). Vain täsmälliset opiskelijanumerot toimivat.',
},
}
@@ -17,6 +17,7 @@ export default {
Opiskelijoiden kumulatiiviset opintopistekertymät opiskelijakohtaisina kuvaajina.
Kun hiiren vie kuvaajien päälle, saa näkyviin jokaista kuvaajaa vastaavan opiskelijanumeron.
Opiskelijanumeron yhteydessä olevaa ympyrää klikkaamalla siirtyy ”student statistics” puolelle ja näkee valitun opiskelijan opintotiedot.
Timantin muotoinen ikoni kertoo opiskelijan valmistuneen valitusta koulutusohjelmasta kyseisellä hetkellä.
X-akselin skaalaa voi säätää harmaista painikkeista ”1m”, ”3m”, ”6m”, ”YTD”, ”1y” ja ”ALL” tai kuvaajan alla olevan sinisen näkymän harmaita ”kahvoja” siirtämällä.
Y-akselin skaalaa voi säätää harmaista painikkeista ”small” ”medium” ja "large”`,
@@ -25,8 +26,10 @@ export default {
Taulukko kertoo opintopistekertymän valitulle opiskelijapopulaatiolle. Opintopisteluokat on suhteutettu tarkasteltavan populaation aloitusvuoteen.
Ylin luokka kertoo siksi aina tavoiteajassa etenevien määrän ja suhteellisen osuuden.
Huom! nollasuorittajissa on mukana myös poissaolevaksi ilmoittautuneet, ellei heitä ole erikseen suodatettu populaatiosta pois.
Opintopisteet sisältävät opiskelijoiden **kaikki opintopisteet**, eli siis myös ennen valitun tutkinnon alkamista suoritetut opinnot.
Huom! Nollasuorittajissa on mukana myös poissaolevaksi ilmoittautuneet, ellei heitä ole erikseen suodatettu populaatiosta pois.
Kategorioita voi myös itsessään käyttää suodattimina. Klikkaamalla rivin vasemmassa reunassa olevaa suodatinta jäävät näkyviin vain sen kategorian opiskelijat.
Suodatin aukeaa samalla ikkunan vasempaan laitaan, josta sitä voi käyttää vapaavalintaisilla arvoilla.
@@ -49,27 +52,46 @@ export default {
**Cumulative**-valinnan ollessa käytössä kunkin opiskelijan kaikki aiemmat opintopisteet lasketaan mukaan opintopistemäärään. Muutoin vain pylvästä vastaavalla aikavälillä kirjatut opintopisteet otetaan huomioon.
`,
CoursesOfPopulation: `**Courses of class**
CoursesOfClass: `**Courses of class**
Tämä taulukko sisältää näkyvissä olevien opiskelijoiden kaikki kurssisuoritukset.
Opiskelijat on mahdollista suodattaa kurssin perusteella käyttämällä kurssirivin vasemmassa laidassa olevaa suodatinpainiketta.
Kurssisuodatin aukeaa ikkunan vasempaan laitaan ja tarjoaa lisää vaihtoehtoja suodatukseen. Voit myös valita monta kurssia kerralla suodattimeen.
Klikkaamalla kurssin nimen vieressä olevaa sinistä nuolta, siirryt ko. kurssin *Course Statistics* -näkymään.
Klikkaamalla kurssin nimen vieressä olevaa sinistä nuolta, siirryt ko. kurssin **Course Statistics** -näkymään.
Lisäksi kurssin nimi **name** ja koodisarakkeissa **code** suodatinkuvaketta klikkaamalla tulee esiin hakukentät suodattamista varten.
Suodattimet saa tyhjennettyä painamalla kentän vieressä olevaa ruksia tai valitsemall valikon alaosasta **Reset column filter**.
**Huom! Nämä suodattimet vaikuttavat vain tähän taulukkoon, eivät koko populatioon.**
**Taulukot**
- **pass/fail** - näkymä sisältää kurssisuoritukset, hylätyt, yritykset sekä ilmoittautumistiedot. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
- **Total students** - sarake näyttää kaikkien opiskelijoiden määrän, mukaanlukien ilmoittautuneet ilman arvosanaa.
- **Enrolled, no grade** - sarake näyttää niiden opiskelijoiden määrän, joilla on ilmoittautuminen muttei arvosanaa eikä hylättyä suoritusta.
- **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.
- **students** - opiskelijan arvosanat kursseittain.
`,
CoursesOfPopulation: `**Courses of Population**
Tämä taulukko sisältää näkyvissä olevien opiskelijoiden kaikki kurssisuoritukset.
Opiskelijat on mahdollista suodattaa kurssin perusteella käyttämällä kurssirivin vasemmassa laidassa olevaa suodatinpainiketta.
Kurssisuodatin aukeaa ikkunan vasempaan laitaan ja tarjoaa lisää vaihtoehtoja suodatukseen. Voit myös valita monta kurssia kerralla suodattimeen.
Klikkaamalla kurssin nimen vieressä olevaa sinistä nuolta, siirryt ko. kurssin **Course Statistics** -näkymään.
Taulukon tietoja voi suodattaa antamalla opiskelijoiden vähimmäismäärän kurssilla kenttään *Limit to courses where student number at least*.
Lisäksi kurssin nimi- ja koodisarakkeissa on hakukentät *("filter here")* suodattamista varten.
Suodattimet saa tyhjennettyä painamalla kentän vieressä olevaa ruksia.
Lisäksi kurssin nimi **name** ja koodisarakkeissa **code** suodatinkuvaketta klikkaamalla tulee esiin hakukentät suodattamista varten.
Suodattimet saa tyhjennettyä painamalla kentän vieressä olevaa ruksia tai valitsemall valikon alaosasta **Reset column filter**.
**Huom! Nämä suodattimet vaikuttavat vain tähän taulukkoon, eivät koko populatioon.**
**Taulukot**
- **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.
- **pass/fail** - näkymä sisältää kurssisuoritukset, hylätyt, 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.
`,
Search: `Tässä osiossa voi tarkastella koulutusohjelmakohtaisia populaatioita sisäänotto-lukuvuoden mukaisesti,
vuosikurssi kerrallaan. Uudet koulutusohjelmat ovat pääsääntöisesti alkaneet 1.8.2017,
@@ -98,8 +120,8 @@ export default {
- **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ä
- **Programme**: Näyttää opiskelijan uusimman opinto-oikeuden. Jos opiskelijalla on opinto-oikeuksia muihinkin koulutusohjelmiin, ne näkyvät aikajärjestyksessä uusimmasta vanhimpaan Excel-tiedostossa, sekä laittamalla hiiri koulutusohjelman päälle.
- **Started in programme**: Opiskelijan valitussa ohjelmassa aloituspäivämäärä. Kandi+maisteriopiskelijoille kandiksi valmistumispäivämäärä +1.
`,
StudentsClass: `**Students**
@@ -119,8 +141,18 @@ export default {
- **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ä
- **Semesters present**: Näyttää opiskelijan ilmoittautumiset lukukausittain. Laita hiiri solun yläpuolelle nähdäksesi tekstimuotoisen selityksen ilmoittautumisista.
- Värikoodit:
- Vihreä = Läsnäoleva
- Keltainen = Ilmoittautunut poissaolevaksi
- Punainen = Ei ilmoittautumista, mutta opiskelijalla on ollut opinto-oikeus
- Harmaa = Opiskelijalla ei ollut tällöin opinto-oikeutta
- Kruunu kertoo opiskelijan valmistuneen kyseisenä lukukautena. Jos näkymässä on yhdistettynä alempi ja ylempi tutkinto, ylemmän valmistumisen kruunussa on timantti.
- Excel-versiossa sarakkeessa näkyy tieto opiskelijan ensimmäisestä ilmoittautumisesta, jonka jälkeen ilmoittautumiset esitettynä symbolein:
- \\+ = Ilmoittautunut läsnäolevaksi
- o = Ilmoittautunut poissaolevaksi
- _ = Ei ilmoittautumista
**Courses**
Tällä välilehdellä näkyy, mitä pakollisiksi määriteltyjä kursseja opiskelija on suorittanut.
@@ -129,6 +161,17 @@ export default {
**Tags**
Opiskelijoille voi luoda tag:eja ryhmittelyä varten kohdassa "Study Program" > "Overview" > haluttu koulutusohjelma.
**Progress**
Tämä näkymä on tällä hetkellä käytössä vain kandivaiheen opinto-ohjelmissa.
Tällä välilehdellä näkyy opintojen kehitys annettujen kriteerien mukaan akateemista vuotta kohti. Kurssi merkataan tehdyksi, jos sillä on suoritusmerkintä tai hyväksiluku
minä tahansa lukuvuonna. Opintopistekriteerin täyttämiseen vaaditaan, että opintopisteet on suoritettu kyseisen akateemisen vuoden aikana.
**Huomaa**, että klikkaamalla taulukon yläreunassa olevaa kolme pistettä sisältävää kuvaketta, voit ladata itsellesi Excel tiedoston. Tiedosto sisältää lisäksi muita sarakkeita, kuten läsnäolon lukukausittain sekä yhteystiedot.
***Yhteystietojen käyttö on kuitenkin sallittua vain opintoneuvontaan liittyvissä asiossa.***
`,
StudentsCustom: `**Students**
@@ -141,6 +184,9 @@ export default {
**All credits** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
**Programme**-sarake näyttää opiskelijan uusimman opinto-oikeuden. Jos opiskelijalla on opinto-oikeuksia muihinkin koulutusohjelmiin, ne näkyvät aikajärjestyksessä uusimmasta vanhimpaan Excel-tiedostossa, sekä laittamalla hiiri koulutusohjelman päälle.
`,
Filters: {
Add: `Harmaata ”add” painiketta klikkaamalla aukeaa filters- eli suodatinvalikko,
@@ -34,6 +34,21 @@ export default {
esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat. Sisältää myös opintopisteet, jotka opiskelija on suorittanut koulutusohjelmaan ennen opintojen aloittamista koulutusohjelmassa tai opintojen päätyttyä.
**Transferred credits**: Sisältää opintopisteet, jotka on suoritettu kyseisenä vuonna, ja hyväksiluettu tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet.
**Non-degree students**: Opiskelijat, joilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
**Transferred credits**: Sisältää opintopisteet, jotka ovat suoritettu kyseisenä vuonna, ja hyväksiluettu tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
`,
StudentsOfProgrammeCourses: `
Sisältää kurssien opiskelijat.
**Not completed**: Sisältää opiskelijat, jotka ovat ilmoittautuneet kurssille, mutta eivät ole suorittaneet sitä ja opiskelijat, jotka ovat saaneet hylätyn arvosanan.
**Major students**: Sisältää opiskelijat, joilla on ensisijainen opinto-oikeus kyseiseen koulutusohjelmaan.
**Non-major students**: Sisältää kurssin opiskelijat, joilla EI ole ollut suoritushetkellä ensisijaista opinto-oikeutta kyseiseen koulutusohjelmaan. Sisältää
esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat.
**Transferred students**: Sisältää opiskelijat, jotka ovat suorittaneet kyseisenä vuonna hyväksiluettuja opintopisteitä tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
`,
GraduatedAndThesisWritersOfTheProgramme: `
**Wrote thesis**: Sisältää kyseisenä vuonna kandidaatintutkielman tai pro gradu-tutkielman hyväksytysti suorittaneet opiskelijat. Mukana ei ole mahdollisia arvosanan korotuksia.
@@ -64,6 +79,19 @@ export default {
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.
Valmistumisajoista on vähennetty lakisääteiset poissaolot.`,
AverageGraduationTimesStudytracksMaster: `
Yksittäinen palkki kertoo, kuinka moni kyseisenä **lukuvuotena ALOITTANEISTA** opiskelijoista on valmistunut.
**Bachelor + master studyright** kuvaajassa vuosi on kandiohjelman aloitusvuosi.
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ä 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.
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.
@@ -76,10 +104,30 @@ export default {
Kuvaa koulutusohjelman kyseiseen ohjelmaan opinto-oikeudella varustettujen opiskelijoiden etenemistä opintopisteissä mitattuna. 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.
**Started studying** Sisältää kaikki opiskelijat, jotka ovat aloittaneet opiskelemaan koulutusohjelmassa kyseisenä lukuvuonna.
**Inactive** Sisältää kaikki opinto-oikeudet, jotka ovat alkaneet kyseisenä vuonna, mutta jotka ovat sittemmin vanhentuneet ilman, että opiskelija on valmistunut ohjelmasta. Sisältää myös opinto-oikeudet,
- **All** Sisältää kaikki opiskelijat, joiden opinto-oikeudella on aloituspäivä kyseisenä vuonna, riippumatta siitä, onko opiskelija aloittanut opiskelemaan ko. vuonna.
- **Started studying** Sisältää kaikki opiskelijat, jotka ovat aloittaneet opiskelemaan koulutusohjelmassa kyseisenä lukuvuonna.
- **Inactive** Sisältää kaikki opinto-oikeudet, jotka ovat alkaneet kyseisenä vuonna, mutta jotka ovat sittemmin vanhentuneet ilman, että opiskelija on valmistunut ohjelmasta. Sisältää myös opinto-oikeudet,
joissa opiskelija on laiminlyönyt ilmoittautumisen tälle lukukaudelle.
Kenttien **Currently enrolled, Absent, Inactive** ja **Graduated** arvot tuottavat yhteenlaskettuna kentän **All** arvon.
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 opintosuunnan opiskelijat. Sekä opiskelijoiden yleistiedot että edistyminen opintopisteittäin
kuvaavat tällöin kyseisen opintosuunnan opiskelijoita.
`,
StudytrackOverviewCombinedProgramme: `
Kuvaa koulutusohjelman kyseiseen ohjelmaan opinto-oikeudella varustettujen opiskelijoiden etenemistä opintopisteissä mitattuna. 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.
- **Started studying** Sisältää kaikki opiskelijat, jotka ovat aloittaneet opiskelemaan koulutusohjelmassa kyseisenä lukuvuonna.
- **Inactive** Sisältää kaikki opinto-oikeudet, jotka ovat alkaneet kyseisenä vuonna, mutta jotka ovat sittemmin vanhentuneet ilman, että opiskelija on valmistunut ohjelmasta. Sisältää myös opinto-oikeudet,
joissa opiskelija on laiminlyönyt ilmoittautumisen tälle lukukaudelle.
- Kenttien **Currently enrolled, Absent, Inactive** ja **Graduated licentiate** (tai **Graduated master**) arvot tuottavat yhteenlaskettuna kentän **All** arvon.
- Lisäksi kenttien **Currently enrolled, Absent, Inactive** arvot sisältävät opiskelijat, joilla on kyseinen status riippumatta siitä, onko opiskelija kandi- vai maisterivaiheen opiskelija.
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.
@@ -96,13 +144,10 @@ export default {
Mikäli yläosan valikosta valittuna on jokin tietty opintosuunta, myös nämä graafit ja taulukot kuvaavat kyseisen opintosuunnan opiskelijoita.
`,
NonDegree: `
Opiskelijat, joilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
Name: `
Pitkät kurssinimet on lyhennetty. Näet ne kokonaan laittamalla hiiren nimen yläpuolelle.
`,
TransferredCourses: `
Sisältää opiskelijat, jotka ovat suorittaneet kyseisenä vuonna hyväksiluettuja opintopisteitä tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
`,
TransferredCredits: `
Sisältää opintopisteet, jotka ovat suoritettu kyseisenä vuonna, ja hyväksiluettu tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
NotCompleted: `
Sisältää opiskelijat, jotka ovat ilmoittautuneet kurssille, mutta eivät ole suorittaneet sitä ja opiskelijat, jotka ovat saaneet hylätyn arvosanan.
`,
}
+116
View File
@@ -0,0 +1,116 @@
import React from 'react'
import { Popup, Icon } from 'semantic-ui-react'
import { copyToClipboard } from 'common'
export const getCopyableEmailColumn = ({
popupStates,
copyToClipboardAll,
sendAnalytics,
handlePopupOpen,
handlePopupClose,
}) => {
return {
mergeHeader: true,
merge: true,
key: 'email',
export: false,
children: [
{
key: 'emailValue',
title: (
<>
Email
<Popup
trigger={
<Icon
size="large"
link
name="copy"
onClick={copyToClipboardAll}
style={{ float: 'right', marginLeft: '0.25em' }}
color="grey"
/>
}
content="Copied email list!"
on="click"
open={popupStates['0']}
onClose={() => handlePopupClose('0')}
onOpen={() => handlePopupOpen('0')}
position="top right"
/>
</>
),
textTitle: 'Email',
getRowVal: s => s.email,
},
{
key: 'copyEmail',
textTitle: 'Secondary email',
getRowVal: s => s.secondaryEmail,
getRowContent: s =>
s.email && !s.obfuscated ? (
<Popup
trigger={
<Icon
link
name="copy outline"
onClick={() => {
copyToClipboard(s.email)
sendAnalytics("Copy student's email to clipboard", "Copy student's email to clipboard")
}}
style={{ float: 'right' }}
/>
}
content="Email copied!"
on="click"
open={popupStates[s.studentNumber]}
onClose={() => handlePopupClose(s.studentNumber)}
onOpen={() => handlePopupOpen(s.studentNumber)}
position="top right"
/>
) : null,
headerProps: { onClick: null, sorted: null },
cellProps: { className: 'iconCellNoPointer' },
},
],
}
}
export const hiddenNameAndEmailForExcel = [
{
key: 'hidden-lastname',
title: 'Last name',
forceToolsMode: 'none',
getRowVal: s => s.lastname,
headerProps: { style: { display: 'none' } },
cellProps: { style: { display: 'none' } },
export: true,
},
{
key: 'hidden-firstnames',
title: 'First names',
getRowVal: s => s.firstnames,
forceToolsMode: 'none',
headerProps: { style: { display: 'none' } },
cellProps: { style: { display: 'none' } },
export: true,
},
{
key: 'hidden-email',
title: 'E-mail',
getRowVal: s => s.email ?? '',
forceToolsMode: 'none',
headerProps: { style: { display: 'none' } },
cellProps: { style: { display: 'none' } },
export: true,
},
{
key: 'hidden-secondary-email',
title: 'Secondary E-mail',
getRowVal: s => s.secondaryEmail ?? '',
forceToolsMode: 'none',
headerProps: { style: { display: 'none' } },
cellProps: { style: { display: 'none' } },
export: true,
},
]
+68 -12
View File
@@ -22,6 +22,49 @@ export const checkUserAccess = (requiredRoles, roles) => {
return intersection(requiredRoles, roles).length > 0
}
export const getStudentToStudyrightStartMap = (students, programmeCode) => {
return students.reduce((res, student) => {
const currentStudyright = student.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 = student.studyrights.find(studyright => studyright.studyrightid === bachelorId)
res[student.studentNumber] = bacherlorStudyright?.startdate || null
} else {
res[student.studentNumber] = currentStudyright?.startdate || null
}
return res
}, {})
}
// Combined study programmes: key bachelor programme, item master/licentiate programme
export const getCombinedProgrammeId = progId => {
const combinedProgrammes = { KH90_001: 'MH90_001' }
return Object.keys(combinedProgrammes).includes(progId) ? combinedProgrammes[progId] : ''
}
// Graph titles for graduation graphs used in studyprogramme Basic info -tab and Programmes and student pop. -tab
export const getGraduationGraphTitle = (studyProgramme, doCombo = false) => {
if (!studyProgramme) return ''
if (['MH30_001', 'MH30_003'].includes(studyProgramme)) return 'Licenciate studyright'
if (doCombo && studyProgramme === 'MH90_001') return 'Bachelor + licentiate studyright'
if (doCombo && studyProgramme.includes('MH')) return 'Bachelor + master studyright'
if (studyProgramme.includes('KH')) return 'Bachelor studyright'
if (studyProgramme.includes('MH')) return 'Master studyright'
return 'Doctoral studyright'
}
export const getUnifiedProgrammeName = (bachelor, masterLisentiate, language) => {
if (language === 'fi')
return `${bachelor} ja ${
masterLisentiate?.includes('lisensiaatin') ? 'lisensiaatin koulutusohjelma' : 'maisterin koulutusohjelma'
}`
if (language === 'en') return `${bachelor?.split(' ')[0]} and ${masterLisentiate}`
if (language === 'sv') return `${bachelor?.split('programmet')[0]}- och ${masterLisentiate}`
return bachelor
}
export const containsOnlyNumbers = str => str.match('^\\d+$')
export const momentFromFormat = (date, format) => moment(date, format)
@@ -49,7 +92,7 @@ export const getStudentTotalCredits = (student, includeTransferredCredits = true
const passedCourses = includeTransferredCredits
? student.courses.filter(c => c.passed && !c.isStudyModuleCredit)
: student.courses.filter(c => c.passed && !c.isStudyModuleCredit && c.credittypecode !== 9)
return passedCourses.reduce((a, b) => a + b.credits, 0)
return Math.round(100 * passedCourses.reduce((a, b) => a + b.credits, 0)) / 100
}
const getGradedCourses = (student, includeTransferredCredits = true) =>
@@ -186,6 +229,7 @@ export const flattenStudyrights = (studyrights, programme) => {
return studyrightcodes
}
// Gives students course completion date
export const getStudentToTargetCourseDateMap = (students, codes) => {
const codeSet = new Set(codes)
return students.reduce((acc, student) => {
@@ -197,7 +241,7 @@ export const getStudentToTargetCourseDateMap = (students, codes) => {
}, {})
}
export const getNewestProgramme = (studyrights, studentNumber, studentToTargetCourseDateMap, elementDetails) => {
export const getAllProgrammesOfStudent = (studyrights, studentNumber, studentToTargetCourseDateMap, elementDetails) => {
const studyprogrammes = []
studyrights.forEach(sr => {
const facultyCode = sr.faculty_code
@@ -223,23 +267,33 @@ export const getNewestProgramme = (studyrights, studentNumber, studentToTargetCo
startdate: newestStudyrightElement.startdate,
code: newestStudyrightElement.code,
facultyCode,
graduated: sr.graduated,
active: sr.active,
})
}
})
const programme = studyprogrammes.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]
if (programme) {
return programme
studyprogrammes.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))
if (studyprogrammes.length > 0) {
return studyprogrammes
}
if (studentToTargetCourseDateMap) {
return {
name: { en: 'No programme at the time of attainment', fi: 'Ei ohjelmaa suorituksen hetkellä' },
startdate: '',
code: '00000',
}
return [
{
name: { en: 'No programme at the time of attainment', fi: 'Ei ohjelmaa suorituksen hetkellä' },
startdate: '',
code: '00000',
},
]
}
return { name: { en: 'No programme', fi: 'Ei ohjelmaa' }, startdate: '', code: '00000' }
return [{ name: { en: 'No programme', fi: 'Ei ohjelmaa' }, startdate: '', code: '00000' }]
}
export const getNewestProgramme = (studyrights, studentNumber, studentToTargetCourseDateMap, elementDetails) => {
return getAllProgrammesOfStudent(studyrights, studentNumber, studentToTargetCourseDateMap, elementDetails)[0]
}
export const getHighestGradeOfCourseBetweenRange = (courses, lowerBound, upperBound) => {
@@ -283,7 +337,9 @@ export const getTargetCreditsForProgramme = code => {
if (code === 'MH30_004') return 150
if (code === 'MH90_001') return 180
if (code.includes('MH')) return 120
if (code.includes('T')) return 40
return 180
// Those codes begin with 'LIS' is it 40 credits or something else?
}
const getMonthsForDegree = code => getTargetCreditsForProgramme(code) / (60 / 12)
@@ -352,7 +408,7 @@ export const resolveStudyPlan = (studyPlans, studyRight) => {
.filter(e => e.element_detail.type === 20)
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0] || {}
if (!code) return null
return studyPlans.find(p => p.programme_code === code)
return studyPlans.find(p => p.programme_code === code && p.studyrightid === studyRight.studyrightid)
}
// These are the new Bachelor's programmes in Matlu, that have BH possibility
@@ -6,6 +6,7 @@ const { sendEvent } = TSA.Matomo
export const ANALYTICS_CATEGORIES = {
populationStudents: 'Population students',
populationStatistics: 'Population statistics',
studentStatistics: 'Student statistics',
degreeCourses: 'Degree courses',
common: 'Common',
}
@@ -0,0 +1,235 @@
import React, { useState, useEffect } from 'react'
import { Modal, Form, Button, TextArea, Loader } from 'semantic-ui-react'
import qs from 'query-string'
import {
useGetSavedCourseListsQuery,
useCreateCourseListMutation,
useUpdateCourseListMutation,
useDeleteCourseListMutation,
} from 'redux/completedCoursesSearch'
import SearchHistory from 'components/SearchHistory'
const CompletedCoursesSearch = ({ setValues, history, location }) => {
const [modal, setModal] = useState(false)
const [courseInput, setCourseInput] = useState('')
const [studentInput, setStudentInput] = useState('')
const [name, setName] = useState('')
const courseLists = useGetSavedCourseListsQuery()
const [searchList, setSearches] = useState(null)
const [selectedSearchId, setSelectedSearchId] = useState('')
const isFetchingOrLoading = courseLists.isLoading || courseLists.isFetching
const isError = courseLists.isError || (courseLists.isSuccess && !courseLists.data)
const [updateCourseList, { isLoading: updateIsLoading, data: updatedData }] = useUpdateCourseListMutation()
const [createCourseList, { isLoading: createIsLoading, data: createdData }] = useCreateCourseListMutation()
const [deleteCourseList, { isLoading: deleteIsLoading, data: deletedData }] = useDeleteCourseListMutation()
const parseQueryFromUrl = () => {
const query = qs.parse(location.search)
if (!Array.isArray(query.courseList)) query.courseList = [query.courseList]
if (!Array.isArray(query.studentList)) query.studentList = [query.studentList]
return query
}
useEffect(() => {
if (courseLists.isSuccess && courseLists.data) {
setSearches(courseLists.data)
}
}, [courseLists])
useEffect(() => {
if (updatedData) {
const updatedSearches = searchList.map(s => (s.id === updatedData.id ? updatedData : s))
setSearches(updatedSearches)
}
}, [updatedData])
useEffect(() => {
if (createdData && !createdData.error) {
const newList = searchList.concat(createdData)
setSearches(newList)
}
}, [createdData])
useEffect(() => {
if (deletedData) {
const filteredsearches = searchList.filter(s => s.id !== deletedData)
setSearches(filteredsearches)
}
}, [deletedData])
useEffect(() => {
setImmediate(() => {
if (location.search) {
const query = parseQueryFromUrl()
setValues(query)
}
})
}, [location.search])
const clearForm = () => {
setCourseInput('')
setStudentInput('')
setSelectedSearchId('')
}
const pushQueryToUrl = query => {
setImmediate(() => {
const searchString = qs.stringify(query)
history.push({ search: searchString })
})
}
const handleClose = () => {
setModal(false)
clearForm()
}
const onDelete = () => {
deleteCourseList({ id: selectedSearchId })
clearForm()
}
const onSave = () => {
const courseList = courseInput
.split(/[\s,]+/)
.map(code => code.trim().toUpperCase())
.filter(code => code !== '')
if (selectedSearchId !== '') {
updateCourseList({ id: selectedSearchId, courseList })
} else {
createCourseList({ courseList, name })
}
}
const onSelectSearch = selectedId => {
if (!selectedId) {
clearForm()
return
}
const { id: selectedSearchId } = selectedId
const selectedSearch = searchList?.find(search => search.id === selectedSearchId)
if (selectedSearch) {
setCourseInput(selectedSearch.courseList.join(', '))
setName(selectedSearch.name)
setSelectedSearchId(selectedSearch.id)
}
}
const onClicker = e => {
e.preventDefault()
const courseList = courseInput
.split(/[\s,]+/)
.map(code => code.trim().toUpperCase())
.filter(c => c !== '')
const studentList = studentInput
.split(/[\s,]+/)
.map(code => code.trim())
.filter(s => s !== '')
.map(s => (s.length === 8 ? `0${s}` : s))
if (courseList.length === 0 || studentList.length === 0 || studentList[0] === null) {
return
}
setValues({
studentList,
courseList,
})
pushQueryToUrl({ courseList, studentList })
handleClose()
}
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
if (isFetchingOrLoading) return <Loader active style={{ marginTop: '15em' }} />
return (
<Modal
trigger={
<Button size="small" color="blue" onClick={() => setModal(true)} data-cy="open-completed-courses-modal-button">
Search completed courses of students
</Button>
}
open={modal}
onClose={handleClose}
size="small"
>
<Modal.Content>
<Form>
<h2>Search completed courses of students</h2>
<Form.Field>
<em>Insert one or more student numbers, separated by a space, a newline, or a comma.</em>
<TextArea
value={studentInput}
placeholder="012345678, 12345678"
onChange={e => setStudentInput(e.target.value)}
data-cy="student-no-input"
/>
</Form.Field>
<Form.Field>
<em>Insert one or more courses, separated by a space, a newline, or a comma.</em>
<TextArea
value={courseInput}
placeholder="TKT12345, PSYK-123"
onChange={e => setCourseInput(e.target.value)}
data-cy="course-list-input"
/>
</Form.Field>
<Form.Field>
<Form.Field data-cy="search-name">
<em> Insert name for this course list if you wish to save it </em>
<Form.Input
disabled={selectedSearchId !== ''}
value={name}
placeholder="name"
onChange={e => setName(e.target.value)}
/>
</Form.Field>
</Form.Field>
<SearchHistory
header="Saved courselists"
items={
searchList
? searchList.map(list => ({
...list,
text: list.name,
timestamp: list.updatedAt,
params: { id: list.id },
}))
: []
}
updateItem={() => null}
handleSearch={onSelectSearch}
/>
<Modal.Actions>
<Button
data-cy="save-courselist"
disabled={!name || updateIsLoading || createIsLoading}
loading={updateIsLoading || createIsLoading}
floated="left"
icon="save"
onClick={onSave}
content="Save"
/>
<Button
disabled={!selectedSearchId || deleteIsLoading}
negative
floated="left"
icon="trash"
onClick={onDelete}
content="Delete"
/>
<Button onClick={handleClose}>Cancel</Button>
<Button positive onClick={e => onClicker(e)} data-cy="completed-courses-search-button">
Search
</Button>
</Modal.Actions>
</Form>
</Modal.Content>
</Modal>
)
}
export default CompletedCoursesSearch
@@ -0,0 +1,206 @@
import SortableTable from 'components/SortableTable'
import React, { useEffect, useState } from 'react'
import { useGetCompletedCoursesQuery } from 'redux/completedCoursesSearch'
import { Icon, Loader } from 'semantic-ui-react'
import moment from 'moment'
import StudentNameVisibilityToggle, { useStudentNameVisibility } from 'components/StudentNameVisibilityToggle'
import useLanguage from 'components/LanguagePicker/useLanguage'
import RightsNotification from 'components/RightsNotification'
import StudentInfoItem from 'components/common/StudentInfoItem'
import { hiddenNameAndEmailForExcel } from 'common/columns'
import sendEvent from '../../common/sendEvent'
const getColumns = (courses, showStudentNames, getTextIn) => {
const isPassed = credit => [4, 7, 9].includes(credit)
// 4=completed, 7=improved, 9=transferred, 10=failed
const getCompletion = (student, courseCode, { icon }) => {
const completion = student.credits.find(c => c.courseCode === courseCode && isPassed(c.creditType))
const enrollment = student.enrollments.find(e => e.courseCode === courseCode)
if (completion === undefined) {
if (!enrollment) {
return icon ? null : ''
}
if (icon) {
if (moment(enrollment.date) > moment().subtract(6, 'months')) {
return <Icon fitted name="minus" color="yellow" />
}
return <Icon fitted name="minus" color="grey" />
}
return `Latest enrollment: ${moment(enrollment.date).format('YYYY-MM-DD')}`
}
const substitutionString = completion.substitution ? ` as ${completion.substitution}` : ''
return icon ? <Icon fitted name="check" color="green" /> : `Passed${substitutionString}`
}
const getTotalPassed = s => s.credits.filter(c => isPassed(c.creditType)).length
const getTotalUnfinished = student => student.enrollments.length
const style = {
verticalAlign: 'middle',
textAlign: 'center',
}
const studentNbrColumn = [
{
key: 'studentnumber',
title: 'Student Number',
cellProps: { style },
getRowVal: s => s.studentNumber,
getRowContent: s => <StudentInfoItem student={s} view="Completed courses search tool" showSisuLink />,
},
]
const getCellProps = (student, courseCode) => {
const creditDate = student.credits.find(credit => credit.courseCode === courseCode)?.date
const enrollmentDate = student.enrollments.find(enrollment => enrollment.courseCode === courseCode)?.date
if (!creditDate && !enrollmentDate) {
return { style }
}
const title = creditDate
? `Passed on ${moment(creditDate).format('YYYY-MM-DD')} `
: `Last enrollment on ${moment(enrollmentDate).format('YYYY-MM-DD')} `
return { style, title }
}
const completionStatusColumns = courses.map(course => {
return {
key: course.code,
title: (
<div key={course.code} style={{ marginLeft: '-6px', width: '8em', whiteSpace: 'normal' }}>
<div key={`key1${course.code}`}>{course.code}</div>
<div key={`key2${course.code}`} style={{ color: 'gray', fontWeight: 'normal' }}>
{getTextIn(course.name)}
</div>
</div>
),
textTitle: course.code,
cellProps: s => getCellProps(s, course.code),
headerProps: { title: course.code },
getRowVal: s => getCompletion(s, course.code, { icon: false }),
getRowContent: s => getCompletion(s, course.code, { icon: true }),
}
})
const statisticColumns = [
{
key: 'passed',
title: 'Passed',
cellProps: { style },
headerProps: { title: 'Passed' },
getRowVal: s => getTotalPassed(s),
child: true,
},
{
key: 'unfinished',
title: 'Unfinished',
headerProps: { title: 'Unfinished' },
getRowVal: s => getTotalUnfinished(s),
cellProps: { style },
child: true,
},
]
const statisticHeader = {
key: 'statistics',
title: <b>Totals:</b>,
textTitle: null,
parent: true,
children: statisticColumns,
}
const emailColumn = showStudentNames
? [
{
key: 'email',
title: 'Email',
cellProps: { style },
headerProps: { title: 'Email' },
getRowVal: s => (s.email ? s.email : ''),
child: true,
export: false,
},
]
: []
const nameColumns = showStudentNames
? [
{
key: 'lastname',
title: 'Last name',
cellProps: { style },
getRowVal: s => s.lastname,
export: false,
},
{
key: 'firstnames',
title: 'First names',
cellProps: { style },
getRowVal: s => s.firstnames,
export: false,
},
]
: []
return [...nameColumns, ...studentNbrColumn, ...completionStatusColumns, statisticHeader, ...emailColumn]
}
const CompletedCoursesSearchResults = ({ searchValues }) => {
const { courseList, studentList } = searchValues
const [data, setData] = useState(null)
const showStudentNames = useStudentNameVisibility()
const completedCoursesTable = useGetCompletedCoursesQuery({ courseList, studentList })
const isFetchingOrLoading = completedCoursesTable.isLoading || completedCoursesTable.isFetching
const isError = completedCoursesTable.isError || (completedCoursesTable.isSuccess && !completedCoursesTable.data)
const { getTextIn } = useLanguage()
const sendAnalytics = sendEvent.populationStudents
useEffect(() => {
if (!isError && !isFetchingOrLoading) {
setImmediate(() => {
const data = completedCoursesTable?.data
setData(data)
})
}
}, [completedCoursesTable])
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
if (isFetchingOrLoading || data === null) return <Loader active style={{ marginTop: '15em' }} />
return (
<div>
<StudentNameVisibilityToggle />
{data.discardedStudentNumbers?.length > 0 && (
<RightsNotification discardedStudentNumbers={data.discardedStudentNumbers} />
)}
<div
style={{ maxWidth: '100vh', overflowX: 'auto', paddingBottom: '50px' }}
data-cy="completed-courses-table-div"
>
<SortableTable
title="Completed courses search"
getRowKey={s => s.studentNumber}
tableProps={{
celled: true,
compact: 'very',
padded: false,
collapsing: true,
basic: true,
striped: true,
singleLine: true,
textAlign: 'center',
}}
columns={getColumns(data.courses, showStudentNames.visible, getTextIn, sendAnalytics)}
onlyExportColumns={hiddenNameAndEmailForExcel}
data={data.students}
/>
</div>
</div>
)
}
export default CompletedCoursesSearchResults
@@ -0,0 +1,56 @@
import React, { useState, useEffect } from 'react'
import { withRouter, useLocation, useHistory } from 'react-router-dom'
import { Message, Icon } from 'semantic-ui-react'
import useLanguage from 'components/LanguagePicker/useLanguage'
import { useTitle } from '../../common/hooks'
import CompletedCoursesSearch from './CompletedCoursesSearch'
import CompletedCoursesSearchResults from './CompletedCoursesSearchResults'
import TSA from '../../common/tsa'
const CompletedCourses = () => {
useTitle('Search completed courses')
const { language } = useLanguage()
const [searchValues, setValues] = useState({})
const location = useLocation()
const history = useHistory()
useEffect(() => {
if (searchValues && searchValues.courseList?.length > 0) {
TSA.Influx.sendEvent({
group: 'Completed Courses Tool Usage',
name: 'completed courses tool',
label: 'completedCoursesTool',
value: 1,
})
}
}, [searchValues])
return (
<div className="segmentContainer">
<Message style={{ maxWidth: '800px', fontSize: '16px' }}>
<Message.Header>Search completed courses</Message.Header>
<p>
Here you can search by a list of student and course numbers to see whether students have completed certain
courses yet. The tool will also show if students have enrolled on the course, if they have not yet completed
it. Course substitutions are taken into account.
</p>
<p>
<Icon fitted name="check" color="green" />: Student has completed the course with a passing grade. <br />
<Icon fitted name="minus" color="yellow" />: Student has not completed the course, but has an active
enrollment from less than 6 months ago. <br />
<Icon fitted name="minus" color="grey" />: Student has not completed the course, but has an enrollment from
more than 6 months ago. <br />
<b>Empty cell</b>: Student has no completion or enrollment for the course.
</p>
</Message>
<CompletedCoursesSearch setValues={setValues} history={history} location={location} />
<div style={{ paddingTop: '25px' }}>
{searchValues && searchValues.courseList?.length > 0 && searchValues.studentList?.length > 0 && (
<CompletedCoursesSearchResults searchValues={searchValues} language={language} />
)}
</div>
</div>
)
}
export default withRouter(CompletedCourses)
@@ -628,7 +628,7 @@ const ProtoC = ({
</Menu.Item>
{sorterNames.map(sorterName => (
<Menu.Item
basic={sorter !== sorterName}
basic={(sorter !== sorterName).toString()}
color={sorter === sorterName ? 'blue' : 'black'}
key={sorterName}
active={sorter === sorterName}
@@ -727,7 +727,7 @@ const ProtoC = ({
</Segment>
<RenderBelowGraph />
<Message>
<ReactMarkdown children={CoolDataScience.protoC} escapeHtml={false} />
<ReactMarkdown children={CoolDataScience.protoC} />
</Message>
</Segment>
)
@@ -1,3 +1,4 @@
/* eslint-disable babel/no-invalid-this */
import React, { useState, useCallback, useMemo } from 'react'
import Highcharts from 'highcharts'
import ReactHighcharts from 'react-highcharts'
@@ -449,7 +450,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
</Menu.Item>
{sorterNames.map(sorterName => (
<Menu.Item
basic={sorter !== sorterName}
basic={(sorter !== sorterName).toString()}
color={sorter === sorterName ? 'blue' : 'black'}
key={sorterName}
active={sorter === sorterName}
@@ -485,7 +486,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
<Message>
{
// eslint-disable-next-line react/no-children-prop
<ReactMarkdown children={CoolDataScience.protoC2} escapeHtml={false} />
<ReactMarkdown children={CoolDataScience.protoC2} />
}
</Message>
</Segment>
@@ -541,7 +542,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
</Menu.Item>
{sorterNames.map(sorterName => (
<Menu.Item
basic={sorter !== sorterName}
basic={(sorter !== sorterName).toString()}
color={sorter === sorterName ? 'blue' : 'black'}
key={sorterName}
active={sorter === sorterName}
@@ -594,7 +595,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
<Message>
{
// eslint-disable-next-line react/no-children-prop
<ReactMarkdown children={CoolDataScience.protoC2} escapeHtml={false} />
<ReactMarkdown children={CoolDataScience.protoC2} />
}
</Message>
</Segment>
@@ -100,7 +100,7 @@ const Chart = React.memo(({ tickDates, snapshots }) => (
return `${d.getDate()}.${d.getMonth()}.${d
.getFullYear()
.toString()
.substr(-2)}`
.substring(-2)}`
}
},
tickPositions: tickDates.map(date => date.getTime()),
@@ -292,7 +292,7 @@ const ProtoG = ({ uberdata, years, isLoading, loadingYears, getUberDispatch, get
)}
<Message>
{//eslint-disable-next-line react/no-children-prop
<ReactMarkdown children={CoolDataScience.protoC} escapeHtml={false} />
<ReactMarkdown children={CoolDataScience.protoC} />
}
</Message>
</Segment>
@@ -185,7 +185,7 @@ export default ({ value, changeSetting, settings, generalHelp }) => {
<div style={{ padding: '1em' }}>
{
// eslint-disable-next-line react/no-children-prop
<ReactMarkdown children={unindent(generalHelp)} escapeHtml={false} />
<ReactMarkdown children={unindent(generalHelp)} />
}
</div>
{settings.map(({ key, label, long }) => (
@@ -196,7 +196,7 @@ export default ({ value, changeSetting, settings, generalHelp }) => {
Valinta "<i>{label}</i>"
</b>
<div style={{ margin: '0.5em', fontSize: '0.9em' }}>
<ReactMarkdown escapeHtml={false}>{unindent(long ?? '')}</ReactMarkdown>
<ReactMarkdown>{unindent(long ?? '')}</ReactMarkdown>
</div>
</div>
</div>
@@ -242,7 +242,7 @@ const CoursePopulation = ({
content: {
content: (
<div ref={programmeRef}>
<CustomPopulationCoursesWrapper filteredStudents={filtered} showFilter={false} />
<CustomPopulationCoursesWrapper filteredStudents={filtered} showFilter />
</div>
),
},
@@ -372,7 +372,12 @@ const CustomPopulationCoursesWrapper = props => {
if (isLoading) return null
return <CustomPopulationCourses {...props} courses={courseStatistics} />
return (
<>
<InfoBox content={infotooltips.PopulationStatistics.CoursesOfPopulation} />
<CustomPopulationCourses {...props} courses={courseStatistics} />
</>
)
}
const mapStateToProps = ({ singleCourseStats, populations, courseSearch }) => {
@@ -71,7 +71,7 @@ const getColumns = (stats, showDetails, showGrades, userHasAccessToAllStats, alt
},
{
key: 'TOTAL',
title: 'Total Students',
title: 'Total\nStudents',
helpText: 'Total count of students, including enrolled students with no grade.',
cellProps: s => ({
style: {
@@ -113,7 +113,7 @@ const getColumns = (stats, showDetails, showGrades, userHasAccessToAllStats, alt
...getGradeColumns(resolveGrades(stats)),
{
key: 'ENROLLMENTS_MISSING_GRADE',
title: 'Enrolled no grade',
title: 'Enrolled,\nno grade',
filterType: 'range',
helpText: 'Total count of students with a valid enrollment and no passing or failing grade.',
getRowVal: s => (s.rowObfuscated ? 5 : s.students.enrolledStudentsWithNoGrade),
@@ -6,7 +6,7 @@ import { useHistory } from 'react-router-dom'
import Highcharts from 'highcharts/highstock'
import { Button, Radio } from 'semantic-ui-react'
import boostcanvas from 'highcharts/modules/boost-canvas'
import _ from 'lodash/fp'
import _ from 'lodash'
import boost from 'highcharts/modules/boost'
import ReactHighstock from 'react-highcharts/ReactHighstock'
import './creditAccumulationGraphHC.css'
@@ -31,6 +31,7 @@ const createGraphOptions = ({
startDate,
endDate,
graduations,
transfers,
studyRightStartLine,
}) => {
const tooltip = {
@@ -94,13 +95,14 @@ const createGraphOptions = ({
min: startDate,
max: endDate,
plotLines: graduations
.map(value => ({
value,
.map(graduation => ({
value: graduation.value,
color: '#a333c8',
width: 2,
dashStyle: 'dash',
label: {
text: `Graduation`,
text: `Graduation ${graduation.studyright}`,
fontSize: 30,
},
}))
.concat(
@@ -111,6 +113,19 @@ const createGraphOptions = ({
dashStyle: 'dash',
label: {
text: `Population study start`,
fontSize: 30,
},
}))
)
.concat(
transfers.map(transfer => ({
value: transfer.value,
color: '#cbd128',
width: 2,
dashStyle: 'dash',
label: {
text: `Transfer ${transfer.studyright}`,
fontSize: 30,
},
}))
),
@@ -138,19 +153,20 @@ const filterCourses = (
byStudyPlanOfCode,
cutStudyPlanCredits,
startDate,
customStudyStartYear
customStudyStartYear,
studyrightid
) => {
if (byStudyPlanOfCode && cutStudyPlanCredits)
return filterCoursesByDate(
filterCoursesByStudyPlan(
student.studyplans.find(p => p.programme_code === byStudyPlanOfCode),
student.studyplans.find(p => p.programme_code === byStudyPlanOfCode && p.studyrightid === studyrightid),
student.courses
),
customStudyStartYear || student.studyrightStart
)
if (byStudyPlanOfCode)
return filterCoursesByStudyPlan(
student.studyplans.find(p => p.programme_code === byStudyPlanOfCode),
student.studyplans.find(p => p.programme_code === byStudyPlanOfCode && p.studyrightid === studyrightid),
student.courses
)
if (singleStudent) return student.courses
@@ -271,6 +287,86 @@ const resolveStudyRightElement = ({ studyright_elements }) => {
)
}
const filterGraduations = (student, selectedStudyRight) => {
const graduated = student.studyrights.filter(({ graduated }) => graduated)
// eslint-disable-next-line camelcase
if (!selectedStudyRight)
return graduated.map(({ enddate, studyright_elements }) => {
const studyrightElem = studyright_elements
? studyright_elements.filter(ele => ele.element_detail?.type === 20)
: []
let elemName = ''
if (studyrightElem.length > 0) {
elemName = getTextIn(studyrightElem[0].element_detail?.name)
}
return {
value: new Date(enddate).getTime(),
studyright: elemName,
}
})
const selectedGraduation = graduated.find(({ studyrightid }) => studyrightid === selectedStudyRight.studyrightid)
if (!selectedGraduation) return []
const element = selectedGraduation.studyright_elements
? selectedGraduation.studyright_elements.filter(ele => ele.element_detail?.type === 20)
: []
return [
{
value: new Date(selectedGraduation.enddate).getTime(),
studyright: element.length > 0 ? getTextIn(element[0].element_detail?.name) : '',
},
]
}
const addGraduation = (points, graduation, notFirst) => {
const graduationX = new Date(graduation.enddate).getTime()
const index = points.findIndex(point => point.x > graduationX)
let graduationY
if (index <= 0) {
graduationY = graduationX > points[0].x ? points[points.length - 1].y : points[0].y
} else {
const yBefore = points[index - 1].y
const yAfter = points[index].y
graduationY = Math.round((yBefore + yAfter) / 2)
}
const marker = {
radius: 10,
symbol: 'diamond',
enabled: true,
}
const masterMarker = {
radius: 12,
symbol: 'circle',
lineWidth: 2,
lineColor: '#ffffff',
enabled: true,
}
points.push({
x: graduationX,
y: graduationY,
marker: notFirst ? masterMarker : marker,
})
points.sort((a, b) => a.x - b.x)
}
const findGraduationsByCodes = (student, programmeCodes) => {
const graduations = student.studyrights
.filter(({ graduated }) => graduated)
.filter(({ studyright_elements }) =>
studyright_elements.find(
({ element_detail }) => programmeCodes.includes(element_detail.code) && element_detail.type === 20
)
)
graduations.sort((a, b) => {
if (a.enddate > b.enddate) return 1
if (a.enddate === b.enddate) return 0
return -1
})
return graduations ?? []
}
const createStudentCreditLines = (
students,
singleStudent,
@@ -278,17 +374,18 @@ const createStudentCreditLines = (
studyRightId,
studyPlanFilterIsActive,
cutStudyPlanCredits,
programmeCode,
programmeCodes,
customStudyStartYear
) =>
students.map(student => {
const { studyrightStart } = student
const startDate = singleStudent ? selectedStartDate : studyrightStart
const { code } = resolveStudyRightElement(
student.studyrights.find(({ studyrightid }) => studyrightid === studyRightId) || {}
)
const studyPlanProgrammeCode = singleStudent ? code : studyPlanFilterIsActive && programmeCode
const studyPlanProgrammeCode = singleStudent
? code
: studyPlanFilterIsActive && programmeCodes?.length > 0 && programmeCodes[0]
const { points } = _.flow(
() =>
@@ -298,13 +395,20 @@ const createStudentCreditLines = (
studyPlanProgrammeCode,
cutStudyPlanCredits,
startDate,
customStudyStartYear
customStudyStartYear,
studyRightId
),
courses => [...courses].filter(({ date }) => new Date(date) <= new Date()),
sortCoursesByDate,
courses => courses.reduce(reduceCreditsToPoints, { credits: 0, points: [], singleStudent })
)(student.courses)
const graduations = programmeCodes ? findGraduationsByCodes(student, programmeCodes) : []
if (points?.length > 0) {
graduations.forEach((graduation, index) => addGraduation(points, graduation, index > 0))
}
return {
name: student.studentNumber,
data: points,
@@ -316,13 +420,17 @@ const createStudentCreditLines = (
}
})
const filterGraduations = (student, selectedStudyRight) => {
const graduated = student.studyrights.filter(({ graduated }) => graduated)
// eslint-disable-next-line camelcase
if (!selectedStudyRight) return graduated.map(({ enddate }) => new Date(enddate).getTime())
const selectedGraduation = graduated.find(({ studyrightid }) => studyrightid === selectedStudyRight.studyrightid)
if (!selectedGraduation) return []
return [new Date(selectedGraduation.enddate).getTime()]
const findTransferName = (student, transfer) =>
student.studyrights.flatMap(right => right.studyright_elements).find(elem => elem.code === transfer.targetcode)
.element_detail.name
const filterTransfers = (student, language) => {
const transferTimes = student.transfers.map(transfer => ({
value: new Date(transfer.transferdate).getTime(),
studyright: `to ${getTextIn(findTransferName(student, transfer), language)}`,
}))
const removeOverlapping = transferTimes.filter((transfer, i, a) => a.indexOf(transfer) === i)
return removeOverlapping
}
const CreditAccumulationGraphHighCharts = ({
@@ -332,7 +440,7 @@ const CreditAccumulationGraphHighCharts = ({
startDate,
endDate,
studyRightId,
programmeCode,
programmeCodes,
customPopulation = false,
studyPlanFilterIsActive,
customStudyStartYear,
@@ -340,7 +448,6 @@ const CreditAccumulationGraphHighCharts = ({
const history = useHistory()
const chartRef = useRef()
const language = useLanguage()
const [graphHeight, setGraphHeight] = useState(700)
const [cutStudyPlanCredits, setCutStudyPlanCredits] = useState(false)
const selectedStudyRight =
@@ -357,7 +464,7 @@ const CreditAccumulationGraphHighCharts = ({
studyRightId,
studyPlanFilterIsActive,
cutStudyPlanCredits,
programmeCode,
programmeCodes,
customStudyStartYear
),
[
@@ -366,9 +473,10 @@ const CreditAccumulationGraphHighCharts = ({
startDate,
studyRightId,
studyPlanFilterIsActive,
programmeCode,
programmeCodes,
cutStudyPlanCredits,
customStudyStartYear,
language,
]
)
@@ -420,8 +528,8 @@ const CreditAccumulationGraphHighCharts = ({
return Math.min(..._.flatten(students.map(({ courses }) => courses.map(({ date }) => new Date(date).getTime()))))
return studyRightStart
}
const graduations = singleStudent ? filterGraduations(students[0], selectedStudyRight) : []
const transfers = singleStudent ? filterTransfers(students[0], language) : []
const studyRightStartLine =
!singleStudent && studyPlanFilterIsActive
? [new Date(customStudyStartYear || students[0].studyrightStart).getTime()]
@@ -436,10 +544,12 @@ const CreditAccumulationGraphHighCharts = ({
tooltipFormatter: point => singleStudent && singleStudentTooltipFormatter(point, students[0], language),
onPointClicked: point => {
if (!singleStudent) {
if (point.series.name.length > 10) return
history.push(`/students/${point.series.name}`)
}
},
graduations,
transfers,
studyRightStartLine,
})
@@ -13,7 +13,6 @@ const getTableData = studentsData => {
courseInfo: { ...studentsData[student].courseInfo },
email: studentsData[student].email,
secondaryEmail: studentsData[student].secondaryEmail,
disseminationInfoAllowed: studentsData[student].disseminationInfoAllowed,
totals: studentsData[student].totals,
},
],
@@ -75,23 +74,7 @@ const getColumns = (labelsToCourses, language) => {
},
]
const getDisseminationInfoAllowed = s => {
if (s.disseminationInfoAllowed === null) return 'unknown'
if (s.disseminationInfoAllowed) return 'yes'
return 'no'
}
const informationColumns = [
{
key: 'disseminationInfoAllowed',
title: 'Marketing Allowed',
getRowVal: s => getDisseminationInfoAllowed(s),
getRowContent: s => getDisseminationInfoAllowed(s),
headerProps: { title: 'Marketing Allowed' },
child: true,
childOf: 'Marketing Allowed',
cellProps: { style },
},
{
key: 'email-child',
title: 'Email',
@@ -122,11 +105,11 @@ const getColumns = (labelsToCourses, language) => {
const findRowValue = (s, courseCode, hidden = false) => {
if (s.courseInfo[courseCode] === undefined) return ''
if (s.courseInfo[courseCode].status.passed && hidden)
return `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('DD-MM-YYYY')}`
return `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('YYYY-MM-DD')}`
if (s.courseInfo[courseCode].status.failed && hidden)
return `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('DD-MM-YYYY')}`
return `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('YYYY-MM-DD')}`
if (s.courseInfo[courseCode].status.unfinished && hidden)
return `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('DD-MM-YYYY')}`
return `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('YYYY-MM-DD')}`
if (s.courseInfo[courseCode].status.passed) return 'Passed'
if (s.courseInfo[courseCode].status.failed) return 'Failed'
if (s.courseInfo[courseCode].status.unfinished) return 'Unfinished'
@@ -139,13 +122,13 @@ const getColumns = (labelsToCourses, language) => {
}
if (s.courseInfo[courseCode] === undefined) return propObj
if (s.courseInfo[courseCode].status.passed)
return { ...propObj, title: `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('DD-MM-YYYY')}` }
return { ...propObj, title: `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('YYYY-MM-DD')}` }
if (s.courseInfo[courseCode].status.failed)
return { ...propObj, title: `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('DD-MM-YYYY')}` }
if (s.courseInfo[courseCode].status.passed)
return { ...propObj, title: `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('YYYY-MM-DD')}` }
if (s.courseInfo[courseCode].status.unfinished)
return {
...propObj,
title: `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('DD-MM-YYYY')}`,
title: `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('YYYY-MM-DD')}`,
}
return propObj
}
@@ -37,10 +37,10 @@ const CustomOpenUniPopulation = () => {
<Message style={{ maxWidth: '800px', fontSize: '16px' }}>
<Message.Header>Open uni student population</Message.Header>
<p>
Here you can create custom population using a list of courses. Clicking the blue custom population button will
open a modal where you can enter a list of courses. <br />
<Icon fitted name="check" color="green" />: Student passed the course. <br />
<Icon fitted name="times" color="red" />: Student failed the course. <br />
Here you can create custom population using a list of courses. Clicking the blue button will open a modal
where you can enter a list of courses. <br />
<Icon fitted name="check" color="green" />: Student has passed the course. <br />
<Icon fitted name="times" color="red" />: Student has failed the course. <br />
<Icon fitted name="minus" color="grey" />: Student has enrolled, but has not received any grade from the
course. <br />
<b>Empty cell</b>: Student has no enrollments for the course. <br />
@@ -25,6 +25,7 @@ const CustomPopulationSearch = ({
loading,
customPopulationSearches,
customPopulationSearchSaving,
setStudentsInput,
}) => {
const [modal, setModal] = useState(false)
const [input, setInput] = useState('')
@@ -54,8 +55,15 @@ const CustomPopulationSearch = ({
clearForm()
}
const parseInput = studentNumbers =>
studentNumbers
.split(/[\s,]+/)
.map(code => code.trim())
.filter(s => s !== '')
.map(s => (s.length === 8 ? `0${s}` : s))
const onSave = () => {
const studentnumberlist = input.match(/[0-9]+/g)
const studentnumberlist = parseInput(input)
if (selectedSearchId) {
updateCustomPopulationSearchDispatch({ id: selectedSearchId, studentnumberlist })
} else {
@@ -87,7 +95,8 @@ const CustomPopulationSearch = ({
const onClicker = e => {
e.preventDefault()
const studentnumbers = input.match(/[0-9]+/g).map(sNumber => (sNumber.length === 8 ? `0${sNumber}` : sNumber))
const studentnumbers = parseInput(input)
setStudentsInput(studentnumbers)
getCustomPopulationDispatch({ studentnumberlist: studentnumbers, onProgress })
getCustomPopulationCoursesByStudentnumbers({ studentnumberlist: studentnumbers })
selectCustomPopulationSearchDispatch(selectedSearchId || null)
@@ -4,7 +4,6 @@ import xlsx from 'xlsx'
import { Dropdown } from 'semantic-ui-react'
import { getStudentGradeMean, getStudentGradeMeanWeightedByCredits, getStudentTotalCredits } from '../../common'
import sendEvent from '../../common/sendEvent'
import DataExport from '../PopulationStatistics/DataExport'
const sendAnalytics = sendEvent.populationStudents
@@ -28,7 +27,6 @@ export default ({ students }) => {
return (
<>
<DataExport students={students} />
<Dropdown.Item
onClick={() => {
xlsx.writeFile(getXlsx(), filename)
@@ -5,6 +5,7 @@ import { Segment, Header, Accordion, Message } from 'semantic-ui-react'
import _ from 'lodash'
import scrollToComponent from 'react-scroll-to-component'
import semestersApi from 'redux/semesters'
import RightsNotification from 'components/RightsNotification'
import { useProgress, useTitle } from '../../common/hooks'
import infotooltips from '../../common/InfoToolTips'
import { getCustomPopulationSearches } from '../../redux/customPopulationSearch'
@@ -27,7 +28,6 @@ import {
tagsFilter,
creditDateFilter,
enrollmentStatusFilter,
studyTrackFilter,
} from '../FilterView/filters'
import useLanguage from '../LanguagePicker/useLanguage'
import CustomPopulationSearch from './CustomPopulationSearch'
@@ -73,7 +73,6 @@ const CustomPopulation = () => {
allSemesters: allSemesters ?? [],
language,
}),
studyTrackFilter,
],
[courseStats, allSemesters, language]
)
@@ -89,12 +88,13 @@ const CustomPopulationContent = ({ students, custompop }) => {
const { language } = useLanguage()
const [activeIndex, setIndex] = useState([])
const [newestIndex, setNewest] = useState(null)
const [studentsInput, setStudentsInput] = useState(null)
const [discardedStudentNumbers, setDiscarded] = useState([])
const creditGainRef = useRef()
const programmeRef = useRef()
const coursesRef = useRef()
const studentRef = useRef()
const refs = [creditGainRef, programmeRef, coursesRef, studentRef]
const { customPopulationSearches, searchedCustomPopulationSearchId } = useSelector(
state => state.customPopulationSearch
)
@@ -108,6 +108,15 @@ const CustomPopulationContent = ({ students, custompop }) => {
studentNumbers: students.map(s => s.studentNumber),
})
useEffect(() => {
setDiscarded(
_.difference(
studentsInput,
students.map(s => s.studentNumber)
)
)
}, [studentsInput, students])
useEffect(() => {
if (newestIndex) {
scrollToComponent(refs[newestIndex].current, { align: 'bottom' })
@@ -179,6 +188,7 @@ const CustomPopulationContent = ({ students, custompop }) => {
content: {
content: (
<div ref={coursesRef}>
<InfoBox content={infotooltips.PopulationStatistics.CoursesOfPopulation} />
<CustomPopulationCourses filteredStudents={students} courses={courseStats} />
</div>
),
@@ -236,7 +246,10 @@ const CustomPopulationContent = ({ students, custompop }) => {
studentnumbers you have access rights to i.e. you have rights to the programme they are in.
</p>
</Message>
<CustomPopulationSearch />
{discardedStudentNumbers?.length > 0 && !populations.pending && (
<RightsNotification discardedStudentNumbers={discardedStudentNumbers} />
)}
<CustomPopulationSearch setStudentsInput={setStudentsInput} />
{custompop.length > 0 && customPopulationFlag ? (
<Segment className="contentSegment">{renderCustomPopulation(students)}</Segment>
) : (
@@ -0,0 +1,64 @@
import React from 'react'
import GraduationTimes from 'components/FacultyStatistics/TimesAndPaths/GraduationTimes'
const FacultyGraduations = ({ faculty, graduationStats, groupByStartYear, showMedian }) => {
const groupBy = groupByStartYear ? 'byStartYear' : 'byGradYear'
const label = groupByStartYear ? 'Start year' : 'Graduation year'
const data = graduationStats?.data?.[groupBy].medians
const goals = graduationStats?.data?.goals
const goalExceptions = { ...goals?.exceptions, needed: faculty === 'H30' }
const programmeData = graduationStats?.data?.[groupBy].programmes.medians
const programmeNames = graduationStats?.data?.programmeNames
const classSizes = graduationStats?.data?.classSizes
const commonProps = { label, programmeNames, showMedian, classSizes, goalExceptions }
return (
<>
<div>
<GraduationTimes
level="bachelor"
title="Bachelor"
data={data?.bachelor}
goal={goals?.bachelor}
levelProgrammeData={programmeData?.bachelor}
{...commonProps}
/>
<GraduationTimes
level="bcMsCombo"
title="Bachelor + Master"
data={data?.bcMsCombo}
goal={goals?.bcMsCombo}
levelProgrammeData={programmeData?.bcMsCombo}
groupBy={groupBy}
{...commonProps}
/>
<GraduationTimes
level="master"
title="Master"
data={data?.master}
goal={goals?.master}
levelProgrammeData={programmeData?.master}
{...commonProps}
/>
<GraduationTimes
level="doctor"
title="Doctor"
data={data?.doctor}
goal={goals?.doctor}
levelProgrammeData={programmeData?.doctor}
{...commonProps}
/>
<GraduationTimes
level="licentiate"
title="Licentiate"
data={data?.licentiate}
goal={goals?.licentiate}
levelProgrammeData={programmeData?.licentiate}
{...commonProps}
/>
</div>
</>
)
}
export default FacultyGraduations
@@ -0,0 +1,153 @@
import React from 'react'
import { Message } from 'semantic-ui-react'
import FacultyProgressTable from '../FacultyStatistics/FacultyProgrammeOverview/FacultyProgressTable'
import FacultyBarChart from '../FacultyStatistics/FacultyProgrammeOverview/FacultyBarChart'
import sortProgrammeKeys from '../FacultyStatistics/facultyHelpers'
const FacultyProgress = ({ faculty, progressStats, language, getDivider }) => {
return (
<>
{getDivider('Bachelor', 'BachelorStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyBachelorsProgress"
data={{
id: faculty,
stats: progressStats?.data.bachelorsGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.bachelorsTableStats}
programmeStats={progressStats?.data.bachelorsProgStats}
titles={progressStats?.data.bachelorTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.bachelorsProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorsProgressTable"
progressTitles={progressStats?.data.yearlyBachelorTitles}
/>
</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
cypress="FacultyBachelorMastersProgress"
data={{
id: faculty,
stats: progressStats?.data.bcMsGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.bcMsTableStats}
programmeStats={progressStats?.data.bcMsProgStats}
titles={progressStats?.data.bcMsTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.bcMsProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty
).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 === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
/>
</div>
</div>
{!(faculty === 'H90') && (
<>
{getDivider('Master', 'MasterStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyMastersProgress"
data={{
id: faculty,
stats: progressStats?.data.mastersGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.mastersTableStats}
programmeStats={progressStats?.data.mastersProgStats}
titles={progressStats?.data.mastersTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.mastersProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty
).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 === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
/>
</div>
</div>
</>
)}
{getDivider('Doctor', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyDoctoralProgress"
data={{
id: faculty,
stats: progressStats?.data.doctoralGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.doctoralTableStats}
programmeStats={progressStats?.data.doctoralProgStats}
titles={progressStats?.data.doctoralTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.doctoralProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyDoctoralProgressTable"
/>
</div>
</div>
</>
)
}
export default FacultyProgress
@@ -0,0 +1,145 @@
import React, { useState } from 'react'
import { Divider, Header, Loader, Message } from 'semantic-ui-react'
import {
useGetFacultiesQuery,
useGetFacultyProgressStatsQuery,
useGetFacultyGraduationTimesQuery,
} from 'redux/facultyStats'
import { getTextIn } from 'common'
import FacultyGraduations from './FacultyGraduations'
import FacultyProgress from './FacultyProgress'
import Toggle from '../StudyProgramme/Toggle'
import InfoBox from '../Info/InfoBox'
import InfotoolTips from '../../common/InfoToolTips'
import useLanguage from '../LanguagePicker/useLanguage'
import '../FacultyStatistics/faculty.css'
const FacultyView = ({ faculty }) => {
const [graduatedGroup, setGraduatedGroup] = useState(false)
const [showMedian, setShowMedian] = useState(false)
const toolTips = InfotoolTips.Faculty
const studyProgrammeFilter = 'NEW_STUDY_PROGRAMMES'
const specials = 'SPECIAL_EXCLUDED'
const graduated = graduatedGroup ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
const { language } = useLanguage()
const allFaculties = useGetFacultiesQuery()
const faculties = allFaculties?.data
const facultyDetails = faculties && faculty && faculties.find(f => f.code === faculty)
const facultyName = facultyDetails && getTextIn(facultyDetails.name, language)
const progressStats = useGetFacultyProgressStatsQuery({
id: faculty,
studyProgrammeFilter,
specialGroups: specials,
graduated,
})
const graduationStats = useGetFacultyGraduationTimesQuery({ id: faculty, studyProgrammeFilter })
const getDivider = (title, toolTipText, content, cypress = undefined) => (
<>
<div className="divider">
<Divider data-cy={`Section-${toolTipText}`} horizontal>
{title}
</Divider>
</div>
{content === 'no-infobox' ? null : <InfoBox content={content} cypress={cypress} />}
</>
)
if (allFaculties.isLoading || allFaculties.isFetching) {
return <Loader active style={{ marginTop: '10em' }} />
}
const isFetchingOrLoading =
progressStats.isLoading || progressStats.isFetching || graduationStats.isLoading || graduationStats.isFetching
const isError =
(progressStats.isError && graduationStats.isError) ||
(progressStats.isSuccess && !progressStats.data && graduationStats.isSuccess && !graduationStats.data)
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
return (
<>
<div align="center" style={{ padding: '30px' }}>
<Header textAlign="center">{facultyName}</Header>
<span>{faculty}</span>
</div>
<Message info>
<Message.Header>This view is an abridged version of Oodikone's Faculty Overview</Message.Header>
<p>
In these statistics, all special studyrights have been excluded, eg. exchange students and non-degree
students.
</p>
<p>
<b>You can find more statistics on this and all other faculties of the university in main Faculty view. </b>
The full view includes details such as: how many have graduated or started in each faculty and in its
programmes; credits produced by the faculty; populations backgrounds and enrollment statuses. This view
provides statistics both on the whole faculty level and a breakdown of how they are spread among the faculty's
programmes.
</p>
<p>Access the full Faculty view by clicking 'Faculty' in the top navigation bar.</p>
</Message>
<div className="faculty-overview">
{isFetchingOrLoading ? (
<Loader active style={{ marginTop: '15em' }} />
) : (
<div className="programmes-overview">
{progressStats.isSuccess && progressStats.data && (
<>
{getDivider(
'Progress of students of the faculty ',
'BachelorStudentsOfTheFacultyByStartingYear',
toolTips.StudentProgress,
'InfoFacultyProgress'
)}
<div className="toggle-container">
<Toggle
cypress="GraduatedToggle"
toolTips={toolTips.GraduatedToggle}
firstLabel="Graduated included"
secondLabel="Graduated excluded"
value={graduatedGroup}
setValue={setGraduatedGroup}
/>
</div>
<FacultyProgress
faculty={faculty}
progressStats={progressStats}
language={language}
getDivider={getDivider}
/>
</>
)}
{graduationStats.isSuccess && graduationStats.data && (
<>
{getDivider('Average graduation times', 'AverageGraduationTimes', toolTips.AverageGraduationTimes)}
<div className="toggle-container">
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Breakdown"
secondLabel="Median times"
value={showMedian}
setValue={setShowMedian}
/>
</div>
<FacultyGraduations
faculty={faculty}
graduationStats={graduationStats}
groupByStartYear={false}
showMedian={showMedian}
/>
</>
)}
</div>
)}
</div>
</>
)
}
export default FacultyView
@@ -0,0 +1,194 @@
import React, { useState } from 'react'
import { Divider, Header, Loader, Message, Segment } from 'semantic-ui-react'
import { useGetEvaluationStatsQuery } from 'redux/studyProgramme'
import { getTextIn } from '../../common'
import useLanguage from '../LanguagePicker/useLanguage'
import Toggle from '../StudyProgramme/Toggle'
import InfoBox from '../Info/InfoBox'
import MedianTimeBarChart from '../StudyProgramme/MedianTimeBarChart'
import BreakdownBarChart from '../StudyProgramme/BreakdownBarChart'
import StackedBarChart from '../StudyProgramme/BasicOverview/StackedBarChart'
import BarChart from '../StudyProgramme/StudytrackOverview/BarChart'
import BasicDataTable from '../StudyProgramme/StudytrackOverview/BasicDataTable'
import DataTable from '../StudyProgramme/BasicOverview/DataTable'
import InfotoolTips from '../../common/InfoToolTips'
import '../StudyProgramme/studyprogramme.css'
const ProgrammeView = ({ studyprogramme }) => {
const { language } = useLanguage()
const [showMedian, setShowMedian] = useState(false)
const [academicYear, setAcademicYear] = useState(false)
const [graduated, setGraduated] = useState(false)
const toolTips = InfotoolTips.Studyprogramme
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
const grad = graduated ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
const statistics = useGetEvaluationStatsQuery({
id: studyprogramme,
yearType,
specialGroups: 'SPECIAL_EXCLUDED',
graduated: grad,
})
const programmeName = statistics?.data?.programmeName && getTextIn(statistics?.data?.programmeName, language)
const graduationData = statistics?.data?.graduations
const progressData = statistics?.data?.progress
const doCombo = graduationData?.doCombo
const timesData = graduationData?.graduationTimes
const getDivider = (title, toolTipText) => (
<>
<div className="divider">
<Divider data-cy={`Section-${toolTipText}`} horizontal>
{title}
</Divider>
</div>
<InfoBox content={toolTips[toolTipText]} />
</>
)
const displayMedian = () => (
<>
{doCombo && (
<MedianTimeBarChart
data={graduationData?.comboTimes?.medians}
goal={graduationData?.comboTimes?.goal}
title="Bachelor + Master studyright"
byStartYear={false}
/>
)}
<MedianTimeBarChart
data={graduationData?.graduationTimes?.medians}
goal={graduationData?.graduationTimes?.goal}
title={doCombo ? 'Master studyright' : ' '}
byStartYear={false}
/>
</>
)
const displayBreakdown = () => (
<>
{doCombo && <BreakdownBarChart data={graduationData?.comboTimes?.medians} title="Bachelor + Master studyright" />}
<BreakdownBarChart data={timesData?.medians} title={doCombo ? 'Master studyright' : ' '} />
</>
)
const isFetchingOrLoading = statistics.isLoading || statistics.isFetching
const isError = statistics.isError || (statistics.isSuccess && !statistics.data)
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
return (
<>
<div align="center" style={{ padding: '30px' }}>
<Header textAlign="center">{programmeName}</Header>
<span>{studyprogramme}</span>
</div>
<div className="studyprogramme-overview">
{isFetchingOrLoading ? (
<Loader active style={{ marginTop: '10em' }} />
) : (
<Segment className="contentSegment">
<Message info>
<Message.Header>This view is an abridged version of Oodikone's Studyprogramme Overview</Message.Header>
<p>
In these statistics, all special studyrights have been excluded, eg. exchange students and non-degree
students.
</p>
<p>
<b>
You can find more statistics on this and other studyprogrammes of the university in main
Studyprogramme Overview.{' '}
</b>
The full view includes details such as: how many have graduated or started in each programme and in its
studytracks; credits produced by the programme; population backgrounds and enrollment statuses.
</p>
<p>
Access the full Studyprogramme Overview by clicking 'Studyprogramme' in the top navigation bar and then
'Overview'. Alternatively, you can select 'Search by class' to explore studyprogrammes by starting class
with interactive statistics and visualizations.
</p>
<p>
{' '}
Note that both views have access restrictions. If you can't access either view and feel you should have
access, please contact oodikone@helsinki.fi.
</p>
</Message>
<div>
{getDivider(`Progress of students of the studyprogramme by starting year`, 'StudytrackProgress')}
<Toggle
cypress="GraduatedToggle"
toolTips={toolTips.GraduatedToggle}
firstLabel="Graduated included"
secondLabel="Graduated excluded"
value={graduated}
setValue={setGraduated}
/>
<div className="section-container">
<BarChart cypress="StudytrackProgress" data={progressData?.graphData} track={studyprogramme} />
<BasicDataTable
cypress="StudytrackProgress"
data={progressData?.creditTableStats}
track={studyprogramme}
titles={progressData?.creditTableTitles}
/>
</div>
{graduationData?.programmesBeforeOrAfterGraphStats && (
<>
{getDivider('Graduation times', 'AverageGraduationTimes')}
<div className="toggle-container">
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Breakdown"
secondLabel="Median time"
value={showMedian}
setValue={setShowMedian}
/>
<div className="toggle-container">
<Toggle
cypress="YearToggle"
toolTips={toolTips.YearToggle}
firstLabel="Calendar year"
secondLabel="Academic year"
value={academicYear}
setValue={setAcademicYear}
/>
</div>
</div>
<div className={`section-container${doCombo ? '' : '-centered'}`}>
{showMedian ? displayMedian() : displayBreakdown()}
</div>
{getDivider(
studyprogramme.includes('KH')
? 'Primary master programme studies after this programme'
: 'Primary bachelor programme studies before this programme',
'ProgrammesBeforeOrAfter'
)}
<div className="section-container">
<StackedBarChart
cypress="ProgrammesBeforeOrAfter"
wideTable
data={graduationData?.programmesBeforeOrAfterGraphStats}
labels={graduationData?.years}
/>
<DataTable
wideTable
data={graduationData?.programmesBeforeOrAfterTableStats}
titles={graduationData?.programmesBeforeOrAfterTitles}
/>
</div>
</>
)}
</div>
</Segment>
)}
</div>
</>
)
}
export default ProgrammeView

Some files were not shown because too many files have changed in this diff Show More