Compare commits

...
306 Commits
Author SHA1 Message Date
Roni LindholmandGitHub 242c361b01 Merge pull request #2563 from UniversityOfHelsinkiCS/trunk
Update Sentry conf
2021-02-01 13:51:26 +02:00
Roni LindholmandGitHub 56e39586e5 Merge branch 'master' into trunk 2021-02-01 12:58:19 +02:00
Roni Lindholm 7a3bda2860 Update Sentry conf 2021-02-01 12:55:49 +02:00
Joonas HäkkinenandGitHub 8697a92218 Merge pull request #2559 from UniversityOfHelsinkiCS/trunk
Debendabot
2021-01-28 00:02:31 +02:00
Joonas HäkkinenandGitHub 1223cf779b Merge branch 'master' into trunk 2021-01-27 23:47:04 +02:00
Joonas HäkkinenandGitHub ee439eb18d Merge pull request #2544 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/trunk/cypress-6.3.0
Bump cypress from 6.2.1 to 6.3.0
2021-01-27 23:45:22 +02:00
Joonas HäkkinenandGitHub 0089d53d7c Merge pull request #2558 from UniversityOfHelsinkiCS/trunk
Is SIS going to happen?
2021-01-27 23:44:59 +02:00
Joonas Häkkinen 90dde03da0 Try out special SIS destruction style 2021-01-27 23:04:05 +02:00
Joonas Häkkinen 47e70e08a3 Fix registration type to enrolment type mapping 2021-01-27 22:25:18 +02:00
Joonas Häkkinen fd7f27b8c8 Finish up UI for updating SIS students by student number 2021-01-27 21:30:01 +02:00
Joonas Häkkinen 5ec38bb459 Fix updater aborting 2021-01-27 21:19:10 +02:00
Joonas Häkkinen 3e4ce66514 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-01-27 19:11:50 +02:00
Joonas Häkkinen 752ea11eb0 Remove fake news 2021-01-27 18:15:24 +02:00
KalleandGitHub 86d1810ffa ageStats default to grouped 2021-01-27 12:06:22 +02:00
KalleandGitHub e2613bc407 Merge pull request #2549 from UniversityOfHelsinkiCS/trunk
age distribution stats for population
2021-01-26 20:52:45 +02:00
Kalle 866766fe09 lint fixes 2021-01-26 20:17:21 +02:00
Kalle f39da01f70 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone into trunk 2021-01-26 20:04:42 +02:00
Darake 22a91ffd69 feat(sis-updater): add endpoint for scheduling courses by code 2021-01-26 19:41:43 +02:00
Darake f257c207df fix(sis-updater): map max_attainment_date properly for course 2021-01-26 19:40:12 +02:00
Kalle e05d36d9b2 closes #2515 age distribution panel for /populations 2021-01-26 19:33:03 +02:00
Darake a90438c736 chore(sis-updater): upgrade sis-importer-db postgres image to 13.1 2021-01-26 17:30:57 +02:00
Kalle fec085e3e4 closes #2538 rename column 2021-01-26 12:49:00 +02:00
dependabot-preview[bot]andGitHub 989926c2ae Bump cypress from 6.2.1 to 6.3.0
Bumps [cypress](https://github.com/cypress-io/cypress) from 6.2.1 to 6.3.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/.releaserc.base.js)
- [Commits](https://github.com/cypress-io/cypress/compare/v6.2.1...v6.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-01-25 03:43:23 +00:00
Matti LuukkainenandGitHub 65978eb3df Merge pull request #2542 from UniversityOfHelsinkiCS/trunk
ignore enrollments without a date
2021-01-22 00:24:36 +02:00
Matti LuukkainenandGitHub 1014916037 Merge branch 'master' into trunk 2021-01-22 00:02:42 +02:00
Matti Luukkainen e0bac55254 ignore enrollments without a date 2021-01-21 23:55:08 +02:00
Matti LuukkainenandGitHub ccf7c7c9d2 Merge pull request #2537 from UniversityOfHelsinkiCS/trunk
Kludge to exclude noshow students from SIS populations
2021-01-21 21:25:23 +02:00
Matti Luukkainen 24c26f5d10 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-01-21 19:08:40 +02:00
Matti Luukkainen 0edf3abe27 remove cacelled students from populations 2021-01-21 19:07:52 +02:00
KalleandGitHub 6429f294be Merge pull request #2536 from UniversityOfHelsinkiCS/trunk
start of studyright actual column to populations test
2021-01-21 15:55:50 +02:00
Kalle e33175df95 start of studyright actual column to populations test 2021-01-21 15:33:24 +02:00
Joonas HäkkinenandGitHub 0a692c25f5 Merge pull request #2526 from UniversityOfHelsinkiCS/trunk
dependabot :mayhem:
2021-01-20 15:26:01 +02:00
Joonas HäkkinenandGitHub 31443d7b07 Merge branch 'master' into trunk 2021-01-20 14:48:35 +02:00
Joonas HäkkinenandGitHub f89bc1ed71 Merge pull request #2525 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/oodikone2-frontend/axios-0.21.1
Bump axios from 0.18.1 to 0.21.1 in /services/oodikone2-frontend
2021-01-20 14:39:08 +02:00
dependabot[bot]andGitHub 1d36b52c76 Bump axios from 0.18.1 to 0.21.1 in /services/oodikone2-frontend
Bumps [axios](https://github.com/axios/axios) from 0.18.1 to 0.21.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.18.1...v0.21.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-20 12:16:07 +00:00
Joonas HäkkinenandGitHub 08ca693b50 Merge pull request #2524 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/backend/oodikone2-backend/axios-0.21.1
Bump axios from 0.19.0 to 0.21.1 in /services/backend/oodikone2-backend
2021-01-20 14:14:58 +02:00
Joonas HäkkinenandGitHub f800082fb0 Merge pull request #2516 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/trunk/husky-4.3.8
Bump husky from 4.3.0 to 4.3.8
2021-01-20 13:52:04 +02:00
dependabot-preview[bot]andGitHub d8e81ba6d3 Bump husky from 4.3.0 to 4.3.8
Bumps [husky](https://github.com/typicode/husky) from 4.3.0 to 4.3.8.
- [Release notes](https://github.com/typicode/husky/releases)
- [Commits](https://github.com/typicode/husky/compare/v4.3.0...v4.3.8)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-01-20 11:09:07 +00:00
Joonas HäkkinenandGitHub f504b594f5 Merge pull request #2497 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/trunk/cypress-6.2.1
Bump cypress from 6.0.1 to 6.2.1
2021-01-20 13:07:12 +02:00
dependabot[bot]andGitHub 7bd6d9d536 Bump axios from 0.19.0 to 0.21.1 in /services/backend/oodikone2-backend
Bumps [axios](https://github.com/axios/axios) from 0.19.0 to 0.21.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.19.0...v0.21.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-20 11:00:55 +00:00
Joonas HäkkinenandGitHub a6b67b68f4 Merge pull request #2496 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/updater_api/axios-0.21.1
Bump axios from 0.19.0 to 0.21.1 in /updater/updater_api
2021-01-20 12:59:53 +02:00
Joonas HäkkinenandGitHub e468a21fcb Merge branch 'master' into dependabot/npm_and_yarn/updater/updater_api/axios-0.21.1 2021-01-20 12:37:31 +02:00
Joonas HäkkinenandGitHub cb38ebcfa0 Merge pull request #2470 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/oodikone2-analytics/ini-1.3.7
Bump ini from 1.3.5 to 1.3.7 in /services/oodikone2-analytics
2021-01-20 12:37:15 +02:00
Joonas HäkkinenandGitHub c5e0a0bd2d Merge branch 'master' into dependabot/npm_and_yarn/services/oodikone2-analytics/ini-1.3.7 2021-01-20 12:04:47 +02:00
Joonas HäkkinenandGitHub 8ec129ad95 Merge pull request #2469 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/sis-updater-worker/ini-1.3.7
Bump ini from 1.3.5 to 1.3.7 in /updater/sis-updater-worker
2021-01-20 12:04:31 +02:00
Joonas HäkkinenandGitHub 973d070216 Merge branch 'master' into dependabot/npm_and_yarn/updater/sis-updater-worker/ini-1.3.7 2021-01-20 11:42:19 +02:00
Joonas HäkkinenandGitHub 5c2cea5331 Merge pull request #2523 from UniversityOfHelsinkiCS/trunk
Trunk
2021-01-20 11:41:42 +02:00
Joonas Häkkinen 115350f061 Add button to copy student numbers 2021-01-20 11:16:02 +02:00
Darake 65429cb6ee fix course statistic crash with sis-data closes #2507 2021-01-19 22:28:43 +02:00
Gert AdamsonandGitHub 25794f18ff Merge pull request #2522 from UniversityOfHelsinkiCS/trunk
sis-updater changes & course statistics FE fix
2021-01-19 16:16:42 +02:00
Darake 97af1900d5 fix Course statistics Faculty statistics crash closes #2519 2021-01-19 15:42:39 +02:00
Darake 8b64d9b31e expand first semester startYear to 1900 in sis data 2021-01-14 10:05:44 +02:00
Joonas Häkkinen ccdbbbea58 Fix stop updater button 2021-01-14 09:58:54 +02:00
KalleandGitHub 2ace3a56b9 Merge pull request #2508 from UniversityOfHelsinkiCS/trunk
Bug fixes of all sorts
2021-01-13 19:34:40 +02:00
Kalle 67493a082a Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone into trunk 2021-01-13 19:13:09 +02:00
Kalle 723310bdf8 fixes #2433 2021-01-13 19:11:48 +02:00
Kalle a7f2ea1d62 fixes #2381 corner case in unifying open uni courses 2021-01-13 18:19:49 +02:00
Darake 05c6515a9e change sis-db dump source 2021-01-13 15:54:41 +02:00
Kalle 26457b3959 closes #2421 show info on filter dropdown option hover 2021-01-13 15:24:06 +02:00
Kalle 1f402625ef closes #2493 don't show meaningless config 2021-01-13 14:07:01 +02:00
dependabot-preview[bot]andGitHub c746718052 Bump cypress from 6.0.1 to 6.2.1
Bumps [cypress](https://github.com/cypress-io/cypress) from 6.0.1 to 6.2.1.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/.releaserc.base.js)
- [Commits](https://github.com/cypress-io/cypress/compare/v6.0.1...v6.2.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-01-11 03:40:52 +00:00
dependabot[bot]andGitHub 0fcb203aa9 Bump axios from 0.19.0 to 0.21.1 in /updater/updater_api
Bumps [axios](https://github.com/axios/axios) from 0.19.0 to 0.21.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.19.0...v0.21.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-06 10:18:33 +00:00
Marko KoskinenandGitHub a9a1b46e3b Merge pull request #2495 from UniversityOfHelsinkiCS/trunk
More analytics for Populations and DegreeCourses
2021-01-05 16:01:21 +02:00
Marko Koskinen 6c3ebd2a34 More analytics for Populations and DegreeCourses 2020-12-30 15:35:39 +02:00
KalleandGitHub e9f0102098 Merge pull request #2492 from UniversityOfHelsinkiCS/trunk
fixes, removerements, improvements
2020-12-30 15:16:55 +02:00
Kalle b91fcfcd56 closes #2490 better label if not graduated (bsch) 2020-12-30 14:56:41 +02:00
Kalle b3f52863cb closes #2474 remove when passed from courses of populations tab 2020-12-30 14:38:09 +02:00
Kalle 4d207269c2 satisfy proptypes with modal 2020-12-30 14:25:37 +02:00
Kalle 3fe7e5caad fixes #2476 (crash when opening students view) 2020-12-30 14:25:11 +02:00
Marko KoskinenandGitHub 7271a22803 Merge pull request #2491 from UniversityOfHelsinkiCS/trunk
Add some analytics to PopulationCourseStats
2020-12-28 17:47:08 +02:00
Marko Koskinen 244f9a147a Add some analytics to PopulationCourseStats 2020-12-28 16:11:40 +02:00
Atte HaarniandGitHub 0024883adc Merge pull request #2486 from UniversityOfHelsinkiCS/trunk
S I S  U P D A T E R
2020-12-18 16:23:03 +02:00
Darake 961b928558 filter out aalto studymodules in sis-updater 2020-12-18 13:01:04 +02:00
atte 058a77b851 Fix errors caused by filtering deleted snapshots 2020-12-18 10:33:19 +02:00
atte 81ae1e0071 Comment programme module code 2020-12-18 10:07:24 +02:00
Darake 11ca6c4979 remove console logs.. 2020-12-17 18:08:22 +02:00
Darake 9115a16d77 remove totalStudyYears from totalMeta in sis-updater 2020-12-17 16:24:33 +02:00
Darake 424725827e fix generated autumn semester enddate in sis-updater 2020-12-17 15:46:43 +02:00
Darake fe48035318 remove semester scheduling & fix semester startYear in sis-updater 2020-12-17 15:18:48 +02:00
Darake 39a35661d1 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-12-17 14:59:38 +02:00
Darake 06297fe97a generate semesters for sis-updater in migration 2020-12-17 14:59:33 +02:00
Joonas Häkkinen aab054ea2d Add SIS updater abort button 2020-12-17 14:33:24 +02:00
atte 26b3649384 sis updater frontend improvements 2020-12-17 13:58:45 +02:00
atte 0be162d813 add notifier script if someone wants to use it 2020-12-17 12:43:28 +02:00
Atte HaarniandGitHub daf22d0e0b Merge pull request #2475 from UniversityOfHelsinkiCS/trunk
SIS updater updates
2020-12-17 10:08:20 +02:00
Atte HaarniandGitHub c6f29f252e Merge branch 'master' into trunk 2020-12-17 09:38:28 +02:00
atte c879b8aa26 Filter document_state DELETED 2020-12-17 09:33:51 +02:00
KalleandGitHub e7a7206f16 Merge pull request #2473 from UniversityOfHelsinkiCS/trunk
Courses of population stuff
2020-12-16 19:18:52 +02:00
Kalle ced2cc419a Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone into trunk 2020-12-16 18:50:15 +02:00
Kalle 1cddd98413 lint complaint fixes 2020-12-16 18:49:38 +02:00
Kalle 372f403d74 closes #2416 courses of population to /coursepopulation 2020-12-16 17:42:57 +02:00
Joonas Häkkinen 1e229a3950 Update README.md 2020-12-16 16:09:33 +02:00
Kalle 1539aa0271 show manage courses shown button also next to students table 2020-12-16 16:02:57 +02:00
Joonas Häkkinen 101d9d7785 Make sis-updater-worker write to correct db 2020-12-16 15:37:29 +02:00
Kalle 985ec28552 closes #2461 filter also applies to courses tab of student list 2020-12-16 15:18:17 +02:00
Joonas Häkkinen b40449cc16 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-12-16 13:53:38 +02:00
Joonas Häkkinen 0985418e09 Use truly local importer-db with updater 2020-12-16 13:53:29 +02:00
Darake c897904fa4 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-12-16 13:00:36 +02:00
Darake 771f673796 improve message aborting a bit 2020-12-16 13:00:03 +02:00
Kalle 6450d1a2cd Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone into trunk 2020-12-16 12:44:07 +02:00
Joonas Häkkinen 47297cacae Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-12-16 12:36:04 +02:00
Joonas Häkkinen 87df42de70 Add importer-db setup to CLI 2020-12-16 12:35:55 +02:00
atte 28b4108e38 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-12-16 12:00:43 +02:00
atte 9f4e0b9e43 Abort system for sis updater 2020-12-16 11:59:56 +02:00
Joonas Häkkinen 2378f1283e Revert "Dco updaer"
This reverts commit 7298d08e25.
2020-12-15 18:21:29 +02:00
atte 7298d08e25 Dco updaer 2020-12-15 17:51:29 +02:00
Kalle a0f8a0b1d8 closes #2460 more obvious modal closing button 2020-12-15 14:44:23 +02:00
Kalle dd46ab9dd1 closes #2459 show & sort code in couse management 2020-12-15 14:14:56 +02:00
atte 910c735154 Add api to sis updater to update by studentnumber 2020-12-11 13:55:32 +02:00
atte 6eb6f1efe5 Create semesters manually 2020-12-11 10:46:25 +02:00
dependabot[bot]andGitHub d331eea5ce Bump ini from 1.3.5 to 1.3.7 in /services/oodikone2-analytics
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-10 20:01:45 +00:00
dependabot[bot]andGitHub 419cb0d831 Bump ini from 1.3.5 to 1.3.7 in /updater/sis-updater-worker
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-10 20:01:31 +00:00
Joonas HäkkinenandGitHub b1a747835f Merge pull request #2467 from UniversityOfHelsinkiCS/trunk
Maintenance
2020-12-10 21:59:34 +02:00
Joonas Häkkinen 19cbeccbf8 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-12-10 21:45:22 +02:00
Joonas Häkkinen f9f1658922 Add SIS real data setup to CLI 2020-12-10 21:45:16 +02:00
Joonas HäkkinenandGitHub 408dcbf0c4 Merge pull request #2466 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/trunk/ini-1.3.7
[Security] Bump ini from 1.3.5 to 1.3.7
2020-12-10 21:33:43 +02:00
Joonas Häkkinen 77e3d6381f Use real SIS data in dev env 2020-12-10 21:16:36 +02:00
dependabot-preview[bot]andGitHub 3ba58ec41a [Security] Bump ini from 1.3.5 to 1.3.7
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7. **This update includes a security fix.**
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-10 17:32:29 +00:00
Joonas Häkkinen 941a16ecd8 Add updater command for safe-keeping 2020-12-10 14:02:47 +02:00
Joonas HäkkinenandGitHub 7d601b29ab Merge pull request #2454 from UniversityOfHelsinkiCS/trunk
Show SIS destruction button in production
2020-12-08 22:29:44 +02:00
Joonas Häkkinen e03d5885be Show SIS destruction button in production even betterly 2020-12-08 22:06:05 +02:00
Joonas Häkkinen 373482497d Show SIS destruction button in production 2020-12-08 21:53:35 +02:00
Joonas HäkkinenandGitHub d4948fbfd3 Merge pull request #2453 from UniversityOfHelsinkiCS/trunk
SIS to prod maybe
2020-12-08 21:32:37 +02:00
Joonas Häkkinen e9917b5bcc Print sis database connection error plz 2020-12-08 20:22:29 +02:00
Joonas HäkkinenandGitHub 641422fbe1 Merge pull request #2449 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/trunk/cypress-6.0.1
Bump cypress from 6.0.0 to 6.0.1
2020-12-08 10:33:37 +02:00
dependabot-preview[bot]andGitHub 957fa4aeef Bump cypress from 6.0.0 to 6.0.1
Bumps [cypress](https://github.com/cypress-io/cypress) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/.releaserc.base.js)
- [Commits](https://github.com/cypress-io/cypress/compare/v6.0.0...v6.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-12-07 03:38:43 +00:00
Gert AdamsonandGitHub 840b131f64 Merge pull request #2448 from UniversityOfHelsinkiCS/trunk
bring back old courses of population & split components
2020-12-05 16:04:16 +02:00
Darake 271c58f9ff bring back old courses of population & split components 2020-12-05 15:21:14 +02:00
Atte HaarniandGitHub ce92d6e32b Merge pull request #2447 from UniversityOfHelsinkiCS/trunk
Quick SIS trends fix
2020-12-04 17:48:21 +02:00
Atte HaarniandGitHub c3989941f5 Merge branch 'master' into trunk 2020-12-04 17:21:51 +02:00
atte f88e291eba Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-12-04 17:20:03 +02:00
atte 10271652b9 SIS trends: fix starting dates 2020-12-04 17:18:57 +02:00
Atte HaarniandGitHub 4f9de878ef Merge pull request #2446 from UniversityOfHelsinkiCS/trunk
SIS cool data science
2020-12-04 16:11:54 +02:00
Atte HaarniandGitHub 9ad1702ee5 Merge branch 'master' into trunk 2020-12-04 15:55:05 +02:00
atte 1b9d290c73 More trends in SIS 2020-12-04 15:49:07 +02:00
atte 7ddd326ee4 SIS trends, 1/4 complete 2020-12-04 13:53:44 +02:00
atte b0b97d39a1 SIS apis: update studyrights 2020-12-04 10:38:49 +02:00
Joonas HäkkinenandGitHub ff8583c8e7 Merge pull request #2429 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/trunk/cypress-6.0.0
Bump cypress from 5.6.0 to 6.0.0
2020-12-03 20:31:50 +02:00
Joonas HäkkinenandGitHub c93d9484a5 Merge pull request #2443 from UniversityOfHelsinkiCS/trunk
Touhutorstai
2020-12-03 20:30:51 +02:00
Joonas Häkkinen b32b78a487 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-12-03 19:48:39 +02:00
Joonas Häkkinen b0d7d05f5b Fix #2441 2020-12-03 19:48:32 +02:00
KalleandGitHub 8e7ea44c91 Merge pull request #2442 from UniversityOfHelsinkiCS/trunk
Degree Courses rename + modal for hiding
2020-12-03 19:24:20 +02:00
Kalle 29b3e714b2 delete test of deleted functionality 2020-12-03 18:59:40 +02:00
Kalle 50f9f15461 closes #2440 2020-12-03 18:44:25 +02:00
Kalle 71f4cdbd9e closes #2426 closes #2439 2020-12-03 18:21:20 +02:00
Darake b836ff6efd fix proptype 2020-12-03 13:06:28 +02:00
Darake 9f1febd502 fix category name in cumulative table 2020-12-03 12:50:57 +02:00
Darake 95f823b1ca remove console log 2020-12-03 11:16:12 +02:00
Darake 38e73911e6 respect language setting in course population closes #2437 2020-12-03 11:13:22 +02:00
Joonas Häkkinen ba26f2095e Refactor 2020-12-02 17:00:32 +02:00
Joonas Häkkinen 32b9ec6347 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-12-02 16:23:36 +02:00
Joonas Häkkinen 925d3e2c38 Fix studyright element types to be ints 2020-12-02 16:23:15 +02:00
KalleandGitHub d5749cf3d0 Merge pull request #2436 from UniversityOfHelsinkiCS/trunk
Mandatory courses improvements
2020-12-01 21:27:11 +02:00
Kalle 2768ccae46 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone into trunk 2020-12-01 18:16:43 +02:00
Kalle c3e46417c1 closes #2434 2020-12-01 17:50:26 +02:00
Kalle b4b40ce750 student table headers link to /students 2020-12-01 16:20:54 +02:00
Kalle 70b76c4c81 fixes #2424 2020-12-01 15:57:33 +02:00
Joonas HäkkinenandGitHub f6ff947457 Merge pull request #2432 from UniversityOfHelsinkiCS/trunk
Smol maintenance fixxxes
2020-12-01 14:50:32 +02:00
Kalle e3759e7cf5 closes #2428 2020-12-01 13:58:52 +02:00
Kalle 52ff00c236 remove unused css 2020-12-01 13:51:57 +02:00
Joonas Häkkinen 16303865e3 Don't just assume the user is running CLI with HY's username 2020-12-01 13:50:29 +02:00
Kalle 499ab3c178 add setup sis data notice to cli welcome.txt 2020-12-01 13:31:57 +02:00
Joonas Häkkinen 295e90b0ab Remove unnecessary code 2020-12-01 11:44:03 +02:00
Joonas Häkkinen 5c84797beb Stop using pinger in development - the alerts are highly annoying 2020-12-01 11:35:36 +02:00
Gert AdamsonandGitHub 279f22d7e9 Merge pull request #2431 from UniversityOfHelsinkiCS/trunk
change old attainments labels closes #2359
2020-12-01 10:06:55 +02:00
Darake 4413a4ddb7 change old attainments labels closes #2359 2020-12-01 09:47:10 +02:00
Gert AdamsonandGitHub eef9aa67ac Merge pull request #2430 from UniversityOfHelsinkiCS/trunk
improve course filter word order closes #2422
2020-12-01 09:30:44 +02:00
Darake e77dadf02a improve course filter word order closes #2422 2020-12-01 09:05:03 +02:00
Joonas HäkkinenandGitHub 89cded5746 Merge pull request #2427 from UniversityOfHelsinkiCS/trunk
Remove admin toggle from collapsible credits
2020-11-30 16:41:40 +02:00
Joonas Häkkinen ff1f9004a7 do the dew 2020-11-30 15:33:47 +02:00
dependabot-preview[bot]andGitHub 73a330dfd5 Bump cypress from 5.6.0 to 6.0.0
Bumps [cypress](https://github.com/cypress-io/cypress) from 5.6.0 to 6.0.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/.releaserc.base.js)
- [Commits](https://github.com/cypress-io/cypress/compare/v5.6.0...v6.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-30 03:39:47 +00:00
Joonas Häkkinen 7e61981b20 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-11-27 16:20:31 +02:00
Joonas Häkkinen 51a5e688aa Remove unnecessary code 2020-11-27 16:20:19 +02:00
atte 7b173fb1c4 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-11-27 15:56:40 +02:00
atte 98e22477c8 Transfers and other small stuff to populations SIS 2020-11-27 15:56:33 +02:00
Joonas Häkkinen 7c75069d31 Plz blacklist me 2020-11-27 15:23:58 +02:00
atte e58cddee75 Refactor option data in old apis 2020-11-27 15:12:52 +02:00
atte 4248c68a17 New population calculation to sis-apis 2020-11-26 14:46:30 +02:00
atte 431530d4c2 Remove admin toggle from collapsible credits 2020-11-26 13:27:58 +02:00
KalleandGitHub 66fb68095c Merge pull request #2423 from UniversityOfHelsinkiCS/trunk
mandatory courses for populations
2020-11-25 12:59:14 +02:00
Kalle f2f78ffbd1 it.skip couple tests temporarily 2020-11-25 12:28:58 +02:00
Kalle 7aa2887fb8 testing tests 2020-11-25 01:33:22 +02:00
Kalle 937dffd7fb fix tests 2020-11-25 00:08:42 +02:00
Kalle eb2ea91668 lint fixes part 2 2020-11-24 20:39:26 +02:00
Kalle 33be83e87c fix lint errors part 1 2020-11-24 20:24:05 +02:00
Kalle 08a94b79a8 closes #2417 2020-11-24 19:45:47 +02:00
Kalle c8ee4b1acb clean up mandatoryToggle related code where it was always false 2020-11-24 19:17:48 +02:00
Kalle a149440340 closes #2403 2020-11-24 19:09:19 +02:00
Kalle b4a185f874 fixes #2404 and tweaks filter style 2020-11-24 18:35:29 +02:00
Kalle 6877a3dc98 closes #2408 2020-11-24 15:50:56 +02:00
Kalle 9a59f3e3b7 closes #2402 2020-11-24 14:11:02 +02:00
atte e1606b7671 Fix courses of population filters 2020-11-24 12:26:04 +02:00
Joonas HäkkinenandGitHub 76fc96ee96 Merge pull request #2419 from UniversityOfHelsinkiCS/trunk
No moar today
2020-11-23 18:03:05 +02:00
Joonas Häkkinen e65c6087af No worreis i just cannot code 2020-11-23 17:09:57 +02:00
Joonas Häkkinen 49c3ac738f Add total credits to UniHow excel 2020-11-23 16:33:17 +02:00
Joonas HäkkinenandGitHub a9d282a49b Merge pull request #2412 from UniversityOfHelsinkiCS/trunk
Unihow data export
2020-11-22 17:00:40 +02:00
Joonas Häkkinen 4b8238c8b2 "Fix" tests 2020-11-22 16:41:03 +02:00
esakemp 309a12f030 Do not select items from dropdowns onBlur 2020-11-22 10:44:35 +02:00
Joonas Häkkinen 184d06bab7 Create new filter for credit dates 2020-11-22 00:40:25 +02:00
Joonas Häkkinen 7b34d60dfa Lol 2020-11-21 21:15:10 +02:00
Joonas Häkkinen 5ddd327235 Exclude transferred credits from UniHow data export 2020-11-21 20:41:41 +02:00
Joonas Häkkinen b7d4a9ec59 Minthify data export button UX :9000: 2020-11-21 18:58:13 +02:00
Joonas Häkkinen 078a71c666 Dropdown thingy for data export btns 2020-11-21 18:44:39 +02:00
Joonas Häkkinen 235f61d797 Remove unused code 2020-11-21 18:09:28 +02:00
Joonas Häkkinen a2c79af784 Add credittypecode to /v3/populationstatisticsbystudentnumber 2020-11-21 17:54:34 +02:00
Joonas Häkkinen 3cf140da8d Refactor excel mayhem into its own component 2020-11-21 11:23:39 +02:00
Joonas Häkkinen 514ce43d96 Add weighted grade mean to data export 2020-11-20 23:56:55 +02:00
Joonas Häkkinen aff7d1bdbd Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-11-20 23:02:04 +02:00
Joonas Häkkinen b40a279ea9 Add unihow data export thingy WIP 2020-11-20 23:01:57 +02:00
Atte HaarniandGitHub d1fcd20fff Merge pull request #2409 from UniversityOfHelsinkiCS/trunk
Explanation to bachelor of honors
2020-11-20 16:41:50 +02:00
atte da8a6b9e3e Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-11-20 16:23:58 +02:00
KalleandGitHub 40910628af Merge pull request #2407 from UniversityOfHelsinkiCS/trunk
Mandatory courses improvements
2020-11-20 16:08:40 +02:00
atte 73ea4c6835 Add reason text to bachelor honors rejection 2020-11-20 16:02:40 +02:00
Kalle ee7f5c8b0e Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone into trunk 2020-11-20 15:44:33 +02:00
Joonas Häkkinen ff53eab82b Fixeroo 2020-11-20 15:39:50 +02:00
Kalle 0560a25d6b Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone into trunk 2020-11-20 15:38:19 +02:00
Joonas Häkkinen 68de6bd0c4 Fix locally missing npm packages 2020-11-20 14:58:35 +02:00
atte 1f529a5115 Fixed broken require 2020-11-20 14:52:49 +02:00
atte 23a3183355 Rename studytrack -> studyprogramme in sis apis 2020-11-20 14:45:15 +02:00
atte cfee92b67b Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-11-20 13:24:47 +02:00
atte 8e0e606fe1 SIS apis: optiondata to programme overview 2020-11-20 13:13:53 +02:00
Kalle 98e94fd403 fixes #2392 2020-11-19 17:33:02 +02:00
Kalle 0f87541a5f closes #2391 2020-11-19 16:00:14 +02:00
Atte HaarniandGitHub 1f00aea669 Merge pull request #2405 from UniversityOfHelsinkiCS/trunk
Sis api updates
2020-11-19 15:52:16 +02:00
Atte HaarniandGitHub 065a845e88 Merge branch 'master' into trunk 2020-11-19 14:11:01 +02:00
atte 3bad9def19 Disable teacher leaderboard text until #2340 is fixed 2020-11-19 14:08:35 +02:00
atte e53b50a19a Make credit stats indent hack unselectable 2020-11-19 13:40:22 +02:00
atte ff040b959a Refactor study options for sis 2020-11-19 13:37:24 +02:00
atte 22a3c3b74f Add option data to SIS population statistic 2020-11-19 12:32:32 +02:00
atte f4d785c95c Fix sis populate script 2020-11-19 11:13:04 +02:00
Joonas Häkkinen a00c5365c8 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-11-18 14:29:08 +02:00
Joonas Häkkinen 6d00be4c32 Refactor custom population 2020-11-18 14:29:05 +02:00
Joonas HäkkinenandGitHub bdaca3dbee Merge pull request #2401 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/sis-updater-worker/dot-prop-4.2.1
Bump dot-prop from 4.2.0 to 4.2.1 in /updater/sis-updater-worker
2020-11-18 11:29:17 +02:00
Joonas HäkkinenandGitHub 249ef9540a Merge branch 'master' into dependabot/npm_and_yarn/updater/sis-updater-worker/dot-prop-4.2.1 2020-11-18 11:02:34 +02:00
Joonas HäkkinenandGitHub d8a8e736ab Merge pull request #2400 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/sis-updater-worker/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19 in /updater/sis-updater-worker
2020-11-18 10:22:11 +02:00
Joonas HäkkinenandGitHub 3ce59c93a6 Merge pull request #2396 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/trunk/cypress-5.6.0
Bump cypress from 5.2.0 to 5.6.0
2020-11-18 10:21:00 +02:00
dependabot[bot]andGitHub b1a16292b6 Bump dot-prop from 4.2.0 to 4.2.1 in /updater/sis-updater-worker
Bumps [dot-prop](https://github.com/sindresorhus/dot-prop) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/sindresorhus/dot-prop/releases)
- [Commits](https://github.com/sindresorhus/dot-prop/compare/v4.2.0...v4.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-17 14:53:09 +00:00
dependabot[bot]andGitHub ae6a181fb2 Bump lodash from 4.17.15 to 4.17.19 in /updater/sis-updater-worker
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-17 14:53:08 +00:00
Joonas HäkkinenandGitHub d78d347f6e Merge pull request #2399 from UniversityOfHelsinkiCS/trunk
I don't always fix CI, but when I do, I use duct tape.
2020-11-17 16:52:24 +02:00
dependabot-preview[bot]andGitHub 112955f0d5 Bump cypress from 5.2.0 to 5.6.0
Bumps [cypress](https://github.com/cypress-io/cypress) from 5.2.0 to 5.6.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/.releaserc.base.js)
- [Commits](https://github.com/cypress-io/cypress/compare/v5.2.0...v5.6.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-17 14:27:38 +00:00
Joonas Häkkinen 98269c9685 Merge branch 'mayhem' into trunk 2020-11-17 16:24:44 +02:00
Joonas Häkkinen 247ae461c9 Install dev deps before userservice tests 2020-11-17 15:37:31 +02:00
Joonas Häkkinen 8d9525a506 Maybe devDependencies are missing? 2020-11-17 15:22:06 +02:00
Joonas Häkkinen 6cf2eb17e4 Run npm ci before tests 2020-11-17 15:07:14 +02:00
Joonas Häkkinen d3ed995146 Run lint with npx 2020-11-17 14:50:53 +02:00
Joonas Häkkinen 5780b35a6f Fix path 2020-11-17 14:30:05 +02:00
Joonas Häkkinen e5eb6bcf6a Maybe baby? 2020-11-17 14:21:35 +02:00
Jami Kousa a7f1697b1c change node_modules convention 2020-11-01 01:32:11 +02:00
Jami Kousa d2bf262a7c separate updater from oodikone 2020-11-01 01:26:22 +02:00
Jami Kousa ead22e5a9b delete old code 2020-10-31 20:51:54 +02:00
atte 34ad10d38d Refactor and improve credit drilldown 2020-10-28 21:54:32 +02:00
Roni LindholmandGitHub 80bac6bbd0 Merge pull request #2385 from UniversityOfHelsinkiCS/trunk
Hotfix HT and TT grades for Kielikeskusjory
2020-10-26 15:14:47 +02:00
atte 85dabdb863 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-10-26 12:03:17 +02:00
atte 06dac6d76d Fix undefined years in course population 2020-10-26 12:02:50 +02:00
Roni Lindholm 9d9671363c Fix props validation 2020-10-26 11:53:04 +02:00
Roni Lindholm 50913a4ff4 Fix course stats grade charts and tables not handling HT or TT grades 2020-10-26 11:52:57 +02:00
atte e4a54bf001 Add back 0 credits and fix collapse for 1-x 2020-10-26 10:44:24 +02:00
atte 241dc99810 Push demo of credit drill behind admin toggle 2020-10-26 10:38:54 +02:00
Roni LindholmandGitHub 20eff65c2a Merge pull request #2379 from UniversityOfHelsinkiCS/trunk
Implement features for Kielikeskus
2020-10-22 12:19:34 +03:00
Roni Lindholm ccbde1faf0 In course statistics display also empty tables on faculty stats
This is more clear for user than hiding courses with no data and make
user wonder where those courses are.
2020-10-22 11:52:53 +03:00
Roni Lindholm fc9abdf3c0 Add info to course stats about year filter in faculty stats
Unify also table column widths in faculty stats.
2020-10-21 13:25:47 +03:00
Roni Lindholm baabada58b Add course stats to navbar if user has course stats access group 2020-10-20 17:00:07 +03:00
Roni Lindholm bdc8a405b4 Fix course stats permissions to allow explicitly course stats group 2020-10-20 16:58:42 +03:00
Roni Lindholm 69e96553b1 Add access group for course statistics 2020-10-20 16:58:12 +03:00
Roni Lindholm 208ffacd03 Fix css lint 2020-10-20 12:55:07 +03:00
Roni Lindholm 35382beed5 Add select all button to course stats search results form
Add also icon to indicate that selected courses can be removed.
2020-10-20 11:43:15 +03:00
Atte HaarniandGitHub cea2837a2a Merge pull request #2370 from UniversityOfHelsinkiCS/trunk
Small fixes to teacher statistics and population credit view
2020-10-16 13:35:12 +03:00
atte 98b038df82 Change credit statistics step, fix credit statistics bug 2020-10-16 11:38:45 +03:00
atte a8f6604ec3 Default year and category set in teacher leaderboards 2020-10-16 11:20:02 +03:00
atte bfe1f822d5 always ping origin with shibboleth pinger 2020-10-15 10:48:53 +03:00
Roni LindholmandGitHub cfbcdc07e0 Merge pull request #2365 from UniversityOfHelsinkiCS/trunk
Friday night madness
2020-10-14 15:26:10 +03:00
Roni Lindholm f179686031 Hide year filter from yearly productivity table if not enough data
Fix also some prop type validation errors.
2020-10-14 14:57:29 +03:00
Roni Lindholm 3c573a5c94 Update course code input placeholder in tests
Old placeholder was too long for narrow input field.
2020-10-14 09:11:13 +03:00
Roni Lindholm 7ad00ed75f Update course code input placeholder in tests
Old placeholder was too long for narrow input field.
2020-10-13 20:35:21 +03:00
Roni Lindholm 1a7649dd93 Adjust course name and code input widths on course stats search form 2020-10-13 19:46:18 +03:00
atte 11e158b846 Improved .only grep 2020-10-13 18:04:53 +03:00
atte 1469386f04 Add .only check script 2020-10-13 17:43:49 +03:00
Joonas Häkkinen fe306727d6 Remove .only :lul: thanks :esa: 2020-10-13 17:37:39 +03:00
Roni Lindholm cc21f7130e One does not simply pass css lint 2020-10-09 16:48:44 +03:00
Roni Lindholm b7ca45fd29 Add an easy way to clear filters on population's courses table 2020-10-09 16:17:52 +03:00
Roni Lindholm c1283c78a0 Remove admin toggle from options table in study programme overview
Fix also options not clearing between study programmes.
2020-10-09 15:07:36 +03:00
Roni Lindholm 6326f77fd8 On semester select do not render years in future
(Hackish) solution to hide options where year is greater than current
year. This does not hide invalid selection for given programme.
2020-10-09 14:22:15 +03:00
Atte HaarniandGitHub 3bd10541a8 Merge pull request #2364 from UniversityOfHelsinkiCS/trunk
Fix course stats semester view showing nothing
2020-10-09 12:39:21 +03:00
atte 4a59f92607 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-10-09 12:20:33 +03:00
atte d8d602436d Fix bug caused by previous fix 2020-10-09 12:08:28 +03:00
Atte HaarniandGitHub 25b4ba7fe9 Merge branch 'master' into trunk 2020-10-09 11:18:41 +03:00
atte 6613c7c5c1 Fix course stats semester view showing nothing 2020-10-09 11:10:01 +03:00
Joonas HäkkinenandGitHub 5f935f8b52 Merge pull request #2363 from UniversityOfHelsinkiCS/trunk
Fixxxxxes
2020-10-09 10:39:18 +03:00
atte 19ded86384 Use getTextIn insted of object access 2020-10-08 14:03:44 +03:00
atte b8208d3da8 Fix user access right page crash 2020-10-08 13:39:46 +03:00
Atte HaarniandGitHub 2ddc1ec97c Merge pull request #2361 from UniversityOfHelsinkiCS/trunk
Small fixes and QoL
2020-10-08 12:54:51 +03:00
atte 936f3791e0 add unfuck shibboleth session 2020-10-08 12:24:10 +03:00
atte 8555acbd6e Updater view gives more info 2020-10-08 12:21:55 +03:00
atte 958d2c5d78 Fixes course search not working if results are the same length 2020-10-08 10:09:08 +03:00
atte f1954ef79e Dockerignore useless large stuff 2020-10-08 09:27:16 +03:00
Joonas HäkkinenandGitHub 99aaf2ed21 Merge pull request #2354 from UniversityOfHelsinkiCS/trunk
Population statistics UX improvement :tunkki:
2020-10-02 10:20:22 +03:00
Joonas Häkkinen d282825f24 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2020-10-01 20:20:42 +03:00
Joonas Häkkinen f988f18490 Fix tests 2020-10-01 20:20:26 +03:00
Roni Lindholm 2469154960 Add links to course stats on teacher's course listing
Hack also course stats page to work when user goes back and forth on
teacher course listing and course stats pages, without a page reload.
2020-10-01 19:25:00 +03:00
Joonas Häkkinen 005cc935fd Fix infobox getting squashed vertically in some cases 2020-10-01 19:05:07 +03:00
Joonas Häkkinen 935b6a6e4f Remove remaining accordion title tooltips 2020-10-01 18:59:50 +03:00
Joonas Häkkinen 0138c6dab2 Disable buggy auto-scroll 2020-10-01 18:36:19 +03:00
Joonas Häkkinen b922a13404 Remove accordion tooltips 2020-10-01 18:20:27 +03:00
Joonas HäkkinenandGitHub d048da7645 Merge pull request #2353 from UniversityOfHelsinkiCS/trunk
Make frontend ping great again
2020-10-01 13:31:48 +03:00
Joonas Häkkinen 61c7178037 Remove unnecessary code 2020-10-01 12:59:18 +03:00
Joonas Häkkinen 164e4db9a2 Add interval to ping backend as keep-alive trick 2020-10-01 12:58:18 +03:00
327 changed files with 6242 additions and 9313 deletions
+2 -2
View File
@@ -94,11 +94,11 @@ jobs:
- deploy:
command: |
if [ "${DEPLOY}" == "true" ]; then
docker exec --env SENTRY_URL --env SENTRY_AUTH_TOKEN --env SENTRY_ORG=sentry --env SENTRY_PROJECT=oodifront frontend \
docker exec --env SENTRY_URL --env SENTRY_AUTH_TOKEN --env SENTRY_ORG=sentry --env SENTRY_PROJECT=oodikone frontend \
sh +e -c 'npm run sentry-cli -- releases info "$(cat /SENTRY_RELEASE_VERSION)" && \
npm run sentry-cli -- releases delete "$(cat /SENTRY_RELEASE_VERSION)" || true'
docker exec --env SENTRY_URL --env SENTRY_AUTH_TOKEN --env SENTRY_ORG=sentry --env SENTRY_PROJECT=oodifront frontend \
docker exec --env SENTRY_URL --env SENTRY_AUTH_TOKEN --env SENTRY_ORG=sentry --env SENTRY_PROJECT=oodikone frontend \
sh -c 'npm run sentry-cli -- releases new --finalize "$(cat /SENTRY_RELEASE_VERSION)" && \
npm run sentry-cli -- releases files "$(cat /SENTRY_RELEASE_VERSION)" upload-sourcemaps --rewrite dist'
fi
+1
View File
@@ -1,3 +1,4 @@
**/node_modules
data
pgdata
pgdata_kone
+23 -3
View File
@@ -19,7 +19,7 @@ To run Oodikone locally, you will need the following:
Launch the CLI and follow the instructions:
```bash
./oodikone.sh
npm run cli
```
*Please use a terminal at least 80 characters wide, the CLI is a bit rudimentary* 😊
@@ -57,8 +57,6 @@ The development environment is entirely configured in the docker-compose.yml fil
The mapping of ports and environment variables are also defined in the docker-compose file. You can start, stop and manage the development environment by running the following commands from a terminal shell in this directory.
## Usage
### Start the development environment
With anonymized data:
@@ -175,6 +173,28 @@ npm run cypress:run
npm run cypress:open
```
## How to Updater
Before running updater you must run `importer-db` setup via the CLI (see [Installation](#Installation)).
If you want to use the control panel UI, first start Oodikone in real data mode:
```bash
npm run docker:up:real
```
Followed by acual updater:
```bash
npm run updater:dev:up
```
When you are done, close hanging containers with:
```bash
npm run updater:dev:down
```
## Other commands
### How to `scp` backup files from oodikone via melkki-proxy
+1 -1
View File
@@ -10,7 +10,7 @@ describe('Course population tests', () => {
it('Can find course population', () => {
cy.url().should('include', '/coursestatistics')
cy.contains("Search for courses")
cy.get("input[placeholder='Search by entering a course code']").type('TKT20003')
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
cy.contains("tr", "TKT20003").click()
cy.contains("Fetch statistics").should('be.enabled').click()
+22 -8
View File
@@ -10,7 +10,7 @@ describe('Course Statistics tests', () => {
it('Searching single course having duplicate mappings shows course statistics', () => {
cy.url().should('include', '/coursestatistics')
cy.contains("Search for courses")
cy.get("input[placeholder='Search by entering a course code']").type('TKT20003')
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
cy.contains("tr", "TKT20003").click()
cy.contains("Fetch statistics").should('be.enabled').click()
cy.contains("Search for courses").should('not.exist')
@@ -40,7 +40,7 @@ describe('Course Statistics tests', () => {
it('Searching multiple courses having duplicate mappings shows course statistics', () => {
cy.url().should('include', '/coursestatistics')
cy.contains("Search for courses")
cy.get("input[placeholder='Search by entering a course code']").type('TKT')
cy.get("input[placeholder='Search by a course code']").type('TKT')
cy.contains("td", "TKT20003").click()
cy.contains("td", /^TKT10002/).click()
cy.contains("Fetch statistics").should('be.enabled').click()
@@ -62,10 +62,10 @@ describe('Course Statistics tests', () => {
cy.contains("Search for courses")
})
it('Consecutive searches should not crash', () => {
it('On consecutive searches should not crash and search should work', () => {
cy.url().should('include', '/coursestatistics')
cy.contains("Search for courses")
cy.get("input[placeholder='Search by entering a course code']").type('TKT20003')
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
cy.contains("td", "TKT20003").click()
cy.contains("Fetch statistics").should('be.enabled').click()
cy.contains("Show population")
@@ -74,11 +74,25 @@ describe('Course Statistics tests', () => {
cy.contains("Course statistics").click()
cy.contains("Search for courses")
//cy.wait(500)
cy.get("input[placeholder='Search by entering a course code']").type('TKT10001')
cy.contains("td", "TKT10001").click()
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
cy.contains("td", "TKT20001").click()
cy.contains("Fetch statistics").should('be.enabled').click()
cy.contains("Show population")
cy.contains("TKT10001")
cy.contains("TKT20001")
})
it('Shows statistics when separating by semester', () => {
const courseCode = 'DATA11002'
cy.url().should('include', '/coursestatistics')
cy.contains("Search for courses")
cy.get("input[placeholder='Search by a course code']").type(courseCode)
cy.contains("td", courseCode).click()
cy.contains("Separate statistics for Spring and Fall semesters").click()
cy.contains("Fetch statistics").should('be.enabled').click()
cy.contains("Kevät 2018")
cy.contains("Syksy 2017")
cy.get("tbody").contains("19")
cy.contains(courseCode)
})
describe('When searching unified course stats', () => {
@@ -86,7 +100,7 @@ describe('Course Statistics tests', () => {
cy.url().should('include', '/coursestatistics')
cy.contains("Search for courses")
cy.get("input[name='unifyOpenUniCourses']").parent().click()
cy.get("input[placeholder='Search by entering a course code']").type('TKT10002')
cy.get("input[placeholder='Search by a course code']").type('TKT10002')
cy.contains("td", "TKT10002, AYTKT10002").click()
cy.contains("Fetch statistics").should('be.enabled').click()
cy.contains("Search for courses").should('not.exist')
+138 -158
View File
@@ -1,187 +1,167 @@
/// <reference types="Cypress" />
const setPopStatsUntil = (until, includeSettings = []) => {
cy.contains("Advanced settings").siblings().get('[data-cy=advanced-toggle]').click()
includeSettings.forEach(setting => {
cy.contains("Advanced settings").parent().siblings().contains(setting).click()
})
cy.get(".adv-stats-until > .form-control").click().clear().type(until)
cy.contains("Fetch population with new settings").click()
cy.contains("Advanced settings")
}
.siblings()
.get("[data-cy=advanced-toggle]")
.click();
includeSettings.forEach((setting) => {
cy.contains("Advanced settings")
.parent()
.siblings()
.contains(setting)
.click();
});
cy.get(".adv-stats-until > .form-control").click().clear().type(until);
cy.contains("Fetch population with new settings").click();
cy.contains("Advanced settings");
};
describe('Population Statistics tests', () => {
describe("Population Statistics tests", () => {
beforeEach(() => {
cy.init()
})
cy.init();
});
const checkAmountOfStudents = (assertion) => {
let students = 0
cy.contains("Credit accumulation").click().invoke('text').then((text) => {
students = Number(text.match(/\d+/g)[0])
expect(students).to.equal(assertion)
})
cy.contains("Credit accumulation").click().siblings().within(() => {
cy.get(".highcharts-series-group").find("path").should('have.length', students ? (students * 2) + 2 : 0) // For each student there should be 2 paths in the graph + 2 for the scrollbar
})
}
let students = 0;
cy.contains("Credit accumulation")
.click()
.invoke("text")
.then((text) => {
students = Number(text.match(/\d+/g)[0]);
expect(students).to.equal(assertion);
});
};
it('Population statistics search form is usable', () => {
cy.cs("navbar-studyProgramme").click()
cy.cs("navbar-class").click()
cy.contains("See population").should('be.disabled')
cy.url().should('include', '/populations')
cy.contains("Search for population")
cy.contains("Class of").parent().within(() => {
cy.get(".form-control").as("enrollmentSelect")
})
it("Population statistics search form is usable", () => {
cy.cs("navbar-studyProgramme").click();
cy.cs("navbar-class").click();
cy.contains("See population").should("be.disabled");
cy.url().should("include", "/populations");
cy.contains("Search for population");
cy.contains("Class of")
.parent()
.within(() => {
cy.get(".form-control").as("enrollmentSelect");
});
cy.get("@enrollmentSelect").its(`${[0]}.value`).then((beforeVal) => {
cy.get("@enrollmentSelect").click()
// go back to 2010-2019
cy.get(".yearSelectInput .rdtPrev").click({ force: true })
cy.get(".yearSelectInput table").contains("2014-2015").click({ force: true })
cy.get("@enrollmentSelect").should('not.have.value', beforeVal)
})
cy.get("@enrollmentSelect")
.its(`${[0]}.value`)
.then((beforeVal) => {
cy.get("@enrollmentSelect").click();
// go back to 2010-2019
cy.get(".yearSelectInput .rdtPrev").click({ force: true });
cy.get(".yearSelectInput table")
.contains("2014-2015")
.click({ force: true });
cy.get("@enrollmentSelect").should("not.have.value", beforeVal);
});
cy.contains("Select study programme").click().siblings().contains("Tietojenkäsittelytieteen koulutusohjelma").click()
cy.contains("Select degree").click().siblings().contains("Luonnontieteiden kandidaatti")
})
cy.contains("Select study programme")
.click()
.siblings()
.contains("Tietojenkäsittelytieteen koulutusohjelma")
.click();
cy.contains("Select degree")
.click()
.siblings()
.contains("Luonnontieteiden kandidaatti");
});
it('Population statistics is usable on general level', () => {
cy.selectStudyProgramme("Tietojenkäsittelytieteen maisteriohjelma")
setPopStatsUntil('September 2019')
// FIXME: re-enable when CI has mandatory courses in the db
it.skip("Population statistics is usable on general level", () => {
cy.selectStudyProgramme("Tietojenkäsittelytieteen maisteriohjelma");
setPopStatsUntil("September 2019");
cy.get(".card").within(() => {
cy.contains("Tietojenkäsittelytieteen maisteriohjelma")
cy.contains("Sample size: 29 students")
cy.contains("Excludes exchange students")
cy.contains("Excludes students who haven't enrolled present nor absent")
})
cy.contains("Courses of population").click({ force: true })
cy.contains("Courses of population").parent().within(() => {
cy.get("tr").its('length').should('be.gte', 10)
cy.route('/api/v3/courseyearlystats**').as('coursePage')
cy.contains("Laskennan mallit")
cy.get(':nth-child(2) > .iconCell > p > .item > .level').click({ force: true })
cy.wait('@coursePage')
cy.url().should('include', '/coursestatistics')
})
cy.contains("TKT20005")
cy.go("back")
cy.contains("Ohjelmoinnin perusteet")
cy.contains("Courses of population").click().parent().within(() => {
cy.contains("Ohjelmoinnin perusteet").siblings().eq(4).should("have.text", "15")
})
cy.contains("Tietojenkäsittelytieteen maisteriohjelma");
cy.contains("Sample size: 29 students");
cy.contains("Excludes exchange students");
cy.contains("Excludes students who haven't enrolled present nor absent");
});
cy.contains("Courses of population").click({ force: true });
checkAmountOfStudents(29)
cy.route("/api/v3/courseyearlystats**").as("coursePage");
cy.wait(150);
cy.cs("expand-CSM10000").click();
cy.cs("coursestats-link-CSM12101").click();
cy.wait("@coursePage");
cy.url().should("include", "/coursestatistics");
cy.contains("CSM12101");
});
let filteredStudents = 1328493
cy.contains("Credit statistics").click()
cy.contains("Credits Gained During First").parentsUntil(".tab").get("table").within(() => {
cy.get("tr").eq(2).find("td").eq(2).invoke("text").then(text => filteredStudents = Number(text))
cy.route('POST', '/api/v2/populationstatistics/courses**').as('courseData')
cy.get("tr").eq(2).find('.filter').click()
cy.wait('@courseData')
}).then(() => {
checkAmountOfStudents(filteredStudents)
})
cy.contains("Courses of population").click({ force: true })
cy.contains("Courses of population").parent().within(() => {
cy.contains("Ohjelmoinnin perusteet").siblings().eq(5).should("have.text", "0")
})
cy.contains("Students (1)").click()
cy.contains("Student names hidden").click()
cy.contains("Luoto").siblings().eq(2).click()
cy.contains("Luoto").invoke('text').then((text) => expect(text).to.equal('Luoto Veli-Matti, 014824094'))
})
it('Student list checking works as intended', () => {
cy.selectStudyProgramme("Tietojenkäsittelytieteen maisteriohjelma")
cy.contains("Students (29)").click()
cy.contains("010111264")
cy.contains("666666666").should('not.exist')
cy.contains('button', "Check studentnumbers").click()
cy.contains('Check for studentnumbers')
cy.get('textarea').type("010111264").type('{enter}').type("666666666")
cy.contains('button', 'check students').click()
cy.contains('#checkstudentsresults', 'Results').within(e => {
cy.contains('Student numbers in list and in oodi').click()
cy.contains('#found', '010111264')
cy.contains('Student numbers in list but not in oodi').click()
cy.contains('#notfound', '666666666')
cy.contains('Student numbers in oodi but not in list').click()
cy.contains('#notsearched', '010533091')
})
})
it('Population statistics wont crash course population', () => {
cy.selectStudyProgramme("Tietojenkäsittelytieteen maisteriohjelma")
cy.contains("Courses of population").click({ force: true })
cy.get(':nth-child(3) > .iconCell > p > .item > .level').click({ force: true })
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click({ force: true })
})
it("Empty 'mandatory courses' tab has a link to the page where mandatory courses can be added", () => {
cy.selectStudyProgramme("Kielten kandiohjelma")
cy.contains("Students (5)").click()
cy.get("[data-cy=student-table-tabs]").contains("Mandatory Courses").click()
cy.contains("No mandatory courses defined. You can define them here.").find("a").click()
cy.contains("Kielten kandiohjelma")
cy.contains("Add courses")
})
it("Student list checking works as intended", () => {
cy.selectStudyProgramme("Tietojenkäsittelytieteen maisteriohjelma");
cy.contains("Students (29)").click();
cy.contains("010111264");
cy.contains("666666666").should("not.exist");
cy.contains("button", "Check studentnumbers").click();
cy.contains("Check for studentnumbers");
cy.get("textarea").type("010111264").type("{enter}").type("666666666");
cy.contains("button", "check students").click();
cy.contains("#checkstudentsresults", "Results").within((e) => {
cy.contains("Student numbers in list and in oodi").click();
cy.contains("#found", "010111264");
cy.contains("Student numbers in list but not in oodi").click();
cy.contains("#notfound", "666666666");
cy.contains("Student numbers in oodi but not in list").click();
cy.contains("#notsearched", "010533091");
});
});
it("Empty 'tags' tab has a link to the page where tags can be created", () => {
cy.selectStudyProgramme("Kielten kandiohjelma")
cy.contains("Students (5)").click()
cy.get("[data-cy=student-table-tabs]").contains("Mandatory Courses").siblings().contains("Tags").click()
cy.contains("No tags defined. You can define them here.").find("a").click()
cy.contains("Kielten kandiohjelma")
cy.contains("Create new tag")
})
it("Advanced settings work", () => {
cy.selectStudyProgramme("Tietojenkäsittelytieteen kandiohjelma")
cy.get('[data-cy=advanced-toggle]').click()
cy.contains('Statistics until')
// only spring
cy.cs('toggle-fall').click()
cy.contains('Fetch population').click()
cy.selectStudyProgramme("Kielten kandiohjelma");
cy.contains("Students (5)").click();
cy.get("[data-cy=student-table-tabs]").contains("Tags").click();
cy.contains("No tags defined. You can define them here.").find("a").click();
cy.contains("Kielten kandiohjelma");
cy.contains("Create new tag");
});
cy.contains('Credit accumulation (for 13 students)')
it("Advanced settings work", () => {
cy.selectStudyProgramme("Tietojenkäsittelytieteen kandiohjelma");
cy.get("[data-cy=advanced-toggle]").click();
cy.contains("Statistics until");
// only spring
cy.cs("toggle-fall").click();
cy.contains("Fetch population").click();
cy.contains("Credit accumulation (for 13 students)");
// only fall
cy.get('[data-cy=advanced-toggle]').click()
cy.cs('toggle-fall').click()
cy.cs('toggle-spring').click()
cy.get("[data-cy=advanced-toggle]").click();
cy.cs("toggle-fall").click();
cy.cs("toggle-spring").click();
cy.contains('Fetch population').click()
cy.contains("Fetch population").click();
// FIXME: This fails randomly both locally and on CI.
cy.contains('Credit accumulation (for 206 students)')
cy.contains("Credit accumulation (for 206 students)");
// spring + fall and include cancelled
cy.get('[data-cy=advanced-toggle]').click()
cy.cs('toggle-spring').click()
cy.cs('toggle-cancelled').click()
cy.contains('Fetch population').click()
cy.get("[data-cy=advanced-toggle]").click();
cy.cs("toggle-spring").click();
cy.cs("toggle-cancelled").click();
cy.contains("Fetch population").click();
cy.contains("Credit accumulation (for 228 students)");
});
cy.contains('Credit accumulation (for 228 students)')
})
it("Credit Statistics, Statistics pane works", () => {
cy.selectStudyProgramme("Tietojenkäsittelytieteen kandiohjelma")
cy.contains("Credit statistics").click()
cy.get("[data-cy='credit-stats-tab'] > .menu > :nth-child(2)").click()
cy.selectStudyProgramme("Tietojenkäsittelytieteen kandiohjelma");
cy.contains("Credit statistics").click();
cy.get("[data-cy='credit-stats-tab'] > .menu > :nth-child(2)").click();
cy.get("[data-cy='credit-stats-table-name-header']").should('contain', 'Statistic for n = 219 Students')
cy.get("[data-cy='credit-stats-mean']").should('contain', '44.36')
cy.get("[data-cy='credit-stats-stdev']").should('contain', '30.77')
cy.get("[data-cy='credit-stats-min']").should('contain', '0')
cy.get("[data-cy='credit-stats-q1']").should('contain', '17')
cy.get("[data-cy='credit-stats-q2']").should('contain', '46')
cy.get("[data-cy='credit-stats-q3']").should('contain', '66')
cy.get("[data-cy='credit-stats-max']").should('contain', '137')
})
})
cy.get("[data-cy='credit-stats-table-name-header']").should(
"contain",
"Statistic for n = 219 Students"
);
cy.get("[data-cy='credit-stats-mean']").should("contain", "44.36");
cy.get("[data-cy='credit-stats-stdev']").should("contain", "30.77");
cy.get("[data-cy='credit-stats-min']").should("contain", "0");
cy.get("[data-cy='credit-stats-q1']").should("contain", "17");
cy.get("[data-cy='credit-stats-q2']").should("contain", "46");
cy.get("[data-cy='credit-stats-q3']").should("contain", "66");
cy.get("[data-cy='credit-stats-max']").should("contain", "137");
});
});
+63 -60
View File
@@ -1,72 +1,75 @@
/// <reference types="Cypress" />
describe('Student Statistics tests', () => {
beforeEach(() => {
cy.init()
cy.contains("Student statistics").click()
cy.contains("Student names hidden")
})
describe("Student Statistics tests", () => {
beforeEach(() => {
cy.init();
cy.contains("Student statistics").click();
cy.contains("Show student names");
});
it('Student statistics search form is usable', () => {
cy.contains('Student names hidden')
cy.url().should('include', '/students')
cy.get('.prompt').type('Oinonen')
cy.contains('Student number')
cy.contains('Started')
cy.contains('Credits')
cy.contains('Heidi Eeva').should('not.exist')
it("Student statistics search form is usable", () => {
cy.contains("Show student names");
cy.url().should("include", "/students");
cy.get(".prompt").type("Oinonen");
cy.contains("Student number");
cy.contains("Started");
cy.contains("Credits");
cy.contains("Heidi Eeva").should("not.exist");
cy.get('label').click()
cy.contains('Heidi Eeva')
cy.get("label").click();
cy.contains("Heidi Eeva");
cy.get('label').click()
cy.contains('Heidi Eeva').should('not.exist')
cy.get("label").click();
cy.contains("Heidi Eeva").should("not.exist");
});
it("Can get student specific page by clicking student", () => {
cy.url().should("include", "/students");
cy.get(".prompt").type("Oinonen");
cy.contains("014473717").click();
cy.contains("Updated at 02.10.2019");
cy.contains("Musiikin didaktiikka (EDUK4212)");
cy.contains("Oinonen").should("not.exist");
})
it('Can get student specific page by clicking student', () => {
cy.url().should('include', '/students')
cy.get('.prompt').type('Oinonen')
cy.contains('014473717').click()
cy.contains('Updated at 02.10.2019')
cy.contains('Musiikin didaktiikka (EDUK4212)')
cy.contains('Oinonen').should('not.exist')
cy.get("label").click();
cy.contains("Oinonen");
cy.get('label').click()
cy.contains('Oinonen')
cy.get("label").click();
cy.contains("Oinonen").should("not.exist");
});
cy.get('label').click()
cy.contains('Oinonen').should('not.exist')
})
it("Can get back to search menu", () => {
cy.get(".prompt").type("Oinonen");
cy.contains("014473717").click();
cy.get(".remove").click();
cy.contains("Student number").should("not.exist");
cy.contains("Credits").should("not.exist");
});
it('Can get back to search menu', () => {
cy.get('.prompt').type('Oinonen')
cy.contains('014473717').click()
cy.get('.remove').click()
cy.contains('Student number').should('not.exist')
cy.contains('Credits').should('not.exist')
})
it("Can jump to course", () => {
cy.get(".prompt").type("Oinonen");
cy.contains("014473717").click();
cy.contains("Musiikin didaktiikka (EDUK4212)")
.siblings()
.within(() => {
cy.get(".level").click();
});
cy.url().should("include", "/coursestatistics");
cy.contains("Musiikin didaktiikka");
});
it('Can jump to course', () => {
cy.get('.prompt').type('Oinonen')
cy.contains('014473717').click()
cy.contains('Musiikin didaktiikka (EDUK4212)').siblings().within(() => { cy.get('.level').click() })
cy.url().should('include', '/coursestatistics')
cy.contains('Musiikin didaktiikka')
})
it("Searching with bad inputs doesnt yield results", () => {
cy.get(".prompt").type("Oin");
cy.contains("Student number").should("not.exist");
it('Searching with bad inputs doesnt yield results', () => {
cy.get('.prompt').type('Oin')
cy.contains('Student number').should('not.exist')
cy.get(".prompt").clear().type("01114");
cy.contains("Student number").should("not.exist");
});
cy.get('.prompt').clear().type('01114')
cy.contains('Student number').should('not.exist')
})
it('Can jump to population page', () => {
cy.get('.prompt').type('Veli-Matti')
cy.contains('014824094').click()
cy.get('i.level.up.alternate.icon').eq(0).click()
cy.contains('Population statistics')
cy.contains('Tietojenkäsittelytieteen maisteriohjelma')
})
})
it("Can jump to population page", () => {
cy.get(".prompt").type("Veli-Matti");
cy.contains("014824094").click();
cy.get("i.level.up.alternate.icon").eq(0).click();
cy.contains("Population statistics");
cy.contains("Tietojenkäsittelytieteen maisteriohjelma");
});
});
+136 -151
View File
@@ -1,176 +1,161 @@
/// <reference types="Cypress" />
const deleteTag = (name) => {
cy.contains(name).siblings().contains('Delete').click()
cy.contains('Are you sure you want to delete tag')
cy.contains('Confirm').click()
cy.contains(name).should('not.exist')
}
cy.contains(name).siblings().contains("Delete").click();
cy.contains("Are you sure you want to delete tag");
cy.contains("Confirm").click();
cy.contains(name).should("not.exist");
};
describe('Studyprogramme overview', () => {
describe("Studyprogramme overview", () => {
beforeEach(() => {
cy.init()
cy.contains("Study programme").click().siblings().contains("Overview").click()
cy.contains("Study Programme", { timeout: 100000 })
})
cy.init();
cy.contains("Study programme")
.click()
.siblings()
.contains("Overview")
.click();
cy.contains("Study Programme", { timeout: 100000 });
});
it('can search for course mappings', () => {
cy.contains("Tietojenkäsittelytieteen kandiohjelma").click()
cy.contains('Code Mapper').click()
cy.contains('tr', 'TKT20003 Käyttöjärjestelmät').within(($tr) => { cy.get('input').type('582219') })
cy.contains('tr', 'TKT20003 Käyttöjärjestelmät').within(($tr) => { cy.get('.results').contains("Käyttöjärjestelmät (582219)") })
cy.contains('tr', 'TKT20003 Käyttöjärjestelmät').within(($tr) => { cy.contains('button', "Add") })
})
// FIXME: re-enable when CI has mandatory courses in the db
it.skip("can search for course mappings", () => {
cy.contains("Tietojenkäsittelytieteen kandiohjelma").click();
cy.contains("Code Mapper").click();
cy.wait(150);
cy.contains("tr", "TKT20003 Käyttöjärjestelmät").within(($tr) => {
cy.get("input").type("582219");
});
cy.contains("tr", "TKT20003 Käyttöjärjestelmät").within(($tr) => {
cy.get(".results").contains("Käyttöjärjestelmät (582219)");
});
cy.contains("tr", "TKT20003 Käyttöjärjestelmät").within(($tr) => {
cy.contains("button", "Add");
});
});
it('can view course groups', () => {
cy.contains("Kasvatustieteiden kandiohjelma").click()
cy.contains('Course Groups').click()
it("can view course groups", () => {
cy.contains("Kasvatustieteiden kandiohjelma").click();
cy.contains("Course Groups").click();
cy.contains('tr', 'Test course group').get('i.edit').click()
cy.contains("Edit group")
cy.get('.prompt').type("Professori Pekka")
cy.contains("Add teacher").parent().contains("000960").click()
cy.contains("Teachers in group").parent().contains("000960")
cy.contains("tr", "Test course group").get("i.edit").click();
cy.contains("Edit group");
cy.get(".prompt").type("Professori Pekka");
cy.contains("Add teacher").parent().contains("000960").click();
cy.contains("Teachers in group").parent().contains("000960");
cy.get("i.reply.link.icon").click()
cy.contains('tr a', 'Test course group').click()
cy.contains("Total teachers")
cy.get("i.reply.icon").click()
})
cy.get("i.reply.link.icon").click();
cy.contains("tr a", "Test course group").click();
cy.contains("Total teachers");
cy.get("i.reply.icon").click();
});
it('renders progress and productivity tables', () => {
cy.contains("Tietojenkäsittelytieteen kandiohjelma").click()
cy.contains("Admin").click()
cy.contains("productivity").click()
cy.contains("throughput").click()
it("renders progress and productivity tables", () => {
cy.contains("Tietojenkäsittelytieteen kandiohjelma").click();
cy.contains("Admin").click();
cy.contains("productivity").click();
cy.contains("throughput").click();
cy.wait(1000)
cy.get('.attached > :nth-child(1)').click()
cy.get('table').should('have.length', 3)
cy.contains('Population progress')
cy.contains('Yearly productivity')
cy.contains("2017-2018").siblings().contains('228').siblings().contains('178')
cy.wait(1000);
cy.get(".attached > :nth-child(1)").click();
cy.get("table").should("have.length", 3);
cy.contains("Population progress");
cy.contains("Yearly productivity");
cy.contains("2017-2018")
.siblings()
.contains("228")
.siblings()
.contains("178");
cy.get('table').eq(1).contains('2018').siblings().contains('3023.00').siblings().contains('2631.00')
cy.get('table').eq(1).contains('2017').siblings().contains('3159.00').siblings().contains("2914.00")
})
cy.get("table")
.eq(1)
.contains("2018")
.siblings()
.contains("3023.00")
.siblings()
.contains("2631.00");
cy.get("table")
.eq(1)
.contains("2017")
.siblings()
.contains("3159.00")
.siblings()
.contains("2914.00");
});
it('can add and delete mandatory courses and labels', () => {
cy.contains("Tietojenkäsittelytieteen kandiohjelma").click()
cy.contains('Mandatory Courses').click()
cy.get('button').contains('Add Courses').click()
cy.route('/api/v2/coursesmulti**').as('searchResponse')
cy.get('input').eq(0).type('Code Generation')
cy.wait('@searchResponse')
cy.contains('Searched courses')
cy.get(':nth-child(1) > .ten').click()
cy.contains('select label')
it("can open Thesis page", () => {
cy.contains("Tietojenkäsittelytieteen kandiohjelma").click();
cy.contains("Thesis Courses").click();
cy.contains("Add thesis course").click();
cy.contains("No results");
});
cy.contains('Group labels').click()
const label = 'cypress test label'
cy.contains(label).should('not.exist')
cy.contains('button', 'Add').should('be.disabled')
cy.get('input').eq(0).type(label)
cy.contains('button', 'Add').should('be.enabled').click()
cy.contains('button', 'Add').should('be.disabled')
cy.contains('table', label)
it("can move to Population statistics page by clickin", () => {
cy.contains("Tietojenkäsittelytieteen maisteriohjelma").click();
cy.get("i.level.up.alternate.icon").eq(0).click();
cy.contains("Credit accumulation (for 29 students)");
});
cy.contains('Mandatory courses').click()
cy.get('table').eq(0).contains('tr', 'Code Generation').contains('select label').click()
cy.contains(label).click()
cy.get('table').eq(0).contains('tr', 'Code Generation').within((el) => {
cy.get('div.dropdown>div').eq(0).within((el) => {
cy.contains('select label').should('not.exist')
cy.contains(label)
})
cy.get('i.clear').click()
cy.get('div.dropdown>div').eq(0).within((el) => {
cy.contains(label).should('not.exist')
cy.contains('select label')
})
})
cy.get('table').eq(0).contains('tr', 'Code Generation').contains('select label').click()
cy.contains(label).click()
cy.get('table').eq(0).contains('tr', 'Code Generation').contains('button', 'Delete').click()
cy.contains('Code Generation').should('not.exist')
it("can create and delete tags for population", () => {
const name = `tag-${new Date().getTime()}`;
cy.contains("Tietojenkäsittelytieteen kandiohjelma").click();
cy.get(".attached > :nth-child(6)").click();
cy.get(":nth-child(1) > .field > .ui > input").type(name);
cy.get(".form-control").type("2018");
cy.contains("Create new tag").click();
cy.contains(name);
cy.contains("2018");
deleteTag(name);
});
cy.contains('Group labels').click()
cy.contains('tr', label).within((el) => {
cy.get('i.remove').click()
})
cy.contains('button', 'Remove').click()
cy.contains(label).should('not.exist')
})
it("can create personal tags", () => {
const name = `tag-${new Date().getTime()}`;
cy.contains("Tietojenkäsittelytieteen kandiohjelma").click();
cy.get(".attached > :nth-child(6)").click();
cy.get(":nth-child(1) > .field > .ui > input").type(name);
cy.get(".form-control").type("2018");
cy.get(".ui > label").click();
cy.contains("Create new tag").click();
cy.get(".purple");
cy.contains(name);
deleteTag(name);
});
it('can open Thesis page', () => {
cy.contains("Tietojenkäsittelytieteen kandiohjelma").click()
cy.contains('Thesis Courses').click()
cy.contains('Add thesis course').click()
cy.contains('No results')
})
it("can add tags to students", () => {
const name = `tag-${new Date().getTime()}`;
it('can move to Population statistics page by clickin', () => {
cy.contains('Tietojenkäsittelytieteen maisteriohjelma').click()
cy.get('i.level.up.alternate.icon').eq(0).click()
cy.contains('Credit accumulation (for 29 students)')
})
cy.contains("Tietojenkäsittelytieteen kandiohjelma").click();
cy.get(".attached > :nth-child(6)").click();
cy.get(":nth-child(1) > .field > .ui > input").type(name);
cy.get(".form-control").type("2018");
cy.contains("Create new tag").click();
cy.contains(name);
it('can create and delete tags for population', () => {
const name = `tag-${new Date().getTime()}`
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached > :nth-child(6)').click()
cy.get(':nth-child(1) > .field > .ui > input').type(name)
cy.get('.form-control').type('2018')
cy.contains('Create new tag').click()
cy.contains(name)
cy.contains('2018')
deleteTag(name)
})
cy.contains("Add tags to students").click();
cy.get(".form > .field > .dropdown")
.click()
.get(".ui > .search")
.type(name)
.click();
it('can create personal tags', () => {
const name = `tag-${new Date().getTime()}`
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached > :nth-child(6)').click()
cy.get(':nth-child(1) > .field > .ui > input').type(name)
cy.get('.form-control').type('2018')
cy.get('.ui > label').click()
cy.contains('Create new tag').click()
cy.get('.purple')
cy.contains(name)
deleteTag(name)
})
cy.get(".form > .field > .dropdown > .visible").contains(name).click();
it('can add tags to students', () => {
const name = `tag-${new Date().getTime()}`
cy.get("textarea").type("010623419");
cy.get(".positive").click();
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached > :nth-child(6)').click()
cy.get(':nth-child(1) > .field > .ui > input').type(name)
cy.get('.form-control').type('2018')
cy.contains('Create new tag').click()
cy.contains(name)
cy.contains("Student statistics").click();
cy.get(".prompt").type("010623419");
cy.contains("10").click();
cy.contains(name);
cy.contains('Add tags to students').click()
cy.get('.form > .field > .dropdown').click().get('.ui > .search').type(name).click()
cy.get('.form > .field > .dropdown > .visible').contains(name).click()
cy.go("back");
cy.go("back");
cy.get('textarea').type('010623419')
cy.get('.positive').click()
deleteTag(name);
cy.contains('Student statistics').click()
cy.get('.prompt').type('010623419')
cy.contains('10').click()
cy.contains(name)
cy.go('back')
cy.go('back')
deleteTag(name)
cy.contains('Student statistics').click()
cy.get('.prompt').type('010623419')
cy.contains('10').click()
cy.contains(name).should('not.exist')
})
})
cy.contains("Student statistics").click();
cy.get(".prompt").type("010623419");
cy.contains("10").click();
cy.contains(name).should("not.exist");
});
});
+14 -13
View File
@@ -46,17 +46,18 @@ describe('Teachers page tests', () => {
cy.contains('Name').should('not.exist')
})
it("Check leaderboad works", () => {
cy.get('.borderless > :nth-child(2)').click({ force: true })
cy.get(':nth-child(1) > .ui > .search').click({ force: true })
cy.contains('2017-18').click({ force: true })
cy.contains("Recalculate this year").click({ force: true })
cy.wait(5000)
cy.reload()
cy.get(':nth-child(1) > .ui > .search').click({ force: true })
cy.contains('2017-18').click()
cy.contains("Passed")
cy.contains("Antero")
cy.contains("Sassmannshausen")
})
// it("Check leaderboad works", () => {
// cy.get('.borderless > :nth-child(2)').click({ force: true })
// cy.get(':nth-child(1) > .ui > .search').click({ force: true })
// cy.get(':nth-child(1) > .ui > .search').click({ force: true })
// cy.contains('2017-18').click({ force: true })
// cy.contains("Recalculate this year").click({ force: true })
// cy.wait(5000)
// cy.reload()
// cy.get(':nth-child(1) > .ui > .search').click({ force: true })
// cy.contains('2017-18').click()
// cy.contains("Passed")
// cy.contains("Antero")
// cy.contains("Sassmannshausen")
// })
})
+75
View File
@@ -0,0 +1,75 @@
version: "3.2"
services:
nats:
image: nats-streaming:0.15.1
command: -cid updaterNATS --file_slice_max_bytes 0 --file_slice_max_age 100h -store file -dir datastore
expose:
- "4222"
ports:
- "8222:8222"
- "4222:4222"
container_name: nats
volumes:
- datastore:/datastore
db:
image: postgres:10-alpine
ports:
- "5421:5432"
volumes:
- pgdata:/var/lib/postgresql/data
- ./scripts/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
container_name: oodi_db
environment:
- POSTGRES_USER=postgres
- POSTGRES_HOST_AUTH_METHOD=trust
db_sis:
image: postgres:9.6.3
ports:
- "5426:5432"
volumes:
- pgdata_sis:/var/lib/postgresql/data
- ./scripts/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
container_name: db_sis
environment:
- POSTGRES_USER=postgres
redis:
image: redis
command: ["redis-server", "--appendonly", "yes"]
ports:
- "6379:6379"
volumes:
- redis-data:/data
container_name: redis
updater_writer:
build:
context: services/backend
dockerfile: updater_writer/Dockerfile
depends_on:
- nats
- db
environment:
NATS_URI: nats://nats:4222
DB_URL: postgres://postgres@db:5432/tkt_oodi
TEST_DB: postgres://postgres@db:5432/tkt_oodi_test
TEST_DB_SCHEMA: updater_test
container_name: updater_writer
sis-importer-db:
image: postgres:13.1
environment:
POSTGRES_HOST_AUTH_METHOD: trust
volumes:
- sis-importer-pgdata:/var/lib/postgresql/data
container_name: sis-importer-db
volumes:
datastore:
pgdata:
pgdata_sis:
redis-data:
sis-importer-pgdata:
-284
View File
@@ -1,284 +0,0 @@
version: "3.2"
services:
nats:
image: nats-streaming:0.15.1
command: -cid updaterNATS --file_slice_max_bytes 0 --file_slice_max_age 100h -store file -dir datastore
expose:
- "4222"
ports:
- "8222:8222"
- "4222:4222"
container_name: nats
volumes:
- datastore:/datastore
analytics_db:
image: postgres:9.6.3
ports:
- "5423:5432"
volumes:
- analytics_pgdata:/var/lib/postgresql/data
- ./scripts/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
container_name: oodi_analytics_db
environment:
- POSTGRES_USER=postgres
user_db:
image: postgres:9.6.3
ports:
- "5422:5432"
volumes:
- user_pgdata:/var/lib/postgresql/data
container_name: oodi_user_db
environment:
- POSTGRES_USER=postgres
usage_db:
image: postgres:9.6.3
ports:
- "5424:5432"
volumes:
- usage_pgdata:/var/lib/postgresql/data
container_name: oodi_usage_db
environment:
- POSTGRES_USER=postgres
- POSTGRES_DB=usage_db
db:
image: postgres:10-alpine
ports:
- "5421:5432"
volumes:
- pgdata:/var/lib/postgresql/data
- ./scripts/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
container_name: oodi_db
environment:
- POSTGRES_USER=postgres
- POSTGRES_HOST_AUTH_METHOD=trust
db_kone:
image: postgres:9.6.3
ports:
- "5425:5432"
volumes:
- pgdata_kone:/var/lib/postgresql/data
- ./scripts/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
container_name: db_kone
environment:
- POSTGRES_MULTIPLE_DATABASES=db_kone
- POSTGRES_USER=postgres
db_sis:
image: postgres:9.6.3
ports:
- "5426:5432"
volumes:
- pgdata_sis:/var/lib/postgresql/data
- ./scripts/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
container_name: db_sis
environment:
- POSTGRES_USER=postgres
redis:
image: redis
command: ["redis-server", "--appendonly", "yes"]
ports:
- "6379:6379"
volumes:
- redis-data:/data
container_name: redis
backend:
build:
context: services/backend
dockerfile: oodikone2-backend/Dockerfile
ports:
- "8080:8080"
environment:
REDIS: redis
DB_URL: postgres://postgres@db:5432/tkt_oodi
# DB_MAX_CONNECTIONS: 90 # defaults to 5
DB_URL_KONE: postgres://postgres@db_kone:5432/db_kone
SIS_DB_URL: postgres://postgres@db_sis:5432/db_sis
# if NODE_ENV === 'test', these are used instead
TEST_DB: postgres://postgres@db:5432/tkt_oodi_test
TEST_DB_KONE: postgres://postgres@db_kone:5432/db_kone_test
# same but for NODE_ENV === 'anon'
# ANON_DB:
# ANON_DB_KONE:
TOKEN_SECRET: IWannaBeTheVeryBest
SECRET_TOKEN: dev
USERSERVICE_URL: http://userservice:4567
USAGESERVICE_URL: http://usageservice:545
USERSERVICE_SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
USAGESERVICE_SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
ANALYTICS_URL: http://analytics:4568
NATS_URI: nats://nats:4222
UPDATER_URL: http://updater_scheduler:3678
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
# Set these in production:
# ANALYTICS_INFLUXDB_URL
# ANALYTICS_INFLUXDB_DB
# ANALYTICS_INFLUXDB_PASSWORD
# ANALYTICS_INFLUXDB_USER
# MATOMO_SITE_ID
# MATOMO_URL
container_name: backend
depends_on:
- db
- redis
- nats
frontend:
build:
context: services/oodikone2-frontend
environment:
BACKEND_ADDR: backend
ADMINER_URL: http://localhost:5050/?pgsql=db&username=postgres
container_name: frontend
userservice:
build:
context: services/oodikone2-userservice
ports:
- 4567:4567
environment:
DB_URL: postgres://postgres@user_db:5432/user_db
TEST_DB: postgres://postgres@user_db:5432/user_db_test
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
TOKEN_SECRET: IWannaBeTheVeryBest
depends_on:
- user_db
container_name: userservice
usageservice:
build:
context: services/oodikone2-usageservice
ports:
- 545:545
environment:
PORT: "545"
DB_URL: postgres://postgres@usage_db:5432/usage_db
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
depends_on:
- usage_db
container_name: usageservice
updater_api:
build:
context: services/updater_api
depends_on:
- nats
environment:
- NATS_URI=nats://nats:4222
- OODI_ADDR=https://oodikone.cs.helsinki.fi/oodi/test
- TOKEN=jotaindiibadaabaa
container_name: updater_api
updater_writer:
build:
context: services/backend
dockerfile: updater_writer/Dockerfile
depends_on:
- nats
- db
environment:
NATS_URI: nats://nats:4222
DB_URL: postgres://postgres@db:5432/tkt_oodi
TEST_DB: postgres://postgres@db:5432/tkt_oodi_test
TEST_DB_SCHEMA: updater_test
container_name: updater_writer
scheduler_mongo:
image: mongo:4.0.10
volumes:
- mongodata:/data/db
expose:
- "27017"
container_name: scheduler_mongo
updater_scheduler:
build:
context: services/updater_scheduler
depends_on:
- nats
- scheduler_mongo
environment:
- NATS_URI=nats://nats:4222
- MONGO_URI=mongodb://scheduler_mongo:27017
- PORT=3678
container_name: updater_scheduler
analytics:
build:
context: services/oodikone2-analytics
ports:
- 4568:4568
environment:
- DB_URL=postgres://postgres@analytics_db:5432/analytics_db
- TEST_DB=postgres://postgres@analytics_db:5432/analytics_db_test
depends_on:
- analytics_db
container_name: analytics
sis-updater-nats:
image: nats-streaming:0.16.2
command: -cid sis-updater-nats -m 8222 -p 4222 --max_age 2h --hb_fail_count 2 --hb_timeout 10s --hb_interval 10s --auth dev --file_slice_max_bytes 0 --file_slice_max_age 60s --max_msgs 0 -store file -dir datastore
container_name: sis-updater-nats
volumes:
- sis-updater-datastore:/datastore
sis-updater-scheduler:
build:
context: services/sis-updater-scheduler
environment:
REDIS_URI: //redis:6379
SIS_NATS_URI: sis-updater-nats://sis-updater-nats:4222
SIS_NATS_TOKEN: dev
SIS_IMPORTER_HOST: importer-db
SIS_IMPORTER_USER: dev
SIS_IMPORTER_PASSWORD: dev
SIS_IMPORTER_DATABASE: importer-db
SECRET_TOKEN: dev
# LOG_HOST: host.docker.internal
# LOG_HOSTNAME: sis-updater-scheduler
# LOG_PORT: 9501
ports:
- "8082:8082"
container_name: sis-updater-scheduler
depends_on:
- redis
- sis-updater-nats
sis-updater-worker:
build:
context: services/sis-updater-worker
environment:
REDIS_URI: //redis:6379
DB_URL: postgres://postgres@db_sis:5432/db_sis
SIS_NATS_URI: sis-updater-nats://sis-updater-nats:4222
SIS_NATS_TOKEN: dev
SIS_IMPORTER_HOST: importer-db
SIS_IMPORTER_USER: dev
SIS_IMPORTER_PASSWORD: dev
SIS_IMPORTER_DATABASE: importer-db
# LOG_HOST: host.docker.internal
# LOG_HOSTNAME: sis-updater-worker
# LOG_PORT: 9501
depends_on:
- redis
- sis-updater-nats
volumes:
datastore:
analytics_pgdata:
user_pgdata:
usage_pgdata:
pgdata:
pgdata_kone:
pgdata_sis:
redis-data:
mongodata:
sis-updater-datastore:
+15
View File
@@ -0,0 +1,15 @@
version: '3.2'
services:
db:
environment:
- POSTGRES_MULTIPLE_DATABASES=tkt_oodi,tkt_oodi_test
db_sis:
environment:
- POSTGRES_MULTIPLE_DATABASES=db_sis
volumes:
datastore-real:
redis-data-real:
mongodata-real:
+14
View File
@@ -0,0 +1,14 @@
version: '3.2'
services:
nats:
volumes:
- datastore-real:/datastore
redis:
volumes:
- redis-data-real:/data
volumes:
datastore-real:
redis-data-real:
+28
View File
@@ -0,0 +1,28 @@
version: '3.2'
services:
db:
environment:
- POSTGRES_MULTIPLE_DATABASES=tkt_oodi,tkt_oodi_test,tkt_oodi_real
restart: unless-stopped
shm_size: '2gb'
db_sis:
command:
- "postgres"
- "-c"
- "max_wal_size=8GB"
environment:
- POSTGRES_MULTIPLE_DATABASES=db_sis,db_sis_real
restart: unless-stopped
adminer:
image: adminer:4.6.2
environment:
- ADMINER_DESIGN=pepa-linha
volumes:
- ./adminer-plugins:/var/www/html/plugins-enabled
ports:
- 5050:8080
container_name: adminer
restart: unless-stopped
-40
View File
@@ -1,40 +0,0 @@
version: '3.2'
services:
nats:
volumes:
- datastore-real:/datastore
backend:
environment:
DB_URL: postgres://postgres@db:5432/tkt_oodi_real
DB_URL_KONE: postgres://postgres@db_kone:5432/db_kone_real
userservice:
environment:
DB_URL: postgres://postgres@user_db:5432/user_db_real
analytics:
environment:
DB_URL: postgres://postgres@analytics_db:5432/analytics_db_real
updater_writer:
environment:
DB_URL: postgres://postgres@db:5432/tkt_oodi_real
updater_api:
environment:
- OODI_ADDR=https://oodikone.cs.helsinki.fi/oodi
- TOKEN
redis:
volumes:
- redis-data-real:/data
scheduler_mongo:
volumes:
- mongodata-real:/data/db
volumes:
datastore-real:
redis-data-real:
mongodata-real:
-186
View File
@@ -1,186 +0,0 @@
version: '3.2'
services:
user_db:
environment:
- POSTGRES_MULTIPLE_DATABASES=user_db,user_db_real
restart: unless-stopped
analytics_db:
environment:
- POSTGRES_MULTIPLE_DATABASES=analytics_db,analytics_db_real
restart: unless-stopped
db:
environment:
- POSTGRES_MULTIPLE_DATABASES=tkt_oodi,tkt_oodi_test,tkt_oodi_real
restart: unless-stopped
shm_size: '2gb'
db_sis:
command:
- "postgres"
- "-c"
- "max_wal_size=8GB"
environment:
- POSTGRES_MULTIPLE_DATABASES=db_sis
restart: unless-stopped
backend:
build:
args:
NODE_ENV: 'development'
TAG: 'development'
command: ['sh', '-c', 'cp -r /usr/src/cache/node_modules/. /usr/src/app/node_modules/ && npm run dev']
environment:
FRONT_URL: http://localhost:8000
volumes:
- ./services/backend/oodikone2-backend/:/usr/src/app
- ./services/backend/shared/models:/usr/src/app/src/models
- ./services/backend/shared/migrations:/usr/src/app/src/database/migrations
- ./services/backend/shared/migrations_kone:/usr/src/app/src/database/migrations_kone
restart: unless-stopped
frontend:
build:
args:
NODE_ENV: 'development'
TAG: 'development'
command: ['sh', '-c', 'cp -r /usr/src/cache/node_modules/. /usr/src/app/node_modules/ && npm run docker']
ports:
- "8081:8081"
- "7777:7777"
environment:
USER_ADMINER_URL: http://localhost:5050/?pgsql=user_db&username=postgres
ANALYTICS_ADMINER_URL: http://localhost:5050/?pgsql=analytics_db&username=postgres
USAGE_ADMINER_URL: http://localhost:5050/?pgsql=usage_db&username=postgres
KONE_ADMINER_URL: http://localhost:5050/?pgsql=db_kone&username=postgres
SIS_ADMINER_URL: http://localhost:5050/?pgsql=db_sis&username=postgres
volumes:
- ./services/oodikone2-frontend/:/usr/src/app
restart: unless-stopped
userservice:
build:
args:
NODE_ENV: 'development'
command: ['sh', '-c', 'cp -r /usr/src/cache/node_modules/. /usr/src/app/node_modules/ && npm run dev']
volumes:
- ./services/oodikone2-userservice/:/usr/src/app
restart: unless-stopped
usageservice:
build:
args:
NODE_ENV: 'development'
command: ['sh', '-c', 'cp -r /usr/src/cache/node_modules/. /usr/src/app/node_modules/ && npm run dev']
volumes:
- ./services/oodikone2-usageservice/:/usr/src/app
restart: unless-stopped
updater_api:
build:
args:
NODE_ENV: 'development'
command: ['sh', '-c', 'cp -r /usr/src/cache/node_modules/. /usr/src/app/node_modules/ && npm run dev']
volumes:
- ./services/updater_api/:/usr/src/app
restart: unless-stopped
updater_writer:
build:
args:
NODE_ENV: 'development'
command: ['sh', '-c', 'cp -r /usr/src/cache/node_modules/. /usr/src/app/node_modules/ && npm run dev']
volumes:
- ./services/backend/updater_writer/:/usr/src/app
- ./services/backend/shared/models:/usr/src/app/models
- ./services/backend/shared/migrations:/usr/src/app/database/migrations
- ./services/backend/shared/migrations_kone:/usr/src/app/database/migrations_kone
environment:
DB_SCHEMA_KONE: kone_data
restart: unless-stopped
updater_scheduler:
build:
args:
NODE_ENV: 'development'
command: ['sh', '-c', 'cp -r /usr/src/cache/node_modules/. /usr/src/app/node_modules/ && npm run dev']
volumes:
- ./services/updater_scheduler/:/usr/src/app
restart: unless-stopped
analytics:
build:
args:
NODE_ENV: 'development'
command: ['sh', '-c', 'cp -r /usr/src/cache/node_modules/. /usr/src/app/node_modules/ && npm run dev']
volumes:
- ./services/oodikone2-analytics/:/usr/src/app
restart: unless-stopped
adminer:
image: adminer:4.6.2
environment:
- ADMINER_DESIGN=pepa-linha
volumes:
- ./adminer-plugins:/var/www/html/plugins-enabled
ports:
- 5050:8080
container_name: adminer
restart: unless-stopped
sis-updater-scheduler:
build:
args:
NODE_ENV: 'development'
command: ['sh', '-c', 'cp -r /usr/src/cache/node_modules/. /usr/src/app/node_modules/ && npm run dev']
volumes:
- ./services/sis-updater-scheduler/:/usr/src/app
restart: unless-stopped
sis-updater-worker:
build:
args:
NODE_ENV: 'development'
command: 'npm run dev'
volumes:
- ./services/sis-updater-worker/:/usr/src/app
restart: unless-stopped
networks:
default:
# zookeeper:
# image: wurstmeister/zookeeper
# ports:
# - "2181:2181"
# environment:
# - KAFKA_BROKER_ID=1
# container_name: zookeeper
# kafka:
# image: wurstmeister/kafka
# container_name: kafka
# depends_on:
# - zookeeper
# ports:
# - "9092:9092"
# environment:
# - HOSTNAME_COMMAND=route -n | awk '/UG[ \t]/{print $$2}'
# - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
# - KAFKA_AUTO_CREATE_TOPICS_ENABLE=true
# - KAFKA_DELETE_TOPIC_ENABLE=true
# - KAFKA_CREATE_TOPICS=studentnumbers:1:1,priority_studentnumbers:1:1
# - KAFKA_LOG_DIRS=/kafka/kafka-logs-1
# volumes:
# - ./kafka/docker.sock:/var/run/docker.sock
# - ./kafka/logs:/kafka/kafka-logs-1
# mongo_db:
# image: mongo
# volumes:
# - ./mongodata:/data/db
# ports:
# - "27017:27017"
# container_name: mongo_db
+119 -113
View File
@@ -1081,9 +1081,9 @@
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
@@ -1100,12 +1100,6 @@
"any-observable": "^0.3.0"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -1113,9 +1107,9 @@
"dev": true
},
"@types/sinonjs__fake-timers": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz",
"integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz",
"integrity": "sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==",
"dev": true
},
"@types/sizzle": {
@@ -1158,9 +1152,9 @@
"dev": true
},
"ajv": {
"version": "6.12.5",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz",
"integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==",
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
@@ -1215,9 +1209,9 @@
}
},
"arch": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/arch/-/arch-2.1.2.tgz",
"integrity": "sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
"integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
"dev": true
},
"arr-diff": {
@@ -1348,9 +1342,9 @@
"dev": true
},
"aws4": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz",
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
"dev": true
},
"babel-plugin-add-module-exports": {
@@ -2088,9 +2082,9 @@
}
},
"commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"dev": true
},
"common-tags": {
@@ -2228,9 +2222,9 @@
},
"dependencies": {
"parse-json": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
"integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
@@ -2317,9 +2311,9 @@
}
},
"cypress": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-5.2.0.tgz",
"integrity": "sha512-9S2spcrpIXrQ+CQIKHsjRoLQyRc2ehB06clJXPXXp1zyOL/uZMM3Qc20ipNki4CcNwY0nBTQZffPbRpODeGYQg==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-6.3.0.tgz",
"integrity": "sha512-Ec6TAFOxdSB2HPINNJ1f7z75pENXcfCaQkz+A9j0eGSvusFJ2NNErq650DexCbNJAnCQkPqXB4XPH9kXnSQnUA==",
"dev": true,
"requires": {
"@cypress/listr-verbose-renderer": "^0.4.1",
@@ -2334,7 +2328,7 @@
"chalk": "^4.1.0",
"check-more-types": "^2.24.0",
"cli-table3": "~0.6.0",
"commander": "^4.1.1",
"commander": "^5.1.0",
"common-tags": "^1.8.0",
"debug": "^4.1.1",
"eventemitter2": "^6.4.2",
@@ -2352,10 +2346,10 @@
"minimist": "^1.2.5",
"moment": "^2.27.0",
"ospath": "^1.2.2",
"pretty-bytes": "^5.3.0",
"pretty-bytes": "^5.4.1",
"ramda": "~0.26.1",
"request-progress": "^3.0.0",
"supports-color": "^7.1.0",
"supports-color": "^7.2.0",
"tmp": "~0.2.1",
"untildify": "^4.0.0",
"url": "^0.11.0",
@@ -2363,12 +2357,11 @@
},
"dependencies": {
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
@@ -2706,9 +2699,9 @@
}
},
"execa": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz",
"integrity": "sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
"integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
"dev": true,
"requires": {
"cross-spawn": "^7.0.0",
@@ -3028,12 +3021,12 @@
}
},
"find-versions": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz",
"integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz",
"integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==",
"dev": true,
"requires": {
"semver-regex": "^2.0.0"
"semver-regex": "^3.1.2"
}
},
"for-in": {
@@ -3075,31 +3068,31 @@
}
},
"fs-extra": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
"universalify": "^2.0.0"
},
"dependencies": {
"jsonfile": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz",
"integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^1.0.0"
"universalify": "^2.0.0"
}
},
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"dev": true
}
}
@@ -3208,12 +3201,12 @@
}
},
"global-dirs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz",
"integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz",
"integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==",
"dev": true,
"requires": {
"ini": "^1.3.5"
"ini": "1.3.7"
}
},
"globals": {
@@ -3393,30 +3386,29 @@
"dev": true
},
"husky": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/husky/-/husky-4.3.0.tgz",
"integrity": "sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==",
"version": "4.3.8",
"resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz",
"integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==",
"dev": true,
"requires": {
"chalk": "^4.0.0",
"ci-info": "^2.0.0",
"compare-versions": "^3.6.0",
"cosmiconfig": "^7.0.0",
"find-versions": "^3.2.0",
"find-versions": "^4.0.0",
"opencollective-postinstall": "^2.0.2",
"pkg-dir": "^4.2.0",
"pkg-dir": "^5.0.0",
"please-upgrade-node": "^3.2.0",
"slash": "^3.0.0",
"which-pm-runs": "^1.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
@@ -3469,9 +3461,9 @@
"dev": true
},
"import-fresh": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
"integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
"requires": {
"parent-module": "^1.0.0",
@@ -3501,9 +3493,9 @@
"dev": true
},
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz",
"integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==",
"dev": true
},
"inline-source-map": {
@@ -4071,12 +4063,11 @@
},
"dependencies": {
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
@@ -4285,18 +4276,18 @@
}
},
"mime-db": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==",
"version": "1.45.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
"dev": true
},
"mime-types": {
"version": "2.1.27",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
"version": "2.1.28",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
"dev": true,
"requires": {
"mime-db": "1.44.0"
"mime-db": "1.45.0"
}
},
"mimic-fn": {
@@ -4408,9 +4399,9 @@
}
},
"moment": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.28.0.tgz",
"integrity": "sha512-Z5KOjYmnHyd/ukynmFd/WwyXHd7L4J9vTI/nn5Ap9AVUgaAE15VvQ9MOGmJJygEUklupqIrFnor/tjTwRU+tQw==",
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
"dev": true
},
"ms": {
@@ -4769,40 +4760,49 @@
"dev": true
},
"pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
"integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==",
"dev": true,
"requires": {
"find-up": "^4.0.0"
"find-up": "^5.0.0"
},
"dependencies": {
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
"p-locate": "^5.0.0"
}
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
"p-limit": "^3.0.2"
}
},
"path-exists": {
@@ -4829,9 +4829,9 @@
"dev": true
},
"pretty-bytes": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.4.1.tgz",
"integrity": "sha512-s1Iam6Gwz3JI5Hweaz4GoCD1WUNUIyzePFy5+Js2hjwGVt2Z79wNN+ZKOZ2vB6C+Xs6njyB84Z1IthQg8d9LxA==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.5.0.tgz",
"integrity": "sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA==",
"dev": true
},
"process": {
@@ -5213,9 +5213,9 @@
"dev": true
},
"semver-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz",
"integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz",
"integrity": "sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA==",
"dev": true
},
"set-blocking": {
@@ -5954,9 +5954,9 @@
"dev": true
},
"uri-js": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
"integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"requires": {
"punycode": "^2.1.0"
@@ -6221,6 +6221,12 @@
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
}
}
}
+31 -19
View File
@@ -1,10 +1,10 @@
{
"name": "oodikone",
"version": "1.0.0",
"description": "Simple CLI tool for setting up the development environment for Oodikone. The entire development environment runs inside of a Docker network that's defined in the docker-compose.yml file. The specifics of what this means for setting up the environment and accessing logs is discussed in a later section.",
"description": "Simple CLI tool for setting up the development environment for Oodikone. The entire development environment runs inside of a Docker network that's defined in the dco.yml file. The specifics of what this means for setting up the environment and accessing logs is discussed in a later section.",
"main": "./scripts/run.sh",
"scripts": {
"cli": "./scripts/run.sh",
"cli": "./oodikone.sh",
"cypress": "cypress",
"cypress:open": "cypress open --config baseUrl=http://localhost:8081/",
"cypress:run": "CYPRESS_baseUrl=http://frontend:8081/ npm run docker:cypress",
@@ -17,26 +17,38 @@
"test_services": "npm run services -- -e -c test",
"test": "./scripts/test.sh",
"test:smoke": "./scripts/test.sh -s",
"docker": "docker-compose -f docker-compose.yml -f ./docker/docker-compose.dev.yml",
"docker:cypress": "npm run docker -- -f ./docker/docker-compose.cypress.yml run --rm cypress",
"docker:real": "docker-compose -f docker-compose.yml -f ./docker/docker-compose.dev.yml -f ./docker/docker-compose.dev.real.yml",
"docker:ci": "docker-compose -f docker-compose.yml -f ./docker/docker-compose.lateste2e.yml",
"docker:ci:cypress": "npm run docker:ci -- -f ./docker/docker-compose.cypress.yml run --rm cypress",
"docker:build": "npm run docker -- build",
"docker:up": "npm run docker -- up -d",
"docker:up:real": "npm run docker:real -- up -d",
"docker:down": "npm run docker -- down",
"docker:logs": "npm run docker -- logs -f",
"docker:logs:backend": "npm run docker -- logs -f backend",
"docker:restart": "npm run docker -- restart",
"docker:restart:backend": "npm run docker -- restart backend",
"docker:updater:base": "docker-compose -f ./dco.data.yml -f ./updater/dco.updater.yml",
"docker:updater:dev": "npm run docker:updater:base -- -f ./docker/dco.data.dev.yml -f ./updater/docker/dco.dev.yml",
"docker:updater:real": "npm run docker:updater:dev -- -f ./docker/dco.data.dev.real.yml -f ./updater/docker/dco.dev.real.yml",
"docker:oodikone:base": "docker-compose -f ./dco.data.yml -f ./services/dco.oodikone.yml",
"docker:oodikone:dev": "npm run docker:oodikone:base -- -f ./docker/dco.data.dev.yml -f ./services/docker/dco.dev.yml",
"docker:oodikone:real": "npm run docker:oodikone:dev -- -f ./docker/dco.data.dev.real.yml -f ./services/docker/dco.dev.real.yml",
"docker:both:base": "docker-compose -f ./dco.data.yml -f ./updater/dco.updater.yml -f ./services/dco.oodikone.yml",
"docker:both:dev": "npm run docker:both:base -- -f ./docker/dco.data.dev.yml -f ./updater/docker/dco.dev.yml -f ./services/docker/dco.dev.yml",
"docker:both:real": "npm run docker:both:dev -- -f ./docker/dco.data.dev.real.yml -f ./updater/docker/dco.dev.real.yml -f ./services/docker/dco.dev.real.yml",
"docker:cypress": "npm run docker:oodikone:dev -- -f ./docker/dco.cypress.yml run --rm cypress",
"docker:ci": "npm run docker:both:base -- -f ./docker/dco.data.ci.yml -f ./services/docker/dco.ci.yml -f ./updater/docker/dco.ci.yml",
"docker:ci:cypress": "npm run docker:ci -- -f ./docker/dco.cypress.yml run --rm cypress",
"docker:build": "npm run docker:oodikone:dev -- build",
"docker:mac:up": "npm run docker:oodikone:dev -- up -d backend && npm run docker:up",
"docker:mac:up:real": "npm run docker:oodikone:real -- up -d backend && npm run docker:up:real",
"docker:up": "npm run docker:oodikone:dev -- up -d",
"docker:up:real": "npm run docker:oodikone:real -- up -d",
"docker:down": "npm run docker:oodikone:dev -- down",
"docker:logs": "npm run docker:oodikone:dev -- logs -f",
"docker:logs:backend": "npm run docker:oodikone:dev -- logs -f backend",
"docker:restart": "npm run docker:oodikone:dev -- restart",
"docker:restart:backend": "npm run docker:oodikone:dev -- restart backend",
"docker:obliterate": "docker stop $(docker ps -aq) ; docker rmi -f $(docker images -a -q) ; docker system prune -a",
"start": "npm run docker:up && npm run docker:logs",
"start:real": "npm run docker:real -- up -d && npm run docker:real -- logs -f",
"start:updater": "npm run docker:up -- --scale sis-updater-worker=4 sis-updater-scheduler sis-updater-worker db_sis sis-updater-nats db adminer redis && npm run docker:logs -- --tail=\"1000\""
"start:real": "npm run docker:oodikone:real -- up -d && npm run docker:oodikone:real -- logs -f",
"start:updater": "npm run docker:up -- --scale sis-updater-worker=4 sis-updater-scheduler sis-updater-worker db_sis sis-updater-nats db adminer redis && npm run docker:logs -- --tail=\"1000\"",
"updater:dev:up": "npm run docker:updater:real -- up",
"updater:dev:down": "npm run docker:updater:real -- down --remove-orphans"
},
"husky": {
"hooks": {
"pre-commit": "scripts/grep_only.sh",
"pre-push": "npm run lint"
}
},
@@ -52,9 +64,9 @@
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
"devDependencies": {
"concurrently": "^5.3.0",
"cypress": "^5.2.0",
"cypress": "^6.3.0",
"cypress-smokify": "0.0.3",
"husky": "^4.3.0"
"husky": "^4.3.8"
},
"dependencies": {}
}
+1 -1
View File
@@ -2,7 +2,7 @@
This tool helps you in managing the project configuration. If you are new to
Oodikone development, you should probably run "Set up with anonymous data"
which will take care of setting up and starting Oodikone for you.
which will take care of setting up and starting Oodikone for you.
Please use the anonymous data whenever possible!
-6
View File
@@ -43,14 +43,8 @@ else
services=(
backend
frontend
updater_writer
updater_api
updater_scheduler
analytics
userservice
usageservice
sis-updater-scheduler
oodikone_sis-updater-worker_1
)
fi
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
RESULT=$(git grep -HEn --cached "(it|describe)\.only" $(git diff --cached --name-only) ./cypress)
if [ ! -z "$RESULT" ]; then
echo -e "\e[31mDo not commit .only-tests. Did you remember to git add?\e[0m"
echo -e "$RESULT"
exit 1
else
echo "No .only-tests found"
exit 0
fi
+5 -2
View File
@@ -46,15 +46,18 @@ export -f retry
export -f drop_create_populate
mkdir -p backups
echo "Enter your Uni Helsinki username:"
read username
echo "Fetching latest staging SIS data & OODI demo data"
scp -r -o ProxyCommand="ssh -W %h:%p melkinpaasi.cs.helsinki.fi" oodikone-staging:/home/tkt_oodi/backups/\{sis-updater-staging.sqz,demo-anon-staging.sqz\} $BACKUPS
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkinpaasi.cs.helsinki.fi" $username@oodikone-staging:/home/tkt_oodi/backups/\{sis-updater-staging.sqz,demo-anon-staging.sqz\} $BACKUPS
echo "Setting up services: ${SERVICES[@]}"
npm run docker:down --prefix $DIR_PATH
npm run docker:up --prefix $DIR_PATH -- ${SERVICES[@]}
echo "Populating DB_SIS and DB_ANON"
echo $DB_SIS_ARGS $DB_ANON_ARGS | xargs -n 3 -P 2 sh -c 'drop_create_populate $1 $2 $3' sh
echo $DB_SIS_ARGS $DB_ANON_ARGS | xargs -n 3 -P 2 bash -c 'drop_create_populate $1 $2 $3' bash
echo "Cleaning up"
npm run docker:down --prefix $DIR_PATH
+7 -5
View File
@@ -24,9 +24,9 @@ options=(
"Set up with real data."
"Reset anonymous database."
"Reset real data database."
"Download latest anonynmous data."
"Download latest anonymous data."
"Download latest real data."
"Setup SIS data."
"Set up importer-db."
"Quit."
)
@@ -36,6 +36,7 @@ while true; do
"Set up with anonymous data.")
mopo
run_anon_full_setup
./scripts/populate-db.sh
;;
"Set up with real data.")
mopo
@@ -43,18 +44,19 @@ while true; do
;;
"Reset anonymous database.")
reset_db
#./scripts/populate-db.sh
;;
"Reset real data database.")
reset_real_db
;;
"Download latest anonynmous data.")
"Download latest anonymous data.")
get_anon_oodikone
;;
"Download latest real data.")
get_oodikone_server_backup
;;
"Setup SIS data.")
./scripts/populate-db.sh
"Set up importer-db.")
run_importer_setup
;;
"Quit.")
break 2
+49 -14
View File
@@ -13,9 +13,10 @@ PSQL_REAL_DB_BACKUP="$BACKUP_DIR/latest-pg.sqz"
KONE_REAL_DB_BACKUP="$BACKUP_DIR/latest-kone-pg.sqz"
USER_REAL_DB_BACKUP="$BACKUP_DIR/latest-user-pg.sqz"
ANALYTICS_REAL_DB_BACKUP="$BACKUP_DIR/latest-analytics-pg.sqz"
SIS_REAL_DB_BACKUP="$BACKUP_DIR/latest-sis.sqz"
docker-compose-dev () {
docker-compose -f docker-compose.yml -f ./docker/docker-compose.dev.yml "$@"
npm run docker:oodikone:dev -- "$@"
}
retry () {
@@ -40,6 +41,7 @@ get_oodikone_server_backup() {
read username
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkinpaasi.cs.helsinki.fi" $username@oodikone.cs.helsinki.fi:/home/tkt_oodi/backups/* "$BACKUP_DIR/"
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkinpaasi.cs.helsinki.fi" $username@svm-77.cs.helsinki.fi:/home/tkt_oodi/backups/* "$BACKUP_DIR/"
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkinpaasi.cs.helsinki.fi" $username@svm-96.cs.helsinki.fi:/home/updater_user/backups/* "$BACKUP_DIR/"
}
get_anon_oodikone() {
@@ -48,7 +50,11 @@ get_anon_oodikone() {
}
restore_psql_from_backup () {
echo ""
echo "Restoring database from backup ($1/$2):"
echo " 1. Copying dump..."
docker cp $1 $2:/asd.sqz
echo " 2. Writing database..."
docker exec $2 pg_restore -U postgres --no-owner -F c --dbname=$3 -j4 /asd.sqz
}
@@ -56,13 +62,15 @@ ping_psql () {
drop_psql $1 $2
echo "Creating psql in container $1 with db name $2"
retry docker exec -u postgres $1 pg_isready --dbname=$2
docker exec -u postgres $1 psql -c "CREATE DATABASE $2" || echo "container $1 DB $2 already exists"
#docker exec -u postgres $1 psql -c "CREATE DATABASE $2" || echo "container $1 DB $2 already exists"
docker exec -u postgres $1 createdb $2 || echo "container $1 DB $2 already exists"
}
drop_psql () {
echo "Dropping psql in container $1 with db name $2"
retry docker exec -u postgres $1 pg_isready --dbname=$2
docker exec -u postgres $1 psql -c "DROP DATABASE $2" || echo "container $1 DB $2 doesn't exists"
#docker exec -u postgres $1 psql -c "DROP DATABASE $2" || echo "container $1 DB $2 doesn't exists"
docker exec -u postgres $1 dropdb $2 || echo "container $1 DB $2 does not exist"
}
db_setup_full () {
@@ -75,13 +83,25 @@ db_setup_full () {
restore_psql_from_backup $USER_REAL_DB_BACKUP oodi_user_db user_db_real
ping_psql "oodi_analytics_db" "analytics_db_real"
restore_psql_from_backup $ANALYTICS_REAL_DB_BACKUP oodi_analytics_db analytics_db_real
# echo "Restoring MongoDB from backup"
# retry restore_mongodb_from_backup
ping_psql "db_sis" "db_sis_real"
restore_psql_from_backup $SIS_REAL_DB_BACKUP db_sis db_sis_real
echo "Database setup finished"
}
run_importer_setup () {
echo "Setting up importer database."
echo "Enter your Uni Helsinki username:"
read username
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkinpaasi.cs.helsinki.fi" $username@importer:/home/importer_user/importer-db/backup/importer-db.sqz "$BACKUP_DIR/"
docker-compose -f dco.data.yml up -d sis-importer-db
ping_psql "sis-importer-db" "importer-db"
restore_psql_from_backup "$BACKUP_DIR/importer-db.sqz" sis-importer-db importer-db
docker-compose -f dco.data.yml down
}
db_anon_setup_full () {
echo "Restoring PostgreSQL from backup"
ping_psql "oodi_db" "tkt_oodi"
ping_psql "oodi_db" "tkt_oodi_test"
restore_psql_from_backup $PSQL_DB_BACKUP oodi_db tkt_oodi
@@ -95,21 +115,20 @@ db_anon_setup_full () {
ping_psql "oodi_analytics_db" "analytics_db"
restore_psql_from_backup $ANALYTICS_DB_BACKUP oodi_analytics_db analytics_db
# echo "Restoring MongoDB from backup"
# retry restore_mongodb_from_backup
echo "Database setup finished"
}
reset_real_db () {
docker-compose-dev down
docker-compose-dev up -d db user_db db_kone analytics_db
docker-compose-dev up -d db user_db db_kone analytics_db db_sis
db_setup_full
docker-compose-dev down
}
reset_db () {
docker-compose-dev down
docker-compose-dev up -d db user_db db_kone analytics_db
docker-compose-dev up -d db user_db db_kone analytics_db db_sis
db_anon_setup_full
docker-compose-dev down
}
@@ -124,17 +143,31 @@ reset_db_for_cypress () {
retry curl --silent --fail localhost:8080/ping
}
install_cli_npm_packages () {
install_local_npm_packages () {
npm ci
# The rest is required for linting to work.
cd services/oodikone2-frontend
npm ci
cd ../oodikone2-analytics
npm ci
cd ../oodikone2-userservice
npm ci
cd ../backend/oodikone2-backend
npm ci
cd ../updater_writer
npm ci
cd ../../../
}
show_instructions () {
cat ./scripts/assets/instructions.txt
cat scripts/assets/instructions.txt
}
run_full_setup () {
echo "Setup npm packages"
install_cli_npm_packages
install_local_npm_packages
echo "Init dirs"
init_dirs
echo "Getting backups from the Oodikone server, this will prompt you for your password. "
@@ -144,16 +177,17 @@ run_full_setup () {
echo "Building images"
docker-compose-dev build
echo "Setup oodikone db from dump."
docker-compose-dev up -d db user_db db_kone analytics_db
docker-compose-dev up -d db user_db db_kone analytics_db db_sis
db_setup_full
db_anon_setup_full
docker-compose-dev down
npm run docker:up:real
show_instructions
}
run_anon_full_setup () {
echo "Setup npm packages"
install_cli_npm_packages
install_local_npm_packages
echo "Init dirs"
init_dirs
echo "Getting anon backups from the private repository. "
@@ -170,6 +204,7 @@ run_anon_full_setup () {
docker-compose-dev up -d db user_db db_kone analytics_db
db_anon_setup_full
docker-compose-dev down
npm run docker:up
show_instructions
if [[ ! -z anon_data_error ]] ; then
tput setaf 1; echo "$anon_data_error"; tput sgr0
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
OLD=$(git diff --name-only --cached | grep -Ec "(\/services\/|\/routes\/)")
NEW=$(git diff --name-only --cached | grep -Ec "(\/servicesV2\/|\/routesV2\/)")
if (( $OLD > $NEW )); then
echo "Do not update old services or routes without updating SIS"
exit 1
else
exit 0
fi
@@ -4,17 +4,14 @@ WORKDIR /usr/src/app
EXPOSE 8080
CMD npm start
COPY ./oodikone2-backend/package.json /usr/src/app/package.json
COPY ./oodikone2-backend/package-lock.json /usr/src/app/package-lock.json
COPY ./oodikone2-backend/package.json /usr/src/cache/package.json
COPY ./oodikone2-backend/package-lock.json /usr/src/cache/package-lock.json
COPY ./oodikone2-backend/package* ./
RUN npm ci
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
ARG TAG=local
ENV TAG=${TAG}
RUN if [ "${NODE_ENV}" != "production" ]; then cp -r /usr/src/app/node_modules /usr/src/cache/node_modules; fi
COPY ./oodikone2-backend /usr/src/app
COPY ./shared/models /usr/src/app/src/models
+39 -40
View File
@@ -3105,19 +3105,11 @@
"dev": true
},
"axios": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
},
"dependencies": {
"is-buffer": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
"integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
}
"follow-redirects": "^1.10.0"
}
},
"babel-eslint": {
@@ -4271,7 +4263,8 @@
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
},
"content-disposition": {
"version": "0.5.3",
@@ -5522,22 +5515,9 @@
"dev": true
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
}
}
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
},
"for-in": {
"version": "1.0.2",
@@ -5619,7 +5599,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@@ -5640,12 +5621,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5660,17 +5643,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -5787,7 +5773,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@@ -5799,6 +5786,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -5813,6 +5801,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -5820,12 +5809,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -5844,6 +5835,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -5924,7 +5916,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -5936,6 +5929,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -6021,7 +6015,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -6057,6 +6052,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -6076,6 +6072,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -6119,12 +6116,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},
@@ -14,6 +14,7 @@
"start-debug": "node --inspect-brk=0.0.0.0:9229 --max_old_space_size=4096 index.js",
"docker_local": "NODE_ENV=docker_local nodemon index.js",
"dev": "NODE_ENV=dev nodemon --max_old_space_size=4096 index.js",
"debug": "NODE_ENV=dev nodemon --max_old_space_size=4096 inspect index.js",
"anon": "NODE_ENV=test nodemon index.js",
"test": "npm run lint && npm run jest",
"jest": "NODE_ENV=test jest --runInBand --verbose",
@@ -39,7 +40,7 @@
"@sentry/node": "^5.11.0",
"async": "^3.1.0",
"async-lock": "^1.2.2",
"axios": "^0.19.0",
"axios": "^0.21.1",
"bluebird": "^3.5.5",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
@@ -24,7 +24,7 @@ initializeDatabaseConnection()
const app = express()
Sentry.init({
dsn: process.env.SENTRY_ADDR,
dsn: 'https://c55e6d020db640e889948cc25ced1c19@sentry.toska.cs.helsinki.fi/2',
environment: process.env.TAG
})
app.use(Sentry.Handlers.requestHandler())
@@ -32,7 +32,6 @@ const KEY_PATH = process.env.KEY_PATH // production/staging only
const USERSERVICE_URL = process.env.USERSERVICE_URL
const UPDATER_URL = process.env.UPDATER_URL
const SIS_UPDATER_URL = process.env.SIS_UPDATER_URL
const USAGESERVICE_URL = process.env.USAGESERVICE_URL
const ANALYTICS_URL = process.env.ANALYTICS_URL
const PORT = isTest ? 8079 : 8080
const OODI_SECRET = process.env.OODI_SECRET
@@ -100,7 +99,6 @@ module.exports = {
ACCESS_TOKEN_HEADER_KEY,
PORT,
ANALYTICS_URL: formatURL(ANALYTICS_URL),
USAGESERVICE_URL,
requiredGroup,
OODI_SECRET,
OODI_SECRET_HEADER_KEY,
@@ -31,6 +31,7 @@ class DbConnection extends EventEmitter {
return
}
console.log(`Sis database connection failed! Attempt ${attempt}/${this.RETRY_ATTEMPTS}`)
console.log(e)
setTimeout(() => this.connect(attempt + 1), 1000 * attempt)
}
}
@@ -20,7 +20,7 @@ const { productivityStatsForCode, throughputStatsForCode } = require('./services
const {
productivityStatsForStudytrack: productivityStatsForStudytrackV2,
throughputStatsForStudytrack: throughputStatsForStudytrackV2
} = require('./servicesV2/studytrack')
} = require('./servicesV2/studyprogramme')
const { calculateFacultyYearlyStats } = require('./services/faculties')
const topteachers = require('./services/topteachers')
@@ -1,5 +1,4 @@
const morgan = require('morgan')
const usageService = require('../services/usageService')
const logger = require('../util/logger')
// So this appears to be a hack to get neatly formatted stats like response-time etc. from morgan
@@ -26,8 +25,10 @@ const accessLogger = morgan((tokens, req, res) => {
'ms'
].join(' ')
usageService.log(message, meta).catch(e => {
logger.error('Failed to save usage stats!', e)
logger.info(message, {
...meta,
// pass this as a custom field so we can filter by it in graylog
isUsageStats: true
})
})
@@ -10,13 +10,24 @@ const TOKEN_VERSION = 1 // When token structure changes, increment in userservic
const checkAuth = async (req, res, next) => {
const token = req.headers[ACCESS_TOKEN_HEADER_KEY]
const uid = req.headers['uid']
if (!token) return res.status(403).json({ error: 'No token in headers' })
if (!token) {
return res.status(403).json({ error: 'No token in headers' })
}
jwt.verify(token, conf.TOKEN_SECRET, async (err, decoded) => {
if (err) return res.status(403).json(err)
if (decoded.version !== TOKEN_VERSION)
if (err) {
return res.status(403).json(err)
}
if (decoded.version !== TOKEN_VERSION) {
return res.status(401).json({ error: 'Token needs to be refreshed - invalid version' })
if (decoded.mockedBy ? decoded.mockedBy !== uid : decoded.userId !== uid)
}
if (decoded.mockedBy ? decoded.mockedBy !== uid : decoded.userId !== uid) {
return res.status(403).json({ error: 'User shibboleth id and token id did not match' })
}
const userData = await getUserDataFor(decoded.userId)
req.decodedToken = decoded
Object.assign(req, userData)
@@ -27,12 +38,12 @@ const checkAuth = async (req, res, next) => {
const roles = requiredRoles => async (req, res, next) => {
if (req.decodedToken) {
const { roles } = req
if (requiredRoles.every(r => roles.indexOf(r) >= 0) || roles.includes('admin')) {
next()
return
return next()
}
}
console.log(`Missing required roles ${requiredRoles}`)
res.status(403).json({ error: 'missing required roles' })
}
@@ -40,25 +51,28 @@ const checkRequiredGroup = async (req, res, next) => {
const hyGroups = parseHyGroups(req.headers['hygroupcn'])
const enabled = hasRequiredGroup(hyGroups)
const tokenOutdated = req.decodedToken.enabled !== enabled
if (tokenOutdated) {
res.status(401).json({
error: 'Token needs to be refreshed - enabled doesnt match hy-group requirement'
return res.status(401).json({
error: 'Token needs to be refreshed - enabled does not match hy-group requirement'
})
} else if (!enabled) {
res.status(403).json({ error: 'User is not enabled' })
} else {
next()
}
if (!enabled) {
return res.status(403).json({ error: 'User is not enabled' })
}
next()
}
const checkUserBlacklisting = async (req, res, next) => {
const { userId, createdAt } = req.decodedToken
const isBlacklisted = await blacklist.isUserBlacklisted(userId, createdAt)
if (isBlacklisted) {
res.status(401).json({ error: 'Token needs to be refreshed - blacklisted' })
} else {
next()
return res.status(401).json({ error: 'Token needs to be refreshed - blacklisted' })
}
next()
}
module.exports = {
@@ -57,7 +57,7 @@ Credit.belongsToMany(Teacher, { through: CreditTeacher, foreignKey: 'credit_id'
Teacher.belongsToMany(Credit, { through: CreditTeacher, foreignKey: 'teacher_id' })
Organization.hasMany(Studyright, { foreignKey: 'facultyCode', sourceKey: 'code' })
Studyright.belongsTo(Organization, { foreignKey: 'facultyCode', sourceKey: 'code' })
Studyright.belongsTo(Organization, { foreignKey: 'facultyCode', targetKey: 'code' })
Studyright.belongsTo(Student, { foreignKey: 'studentStudentnumber', targetKey: 'studentnumber' })
Student.hasMany(Studyright, { foreignKey: 'studentStudentnumber', sourceKey: 'studentnumber' })
@@ -1,7 +1,6 @@
const shibbolethHeadersFix = require('unfuck-utf8-headers-middleware')
const accessLogger = require('./middleware/accesslogger')
const sentryUserId = require('./middleware/sentryUserId')
const log = require('./routes/log')
const courses = require('./routes/courses')
const students = require('./routes/students')
const population = require('./routes/population')
@@ -12,7 +11,6 @@ const users = require('./routes/users')
const elementdetails = require('./routes/elementdetails')
const auth = require('./middleware/auth')
const teachers = require('./routes/teachers')
const usage = require('./routes/usage')
const providers = require('./routes/providers')
const faculties = require('./routes/faculties')
const facultyStats = require('./routes/facultyStats')
@@ -36,7 +34,6 @@ module.exports = (app, url) => {
app.use(url, ping)
app.use(shibbolethHeadersFix(['hyGroupCn', 'SHIB_LOGOUT_URL', 'eduPersonAffiliation', 'uid', 'displayName', 'mail']))
app.use(url, matomoInit)
app.use(url, log)
app.use(url, login)
app.use(`${url}/superlogin`, superlogin)
app.use(auth.checkAuth, auth.checkRequiredGroup, auth.checkUserBlacklisting, accessLogger, sentryUserId)
@@ -55,7 +52,6 @@ module.exports = (app, url) => {
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
app.use(`${url}/users`, auth.roles(['users']), users)
app.use(`${url}/feedback`, feedback)
app.use(`${url}/usage`, auth.roles(['usage']), usage)
app.use(`${url}/course-groups`, auth.roles(['coursegroups']), courseGroups)
app.use(`${url}/mandatory_courses`, mandatoryCourses)
app.use(`${url}/mandatory-course-labels`, mandatoryCourseLabels)
@@ -8,6 +8,8 @@ const {
getStartYears,
getGraduatedStatus
} = require('../services/coolDataScience')
const useSisRouter = require('../util/useSisRouter')
const trendsV2 = require('../routesV2/trends')
router.get('/start-years', async (req, res) => {
const years = await getStartYears()
@@ -76,4 +78,4 @@ router.get(
})
)
module.exports = router
module.exports = useSisRouter(trendsV2, router)
@@ -71,14 +71,15 @@ router.get('/v2/courseyearlystats', async (req, res) => {
router.get('/v3/courseyearlystats', async (req, res) => {
try {
const { rights, roles } = req
const admin = roles && roles.includes('admin')
if (!admin) {
// If user has rights to see at least one programme, then they are allowed
// to see all of them
if (rights.length <= 0) {
return res.status(403).json({ error: 'No programmes so no access to course stats' })
}
const allowedByRoles = roles && ['admin', 'courseStatistics'].some(role => roles.includes(role))
// If user has rights to see at least one programme, then they are allowed
// to see all of them
if (!allowedByRoles && rights.length < 1) {
return res.status(403).json({ error: 'No programmes so no access to course stats' })
}
const { codes, separate: sep, unifyOpenUniCourses: unify } = req.query
const separate = !sep ? false : JSON.parse(sep)
@@ -2,12 +2,7 @@ const router = require('express').Router()
const { getStudentsUserCanAccess } = require('../services/userService')
const { getAllDegreesAndProgrammes, getAllProgrammes, getAllElementDetails } = require('../services/studyrights')
const MandatoryCourses = require('../services/mandatoryCourses')
const {
productivityStatsForCode,
throughputStatsForCode,
bachelorData,
masterData
} = require('../services/studyprogramme')
const { productivityStatsForCode, throughputStatsForCode, optionData } = require('../services/studyprogramme')
const { findProgrammeTheses, createThesisCourse, deleteThesisCourse } = require('../services/thesis')
const {
getProductivity,
@@ -92,10 +87,10 @@ router.get('/v2/studyprogrammes/:id/optiondata', async (req, res) => {
const code = req.params.id
if (code) {
if (code.includes('MH')) {
const data = await bachelorData(new Date('2017-07-31'), new Date(), code)
const data = await optionData(new Date('2017-07-31'), new Date(), code, 'MSC')
return res.json(data)
} else if (code.includes('KH')) {
const data = await masterData(new Date('2017-07-31'), new Date(), code)
const data = await optionData(new Date('2017-07-31'), new Date(), code, 'BSC')
return res.json(data)
} else {
return res.json([])
@@ -1,9 +0,0 @@
const router = require('express').Router()
const logger = require('../util/logger')
router.post('/log', async (req, res) => {
logger.error(req.body.message, req.body['full_message'])
res.status(200).end()
})
module.exports = router
@@ -495,7 +495,7 @@ router.post('/updatedatabase', async (req, res) => {
try {
const response = await updateStudents(studentnumbers)
if (response) {
res.status(200).json('Scheduled')
res.status(200).json({ ...response })
}
} catch (err) {
res.status(500).json(err)
@@ -12,7 +12,13 @@ const {
updateNoStudents,
updateDaily
} = require('../services/updaterService')
const { updateSISMetadata, updateSISStudents, updateSISProgrammes } = require('../services/sisUpdaterService')
const {
updateSISMetadata,
updateSISStudents,
updateSISProgrammes,
updateStudentsByStudentNumber,
abort
} = require('../services/sisUpdaterService')
const { refreshStatistics, refreshStatisticsV2 } = require('../events')
router.post('/update/oldest', async (req, res) => {
@@ -24,7 +30,7 @@ router.post('/update/oldest', async (req, res) => {
try {
const response = await updateOldestStudents(amount)
if (response) {
res.status(200).json('Scheduled')
res.status(200).json({ ...response })
}
} catch (err) {
res.status(500).json(err)
@@ -35,7 +41,7 @@ router.post('/update/all', async (req, res) => {
try {
const response = await updateAllStudents()
if (response) {
res.status(200).json('Scheduled')
res.status(200).json({ ...response })
}
} catch (err) {
res.status(500).json(err)
@@ -46,7 +52,7 @@ router.post('/update/active', async (req, res) => {
try {
const response = await updateActiveStudents()
if (response) {
res.status(200).json('Scheduled')
res.status(200).json({ ...response })
}
} catch (err) {
res.status(500).json(err)
@@ -57,7 +63,7 @@ router.post('/update/no_student', async (req, res) => {
try {
const response = await updateNoStudents()
if (response) {
res.status(200).json('Scheduled')
res.status(200).json({ ...response })
}
} catch (err) {
res.status(500).json(err)
@@ -68,7 +74,7 @@ router.post('/update/attainment', async (req, res) => {
try {
const response = await updateAttainments()
if (response) {
res.status(200).json('Scheduled')
res.status(200).json({ ...response })
}
} catch (err) {
res.status(500).json(err)
@@ -79,7 +85,7 @@ router.post('/update/meta', async (req, res) => {
try {
const response = await updateMetadata()
if (response) {
res.status(200).json('Scheduled')
res.status(200).json({ ...response })
}
} catch (err) {
res.status(500).json(err)
@@ -90,7 +96,7 @@ router.post('/update/studentlist', async (req, res) => {
try {
const response = await updateStudentlist()
if (response) {
res.status(200).json('Scheduled')
res.status(200).json({ ...response })
}
} catch (err) {
res.status(500).json(err)
@@ -111,7 +117,7 @@ router.post('/reschedule/scheduled', async (req, res) => {
try {
const response = await rescheduleScheduled()
if (response) {
res.status(200).json('Scheduled')
res.status(200).json({ ...response })
}
} catch (err) {
res.status(500).json(err)
@@ -122,7 +128,7 @@ router.post('/reschedule/fetched', async (req, res) => {
try {
const response = await rescheduleFetched()
if (response) {
res.status(200).json('Scheduled')
res.status(200).json({ ...response })
}
} catch (err) {
res.status(500).json(err)
@@ -133,7 +139,7 @@ router.post('/update/daily', async (req, res) => {
try {
const response = await updateDaily()
if (response) {
res.status(200).json('Scheduled')
res.status(200).json({ ...response })
}
} catch (err) {
res.status(500).json(err)
@@ -170,7 +176,22 @@ router.get('/update/v2/students', async (req, res) => {
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
try {
const response = await updateSISStudents(req)
const response = await updateSISStudents()
if (response) {
res.status(200).json('Update SIS students scheduled')
}
} catch (err) {
console.log(err)
res.status(500).json({ error: 'error' })
}
})
router.post('/update/v2/students', async (req, res) => {
const { roles } = req
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
try {
const response = await updateStudentsByStudentNumber(req.body)
if (response) {
res.status(200).json('Update SIS students scheduled')
}
@@ -205,4 +226,14 @@ router.post('/refresh_statistic_v2', async (req, res) => {
}
})
router.get('/abort', async (req, res) => {
try {
await abort()
res.status(200).json()
} catch (err) {
console.log(err)
res.status(500).json({ error: 'error' })
}
})
module.exports = router
@@ -1,14 +0,0 @@
const router = require('express').Router()
const UsageService = require('../services/usageService')
const S_TO_MS = 1000
router.get('/', async (req, res) => {
const from = req.query.from ? new Date(req.query.from) : new Date(1 * S_TO_MS)
const to = req.query.to ? new Date(req.query.to) : new Date(new Date().getTime() + 60 * S_TO_MS)
const resultStream = await UsageService.getStream(from, to)
res.contentType('json')
resultStream.pipe(res)
})
module.exports = router
@@ -2,7 +2,11 @@ const router = require('express').Router()
const { getStudentsUserCanAccess } = require('../servicesV2/userService')
//const { getAllDegreesAndProgrammes, getAllProgrammes, getAllElementDetails } = require('../services/studyrights')
const MandatoryCourses = require('../servicesV2/mandatoryCourses')
const { productivityStatsForStudytrack, throughputStatsForStudytrack } = require('../servicesV2/studytrack')
const {
productivityStatsForStudytrack,
throughputStatsForStudytrack,
optionData
} = require('../servicesV2/studyprogramme')
const { findProgrammeTheses /* , createThesisCourse, deleteThesisCourse */ } = require('../services/thesis')
const {
@@ -36,6 +40,22 @@ const programmeStatsSince = new Date('2017-07-31')
}
}) */
router.get('/v2/studyprogrammes/:id/optiondata', async (req, res) => {
const code = req.params.id
let level
if (code.includes('MH')) {
level = 'MSC'
} else if (code.includes('KH')) {
level = 'BSC'
} else {
return res.json([])
}
const data = await optionData(new Date('2017-07-31'), new Date(), code, level)
res.json(data)
})
router.get('/v2/studyprogrammes/:id/present_students', async (req, res) => {
try {
const {
@@ -214,6 +214,7 @@ router.post('/v2/populationstatistics/coursesbystudentnumberlist', async (req, r
router.get('/v3/populationstatistics', async (req, res) => {
const { year, semesters, studyRights: studyRightsJSON } = req.query
const { decodedToken } = req
try {
if (!year || !semesters || !studyRightsJSON) {
res.status(400).json({ error: 'The query should have a year, semester and studyRights defined' })
@@ -0,0 +1,79 @@
const router = require('express').Router()
const {
withErr,
getProtoC,
getProtoCProgramme,
getStatus,
getUber,
getStartYears,
getGraduatedStatus
} = require('../servicesV2/trends')
router.get('/start-years', async (req, res) => {
const years = await getStartYears()
res.json(years.map(({ studystartdate }) => studystartdate))
})
router.get(
'/uber-data',
withErr(async (req, res) => {
const data = await getUber(req.query)
res.json(data)
})
)
router.get(
'/proto-c-data-programme',
withErr(async (req, res) => {
const data = await getProtoCProgramme(req.query)
res.json(data)
})
)
router.get(
'/proto-c-data',
withErr(async (req, res) => {
const data = await getProtoC(req.query)
res.json(data)
})
)
router.get(
'/status-graduated',
withErr(async (req, res) => {
const { date: unixMillis, showByYear } = req.query
const date = new Date(Number(unixMillis))
const endOfToday = new Date()
endOfToday.setHours(23, 59, 59, 999)
if (isNaN(date.getTime()) || date.getTime() > endOfToday.getTime()) {
return res.status(400).json({ error: 'Invalid date' })
}
// End of day
date.setHours(23, 59, 59, 999)
const data = await getGraduatedStatus(date.getTime(), showByYear)
res.json(data)
})
)
router.get(
'/status',
withErr(async (req, res) => {
const { date: unixMillis, showByYear } = req.query
const date = new Date(Number(unixMillis))
const endOfToday = new Date()
endOfToday.setHours(23, 59, 59, 999)
if (isNaN(date.getTime()) || date.getTime() > endOfToday.getTime()) {
return res.status(400).json({ error: 'Invalid date' })
}
// End of day
date.setHours(23, 59, 59, 999)
const status = await getStatus(date.getTime(), showByYear)
res.json(status)
})
)
module.exports = router
@@ -11,9 +11,9 @@ const addUserToBlacklist = async userId => {
const isUserBlacklisted = async (userId, tokenCreatedAt) => {
const isBlacklisted = async () => {
const blacklistTimestamp = await redisClient.getAsync(MAKE_REDIS_HASH_KEY(userId))
const status = !!(tokenCreatedAt && blacklistTimestamp && moment(tokenCreatedAt) < moment(blacklistTimestamp))
return status
return tokenCreatedAt && blacklistTimestamp && moment(tokenCreatedAt) < moment(blacklistTimestamp)
}
const timer = () => new Promise(resolve => setTimeout(() => resolve(true), 5000))
return Promise.race([isBlacklisted(), timer()])
}
@@ -737,7 +737,7 @@ const yearlyStatsOfNew = async (coursecode, separate, unifyOpenUniCourses) => {
...statistics,
coursecode,
alternatives: uniqueCodes,
name: course.name.fi
name: course.name
}
}
@@ -55,7 +55,8 @@ const formatStudentForPopulationStatistics = (
gender_sv,
gender_en,
tags,
option
option,
birthdate
},
credits,
startDate,
@@ -72,13 +73,15 @@ const formatStudentForPopulationStatistics = (
.toISOString()
: attainment_date
const passed = Credit.passed({ credittypecode })
return {
course_code,
date: attainment_date_normailized,
passed,
grade: passed ? grade : 'Hyl.',
credits,
isStudyModuleCredit: isStudyModule
isStudyModuleCredit: isStudyModule,
credittypecode
}
}
@@ -103,7 +106,8 @@ const formatStudentForPopulationStatistics = (
tags: tags || [],
studyrightStart: startDate,
starting: moment(started).isBetween(startDateMoment, endDateMoment, null, '[]'),
option
option,
birthdate
}
}
@@ -202,7 +206,8 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'gender_code',
'gender_fi',
'gender_sv',
'gender_en'
'gender_en',
'birthdate'
],
include: [
{
@@ -220,7 +225,8 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'graduated',
'canceldate',
'prioritycode',
'faculty_code'
'faculty_code',
'studystartdate'
],
separate: true,
include: [
@@ -550,11 +556,28 @@ const parseQueryParams = query => {
}
}
const getMastersForStudents = async (students, code) => {
const getOptionsForStudents = async (students, code, level) => {
if (!code || !students.length) return {}
let graduated
let currentExtent
let optionExtent
if (level === 'BSC') {
graduated = { graduated: 1 }
currentExtent = 1
optionExtent = 2
} else if (level === 'MSC') {
graduated = {}
currentExtent = 2
optionExtent = 1
} else {
throw new Error('Invalid study level ' + level)
}
const programmes = await getAllProgrammes()
const bachelors = await Studyright.findAll({
const currentStudyrights = await Studyright.findAll({
include: [
{
model: StudyrightElement,
@@ -567,20 +590,21 @@ const getMastersForStudents = async (students, code) => {
}
],
where: {
graduated: 1,
extentcode: 1,
...graduated,
extentcode: currentExtent,
student_studentnumber: {
[Op.in]: students
}
},
attributes: ['student_studentnumber', 'givendate']
})
const bachelorsMap = bachelors.reduce((obj, studyright) => {
const currentStudyrightsMap = currentStudyrights.reduce((obj, studyright) => {
obj[studyright.student_studentnumber] = studyright.givendate
return obj
}, {})
const masters = await Studyright.findAll({
const options = await Studyright.findAll({
include: [
{
model: StudyrightElement,
@@ -602,7 +626,7 @@ const getMastersForStudents = async (students, code) => {
}
],
where: {
extentcode: 2,
extentcode: optionExtent,
student_studentnumber: {
[Op.in]: students
}
@@ -610,80 +634,10 @@ const getMastersForStudents = async (students, code) => {
order: [[StudyrightElement, 'startdate', 'DESC']],
attributes: ['student_studentnumber', 'givendate']
})
return masters
.filter(m => m.student_studentnumber in bachelorsMap)
.filter(m => m.givendate.getTime() === bachelorsMap[m.student_studentnumber].getTime())
.reduce((obj, element) => {
obj[element.student_studentnumber] = {
code: element.studyright_elements[0].code,
name: element.studyright_elements[0].element_detail.name
}
return obj
}, {})
}
const getBachelorsForStudents = async (students, code) => {
if (!code || !students.length) return {}
const programmes = await getAllProgrammes()
const masters = await Studyright.findAll({
include: [
{
model: StudyrightElement,
where: {
studentnumber: {
[Op.in]: students
},
code: code
}
}
],
where: {
extentcode: 2,
student_studentnumber: {
[Op.in]: students
}
},
attributes: ['student_studentnumber', 'givendate']
})
const mastersMap = masters.reduce((obj, studyright) => {
obj[studyright.student_studentnumber] = studyright.givendate
return obj
}, {})
const bachelors = await Studyright.findAll({
include: [
{
model: StudyrightElement,
where: {
studentnumber: {
[Op.in]: students
},
code: {
[Op.in]: programmes.map(p => p.code)
}
},
include: [
{
model: ElementDetails,
attributes: ['name']
}
],
attributes: ['code', 'startdate']
}
],
where: {
extentcode: 1,
student_studentnumber: {
[Op.in]: students
}
},
order: [[StudyrightElement, 'startdate', 'DESC']],
attributes: ['student_studentnumber', 'givendate']
})
return bachelors
.filter(b => b.student_studentnumber in mastersMap)
.filter(b => b.givendate.getTime() === mastersMap[b.student_studentnumber].getTime())
return options
.filter(m => m.student_studentnumber in currentStudyrightsMap)
.filter(m => m.givendate.getTime() === currentStudyrightsMap[m.student_studentnumber].getTime())
.reduce((obj, element) => {
obj[element.student_studentnumber] = {
code: element.studyright_elements[0].code,
@@ -786,19 +740,15 @@ const formatQueryParamArrays = (query, params) => {
const optimizedStatisticsOf = async (query, studentnumberlist) => {
const formattedQueryParams = formatQueryParamArrays(query, ['semesters', 'studentStatuses'])
if (
!formattedQueryParams.semesters.map(semester => semester === 'FALL' || semester === 'SPRING').every(e => e === true)
) {
if (!formattedQueryParams.semesters.every(semester => semester === 'FALL' || semester === 'SPRING')) {
return { error: 'Semester should be either SPRING OR FALL' }
}
if (
formattedQueryParams.studentStatuses &&
!formattedQueryParams.studentStatuses
.map(
status => status === 'CANCELLED' || status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
)
.every(e => e === true)
!formattedQueryParams.studentStatuses.every(
status => status === 'CANCELLED' || status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
)
) {
return { error: 'Student status should be either CANCELLED or EXCHANGE or NONDEGREE or TRANSFERRED' }
}
@@ -829,12 +779,13 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
transferredStudents
)
// wtf
// plz
const code = studyRights[0] || ''
let optionData = {}
if (code.includes('MH')) {
optionData = await getBachelorsForStudents(studentnumbers, code)
optionData = await getOptionsForStudents(studentnumbers, code, 'MSC')
} else if (code.includes('KH')) {
optionData = await getMastersForStudents(studentnumbers, code)
optionData = await getOptionsForStudents(studentnumbers, code, 'BSC')
}
const { students, credits, extents, semesters, elementdetails, courses } = await getStudentsIncludeCoursesBetween(
@@ -8,6 +8,12 @@ const updateSISStudents = async () => {
return response.data
}
const updateStudentsByStudentNumber = async studentnumbers => {
const data = { studentnumbers }
const response = await client.post('/v1/students', data, { params: { token: SECRET_TOKEN } })
return response.data
}
const updateSISMetadata = async () => {
const response = await client.get('/v1/meta', { params: { token: SECRET_TOKEN } })
return response.data
@@ -18,8 +24,15 @@ const updateSISProgrammes = async () => {
return response.data
}
const abort = async () => {
const response = await client.get('/v1/abort', { params: { token: SECRET_TOKEN } })
return response.data
}
module.exports = {
updateSISMetadata,
updateSISStudents,
updateSISProgrammes
updateSISProgrammes,
updateStudentsByStudentNumber,
abort
}
@@ -517,11 +517,27 @@ const startedStudyright = async (studentnumbers, startDate, studytrack, endDate)
})
}
const masterData = async (startDate, endDate, code) => {
const optionData = async (startDate, endDate, code, level) => {
const programmes = await getAllProgrammes()
const students = await studentnumbersWithAllStudyrightElements([code], startDate, endDate, false, true)
const bachelors = await Studyright.findAll({
let graduated
let currentExtent
let optionExtent
if (level === 'BSC') {
graduated = { graduated: 1 }
currentExtent = 1
optionExtent = 2
} else if (level === 'MSC') {
graduated = {}
currentExtent = 2
optionExtent = 1
} else {
throw new Error('Invalid study level ' + level)
}
const currentStudyrights = await Studyright.findAll({
include: [
{
model: StudyrightElement,
@@ -532,8 +548,8 @@ const masterData = async (startDate, endDate, code) => {
}
],
where: {
graduated: 1,
extentcode: 1,
...graduated,
extentcode: currentExtent,
student_studentnumber: {
[Op.in]: students
}
@@ -541,7 +557,7 @@ const masterData = async (startDate, endDate, code) => {
attributes: ['student_studentnumber', 'givendate', 'studystartdate']
})
const bachelorsMap = bachelors
const currentStudyrightsMap = currentStudyrights
.filter(b => b.studystartdate)
.reduce((obj, studyright) => {
const acualDate = new Date(Math.max(+studyright.studystartdate, +studyright.studyright_elements[0].startdate))
@@ -549,7 +565,7 @@ const masterData = async (startDate, endDate, code) => {
return obj
}, {})
const masters = await Studyright.findAll({
const options = await Studyright.findAll({
include: [
{
model: StudyrightElement,
@@ -571,7 +587,7 @@ const masterData = async (startDate, endDate, code) => {
}
],
where: {
extentcode: 2,
extentcode: optionExtent,
student_studentnumber: {
[Op.in]: students
}
@@ -583,11 +599,11 @@ const masterData = async (startDate, endDate, code) => {
const data = {}
const years = new Set()
masters
.filter(m => m.student_studentnumber in bachelorsMap)
.filter(m => m.givendate.getTime() === bachelorsMap[m.student_studentnumber].givendate.getTime())
options
.filter(m => m.student_studentnumber in currentStudyrightsMap)
.filter(m => m.givendate.getTime() === currentStudyrightsMap[m.student_studentnumber].givendate.getTime())
.forEach(b => {
const date = bachelorsMap[b.student_studentnumber].startdate
const date = currentStudyrightsMap[b.student_studentnumber].startdate
const year =
date.getMonth() > 6 || (date.getMonth() === 6 && date.getDate() == 31)
? date.getFullYear()
@@ -606,103 +622,7 @@ const masterData = async (startDate, endDate, code) => {
data[code][year] += 1
data[code].total += 1
})
const dataAsArray = Object.keys(data).map(code => {
const { name, total } = data[code]
const output = { code, name, total }
years.forEach(y => (output[y] = data[code][y] || 0))
return output
})
return { data: dataAsArray, years: Array.from(years).sort() }
}
const bachelorData = async (startDate, endDate, code) => {
const programmes = await getAllProgrammes()
const students = await studentnumbersWithAllStudyrightElements([code], startDate, endDate, false, true)
const masters = await Studyright.findAll({
include: [
{
model: StudyrightElement,
where: {
code: code
},
attributes: ['startdate']
}
],
where: {
extentcode: 2,
student_studentnumber: {
[Op.in]: students
}
},
attributes: ['student_studentnumber', 'givendate', 'studystartdate']
})
const mastersMap = masters
.filter(m => m.studystartdate)
.reduce((obj, studyright) => {
const acualDate = new Date(Math.max(+studyright.studystartdate, +studyright.studyright_elements[0].startdate))
obj[studyright.student_studentnumber] = { givendate: studyright.givendate, startdate: acualDate }
return obj
}, {})
const bachelors = await Studyright.findAll({
include: [
{
model: StudyrightElement,
where: {
studentnumber: {
[Op.in]: students
},
code: {
[Op.in]: programmes.map(p => p.code)
}
},
include: [
{
model: ElementDetails,
attributes: ['name']
}
],
attributes: ['code']
}
],
where: {
extentcode: 1,
student_studentnumber: {
[Op.in]: students
}
},
order: [[StudyrightElement, 'startdate', 'DESC']],
attributes: ['student_studentnumber', 'startdate', 'givendate']
})
const data = {}
const years = new Set()
bachelors
.filter(b => b.student_studentnumber in mastersMap)
.filter(b => b.givendate.getTime() === mastersMap[b.student_studentnumber].givendate.getTime())
.forEach(b => {
const date = mastersMap[b.student_studentnumber].startdate
const year =
date.getMonth() > 6 || (date.getMonth() === 6 && date.getDate() == 31)
? date.getFullYear()
: date.getFullYear() - 1
years.add(year)
const code = b.studyright_elements[0].code
if (!data[code]) {
data[code] = {}
data[code].name = b.studyright_elements[0].element_detail.name
data[code].total = 0
}
if (!data[code][year]) {
data[code][year] = 0
}
data[code][year] += 1
data[code].total += 1
})
const dataAsArray = Object.keys(data).map(code => {
const { name, total } = data[code]
const output = { code, name, total }
@@ -984,6 +904,5 @@ module.exports = {
findProgrammeThesisCredits,
thesisProductivityFromCredits,
thesisProductivityForStudytrack,
bachelorData,
masterData
optionData
}
@@ -1,26 +0,0 @@
const axios = require('axios')
const { USAGESERVICE_URL } = require('../conf-backend')
/** @type {axios.AxiosInstance} */
const client = axios.create({ baseURL: USAGESERVICE_URL, headers: { secret: process.env.USAGESERVICE_SECRET } })
const ping = async () => {
const url = '/ping'
await client.get(url)
}
const log = async (message, meta) => {
await client.post('/log', { message, meta })
}
/** @returns {Promise<stream.Readable>} */
const getStream = async (from, to) => {
const res = await client.get('/log', { params: { from, to }, responseType: 'stream' })
return res.data
}
module.exports = {
ping,
log,
getStream
}
@@ -734,7 +734,7 @@ const yearlyStatsOfNew = async (coursecode, separate, unifyOpenUniCourses) => {
...statistics,
coursecode,
alternatives: uniqueCodes,
name: course.name.fi
name: course.name
}
}
@@ -1,6 +1,7 @@
const { Op } = require('sequelize')
const moment = require('moment')
const { sortBy } = require('lodash')
const { getCurrentSemester } = require('./semesters')
const {
Student,
@@ -23,6 +24,7 @@ const { Tag, TagStudent } = require('../models/models_kone')
const { getCodeToMainCourseMap, unifyOpenUniversity } = require('./courses')
const { CourseStatsCounter } = require('./course_stats_counter')
const { getPassingSemester, semesterEnd, semesterStart } = require('../util/semester')
const { getAllProgrammes } = require('./studyrights')
const enrolmentDates = () => {
const query = 'SELECT DISTINCT s.dateOfUniversityEnrollment as date FROM Student s'
@@ -55,7 +57,9 @@ const formatStudentForPopulationStatistics = (
gender_fi,
gender_sv,
gender_en,
tags
tags,
option,
birthdate
},
credits,
startDate,
@@ -71,14 +75,16 @@ const formatStudentForPopulationStatistics = (
.add(1, 'day')
.toISOString()
: attainment_date
const passed = Credit.passed({ credittypecode })
return {
course_code,
date: attainment_date_normailized,
passed: Credit.passed({ credittypecode }),
grade,
passed,
grade: passed ? grade : 'Hyl.',
credits,
isStudyModuleCredit: isStudyModule
isStudyModuleCredit: isStudyModule,
credittypecode
}
}
@@ -102,7 +108,9 @@ const formatStudentForPopulationStatistics = (
updatedAt: updatedAt || createdAt,
tags: tags || [],
studyrightStart: startDate,
starting: moment(started).isBetween(startDateMoment, endDateMoment, null, '[]')
starting: moment(started).isBetween(startDateMoment, endDateMoment, null, '[]'),
option,
birthdate
}
}
@@ -198,7 +206,8 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'abbreviatedname',
'email',
'updatedAt',
'gender_code'
'gender_code',
'birthdate'
/* 'gender_fi',
'gender_sv',
'gender_en' */
@@ -214,11 +223,13 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
attributes: [
'studyrightid',
'startdate',
'enddate',
'extentcode',
'graduated',
'canceldate',
'prioritycode',
'faculty_code'
'faculty_code',
'studystartdate'
],
separate: true,
include: [
@@ -359,6 +370,7 @@ const studentnumbersWithAllStudyrightElements = async (
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
tag
) => {
// eslint-disable-line
@@ -378,6 +390,7 @@ const studentnumbersWithAllStudyrightElements = async (
if (!nondegreeStudents) {
filteredExtents.push(33, 99, 14, 13)
}
// this does not work in SIS, that is why we have a kludge below
if (!cancelledStudents) {
studyrightWhere.canceldate = null
}
@@ -421,11 +434,16 @@ const studentnumbersWithAllStudyrightElements = async (
[Op.ne]: 20
}
},
{
['$studyright_elements.startdate$']: {
sequelize.where(
sequelize.fn(
'GREATEST',
sequelize.col('studyright_elements.startdate'),
sequelize.col('studyright.studystartdate')
),
{
[Op.between]: [formattedStartDate, endDate]
}
}
)
],
...studyrightWhere
},
@@ -434,13 +452,56 @@ const studentnumbersWithAllStudyrightElements = async (
raw: true
})
const studentnumbers = [...new Set(students.map(s => s.student_studentnumber))]
let studentnumbers = [...new Set(students.map(s => s.student_studentnumber))]
// study right cancel does not work in SIS, but the below kludge should do the trick
// since canceldate != null === student does not have any enrolment
if (!cancelledStudents) {
const { semestercode } = await getCurrentSemester()
const enrolments = await SemesterEnrollment.findAll({
where: {
studentnumber: {
[Op.in]: studentnumbers
},
semestercode,
enrollment_date: {
[Op.not]: null
}
}
})
const enrolledStudentnumbers = enrolments.map(e => e.studentnumber)
studentnumbers = enrolledStudentnumbers
}
// bit hacky solution, but this is used to filter out studentnumbers who have since changed studytracks
const rights = await Studyright.findAll({
attributes: ['studyrightid'],
where: {
student_studentnumber: {
[Op.in]: studentnumbers
}
},
include: {
attributes: [],
model: StudyrightElement,
where: {
code: {
[Op.in]: studyRights
}
}
},
group: ['studyright.studyrightid'],
having: count('studyright_elements.id', studyRights.length, true),
raw: true
})
// bit hacky solution, but this is used to filter out studentnumbers who have since changed studytracks
const allStudytracksForStudents = await StudyrightElement.findAll({
where: {
studentnumber: {
[Op.in]: studentnumbers
studyrightid: {
[Op.in]: rights.map(r => r.studyrightid)
}
},
include: {
@@ -460,11 +521,35 @@ const studentnumbersWithAllStudyrightElements = async (
}, {})
const filteredStudentnumbers = studentnumbers.filter(studentnumber => {
const newestStudytrack = sortBy(formattedStudytracks[studentnumber], 'startdate').reverse()[0]
const newestStudytrack = sortBy(formattedStudytracks[studentnumber], ['enddate', 'startdate']).reverse()[0]
if (!newestStudytrack) return false
return studyRights.includes(newestStudytrack.code)
})
return filteredStudentnumbers.length > 0 ? filteredStudentnumbers : studentnumbers
const studentnumberlist = filteredStudentnumbers.length > 0 ? filteredStudentnumbers : studentnumbers
// fetch students that have transferred out of the programme and filter out these studentnumbers
if (!transferredStudents) {
const transferredOutStudents = await Transfer.findAll({
attributes: ['studentnumber'],
where: {
sourcecode: {
[Op.in]: studyRights
},
transferdate: {
[Op.gt]: startDate
},
studentnumber: {
[Op.in]: studentnumberlist
}
},
raw: true
}).map(s => s.studentnumber)
const notTransferredStudents = studentnumberlist.filter(sn => !transferredOutStudents.includes(sn))
return notTransferredStudents
}
return studentnumberlist
}
const parseQueryParams = query => {
@@ -482,10 +567,13 @@ const parseQueryParams = query => {
const exchangeStudents = studentStatuses && studentStatuses.includes('EXCHANGE')
const cancelledStudents = studentStatuses && studentStatuses.includes('CANCELLED')
const nondegreeStudents = studentStatuses && studentStatuses.includes('NONDEGREE')
const transferredStudents = studentStatuses && studentStatuses.includes('TRANSFERRED')
return {
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
// if someone passes something falsy like null as the studyright, remove it so it doesn't break
// the sequelize query
studyRights: (Array.isArray(studyRights) ? studyRights : Object.values(studyRights)).filter(Boolean),
@@ -496,11 +584,103 @@ const parseQueryParams = query => {
}
}
const getOptionsForStudents = async (students, code, level) => {
if (!code || !students.length) return {}
let graduated
let currentExtent
let optionExtent
if (level === 'BSC') {
graduated = { graduated: 1 }
currentExtent = 1
optionExtent = 2
} else if (level === 'MSC') {
graduated = {}
currentExtent = 2
optionExtent = 1
} else {
throw new Error('Invalid study level ' + level)
}
const programmes = await getAllProgrammes()
const currentStudyrights = await Studyright.findAll({
include: [
{
model: StudyrightElement,
where: {
studentnumber: {
[Op.in]: students
},
code: code
}
}
],
where: {
...graduated,
extentcode: currentExtent,
student_studentnumber: {
[Op.in]: students
}
},
attributes: ['studentStudentnumber', 'givendate']
})
const currentStudyrightsMap = currentStudyrights.reduce((obj, studyright) => {
obj[studyright.studentStudentnumber] = studyright.givendate
return obj
}, {})
const options = await Studyright.findAll({
include: [
{
model: StudyrightElement,
where: {
studentnumber: {
[Op.in]: students
},
code: {
[Op.in]: programmes.map(p => p.code)
}
},
include: [
{
model: ElementDetail,
attributes: ['name']
}
],
attributes: ['code', 'startdate']
}
],
where: {
extentcode: optionExtent,
student_studentnumber: {
[Op.in]: students
}
},
order: [[StudyrightElement, 'startdate', 'DESC']],
attributes: ['studentStudentnumber', 'givendate']
})
return options
.filter(m => m.studentStudentnumber in currentStudyrightsMap)
.filter(m => m.givendate.getTime() === currentStudyrightsMap[m.studentStudentnumber].getTime())
.reduce((obj, element) => {
obj[element.studentStudentnumber] = {
code: element.studyright_elements[0].code,
name: element.studyright_elements[0].element_detail.name
}
return obj
}, {})
}
const formatStudentsForApi = async (
{ students, credits, extents, semesters, elementdetails, courses },
startDate,
endDate,
{ studyRights }
{ studyRights },
optionData
) => {
const startDateMoment = moment(startDate)
const endDateMoment = moment(endDate)
@@ -528,7 +708,8 @@ const formatStudentsForApi = async (
stats.transfers.targets[transfer.targetcode] = target
stats.transfers.sources[transfer.sourcecode] = source
})
if (student.studentnumber in optionData) student.option = optionData[student.studentnumber]
else student.option = null
stats.students.push(
formatStudentForPopulationStatistics(student, credits, startDate, endDate, startDateMoment, endDateMoment)
)
@@ -587,19 +768,17 @@ const formatQueryParamArrays = (query, params) => {
const optimizedStatisticsOf = async (query, studentnumberlist) => {
const formattedQueryParams = formatQueryParamArrays(query, ['semesters', 'studentStatuses'])
if (
!formattedQueryParams.semesters.map(semester => semester === 'FALL' || semester === 'SPRING').every(e => e === true)
) {
if (!formattedQueryParams.semesters.every(semester => semester === 'FALL' || semester === 'SPRING')) {
return { error: 'Semester should be either SPRING OR FALL' }
}
if (
formattedQueryParams.studentStatuses &&
!formattedQueryParams.studentStatuses
.map(status => status === 'CANCELLED' || status === 'EXCHANGE' || status === 'NONDEGREE')
.every(e => e === true)
!formattedQueryParams.studentStatuses.every(
status => status === 'CANCELLED' || status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
)
) {
return { error: 'Student status should be either CANCELLED or EXCHANGE or NONDEGREE' }
return { error: 'Student status should be either CANCELLED or EXCHANGE or NONDEGREE or TRANSFERRED' }
}
const {
studyRights,
@@ -609,6 +788,7 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
tag
} = parseQueryParams(formattedQueryParams)
@@ -623,8 +803,19 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
endDate,
exchangeStudents,
cancelledStudents,
nondegreeStudents
nondegreeStudents,
transferredStudents
)
// wtf
// plz
const code = studyRights[0] || ''
let optionData = {}
if (code.includes('MH')) {
optionData = await getOptionsForStudents(studentnumbers, code, 'MSC')
} else if (code.includes('KH')) {
optionData = await getOptionsForStudents(studentnumbers, code, 'BSC')
}
const { students, credits, extents, semesters, elementdetails, courses } = await getStudentsIncludeCoursesBetween(
studentnumbers,
startDate,
@@ -637,8 +828,10 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
{ students, credits, extents, semesters, elementdetails, courses },
startDate,
endDate,
formattedQueryParams
formattedQueryParams,
optionData
)
return formattedStudents
}
@@ -693,15 +886,25 @@ const parseCreditInfo = credit => ({
const bottlenecksOf = async (query, studentnumberlist) => {
const isValidRequest = async (query, params) => {
const { studyRights, startDate, endDate, exchangeStudents, cancelledStudents, nondegreeStudents } = params
const {
studyRights,
startDate,
endDate,
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents
} = params
if (!query.semesters.every(semester => semester === 'FALL' || semester === 'SPRING')) {
return { error: 'Semester should be either SPRING OR FALL' }
}
if (
query.studentStatuses &&
!query.studentStatuses.every(status => status === 'CANCELLED' || status === 'EXCHANGE' || status === 'NONDEGREE')
!query.studentStatuses.every(
status => status === 'CANCELLED' || status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
)
) {
return { error: 'Student status should be either CANCELLED or EXCHANGE or NONDEGREE' }
return { error: 'Student status should be either CANCELLED or EXCHANGE or NONDEGREE or TRANSFERRED' }
}
if (query.selectedStudents) {
const allStudents = await studentnumbersWithAllStudyrightElements(
@@ -711,6 +914,7 @@ const bottlenecksOf = async (query, studentnumberlist) => {
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
query.tag
)
const disallowedRequest = checkThatSelectedStudentsAreUnderRequestedStudyright(
@@ -731,6 +935,7 @@ const bottlenecksOf = async (query, studentnumberlist) => {
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
tag
} = params
const studentnumbers =
@@ -742,6 +947,7 @@ const bottlenecksOf = async (query, studentnumberlist) => {
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
tag
))
const allstudents = studentnumbers.reduce((numbers, num) => {
@@ -6,7 +6,10 @@ const { dbConnections: sisConnections } = require('../databaseV2/connection')
const { sequelizeKone } = require('../database/connection')
const byProgrammeCode = async code => {
// TODO use only sis connection once sis is stable
const connection = sisConnections.established ? sisConnections.sequelize : sequelizeKone
// recursively get a module and all of its children and all of the children of the children...
const [result] = await connection.query(
`
WITH RECURSIVE children as (
@@ -30,13 +33,14 @@ const byProgrammeCode = async code => {
}
})
// this labels the modules to match the old system in frontend
const labeled = result.map(module => {
const label = {
id: module.label_name.fi,
label: `${module.label_code}\n${module.label_name.fi}`,
orderNumber: module.module_order
}
// bit hacky
// 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 }
@@ -47,10 +51,6 @@ const byProgrammeCode = async code => {
}
const addExcludedCourses = async (programmecode, coursecodes) => {
// return ExcludedCourse.create({
// programme_code: programmecode,
// course_code: coursecode
// })
return ExcludedCourse.bulkCreate(coursecodes.map(c => ({ programme_code: programmecode, course_code: c })))
}
// just copy pasted from above since almost same query
@@ -12,7 +12,7 @@ const {
ElementDetail,
Transfer
} = require('../modelsV2')
const { getAssociations } = require('./studyrights')
const { getAssociations, getAllProgrammes } = require('./studyrights')
const { ThesisCourse, ThesisTypeEnums } = require('../models/models_kone')
const { studentnumbersWithAllStudyrightElements } = require('./populations')
const { semesterStart, semesterEnd } = require('../util/semester')
@@ -506,6 +506,121 @@ const startedStudyright = async (studentnumbers, startDate, studytrack, endDate)
})
}
const optionData = async (startDate, endDate, code, level) => {
const programmes = await getAllProgrammes()
const students = await studentnumbersWithAllStudyrightElements([code], startDate, endDate, false, true)
let graduated
let currentExtent
let optionExtent
if (level === 'BSC') {
graduated = { graduated: 1 }
currentExtent = 1
optionExtent = 2
} else if (level === 'MSC') {
graduated = {}
currentExtent = 2
optionExtent = 1
} else {
throw new Error('Invalid study level ' + level)
}
const currentStudyrights = await Studyright.findAll({
include: [
{
model: StudyrightElement,
where: {
code: code
},
attributes: ['startdate']
}
],
where: {
...graduated,
extentcode: currentExtent,
student_studentnumber: {
[Op.in]: students
}
},
attributes: ['studentStudentnumber', 'givendate', 'studystartdate']
})
const currentStudyrightsMap = currentStudyrights
.filter(b => b.studystartdate)
.reduce((obj, studyright) => {
const acualDate = new Date(Math.max(+studyright.studystartdate, +studyright.studyright_elements[0].startdate))
obj[studyright.studentStudentnumber] = { givendate: studyright.givendate, startdate: acualDate }
return obj
}, {})
const options = await Studyright.findAll({
include: [
{
model: StudyrightElement,
where: {
studentnumber: {
[Op.in]: students
},
code: {
[Op.in]: programmes.map(p => p.code)
}
},
include: [
{
model: ElementDetail,
attributes: ['name']
}
],
attributes: ['code']
}
],
where: {
extentcode: optionExtent,
student_studentnumber: {
[Op.in]: students
}
},
order: [[StudyrightElement, 'startdate', 'DESC']],
attributes: ['studentStudentnumber', 'startdate', 'givendate']
})
const data = {}
const years = new Set()
options
.filter(m => m.studentStudentnumber in currentStudyrightsMap)
.filter(m => m.givendate.getTime() === currentStudyrightsMap[m.studentStudentnumber].givendate.getTime())
.forEach(b => {
const date = currentStudyrightsMap[b.studentStudentnumber].startdate
const year =
date.getMonth() > 6 || (date.getMonth() === 6 && date.getDate() == 31)
? date.getFullYear()
: date.getFullYear() - 1
years.add(year)
const code = b.studyright_elements[0].code
if (!data[code]) {
data[code] = {}
data[code].name = b.studyright_elements[0].element_detail.name
data[code].total = 0
}
if (!data[code][year]) {
data[code][year] = 0
}
data[code][year] += 1
data[code].total += 1
})
const dataAsArray = Object.keys(data).map(code => {
const { name, total } = data[code]
const output = { code, name, total }
years.forEach(y => (output[y] = data[code][y] || 0))
return output
})
return { data: dataAsArray, years: Array.from(years).sort() }
}
const statsForClass = async (studentnumbers, startDate, studyprogramme, endDate) => {
return Promise.all([
creditsAfter(studentnumbers, startDate),
@@ -784,5 +899,6 @@ module.exports = {
throughputStatsForStudytrack,
findProgrammeThesisCredits,
thesisProductivityFromCredits,
thesisProductivityForStudytrack
thesisProductivityForStudytrack,
optionData
}
@@ -341,7 +341,8 @@ const calculateAssociationsFromDb = async (chunksize = 100000) => {
type: type,
name: name,
code: code,
enrollmentStartYears: {}
enrollmentStartYears: {},
studytracks: []
}
const momentstartdate = moment(startdate)
const enrollment = momentstartdate.isValid() ? getEnrollmentStartYear(momentstartdate) : null
@@ -388,6 +389,8 @@ const calculateAssociationsFromDb = async (chunksize = 100000) => {
name: e.name,
code: e.code
}
if (!associations.programmes[code].studytracks.includes(e.code))
associations.programmes[code].studytracks.push(e.code)
}
associations.studyTracks[e.code] = associations.studyTracks[e.code] || {
type: e.type,
File diff suppressed because it is too large Load Diff
+3 -6
View File
@@ -4,18 +4,15 @@ WORKDIR /usr/src/app
CMD npm start
RUN apk add postgresql-client
COPY ./updater_writer/package.json /usr/src/app/package.json
COPY ./updater_writer/package-lock.json /usr/src/app/package-lock.json
COPY ./updater_writer/package.json /usr/src/cache/package.json
COPY ./updater_writer/package-lock.json /usr/src/cache/package-lock.json
COPY ./updater_writer/package* ./
RUN npm ci
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
RUN if [ "${NODE_ENV}" != "production" ]; then cp -r /usr/src/app/node_modules /usr/src/cache/node_modules; fi
COPY ./updater_writer /usr/src/app
COPY ./shared/models /usr/src/app/models
COPY ./shared/migrations /usr/src/app/database/migrations
COPY ./shared/migrations_kone /usr/src/app/database/migrations_kone
@@ -16,7 +16,6 @@ const TOKEN_SECRET = process.env.TOKEN_SECRET
const CERT_PATH = process.env.CERT_PATH // production/staging only
const KEY_PATH = process.env.KEY_PATH // production/staging only
const USERSERVICE_URL = process.env.USERSERVICE_URL
const USAGESERVICE_URL = process.env.USAGESERVICE_URL
const ANALYTICS_URL = process.env.ANALYTICS_URL
const PORT = isTest ? 8079 : 8080
const OODI_SECRET = process.env.OODI_SECRET
@@ -71,7 +70,6 @@ module.exports = {
ACCESS_TOKEN_HEADER_KEY,
PORT,
ANALYTICS_URL: formatURL(ANALYTICS_URL),
USAGESERVICE_URL,
requiredGroup,
OODI_SECRET,
OODI_SECRET_HEADER_KEY,
+114
View File
@@ -0,0 +1,114 @@
version: "3.2"
services:
frontend:
build:
context: services/oodikone2-frontend
environment:
BACKEND_ADDR: backend
ADMINER_URL: http://localhost:5050/?pgsql=db&username=postgres
container_name: frontend
backend:
build:
context: services/backend
dockerfile: oodikone2-backend/Dockerfile
ports:
- "8080:8080"
environment:
REDIS: redis
DB_URL: postgres://postgres@db:5432/tkt_oodi
# DB_MAX_CONNECTIONS: 90 # defaults to 5
DB_URL_KONE: postgres://postgres@db_kone:5432/db_kone
SIS_DB_URL: postgres://postgres@db_sis:5432/db_sis
# if NODE_ENV === 'test', these are used instead
TEST_DB: postgres://postgres@db:5432/tkt_oodi_test
TEST_DB_KONE: postgres://postgres@db_kone:5432/db_kone_test
# same but for NODE_ENV === 'anon'
# ANON_DB:
# ANON_DB_KONE:
TOKEN_SECRET: IWannaBeTheVeryBest
SECRET_TOKEN: dev
USERSERVICE_URL: http://userservice:4567
USERSERVICE_SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
ANALYTICS_URL: http://analytics:4568
NATS_URI: nats://nats:4222
UPDATER_URL: http://updater_scheduler:3678
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
# Set these in production:
# ANALYTICS_INFLUXDB_URL
# ANALYTICS_INFLUXDB_DB
# ANALYTICS_INFLUXDB_PASSWORD
# ANALYTICS_INFLUXDB_USER
# MATOMO_SITE_ID
# MATOMO_URL
container_name: backend
depends_on:
- db
- redis
- nats
db_kone:
image: postgres:9.6.3
ports:
- "5425:5432"
volumes:
- pgdata_kone:/var/lib/postgresql/data
- ./scripts/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
container_name: db_kone
environment:
- POSTGRES_MULTIPLE_DATABASES=db_kone
- POSTGRES_USER=postgres
userservice:
build:
context: services/oodikone2-userservice
ports:
- 4567:4567
environment:
DB_URL: postgres://postgres@user_db:5432/user_db
TEST_DB: postgres://postgres@user_db:5432/user_db_test
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
TOKEN_SECRET: IWannaBeTheVeryBest
depends_on:
- user_db
container_name: userservice
user_db:
image: postgres:9.6.3
ports:
- "5422:5432"
volumes:
- user_pgdata:/var/lib/postgresql/data
container_name: oodi_user_db
environment:
- POSTGRES_USER=postgres
analytics:
build:
context: services/oodikone2-analytics
ports:
- 4568:4568
environment:
- DB_URL=postgres://postgres@analytics_db:5432/analytics_db
- TEST_DB=postgres://postgres@analytics_db:5432/analytics_db_test
depends_on:
- analytics_db
container_name: analytics
analytics_db:
image: postgres:9.6.3
ports:
- "5423:5432"
volumes:
- analytics_pgdata:/var/lib/postgresql/data
- ./scripts/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
container_name: oodi_analytics_db
environment:
- POSTGRES_USER=postgres
volumes:
analytics_pgdata:
user_pgdata:
pgdata_kone:
@@ -5,18 +5,10 @@ services:
environment:
- POSTGRES_DB=user_db
db:
environment:
- POSTGRES_MULTIPLE_DATABASES=tkt_oodi,tkt_oodi_test
analytics_db:
environment:
- POSTGRES_MULTIPLE_DATABASES=analytics_db
db_sis:
environment:
- POSTGRES_MULTIPLE_DATABASES=db_sis
backend:
image: toska/oodikone2-backend:${TAG}
build:
@@ -54,38 +46,6 @@ services:
- toska/oodikone2-userservice:staging
- toska/oodikone2-userservice:latest
usageservice:
image: toska/oodikone2-usageservice:${TAG}
build:
cache_from:
- toska/oodikone2-usageservice:trunk
- toska/oodikone2-usageservice:staging
- toska/oodikone2-usageservice:latest
updater_api:
image: toska/updater_api:${TAG}
build:
cache_from:
- toska/updater_api:trunk
- toska/updater_api:staging
- toska/updater_api:latest
updater_writer:
image: toska/updater_writer:${TAG}
build:
cache_from:
- toska/updater_writer:trunk
- toska/updater_writer:staging
- toska/updater_writer:latest
updater_scheduler:
image: toska/updater_scheduler:${TAG}
build:
cache_from:
- toska/updater_scheduler:trunk
- toska/updater_scheduler:staging
- toska/updater_scheduler:latest
analytics:
image: toska/oodikone2-analytics:${TAG}
build:
@@ -94,27 +54,6 @@ services:
- toska/oodikone2-analytics:staging
- toska/oodikone2-analytics:latest
sis-updater-scheduler:
image: toska/sis-updater-scheduler:${TAG}
environment:
CI: 1
build:
cache_from:
- toska/sis-updater-scheduler:trunk
- toska/sis-updater-scheduler:staging
- toska/sis-updater-scheduler:latest
sis-updater-worker:
image: toska/sis-updater-worker:${TAG}
environment:
CI: 1
build:
cache_from:
- toska/sis-updater-worker:trunk
- toska/sis-updater-worker:staging
- toska/sis-updater-worker:latest
container_name: oodikone_sis-updater-worker_1
nginx:
image: nginx:latest
volumes:
+16
View File
@@ -0,0 +1,16 @@
version: '3.2'
services:
backend:
environment:
DB_URL: postgres://postgres@db:5432/tkt_oodi_real
DB_URL_KONE: postgres://postgres@db_kone:5432/db_kone_real
SIS_DB_URL: postgres://postgres@db_sis:5432/db_sis_real
userservice:
environment:
DB_URL: postgres://postgres@user_db:5432/user_db_real
analytics:
environment:
DB_URL: postgres://postgres@analytics_db:5432/analytics_db_real
+73
View File
@@ -0,0 +1,73 @@
version: '3.2'
services:
user_db:
environment:
- POSTGRES_MULTIPLE_DATABASES=user_db,user_db_real
restart: unless-stopped
analytics_db:
environment:
- POSTGRES_MULTIPLE_DATABASES=analytics_db,analytics_db_real
restart: unless-stopped
backend:
build:
args:
NODE_ENV: 'development'
TAG: 'development'
command: ['npm', 'run', 'dev']
environment:
FRONT_URL: http://localhost:8000
volumes:
- backend-nodemod:/usr/src/app/node_modules
- ./services/backend/oodikone2-backend/:/usr/src/app
- ./services/backend/shared/models:/usr/src/app/src/models
- ./services/backend/shared/migrations:/usr/src/app/src/database/migrations
- ./services/backend/shared/migrations_kone:/usr/src/app/src/database/migrations_kone
restart: unless-stopped
frontend:
build:
args:
NODE_ENV: 'development'
TAG: 'development'
command: ['npm', 'run', 'docker']
ports:
- "8081:8081"
- "7777:7777"
environment:
USER_ADMINER_URL: http://localhost:5050/?pgsql=user_db&username=postgres
ANALYTICS_ADMINER_URL: http://localhost:5050/?pgsql=analytics_db&username=postgres
KONE_ADMINER_URL: http://localhost:5050/?pgsql=db_kone&username=postgres
SIS_ADMINER_URL: http://localhost:5050/?pgsql=db_sis&username=postgres
volumes:
- frontend-nodemod:/usr/src/app/node_modules
- ./services/oodikone2-frontend/:/usr/src/app
restart: unless-stopped
userservice:
build:
args:
NODE_ENV: 'development'
command: ['npm', 'run', 'dev']
volumes:
- userservice-nodemod:/usr/src/app/node_modules
- ./services/oodikone2-userservice/:/usr/src/app
restart: unless-stopped
analytics:
build:
args:
NODE_ENV: 'development'
command: ['npm', 'run', 'dev']
volumes:
- analytics-nodemod:/usr/src/app/node_modules
- ./services/oodikone2-analytics/:/usr/src/app
restart: unless-stopped
volumes:
backend-nodemod:
analytics-nodemod:
frontend-nodemod:
userservice-nodemod:
+7 -9
View File
@@ -1,18 +1,16 @@
FROM node:10.15-alpine
WORKDIR /usr/src/app
EXPOSE 4568
CMD npm start
COPY package.json /usr/src/app/package.json
COPY package-lock.json /usr/src/app/package-lock.json
COPY package.json /usr/src/cache/package.json
COPY package-lock.json /usr/src/cache/package-lock.json
RUN npm ci
EXPOSE 4568
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
RUN if [ "${NODE_ENV}" != "production" ]; then cp -r /usr/src/app/node_modules /usr/src/cache/node_modules; fi
COPY . /usr/src/app
COPY ./package* ./
RUN npm ci
COPY . .
CMD npm start
+3 -9
View File
@@ -1669,12 +1669,6 @@
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
"bundled": true,
"dev": true,
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
@@ -2255,9 +2249,9 @@
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz",
"integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==",
"dev": true
},
"inquirer": {
+1 -1
View File
@@ -6,7 +6,7 @@
"scripts": {
"start": "node index.js",
"dev": "NODE_ENV=dev nodemon index.js",
"test": "npm run lint",
"test": "npm install --dev && npm run lint",
"lint": "eslint .",
"lintfix": "prettier --write **/*.{js,json} && eslint --fix ."
},
+2 -5
View File
@@ -4,10 +4,8 @@ WORKDIR /usr/src/app
EXPOSE 5000
CMD serve -s -l 5000 dist
COPY package.json /usr/src/app/package.json
COPY package-lock.json /usr/src/app/package-lock.json
COPY package.json /usr/src/cache/package.json
COPY package-lock.json /usr/src/cache/package-lock.json
COPY ./package* ./
RUN npm ci
ARG BASE_PATH=/
@@ -16,7 +14,6 @@ ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
ARG TAG=local
ENV TAG=${TAG}
RUN if [ "${NODE_ENV}" != "production" ]; then cp -r /usr/src/app/node_modules /usr/src/cache/node_modules; fi
COPY . /usr/src/app
RUN if [ "${NODE_ENV}" != "development" ]; then \
+39 -30
View File
@@ -3229,35 +3229,11 @@
}
},
"axios": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
}
},
"is-buffer": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
"integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
}
"follow-redirects": "^1.10.0"
}
},
"axobject-query": {
@@ -7468,8 +7444,7 @@
"follow-redirects": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.12.0.tgz",
"integrity": "sha512-JgawlbfBQKjbKegPn8vUsvJqplE7KHJuhGO4yPcb+ZOIYKSr+xobMVlfRBToZwZUUxy7lFiKBdFNloz9ui368Q==",
"dev": true
"integrity": "sha512-JgawlbfBQKjbKegPn8vUsvJqplE7KHJuhGO4yPcb+ZOIYKSr+xobMVlfRBToZwZUUxy7lFiKBdFNloz9ui368Q=="
},
"for-in": {
"version": "1.0.2",
@@ -17722,6 +17697,40 @@
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz",
"integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg=="
},
"unfuck-spa-shibboleth-session": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/unfuck-spa-shibboleth-session/-/unfuck-spa-shibboleth-session-3.1.2.tgz",
"integrity": "sha512-AHDN0JCyZzDyhBE+P9R7bZfDlFqgkeT0hA7SZ05ZjWbod/1qvsWCcLod58h/jYf6zoKAlMH5130Ed2loJDqqDg==",
"requires": {
"axios": "^0.19.2"
},
"dependencies": {
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"requires": {
"follow-redirects": "1.5.10"
}
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
}
}
}
},
"unherit": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.1.tgz",
+2 -1
View File
@@ -67,7 +67,7 @@
"@babel/preset-react": "^7.0.0",
"@sentry/browser": "^5.9.1",
"@sentry/cli": "^1.47.1",
"axios": "^0.18.1",
"axios": "^0.21.1",
"babel-polyfill": "^6.26.0",
"dotenv": "^6.2.0",
"highcharts": "^7.2.2",
@@ -102,6 +102,7 @@
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^0.88.2",
"terser-webpack-plugin": "^1.3.0",
"unfuck-spa-shibboleth-session": "^3.1.2",
"uuid": "^3.3.2",
"xlsx": "^0.14.4"
}
@@ -128,12 +128,6 @@ export const handleRequest = store => next => async action => {
const res = await callApi(route, method, data, params, 0, onProgress)
store.dispatch({ type: `${prefix}SUCCESS`, response: res.data, query })
} catch (e) {
// handle stale Shibboleth session by reloading frontend
if (e.message.toLowerCase() === 'network error') {
window.location.reload(true)
return
}
store.dispatch({ type: `${prefix}FAILURE`, response: e, query })
handleError(e, store.getState().actionHistory)
}
@@ -160,12 +154,6 @@ export const handleAuth = store => next => async action => {
}
store.dispatch({ type: 'LOGIN_SUCCESS', token })
} catch (err) {
// handle stale Shibboleth session by reloading frontend
if (err.message.toLowerCase() === 'network error') {
window.location.reload(true)
return
}
store.dispatch({ type: 'LOGIN_FAILURE' })
handleError(err, store.getState().actionHistory)
}
@@ -174,14 +162,8 @@ export const handleAuth = store => next => async action => {
await logout()
store.dispatch({ type: 'LOGOUT_SUCCESSFUL' })
} catch (err) {
if (err.message.toLowerCase() === 'network error') {
window.location.reload(true)
return
}
store.dispatch({ type: 'LOGOUT_FAILURE' })
handleError(err, store.getState().actionHistory)
}
}
}
export const sendLog = async data => callApi('/log', 'post', data)
+1 -1
View File
@@ -18,7 +18,7 @@ try {
if (IS_PRODUCTION && BASE_PATH === '/' && ['staging', 'latest'].includes(process.env.TAG)) {
Sentry.init({
environment: process.env.TAG,
dsn: 'https://02d07bd40f404cc0965f38f06183d9fb@sentry.toska.cs.helsinki.fi/3',
dsn: 'https://c55e6d020db640e889948cc25ced1c19@sentry.toska.cs.helsinki.fi/2',
release: process.env.SENTRY_RELEASE_VERSION
})
}
@@ -21,9 +21,6 @@ export default {
StartingThisSemester: `Suodattaa opiskelijat, jotka aloittivat opiskelun valittuna lukuvuonna tai sitä ennen`,
EnrollmentStatus: 'Näyttää opiskelijat, jotka olivat läsnä- tai poissaolevana valittuina lukukausina',
CanceledStudyright: `Suodattaa opiskelijat, jotka ovat/eivät ole ilmoittautuneet läsnä- tai poissaoleviksi`,
DisciplineTypes: `Asettaa annetun tyyppisille annettujen tieteenalojen
kursseille automaattisesti osallistumissuodattimet.
Voit asettaa rajan osallistumismäärälle tuodaksesi esiin vain suositut kurssit`,
ExtentGraduated: `Suodattaa opiskelijat, jotka ovat/eivät ole valmistuneet.`,
TransferFilter: `Voit valita opiskelijat, jotka ovat vaihtaneet annetusta koulutusohjelmasta toiseen.
Vaihtaminen tarkoittaa sitä, kun opiskelija vaihtaa koulutusohjelmaansa kesken opinto-oikeutensa.
@@ -37,20 +34,6 @@ export default {
CreditsBeforeStudyright: `Suodattaa pois opiskelijat joilla vähemmän opintopisteitä ennen kyseisessä ohjelmassa aloittamista`,
StudytrackFilter: `Palauttaa opiskelijat joilla on valittu kyseinen suuntautumisvaihtoehto`
},
CreditAccumulationGraph: {
AccordionTitle: ` **Credit accumulation graph**
Opiskelijoiden kumulatiiviset opintopistekertymät opiskelijakohtaisina kuvaajina.`
},
CoursesOf: {
AccordionTitle: `**Courses of population**
Tämä taulukko sisältää kaikkien populaatioon liittyvien opiskelijoiden kaikki kurssisuoritukset.`
},
Students: {
AccordionTitle: `**Students** Taulukko joka näyttää informaatiota opiskelijoista.`
},
CreditStatistics: {
AccordionTitle: `**Credit Statistics** Opintopistekertymä-kategoria skaalautuvat valitun tarkastelujakson mukaan. `
},
ProgrammeDistributionCustomPopulation: `Näyttää opiskelijan viimeisimmän koulutusohjelman`,
ProgrammeDistributionCoursePopulation: `Näyttää sen koulutusohjelman, jossa opiskelija oli suorittaessaan kurssin`,
GradeDistributionCoursePopulation: `Näyttää korkeimman arvosanan, jonka opiskelija on saanut valitulla aikavälillä.`,
@@ -76,17 +76,32 @@ export const byCodeDesc = (a, b) => b.code.localeCompare(a.code)
export const studyRightRegex = new RegExp(/.*master|bachelor|doctor|licentiate|specialist.*/)
export const studyrightTypes = { degree: '10', programme: '20', speciality: '30' } // speciality???
export const studyrightElementTypes = { degree: 10, programme: 20, speciality: 30 }
export const getStudentTotalCredits = student => {
const passedCourses = student.courses.filter(c => c.passed && !c.isStudyModuleCredit)
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)
}
export const getStudentGradeMean = student => {
const gradedCourses = student.courses.filter(c => Number(c.grade) && !c.isStudyModuleCredit)
const gradeTotal = gradedCourses.reduce((a, b) => a + Number(b.grade), 0)
const mean = gradeTotal / gradedCourses.length || 0
const getGradedCourses = (student, includeTransferredCredits = true) =>
includeTransferredCredits
? student.courses.filter(c => Number(c.grade) && !c.isStudyModuleCredit)
: student.courses.filter(c => Number(c.grade) && !c.isStudyModuleCredit && c.credittypecode !== 9)
export const getStudentGradeMean = (student, includeTransferredCredits = true) => {
const courses = getGradedCourses(student, includeTransferredCredits)
const gradeTotal = courses.reduce((a, b) => a + Number(b.grade), 0)
const mean = gradeTotal / courses.length || 0
return mean
}
export const getStudentGradeMeanWeightedByCredits = (student, includeTransferredCredits = true) => {
const courses = getGradedCourses(student, includeTransferredCredits)
const gradeTotal = courses.reduce((a, b) => a + Number(b.grade) * Number(b.credits), 0)
const sumWeights = courses.reduce((a, b) => a + Number(b.credits), 0)
const mean = gradeTotal / sumWeights || 0
return mean
}
@@ -6,7 +6,7 @@ Opiskelijat on mahdollista suodattaa kurssin perusteella käyttämällä kurssir
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 pois tyhjentämällä kentän ja painamalla ENTER. *Huom! Nämä suodattimet vaikuttavat vain tähän taulukkoon, eivät koko populatioon.*
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. *Huom! Nämä suodattimet vaikuttavat vain tähän taulukkoon, eivät koko populatioon.*
## Taulukot:
@@ -1,13 +1,16 @@
/**
* Global definitions for Population Statistics HTTP GET query.
* Global definitions for query params.
*/
import qs from 'qs'
const parseParams = location => qs.parse(location.search, { ignoreQueryPrefix: true })
/**
* All query params from given `location`.
* @param {object} location React-Router's `location` object.
*/
export const queryParamsFromUrl = location => qs.parse(location.search, { ignoreQueryPrefix: true })
/**
* Get `months` parameter from query.
* @param {object} location React-Router's `location` object.
*/
/* eslint-disable-next-line import/prefer-default-export */
export const getMonths = location => parseParams(location).months
export const getMonths = location => queryParamsFromUrl(location).months
@@ -4,12 +4,24 @@ import TSA from './tsa'
const { sendEvent } = TSA.Matomo
export const ANALYTICS_CATEGORIES = {
populationStudents: 'Population students'
populationStudents: 'Population students',
populationStatistics: 'Population statistics',
degreeCourses: 'Degree courses',
common: 'Common'
}
export default Object.fromEntries(
Object.entries(ANALYTICS_CATEGORIES).map(([catKey, catName]) => [
catKey,
(action, name, value) => sendEvent(catName, action, name, value)
])
)
// This export could be created with Object.fromEntries, but then autocomplete wont work.
export default {
populationStudents: (action, name, value) => {
sendEvent(ANALYTICS_CATEGORIES.populationStudents, action, name, value)
},
populationStatistics: (action, name, value) => {
sendEvent(ANALYTICS_CATEGORIES.populationStatistics, action, name, value)
},
degreeCourses: (action, name, value) => {
sendEvent(ANALYTICS_CATEGORIES.degreeCourses, action, name, value)
},
common: (action, name, value) => {
sendEvent(ANALYTICS_CATEGORIES.common, action, name, value)
}
}
@@ -0,0 +1,7 @@
import { getTestUserSIS } from '.'
export default getTestUserSIS()
? {
backgroundColor: '#ffe9f0'
}
: null
@@ -617,7 +617,7 @@ const ProtoC = ({
/>
<Checkbox
style={{ marginLeft: '10px' }}
label="Include old attainments"
label="Include attainments attained before the studyright start"
onChange={handleOldAttainmentToggled}
checked={includeOldAttainments}
/>
@@ -457,7 +457,7 @@ const ProtoC = ({
/>
<Checkbox
style={{ marginLeft: '10px' }}
label="Include old attainments"
label="Include attainments attained before the studyright start"
onChange={handleOldAttainmentToggled}
checked={includeOldAttainments}
/>
@@ -520,7 +520,7 @@ const ProtoC = ({
/>
<Checkbox
style={{ marginLeft: '10px' }}
label="Include old attainments"
label="Include attainments attained before the studyright start"
onChange={handleOldAttainmentToggled}
checked={includeOldAttainments}
/>
@@ -217,7 +217,7 @@ const ProtoG = ({ uberdata, years, isLoading, loadingYears, getUberDispatch, get
<h2>Kandiohjelmat: Suhteellisen tavoiteaikaerittelyn kehitys</h2>
<Checkbox
style={{ marginLeft: 'auto' }}
label="Include old attainments"
label="Include attainments attained before the studyright start"
onChange={handleOldAttainmentToggled}
checked={includeOldAttainments}
/>
@@ -2,20 +2,20 @@ import React, { useEffect, useState, useMemo, useRef } from 'react'
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
import { shape, func, bool } from 'prop-types'
import { Segment, Header, Accordion, Popup } from 'semantic-ui-react'
import qs from 'query-string'
import ReactMarkdown from 'react-markdown'
import { Segment, Header, Accordion } from 'semantic-ui-react'
import scrollToComponent from 'react-scroll-to-component'
import { getCoursePopulation } from '../../redux/populations'
import { getSingleCourseStats } from '../../redux/singleCourseStats'
import { getCustomPopulationCoursesByStudentnumbers } from '../../redux/populationCourses'
import { getFaculties } from '../../redux/faculties'
import { getSemesters } from '../../redux/semesters'
import PopulationStudents from '../PopulationStudents'
import CoursePopulationGradeDist from './CoursePopulationGradeDist'
import CoursePopulationCreditGainTable from './CoursePopulationCreditGainTable'
import CustomPopulationProgrammeDist from '../CustomPopulation/CustomPopulationProgrammeDist'
import CustomPopulationCourses from '../CustomPopulation/CustomPopulationCourses'
import ProgressBar from '../ProgressBar'
import { getStudentToTargetCourseDateMap, getUserIsAdmin } from '../../common'
import { getStudentToTargetCourseDateMap, getUserIsAdmin, getTextIn } from '../../common'
import { useProgress, useTitle } from '../../common/hooks'
import infotooltips from '../../common/InfoToolTips'
import InfoBox from '../InfoBox'
@@ -23,6 +23,7 @@ import FilterTray from '../FilterTray'
import useFilters from '../FilterTray/useFilters'
import { CoursePopulationFilters } from '../FilterTray/FilterSets'
import useLanguage from '../LanguagePicker/useLanguage'
import { queryParamsFromUrl } from '../../common/query'
const CoursePopulation = ({
getCoursePopulationDispatch,
@@ -33,17 +34,13 @@ const CoursePopulation = ({
courseData,
getSemestersDispatch,
semesters,
getFacultiesDispatch
getFacultiesDispatch,
getCustomPopulationCoursesByStudentnumbers
}) => {
const { language } = useLanguage()
const { setAllStudents, filteredStudents } = useFilters()
const selectedStudents = filteredStudents.map(stu => stu.studentNumber)
const parseQueryFromUrl = () => {
const { location } = history
const query = qs.parse(location.search)
return query
}
const [codes, setCodes] = useState([])
const [headerYears, setYears] = useState('')
const [dateFrom, setDateFrom] = useState(null)
@@ -67,6 +64,12 @@ const CoursePopulation = ({
// Pass students to filter context.
useEffect(() => {
setAllStudents(studentData.students || [])
// Data fetching for courses of population tab
if (!studentData.students) return
getCustomPopulationCoursesByStudentnumbers({
studentnumberlist: studentData.students.map(student => student.studentNumber)
})
}, [studentData.students])
useEffect(() => {
@@ -74,7 +77,7 @@ const CoursePopulation = ({
}, [])
useEffect(() => {
if (newestIndex) {
if (newestIndex && refs[newestIndex]) {
scrollToComponent(refs[newestIndex].current, { align: 'bottom' })
}
}, [activeIndex])
@@ -93,7 +96,7 @@ const CoursePopulation = ({
useEffect(() => {
if (semesters.years && semesters.semesters) {
const { coursecodes, from, to, years, separate } = parseQueryFromUrl()
const { coursecodes, from, to, years, years2, separate } = queryParamsFromUrl(history.location)
const parsedCourseCodes = JSON.parse(coursecodes)
getCoursePopulationDispatch({ coursecodes, from, to, onProgress, separate })
getSingleCourseStatsDispatch({
@@ -103,7 +106,11 @@ const CoursePopulation = ({
separate
})
setCodes(parsedCourseCodes)
setYears(years)
if (years) {
setYears(years)
} else {
setYears(years2)
}
getFromToDates(from, to, separate)
getFacultiesDispatch()
@@ -113,7 +120,7 @@ const CoursePopulation = ({
}
}, [semesters])
const header = courseData ? `${courseData.name} ${headerYears}` : null
const header = courseData ? `${getTextIn(courseData.name, language)} ${headerYears}` : null
const subHeader = codes.join(', ')
if (!dateFrom || !dateTo) return null
@@ -143,29 +150,9 @@ const CoursePopulation = ({
key: 0,
title: {
content: (
<>
{activeIndex.includes(0) ? (
<>Grade distribution</>
) : (
<Popup
trigger={
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Grade distribution
</span>
}
position="top center"
wide="very"
>
<Popup.Content>
{' '}
<ReactMarkdown
source={infotooltips.PopulationStatistics.GradeDistributionCoursePopulation}
escapeHtml={false}
/>
</Popup.Content>
</Popup>
)}
</>
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Grade distribution
</span>
)
},
onTitleClick: () => handleClick(0),
@@ -188,29 +175,9 @@ const CoursePopulation = ({
key: 1,
title: {
content: (
<>
{activeIndex.includes(1) ? (
<>Programme distribution</>
) : (
<Popup
trigger={
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Programme distribution
</span>
}
position="top center"
wide="very"
>
<Popup.Content>
{' '}
<ReactMarkdown
source={infotooltips.PopulationStatistics.ProgrammeDistributionCoursePopulation}
escapeHtml={false}
/>
</Popup.Content>
</Popup>
)}
</>
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Programme distribution
</span>
)
},
onTitleClick: () => handleClick(1),
@@ -231,32 +198,30 @@ const CoursePopulation = ({
key: 2,
title: {
content: (
<>
{activeIndex.includes(2) ? (
<>Credit gains</>
) : (
<Popup
trigger={
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Credit gains
</span>
}
position="top center"
wide="very"
>
<Popup.Content>
{' '}
<ReactMarkdown
source={infotooltips.PopulationStatistics.CreditDistributionCoursePopulation}
escapeHtml={false}
/>
</Popup.Content>
</Popup>
)}
</>
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Courses of population
</span>
)
},
onTitleClick: () => handleClick(2),
content: {
content: (
<div ref={programmeRef}>
<CustomPopulationCourses selectedStudents={selectedStudents} showFilter={false} />
</div>
)
}
},
{
key: 3,
title: {
content: (
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Credit gains
</span>
)
},
onTitleClick: () => handleClick(3),
content: {
content: (
<div ref={creditGainRef}>
@@ -273,42 +238,21 @@ const CoursePopulation = ({
}
},
{
key: 3,
key: 4,
title: {
content: (
<>
{activeIndex.includes(3) ? (
<>Students ({selectedStudents.length})</>
) : (
<Popup
trigger={
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Students ({selectedStudents.length})
</span>
}
position="top center"
wide="very"
>
<Popup.Content>
{' '}
<ReactMarkdown
source={infotooltips.PopulationStatistics.Students.AccordionTitle}
escapeHtml={false}
/>
</Popup.Content>
</Popup>
)}
</>
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Students ({selectedStudents.length})
</span>
)
},
onTitleClick: () => handleClick(3),
onTitleClick: () => handleClick(4),
content: {
content: (
<div ref={studentRef}>
<PopulationStudents
studentToTargetCourseDateMap={studentToTargetCourseDateMap}
filteredStudents={studentData.students.filter(stu => selectedStudents.includes(stu.studentNumber))}
mandatoryToggle={false}
coursePopulation
language={language}
/>
@@ -347,7 +291,8 @@ CoursePopulation.propTypes = {
semesters: shape({}),
years: shape({})
}).isRequired,
getFacultiesDispatch: func.isRequired
getFacultiesDispatch: func.isRequired,
getCustomPopulationCoursesByStudentnumbers: func.isRequired
}
const mapStateToProps = ({
@@ -361,7 +306,6 @@ const mapStateToProps = ({
return {
studentData: populations.data,
pending: populations.pending,
query: populations.query,
courseData: singleCourseStats.stats || {},
semesters: semesters.data,
isAdmin: getUserIsAdmin(roles)
@@ -375,7 +319,8 @@ export default withRouter(
getCoursePopulationDispatch: getCoursePopulation,
getSingleCourseStatsDispatch: getSingleCourseStats,
getSemestersDispatch: getSemesters,
getFacultiesDispatch: getFaculties
getFacultiesDispatch: getFaculties,
getCustomPopulationCoursesByStudentnumbers
}
)(CoursePopulation)
)
@@ -5,11 +5,12 @@ import { func, arrayOf, shape, string, bool } from 'prop-types'
import { getActiveYears } from '../courseStatisticsUtils'
import useLanguage from '../../LanguagePicker/useLanguage'
import './courseTable.css'
import { getTextIn } from '../../../common'
const CourseTable = ({ courses, onSelectCourse, hidden, title, emptyListText, mandatory = false }) => {
const CourseTable = ({ courses, onSelectCourse, hidden, title, emptyListText, mandatory = false, controlIcon }) => {
const { language } = useLanguage()
const noContent = courses.length === 0
const sortCourses = courses => sortBy(courses, course => course.name[language])
const sortCourses = courses => sortBy(courses, course => getTextIn(course.name, language))
const getEmptyListRow = () => (
<Table.Row>
@@ -24,10 +25,11 @@ const CourseTable = ({ courses, onSelectCourse, hidden, title, emptyListText, ma
onClick={() => (course.min_attainment_date || mandatory ? onSelectCourse(course) : null)}
>
<Table.Cell width={10}>
<div>{course.name[language]}</div>
<div>{getTextIn(course.name, language)}</div>
<div>{getActiveYears(course)}</div>
</Table.Cell>
<Table.Cell content={!course.alternatives ? course.code : course.alternatives.map(a => a.code).join(', ')} />
{controlIcon ? <Table.Cell icon={controlIcon} /> : null}
</Table.Row>
)
@@ -39,6 +41,7 @@ const CourseTable = ({ courses, onSelectCourse, hidden, title, emptyListText, ma
<Table.Row>
<Table.HeaderCell content={title} />
<Table.HeaderCell content="Code" />
{controlIcon ? <Table.HeaderCell /> : null}
</Table.Row>
</Table.Header>
<Table.Body>{noContent ? getEmptyListRow() : sortCourses(courses).map(toCourseRow)}</Table.Body>
@@ -54,18 +57,19 @@ CourseTable.propTypes = {
hidden: bool.isRequired,
title: string.isRequired,
emptyListText: string,
controlIcon: string.isRequired
controlIcon: string
}
CourseTable.defaultProps = {
emptyListText: 'No results.'
emptyListText: 'No results.',
controlIcon: null
}
function areEqual(prevProps, nextProps) {
if (prevProps.courses.length !== nextProps.courses.length) {
return false
}
return true
return prevProps.courses.every(c1 => nextProps.courses.some(c2 => c1.code === c2.code))
}
export default React.memo(CourseTable, areEqual)
@@ -9,33 +9,42 @@ import useLanguage from '../../LanguagePicker/useLanguage'
const CourseTableRow = ({ facultyCode, students, credits, facultyName }) => {
return (
<Table.Row>
<Table.Cell>
<Table.Cell width={13}>
{facultyCode}, {facultyName}
</Table.Cell>
<Table.Cell>{students}</Table.Cell>
<Table.Cell>{credits}</Table.Cell>
<Table.Cell width={2}>{students}</Table.Cell>
<Table.Cell width={1}>{credits}</Table.Cell>
</Table.Row>
)
}
const CourseTable = ({ course, courseInstance, language }) => {
const { coursecode, name } = course
const CourseTable = ({ course, courseInstance, language, selectedYear }) => {
const { coursecode } = course
const name = getTextIn(course.name, language)
const rows = courseInstance ? (
Object.entries(courseInstance.faculties)
.sort(([facultyCodeA], [facultyCodeB]) => facultyCodeA.localeCompare(facultyCodeB))
.map(([facultyCode, instanceFaculty]) => (
<CourseTableRow
key={`${coursecode}-${facultyCode}`}
facultyCode={facultyCode}
facultyName={getTextIn(instanceFaculty.name, language)}
students={instanceFaculty.students.length}
credits={instanceFaculty.credits}
/>
))
) : (
<Table.Row>
<Table.Cell width={13}>No data for selected year</Table.Cell>
</Table.Row>
)
const rows = Object.entries(courseInstance.faculties)
.sort(([facultyCodeA], [facultyCodeB]) => facultyCodeA.localeCompare(facultyCodeB))
.map(([facultyCode, instanceFaculty]) => (
<CourseTableRow
key={`${coursecode}-${facultyCode}`}
facultyCode={facultyCode}
facultyName={getTextIn(instanceFaculty.name, language)}
students={instanceFaculty.students.length}
credits={instanceFaculty.credits}
/>
))
return (
<>
<Header>
{courseInstance.year} {coursecode}
{courseInstance ? courseInstance.year : `${1949 + Number(selectedYear)}-${1950 + Number(selectedYear)}`}{' '}
{coursecode}
</Header>
<Header size="small">
{name} ({coursecode})
@@ -43,19 +52,27 @@ const CourseTable = ({ course, courseInstance, language }) => {
<Table compact>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Faculty</Table.HeaderCell>
<Table.HeaderCell>Students</Table.HeaderCell>
<Table.HeaderCell>Credits</Table.HeaderCell>
<Table.HeaderCell width={13}>Faculty</Table.HeaderCell>
<Table.HeaderCell width={2}>Students</Table.HeaderCell>
<Table.HeaderCell width={1}>Credits</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>{rows}</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell style={{ fontWeight: '700' }}>Total</Table.HeaderCell>
<Table.HeaderCell style={{ fontWeight: '700' }}>{courseInstance.allStudents.length}</Table.HeaderCell>
<Table.HeaderCell style={{ fontWeight: '700' }}>{courseInstance.allCredits}</Table.HeaderCell>
</Table.Row>
</Table.Footer>
{courseInstance ? (
<Table.Footer>
<Table.Row>
<Table.HeaderCell width={13} style={{ fontWeight: '700' }}>
Total
</Table.HeaderCell>
<Table.HeaderCell width={2} style={{ fontWeight: '700' }}>
{courseInstance ? courseInstance.allStudents.length : 0}
</Table.HeaderCell>
<Table.HeaderCell width={1} style={{ fontWeight: '700' }}>
{courseInstance ? courseInstance.allCredits : 0}
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
) : null}
</Table>
</>
)
@@ -80,13 +97,20 @@ const FacultyLevelStatistics = () => {
}))
const yearsCourseStats = Object.values(courseStats)
.filter(course => !!course.facultyStats[selectedYear])
.map(course => ({
course,
courseInstance: course.facultyStats[selectedYear]
}))
.sort((a, b) => (a.courseInstance === undefined) - (b.courseInstance === undefined))
const courseTables = yearsCourseStats.map(({ course, courseInstance }) => (
<CourseTable course={course} courseInstance={courseInstance} language={language} key={course.coursecode} />
<CourseTable
course={course}
courseInstance={courseInstance}
language={language}
key={course.coursecode}
selectedYear={selectedYear}
/>
))
return (
@@ -115,8 +139,13 @@ CourseTableRow.propTypes = {
CourseTable.propTypes = {
course: shape({}).isRequired,
courseInstance: shape({}).isRequired,
language: string.isRequired
courseInstance: shape({}),
language: string.isRequired,
selectedYear: string.isRequired
}
CourseTable.defaultProps = {
courseInstance: null
}
export default FacultyLevelStatistics
@@ -36,18 +36,24 @@ const getTableData = (stats, isGradeSeries, isRelative) =>
}
})
const getGradeColumns = isGradeSeries =>
isGradeSeries
? [
getSortableColumn('0', '0', s => s['0']),
getSortableColumn('1', '1', s => s['1']),
getSortableColumn('2', '2', s => s['2']),
getSortableColumn('3', '3', s => s['3']),
getSortableColumn('4', '4', s => s['4']),
getSortableColumn('5', '5', s => s['5']),
getSortableColumn('OTHER_PASSED', 'Other passed', s => s['Hyv.'])
]
: THESIS_GRADE_KEYS.map(k => getSortableColumn(k, k, s => s[k]))
const includesHTOrTT = stats =>
stats.some(({ cumulative }) => ['HT', 'TT'].some(grade => Object.keys(cumulative.grades).includes(grade)))
const getGradeColumns = (isGradeSeries, addHTAndTT) => {
if (!isGradeSeries) return THESIS_GRADE_KEYS.map(k => getSortableColumn(k, k, s => s[k]))
const columns = [
getSortableColumn('0', '0', s => s['0']),
getSortableColumn('1', '1', s => s['1']),
getSortableColumn('2', '2', s => s['2']),
getSortableColumn('3', '3', s => s['3']),
getSortableColumn('4', '4', s => s['4']),
getSortableColumn('5', '5', s => s['5']),
getSortableColumn('OTHER_PASSED', 'Other passed', s => s['Hyv.'])
]
if (addHTAndTT)
columns.splice(6, 0, getSortableColumn('HT', 'HT', s => s.HT), getSortableColumn('TT', 'TT', s => s.TT))
return columns
}
const GradesTable = ({ stats, name, alternatives, separate, isRelative }) => {
const {
@@ -84,7 +90,7 @@ const GradesTable = ({ stats, name, alternatives, separate, isRelative }) => {
)
),
getSortableColumn('ATTEMPTS', 'Attempts', s => s.attempts),
...getGradeColumns(isGradeSeries)
...getGradeColumns(isGradeSeries, includesHTOrTT(stats))
]
const data = getTableData(stats, isGradeSeries, isRelative)
@@ -33,7 +33,9 @@ const getGradeSeries = series => {
getDataObject('3', newSeries[3], 'd'),
getDataObject('4', newSeries[4], 'e'),
getDataObject('5', newSeries[5], 'f'),
getDataObject('Hyv.', newSeries['Hyv.'], 'g')
getDataObject('HT', newSeries.HT, 'g'),
getDataObject('TT', newSeries.TT, 'h'),
getDataObject('Hyv.', newSeries['Hyv.'], 'i')
],
relative: [
getDataObject('0', newSeries[0].map(absoluteToRelative(sumAll)), 'a'),
@@ -42,7 +44,9 @@ const getGradeSeries = series => {
getDataObject('3', newSeries[3].map(absoluteToRelative(sumAll)), 'd'),
getDataObject('4', newSeries[4].map(absoluteToRelative(sumAll)), 'e'),
getDataObject('5', newSeries[5].map(absoluteToRelative(sumAll)), 'f'),
getDataObject('Hyv.', newSeries['Hyv.'].map(absoluteToRelative(sumAll)), 'g')
getDataObject('HT', newSeries.HT.map(absoluteToRelative(sumAll)), 'g'),
getDataObject('TT', newSeries.TT.map(absoluteToRelative(sumAll)), 'h'),
getDataObject('Hyv.', newSeries['Hyv.'].map(absoluteToRelative(sumAll)), 'i')
]
}
: {
@@ -82,6 +82,8 @@ export const getGradeSpread = (series, isRelative) => {
3: [],
4: [],
5: [],
HT: [],
TT: [],
'Hyv.': []
}
const newSeries = series.reduce(
@@ -9,7 +9,7 @@ import { clearCourses, findCoursesV2, toggleUnifyOpenUniCourses } from '../../..
import { getCourseStats, clearCourseStats } from '../../../redux/coursestats'
import { getCourseSearchResults } from '../../../selectors/courses'
import { useSearchHistory, usePrevious } from '../../../common/hooks'
import { validateInputLength } from '../../../common'
import { validateInputLength, getTextIn } from '../../../common'
import TSA from '../../../common/tsa'
import { mergeCourses } from '../courseStatisticsUtils'
import AutoSubmitSearchInput from '../../AutoSubmitSearchInput'
@@ -138,7 +138,7 @@ const SearchForm = props => {
separate,
unifyOpenUniCourses: props.unifyOpenUniCourses
}
const searchHistoryText = codes.map(code => `${selectedCourses[code].name[language]} ${code}`)
const searchHistoryText = codes.map(code => `${getTextIn(selectedCourses[code].name, language)} ${code}`)
addItemToSearchHistory({
text: searchHistoryText.join(', '),
params
@@ -179,14 +179,33 @@ const SearchForm = props => {
const selected = Object.values(selectedCourses).map(course => ({ ...course, selected: true }))
const noSelectedCourses = selected.length === 0
const addAllCourses = () => {
courses.forEach(course => {
course.selected = true
})
const newSelectedCourses = courses.reduce((newSelected, course) => {
newSelected[course.code] = { ...course }
return newSelected
}, {})
setState({
...state,
selectedCourses: {
...selectedCourses,
...newSelectedCourses
}
})
}
return (
<React.Fragment>
<Segment loading={isLoading}>
<Form>
<Header content="Search for courses" />
<div style={{ marginBottom: '15px' }}>
<Form.Group widths="equal">
<Form.Field>
<Form.Group>
<Form.Field width={8}>
<label>Name:</label>
<AutoSubmitSearchInput
doSearch={fetchCourses}
@@ -197,11 +216,11 @@ const SearchForm = props => {
minSearchLength={0}
/>
</Form.Field>
<Form.Field>
<Form.Field width={3}>
<label>Code:</label>
<AutoSubmitSearchInput
doSearch={fetchCourses}
placeholder="Search by entering a course code"
placeholder="Search by a course code"
value={courseCode}
onChange={cc => setState({ ...state, courseCode: cc })}
loading={props.coursesLoading}
@@ -209,7 +228,7 @@ const SearchForm = props => {
data-cy="course-code-input"
/>
</Form.Field>
<Form.Field style={{ display: 'flex', flexDirection: 'column' }}>
<Form.Field width={4} style={{ display: 'flex', flexDirection: 'column' }}>
<label>Unify open university courses:</label>
<span style={{ flex: 1, display: 'flex', alignItems: 'center' }}>
<Form.Checkbox
@@ -225,7 +244,7 @@ const SearchForm = props => {
hidden={noSelectedCourses}
courses={selected}
onSelectCourse={onSelectCourse}
controlIcon="remove"
controlIcon="trash alternate outline"
/>
{!noSelectedCourses && (
<Fragment>
@@ -254,8 +273,19 @@ const SearchForm = props => {
courses={courses}
title="Searched courses"
onSelectCourse={onSelectCourse}
controlIcon="plus"
/>
{courses.length ? (
<div className="select-all-container">
<Form.Button
type="button"
size="large"
content="Select all search results"
basic
color="green"
onClick={addAllCourses}
/>
</div>
) : null}
</div>
</Form>
</Segment>
@@ -143,7 +143,7 @@ const SingleCourseStats = ({
const filteredSemesters = semesters.filter(timeFilter)
const filteredYears = years.filter(timeFilter)
return separate
? filteredSemesters.find(year => year.texts.includes(name))
? filteredSemesters.find(year => year.texts.includes(getTextIn(name, language)))
: filteredYears.find(year => year.text === name)
}
@@ -161,8 +161,10 @@ const SingleCourseStats = ({
grades: countFilteredStudents(allstudents.grades, filter),
categories: countFilteredStudents(allstudents.classes, filter)
}
return { code, name, cumulative, students, coursecode }
const parsedName = separate ? getTextIn(name, language) : name
return { code, name: parsedName, cumulative, students, coursecode }
})
const totals = progStats.reduce(
(acc, curr) => {
const passed = acc.cumulative.categories.passed + curr.cumulative.categories.passed
@@ -331,7 +333,7 @@ const SingleCourseStats = ({
if (stats.statistics.length < 1) return <Segment>No data for selected course</Segment>
const options = filteredProgrammes
.map(({ text, ...rest }) => ({ text: typeof text === 'string' ? text : text[language], ...rest }))
.map(({ text, ...rest }) => ({ text: typeof text === 'string' ? text : getTextIn(text, language), ...rest }))
.map(prog => ({ ...prog, name: prog.text }))
return (
@@ -417,7 +419,7 @@ SingleCourseStats.propTypes = {
)
})
),
name: string,
name: shape({ fi: string, en: string, sv: string }),
coursecode: string
}).isRequired,
programmes: arrayOf(shape({})).isRequired,
@@ -1,12 +1,19 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { Segment, Label, Header, Divider, Form } from 'semantic-ui-react'
import { shape, arrayOf, oneOfType, number, string } from 'prop-types'
import SingleCourseStats from '../SingleCourseStats'
import useLanguage from '../../LanguagePicker/useLanguage'
import selectors from '../../../selectors/courseStats'
import { getTextIn } from '../../../common'
const SingleCourseTab = ({ selected, stats, courses }) => {
const [selection, setSelection] = useState(selected)
const { language } = useLanguage()
useEffect(() => {
setSelection(selected)
}, [selected])
if (!stats[selection]) return null
return (
@@ -29,7 +36,10 @@ const SingleCourseTab = ({ selected, stats, courses }) => {
<Label.Group>
<Label
key={stats[selection].coursecode}
content={`${stats[selection].alternatives.map(code => ` ${code}`)} ${stats[selection].name} `}
content={`${stats[selection].alternatives.map(code => ` ${code}`)} ${getTextIn(
stats[selection].name,
language
)} `}
/>
</Label.Group>
</Form>
@@ -8,6 +8,7 @@ import { fields, setValue } from '../../../redux/coursesSummaryForm'
import CumulativeTable from '../CumulativeTable'
import ProgrammeDropdown from '../ProgrammeDropdown'
import useLanguage from '../../LanguagePicker/useLanguage'
import { getTextIn } from '../../../common'
const SummaryTab = ({ form, setValue, statistics, programmes, queryInfo, onClickCourse }) => {
const { language } = useLanguage()
@@ -24,7 +25,7 @@ const SummaryTab = ({ form, setValue, statistics, programmes, queryInfo, onClick
const { passed, failed, passrate } = summary
return {
id: coursecode,
category: name,
category: getTextIn(name, language),
passed,
failed,
passrate,
@@ -35,7 +36,7 @@ const SummaryTab = ({ form, setValue, statistics, programmes, queryInfo, onClick
const options = programmes
.map(e => ({ ...e, size: new Set(flatten(Object.values(e.students))).size }))
.filter(e => e.size > 0)
.map(({ text, ...rest }) => ({ text: typeof text === 'string' ? text : text[language], ...rest }))
.map(({ text, ...rest }) => ({ text: typeof text === 'string' ? text : getTextIn(text, language), ...rest }))
.map(prog => ({ ...prog, name: prog.text }))
return (
@@ -69,7 +70,11 @@ SummaryTab.propTypes = {
statistics: arrayOf(
shape({
coursecode: oneOfType([number, string]),
name: string,
name: shape({
fi: string,
en: string,
sv: string
}),
summary: shape({
failed: number,
passed: number,
@@ -31,3 +31,9 @@
.yearControlButton {
font-size: 8px !important;
}
.select-all-container {
width: 100%;
display: flex;
justify-content: space-around;
}
@@ -49,42 +49,68 @@ const sortAlternatives = alternatives =>
['asc', 'desc', 'desc']
)
const mergeCourses = (groups, courses, groupMeta, unifyOpenUniCourses = false) => {
const mergedCourses = {}
const isAvoin = code => !!code.match(/^AY?(.+?)(?:en|fi|sv)?$/)
courses.forEach(course => {
const avoinRegex = !!course.code.match(/^AY?(.+?)(?:en|fi|sv)?$/)
const isAvoin = avoinRegex
const fixGroups = groupsArgs => {
const notAvoin = new Set()
const groups = {}
const groupId = isAvoin && !unifyOpenUniCourses ? course.code : groups[course.code]
Object.entries(groupsArgs).forEach(([code, parentCode]) => {
groups[code] = parentCode
if (!mergedCourses[groupId]) {
mergedCourses[groupId] = {
...course,
code: (groupMeta[groupId] && groupMeta[groupId].code) || course.code,
name: (groupMeta[groupId] && groupMeta[groupId].name) || course.name,
alternatives: [{ code: course.code, latestInstanceDate: new Date(course.latest_instance_date) }],
min_attainment_date: new Date(course.min_attainment_date),
max_attainment_date: new Date(course.max_attainment_date)
}
} else {
const mergedCourse = mergedCourses[groupId]
mergedCourse.min_attainment_date = new Date(
Math.min(mergedCourse.min_attainment_date, new Date(course.min_attainment_date))
)
mergedCourse.max_attainment_date = new Date(
Math.max(mergedCourse.max_attainment_date, new Date(course.max_attainment_date))
)
mergedCourse.alternatives.push({
code: course.code,
latestInstanceDate: new Date(course.latest_instance_date)
})
// handle special case where the course's actual code starts with "A" so it is mistakenly taken as Open Uni course
if (groupsArgs[`AY${code}`] && code.startsWith('A')) {
groups[code] = code
notAvoin.add(code)
}
})
return { groups, notAvoin }
}
const mergeCourses = (groupsArgs, courses, groupMeta, unifyOpenUniCourses = false) => {
const mergedCourses = {}
const { groups, notAvoin } = fixGroups(groupsArgs)
courses
// Sort the codes so that we know non-Open Uni courses come up first
.sort((a, b) => a.code.length - b.code.length)
.forEach(course => {
const groupId =
isAvoin(course.code) && !notAvoin.has(course.code) && !unifyOpenUniCourses ? course.code : groups[course.code]
if (!mergedCourses[groupId]) {
mergedCourses[groupId] = {
...course,
code: (groupMeta[groupId] && groupMeta[groupId].code) || course.code,
name: (groupMeta[groupId] && groupMeta[groupId].name) || course.name,
alternatives: [{ code: course.code, latestInstanceDate: new Date(course.latest_instance_date) }],
min_attainment_date: new Date(course.min_attainment_date),
max_attainment_date: new Date(course.max_attainment_date)
}
} else {
const mergedCourse = mergedCourses[groupId]
mergedCourse.min_attainment_date = new Date(
Math.min(mergedCourse.min_attainment_date, new Date(course.min_attainment_date))
)
mergedCourse.max_attainment_date = new Date(
Math.max(mergedCourse.max_attainment_date, new Date(course.max_attainment_date))
)
mergedCourse.alternatives.push({
code: course.code,
latestInstanceDate: new Date(course.latest_instance_date)
})
}
})
return Object.values(mergedCourses).map(c => ({
...c,
alternatives: sortAlternatives(c.alternatives)
// If special case of Open Uni course that starts with A let's just sort them length, that
// should be good enough in this case.
alternatives: c.alternatives.some(course => notAvoin.has(course.code))
? c.alternatives.sort((a, b) => a.code.length - b.code.length)
: sortAlternatives(c.alternatives)
}))
}
@@ -41,6 +41,10 @@ const CourseStatistics = props => {
const { onProgress, progress } = useProgress(loading)
useTitle('Course statistics')
useEffect(() => {
setSelected(initCourseCode)
}, [initCourseCode])
useEffect(() => {
if (statsIsEmpty) {
setSelected(initCourseCode)
@@ -13,7 +13,7 @@ import './creditAccumulationGraphHC.css'
import CreditGraphTooltip from '../CreditGraphTooltip'
import { clearLoading } from '../../redux/graphSpinner'
import { setChartHeight } from '../../redux/settings'
import { reformatDate } from '../../common'
import { reformatDate, getTextIn } from '../../common'
import { DISPLAY_DATE_FORMAT, API_DATE_FORMAT } from '../../constants'
// boost canvas needed because tests break with large population
@@ -126,7 +126,7 @@ class CreditAccumulationGraphHighCharts extends Component {
payload: {
...targetCourse,
date: reformatDate(targetCourse.date, DISPLAY_DATE_FORMAT),
title: targetCourse.course.name[language]
title: getTextIn(targetCourse.course.name, language)
}
}
]
@@ -1,14 +1,14 @@
import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import { shape, arrayOf, string, bool } from 'prop-types'
import PopulationCourseStats from '../../PopulationCourseStats'
import PopulationCourseStatsFlat from '../../PopulationCourseStats/PopulationCourseStatsFlat'
import useCourseFilter from '../../FilterTray/filters/Courses/useCourseFilter'
const CustomPopulationCourses = ({ courses, pending, selectedStudents, query, error }) => {
const CustomPopulationCourses = ({ courses, pending, selectedStudents, query, error, showFilter }) => {
const { setCoursesOnce, resetCourses } = useCourseFilter()
useEffect(() => {
if (!pending && !error) {
if (!pending && !error && courses.coursestatistics) {
setCoursesOnce(courses.coursestatistics)
}
}, [courses])
@@ -19,12 +19,13 @@ const CustomPopulationCourses = ({ courses, pending, selectedStudents, query, er
}, [])
return (
<PopulationCourseStats
<PopulationCourseStatsFlat
courses={courses}
query={query}
pending={pending}
selectedStudents={selectedStudents}
customPopulation
showFilter={showFilter}
/>
)
}
@@ -34,7 +35,12 @@ CustomPopulationCourses.propTypes = {
courses: shape([]).isRequired,
selectedStudents: arrayOf(string).isRequired,
query: shape({}).isRequired,
error: bool.isRequired
error: bool.isRequired,
showFilter: bool
}
CustomPopulationCourses.defaultProps = {
showFilter: false
}
const mapStateToProps = ({ populationCourses }) => ({
@@ -0,0 +1,188 @@
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { Button, Modal, Form, TextArea } from 'semantic-ui-react'
import { shape, func, arrayOf, bool } from 'prop-types'
import { useProgress, useTitle } from '../../common/hooks'
import { getCustomPopulation } from '../../redux/populations'
import {
getCustomPopulationSearches,
saveCustomPopulationSearch,
updateCustomPopulationSearch,
selectCustomPopulationSearch,
deleteCustomPopulationSearch
} from '../../redux/customPopulationSearch'
import { getCustomPopulationCoursesByStudentnumbers } from '../../redux/populationCourses'
import SearchHistory from '../SearchHistory'
const CustomPopulationSearch = ({
getCustomPopulationDispatch,
getCustomPopulationCoursesByStudentnumbers,
getCustomPopulationSearchesDispatch,
saveCustomPopulationSearchDispatch,
updateCustomPopulationSearchDispatch,
selectCustomPopulationSearchDispatch,
deleteCustomPopulationSearchDispatch,
loading,
customPopulationSearches,
customPopulationSearchSaving
}) => {
const [modal, setModal] = useState(false)
const [input, setInput] = useState('')
const [name, setName] = useState('')
const [selectedSearchId, setSelectedSearchId] = useState('')
const { onProgress } = useProgress(loading)
useTitle('Custom population')
useEffect(() => {
getCustomPopulationSearchesDispatch()
}, [])
const handleNameChange = e => {
setName(e.target.value)
}
const clearForm = () => {
setName('')
setInput('')
setSelectedSearchId('')
}
const handleClose = () => {
setModal(false)
clearForm()
}
const onSave = () => {
const studentnumberlist = input.match(/[0-9]+/g)
if (selectedSearchId) {
updateCustomPopulationSearchDispatch({ id: selectedSearchId, studentnumberlist })
} else {
saveCustomPopulationSearchDispatch({ name, studentnumberlist })
}
}
const onDelete = () => {
if (selectedSearchId) {
deleteCustomPopulationSearchDispatch({ id: selectedSearchId })
clearForm()
}
}
const onSelectSearch = selected => {
if (!selected) {
clearForm()
return
}
const { id: selectedSearchId } = selected
const selectedSearch = customPopulationSearches.find(({ id }) => id === selectedSearchId)
if (selectedSearch) {
setInput(selectedSearch.students.join('\n'))
setName(selectedSearch.name)
setSelectedSearchId(selectedSearch.id)
}
}
const onClicker = e => {
e.preventDefault()
const studentnumbers = input.match(/[0-9]+/g)
getCustomPopulationDispatch({ studentnumberlist: studentnumbers, onProgress })
getCustomPopulationCoursesByStudentnumbers({ studentnumberlist: studentnumbers })
selectCustomPopulationSearchDispatch(selectedSearchId || null)
handleClose()
}
return (
<Modal
trigger={
<Button size="small" color="blue" onClick={() => setModal(true)} data-cy="custom-pop-search-button">
Custom population
</Button>
}
open={modal}
onClose={handleClose}
size="small"
>
<Modal.Content>
<Form>
<h2> Custom population new</h2>
<Form.Field>
<em> Insert name for this custom population if you wish to save it </em>
<Form.Input disabled={!!selectedSearchId} value={name} placeholder="name" onChange={handleNameChange} />
</Form.Field>
<Form.Field>
<em> Insert studentnumbers you wish to use for population here </em>
<TextArea
value={input}
placeholder="011111111"
onChange={e => setInput(e.target.value)}
data-cy="student-no-input"
/>
</Form.Field>
</Form>
<SearchHistory
header="Saved populations"
items={customPopulationSearches.map(s => ({
...s,
text: s.name,
timestamp: s.updatedAt,
params: { id: s.id }
}))}
updateItem={() => null}
handleSearch={onSelectSearch}
/>
</Modal.Content>
<Modal.Actions>
<Button
disabled={!name || customPopulationSearchSaving}
loading={customPopulationSearchSaving}
floated="left"
icon="save"
onClick={onSave}
content="Save"
/>
<Button disabled={!selectedSearchId} negative floated="left" icon="trash" onClick={onDelete} content="Delete" />
<Button onClick={handleClose}>Cancel</Button>
<Button positive onClick={e => onClicker(e)} data-cy="search-button">
Search population
</Button>
</Modal.Actions>
</Modal>
)
}
CustomPopulationSearch.propTypes = {
getCustomPopulationDispatch: func.isRequired,
getCustomPopulationCoursesByStudentnumbers: func.isRequired,
loading: bool.isRequired,
customPopulationSearches: arrayOf(shape({})).isRequired,
customPopulationSearchSaving: bool.isRequired,
saveCustomPopulationSearchDispatch: func.isRequired,
getCustomPopulationSearchesDispatch: func.isRequired,
updateCustomPopulationSearchDispatch: func.isRequired,
selectCustomPopulationSearchDispatch: func.isRequired,
deleteCustomPopulationSearchDispatch: func.isRequired
}
const mapStateToProps = ({ populations, populationCourses, customPopulationSearch }) => ({
loading: populations.pending,
custompop: populations.data.students || [],
courses: populationCourses.data,
customPopulationSearches: customPopulationSearch.customPopulationSearches,
customPopulationSearchSaving: customPopulationSearch.saving
})
export default connect(
mapStateToProps,
{
getCustomPopulationDispatch: getCustomPopulation,
getCustomPopulationCoursesByStudentnumbers,
saveCustomPopulationSearchDispatch: saveCustomPopulationSearch,
getCustomPopulationSearchesDispatch: getCustomPopulationSearches,
updateCustomPopulationSearchDispatch: updateCustomPopulationSearch,
selectCustomPopulationSearchDispatch: selectCustomPopulationSearch,
deleteCustomPopulationSearchDispatch: deleteCustomPopulationSearch
}
)(CustomPopulationSearch)
@@ -0,0 +1,45 @@
import React from 'react'
import moment from 'moment'
import xlsx from 'xlsx'
import { Dropdown } from 'semantic-ui-react'
import { getStudentGradeMean, getStudentGradeMeanWeightedByCredits, getStudentTotalCredits } from '../../common'
import useFilters from '../FilterTray/useFilters'
import sendEvent from '../../common/sendEvent'
import DataExport from '../PopulationStatistics/DataExport'
const sendAnalytics = sendEvent.populationStudents
export default () => {
const { filteredStudents } = useFilters()
const getXlsx = () => {
const data = filteredStudents.map(student => ({
'Student Number': student.studentNumber,
'Total Credits (no transferred credits)': getStudentTotalCredits(student, false),
'Grade Mean (no transferred credits)': getStudentGradeMean(student, false),
'Grade Mean Weighted by ECTS (no transferred credits)': getStudentGradeMeanWeightedByCredits(student, false)
}))
const sheet = xlsx.utils.json_to_sheet(data)
const workbook = xlsx.utils.book_new()
xlsx.utils.book_append_sheet(workbook, sheet)
return workbook
}
const filename = `oodikone_export_${filteredStudents.length}_students_${moment().format('YYYYMMDD-hhmmss')}.xlsx`
return (
<>
<DataExport />
<Dropdown.Item
onClick={() => {
xlsx.writeFile(getXlsx(), filename)
sendAnalytics('Download excel button clicked', 'Download excel button clicked (UniHow)')
}}
text="Excel Workbook (UniHow)"
icon="file excel"
/>
</>
)
}
@@ -1,22 +1,13 @@
import React, { useState, useEffect, useRef } from 'react'
import { connect } from 'react-redux'
import { Button, Modal, Form, TextArea, Segment, Header, Accordion, Popup, Message } from 'semantic-ui-react'
import { Segment, Header, Accordion, Message } from 'semantic-ui-react'
import { shape, func, arrayOf, bool, string } from 'prop-types'
import ReactMarkdown from 'react-markdown'
import scrollToComponent from 'react-scroll-to-component'
import { useProgress, useTitle } from '../../common/hooks'
import infotooltips from '../../common/InfoToolTips'
import { getCustomPopulation } from '../../redux/populations'
import {
getCustomPopulationSearches,
saveCustomPopulationSearch,
updateCustomPopulationSearch,
selectCustomPopulationSearch,
deleteCustomPopulationSearch
} from '../../redux/customPopulationSearch'
import { getCustomPopulationSearches } from '../../redux/customPopulationSearch'
import { getCustomPopulationCoursesByStudentnumbers } from '../../redux/populationCourses'
import CreditAccumulationGraphHighCharts from '../CreditAccumulationGraphHighCharts'
import SearchHistory from '../SearchHistory'
import PopulationStudents from '../PopulationStudents'
import CustomPopulationCourses from './CustomPopulationCourses'
import CustomPopulationProgrammeDist from './CustomPopulationProgrammeDist'
@@ -27,35 +18,27 @@ import useFilters from '../FilterTray/useFilters'
import { CustomPopulationFilters } from '../FilterTray/FilterSets'
import useFilterTray from '../FilterTray/useFilterTray'
import useLanguage from '../LanguagePicker/useLanguage'
import CustomPopulationSearch from './CustomPopulationSearch'
import UnihowDataExport from './UnihowDataExport'
const CustomPopulation = ({
getCustomPopulationDispatch,
getCustomPopulationCoursesByStudentnumbers,
getCustomPopulationSearchesDispatch,
saveCustomPopulationSearchDispatch,
updateCustomPopulationSearchDispatch,
selectCustomPopulationSearchDispatch,
deleteCustomPopulationSearchDispatch,
custompop,
customPopulationFlag,
loading,
customPopulationSearches,
latestCreatedCustomPopulationSearchId,
customPopulationSearchSaving,
searchedCustomPopulationSearchId
}) => {
const { language } = useLanguage()
const [modal, setModal] = useState(false)
const [input, setInput] = useState('')
const [name, setName] = useState('')
const [activeIndex, setIndex] = useState([])
const [selectedSearchId, setSelectedSearchId] = useState('')
const [, setSelectedSearchId] = useState('')
const [newestIndex, setNewest] = useState(null)
const { setAllStudents, filteredStudents } = useFilters()
const [trayOpen] = useFilterTray()
const selectedStudents = filteredStudents.map(stu => stu.studentNumber)
const { onProgress, progress } = useProgress(loading)
const { progress } = useProgress(loading)
const creditGainRef = useRef()
const programmeRef = useRef()
@@ -86,118 +69,6 @@ const CustomPopulation = ({
}
}, [latestCreatedCustomPopulationSearchId])
const handleNameChange = e => {
setName(e.target.value)
}
const clearForm = () => {
setName('')
setInput('')
setSelectedSearchId('')
}
const handleClose = () => {
setModal(false)
clearForm()
}
const onSave = () => {
const studentnumberlist = input.match(/[0-9]+/g)
if (selectedSearchId) {
updateCustomPopulationSearchDispatch({ id: selectedSearchId, studentnumberlist })
} else {
saveCustomPopulationSearchDispatch({ name, studentnumberlist })
}
}
const onDelete = () => {
if (selectedSearchId) {
deleteCustomPopulationSearchDispatch({ id: selectedSearchId })
clearForm()
}
}
const onSelectSearch = selected => {
if (!selected) {
clearForm()
return
}
const { id: selectedSearchId } = selected
const selectedSearch = customPopulationSearches.find(({ id }) => id === selectedSearchId)
if (selectedSearch) {
setInput(selectedSearch.students.join('\n'))
setName(selectedSearch.name)
setSelectedSearchId(selectedSearch.id)
}
}
const onClicker = e => {
e.preventDefault()
const studentnumbers = input.match(/[0-9]+/g)
getCustomPopulationDispatch({ studentnumberlist: studentnumbers, onProgress })
getCustomPopulationCoursesByStudentnumbers({ studentnumberlist: studentnumbers })
selectCustomPopulationSearchDispatch(selectedSearchId || null)
handleClose()
}
const renderCustomPopulationSearch = () => (
<Modal
trigger={
<Button size="small" color="blue" onClick={() => setModal(true)} data-cy="custom-pop-search-button">
Custom population
</Button>
}
open={modal}
onClose={handleClose}
size="small"
>
<Modal.Content>
<Form>
<h2> Custom population </h2>
<Form.Field>
<em> Insert name for this custom population if you wish to save it </em>
<Form.Input disabled={!!selectedSearchId} value={name} placeholder="name" onChange={handleNameChange} />
</Form.Field>
<Form.Field>
<em> Insert studentnumbers you wish to use for population here </em>
<TextArea
value={input}
placeholder="011111111"
onChange={e => setInput(e.target.value)}
data-cy="student-no-input"
/>
</Form.Field>
</Form>
<SearchHistory
header="Saved populations"
items={customPopulationSearches.map(s => ({
...s,
text: s.name,
timestamp: s.updatedAt,
params: { id: s.id }
}))}
updateItem={() => null}
handleSearch={onSelectSearch}
/>
</Modal.Content>
<Modal.Actions>
<Button
disabled={!name || customPopulationSearchSaving}
loading={customPopulationSearchSaving}
floated="left"
icon="save"
onClick={onSave}
content="Save"
/>
<Button disabled={!selectedSearchId} negative floated="left" icon="trash" onClick={onDelete} content="Delete" />
<Button onClick={handleClose}>Cancel</Button>
<Button positive onClick={e => onClicker(e)} data-cy="search-button">
Search population
</Button>
</Modal.Actions>
</Modal>
)
const handleClick = index => {
const indexes = [...activeIndex].sort()
if (indexes.includes(index)) {
@@ -215,29 +86,9 @@ const CustomPopulation = ({
key: 0,
title: {
content: (
<>
{activeIndex.includes(0) ? (
<>Credit accumulation (for {selectedStudents.length} students)</>
) : (
<Popup
trigger={
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Credit accumulation (for {selectedStudents.length} students)
</span>
}
position="top center"
wide="very"
>
<Popup.Content>
{' '}
<ReactMarkdown
source={infotooltips.PopulationStatistics.CreditAccumulationGraph.AccordionTitle}
escapeHtml={false}
/>
</Popup.Content>
</Popup>
)}
</>
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Credit accumulation (for {selectedStudents.length} students)
</span>
)
},
onTitleClick: () => handleClick(0),
@@ -259,29 +110,9 @@ const CustomPopulation = ({
key: 1,
title: {
content: (
<>
{activeIndex.includes(1) ? (
<>Programme distribution</>
) : (
<Popup
trigger={
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Programme distribution
</span>
}
position="top center"
wide="very"
>
<Popup.Content>
{' '}
<ReactMarkdown
source={infotooltips.PopulationStatistics.ProgrammeDistributionCoursePopulation}
escapeHtml={false}
/>
</Popup.Content>
</Popup>
)}
</>
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Programme distribution
</span>
)
},
onTitleClick: () => handleClick(1),
@@ -298,29 +129,9 @@ const CustomPopulation = ({
key: 2,
title: {
content: (
<>
{activeIndex.includes(2) ? (
<>Courses of population</>
) : (
<Popup
trigger={
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Courses of population
</span>
}
position="top center"
wide="very"
>
<Popup.Content>
{' '}
<ReactMarkdown
source={infotooltips.PopulationStatistics.CreditDistributionCoursePopulation}
escapeHtml={false}
/>
</Popup.Content>
</Popup>
)}
</>
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Courses of population
</span>
)
},
onTitleClick: () => handleClick(2),
@@ -336,29 +147,9 @@ const CustomPopulation = ({
key: 3,
title: {
content: (
<>
{activeIndex.includes(3) ? (
<>Students ({selectedStudents.length})</>
) : (
<Popup
trigger={
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Students ({selectedStudents.length})
</span>
}
position="top center"
wide="very"
>
<Popup.Content>
{' '}
<ReactMarkdown
source={infotooltips.PopulationStatistics.Students.AccordionTitle}
escapeHtml={false}
/>
</Popup.Content>
</Popup>
)}
</>
<span style={{ paddingTop: '1vh', paddingBottom: '1vh', color: 'black', fontSize: 'large' }}>
Students ({selectedStudents.length})
</span>
)
},
onTitleClick: () => handleClick(3),
@@ -367,8 +158,8 @@ const CustomPopulation = ({
<div ref={studentRef}>
<PopulationStudents
language={language}
mandatoryToggle={false}
filteredStudents={filteredStudents}
dataExport={<UnihowDataExport />}
customPopulation
/>
</div>
@@ -403,7 +194,7 @@ const CustomPopulation = ({
studentnumbers you have access rights to i.e. you have rights to the programme they are in.
</p>
</Message>
{renderCustomPopulationSearch()}
<CustomPopulationSearch />
{custompop.length > 0 && customPopulationFlag ? (
<Segment className="contentSegment">{renderCustomPopulation()}</Segment>
) : (
@@ -424,28 +215,19 @@ CustomPopulation.defaultProps = {
CustomPopulation.propTypes = {
custompop: arrayOf(shape({})).isRequired,
customPopulationFlag: bool.isRequired,
getCustomPopulationDispatch: func.isRequired,
getCustomPopulationCoursesByStudentnumbers: func.isRequired,
loading: bool.isRequired,
customPopulationSearches: arrayOf(shape({})).isRequired,
customPopulationSearchSaving: bool.isRequired,
latestCreatedCustomPopulationSearchId: string,
searchedCustomPopulationSearchId: string,
saveCustomPopulationSearchDispatch: func.isRequired,
getCustomPopulationSearchesDispatch: func.isRequired,
updateCustomPopulationSearchDispatch: func.isRequired,
selectCustomPopulationSearchDispatch: func.isRequired,
deleteCustomPopulationSearchDispatch: func.isRequired
getCustomPopulationSearchesDispatch: func.isRequired
}
const mapStateToProps = ({ populations, populationCourses, customPopulationSearch }) => ({
loading: populations.pending,
custompop: populations.data.students || [],
courses: populationCourses.data,
pending: populationCourses.pending,
customPopulationFlag: populations.customPopulationFlag,
customPopulationSearches: customPopulationSearch.customPopulationSearches,
customPopulationSearchSaving: customPopulationSearch.saving,
latestCreatedCustomPopulationSearchId: customPopulationSearch.latestCreatedCustomPopulationSearchId,
searchedCustomPopulationSearchId: customPopulationSearch.searchedCustomPopulationSearchId
})
@@ -453,12 +235,7 @@ const mapStateToProps = ({ populations, populationCourses, customPopulationSearc
export default connect(
mapStateToProps,
{
getCustomPopulationDispatch: getCustomPopulation,
getCustomPopulationCoursesByStudentnumbers,
saveCustomPopulationSearchDispatch: saveCustomPopulationSearch,
getCustomPopulationSearchesDispatch: getCustomPopulationSearches,
updateCustomPopulationSearchDispatch: updateCustomPopulationSearch,
selectCustomPopulationSearchDispatch: selectCustomPopulationSearch,
deleteCustomPopulationSearchDispatch: deleteCustomPopulationSearch
getCustomPopulationSearchesDispatch: getCustomPopulationSearches
}
)(CustomPopulation)

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