Compare commits

...
563 Commits
Author SHA1 Message Date
Matti LuukkainenandGitHub 7e5c7fca90 Merge pull request #429 from UniversityOfHelsinkiCS/mluukkai-patch-1
Update README.md
2018-10-01 14:22:15 +03:00
Matti LuukkainenandGitHub 2522c2030f Update README.md 2018-10-01 14:05:24 +03:00
Matti LuukkainenandGitHub 8ede378faf Merge pull request #428 from UniversityOfHelsinkiCS/trunk
Sober
2018-10-01 14:04:23 +03:00
Matti Luukkainen 0f968d073f fix 2018-10-01 13:28:34 +03:00
Matti Luukkainen cb19e81709 better filter naming, cleanup for course search 2018-10-01 13:25:16 +03:00
Sasu MäkinenandGitHub 937d607237 Merge pull request #427 from UniversityOfHelsinkiCS/trunk
textAndDescription search and alert for IE
2018-09-27 17:44:38 +03:00
Mäkinen Sasu S 503e540814 eslint 2018-09-27 17:38:23 +03:00
Mäkinen Sasu S e92e3ccc31 eslint 2018-09-27 17:37:43 +03:00
Mäkinen Sasu S e749dcfde2 textAndDescription search and alert for IE 2018-09-27 17:36:46 +03:00
Sasu MäkinenandGitHub bf35389164 Merge pull request #426 from UniversityOfHelsinkiCS/trunk
rare memes
2018-09-27 16:31:44 +03:00
Mäkinen Sasu S 94544d90c2 eslint 2018-09-27 16:25:18 +03:00
Mäkinen Sasu S 486e36476c rarer memes 2018-09-27 16:24:30 +03:00
Sasu MäkinenandGitHub 6b3dd8dd57 Merge pull request #425 from UniversityOfHelsinkiCS/trunk
irtomikko
2018-09-27 16:16:51 +03:00
Mäkinen Sasu S 831343ee4c rm clog 2018-09-27 14:59:40 +03:00
Mäkinen Sasu S 0919101419 irtomikko easter egg 2018-09-27 14:58:51 +03:00
Mäkinen Sasu S 3a9ac1fc6b bugfix 2018-09-27 10:07:48 +03:00
Mäkinen Sasu S 7dcbce6c09 course search shows result only when focused 2018-09-27 09:55:45 +03:00
ikuismaandGitHub 3fef2d1a74 Merge pull request #424 from UniversityOfHelsinkiCS/trunk
Clear course stats result
2018-09-27 08:14:15 +03:00
ikuisma 93ae07261c Clear course stats search from store when form is expanded 2018-09-27 07:59:34 +03:00
ikuisma 4b764b11cc Clear course search results when course stats form becomes visible 2018-09-27 07:51:48 +03:00
ikuisma 23aea9bb06 Clear course search form 2018-09-27 07:51:18 +03:00
ikuisma 2845dd99ba New clear action to generic item reducer 2018-09-27 07:51:00 +03:00
ikuismaandGitHub 328748dd00 Merge pull request #423 from UniversityOfHelsinkiCS/trunk
Single-view course stats search form
2018-09-27 07:41:24 +03:00
ikuisma 78c89589cd Single-view course stats search form 2018-09-27 07:30:58 +03:00
Matti LuukkainenandGitHub 6e16f0a2f4 Merge pull request #422 from UniversityOfHelsinkiCS/trunk
toggle grade chart out from normal users
2018-09-26 17:29:23 +03:00
Matti Luukkainen 0507f20c56 toggle grade chart out from normal users 2018-09-26 17:16:54 +03:00
Matti Luukkainen def60b1a64 hide link to coursepage 2018-09-26 14:38:32 +03:00
ikuismaandGitHub 46f5817d2f Merge pull request #421 from UniversityOfHelsinkiCS/trunk
New user edit page
2018-09-25 19:59:33 +03:00
ikuismaandGitHub 65880ec63a Merge branch 'master' into trunk 2018-09-25 19:49:19 +03:00
ikuisma 017a754af7 lint fix 2018-09-25 19:46:38 +03:00
ikuisma bc4c4ab02b New users page with updated SRE access form 2018-09-25 19:45:21 +03:00
ikuisma a31284586b New SRE reducer to redux + method for making POST to new SRE access endpoint. 2018-09-25 19:44:43 +03:00
ikuisma 8e3141680a Refactored EnableUsers page to use UserPage component 2018-09-25 18:03:06 +03:00
ikuisma dfc9bd2d88 New user page component that was gutted from massive EnableUsers component 2018-09-25 18:02:33 +03:00
Sasu MäkinenandGitHub 504aa731ba Merge pull request #419 from UniversityOfHelsinkiCS/trunk
New Forms
2018-09-25 15:14:04 +03:00
Mäkinen Sasu S 59eeea02e8 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-09-25 15:00:20 +03:00
Mäkinen Sasu S c0730b2b5f new population query form 2018-09-25 14:59:54 +03:00
ikuisma b33c3a3376 Type and discipline to course search 2018-09-25 12:10:34 +03:00
ikuisma 44dda86745 Refactor course statistics to use new auto submit search input component. 2018-09-25 11:47:09 +03:00
ikuisma f85661e128 New generic auto submit search input component. 2018-09-25 11:46:15 +03:00
ikuisma b8051a59f1 Disable submit button if form is not filled out 2018-09-25 10:33:39 +03:00
Sasu MäkinenandGitHub a87e7abc0c Merge pull request #415 from UniversityOfHelsinkiCS/trunk
Course statistics
2018-09-24 08:56:00 +03:00
Mäkinen Sasu S 646abf09cb student view on course statistics 2018-09-21 13:58:09 +03:00
Mäkinen Sasu S 5384a29acc removed old course statistics 2018-09-20 16:08:07 +03:00
Mäkinen Sasu S d06739d35f course graphs redone for new course page with highcharts instead of recharts 2018-09-20 16:07:08 +03:00
ikuisma 6cdff731c9 Use new cumulative table in summary tab + better proptypes. 2018-09-20 10:49:03 +03:00
ikuisma 2515ed6ea5 New separate component for cumulative category stat table. 2018-09-20 10:47:35 +03:00
ikuisma 1b49b46e07 Refactor summary tab to use redux and selectors instead of own state. 2018-09-20 09:11:07 +03:00
ikuisma af3e6b44e9 Selector for calculating course summary stats for a specific study programme. 2018-09-20 09:10:31 +03:00
ikuisma d013216b72 Hook up reducer 2018-09-20 09:09:47 +03:00
ikuisma 9dacec810b Form reducer for course summary form to allow for use of reselect in stat calculation. 2018-09-20 09:09:33 +03:00
ikuisma 33fd9587dc Generic form reducer 2018-09-20 09:09:02 +03:00
ikuisma 2107cc72a8 Removed unused import 2018-09-19 18:14:17 +03:00
ikuisma 9ba2fbf774 First viable version of new course stats page 2018-09-19 18:13:39 +03:00
Sasu MäkinenandGitHub f5798f6971 Merge pull request #401 from UniversityOfHelsinkiCS/trunk
Academic year reprsesentation, graduation dates and email service
2018-09-13 15:56:48 +03:00
Mäkinen Sasu S 8138971ae5 send mail on user enable 2018-09-13 15:42:48 +03:00
Mäkinen Sasu S 77b2f2a7fc added graduation dates and info of canceled studyright to student statistics. Also sorted studyrights 2018-09-13 12:14:10 +03:00
Mäkinen Sasu S c8caf84072 Updated population query form and card to academic year representation 2018-09-13 10:47:39 +03:00
Matti LuukkainenandGitHub 1942c87d1f Merge pull request #396 from UniversityOfHelsinkiCS/trunk
population query improvements
2018-09-10 14:22:58 +03:00
Mäkinen Sasu S 78011eaef1 Population query can be now chosen from both FALL and SPRING and considers the year as ACADEMIC YEAR 2018-09-10 13:49:32 +03:00
Mäkinen Sasu S 0d2cf58289 filtering out people who canceled their studyright as default 2018-09-05 08:37:20 +03:00
Sasu MäkinenandGitHub fbe21bed1b Merge pull request #385 from UniversityOfHelsinkiCS/trunk
changed numbers in credit statistics
2018-09-04 17:27:35 +03:00
Mäkinen Sasu S 12c585acde changed numbers in credit statistics 2018-09-04 17:20:14 +03:00
Sasu MäkinenandGitHub e829c35396 Merge pull request #384 from UniversityOfHelsinkiCS/trunk
priority filter
2018-09-04 16:41:15 +03:00
Mäkinen Sasu S d4371d2e09 priority filter 2018-09-04 16:28:03 +03:00
ikuismaandGitHub 090dfb7665 Merge pull request #383 from UniversityOfHelsinkiCS/trunk
Category selection to teacher leader board + info tooltips for filters
2018-09-03 13:07:36 +03:00
ikuisma 3cc06c5a7b Removed redundant getSemesters call from teacher leader board. 2018-09-03 13:00:26 +03:00
ikuisma 62bfbd884e Implemented category selection to teacher leader board. 2018-09-03 12:59:23 +03:00
ikuisma 2b243a0178 Add category arg to get top teachers action. 2018-09-03 12:58:32 +03:00
ikuisma 69bf9b60f4 Hook up top teacher categories reducer to store. 2018-09-03 12:57:59 +03:00
ikuisma 2392c36c98 New itemreducer for categories of top teachers. 2018-09-03 12:57:36 +03:00
ikuisma 601ea18df8 Fixed data formatting of leader board to match format returned by newer version of API. 2018-08-31 14:34:09 +03:00
Mäkinen Sasu S b51fea8ca9 clicking teacher id now jumps to the teachers page 2018-08-31 12:37:18 +03:00
Mäkinen Sasu S 929ed9d8e8 eslint 2018-08-30 15:17:48 +03:00
Mäkinen Sasu S 436ff94a06 put quarter stats behind a tab 2018-08-30 15:16:33 +03:00
Mäkinen Sasu S 4ce1f9a45e added english infotexts to all filters 2018-08-30 14:52:13 +03:00
Mäkinen Sasu S 5dd3022006 created infotooltip for creditsatleast 2018-08-29 13:46:24 +03:00
ikuismaandGitHub 7b5715fca8 Merge pull request #378 from UniversityOfHelsinkiCS/trunk
Teacher course statistics separation by semester/course
2018-08-29 12:08:08 +03:00
ikuisma 5f0034c4bf Broke function that exceeded lint max line length. 2018-08-29 11:56:46 +03:00
ikuisma 83ac1f6ff9 Removed unused component. 2018-08-29 11:55:14 +03:00
ikuisma 09d7ca36e8 Added new Credits tab for to teacher details. 2018-08-29 11:54:56 +03:00
ikuisma 09c47f2d1e Implemented new CoursesTab with semester/course -wise stats. 2018-08-29 11:53:33 +03:00
ikuisma 1ae263d5c6 Changed no stats message from plain text to Segment. 2018-08-29 11:52:33 +03:00
Matti LuukkainenandGitHub 43f34a417d Merge pull request #375 from UniversityOfHelsinkiCS/trunk
Close button for teacher stats
2018-08-28 16:12:41 +03:00
ikuisma 90975a6ae9 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-28 16:02:58 +03:00
ikuisma fefb37e510 Auto submit academic year in teacher leader board. 2018-08-28 16:02:53 +03:00
Matti LuukkainenandGitHub 6edccdd1a8 Merge branch 'master' into trunk 2018-08-28 16:01:58 +03:00
Matti Luukkainen ea21d8bfd9 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-28 15:59:42 +03:00
ikuismaandGitHub c048e5c1a7 Merge pull request #374 from UniversityOfHelsinkiCS/trunk
Academic year based search for teacher leader board
2018-08-28 15:21:23 +03:00
ikuisma 07b2a665bc Implemented searchable teacher leader board. 2018-08-28 15:13:46 +03:00
ikuisma b33181d8cc Add yearcode to top teachers API call function 2018-08-28 15:12:48 +03:00
Matti Luukkainen 0539306034 close button for teacher stats 2018-08-28 13:52:35 +03:00
ikuismaandGitHub 54300120fc Merge pull request #372 from UniversityOfHelsinkiCS/trunk
Add loader to individual teacher page.
2018-08-27 15:35:35 +03:00
ikuisma 0385a00bc7 Add loader to individual teacher page. 2018-08-27 13:43:38 +03:00
ikuismaandGitHub 7126943ccc Merge pull request #371 from UniversityOfHelsinkiCS/trunk
Teacher stats leaderboard + teacher stats visibility based on new roles.
2018-08-27 12:48:39 +03:00
ikuisma f4e82c7d1e Leader board to teacher statistics + icons to tabs. 2018-08-27 10:56:07 +03:00
ikuisma b6c0dbfa50 New teachers leader board component. 2018-08-27 10:55:35 +03:00
ikuisma a78a19d1ce Add top teachers reducer to store. 2018-08-27 10:55:08 +03:00
ikuisma 330c7a1290 Basic reducer for top teachers. 2018-08-27 10:54:45 +03:00
Mäkinen Sasu S 1c452d498d canceled studyright filter 2018-08-24 17:55:29 +03:00
Mäkinen Sasu S 3d92df6316 default sort in teacher table 2018-08-24 15:32:00 +03:00
Mäkinen Sasu S a822a4a33b merge 2018-08-24 14:18:27 +03:00
Mäkinen Sasu S 7cd202065c Teacher statistics is now shown for admins and czars 2018-08-24 14:17:06 +03:00
Matti LuukkainenandGitHub 18b5699cca Merge pull request #363 from UniversityOfHelsinkiCS/trunk
user request details
2018-08-23 18:29:13 +03:00
Matti LuukkainenandGitHub a44b02cc88 Merge branch 'master' into trunk 2018-08-23 18:22:36 +03:00
Matti Luukkainen a139e99c01 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-23 18:17:36 +03:00
Matti Luukkainen 40f68d2f43 users requests 2018-08-23 18:16:09 +03:00
ikuismaandGitHub 73c97b925f Merge pull request #360 from UniversityOfHelsinkiCS/trunk
Teacher statistics page to use semester codes instead of year codes for queries
2018-08-23 08:48:08 +03:00
ikuisma 3420d6fc09 Changed teacher stats search to use semester start + end date for queries. 2018-08-23 08:36:34 +03:00
ikuisma a200d5103b Changed order of tabs in teacher statistics. 2018-08-23 08:35:53 +03:00
ikuisma 2f2cef2560 Added instructional message to teacher search page. 2018-08-23 08:35:09 +03:00
ikuisma 7cbe19b041 Fix teacher statistics query params to match new API. 2018-08-23 08:34:15 +03:00
Sasu MäkinenandGitHub 648ec2e7fe Merge pull request #358 from UniversityOfHelsinkiCS/trunk
Czars, Teachers and Courses
2018-08-22 16:06:13 +03:00
Mäkinen Sasu S 7a6dcd2520 remove clog and merge 2018-08-22 15:59:35 +03:00
Mäkinen Sasu S d6bde89de8 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-22 15:59:15 +03:00
Mäkinen Sasu S 4ad257f227 coronate to czar at users page 2018-08-22 15:58:46 +03:00
ikuisma d3bdb75156 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-22 15:23:20 +03:00
ikuisma 1e0364c6ef Add sorting to teacher statistics table. 2018-08-22 15:23:12 +03:00
totutotu b5cd123fbb Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-22 15:14:59 +03:00
totutotu a572d387dc Fixed course stats search to include query by code 2018-08-22 15:14:56 +03:00
ikuismaandGitHub 37b76cfd73 Merge pull request #355 from UniversityOfHelsinkiCS/trunk
Teacher yearly statistics page + statistics table refactoring.
2018-08-22 12:49:24 +03:00
ikuisma 2fc8ca6e78 Teacher yearly statistics components. 2018-08-22 12:19:20 +03:00
ikuisma 59983149e5 New reducers required for teacher statistics. 2018-08-22 12:18:53 +03:00
ikuisma 2f2a0728f8 Helper method for creating generic item reducers. 2018-08-22 12:18:14 +03:00
ikuisma d61ca334e5 Fixed query parameter issue. 2018-08-22 11:28:21 +03:00
Sasu MäkinenandGitHub 29283e06fc Merge pull request #350 from UniversityOfHelsinkiCS/trunk
course comparing table
2018-08-21 15:17:18 +03:00
Mäkinen Sasu S 0185d496ed and eslint 2018-08-21 15:06:32 +03:00
Mäkinen Sasu S 06435ab251 remove clog 2018-08-21 15:05:46 +03:00
Mäkinen Sasu S 1c4ad598c2 added comparing to table 2018-08-21 15:05:08 +03:00
Sasu MäkinenandGitHub 4cae824f1b Merge pull request #349 from UniversityOfHelsinkiCS/trunk
comparing courses and courseparticipationNtimes filter and some refactoring
2018-08-21 14:21:54 +03:00
Mäkinen Sasu S 975dcae051 put teachers behind admin users 2018-08-21 14:14:36 +03:00
Mäkinen Sasu S dc75fdf09e remove clog 2018-08-21 14:02:39 +03:00
Mäkinen Sasu S cc76f77831 some fixin 2018-08-21 14:01:43 +03:00
Mäkinen Sasu S ff0383881c fixed stuff totutotu broke 2018-08-21 13:11:19 +03:00
Mäkinen Sasu S 3470ff1892 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-21 12:51:02 +03:00
Mäkinen Sasu S 11e3cc6064 comparing courses is now possible 2018-08-21 12:50:55 +03:00
totutotu c3262ae30b Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-21 12:35:53 +03:00
totutotu 89b503ce31 Course participation filter of Intern. Progress Test for medstudents 2018-08-21 12:35:47 +03:00
ikuisma d12176d2bf Refactored population statistics queries to use params argument instead of hardcoded param string. 2018-08-21 11:35:21 +03:00
ikuisma a3777e8666 Added optional query param passing to axios requests. 2018-08-21 11:33:49 +03:00
ikuisma 67ec3ae395 Add functions for generating _SUCCESS, etc. action types for given prefix. 2018-08-21 10:43:22 +03:00
Matti LuukkainenandGitHub 383dedec0c Merge pull request #347 from UniversityOfHelsinkiCS/trunk
refactoring usage stats
2018-08-21 00:20:18 +03:00
Matti Luukkainen 621f9ab851 refactoring usage stats 2018-08-21 00:08:45 +03:00
Matti LuukkainenandGitHub 925009b671 Merge pull request #346 from UniversityOfHelsinkiCS/trunk
debug
2018-08-20 23:40:59 +03:00
Matti Luukkainen ad8ff6b38d debug 2018-08-20 23:30:55 +03:00
Matti LuukkainenandGitHub c641df4a3e Merge pull request #345 from UniversityOfHelsinkiCS/trunk
replace direct axios usage with callApi
2018-08-20 23:16:18 +03:00
Matti LuukkainenandGitHub 54152e0128 Merge branch 'master' into trunk 2018-08-20 23:08:19 +03:00
Matti Luukkainen 445d25a830 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-20 18:43:47 +03:00
Matti Luukkainen 9b750ddba6 replace direct axios usage with callApi 2018-08-20 18:42:17 +03:00
Matti LuukkainenandGitHub 02c3b19128 Merge pull request #344 from UniversityOfHelsinkiCS/trunk
usage stats, quick'n'dirty
2018-08-20 18:26:01 +03:00
Matti LuukkainenandGitHub e4219ac039 Merge branch 'master' into trunk 2018-08-20 18:19:47 +03:00
Matti Luukkainen 449a7aa6d4 fixes 2018-08-20 18:16:31 +03:00
Matti Luukkainen 97b4e3bcb6 first edition of usage stats, quick and dirty 2018-08-20 18:09:51 +03:00
Matti Luukkainen c8ff5eb0ae first edition of usage stats, quick and dirty 2018-08-20 18:08:58 +03:00
ikuisma 8ce83243ee Removed unused import 2018-08-20 17:03:21 +03:00
ikuisma 24c9032293 Refactored name of TeacherStatistics to Teachers - reserving previous name for new component. 2018-08-20 17:02:45 +03:00
totutotu 51430fdb73 'Selected' table not visible if non selected 2018-08-20 15:50:34 +03:00
ikuismaandGitHub 3249cb0544 Merge pull request #342 from UniversityOfHelsinkiCS/trunk
Trunk
2018-08-20 13:57:28 +03:00
totutotu bd3a920a6d Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-20 13:50:25 +03:00
ikuisma 029ac4ce4b Placeholder text to teacher stats table if no stats found. 2018-08-20 13:49:59 +03:00
totutotu 38d12d9724 Improvements to course stats search form 2018-08-20 13:49:37 +03:00
ikuismaandGitHub 3b12eceb70 Merge pull request #340 from UniversityOfHelsinkiCS/trunk
First version of teacher statistics page for individual teachers
2018-08-20 13:27:10 +03:00
ikuisma 07eda34bd6 Implemented first version of teacher statistics page for individual teachers. 2018-08-20 12:38:49 +03:00
Matti LuukkainenandGitHub f76ab7357f Merge pull request #338 from UniversityOfHelsinkiCS/trunk
user emails
2018-08-18 15:57:31 +03:00
Matti LuukkainenandGitHub 9fc5545aa9 Merge branch 'master' into trunk 2018-08-18 15:48:56 +03:00
Matti Luukkainen 8c539882ec Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-18 15:47:19 +03:00
Matti Luukkainen 0646de00bc user emails 2018-08-18 15:47:15 +03:00
Sasu MäkinenandGitHub 814d20d915 Merge pull request #336 from UniversityOfHelsinkiCS/trunk
teacher statistics and hackfix
2018-08-17 15:33:54 +03:00
Mäkinen Sasu S bb21d7c789 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-17 15:26:53 +03:00
Mäkinen Sasu S b25c10095b hackfixed legacy hackfix in webpack 2018-08-17 15:26:36 +03:00
Mäkinen Sasu S a6ddb2ee57 Revert "moved hot-loader to dev dependencies"
This reverts commit 816ae576d7.
2018-08-17 13:31:02 +03:00
Mäkinen Sasu S 816ae576d7 moved hot-loader to dev dependencies 2018-08-17 13:30:11 +03:00
ikuisma b4d38ad3a1 Route to TeacherStatistics 2018-08-17 12:59:37 +03:00
ikuisma e56426f7d6 Frontend route for teachers and teachers/:id 2018-08-17 12:59:05 +03:00
Sasu MäkinenandGitHub 8eaaf78cd9 Merge pull request #335 from UniversityOfHelsinkiCS/trunk
Extent graduated rewrite and added as default filter
2018-08-17 12:58:38 +03:00
ikuisma c1a7eae149 Teacher statistics components. 2018-08-17 12:58:28 +03:00
ikuisma b1da93e508 Reducer for querying teachers endpoint. 2018-08-17 12:57:54 +03:00
Sasu MäkinenandGitHub e8f56e266f Merge branch 'master' into trunk 2018-08-17 12:51:17 +03:00
Mäkinen Sasu S d7ebc4ebbe proptype fix 2018-08-17 12:50:26 +03:00
Mäkinen Sasu S 39cd9b6ab3 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-17 12:33:08 +03:00
Mäkinen Sasu S a315c0f37c rewritten ExtentGraduated Filter and added some default filters for all populations. Removed all kludges and legacy bullshit from Highcharts credit accumulation graph. Updated react-hot-loader 2018-08-17 12:32:29 +03:00
Tuomo TorppaandGitHub 96a99481f7 Merge pull request #332 from UniversityOfHelsinkiCS/trunk
New course stats form
2018-08-17 10:42:20 +03:00
Tuomo TorppaandGitHub 68f3145716 Merge pull request #331 from UniversityOfHelsinkiCS/new_coursestats
New coursestats
2018-08-17 10:34:36 +03:00
totutotu 2d80613c0d New course stats search form MVP completed 2018-08-17 10:31:14 +03:00
totutotu f40c27d8db New coures statistics 2018-08-16 16:18:24 +03:00
totutotu 15fc168855 v2 added to new enpoint 2018-08-16 10:20:32 +03:00
totutotu 175732fff8 css fix 2018-08-16 09:58:04 +03:00
totutotu 8a08ec16e5 new coursestats, not staging ready yet 2018-08-16 09:53:42 +03:00
Sasu MäkinenandGitHub e0f78b8b83 Merge pull request #328 from UniversityOfHelsinkiCS/trunk
transferfilterfix
2018-08-14 15:07:06 +03:00
Mäkinen Sasu S 534f828679 transferfilterfix 2018-08-14 14:51:29 +03:00
Sasu MäkinenandGitHub 343ba5d629 Merge pull request #319 from UniversityOfHelsinkiCS/trunk
table sorting
2018-08-13 13:36:07 +03:00
Sasu MäkinenandGitHub 8cd4d9fd1f Merge branch 'master' into trunk 2018-08-13 13:28:25 +03:00
Mäkinen Sasu S a3767cca58 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-13 13:24:16 +03:00
Mäkinen Sasu S 5ab92540c0 table sorting greater again 2018-08-13 13:23:29 +03:00
Sasu MäkinenandGitHub 3a0399f3cc Merge pull request #314 from UniversityOfHelsinkiCS/trunk
@sasumaki (drunk) link from population statistics course statistics table course to course statistics with fitting query
2018-08-13 10:03:57 +03:00
Sasu MäkinenandGitHub 774d21c4cf Merge branch 'master' into trunk 2018-08-10 16:41:50 +03:00
Mäkinen Sasu S 4e36236ffd eslint fix 2018-08-10 16:39:02 +03:00
Mäkinen Sasu S 8d39dbb067 link from population statistics course statistics table course cell to course statistics with fitting query 2018-08-10 16:38:30 +03:00
Mäkinen Sasu S c0d8f9bf22 transfer filter back online 2018-08-10 11:55:35 +03:00
Sasu MäkinenandGitHub 2c3514f992 Merge pull request #309 from UniversityOfHelsinkiCS/trunk
Filters and grade distributions redone
2018-08-09 15:49:38 +03:00
Mäkinen Sasu S be05cb834d lint fix 2018-08-09 15:43:26 +03:00
Mäkinen Sasu S 98c13d35a0 redone population statistics course grade distribution table 2018-08-09 15:42:40 +03:00
Mäkinen Sasu S 1ab1d61818 Saving filters redone with description 2018-08-09 12:11:28 +03:00
Sasu MäkinenandGitHub 0f53b6337e Merge pull request #305 from UniversityOfHelsinkiCS/trunk
Advanced options for filters and remove gender filter
2018-08-08 17:55:19 +03:00
Mäkinen Sasu S e00762d595 made superbara use advanced filters 2018-08-08 17:32:13 +03:00
Mäkinen Sasu S 97ead4877a Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-08 17:28:50 +03:00
Mäkinen Sasu S a0cc9f9030 Hide some filter features behind advanced filters option and removed gender filter from use 2018-08-08 17:28:22 +03:00
Tuomo TorppaandGitHub 4676c0a8cb Merge pull request #294 from UniversityOfHelsinkiCS/trunk
Order course search by latest instance held
2018-08-08 10:37:09 +03:00
totutotu 4b085ee560 Order course search by latest instance held 2018-08-08 10:26:04 +03:00
Tuomo TorppaandGitHub 2bf74aa5ef Merge pull request #292 from UniversityOfHelsinkiCS/trunk
Course stats programs update
2018-08-07 16:08:24 +03:00
totutotu e5c8b1dec8 failure % to pass rate 2018-08-07 16:01:48 +03:00
totutotu e2be82aa8f Headers to course stats, stats table NaN bug fixed 2018-08-07 15:55:57 +03:00
totutotu 6e597525f5 Show amount od students in each study programme and order dropdown 2018-08-07 15:31:31 +03:00
totutotu dc049bfea6 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-07 14:12:20 +03:00
totutotu 13ee8e00e2 Programmes to new data format 2018-08-07 14:12:14 +03:00
Mäkinen Sasu S 1519806e07 superbara now works with new dataset 2018-08-07 13:47:42 +03:00
Sasu MäkinenandGitHub 16f71a30e6 Merge pull request #290 from UniversityOfHelsinkiCS/trunk
bugfixes
2018-08-07 12:55:05 +03:00
Mäkinen Sasu S d92bdd64d8 fixed default course student limit amount 2018-08-07 10:30:52 +03:00
Mäkinen Sasu S a2204d1723 fixed rounding error at semseter enrollment 2018-08-07 09:45:51 +03:00
Mäkinen Sasu S 5e0666db64 mapped language to course participation ffilters 2018-08-07 09:38:34 +03:00
ikuismaandGitHub c49145ba42 Merge pull request #288 from UniversityOfHelsinkiCS/trunk
Users page improvements Part 1.
2018-08-06 14:12:45 +03:00
ikuisma 9bf058d256 Implemented single-component version of new users page with /users/:id endpoint for single users. 2018-08-06 14:03:56 +03:00
ikuisma d7194d3873 Added missing pending=false to successful user GET in reducer. 2018-08-06 14:03:08 +03:00
Tuomo TorppaandGitHub 8b026fd215 Merge pull request #286 from UniversityOfHelsinkiCS/trunk
Language chooser to student page 60792a8
2018-08-03 15:27:13 +03:00
totutotu e1de28cb56 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-03 15:20:24 +03:00
totutotu 60792a80d8 Language chooser to student page 2018-08-03 15:20:19 +03:00
Tuomo TorppaandGitHub 38d0b92bea Merge pull request #284 from UniversityOfHelsinkiCS/trunk
Languages based on user preference, anonymizer and studyrights to course statistics
2018-08-03 14:58:47 +03:00
Mäkinen Sasu S 682193c742 surprise eslint 2018-08-03 14:50:39 +03:00
Mäkinen Sasu S 2a14bedb0e merge 2018-08-03 14:50:07 +03:00
Mäkinen Sasu S e7aa6fb439 studyright selection to coursestatistics 2018-08-03 14:46:51 +03:00
totutotu 425b86d5a0 Languages from users preferences from DB 2018-08-03 14:37:29 +03:00
totutotu d2e850ebf5 Languages to course table and everywhere 2018-08-03 12:34:29 +03:00
totutotu 917cc29286 Course statistics languages 2018-08-03 10:57:40 +03:00
ikuismaandGitHub a4fb790f13 Merge pull request #283 from UniversityOfHelsinkiCS/trunk
Separate reducers for studyrightelements that are used by users and pop. statistics page
2018-08-03 10:54:01 +03:00
ikuisma 6c29f8abc1 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-03 10:36:21 +03:00
ikuisma bf19a78202 Replace PopulationSearchFrom unit request to new endpoint 2018-08-03 10:36:11 +03:00
ikuisma f4e6d93d7c New reducer for population statistics degrees and programmes. 2018-08-03 10:35:21 +03:00
Tuomo TorppaandGitHub 12b9e62ddc Merge pull request #282 from UniversityOfHelsinkiCS/trunk
Languages to course search
2018-08-03 10:33:14 +03:00
totutotu 9fd4c8d610 Languages to course search 2018-08-03 10:21:47 +03:00
Tuomo TorppaandGitHub 436e28464d Merge pull request #280 from UniversityOfHelsinkiCS/trunk
Languages to users page, populations page bug fixed
2018-08-03 09:50:48 +03:00
totutotu cf19ddbb4e Languages for user management page 2018-08-03 09:43:53 +03:00
totutotu a94b3693e9 course code mapping ui improvement 2018-08-03 09:06:14 +03:00
totutotu 2f6ae7e88d population page filter bug fixed 2018-08-03 08:52:59 +03:00
totutotu b59a41a766 Commented not-complete language chooser 2018-08-02 15:38:06 +03:00
totutotu 652c31d97c Languages 2018-08-02 15:32:24 +03:00
totutotu dacb82b610 Languages to course code mapper 2018-08-02 15:32:12 +03:00
Tuomo TorppaandGitHub 822ce8af6c Merge pull request #278 from UniversityOfHelsinkiCS/trunk
Trunk
2018-08-02 14:27:23 +03:00
totutotu e7bea0302c Removed console.lof 2018-08-02 14:18:00 +03:00
totutotu 58b1886857 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-02 14:16:14 +03:00
totutotu 27fcff3075 Languages for courses 2018-08-02 14:16:07 +03:00
ikuismaandGitHub a6da50cf19 Merge pull request #277 from UniversityOfHelsinkiCS/trunk
Language selection to Population page and replacement text for invalid student start dates.
2018-08-02 13:20:32 +03:00
ikuisma 50d0586db4 Changed invalid date to be 'Unavailable' instead. ' 2018-08-02 13:11:10 +03:00
totutotu 147864affc Fixed merge 2018-08-02 12:48:32 +03:00
totutotu 82aea61f12 Language selection to Population page 2018-08-02 12:45:52 +03:00
Sasu MäkinenandGitHub 4516e3d328 Merge pull request #274 from UniversityOfHelsinkiCS/trunk
bugfix
2018-08-01 12:10:43 +03:00
Mäkinen Sasu S 2913e01cff Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-01 12:02:12 +03:00
Mäkinen Sasu S 5d1c9feb87 rightfix 2018-08-01 12:01:55 +03:00
Tuomo TorppaandGitHub f64349e695 Merge pull request #273 from UniversityOfHelsinkiCS/trunk
Credit gain table filter to population page, bugfixes
2018-08-01 10:59:55 +03:00
totutotu dad9ebcada population credit gain table division by zero avoided 2018-07-31 13:29:43 +03:00
totutotu f38cfac57f Population credit gain table size fix 2018-07-31 13:04:45 +03:00
totutotu a30e374246 Filter population by clicking credit amount in credit gain table 2018-07-31 12:24:20 +03:00
Mäkinen Sasu S 8036fdd9ac typo fix 2018-07-30 14:30:03 +03:00
Mäkinen Sasu S 25af5d47b7 bugfix2 2018-07-30 13:41:45 +03:00
Sasu MäkinenandGitHub a2d3cf083f Merge pull request #270 from UniversityOfHelsinkiCS/trunk
bugfix
2018-07-30 13:37:32 +03:00
Mäkinen Sasu S 42de728ce5 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-07-30 13:31:21 +03:00
Mäkinen Sasu S 253fe616ac removed highlevelname 2018-07-30 13:31:07 +03:00
Tuomo TorppaandGitHub f1995e84a3 Merge pull request #268 from UniversityOfHelsinkiCS/trunk
Population coursetable new sorting, extent graduation filters rework, updated at fields for students and population
2018-07-30 12:07:27 +03:00
totutotu 422c16bfa5 Sort courses by % of population passed and attempted 2018-07-30 10:21:43 +03:00
Mäkinen Sasu S dc9c10df32 added updated at field for population 2018-07-27 16:27:33 +03:00
Mäkinen Sasu S d7d7295647 added an error message if user has no rights to any student data 2018-07-27 15:38:43 +03:00
Mäkinen Sasu S 6e63ea3363 updated at to student 2018-07-27 14:06:28 +03:00
Mäkinen Sasu S 78a04b6a46 Reworked extent graduation filters and added functionality to create persistant filters 2018-07-26 16:27:54 +03:00
Sasu MäkinenandGitHub 3f2c41c185 Merge pull request #266 from UniversityOfHelsinkiCS/trunk
Million new features
2018-07-26 13:54:14 +03:00
Mäkinen Sasu S c0e7c03f19 Filter by students transferring study programme and some unifying of syntax between filters 2018-07-26 13:42:45 +03:00
Mäkinen Sasu S c799b0edbc cleanup crew 2018-07-25 15:23:49 +03:00
Mäkinen Sasu S 5c478c1d63 Enrollment filter done 2018-07-25 13:52:08 +03:00
Mäkinen Sasu S e3bf53bce3 fixed saving course filters 2018-07-24 15:47:58 +03:00
Mäkinen Sasu S f0cbd6611e fixed coursesearch 2018-07-24 15:16:03 +03:00
Mäkinen Sasu S e304c21523 filtering for discipline and type now possible 2018-07-24 13:59:33 +03:00
Mäkinen Sasu S 4c284a4483 reworked student statistics studyrights table 2018-07-23 15:22:19 +03:00
Mäkinen Sasu S cd30cc7a3a some updates to tests 2018-07-20 10:47:12 +03:00
Mäkinen Sasu S f56e3aa45a randomly filtering out filters for extent codes less than 10 2018-07-19 15:56:31 +03:00
Mäkinen Sasu S 467238e87a Course grade distribution graph 2018-07-19 15:36:59 +03:00
Sasu MäkinenandGitHub 08156b5b90 Merge pull request #259 from UniversityOfHelsinkiCS/trunk
extentcode graduated filters
2018-07-19 11:31:05 +03:00
Mäkinen Sasu S 76b7e97dd3 little tweaks 2018-07-19 11:23:12 +03:00
Mäkinen Sasu S 3b0a6d2235 you can now filter based on if student has a graduated extentcode, need to update to check only for selected studyright 2018-07-18 17:03:21 +03:00
Sasu MäkinenandGitHub 069ebc3ce9 Merge pull request #257 from UniversityOfHelsinkiCS/trunk
degree and program mapping
2018-07-18 13:24:49 +03:00
Mäkinen Sasu S 8157010959 loader for getting studyrights 2018-07-18 13:07:59 +03:00
Mäkinen Sasu S 6e66336841 eslint 2018-07-18 11:57:58 +03:00
Mäkinen Sasu S 1ff98c5efd program, degree associations and some minor tweaks as extra 2018-07-18 11:57:09 +03:00
Sasu MäkinenandGitHub ebffacafdc Merge pull request #256 from UniversityOfHelsinkiCS/trunk
grade distribution in population statistics
2018-07-18 10:29:45 +03:00
Mäkinen Sasu S 5d791dc7f1 grade distribution is now from backend instead of hacking it in frontend. Fixed all the problems 2018-07-18 10:06:00 +03:00
Mäkinen Sasu S 17172faa40 clog removed 2018-07-17 16:33:48 +03:00
Mäkinen Sasu S dc43625df5 bugfix 2018-07-17 16:33:24 +03:00
Mäkinen Sasu S 93edc01e8f eslint fix 2018-07-17 16:10:01 +03:00
Mäkinen Sasu S 2443b611f2 #229 2018-07-17 16:06:07 +03:00
Mäkinen Sasu S b8dcf7f1c4 fixed course participation filters name being in english 2018-07-17 13:49:29 +03:00
Mäkinen Sasu S a0c03be52d added grade distribution to population statistics 2018-07-17 13:33:51 +03:00
Sasu MäkinenandGitHub 748c9d30f8 Merge pull request #254 from UniversityOfHelsinkiCS/trunk
Deleting preset filters
2018-07-16 15:50:56 +03:00
Mäkinen Sasu S 52424a4069 added tests for filters in population statistics 2018-07-16 15:36:54 +03:00
Mäkinen Sasu S 15d6938883 bugfix 2 2018-07-16 13:59:14 +03:00
Mäkinen Sasu S e7ff8952ad bug fix 2018-07-16 13:32:31 +03:00
Mäkinen Sasu S a2e0c00087 remove clog 2018-07-16 12:34:23 +03:00
Mäkinen Sasu S 77e853c1a1 Preset filters can now be deleted from database 2018-07-16 12:33:45 +03:00
Sasu MäkinenandGitHub 0c039d877a Merge pull request #251 from UniversityOfHelsinkiCS/trunk
Preset filter course filters are now re-calculated on load and some p…
2018-07-13 14:16:05 +03:00
Mäkinen Sasu S df68e87d29 Preset filter course filters are now re-calculated on load and some preset filter redesign 2018-07-13 14:02:37 +03:00
Sasu MäkinenandGitHub 8a7aaca325 Merge pull request #249 from UniversityOfHelsinkiCS/trunk
Update students on demand
2018-07-12 15:41:25 +03:00
Mäkinen Sasu S 259fa10e9c removed clog 2018-07-12 15:34:07 +03:00
Mäkinen Sasu S cb437c7559 Update population or student on demand 2018-07-12 15:33:26 +03:00
Mäkinen Sasu S 82d92aa0b5 testing image now updated everytime pushing to trunk and superbara tests open oodikone.cs.helsinki.fi/testing/ instead of localhost 2018-07-11 15:02:16 +03:00
Sasu MäkinenandGitHub aed9029c7c Merge pull request #248 from UniversityOfHelsinkiCS/trunk
Testing environment at oodikone.cs.helsinki.fi/testing/
2018-07-11 14:10:15 +03:00
Mäkinen Sasu S 724a749e2c removed clog 2018-07-11 14:01:40 +03:00
Mäkinen Sasu S 092bc11265 tester user authentication 2018-07-11 14:01:03 +03:00
Mäkinen Sasu S 371f8ba5ab added another needle in the haystack again 2018-07-10 14:58:49 +03:00
Sasu MäkinenandGitHub a916711106 Merge pull request #247 from UniversityOfHelsinkiCS/trunk
trying to fix my bad decisions
2018-07-10 12:07:13 +03:00
Mäkinen Sasu S dc1502de8d trying to fix my bad decisions 2018-07-10 12:00:39 +03:00
Sasu MäkinenandGitHub c570986161 Merge pull request #246 from UniversityOfHelsinkiCS/trunk
reverting login options
2018-07-10 11:41:36 +03:00
Mäkinen Sasu S 8ff11df102 reverting login options 2018-07-10 11:35:07 +03:00
Sasu MäkinenandGitHub 5cd20a8b63 Merge pull request #245 from UniversityOfHelsinkiCS/trunk
Im not sure what happened here
2018-07-10 10:38:29 +03:00
Sasu MäkinenandGitHub 29c39c68ea Merge branch 'master' into trunk 2018-07-10 10:30:06 +03:00
Mäkinen Sasu S 1ff6b7b31f reset 2018-07-10 10:10:59 +03:00
Sasu MäkinenandGitHub 20ab0bbfc9 Merge pull request #244 from UniversityOfHelsinkiCS/travis_master
testing devops mayhem
2018-07-09 14:29:05 +03:00
Sasu MäkinenandGitHub fc096f5cb9 Merge branch 'master' into trunk 2018-07-09 14:24:23 +03:00
Sasu MäkinenandGitHub 04ff187b5b Merge branch 'master' into travis_master 2018-07-09 14:22:08 +03:00
Mäkinen Sasu S b4c3e45323 testing updating production for routing 2018-07-09 14:19:59 +03:00
Mäkinen Sasu S f1c8c339b4 same stuff as always 2018-07-09 12:07:30 +03:00
Mäkinen Sasu S 4f5f98329b trying out new strat 2018-07-09 11:37:23 +03:00
Mäkinen Sasu S 4df1853525 testing stuff 2018-07-06 15:49:17 +03:00
Mäkinen Sasu S 678a4320de filters visible to all 2018-07-05 10:16:23 +03:00
Mäkinen Sasu S aa493e9f31 no detach on docker compose up 2018-07-05 10:13:29 +03:00
Mäkinen Sasu S c19f99953b räääh tsup 2018-07-04 15:36:04 +03:00
Mäkinen Sasu S 510fd8e5a2 lets seeif this wokrs ==) 2018-07-04 14:53:37 +03:00
Mäkinen Sasu S c3eeea14d9 create database in travis 2018-07-04 14:40:44 +03:00
Mäkinen Sasu S e0521e89d2 psql install in travis 2018-07-04 14:32:01 +03:00
Mäkinen Sasu S 1c6268307e travis mayhem 2018-07-04 14:22:46 +03:00
Mäkinen Sasu S e734dbf6e2 docker versions... 2018-07-04 14:06:04 +03:00
Mäkinen Sasu S 32dd714b98 changed version.... 2018-07-04 14:00:41 +03:00
Mäkinen Sasu S 97555865d9 fix yaml 2018-07-04 13:56:13 +03:00
Mäkinen Sasu S 23aabaf959 update docker and docker-compose in travis 2018-07-04 13:50:12 +03:00
Mäkinen Sasu S c3ae5bf15a trying again with docker-compose file 2018-07-04 13:34:30 +03:00
Mäkinen Sasu S 6d79dddd4d travis test 2018-07-04 13:19:10 +03:00
Mäkinen Sasu S 2a00ce59ad testing travis stuff 2018-07-04 13:06:12 +03:00
Sasu MäkinenandGitHub 1a3b924ac9 Merge pull request #237 from UniversityOfHelsinkiCS/revert-236-presetfilter
Revert "Presetfilter"
2018-07-04 11:42:30 +03:00
Sasu MäkinenandGitHub 9ed155b42b Merge pull request #238 from UniversityOfHelsinkiCS/presetfilter
Presetfilter
2018-07-04 11:41:34 +03:00
Sasu MäkinenandGitHub 09189e34be Revert "Presetfilter" 2018-07-04 11:40:11 +03:00
Sasu MäkinenandGitHub 2a8103443d Merge pull request #236 from UniversityOfHelsinkiCS/presetfilter
Preset filter
2018-07-04 11:39:16 +03:00
Mäkinen Sasu S 3f3f295376 tests removed from travis aswell for merge to trunk 2018-07-04 11:36:34 +03:00
Mäkinen Sasu S d38f3774ca removed tests for trunk merge 2018-07-04 10:57:50 +03:00
Mäkinen Sasu S 5abcac8dc2 cleaned up a little 2018-07-04 10:43:42 +03:00
Mäkinen Sasu S cbeca7fbf5 preset filters can now be nested 2018-07-04 10:40:16 +03:00
Mäkinen Sasu S c0fae224f0 trying if xvfb fixes travis superbara 2018-07-03 16:53:37 +03:00
Mäkinen Sasu S 04f46c1828 Saving preset filters now visible only to 'tktl' and 'sasumaki' 2018-07-03 16:47:54 +03:00
Mäkinen Sasu S a021a92ecc preset button basic functionality now working 2018-07-03 16:25:27 +03:00
ikuisma 20d7fc2607 Removed npm test from prepush due to superbara issues. 2018-07-03 15:13:44 +03:00
ikuisma e5b3f00cf5 Added course codes to degree and studyprogram dropdown lists. 2018-07-03 14:30:02 +03:00
Mäkinen Sasu S 1faf4c8c83 Merge branch 'trunk' into presetfilter 2018-07-03 11:38:30 +03:00
Mäkinen Sasu S 2ea11e4724 added google chrome in travis to maybe get superbara working 2018-07-02 16:22:03 +03:00
Mäkinen Sasu S c72cfc0699 trying superbara in travis again 2018-07-02 16:16:36 +03:00
Mäkinen Sasu S 8b436c7442 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-07-02 16:07:16 +03:00
Mäkinen Sasu S 563d3c9459 travis superbara maybe 2018-07-02 16:04:58 +03:00
Sasu MäkinenandGitHub 85fbb02338 Merge branch 'master' into trunk 2018-07-02 15:54:12 +03:00
Mäkinen Sasu S ff6252b889 fixed credit graph tooltip showing course title as an object 2018-07-02 15:47:17 +03:00
Mäkinen Sasu S d741b4741e wait in test 2018-07-02 15:18:22 +03:00
Mäkinen Sasu S 60d1bcbf8d added tests as compulsory pre-push hook. From now on skipping tests will be settled in the court of Toska 2018-07-02 15:14:50 +03:00
Mäkinen Sasu S c0f4abc5f5 progression in filters 2018-06-29 15:31:26 +03:00
ikuismaandGitHub 29a0c40e01 Merge pull request #215 from UniversityOfHelsinkiCS/trunk
Update frontend to work with data returned by the latest version of the backend
2018-06-28 15:04:59 +03:00
ikuisma c7c0241a78 Fixed enable users page to work with new elementdetails array. 2018-06-28 13:06:27 +03:00
Mäkinen Sasu S b059a2f276 Merge branch 'trunk' into presetfilter 2018-06-28 10:43:58 +03:00
Mäkinen Sasu S 52b819424b course names now visible in student page (again) 2018-06-28 10:36:12 +03:00
Mäkinen Sasu S b74cbd8789 updated tests to correspond to new population query 2018-06-28 10:22:06 +03:00
ikuisma 5702f0e49c Removed unused import 2018-06-28 08:58:29 +03:00
ikuisma cf88efde90 Added all elementdetail names be visible in the query search card. 2018-06-28 08:57:58 +03:00
ikuisma b63b845b02 Updated frontend to handle new language objects that were previously strings – should probably be handled by a selector later on. 2018-06-28 07:53:24 +03:00
Mäkinen Sasu S 4858084f24 reworking population query 2018-06-27 20:08:49 +03:00
Mäkinen Sasu S e21b956483 started preset filters 2018-06-27 11:53:43 +03:00
Mäkinen Sasu S 74c0b2486d email now shows in student page 2018-06-27 10:21:23 +03:00
Mäkinen Sasu S cbd84c5ffd fixed an issue with copying email list in population statistics 2018-06-26 15:05:59 +03:00
Mäkinen Sasu S ce8e7def89 population page now shows emails of students. Emails can also be copied to clipboard by clicking copy icon 2018-06-26 14:46:30 +03:00
Mäkinen Sasu S 1c2f36b158 done population statistics: sort population names alphabetically in dropdown #199 2018-06-26 13:14:29 +03:00
Mäkinen Sasu S e1d54e4c63 made graph calculations a bit more robust. Still not correct because of no way to perfectly differentiate study modules from real courses. Also modules are now visually shown on graph tooltips. 2018-06-26 12:55:33 +03:00
Mäkinen Sasu S a95f96b96d rm clog 2018-06-26 12:12:25 +03:00
Mäkinen Sasu S 1c6d45bfdc centrified all credit calculations in common functions and added study module credits are now filtered based on backend information instead of calculated in frontend. Also added a written marker '[Study Module]' on courses that are not calculated in the full credit amount 2018-06-26 12:11:50 +03:00
Sasu MäkinenandGitHub 7cbff2c1b6 Merge pull request #209 from UniversityOfHelsinkiCS/trunk
Million new superbara tests, some population stats updates and student search bugfixes.
2018-06-26 09:27:08 +03:00
Mäkinen Sasu S 88bc3667fc pushing without tests before I get my database :( 2018-06-26 09:24:47 +03:00
Mäkinen Sasu S 0682c89aac prehooks 2018-06-26 09:23:43 +03:00
Mäkinen Sasu S 96ff6c190a prehook push test 2018-06-26 09:23:05 +03:00
Mäkinen Sasu S 9f82cda4bf superbara tests can be now run by writing 'npm test' 2018-06-26 09:14:46 +03:00
Mäkinen Sasu S 24d9ea947e student search: the old search resuts are not zeroed if user moves to different tab #200 2018-06-25 17:35:39 +03:00
Mäkinen Sasu S d4e020669b fixed student statistics superbara test 2018-06-25 16:43:46 +03:00
Mäkinen Sasu S 29e6264660 course statistics end-to-end superbara test done 2018-06-25 16:31:46 +03:00
Mäkinen Sasu S 30cc569236 made population statistics superbara test bigger and better 2018-06-25 14:40:51 +03:00
Mäkinen Sasu S b17688a06e rewritten superbara (yet again) in a PCOM way 2018-06-25 13:58:45 +03:00
Mäkinen Sasu S c5dff7ac63 added new population statistics test 2018-06-21 15:57:55 +03:00
totutotu af6771cbd6 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-21 15:32:35 +03:00
totutotu 78205fd1b2 Credit gain for population stats updated 2018-06-21 15:32:31 +03:00
Mäkinen Sasu S 5094fb3bee navbar_test rewritten 2018-06-21 13:41:30 +03:00
Tuomo TorppaandGitHub 03d28b0e6f Merge pull request #197 from UniversityOfHelsinkiCS/trunk
Credit gain year fix
2018-06-21 11:27:40 +03:00
totutotu 48f8b38ccf Credit gain year fix 2018-06-21 11:24:52 +03:00
Tuomo TorppaandGitHub 0031c5f7f5 First year credit gain table, small bug fixes
First year credit gain table, small bug fixes
2018-06-21 11:07:25 +03:00
totutotu e150bede1c Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-21 10:41:34 +03:00
totutotu e48a9f2ff0 First year credit gain to population statistics 2018-06-21 10:41:29 +03:00
Mäkinen Sasu S 95e98746e4 removed clog 2018-06-21 10:41:09 +03:00
Mäkinen Sasu S 8e6b4b1d11 heisenbug of creditlines not always rendering (maybe) fixed 2018-06-21 10:33:57 +03:00
Mäkinen Sasu S ee58112c97 removed clog 2018-06-20 17:31:03 +03:00
Mäkinen Sasu S 75e7b65a93 fixed study rights not showing because of finnish language 2018-06-20 17:29:18 +03:00
ikuismaandGitHub fe261c288c Merge pull request #191 from UniversityOfHelsinkiCS/trunk
Frontend fixes to work with latest backend
2018-06-20 17:28:02 +03:00
ikuisma 02a3a8531d Removed unused import. 2018-06-20 17:16:33 +03:00
ikuisma 62d0a10a03 Fixed credit accumulation graph date format handling. 2018-06-20 17:15:15 +03:00
totutotu 4213ada22d Remove console.log 2018-06-20 14:51:15 +03:00
totutotu 722bcfb6ee Filter code mappings based on code or name 2018-06-20 14:50:37 +03:00
totutotu 22af3a19eb Started date to students 2018-06-20 14:23:29 +03:00
totutotu 9ea3ebd9e4 removed unnecessary proptype 2018-06-20 11:46:20 +03:00
totutotu 57a0794651 Remove univenrolmentdate which does not exist in new oodi api 2018-06-20 10:29:37 +03:00
ikuisma 436c2cc0d7 Removed unused import. 2018-06-20 09:27:16 +03:00
ikuisma d7dc2d8ab6 Fixed broken student page header for new-style highlevelnames. 2018-06-20 09:26:00 +03:00
Tuomo TorppaandGitHub d8e3215cc8 Code mapping to redis, course stats table updated
Trunk
2018-06-19 15:44:08 +03:00
Tuomo TorppaandGitHub 1b038abbf5 Merge branch 'master' into trunk 2018-06-19 15:40:45 +03:00
Tuomo TorppaandGitHub 0fcfb727c0 Course code mapping to redis, new data format
Coursecode map update
2018-06-19 15:40:23 +03:00
totutotu 51e413a134 Loader to population stats course segment 2018-06-19 15:22:14 +03:00
totutotu b20e8a6e10 Settings page to new redis data format 2018-06-18 15:54:31 +03:00
Mäkinen Sasu S 08f42b50e2 course statistics statistics table now shows percentages in a better format 2018-06-18 13:38:50 +03:00
Sasu MäkinenandGitHub 0434243491 Merge pull request #176 from UniversityOfHelsinkiCS/trunk
course statistics statistics table fix'd
2018-06-18 13:25:01 +03:00
Mäkinen Sasu S ece9fdaff1 removed console.log and fixed eslint 2018-06-18 13:21:44 +03:00
Mäkinen Sasu S 6cdab87f0f course statistics table now dynamically changes with course/student level view 2018-06-18 13:15:30 +03:00
Sasu MäkinenandGitHub e8bd4b9d2c Merge pull request #175 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-18 12:47:49 +03:00
Mäkinen Sasu S 2ec18fa14c Course statistics: Switch for showing course level statistics and student level course statistics 2018-06-18 12:44:38 +03:00
Mäkinen Sasu S 2f39975efd rewritten bar chart to match new backend 2018-06-15 13:12:22 +03:00
Tuomo TorppaandGitHub f4bcfa0539 Merge pull request #174 from UniversityOfHelsinkiCS/trunk
Simple table for course statistics
2018-06-15 10:36:08 +03:00
totutotu e61abe4525 Simple table for course statistics 2018-06-15 10:29:47 +03:00
Tuomo TorppaandGitHub 1cd7bb149f Merge pull request #166 from UniversityOfHelsinkiCS/trunk
Course stats new querys to top, UI improvements
2018-06-14 11:59:56 +03:00
totutotu 873e9609dd Loading spinner moved from on top of the search box 2018-06-14 11:50:58 +03:00
totutotu eec2000eaa Added newest query course on top and transition animation 2018-06-14 10:24:51 +03:00
totutotu 87607165ce student search when 6 numbers or 4 letters 2018-06-14 09:37:29 +03:00
Sasu MäkinenandGitHub 22ec9e4c79 Merge pull request #159 from UniversityOfHelsinkiCS/trunk
Modified navbar, student statistics xAxis and did some filter refactoring.
2018-06-13 16:16:44 +03:00
Mäkinen Sasu S deeec81efd refactored filters to use params as object instead of array 2018-06-13 16:11:20 +03:00
Mäkinen Sasu S 22129188a9 Student statistics graph xAxis is now formatted as real dates instead of months from 0 to X 2018-06-13 15:53:47 +03:00
Mäkinen Sasu S c261b148c5 hid information about settings since the feature is not in production 2018-06-13 14:49:41 +03:00
Mäkinen Sasu S b9690808d8 eslint =) 2018-06-13 14:39:37 +03:00
Mäkinen Sasu S f982fcfbf9 Removed starting page route and improved starting page 2018-06-13 14:38:52 +03:00
ikuismaandGitHub 8adf0109f1 Merge pull request #156 from UniversityOfHelsinkiCS/trunk
New starting page and tabindexes
2018-06-13 13:09:47 +03:00
Mäkinen Sasu S 64e7414c10 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-13 13:04:21 +03:00
Mäkinen Sasu S cd8cd4e7f0 eslint fix 2018-06-13 13:04:09 +03:00
Mäkinen Sasu S 187af16f85 new starting page 2018-06-13 13:02:47 +03:00
ikuisma a4d57e51ea Added -1 tabindex to plus-minus calendar buttons in search filter form. 2018-06-13 12:49:48 +03:00
ikuisma 1531d77d5c Added -1 tabindex to navbar menu items so that the tab focus jumps straight to page content instead of navbar. 2018-06-13 12:49:07 +03:00
Sasu MäkinenandGitHub ae40e59fa9 Merge pull request #155 from UniversityOfHelsinkiCS/trunk
Two new course filters
2018-06-13 11:07:18 +03:00
Mäkinen Sasu S d532788e6a refactored code of courseparticipation filters to be a little bit cleaner and easier to implement new ones by removing copy-paste code 2018-06-13 11:02:50 +03:00
Mäkinen Sasu S c4ac1a04c3 Added two new filters: not participated and not participated or failed 2018-06-13 10:31:09 +03:00
Sasu MäkinenandGitHub 34e76883de Merge pull request #148 from UniversityOfHelsinkiCS/trunk
Refactored student statistics to be able to use browser history.
2018-06-12 16:10:44 +03:00
Mäkinen Sasu S 427f729675 Refactored student statistics to be able to use browser history. 2018-06-12 16:05:48 +03:00
Sasu MäkinenandGitHub 0efdc9dd42 Merge pull request #143 from UniversityOfHelsinkiCS/trunk
Navigation panel for population statistics, popup names for course filters, and formatted yAxis in student graph.
2018-06-11 15:08:05 +03:00
Mäkinen Sasu S d086d8fb09 Placed navigation panel to right bottom corner 2018-06-11 14:50:20 +03:00
Mäkinen Sasu S 7d03b58c3f made population statistics page navigation panel collapsable 2018-06-11 14:42:07 +03:00
Mäkinen Sasu S 5629f3af38 Added navigation buttons to population statistics page 2018-06-11 14:03:04 +03:00
Mäkinen Sasu S fc1b9f110e added a popup on course filters to show its full name in case its truncated for being too long. 2018-06-11 11:39:19 +03:00
Mäkinen Sasu S d1c117da2b Added tickformatter to student statistics graphs Y axis to fix bug of too many decimal numbers 2018-06-11 10:45:29 +03:00
Matti LuukkainenandGitHub 780d9f7e69 Merge pull request #140 from UniversityOfHelsinkiCS/trunk
wording
2018-06-10 00:20:21 +03:00
Matti Luukkainen 34c00de187 wording 2018-06-10 00:17:39 +03:00
Sasu MäkinenandGitHub 4f8b700a12 Merge pull request #137 from UniversityOfHelsinkiCS/trunk
Population Statistics page: structure redone, complementation filter done and some improvementes and refactoring here and there.
2018-06-08 16:06:37 +03:00
Mäkinen Sasu S 7ed20990b9 course filters CSS 2018-06-08 16:04:05 +03:00
Mäkinen Sasu S 8ca3cb6ca0 Fixed removing course filter by clicking course name crashing the app 2018-06-08 15:01:52 +03:00
Mäkinen Sasu S 514ac093be Added filter complementation and refactoring populationstatistics. Updated superbara tests accordingly 2018-06-08 14:51:46 +03:00
Sasu MäkinenandGitHub eae5efdb62 Merge pull request #130 from UniversityOfHelsinkiCS/trunk
Improvements to population graph, course-code mapper and superbara tests.
2018-06-07 15:50:42 +03:00
Mäkinen Sasu S 4716fc0dd6 removed console logging 2018-06-07 15:46:10 +03:00
Mäkinen Sasu S 28c0519ca3 fixed default course limit filter 2018-06-07 15:45:35 +03:00
Mäkinen Sasu S 283303386a Fixed population graph xAxis to react properly when filtering students 2018-06-07 15:16:12 +03:00
totutotu eb6c9d087b Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-07 13:33:10 +03:00
totutotu 27fdcadd3f loading spinner for course code mapper 2018-06-07 13:33:03 +03:00
Mäkinen Sasu S 64f1398f1a fixed superbara tests to work stupidly until push history is done properly 2018-06-07 10:28:16 +03:00
Mäkinen Sasu S 9968ef1685 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-07 10:05:40 +03:00
Mäkinen Sasu S 5577f3ad0c Populationgraph is no longer ordinal so it actually makes sense 2018-06-07 10:05:14 +03:00
Matti LuukkainenandGitHub 7b377d90d8 Merge pull request #121 from UniversityOfHelsinkiCS/trunk
performance improvements on filtering
2018-06-06 22:02:50 +03:00
Matti Luukkainen 3e8046efaf Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-06 21:52:54 +03:00
Matti Luukkainen bdda5fb048 performance improvements on filtering 2018-06-06 21:48:22 +03:00
Mäkinen Sasu S fab8a653dc minor changes to filepath configuration in superbara tests 2018-06-06 16:18:50 +03:00
Matti LuukkainenandGitHub 1bb1824331 Merge pull request #117 from UniversityOfHelsinkiCS/trunk
refacoring to use filter architecture in course based filters
2018-06-06 15:04:42 +03:00
Matti Luukkainen a91ca60fad fixes 2018-06-06 14:57:53 +03:00
Matti LuukkainenandGitHub 4e625c6ba8 Merge pull request #116 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-06 14:28:57 +03:00
Matti Luukkainen d440ef18ff Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-06 14:27:56 +03:00
Matti Luukkainen 80275f793a refacoring to use filter architecture in course based filters 2018-06-06 14:27:50 +03:00
Mäkinen Sasu S ae1846e65b Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-06 14:05:35 +03:00
Mäkinen Sasu S dcba353b2d new superbara tests 2018-06-06 14:04:55 +03:00
totutotu 0e5e470bcb CourseSearch nad yearlystats small UI improvements 2018-06-06 11:35:34 +03:00
totutotu 52ea016995 lint newline 2018-06-06 10:24:33 +03:00
totutotu 3329225b50 Course code mapping filter, add and delete added 2018-06-06 10:20:22 +03:00
ikuisma 204657bd7c Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-05 17:51:52 +03:00
ikuisma 82d651d8b2 Made main.rb work with latest version of superbara, added script for running all tests. 2018-06-05 17:51:48 +03:00
totutotu f05d46cbbc Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-05 17:26:52 +03:00
totutotu 6a478b1d8c course code map course names added 2018-06-05 17:26:46 +03:00
ikuisma 10f068666c Implemented new test for viewing student credit accumulation graph. 2018-06-05 17:18:52 +03:00
ikuisma 49cfd61c16 Refactored populations filter actions to separate helper file. 2018-06-05 16:48:16 +03:00
ikuisma e9f47563b0 Added population statistics test back. 2018-06-05 15:45:31 +03:00
ikuisma 7b38aad1d8 Removed redundant _test from the end of test files. 2018-06-05 15:45:00 +03:00
ikuisma 2a4f5fad27 Refactored tests to use globvar basepath 2018-06-05 15:40:50 +03:00
ikuisma 03e97ceb9a Two first tests made with superbara. 2018-06-05 15:29:18 +03:00
sasumakiandGitHub 054fad76d7 Merge pull request #113 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-04 17:20:45 +03:00
sasumakiandGitHub acdcf8ce1a Merge branch 'master' into trunk 2018-06-04 17:16:45 +03:00
Mäkinen Sasu S 71593d26af StudyRight highlevelname filtering now matches for a more sophisticated regular expression 2018-06-04 17:15:52 +03:00
Mäkinen Sasu S 72aa373bb6 disabled matr filter properly 2018-06-04 16:25:54 +03:00
Mäkinen Sasu S f5b525a382 disabled matriculation examination filter until further notice 2018-06-04 16:11:48 +03:00
Mäkinen Sasu S 5ea94908ca getStudentTotalCredits in common no longer counts courses that the student have not passed 2018-06-04 16:05:24 +03:00
Mäkinen Sasu S 508c365d42 fixed bug when removing matriculation and sex filter removing every other filter as well 2018-06-04 15:55:41 +03:00
ikuisma d41f97f43a Removed console.log 2018-06-04 15:54:04 +03:00
ikuisma 765de6f9d8 Refactored EnableUsers component to match the style of the other pages. 2018-06-04 15:53:14 +03:00
totutotu 8b494b79a9 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-04 15:45:34 +03:00
totutotu 1b8bfcaac9 alternative codes header changed, created unique keys for passaratechart 2018-06-04 15:45:29 +03:00
Mäkinen Sasu S 86f4c46ace student search now doesn't search with less than 3 letters in query 2018-06-04 15:40:35 +03:00
Mäkinen Sasu S faa528d01f eslint 2018-06-04 15:23:38 +03:00
Mäkinen Sasu S e8ec56db00 fixing to expect matriculation examination data to be in true/false values instead of 0/1 2018-06-04 15:22:57 +03:00
totutotu 8cc4802ca1 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-04 14:51:42 +03:00
totutotu 2dc47d3f37 Show alternative codes on yearlystats 2018-06-04 14:51:38 +03:00
sasumakiandGitHub e08776fc07 Merge pull request #112 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-04 13:45:07 +03:00
sasumakiandGitHub 67d7438e09 Merge branch 'master' into trunk 2018-06-04 13:42:20 +03:00
Mäkinen Sasu S f2a7846c26 changed date format in Population search form 2018-06-04 13:06:05 +03:00
Mäkinen Sasu S 05ddbde60b Added sex and matriculation filters 2018-06-04 12:58:07 +03:00
ikuisma 2df28be735 Added translation string for empty query result in PopulationStatistics page. 2018-06-04 10:28:13 +03:00
ikuisma ec97274ad1 Removed trailing whitespace to fix lint error. 2018-06-04 10:19:41 +03:00
ikuisma 0dffe56060 Implemented error message for empty query result in PopulationDetails component. 2018-06-04 10:19:04 +03:00
ikuisma bd2128626a Made conditional rendering of population search form more readable. 2018-06-04 09:35:29 +03:00
Matti LuukkainenandGitHub 59b46cbcc3 Merge pull request #109 from UniversityOfHelsinkiCS/mluukkai-patch-1
Update .travis.yml
2018-06-02 23:47:34 +03:00
Matti LuukkainenandGitHub 3bece7392c Update .travis.yml 2018-06-02 23:44:09 +03:00
Matti LuukkainenandGitHub 109fbb0b40 Merge pull request #107 from UniversityOfHelsinkiCS/trunk
filter by start time
2018-06-02 13:58:13 +03:00
Matti Luukkainen 4d986484a0 filter by start time 2018-06-02 13:55:28 +03:00
Matti LuukkainenandGitHub d19cfc1378 Merge pull request #105 from UniversityOfHelsinkiCS/trunk
support for setting different filters, disabling broken student detail page
2018-06-02 12:14:45 +03:00
Matti Luukkainen 752f589b04 disable broken student search 2018-06-02 12:12:22 +03:00
Matti Luukkainen f74a9900f4 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-02 11:25:54 +03:00
Matti Luukkainen a5047170b6 support for setting different filters 2018-06-02 11:25:49 +03:00
ikuisma f3d97ab3c4 Fixed broken student tables after populations refactor. 2018-06-01 16:26:52 +03:00
ikuisma 281658fc1b Merge 2018-06-01 15:58:09 +03:00
ikuisma 6a7f279844 Fixed filters in populations page after refactor. 2018-06-01 15:55:25 +03:00
ikuisma e1b5c50367 Updated PopulationSearchForm to use queries object instead of array of queries objects. 2018-06-01 15:50:45 +03:00
ikuisma ebc450a97b Refactored PopulationSearchHistory to use the stored populations object instead of an array of the single populations object. 2018-06-01 15:31:20 +03:00
ikuisma 657438d695 Changed population reducer to save a single population object instead of an array, refactored components to work with single object instead of array. 2018-06-01 15:20:18 +03:00
Mäkinen Sasu S bb567047d1 Studyrights filtering no longer cares about prioritycodes 2018-06-01 15:18:43 +03:00
sasumakiandGitHub 71adbcf36a Merge pull request #101 from UniversityOfHelsinkiCS/trunk
added degree filter to details aswell
2018-06-01 14:47:11 +03:00
Mäkinen Sasu S ab363a59e8 added degree filter to details aswell 2018-06-01 14:45:08 +03:00
sasumakiandGitHub 3985d906db Merge pull request #100 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-01 14:16:02 +03:00
Mäkinen Sasu S e609d04e14 Student statistics now shows detailed view of study rights 2018-06-01 14:13:48 +03:00
Mäkinen Sasu S 50b2270f26 Student infocard now shows latest proper degree studyright 2018-06-01 13:37:23 +03:00
sasumakiandGitHub ec9a001a3b Merge pull request #99 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-01 12:45:11 +03:00
Mäkinen Sasu S 85b18406c2 Student page now shows high level name of study rights 2018-06-01 12:33:08 +03:00
Mäkinen Sasu S cc4bb03560 Population query card now shows how many months is in the query. 2018-05-31 16:11:11 +03:00
Mäkinen Sasu S 8415aa6904 population graph now calculates credits correctly and population search form time format changed from YYYY-MM -> YYYY-MMM 2018-05-31 15:46:09 +03:00
sasumakiandGitHub 5e3039fee7 Merge pull request #98 from UniversityOfHelsinkiCS/trunk
Population search month selection upgraded
2018-05-31 13:43:56 +03:00
Mäkinen Sasu S df2e038b70 Population search month selection upgraded 2018-05-31 13:33:59 +03:00
Matti LuukkainenandGitHub fc75025ebc Merge pull request #96 from UniversityOfHelsinkiCS/trunk
travis->dockerhub testing...
2018-05-30 20:00:09 +03:00
Matti Luukkainen 4b700a5c32 eslint test 2018-05-30 19:56:16 +03:00
Matti LuukkainenandGitHub e2bdc7695f Merge pull request #95 from UniversityOfHelsinkiCS/trunk
check if travis->docker works
2018-05-30 19:25:16 +03:00
Matti Luukkainen bc8e7de98a check if travis->dockex works 2018-05-30 19:15:26 +03:00
Matti LuukkainenandGitHub c15a003fa8 Merge pull request #94 from UniversityOfHelsinkiCS/trunk
build docker image with travis
2018-05-30 18:58:55 +03:00
Matti Luukkainen 505043f0bb better travis 2018-05-30 18:55:22 +03:00
Matti Luukkainen 5bd3a9e6fc build docker image with travis 2018-05-30 18:45:06 +03:00
Matti LuukkainenandGitHub 5a306be4b4 Merge pull request #93 from UniversityOfHelsinkiCS/trunk
fix
2018-05-30 17:50:52 +03:00
Matti Luukkainen 830845395a fix 2018-05-30 17:48:04 +03:00
Matti LuukkainenandGitHub 163b0396b6 Merge pull request #91 from UniversityOfHelsinkiCS/trunk
chart size options
2018-05-30 17:25:02 +03:00
Matti Luukkainen 889e193b01 cludge for initial graph render problem 2018-05-30 17:13:39 +03:00
totutotu dc84cae141 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-30 15:56:52 +03:00
Mäkinen Sasu S 23fe77c27c resizing remembered through redux settings 2018-05-30 15:56:10 +03:00
totutotu a22d04bd25 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-30 15:53:21 +03:00
totutotu 34484d9e3d Course duplicate code managing UI started 2018-05-30 15:53:16 +03:00
Mäkinen Sasu S c5253ef5fd Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-30 14:28:57 +03:00
Mäkinen Sasu S 1fe0c2142a eslint fix 2018-05-30 14:28:34 +03:00
Mäkinen Sasu S 2d8249f6e8 Chart size options added 2018-05-30 14:27:08 +03:00
136 changed files with 9204 additions and 1280 deletions
+2 -2
View File
@@ -10,9 +10,9 @@
"env": {
"development": {
"plugins": [
"react-hot-loader/babel",
"transform-object-rest-spread",
"transform-class-properties"
"transform-class-properties",
"react-hot-loader/babel"
]
},
"test": {
+29 -1
View File
@@ -3,4 +3,32 @@ node_js:
- '8'
cache:
directories:
- node_modules
- node_modules
addons:
chrome: stable
sudo: required
before_install:
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
- gem install superbara
before_script:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
- sleep 3 # give xvfb some time to start
services:
- docker
- redis-server
after_success:
- "[[ $TRAVIS_BRANCH =~ (trunk) ]] && docker build -t toska/oodikone2-frontend:testing ."
- "[[ $TRAVIS_BRANCH =~ (trunk) ]] && docker login -u $DOCKER_USER -p $DOCKER_PASS"
- "[[ $TRAVIS_BRANCH =~ (trunk) ]] && docker push toska/oodikone2-frontend:testing"
- "[[ $TRAVIS_BRANCH =~ (^master) ]] && docker build -t toska/oodikone2-frontend:staging ."
- "[[ $TRAVIS_BRANCH =~ (^master) ]] && docker login -u $DOCKER_USER -p $DOCKER_PASS"
- "[[ $TRAVIS_BRANCH =~ (^master) ]] && docker push toska/oodikone2-frontend:staging"
- "[[ $TRAVIS_TAG =~ ([0-1].+) ]] && docker build -t toska/oodikone2-frontend:latest ."
- "[[ $TRAVIS_TAG =~ ([0-1].+) ]] && docker login -u $DOCKER_USER -p $DOCKER_PASS"
- "[[ $TRAVIS_TAG =~ ([0-1].+) ]] && docker push toska/oodikone2-frontend:latest"
+1 -1
View File
@@ -2,7 +2,7 @@
Master:
[![Build Status](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone2-frontend.svg?branch=master)](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone2-frontend)
Trunk:
Drunk:
[![Build Status](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone2-frontend.svg?branch=trunk)](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone2-frontend)
+86 -62
View File
@@ -2921,12 +2921,48 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true
},
"component-clone": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/component-clone/-/component-clone-0.2.2.tgz",
"integrity": "sha1-x/WXmCKID62M+wliuikYbQYe4E8=",
"requires": {
"component-type": "1.1.0"
}
},
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
"dev": true
},
"component-raf": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/component-raf/-/component-raf-1.2.0.tgz",
"integrity": "sha1-srxy1D8bAU/eeks8RHx2S8c8y6o="
},
"component-tween": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/component-tween/-/component-tween-1.2.0.tgz",
"integrity": "sha1-zDnOXbqwW1KCX0HRlHY4oLAbK4o=",
"requires": {
"component-clone": "0.2.2",
"component-emitter": "1.2.0",
"component-type": "1.1.0",
"ease-component": "1.0.0"
},
"dependencies": {
"component-emitter": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.0.tgz",
"integrity": "sha1-zNETqGOI0GSC0D3j/H35hSa6jv4="
}
}
},
"component-type": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/component-type/-/component-type-1.1.0.tgz",
"integrity": "sha1-lbZmqtU+XI0fK+E1xFtdSZGXwMU="
},
"compressible": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.13.tgz",
@@ -4024,6 +4060,11 @@
"stream-shift": "1.0.0"
}
},
"ease-component": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ease-component/-/ease-component-1.0.0.tgz",
"integrity": "sha1-s3VybbC1sEWVt3RAOW/sfapdd8k="
},
"editions": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz",
@@ -4158,14 +4199,6 @@
"is-arrayish": "0.2.1"
}
},
"error-stack-parser": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-1.3.6.tgz",
"integrity": "sha1-4Oc7k+QXE40c18C3RrGkoUhUwpI=",
"requires": {
"stackframe": "0.3.1"
}
},
"es-abstract": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.10.0.tgz",
@@ -4875,8 +4908,7 @@
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
},
"fastparse": {
"version": "1.1.1",
@@ -11777,11 +11809,6 @@
}
}
},
"react-deep-force-update": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-deep-force-update/-/react-deep-force-update-2.1.1.tgz",
"integrity": "sha1-jqQmPNZFWgULN0RbPwj9g52G6Qk="
},
"react-dev-utils": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-4.2.1.tgz",
@@ -11834,17 +11861,30 @@
}
},
"react-hot-loader": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-3.1.3.tgz",
"integrity": "sha512-d7nZf78irxoGN5PY4zd6CSgZiroOhvIWzRast3qwTn4sSnBwlt08kV8WMQ9mitmxEdlCTwZt+5ClrRSjxWguMQ==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.3.4.tgz",
"integrity": "sha512-LlKjtHq+RhDq9xm6crXojbkzrEvli5F4/RaeJ//XtDWrwwsAHDjEqKfZZiPCxv7gWV2cxE3YE8TXeE9BDzLqOA==",
"requires": {
"fast-levenshtein": "2.0.6",
"global": "4.3.2",
"react-deep-force-update": "2.1.1",
"react-proxy": "3.0.0-alpha.1",
"redbox-react": "1.5.0",
"source-map": "0.6.1"
"hoist-non-react-statics": "2.5.5",
"prop-types": "15.6.1",
"react-lifecycles-compat": "3.0.4",
"shallowequal": "1.1.0"
},
"dependencies": {
"hoist-non-react-statics": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
"integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
}
}
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-localize-redux": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/react-localize-redux/-/react-localize-redux-2.16.0.tgz",
@@ -11859,14 +11899,6 @@
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz",
"integrity": "sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg=="
},
"react-proxy": {
"version": "3.0.0-alpha.1",
"resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-3.0.0-alpha.1.tgz",
"integrity": "sha1-RABCa8+oDKpnJMd1VpUxUgn6Swc=",
"requires": {
"lodash": "4.17.5"
}
},
"react-redux": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.7.tgz",
@@ -11927,6 +11959,14 @@
"warning": "3.0.0"
}
},
"react-scroll-to-component": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-scroll-to-component/-/react-scroll-to-component-1.0.2.tgz",
"integrity": "sha1-8mDck2xipT53J4bXgy/giE4ZU1Q=",
"requires": {
"scroll-to": "0.0.2"
}
},
"react-smooth": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-1.0.0.tgz",
@@ -12116,17 +12156,6 @@
}
}
},
"redbox-react": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/redbox-react/-/redbox-react-1.5.0.tgz",
"integrity": "sha512-mdxArOI3sF8K5Nay5NG+lv/VW516TbXjjd4h1wcV1Iy4IMDQPnCayjoQXBAycAFSME4nyXRUXCjHxsw2rYpVRw==",
"requires": {
"error-stack-parser": "1.3.6",
"object-assign": "4.1.1",
"prop-types": "15.6.1",
"sourcemapped-stacktrace": "1.1.8"
}
},
"redent": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
@@ -12648,6 +12677,15 @@
"integrity": "sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=",
"dev": true
},
"scroll-to": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/scroll-to/-/scroll-to-0.0.2.tgz",
"integrity": "sha1-k205ipEzZgokkhRcLACB38sHKPM=",
"requires": {
"component-raf": "1.2.0",
"component-tween": "1.2.0"
}
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -12814,6 +12852,11 @@
"safe-buffer": "5.1.1"
}
},
"shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -13085,7 +13128,8 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-resolve": {
"version": "0.5.1",
@@ -13123,21 +13167,6 @@
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
"dev": true
},
"sourcemapped-stacktrace": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/sourcemapped-stacktrace/-/sourcemapped-stacktrace-1.1.8.tgz",
"integrity": "sha512-OkVoI7GQOLl/laR1qsSo1c87tS8kF2VXhQq2SrQCDdXufBAcm8FgXogWso96ciMYoDtTw1Dn70CVdwYzoYs6Pg==",
"requires": {
"source-map": "0.5.6"
},
"dependencies": {
"source-map": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
"integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI="
}
}
},
"spawn-command": {
"version": "0.0.2-1",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
@@ -13224,11 +13253,6 @@
"safe-buffer": "5.1.1"
}
},
"stackframe": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-0.3.1.tgz",
"integrity": "sha1-M6qE8Rd6VUjIk1Uzy/6zQgl19aQ="
},
"state-toggle": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.0.tgz",
+3 -2
View File
@@ -16,7 +16,7 @@
"stats": "webpack --json --config webpack.config.production.js > stats.json",
"prepush": "npm run lint",
"pretest": "npm run lint",
"test": "echo 'no tests'"
"test": ""
},
"licence": "",
"main": "src/app.jsx",
@@ -81,11 +81,12 @@
"react-datetime": "^2.14.0",
"react-dom": "^16.4.0",
"react-highcharts": "^16.0.2",
"react-hot-loader": "^3.1.3",
"react-hot-loader": "^4.3.4",
"react-localize-redux": "^2.16.0",
"react-redux": "^5.0.7",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"react-scroll-to-component": "^1.0.2",
"recharts": "^1.0.0-beta.10",
"redux": "^3.7.2",
"redux-logger": "^3.0.6",
+47 -12
View File
@@ -1,20 +1,48 @@
import axios from 'axios'
import { getToken, setToken } from '../common'
import { API_BASE_PATH, TOKEN_NAME } from '../constants'
import { API_BASE_PATH, TOKEN_NAME, BASE_PATH } from '../constants'
const getAxios = () => axios.create({ baseURL: API_BASE_PATH })
const isTestEnv = BASE_PATH === '/testing/'
const isDevEnv = process.env.NODE_ENV === 'development'
const devOptions = {
headers: {
uid: 'tktl',
displayName: 'Development Käyttäjä',
displayName: 'Development Kayttaja',
'shib-session-id': 'mock-session'
}
}
const testOptions = {
headers: {
uid: 'tester',
displayName: 'Testing Käyttäjä',
'shib-session-id': 'mock-session'
}
}
const types = {
attempt: prefix => `${prefix}ATTEMPT`,
failure: prefix => `${prefix}FAILURE`,
success: prefix => `${prefix}SUCCESS`
}
export const actionTypes = prefix => ({
attempt: types.attempt(prefix),
failure: types.failure(prefix),
success: types.success(prefix)
})
export const login = async () => {
const options = isDevEnv ? devOptions : null
let options = null
if (isDevEnv) {
options = devOptions
}
if (isTestEnv) {
options = testOptions
}
const response = await getAxios().post('/login', null, options)
return response.data.token
}
@@ -25,32 +53,39 @@ export const swapDevUser = async (newHeaders) => {
setToken(token)
}
const callApi = async (url, method = 'get', data) => {
const options = isDevEnv ? devOptions : { headers: {} }
export const callApi = async (url, method = 'get', data, params) => {
let options = { headers: {} }
if (isDevEnv) {
options = devOptions
}
if (isTestEnv) {
options = testOptions
}
const token = await getToken()
options.headers['x-access-token'] = token
switch (method) {
case 'get':
return getAxios().get(url, options)
return getAxios().get(url, { ...options, params })
case 'post':
return getAxios().post(url, data, options)
case 'put':
return getAxios().put(url, data, options)
case 'delete':
return getAxios().delete(url, options)
return getAxios().delete(url, { headers: options.headers, data })
default:
return Promise.reject(new Error('Invalid http method'))
}
}
export const callController = (route, prefix, data, method = 'get', query) => {
export const callController = (route, prefix, data, method = 'get', query, params) => {
const requestSettings = {
route,
method,
data,
prefix,
query
query,
params
}
return { type: `${prefix}ATTEMPT`, requestSettings }
}
@@ -60,16 +95,16 @@ export const handleRequest = store => next => async (action) => {
const { requestSettings } = action
if (requestSettings) {
const {
route, method, data, prefix, query
route, method, data, prefix, query, params
} = requestSettings
try {
const res = await callApi(route, method, data)
const res = await callApi(route, method, data, params)
store.dispatch({ type: `${prefix}SUCCESS`, response: res.data, query })
} catch (e) {
// Something failed. Assume it's the token and try again.
try {
await getToken(true)
const res = await callApi(route, method, data)
const res = await callApi(route, method, data, params)
store.dispatch({ type: `${prefix}SUCCESS`, response: res.data, query })
} catch (err) {
store.dispatch({ type: `${prefix}FAILURE`, response: err, query })
+1 -1
View File
@@ -16,7 +16,7 @@ import { handleRequest } from './apiConnection'
import Main from './components/Main'
try {
Raven.config('http://02d07bd40f404cc0965f38f06183d9fb@toska.cs.helsinki.fi:8500/3').install() // eslint-disable-line
Raven.config('https://02d07bd40f404cc0965f38f06183d9fb@toska.cs.helsinki.fi/3').install() // eslint-disable-line
} catch (e) { } // eslint-disable-line
const translations = require('./i18n/translations.json')
Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+1
View File
@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.89 425.28"><defs><style>.cls-1{fill:#aa4137;}.cls-2{fill:#ea9939;}</style></defs><title>toscalogo_color</title><path class="cls-1" d="M627.67,180.6a120.22,120.22,0,0,1,43.17-25.13c16.28-5.49,31.91-7.19,46.87-7.19q41.88,0,66.82,19.7A57.53,57.53,0,0,1,796,180.23V48H542.15V259Z"/><polygon class="cls-1" points="95.75 48.22 95.75 47.96 45.89 47.96 45.89 148.57 95.75 148.57 95.75 48.22"/><path class="cls-1" d="M125.67,148.57H281.91q26.41,0,48.61,9a114.28,114.28,0,0,1,38.4,25.18,111.19,111.19,0,0,1,9.82,11.44,50.92,50.92,0,0,1,3.82-9,64.26,64.26,0,0,1,18.2-20.7,78.87,78.87,0,0,1,25.18-12.21,102.94,102.94,0,0,1,28.42-4q28.42,0,49.13,10.47a48.64,48.64,0,0,1,8.75,5.76V48H125.67V148.57Z"/><path class="cls-2" d="M378.75,345.32a107.41,107.41,0,0,1-38.17,32h62.78C391.81,370.34,382.4,359.87,378.75,345.32Z"/><path class="cls-2" d="M635.45,365.47h0L542.15,263v56l0,.05c0,.11,0,.21,0,.32q0,17.94-6.74,31.16A64.36,64.36,0,0,1,517,372.68a79.42,79.42,0,0,1-6.82,4.64H649.59c-1.24-.78-2.5-1.54-3.69-2.39A62.55,62.55,0,0,1,635.45,365.47Z"/><path class="cls-2" d="M169.7,318.37q-9-22.19-9-48.62t9-48.62a116.3,116.3,0,0,1,24.94-38.4,104.39,104.39,0,0,1,7.93-7.23h-76.9V331.29q0,15.46,8.48,23.94c5.65,5.66,13.46,8.77,23.43,8.77h45a104.25,104.25,0,0,1-7.92-7.23A116.09,116.09,0,0,1,169.7,318.37Z"/><path class="cls-2" d="M348.72,203.18A87.93,87.93,0,0,0,319.81,183q-17-7.48-37.9-7.47T244,183a87.77,87.77,0,0,0-28.92,20.2,85.31,85.31,0,0,0-18.2,29.92,110.75,110.75,0,0,0,0,73.3,85.26,85.26,0,0,0,18.2,29.93A87.89,87.89,0,0,0,244,356.52q17,7.47,37.9,7.48t37.9-7.48a88.05,88.05,0,0,0,28.91-20.19,85.58,85.58,0,0,0,18.21-29.93,110.92,110.92,0,0,0,0-73.3A85.63,85.63,0,0,0,348.72,203.18Z"/><path class="cls-2" d="M99,354.48a79.45,79.45,0,0,1-3.25-23.19V175.5H45.89V309.77a67.54,67.54,0,0,0,67.54,67.55h0a41.19,41.19,0,0,1-4.21-4.14A49.71,49.71,0,0,1,99,354.48Z"/><path class="cls-2" d="M713,271.45a128.4,128.4,0,0,0-31.91,8.48q-14.2,6-22.69,16t-8.48,24.93a36.7,36.7,0,0,0,4.73,18.69A41.51,41.51,0,0,0,666.85,353a57.24,57.24,0,0,0,16.7,8,65.22,65.22,0,0,0,18.7,2.74q23.94,0,39.4-7t24.68-18.2a63.62,63.62,0,0,0,12.72-25.44,120.09,120.09,0,0,0,3.48-28.67V269H749.62A262.81,262.81,0,0,0,713,271.45Z"/><path class="cls-2" d="M476.81,284.91a303.81,303.81,0,0,0-32.91-7.72,141.27,141.27,0,0,1-32.92-10,78.92,78.92,0,0,1-8.2-4.36c.11,2.29.3,4.55.3,6.9A133.4,133.4,0,0,1,398,307c-7.54,24,10.15,40.46,22.43,46.94a82.32,82.32,0,0,0,38.9,9.72,62.51,62.51,0,0,0,18.95-3,58.11,58.11,0,0,0,17-8.47A46.14,46.14,0,0,0,507.47,339a33.16,33.16,0,0,0,4.74-17.71q0-15.94-10.22-23.93A76.17,76.17,0,0,0,476.81,284.91Z"/><path class="cls-2" d="M764.08,188.17q-18.44-12.94-46.37-13c-26.6,0-52.83,10.46-77.43,32.26L581.05,260l41.17,43.59c2.13-9,5.73-16.85,11.22-23.14a83.8,83.8,0,0,1,33.16-23.19,157.29,157.29,0,0,1,41.89-10Q730.66,245,747.63,245h34.9v-14Q782.53,201.16,764.08,188.17Z"/><path class="cls-2" d="M488,181.66c-9.33-3.87-16-6.45-33.64-6.45a60.78,60.78,0,0,0-16.7,2.49,59,59,0,0,0-15.71,7,40.61,40.61,0,0,0-11.71,11.47,27.68,27.68,0,0,0-4.74,15.95q0,15.48,10.22,23t25.18,11.72A306.07,306.07,0,0,0,473.81,254a133,133,0,0,1,32.92,9.72c1.91.86,3.71,1.86,5.51,2.87V210.9C512.24,195.71,494.06,184.18,488,181.66Z"/></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+54 -6
View File
@@ -1,12 +1,19 @@
import moment from 'moment'
import jwtDecode from 'jwt-decode'
import Datetime from 'react-datetime'
import _ from 'lodash'
import { API_DATE_FORMAT, DISPLAY_DATE_FORMAT, TOKEN_NAME } from '../constants'
import toskaLogo from '../assets/toska.png'
import irtomikko from '../assets/irtomikko.png'
import { sendLog, login } from '../apiConnection'
export const setToken = token => localStorage.setItem(TOKEN_NAME, token)
export const textAndDescriptionSearch = (dropDownOptions, param) =>
_.filter(dropDownOptions, option =>
option.text.toLowerCase().concat(option.description.toLowerCase())
.includes(param.toLowerCase()))
export const decodeToken = (token) => {
try {
return jwtDecode(token)
@@ -14,7 +21,10 @@ export const decodeToken = (token) => {
return {}
}
}
export const images = {
toskaLogo,
irtomikko
}
export const tokenAccessInvalid = (token) => {
const decodedToken = decodeToken(token)
// Expired
@@ -43,6 +53,28 @@ export const userIsAdmin = async () => {
const token = await getToken()
return token ? decodeToken(token).admin : false
}
export const userIsCzar = async () => {
const token = await getToken()
return token ? decodeToken(token).czar : false
}
export const getUserName = async () => {
const token = await getToken()
return token ? decodeToken(token).userId : false
}
export const setUserLanguage = (language) => {
localStorage.setItem('language', language)
}
export const getUserLanguage = async () => {
let lang = localStorage.getItem('language')
if (!lang) {
const token = await getToken()
lang = decodeToken(token).language
setUserLanguage(lang)
}
return lang
}
export const userIsEnabled = async () => {
const token = await getToken()
@@ -53,7 +85,7 @@ export const containsOnlyNumbers = str => str.match('^\\d+$')
export const momentFromFormat = (date, format) => moment(date, format)
export const reformatDate = (date, outputFormat) => moment(date).format(outputFormat)
export const reformatDate = (date, outputFormat) => (!date ? 'Unavailable' : moment(date).format(outputFormat))
export const isInDateFormat = (date, format) => moment(date, format, true).isValid()
export const isValidYear = year => (year.isSameOrBefore(Datetime.moment(), 'year')
@@ -70,12 +102,15 @@ export const byName = (a, b) => a.name.localeCompare(b.name)
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???
/* This should be done in backend */
export const removeInvalidCreditsFromStudent = student => ({
...student,
courses: student.courses.map((course) => {
if (course.credits >= 25) {
course.credits = 0
if (course.credits) {
course.credits = course.credits // DOES NOTHING NOW, TOO AFRAID TO REMOVE
}
return course
})
@@ -84,7 +119,11 @@ export const removeInvalidCreditsFromStudent = student => ({
export const removeInvalidCreditsFromStudents = students =>
students.map(student => removeInvalidCreditsFromStudent(student))
export const getStudentTotalCredits = student => student.courses.reduce((a, b) => a + b.credits, 0)
export const getStudentTotalCredits = student => student.courses
.filter(c => c.passed && !c.isStudyModuleCredit).reduce((a, b) => a + b.credits, 0)
export const getTotalCreditsFromCourses = courses =>
courses.filter(c => c.passed && !c.isStudyModuleCredit).reduce((a, b) => a + b.credits, 0)
export const log = async (msg, meta) => {
const token = await getToken()
@@ -92,3 +131,12 @@ export const log = async (msg, meta) => {
const combinedMeta = { ...decoded, ...meta }
sendLog({ message: msg, full_message: combinedMeta })
}
export const copyToClipboard = (text) => {
const textField = document.createElement('textarea')
textField.innerText = text
document.body.appendChild(textField)
textField.select()
document.execCommand('copy')
textField.remove()
}
+16
View File
@@ -0,0 +1,16 @@
const infoTooltips = {
PopulationStatistics: {
Filters: {
CreditsAtLeast: { en: 'Removes students that do not have at least given amount of credits' },
CreditsLessThan: { en: 'Removes students that have more than the given amount of credits' },
StartingThisSemester: { en: 'Switch the toggle to choose a filter that removes students that have studied before this population enrollment date or vice versa' },
EnrollmentStatus: { en: 'Show students that we\'re present or absent during the chosen semesters' },
CanceledStudyright: { en: 'Show students that officially canceled their studyright to the queried program' },
DisciplineTypes: { en: 'Automatically gives course participation filters for the given course types of the given discipline. You can set a limit for participations to bring up only the popular courses.' },
ExtentGraduated: { en: 'Build a filter to students that have or have not studied or graduated a given extent of studies. For example here you can show students that have graduated from a bachelor\'s degree' },
TransferFilter: { en: 'You can choose students that transfered from a given programme to another. Transfer means a student that changed their program during his studyright (not a student getting another studyright at some other institution)' }
}
}
}
export default infoTooltips
+61 -9
View File
@@ -1,7 +1,11 @@
import React from 'react'
import { Dimmer, Header, Icon, Container, Button } from 'semantic-ui-react'
import { Dimmer, Header, Image, Container, Button } from 'semantic-ui-react'
import { bool } from 'prop-types'
import Highcharts from 'highcharts'
import ReactHighchart from 'react-highcharts'
import { logout } from '../../apiConnection'
import { images } from '../../common'
import MulticolorBarChart from '../MulticolorBarChart'
@@ -13,25 +17,73 @@ const dummyData = [
]
const AccessDenied = ({ itWasError }) => {
const header = itWasError ? 'Something broke' : 'You do not have access to Oodikone'
const header = itWasError ? 'Something broke' : 'Welcome to Oodikone!'
const guide = 'try refreshing your browser window, pressing log out or contacting grp-toska@helsinki.fi'
const subheader = itWasError ? `If this was not intended ${guide}` : `If you should have access ${guide}`
const subheader = itWasError ? `If this was not intended ${guide}` : 'You\'re currently not allowed to enter but you will get an email when you\'re authorized'
return (
<div >
<Container style={{ margin: '5%' }}>
<MulticolorBarChart chartTitle="Past" chartData={dummyData} />
<ReactHighchart
highcharts={Highcharts}
constructorType="stockChart"
config={{
title: {
text: 'Students of Computer Science 2018-2019'
},
yAxis: {
title: {
text: 'Cumulative credits'
}
},
plotOptions: {
series: {
label: {
connectorAllowed: false
},
pointStart: 2010
}
},
series: [{
name: 'mluukkai',
data: [18937, 29057, 33213, 40949, 55880, 60421, 77543, 87691]
}, {
name: 'jakousa',
data: [15411, 17960, 34037, 58382, 56367, 63103, 79570, 83898]
}, {
name: 'totutotu',
data: [12482, 26592, 32348, 69576, 78155, 80379, 83568, 83165]
}, {
name: 'sasumaki',
data: [17358, 24823, 36578, 47617, 59341, 68391, 72326, 96022]
}, {
name: 'ikuisma',
data: [8536, 21650, 35013, 45562, 65750, 68431, 74402, 83202]
},
{
name: 'eero3',
data: [18855, 24929, 38722, 45049, 51706, 68569, 66225, 72269]
},
{
name: 'mitiaine',
data: [500, 1000, 20000, 56000, 59000, 65425, 69800, 80000]
}]
}}
/>
<MulticolorBarChart chartTitle="Your students future" chartData={dummyData.concat(dummyData)} />
</Container>
<Dimmer
active
page
>
<Header as="h2" icon inverted>
<Icon name="heart" />
Sorry
<Header.Subheader>{header}</Header.Subheader>
<br />
<Image src={images.toskaLogo} size="medium" centered style={{ paddingTop: '2%' }} />
<Header as="h2" inverted>
<p>{header}</p>
<Header.Subheader>{subheader}</Header.Subheader>
<br />
<Button onClick={logout} color="pink"> Log out </Button>
@@ -0,0 +1,76 @@
import React, { Component } from 'react'
import { Input } from 'semantic-ui-react'
import { func, string, number } from 'prop-types'
import Timeout from '../Timeout'
const TIMEOUTS = {
FETCH: 'fetch',
SEARCH: 'search'
}
class AutoSubmitSearchInput extends Component {
state = {
searchterm: '',
loading: false
}
resetComponent = () => {
this.setState({ searchterm: '' })
}
executeSearch = (searchterm) => {
this.setState({ loading: true })
this.props.setTimeout(TIMEOUTS.FETCH, () => {
}, this.props.latency)
this.props.doSearch(searchterm).then(() => {
this.props.clearTimeout(TIMEOUTS.FETCH)
this.setState({ loading: false })
})
}
handleSearchChange = (e, { value }) => {
this.props.clearTimeout(TIMEOUTS.SEARCH)
if (value.length > 0) {
this.setState({ searchterm: value })
if (value.length > this.props.minSearchLength) {
this.props.setTimeout(TIMEOUTS.SEARCH, () => {
this.executeSearch(value)
}, this.props.latency)
}
} else {
this.resetComponent()
}
}
render() {
return (
<Input
fluid
icon={this.props.icon}
value={this.state.searchterm}
onChange={this.handleSearchChange}
placeholder={this.props.placeholder}
loading={this.state.loading}
/>
)
}
}
AutoSubmitSearchInput.propTypes = {
clearTimeout: func.isRequired,
setTimeout: func.isRequired,
doSearch: func.isRequired,
placeholder: string,
icon: string,
latency: number,
minSearchLength: number
}
AutoSubmitSearchInput.defaultProps = {
placeholder: 'Search...',
icon: 'search',
latency: 250,
minSearchLength: 4
}
export default Timeout(AutoSubmitSearchInput)
@@ -0,0 +1,3 @@
.button {
margin-top: 10px !important;
}
+191
View File
@@ -0,0 +1,191 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Header, Button, Message, Table, Input, Segment, Icon, Loader } from 'semantic-ui-react'
import { getDuplicates, addDuplicate, removeDuplicate } from '../../redux/coursecodeduplicates'
import CourseSearch from '../CourseSearch'
import LanguageChooser from '../LanguageChooser'
import sharedStyles from '../../styles/shared'
import styles from './courseCodeMapper.css'
const { func, shape, string } = PropTypes
class CourseCodeMapper extends Component {
constructor(props) {
super(props)
props.getDuplicates()
}
state = {
codeFilter: '',
nameFilter: '',
code1: '',
code2: ''
}
getName = (name) => {
const { language } = this.props
const res = name[language] ? name[language] : name.fi
return res
}
getTableRows = () => {
const { courseCodeDuplicates, language } = this.props
const { data } = courseCodeDuplicates
const filteredKeys = this.filterNames(data, this.filterCodes(data))
const rows = filteredKeys.map((key) => {
const course = courseCodeDuplicates.data[key]
return (
<Table.Row key={key + Object.keys(course.alt).map(altkey => altkey + course.alt[key])}>
<Table.Cell>{key}</Table.Cell>
<Table.Cell>{this.getName(course.name)}</Table.Cell>
<Table.Cell>{course.main}</Table.Cell>
<Table.Cell>{Object.keys(course.alt).map(altkey => (
<React.Fragment key={course.alt[altkey] + altkey}>
{altkey}
<Icon color="red" name="remove circle" onClick={this.removeDuplicate(key, altkey)} />
</React.Fragment>))}
</Table.Cell>
<Table.Cell>
{Object.keys(course.alt).map(altKey => course.alt[altKey][language]).toString()}
</Table.Cell>
</Table.Row>)
})
return rows
}
filterNames = (duplicates, keys) => {
const filter = this.state.nameFilter.toLocaleLowerCase()
return keys.filter((key) => {
const course = duplicates[key]
return (
this.getName(course.name).toLocaleLowerCase().includes(filter) ||
Object.values(course.alt).find(name =>
this.getName(name).toLocaleLowerCase().includes(filter))
)
})
}
filterCodes = (duplicates) => {
const filter = this.state.codeFilter.toLocaleLowerCase()
const keys = Object.keys(duplicates)
return keys.filter((key) => {
const course = duplicates[key]
return (
key.toLocaleLowerCase().includes(filter) ||
Object.keys(course.alt).find(code =>
code.toLocaleLowerCase().includes(filter))
)
})
}
handleCodeFilterChange = (e) => {
this.setState({ codeFilter: e.target.value })
}
handleNameFilterChange = (e) => {
this.setState({ nameFilter: e.target.value })
}
handleResultSelect1 = (e, { result }) => {
this.setState({ code1: result.code })
}
handleResultSelect2 = (e, { result }) => {
this.setState({ code2: result.code })
}
addDuplicate = (code1, code2) => () => {
this.props.addDuplicate(code1, code2)
}
removeDuplicate = (code1, code2) => () => {
this.props.removeDuplicate(code1, code2)
}
render() {
const { pending } = this.props.courseCodeDuplicates
const disabled = !((this.state.code1 && this.state.code2) &&
(this.state.code1 !== this.state.code2))
return (
<div className={sharedStyles.segmentContainer}>
<Segment className={sharedStyles.contentSegment}>
<Header className={sharedStyles.segmentTitle} size="large">Course Code Mapping</Header>
<Message
header="Map corresponding course codes to each other"
content="By default courses with different codes are considered as separate courses.
If this is not the case use this to combine old and new course codes to each other."
/>
<Loader active={pending} />
<Segment.Group vertical>
<Segment.Group horizontal>
<Segment>
<Header content="Filter course codes" />
<Input
fluid
placeholder="By Code"
onChange={this.handleCodeFilterChange}
/>
<Input
fluid
placeholder="By Name"
onChange={this.handleNameFilterChange}
/>
</Segment>
<Segment>
<Header>Add new corresponding code</Header>
<CourseSearch handleResultSelect={this.handleResultSelect1} />
<CourseSearch handleResultSelect={this.handleResultSelect2} />
<Button disabled={disabled} className={styles.button} content="Add" onClick={this.addDuplicate(this.state.code1, this.state.code2)} />
</Segment>
</Segment.Group>
<Segment>
<Header>Language</Header>
<LanguageChooser />
</Segment>
</Segment.Group>
<Table striped>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Code</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Main code</Table.HeaderCell>
<Table.HeaderCell>Alternative code(s)</Table.HeaderCell>
<Table.HeaderCell>Alternative name(s)</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{this.getTableRows()}
</Table.Body>
</Table>
</Segment>
</div>
)
}
}
CourseCodeMapper.propTypes = {
language: string.isRequired,
getDuplicates: func.isRequired,
addDuplicate: func.isRequired,
removeDuplicate: func.isRequired,
courseCodeDuplicates: shape({}).isRequired
}
const mapStateToProps = ({ courseCodeDuplicates, settings }) => ({
courseCodeDuplicates,
language: settings.language
})
const mapDispatchToProps = dispatch => ({
getDuplicates: () =>
dispatch(getDuplicates()),
addDuplicate: (code1, code2) =>
dispatch(addDuplicate(code1, code2)),
removeDuplicate: (code1, code2) =>
dispatch(removeDuplicate(code1, code2))
})
export default connect(mapStateToProps, mapDispatchToProps)(CourseCodeMapper)
+2
View File
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types'
import { Dropdown, Header, List, Button } from 'semantic-ui-react'
import CourseInstanceStatistics from '../CourseInstanceStatistics'
import CourseSearch from '../CourseSearch'
import LanguageChooser from '../LanguageChooser'
import Timeout from '../Timeout'
import { reformatDate } from '../../common'
@@ -75,6 +76,7 @@ class CourseInstances extends Component {
<Header className={sharedStyles.segmentTitle} size="large">
Course Statistics
</Header>
<LanguageChooser />
<CourseSearch handleResultSelect={this.handleResultSelect} />
<Header as="h3">
@@ -1,62 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts'
import { Header, Button } from 'semantic-ui-react'
import sharedStyles from '../../styles/shared'
import styles from './coursePassRateChart.css'
import { red, green, turquoise } from '../../styles/variables/colors'
const { array, shape, string, func } = PropTypes
const StackedBarChart = ({ stats, removeCourseStatistics }) => {
const data = stats.stats.map(year => (
{ name: year.time, passed: year.passed, failed: year.failed, all: year.failed + year.passed }))
const { name, code, start, end, separate } = stats
const query = { code, start, end, separate }
if (data.length > 0) {
return (
<div>
<Header className={sharedStyles.segmentTitle} size="large">{name}, {code}</Header>
<div className={styles.chartContainer}>
<BarChart
height={700}
width={1200}
data={data}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="all" stackId="a" fill={turquoise} />
<Bar dataKey="passed" stackId="b" fill={green} />
<Bar dataKey="failed" stackId="c" fill={red} />
</BarChart>
<Button className={styles.remove} onClick={removeCourseStatistics(query)}>Remove</Button>
</div>
</div>
)
}
return (
<div className={sharedStyles.container}>
<Header className={sharedStyles.segmentTitle} size="large">
No statistics for course {name}, {code} during time {start}-{end}
</Header>
<Button onClick={removeCourseStatistics(query)}>Remove</Button>
</div>
)
}
StackedBarChart.propTypes = {
removeCourseStatistics: func.isRequired,
stats: shape({
code: string.isRequired,
stats: array.isRequired
}).isRequired
}
export default StackedBarChart
@@ -3,3 +3,10 @@
width: 100%;
max-width: 750px;
}
.container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
+13 -5
View File
@@ -41,15 +41,21 @@ class CourseSearch extends Component {
}
fetchCoursesList = (searchStr) => {
const { language } = this.props
if (searchStr.length >= 3) {
this.setState({ isLoading: true })
this.props.findCourses(searchStr)
this.props.findCourses(searchStr, language)
.then(() => this.setState({ isLoading: false }))
} else {
this.props.findCourses('')
}
}
selectCourse = (a, b) => {
this.setState({ searchStr: `${b.result.name} ( ${b.result.code} )` })
this.props.handleResultSelect(a, b)
}
render() {
const { isLoading, searchStr } = this.state
const { courseList } = this.props
@@ -62,7 +68,7 @@ class CourseSearch extends Component {
input={{ fluid: true }}
loading={isLoading}
placeholder="Search by entering a course code or name"
onResultSelect={this.props.handleResultSelect}
onResultSelect={this.selectCourse}
onSearchChange={this.handleSearchChange}
results={coursesToRender}
resultRenderer={CourseListRenderer}
@@ -73,6 +79,7 @@ class CourseSearch extends Component {
}
CourseSearch.propTypes = {
language: string.isRequired,
findCourses: func.isRequired,
courseList: arrayOf(object).isRequired,
setTimeout: func.isRequired,
@@ -82,14 +89,15 @@ CourseSearch.propTypes = {
const sortCourses = makeSortCourses()
const mapStateToProps = ({ locale, courses }) => ({
const mapStateToProps = ({ locale, courses, settings }) => ({
language: settings.language,
courseList: sortCourses(courses),
translate: getTranslate(locale),
currentLanguage: getActiveLanguage(locale).value
})
const mapDispatchToProps = dispatch => ({
findCourses: query =>
dispatch(findCourses(query))
findCourses: (query, language) =>
dispatch(findCourses(query, language))
})
export default connect(mapStateToProps, mapDispatchToProps)(Timeout(CourseSearch))
@@ -0,0 +1,39 @@
import React from 'react'
import { Table } from 'semantic-ui-react'
import { string, arrayOf, shape, number, oneOfType } from 'prop-types'
const CumulativeTable = ({ categoryName, data }) => (
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell content={categoryName} />
<Table.HeaderCell content="Passed" />
<Table.HeaderCell content="Failed" />
<Table.HeaderCell content="Pass rate" />
</Table.Row>
</Table.Header>
<Table.Body>
{ data.map(({ id, category, passed, failed, passrate }) => (
<Table.Row key={id}>
<Table.Cell content={category} />
<Table.Cell content={passed} />
<Table.Cell content={failed} />
<Table.Cell content={`${passrate || 0} %`} />
</Table.Row>
))}
</Table.Body>
</Table>
)
CumulativeTable.propTypes = {
categoryName: string.isRequired,
data: arrayOf(shape({
id: oneOfType([number, string]),
category: oneOfType([number, string]),
passed: oneOfType([number, string]),
failed: oneOfType([number, string]),
passrate: oneOfType([number, string])
})).isRequired
}
export default CumulativeTable
@@ -0,0 +1,402 @@
import React, { Component } from 'react'
import { Tab, Table, Grid, Header, Radio, Form } from 'semantic-ui-react'
import { shape, string, number, oneOfType, arrayOf, func, bool } from 'prop-types'
import _ from 'lodash'
import StackedBarChart from '../../StackedBarChart'
import { passRateCumGraphOptions, passRateStudGraphOptions, gradeGraphOptions } from '../../../constants'
import { userIsAdmin } from '../../../common'
const StatsCumTable = ({ stats, name }) => (
<div>
<Header as="h3" content={name} textAlign="center" />
<Table
headerRow={['Time', 'Passed', 'Failed', 'Pass rate']}
tableData={stats}
renderBodyRow={stat => ({
key: stat.code,
cells: [
stat.name,
stat.cumulative.categories.passed || 0,
stat.cumulative.categories.failed || 0,
`${Number((100 * stat.cumulative.categories.passed) /
(stat.cumulative.categories.failed + stat.cumulative.categories.passed)).toFixed(2)} %`
]
})}
/>
</div>
)
StatsCumTable.propTypes = {
stats: arrayOf(shape({})).isRequired,
name: oneOfType([number, string]).isRequired
}
const StatsStudTable = ({ stats, name }) => (
<div>
<Header as="h3" content={name} textAlign="center" />
<Table
headerRow={['Time', 'Passed on first try', 'Passed after retry', 'Failed on first try', 'Failed after retry']}
tableData={stats}
renderBodyRow={stat => ({
key: stat.code,
cells: [
stat.name,
stat.students.categories.passedFirst || 0,
stat.students.categories.passedRetry || 0,
stat.students.categories.failedFirst || 0,
stat.students.categories.failedRetry || 0
]
})}
/>
</div>)
StatsStudTable.propTypes = {
stats: arrayOf(shape({})).isRequired,
name: oneOfType([number, string]).isRequired
}
const getPassRateCumSeriesFromStats = (stats, multiplier = 1, name = '') => {
const all = []
const passed = []
const failed = []
stats.forEach((year) => {
const { passed: p, failed: f } = year.cumulative.categories
all.push((p * multiplier) + (f * multiplier))
passed.push(p * multiplier)
failed.push(f * multiplier)
})
const series = [
{
name: `${name} all`,
data: all,
stack: 'a'
},
{
name: `${name} passed`,
data: passed,
stack: 'b'
},
{
name: `${name} failed`,
data: failed,
stack: 'c'
}
]
return series
}
const getPassRateStudSeriesFromStats = (stats, multiplier = 1, name = '') => {
const all = []
const failedFirst = []
const failedRetry = []
const passedFirst = []
const passedRetry = []
stats.forEach((year) => {
const {
failedFirst: ff,
failedRetry: fr,
passedFirst: pf,
passedRetry: pr
} = year.students.categories
all.push((ff * multiplier) + (fr * multiplier) + (pf * multiplier) + (pr * multiplier))
failedFirst.push(ff * multiplier)
failedRetry.push(fr * multiplier)
passedFirst.push(pf * multiplier)
passedRetry.push(pr * multiplier)
})
const series = [
{
name: `${name} all`,
data: all,
stack: 'a'
},
{
name: `${name} passed on first try`,
data: passedFirst,
stack: 'b'
},
{
name: `${name} passed after retry`,
data: passedRetry,
stack: 'b'
},
{
name: `${name} failed on first try`,
data: failedFirst,
stack: 'c'
},
{
name: `${name} failed after retry`,
data: failedRetry,
stack: 'c'
}
]
return series
}
const getGradeCumSeriesFromStats = (stats, multiplier = 1, name = '') => {
const zeros = []
const grades = {}
stats.forEach((year) => {
const fails = []
Object.entries(year.cumulative.grades).forEach(([key, value]) => {
if (['Eisa', 'Hyl.', '0', 'Luop'].includes(key)) {
fails.push(value)
}
if (!grades[key]) {
grades[key] = []
}
grades[key].push(value * multiplier)
})
zeros.push(_.sum(fails) * multiplier)
})
const seriesData = { 0: zeros, ...grades }
const series = [
{
name: `${name} 0`,
data: seriesData['0'],
stack: 'a'
},
{
name: `${name} 1`,
data: seriesData['1'],
stack: 'b'
},
{
name: `${name} 2`,
data: seriesData['2'],
stack: 'c'
},
{
name: `${name} 3`,
data: seriesData['3'],
stack: 'd'
},
{
name: `${name} 4`,
data: seriesData['4'],
stack: 'e'
},
{
name: `${name} 5`,
data: seriesData['5'],
stack: 'f'
},
{
name: `${name} Hyv.`,
data: seriesData['Hyv.'],
stack: 'g'
}
]
return series
}
const getGradeStudSeriesFromStats = (stats, multiplier = 1, name = '') => {
const zeros = []
const grades = {}
stats.forEach((year) => {
const fails = []
Object.entries(year.students.grades).forEach(([key, value]) => {
if (['Eisa', 'Hyl.', '0', 'Luop'].includes(key)) {
fails.push(value)
}
if (!grades[key]) {
grades[key] = []
}
grades[key].push(value * multiplier)
})
zeros.push(_.sum(fails) * multiplier)
})
const seriesData = { 0: zeros, ...grades }
const series = [
{
name: `${name} 0`,
data: seriesData['0'],
stack: 'a'
},
{
name: `${name} 1`,
data: seriesData['1'],
stack: 'b'
},
{
name: `${name} 2`,
data: seriesData['2'],
stack: 'c'
},
{
name: `${name} 3`,
data: seriesData['3'],
stack: 'd'
},
{
name: `${name} 4`,
data: seriesData['4'],
stack: 'e'
},
{
name: `${name} 5`,
data: seriesData['5'],
stack: 'f'
},
{
name: `${name} Hyv.`,
data: seriesData['Hyv.'],
stack: 'g'
}
]
return series
}
class ResultTabs extends Component {
state = {}
async componentDidMount() {
const adminRights = await userIsAdmin()
this.setState({ adminRights })
}
render() {
const { max, primary, comparison, changeMode, cumMode } = this.props
const { maxPassRateVal, maxGradeVal } = max
const passGraphSerie = cumMode ?
getPassRateCumSeriesFromStats(primary.stats, 1, 'primary').concat(getPassRateCumSeriesFromStats(comparison ? comparison.stats : [], -1, 'comparison'))
:
getPassRateStudSeriesFromStats(primary.stats, 1, 'primary').concat(getPassRateStudSeriesFromStats(comparison ? comparison.stats : [], -1, 'comparison'))
const graphOptions = cumMode ?
passRateCumGraphOptions(primary.stats.map(year =>
year.name), maxPassRateVal)
:
passRateStudGraphOptions(primary.stats.map(year =>
year.name), maxPassRateVal)
const gradeGraphSerie = cumMode ?
getGradeCumSeriesFromStats(primary.stats, 1, 'primary').concat(getGradeCumSeriesFromStats(comparison ? comparison.stats : [], -1, 'comparison'))
:
getGradeStudSeriesFromStats(primary.stats, 1, 'primary').concat(getGradeCumSeriesFromStats(comparison ? comparison.stats : [], -1, 'comparison'))
const panes = [
{
menuItem: { key: 'Table', icon: 'table', content: 'Table' },
render: () => (
<Grid padded="vertically" columns="equal">
<Grid.Row>
{primary && (
<Grid.Column>
{cumMode ?
<StatsCumTable name={primary.name} stats={primary.stats} />
:
<StatsStudTable name={primary.name} stats={primary.stats} />
}
</Grid.Column>
)}
{
comparison && (
<Grid.Column>
{cumMode ?
<StatsCumTable name={primary.name} stats={primary.stats} />
:
<StatsStudTable name={primary.name} stats={primary.stats} />
}
</Grid.Column>
)
}
</Grid.Row>
</Grid>
)
},
{
menuItem: { key: 'pass', icon: 'balance', content: 'Pass rate chart' },
render: () => (
<Grid padded="vertically" columns="equal">
<Grid.Row>
{primary && (
<Grid.Column>
<StackedBarChart
options={graphOptions}
series={passGraphSerie}
/>
</Grid.Column>
)}
</Grid.Row>
</Grid>
)
}
]
if (this.state.adminRights) {
panes.push({
menuItem: { key: 'grade', icon: 'chart bar', content: 'Grade distribution chart' },
render: () => (
<Grid padded="vertically" columns="equal">
<Grid.Row>
{primary && (
<Grid.Column>
<StackedBarChart
options={gradeGraphOptions(primary.stats.map(year =>
year.name), maxGradeVal)}
series={gradeGraphSerie}
/>
</Grid.Column>
)}
</Grid.Row>
</Grid>
)
})
}
return (
<div>
<Form>
<Form.Group inline >
<Form.Field>
<label>Cumulative</label>
</Form.Field>
<Form.Field>
<Radio
toggle
onChange={changeMode}
/>
</Form.Field>
<Form.Field>
<label>Student</label>
</Form.Field>
</Form.Group>
</Form>
<Tab
panes={panes}
/>
</div>)
}
}
ResultTabs.propTypes = {
primary: shape({
name: string,
code: oneOfType([string, number]),
stats: arrayOf(shape({}))
}).isRequired,
comparison: shape({
name: string,
code: oneOfType([string, number]),
stats: arrayOf(shape({}))
}),
max: shape({
maxPassRateVal: number,
maxGradeVal: number
}).isRequired,
changeMode: func.isRequired,
cumMode: bool.isRequired
}
ResultTabs.defaultProps = {
comparison: undefined
}
export default ResultTabs
@@ -0,0 +1,318 @@
import React, { Component } from 'react'
import { Segment, Header, Form, Table, Radio, Menu } from 'semantic-ui-react'
import { connect } from 'react-redux'
import { func, arrayOf, shape, string, bool } from 'prop-types'
import { getCourseTypes } from '../../../redux/coursetypes'
import { getCourseDisciplines } from '../../../redux/coursedisciplines'
import { getSemesters } from '../../../redux/semesters'
import { findCourses, clearCourses } from '../../../redux/coursesearch'
import { getCourseStats, clearCourseStats } from '../../../redux/coursestats'
import AutoSubmitSearchInput from '../../AutoSubmitSearchInput'
const CourseTable = ({ courses, selected, onSelectCourse }) => (
<Segment basic style={{ padding: '0' }} >
<Table>
{selected.length > 0 && (
<Table.Header>
<Table.Row>
<Table.HeaderCell content="Selected courses" />
<Table.HeaderCell content="Code" />
<Table.HeaderCell content="Select" />
</Table.Row>
</Table.Header>
)}
{selected.length > 0 && (
<Table.Body>
{selected.map(course => (
<Table.Row key={course.code}>
<Table.Cell content={course.name} />
<Table.Cell content={course.code} />
<Table.Cell>
<Radio toggle checked={course.selected} onChange={() => onSelectCourse(course)} />
</Table.Cell>
</Table.Row>
))}
</Table.Body>
)}
<Table.Header>
<Table.Row>
<Table.HeaderCell content="Searched courses" />
<Table.HeaderCell content="Code" />
<Table.HeaderCell content="Select" />
</Table.Row>
</Table.Header>
<Table.Body>
{courses.length === 0 ? (
<Table.Row>
<Table.Cell colSpan="3" content="No courses matched your query" />
</Table.Row>
) : courses.map(course => (
<Table.Row key={course.code}>
<Table.Cell content={course.name} />
<Table.Cell content={course.code} />
<Table.Cell>
<Radio toggle checked={course.selected} onChange={() => onSelectCourse(course)} />
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</Segment>
)
CourseTable.propTypes = {
courses: arrayOf(shape({ code: string, name: string, seleted: bool })).isRequired,
selected: arrayOf(shape({ code: string, name: string, seleted: bool })).isRequired,
onSelectCourse: func.isRequired
}
const INITIAL = {
displaycourses: false,
searchterm: undefined,
selectedcourses: {},
fromYear: undefined,
toYear: undefined,
separate: false,
expanded: true,
discipline: undefined,
type: undefined,
focus: false
}
class SearchForm extends Component {
state = { ...INITIAL }
componentDidMount() {
this.props.clearCourses()
this.props.getCourseTypes()
this.props.getCourseDisciplines()
this.props.getSemesters()
}
toggleCourseView = () => {
this.setState({ displaycourses: !this.state.displaycourses })
}
searchOnEnter = (e) => {
if (e.key === 'Enter') {
this.fetchCourses(this.state.searchterm)
}
}
toggleCourse = (course) => {
course.selected = !course.selected
const { selectedcourses } = this.state
const isSelected = !!selectedcourses[course.code]
if (isSelected) {
const { [course.code]: omit, ...rest } = selectedcourses
this.setState({ selectedcourses: rest })
} else {
this.setState({
selectedcourses: {
...selectedcourses,
[course.code]: { ...course, selected: true }
}
})
}
}
handleChange = (e, target) => {
const { name, value } = target
this.setState({ [name]: value })
}
handleCourseFormChange = (e, target) => {
const { name, value } = target
const { type, discipline } = { ...this.state, [name]: value }
this.setState({ type, discipline })
if (type && discipline) {
this.props.findCourses({ type, discipline })
}
}
toggleCheckbox = (e, target) => {
const { name } = target
this.setState({ [name]: !this.state[name] })
}
toggleExpanded = () => {
this.setState({ expanded: !this.state.expanded })
}
fetchCourses = (name) => {
const { type, discipline } = this.state
return this.props.findCourses({ name, ...{ type, discipline } })
}
handleSearchChange = (e, { value: searchterm }) => {
this.setState({ searchterm })
}
submitForm = async () => {
const { fromYear, toYear, selectedcourses, separate } = this.state
const params = {
fromYear,
toYear,
courseCodes: Object.keys(selectedcourses),
separate
}
await this.props.getCourseStats(params)
this.setState({ ...INITIAL, expanded: false })
}
expandForm = () => {
this.props.clearCourses()
this.props.clearCourseStats()
this.toggleExpanded()
}
render() {
const { disciplines, coursetypes, years, loading } = this.props
const { selectedcourses, fromYear, toYear, separate, focus } = this.state
const courses = this.props.matchingCourses.map(course => ({
...course,
selected: !!selectedcourses[course.code]
}))
const disabled = (!fromYear || Object.keys(selectedcourses).length === 0)
const selected = Object.values(selectedcourses).map(course => ({ ...course, selected: true }))
return !this.state.expanded
? (
<Menu>
<Menu.Item icon="search" content="New query" onClick={this.expandForm} />
</Menu>
) : (
<Segment loading={loading}>
<Form>
<Header content="Search parameters" onClick={this.toggleExpanded} />
<Form.Group widths="equal">
<Form.Dropdown
label="From:"
name="fromYear"
options={years}
selection
placeholder="Select academic year"
onChange={this.handleChange}
value={fromYear}
/>
<Form.Dropdown
label="To:"
name="toYear"
options={years}
selection
placeholder="Select academic year"
onChange={this.handleChange}
value={toYear}
/>
</Form.Group>
<Form.Checkbox
label="Separate statistics for Spring and Fall semesters"
name="separate"
onChange={this.toggleCheckbox}
checked={separate}
/>
<Header content="Search for courses" />
<Form.Group
widths="equal"
style={{ display: 'none' }}
>
<Form.Dropdown
onChange={this.handleCourseFormChange}
value={this.state.discipline}
label="Discipline:"
name="discipline"
search
options={disciplines}
selection
placeholder="Select a course discipline"
/>
<Form.Dropdown
onChange={this.handleCourseFormChange}
label="Type:"
name="type"
value={this.state.type}
search
options={coursetypes}
selection
placeholder="Select a course type"
/>
</Form.Group>
<div
style={{ marginBottom: '15px' }}
onFocus={() => this.setState({ focus: true })}
onBlur={() => this.setState({ focus: false })}
>
<Form.Field>
<AutoSubmitSearchInput
doSearch={this.fetchCourses}
placeholder="Search by entering a course code or name"
/>
</Form.Field>
{focus ?
<CourseTable
onFocus={() => this.setState({ focus: true })}
loading={this.props.coursesLoading}
selected={selected}
courses={courses}
onSelectCourse={this.toggleCourse}
/>
: null}
</div>
<Form.Button type="button" disabled={disabled} fluid basic positive content="Fetch statistics" onClick={this.submitForm} />
</Form>
</Segment>
)
}
}
SearchForm.propTypes = {
getCourseTypes: func.isRequired,
getCourseDisciplines: func.isRequired,
findCourses: func.isRequired,
getSemesters: func.isRequired,
getCourseStats: func.isRequired,
clearCourses: func.isRequired,
clearCourseStats: func.isRequired,
disciplines: arrayOf(shape({})).isRequired,
coursetypes: arrayOf(shape({})).isRequired,
matchingCourses: arrayOf(shape({})).isRequired,
years: arrayOf(shape({})).isRequired,
loading: bool.isRequired,
coursesLoading: bool.isRequired
}
const mapStateToProps = (state) => {
const disciplines = state.courseDisciplines.data.map(({ discipline_id: id, name }) => ({
key: id,
value: id,
text: name.fi || name.en
}))
const coursetypes = state.courseTypes.data.map(({ coursetypecode: code, name }) => ({
key: code,
value: code,
text: name.fi || name.en
}))
const { years = [] } = state.semesters.data
const { pending } = state.courseStats
return {
disciplines,
coursetypes,
matchingCourses: state.courseSearch.data,
years: Object.values(years).map(({ yearcode, yearname }) => ({
key: yearcode,
text: yearname,
value: yearcode
})).reverse(),
loading: pending,
coursesLoading: state.courseSearch.pending
}
}
export default connect(mapStateToProps, {
getCourseTypes,
getCourseDisciplines,
findCourses,
getSemesters,
getCourseStats,
clearCourses,
clearCourseStats
})(SearchForm)
@@ -0,0 +1,205 @@
import React, { Component } from 'react'
import { Segment, Header, Label, Form, Divider } from 'semantic-ui-react'
import { shape, string, arrayOf, objectOf, oneOfType, number } from 'prop-types'
import _ from 'lodash'
import ResultTabs from '../ResultTabs'
const ALL = {
key: 'all',
value: 'all',
text: 'All'
}
const label = count => ({ content: count, icon: 'user', basic: true })
const countFilteredStudents = (stat, filter) => Object.entries(stat).reduce((acc, entry) => {
const [category, students] = entry
return {
...acc,
[category]: students.filter(filter).length
}
}, {})
class SingleCourseStats extends Component {
state = {
primary: ALL.value,
comparison: undefined,
cumMode: true
}
getMax = stats => ({
maxPassRateVal: _.max(stats.map(year =>
year.attempts.classes.passed.concat(year.attempts.classes.failed).length)),
maxGradeVal: _.max(_.max(stats.map(year =>
Object.values(Object.entries(year.attempts.grades).reduce((acc, [key, value]) => {
if (['Eisa', 'Hyl.', '0', 'Luop'].includes(key)) {
return { ...acc, 0: acc['0'] + value.length }
}
return { ...acc, [key]: value.length }
}, { 0: 0 })))))
})
getProgrammeName = (progcode) => {
if (progcode === ALL.value) {
return 'All'
}
const { name } = this.props.stats.programmes[progcode]
return name.fi || name.en || name.sv
}
programmeOptions = () => {
const { programmes } = this.props.stats
const options = Object.entries(programmes)
.map(([code, { name, students }]) => ({
key: code,
value: code,
text: name.fi || name.en || name.sv,
students: students.length,
label: label(students.length)
}))
.sort((op1, op2) => op2.students - op1.students)
const total = options.reduce((acc, opt) => acc + opt.students, 0)
return [
{ ...ALL, label: label(total) },
...options
]
}
belongsToProgramme = (code) => {
if (code === ALL.value) {
return () => true
}
const { programmes } = this.props.stats
const programme = programmes[code]
if (!programme) {
return () => false
}
const numberset = new Set(programme.students)
return studentnumber => numberset.has(studentnumber)
}
validProgCode = (code) => {
const { programmes } = this.props.stats
return programmes[code] || (code === ALL.value)
}
statsForProgramme = (progcode) => {
const { statistics } = this.props.stats
const filter = this.belongsToProgramme(progcode)
const progstats = statistics.map(({ code, name, students: allstudents, attempts }) => {
const cumulative = {
grades: countFilteredStudents(attempts.grades, filter),
categories: countFilteredStudents(attempts.classes, filter)
}
const students = {
grades: countFilteredStudents(allstudents.grades, filter),
categories: countFilteredStudents(allstudents.classes, filter)
}
return { code, name, cumulative, students }
})
return {
code: progcode,
name: this.getProgrammeName(progcode),
stats: progstats
}
}
handleChange = (e, { name, value }) => {
this.setState({ [name]: value })
}
filteredProgrammeStatistics = () => {
const { primary, comparison } = this.state
const pstats = !this.validProgCode(primary) ? undefined : this.statsForProgramme(primary)
const cstats = !this.validProgCode(comparison)
? undefined
: this.statsForProgramme(comparison)
return {
primary: pstats,
comparison: cstats
}
}
selectedProgrammes = () => {
const { primary: p, comparison: c } = this.state
const { programmes } = this.props.stats
const primary = !programmes[p] ? ALL.value : p
const comparison = !programmes[c] ? undefined : c
return { primary, comparison }
}
changeMode = () => {
this.setState({ cumMode: !this.state.cumMode })
}
render() {
const { stats } = this.props
const { primary, comparison } = this.selectedProgrammes()
const options = this.programmeOptions()
const statistics = this.filteredProgrammeStatistics()
const max = this.getMax(stats.statistics)
return (
<div>
<Segment>
<Header content={stats.name} />
<Label.Group>
{stats.alternatives.map(code => <Label key={code} content={code} />)}
</Label.Group>
<Divider />
<Form>
<Header content="Filter statistics by study programme" as="h4" />
<Form.Group widths="equal" >
<Form.Dropdown
name="primary"
options={options}
label="Primary group"
placeholder="Select a study programme"
value={primary}
onChange={this.handleChange}
search
selection
/>
<Form.Dropdown
name="comparison"
options={options}
label="Comparison group"
placeholder="Optional"
value={comparison}
onChange={this.handleChange}
selection
/>
</Form.Group>
</Form>
</Segment>
<ResultTabs
max={max}
primary={statistics.primary}
comparison={statistics.comparison}
changeMode={this.changeMode}
cumMode={this.state.cumMode}
/>
</div>
)
}
}
SingleCourseStats.propTypes = {
stats: shape({
alternatives: arrayOf(string),
programmes: objectOf(shape({
name: shape({}),
students: arrayOf(string)
})),
statistics: arrayOf(shape({
code: oneOfType([number, string]),
name: string,
attempts: objectOf(shape({
failed: arrayOf(string),
passed: arrayOf(string)
}))
})),
name: string,
coursecode: string
}).isRequired
}
export default SingleCourseStats
@@ -0,0 +1,71 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Table, Radio } from 'semantic-ui-react'
import { shape } from 'prop-types'
import SingleCourseStats from '../SingleCourseStats'
class SingleCourse extends Component {
state={
selected: undefined
}
getStats = () => {
const { stats } = this.props
const statistics = Object.values(stats)
let { selected } = this.state
if (!selected && statistics.length > 0) {
selected = statistics[0].coursecode
}
return {
selected,
statistics,
selectedStatistic: stats[selected]
}
}
render() {
const { selected, statistics, selectedStatistic } = this.getStats()
return (
<div>
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell content="Name" />
<Table.HeaderCell content="Code" />
<Table.HeaderCell content="Select" />
</Table.Row>
</Table.Header>
<Table.Body>
{statistics.map(stat => (
<Table.Row key={stat.coursecode}>
<Table.Cell content={stat.name} />
<Table.Cell content={stat.coursecode} />
<Table.Cell>
<Radio
toggle
checked={stat.coursecode === selected}
onClick={() => this.setState({ selected: stat.coursecode })}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
{ selected && <SingleCourseStats stats={selectedStatistic} /> }
</div>
)
}
}
SingleCourse.propTypes = {
stats: shape({}).isRequired
}
const mapStateToProps = (state) => {
const { data } = state.courseStats
return {
stats: data
}
}
export default connect(mapStateToProps)(SingleCourse)
@@ -0,0 +1,75 @@
import React, { Component } from 'react'
import { Form } from 'semantic-ui-react'
import { connect } from 'react-redux'
import { shape, arrayOf, func, oneOfType, number, string } from 'prop-types'
import selectors from '../../../selectors/courseStats'
import { fields, setValue } from '../../../redux/coursesSummaryForm'
import CumulativeTable from '../CumulativeTable'
class SummaryTab extends Component {
handleChange = (e, { name, value }) => this.props.setValue(name, value)
render() {
const { statistics, programmes } = this.props
const options = programmes.map(p => ({
...p,
label: {
content: p.size,
icon: 'user',
basic: true
}
}))
return (
<div>
<Form>
<Form.Dropdown
options={options}
selection
fluid
label="Study programme"
name={fields.programme}
onChange={this.handleChange}
value={this.props.form[fields.programme]}
/>
</Form>
<CumulativeTable
categoryName="Course"
data={statistics.map(s => ({
id: s.coursecode,
category: s.name,
passed: s.summary.passed,
failed: s.summary.failed,
passrate: s.summary.passrate
}))}
/>
</div>
)
}
}
SummaryTab.propTypes = {
statistics: arrayOf(shape({
coursecode: oneOfType([number, string]),
name: string,
summary: shape({
failed: number,
passed: number,
passrate: oneOfType([number, string])
})
})).isRequired,
programmes: arrayOf(shape({})).isRequired,
form: shape({}).isRequired,
setValue: func.isRequired
}
const mapStateToProps = (state) => {
const programmes = selectors.getAllStudyProgrammes(state)
const programme = state.courseSummaryForm[fields.programme]
return {
form: state.courseSummaryForm,
statistics: selectors.summaryStatistics(state, { programmes, programme }),
programmes
}
}
export default connect(mapStateToProps, { setValue })(SummaryTab)
+30 -211
View File
@@ -1,234 +1,53 @@
import React, { Component } from 'react'
import { Header, Segment, Tab } from 'semantic-ui-react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { Form, Button, Header, Checkbox, Message } from 'semantic-ui-react'
import Datetime from 'react-datetime'
import Timeout from '../Timeout'
import CourseSearch from '../CourseSearch'
import CoursePassRateChart from '../CoursePassRateChart'
import { getCourseStatistics, removeCourseStatistics } from '../../redux/courseStatistics'
import { isValidYear, isInDateFormat, reformatDate, momentFromFormat } from '../../common'
import { bool } from 'prop-types'
import style from './courseStatistics.css'
import sharedStyles from '../../styles/shared'
import SearchForm from './SearchForm'
import SingleCourseTab from './SingleCourseTab'
import SummaryTab from './SummaryTab'
const { shape, func, array } = PropTypes
const INITIAL_YEARS = {
start: '2017',
end: '2018'
}
const SearchResults = () => (
<Tab
menu={{ attached: false }}
panes={[
{
menuItem: 'Summary',
render: () => <SummaryTab />
},
{
menuItem: 'Course',
render: () => <SingleCourseTab />
}
]}
/>
)
class CourseStatistics extends Component {
state = {
selectedCourse: { name: 'No course selected', code: '' },
...INITIAL_YEARS,
separate: false,
validYear: true,
error: '',
isLoading: false
}
handleResultSelect = (e, { result }) => {
this.setState({ selectedCourse: result }, () => {
this.fetchCourseStatistics()
})
}
addYear = change => () => {
const year = this.state[change]
const nextYear = momentFromFormat(year, 'YYYY').add(1, 'year')
this.handleYearSelection(nextYear, change)
}
subtractYear = change => () => {
const year = this.state[change]
const previousYear = momentFromFormat(year, 'YYYY').subtract(1, 'year')
this.handleYearSelection(previousYear, change)
}
fetchCourseStatistics = () => {
const { code } = this.state.selectedCourse
const { start, end, separate } = this.state
const { selected } = this.props.courseStatistics
const query = { code, start: Number(start), end: Number(end), separate: String(separate) }
const aa = selected.find(olquery =>
olquery.separate === query.separate &&
olquery.end === query.end &&
olquery.start === query.start &&
olquery.code === query.code)
if (!aa) {
this.setState({ isLoading: true })
this.props.getCourseStatistics(query)
.then(() => {
this.setState({ error: '', isLoading: false })
})
this.setState({ error: '' })
} else this.setState({ error: 'Course with selected parameters already in analysis' })
}
removeCourseStatistics = query => () => this.props.removeCourseStatistics(query)
startBeforeEnd = (year, change) => {
if (change === 'start') {
return this.state.end > reformatDate(year, 'YYYY')
}
return this.state.start < reformatDate(year, 'YYYY')
}
handleYearSelection = (year, change) => {
this.setState({ error: '' })
const validYear = isInDateFormat(year, 'YYYY') && isValidYear(year) && this.startBeforeEnd(year, change)
if (validYear) {
this.setState({
validYear,
[change]: reformatDate(year, 'YYYY')
})
} else {
this.setState({ validYear })
}
}
handleStartYearSelection = (year) => {
this.handleYearSelection(year, 'start')
}
handleEndYearSelection = (year) => {
this.handleYearSelection(year, 'end')
}
handleSemesterSeparate = () => {
this.setState({ error: '' })
const bool = this.state.separate
this.setState({ separate: !bool })
}
renderErrorMessage = () => {
const { error } = this.state
if (error) {
return (<Message
error
color="red"
header={error}
/>)
}
return error
}
renderYearSelector = () => {
const { validYear, start, end, isLoading } = this.state
return (
<Form loading={isLoading}>
<Form.Group key="year" className={style.yearSelectorGroup}>
<Form.Field error={!validYear} className={style.yearSelect}>
<label>Start year</label>
<Datetime
className={style.yearSelectInput}
control={Datetime}
dateFormat="YYYY"
timeFormat={false}
closeOnSelect
value={start}
isValidDate={isValidYear}
onChange={this.handleStartYearSelection}
/>
</Form.Field>
<Form.Field className={style.yearControl}>
<Button.Group basic vertical className={style.yearControlButtonGroup}>
<Button
icon="plus"
className={style.yearControlButton}
onClick={this.addYear('start')}
/>
<Button
icon="minus"
className={style.yearControlButton}
onClick={this.subtractYear('start')}
/>
</Button.Group>
</Form.Field>
<Form.Field error={!validYear} className={style.yearSelect}>
<label>End year</label>
<Datetime
className={style.yearSelectInput}
control={Datetime}
dateFormat="YYYY"
timeFormat={false}
closeOnSelect
value={end}
isValidDate={isValidYear}
onChange={this.handleEndYearSelection}
/>
</Form.Field>
<Form.Field className={style.yearControl}>
<Button.Group basic vertical className={style.yearControlButtonGroup}>
<Button
icon="plus"
className={style.yearControlButton}
onClick={this.addYear('end')}
/>
<Button
icon="minus"
className={style.yearControlButton}
onClick={this.subtractYear('end')}
/>
</Button.Group>
</Form.Field>
<Form.Field>
<Checkbox
label="Separate Spring/Fall"
onChange={this.handleSemesterSeparate}
/>
</Form.Field>
</Form.Group>
</Form>
)
}
state={}
render() {
const { data } = this.props.courseStatistics
const { statsIsEmpty } = this.props
return (
<div className={style.container}>
<Header className={sharedStyles.segmentTitle} size="large">
Course Statistics
</Header>
{this.renderYearSelector()}
{this.renderErrorMessage()}
<CourseSearch handleResultSelect={this.handleResultSelect} />
{data.map(course => (<CoursePassRateChart
removeCourseStatistics={this.removeCourseStatistics}
key={course.code}
stats={course}
/>))
}
<Segment className={sharedStyles.contentSegment} >
<SearchForm />
{!statsIsEmpty && <SearchResults />}
</Segment>
</div>
)
}
}
CourseStatistics.propTypes = {
getCourseStatistics: func.isRequired,
removeCourseStatistics: func.isRequired,
courseStatistics: shape({
data: array.isRequired,
selected: array.isRequired
}).isRequired
statsIsEmpty: bool.isRequired
}
const mapStateToProps = ({ courses, courseStatistics }) => ({
courses,
courseStatistics
const mapStateToProps = ({ courseStats }) => ({
statsIsEmpty: Object.keys(courseStats.data).length === 0
})
const mapDispatchToProps = dispatch => ({
getCourseStatistics: query =>
dispatch(getCourseStatistics(query)),
removeCourseStatistics: query =>
dispatch(removeCourseStatistics(query))
})
export default connect(mapStateToProps, mapDispatchToProps)(Timeout(CourseStatistics))
export default connect(mapStateToProps)(CourseStatistics)
@@ -0,0 +1,33 @@
.container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.yearSelectorGroup {
align-items: center !important;
}
.yearSelect {
padding-right: 0 !important;
}
.yearSelectInput input {
border-bottom-right-radius: 0 !important;
border-top-right-radius: 0 !important;
}
.yearControl {
padding-top: 22px !important;
padding-left: 0 !important;
}
.yearControlButtonGroup {
border-bottom-left-radius: 0 !important;
border-top-left-radius: 0 !important;
}
.yearControlButton {
font-size: 8px !important;
}
@@ -0,0 +1,284 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import { Search, Dropdown, Form, Table, Checkbox, Button } from 'semantic-ui-react'
import { connect } from 'react-redux'
import { getActiveLanguage, getTranslate } from 'react-localize-redux'
import Timeout from '../Timeout'
import { getCourseTypes, getCourseDisciplines, getMultipleCourseStatistics } from '../../redux/courseStatistics'
import { findMultipleCourses, emptyCourseSearch } from '../../redux/courses'
import { makeSortCourses } from '../../selectors/courses'
import { reformatDate } from '../../common'
import style from './CourseStatisticsSearch.css'
const { func, string, arrayOf, object, shape } = PropTypes
class CourseStatisticsSearch extends Component {
state = {
isLoading: false,
searchStr: '',
type: { value: 'All', key: null },
discipline: { value: 'All', key: null }
}
componentDidMount() {
this.props.getCourseTypes()
this.props.getCourseDisciplines()
}
resetComponent = () => {
this.setState({
isLoading: false,
searchStr: '',
type: { value: 'All', key: null },
discipline: { value: 'All', key: null }
})
}
handleTypeSelect = (e, { value, options }) => {
const { key } = options.find(opt => opt.text === value)
this.setState({ type: { value, key } }, () => this.fetchCoursesList())
}
handleDisciplineSelect = (e, { value, options }) => {
const { key } = options.find(opt => opt.text === value)
this.setState({ discipline: { value, key } }, () => this.fetchCoursesList())
}
handleSearchChange = (e, { value: searchStr }) => {
const { type, discipline } = this.state
this.props.clearTimeout('search')
this.setState({ searchStr })
if (!type.key && !discipline.key) {
this.props.setTimeout('search', () => {
this.fetchCoursesList()
}, 250)
}
}
fetchCoursesList = () => {
const { type, discipline, searchStr } = this.state
const { language } = this.props
if (searchStr.length >= 3 && !type.key && !discipline.key) {
this.setState({ isLoading: true })
this.props.findMultipleCourses({
searchStr,
type: type.key,
discipline: discipline.key
}, language)
.then(() => this.setState({ isLoading: false }))
} else if (searchStr.length >= 3 || type.key || discipline.key) {
this.setState({ isLoading: true })
this.props.findMultipleCourses({
searchStr: null,
type: type.key,
discipline: discipline.key
}, language)
.then(() => this.setState({ isLoading: false }))
}
}
fetchSelectedCoursesStatistics = () => {
this.props.fetchCourseStatistics()
this.props.emptyCourseSearch()
this.resetComponent()
}
renderCourseTypesDropdown = types => (
<Dropdown
placeholder="All"
search
value={this.state.type.value}
selection
options={types}
loading={types === null}
onChange={this.handleTypeSelect}
closeOnChange
basic
header="Select course type"
/>
)
renderDisciplinesDropdown = disciplines => (
<Dropdown
placeholder="All"
search
value={this.state.discipline.value}
selection
loading={disciplines === null}
options={disciplines}
onChange={this.handleDisciplineSelect}
closeOnChange
basic
header="Select discipline"
/>
)
renderResultTable = () => {
const { searchStr } = this.state
const { courseList } = this.props
const coursesToRender = courseList.filter(c => c.name)
.filter(c => c.name.toLocaleLowerCase()
.includes(searchStr.toLocaleLowerCase()) ||
c.code.toLocaleLowerCase()
.includes(searchStr.toLocaleLowerCase()))
.slice(0, 20)
.map(c =>
({ values: [c.name, c.code, c.date ? reformatDate(c.date, 'DD.MM.YYYY') : null], selected: c.selected }))
const headers = ['Name', 'Code', 'Latest instance held', 'Select']
return (
<Table
style={{
width: '100%',
maxWidth: '850px'
}}
unstackable
selectable
>
<Table.Header>
<Table.Row>
{headers.map(header => (
<Table.HeaderCell key={`header-${header}`}>
{header}
</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{coursesToRender.map(course => (
<Table.Row key={`${course.values[0]}-${course.values[1]}`}>{course.values.map(c =>
<Table.Cell key={c}>{c}</Table.Cell>)
.concat( // eslint-disable-line function-paren-newline
<Table.Cell key={`${course.code}-checkbox`}>
<Checkbox
checked={course.selected}
key={course.code}
value={course.values[1]}
toggle
onChange={this.props.handleResultSelect}
/>
</Table.Cell>)}
</Table.Row>))}
</Table.Body>
</Table>)
}
renderResultDropDown = () => { // unused, may delete
const { courseList } = this.props
const search = (options, query) =>
options.filter(opt => opt.text.includes(query) || opt.value.includes(query))
const coursesToRender = courseList.map(course => ({ key: course.code, value: course.code, text: course.name, description: course.code, associations: 'hello' }))
return (
<Dropdown
style={{
width: '100%',
maxWidth: '850px'
}}
placeholder="Select courses"
fluid
multiple
selection
options={coursesToRender}
search={search}
onSearchChange={this.handleSearchChange}
/>
)
}
render() {
const { isLoading, searchStr } = this.state
const { language, courseList } = this.props
const { courseTypes, courseDisciplines } = this.props.courseStatistics
const text = { en: 'All', fi: 'Kaikki', sv: 'Allt' }
const disciplineOptions = courseDisciplines ?
[
{ text: text[language], value: text[language] },
..._.orderBy(courseDisciplines.map(disc => (
{
text: disc.name.fi ? disc.name.fi : `Name missing (${language})`, // only finnish names available
value: disc.name.fi ? disc.name.fi : `Name missing (${language})`,
key: disc.discipline_id
}
)), 'text', 'asc')] : null
const typeOptions = courseTypes ?
[
{ text: text[language], value: text[language] },
..._.orderBy(courseTypes.map(ty => (
{
text: ty.name[language] ? ty.name[language] : `Name missing (${language})`,
value: ty.name[language] ? ty.name[language] : `Name missing (${language})`,
key: ty.coursetypecode
}
)), 'text', 'asc')] : null
return (
<div>
<Form>
<Form.Group key="optionals" className={style.yearSelectorGroup}>
<Form.Field>
<label>Course discipline (optional)</label>
{this.renderDisciplinesDropdown(disciplineOptions)}
</Form.Field>
<Form.Field>
<label>Course type (optional)</label>
{this.renderCourseTypesDropdown(typeOptions)}
</Form.Field>
</Form.Group>
</Form>
<Search
input={{ fluid: true }}
loading={isLoading}
placeholder="Search by entering a course code or name"
onSearchChange={this.handleSearchChange}
value={searchStr}
showNoResults={false}
/>
<Button disabled={isLoading} style={{ marginTop: '14px' }} fluid onClick={this.fetchSelectedCoursesStatistics} content="Fetch statistics" />
{courseList.length > 0 ? this.renderResultTable() : null}
{/* this.renderResultDropDown() */}
</div>
)
}
}
CourseStatisticsSearch.propTypes = {
language: string.isRequired,
courseList: arrayOf(object).isRequired,
setTimeout: func.isRequired,
clearTimeout: func.isRequired,
handleResultSelect: func.isRequired,
fetchCourseStatistics: func.isRequired,
courseStatistics: shape({
courseTypes: arrayOf(shape({})),
courseDisciplines: arrayOf(shape({}))
}).isRequired,
getCourseTypes: func.isRequired,
getCourseDisciplines: func.isRequired,
findMultipleCourses: func.isRequired,
emptyCourseSearch: func.isRequired
}
const sortCourses = makeSortCourses()
const mapStateToProps = ({ locale, courses, settings, courseStatistics }) => ({
language: settings.language,
courseList: sortCourses(courses),
translate: getTranslate(locale),
currentLanguage: getActiveLanguage(locale).value,
courseStatistics
})
export default connect(mapStateToProps, {
findMultipleCourses,
getMultipleCourseStatistics,
getCourseDisciplines,
getCourseTypes,
emptyCourseSearch
})(Timeout(CourseStatisticsSearch))
@@ -0,0 +1,99 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Table } from 'semantic-ui-react'
const { shape, arrayOf } = PropTypes
const CourseStatisticsTable = ({ stats }) =>
((stats.some(year => year.c_passed > 0) || stats.some(year => year.c_failed > 0)) ?
(
<Table celled padded>
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan={4}>Programme</Table.HeaderCell>
<Table.HeaderCell colSpan={3}>Compared</Table.HeaderCell>
</Table.Row>
<Table.Row>
<Table.HeaderCell>Time</Table.HeaderCell>
<Table.HeaderCell>Passed Students</Table.HeaderCell>
<Table.HeaderCell>Failed Students</Table.HeaderCell>
<Table.HeaderCell>Pass rate</Table.HeaderCell>
<Table.HeaderCell>Passed Students</Table.HeaderCell>
<Table.HeaderCell>Failed Students</Table.HeaderCell>
<Table.HeaderCell>Pass rate</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{stats.map(year => (
<Table.Row key={year.time}>
<Table.Cell>
{year.time}
</Table.Cell>
<Table.Cell>
{year.passed}
</Table.Cell>
<Table.Cell>
{year.failed}
</Table.Cell>
<Table.Cell>
{`${year.passed + year.failed > 0 ? Math.round((year.passed /
(year.passed + year.failed)) * 100) : 0} %`}
</Table.Cell>
<Table.Cell>
{year.c_passed}
</Table.Cell>
<Table.Cell>
{year.c_failed}
</Table.Cell>
<Table.Cell>
{`${year.c_passed + year.c_failed > 0 ? Math.round((year.c_passed /
(year.c_passed + year.c_failed)) * 100) : 0} %`}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
)
:
(
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Time</Table.HeaderCell>
<Table.HeaderCell>Passed Students</Table.HeaderCell>
<Table.HeaderCell>Failed Students</Table.HeaderCell>
<Table.HeaderCell>Pass rate</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{stats.map(year => (
<Table.Row key={year.time}>
<Table.Cell>
{year.time}
</Table.Cell>
<Table.Cell>
{year.passed}
</Table.Cell>
<Table.Cell>
{year.failed}
</Table.Cell>
<Table.Cell>
{`${year.passed + year.failed > 0 ? Math.round((year.passed /
(year.passed + year.failed)) * 100) : 0} %`}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
))
CourseStatisticsTable.propTypes = {
stats: arrayOf(shape({})).isRequired
}
export default CourseStatisticsTable
@@ -8,7 +8,7 @@ import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
import { clearLoading } from '../../redux/graphSpinner'
import { DISPLAY_DATE_FORMAT, CHART_COLORS, API_DATE_FORMAT } from '../../constants'
import { DISPLAY_DATE_FORMAT, CHART_COLORS } from '../../constants'
import { reformatDate, sortDatesWithFormat } from '../../common'
import { turquoise } from '../../styles/variables/colors'
@@ -80,7 +80,7 @@ class CreditAccumulationGraph extends Component {
}
getXAxisMonth = (date, startDate) =>
Math.max(moment(date, API_DATE_FORMAT).diff(moment(startDate, API_DATE_FORMAT), 'days') / 30, 0)
Math.max(moment.utc(date).diff(moment.utc(startDate), 'days') / 30, 0)
getReferenceLineForStudent = (student) => {
try {
@@ -119,6 +119,7 @@ class CreditAccumulationGraph extends Component {
getStudentCourseData = (student) => {
const { studentNumber, courses, studyrightStart, started } = student
const { language } = this.props
const startDate = this.props.selectedStudents.length === 1 ? started : studyrightStart
@@ -128,19 +129,20 @@ class CreditAccumulationGraph extends Component {
let totalCredits = 0
return filteredCourses.map((c) => {
const {
course, date, credits, grade, passed
course, date, credits, grade, passed, isStudyModuleCredit
} = c
if (passed) {
if (passed && !isStudyModuleCredit) {
totalCredits += credits
}
return {
title: `${course.name} (${course.code})`,
title: `${course.name[language]} (${course.code})`,
[studentNumber]: totalCredits,
credits,
date: reformatDate(date, DISPLAY_DATE_FORMAT),
month: this.getXAxisMonth(date, startDate),
grade,
passed
passed,
isStudyModuleCredit
}
})
}
@@ -236,19 +238,25 @@ class CreditAccumulationGraph extends Component {
return null
}
}
xAxisFormatter = (startDate, month) => (moment(startDate).add(month, 'months').format('MMM YY'))
render() {
const { students, translate, maxCredits } = this.props
const { combinedStudentData } = this.state
const isSingleStudent = this.isSingleStudentGraph(students)
if (!isSingleStudent) {
return null
}
const minTick = combinedStudentData && combinedStudentData.length > 0 ?
combinedStudentData[0].month : 0
const maxTick = combinedStudentData && combinedStudentData.length > 0 ?
Math.ceil(combinedStudentData[combinedStudentData.length - 1].month) : 8
const referenceLine = isSingleStudent && this.getReferenceLine(translate('graphs.referenceCredits'))
const toolTip = isSingleStudent && this.getTooltip(this.props)
let firstDate = moment()
if (isSingleStudent) {
firstDate = moment(students[0].started)
}
return (
<div className={styles.graphContainer}>
<Segment attached="bottom">
@@ -262,10 +270,12 @@ class CreditAccumulationGraph extends Component {
domain={[minTick, maxTick]}
tick={{ fontSize: '15' }}
tickCount={20}
tickFormatter={tick => this.xAxisFormatter(firstDate, tick)}
/>
<YAxis
type="number"
domain={[0, maxCredits]}
tickFormatter={tick => Math.round(tick)}
/>
<CartesianGrid strokeDasharray="3 3" />
I {toolTip}
@@ -280,6 +290,7 @@ class CreditAccumulationGraph extends Component {
}
CreditAccumulationGraph.propTypes = {
language: string.isRequired,
translate: func.isRequired,
students: arrayOf(object).isRequired,
title: string.isRequired,
@@ -291,6 +302,7 @@ CreditAccumulationGraph.propTypes = {
}
const mapStateToProps = state => ({
language: state.settings.language,
spinner: state.graphSpinner
})
@@ -10,3 +10,7 @@
cursor: pointer;
}
.graphOptions {
float: right;
}
@@ -1,35 +1,38 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import moment from 'moment'
import { arrayOf, object, string } from 'prop-types'
import { arrayOf, object, string, func, number } from 'prop-types'
import { withRouter } from 'react-router-dom'
import Highcharts from 'highcharts/highstock'
import { Button } from 'semantic-ui-react'
import boost from 'highcharts/modules/boost'
import ReactHighstock from 'react-highcharts/ReactHighstock'
import styles from './creditAccumulationGraphHC.css'
import { clearLoading } from '../../redux/graphSpinner'
import { setChartHeight } from '../../redux/settings'
import { reformatDate, sortDatesWithFormat } from '../../common'
import { DISPLAY_DATE_FORMAT, API_DATE_FORMAT } from '../../constants'
boost(Highcharts)
class CreditAccumulationGraphHighCharts extends Component {
state = {
studentCreditLines: [],
timeout: undefined,
options: [],
initialLoad: true
constructor(props) {
super(props)
this.state = {
studentCreditLines: [],
options: []
}
}
componentDidMount() {
async componentDidMount() {
const { students } = this.props
const timeout = setTimeout(() => this.getMoreCreditLines(students), 1000)
await this.getMoreCreditLines(students)
const self = this
const dataOfSelected = this.createStudentCreditLines(students).filter(line =>
this.props.selectedStudents.includes(line.name))
const options = {
chart: {
height: 1000
height: this.props.currentGraphSize
},
plotOptions: {
series: {
@@ -62,49 +65,50 @@ class CreditAccumulationGraphHighCharts extends Component {
}
}
},
series: dataOfSelected
series: dataOfSelected,
xAxis: {
ordinal: false,
max: students.maxDate,
min: students.minDate
}
}
this.setState({ timeout, options })
this.setState({ options })
}
componentWillReceiveProps(nextProps) {
async componentWillReceiveProps(nextProps) {
if (nextProps.students) {
const nextStudents = nextProps.students.map(student => student.studentNumber)
const oldStudents = this.props.students.map(student => student.studentNumber)
const changed =
nextStudents.some(student => !oldStudents.includes(student)) ||
oldStudents.some(student => !nextStudents.includes(student))
const dataOfSelected = this.state.studentCreditLines.filter(line =>
nextProps.selectedStudents.includes(line.name))
const options = { ...this.state.options, yAxis: { max: nextProps.students.maxCredits, title: { text: 'Credits' } }, series: dataOfSelected }
const options = {
...this.state.options,
yAxis: {
max: nextProps.students.maxCredits,
title: { text: 'Credits' }
},
xAxis: {
max: nextProps.students.maxDate,
min: nextProps.students.minDate
},
series: dataOfSelected,
chart: {
height: nextProps.currentGraphSize
}
}
this.setState({ options })
if (changed) {
const { students } = nextProps
const timeout = setTimeout(() => this.getMoreCreditLines(students), 1000)
this.setState({ timeout })
await this.getMoreCreditLines(students)
}
}
}
componentDidUpdate() {
if (this.state.initialLoad) {
if (
this.props.selectedStudents &&
this.state.studentCreditLines.length === this.props.selectedStudents.length
) {
this.setState({ initialLoad: false }) // eslint-disable-line
}
}
}
componentWillUnmount() {
if (this.state.timeout) {
clearInterval(this.state.timeout)
}
}
getXAxisMonth = (date, startDate) =>
Math.max(moment(date, API_DATE_FORMAT).diff(moment(startDate, API_DATE_FORMAT), 'days') / 30, 0)
@@ -146,7 +150,7 @@ class CreditAccumulationGraphHighCharts extends Component {
})
}
getMoreCreditLines = (students) => {
getMoreCreditLines = async (students) => {
const studentCreditLines = this.state.studentCreditLines
.concat(this.createStudentCreditLines(students))
this.setState({ studentCreditLines })
@@ -166,38 +170,66 @@ class CreditAccumulationGraphHighCharts extends Component {
students.map((student) => {
let credits = 0
const points = student.courses.map((course) => {
if (course.grade !== 'Hyl.') {
if (!['Luop', 'Hyl.', 'Eisa', '0', 'Fail'].includes(course.grade) && !course.isStudyModuleCredit) {
credits += course.credits
}
return [new Date(course.date).getTime(), credits]
})
return { name: student.studentNumber, data: points, boostThreshold: 500 }
return { name: student.studentNumber, data: points, seriesThreshold: 150 }
})
resizeChart = (size) => {
if (this.chart) {
this.props.setChartHeight(size)
}
}
render() {
return (
<div className={styles.graphContainer}>
<ReactHighstock
highcharts={Highcharts}
ref={(HighchartsReact) => {
this.chart = HighchartsReact
}}
constructorType="stockChart"
config={this.state.options}
/>
<div>
<div className={styles.graphContainer}>
<div className={styles.graphOptions}>
<Button
active={this.props.currentGraphSize === 400}
onClick={() => this.resizeChart(400)}
content="Small"
/>
<Button
active={this.props.currentGraphSize === 600}
onClick={() => this.resizeChart(600)}
content="Medium"
/>
<Button
active={this.props.currentGraphSize === 1000}
onClick={() => this.resizeChart(1000)}
content="Large"
/>
</div>
<ReactHighstock
highcharts={Highcharts}
ref={(HighchartsReact) => {
this.chart = HighchartsReact
}}
constructorType="stockChart"
config={this.state.options}
/>
</div>
</div>
)
}
}
CreditAccumulationGraphHighCharts.propTypes = {
students: arrayOf(object).isRequired,
selectedStudents: arrayOf(string).isRequired
selectedStudents: arrayOf(string).isRequired,
setChartHeight: func.isRequired,
currentGraphSize: number.isRequired
}
const mapStateToProps = state => ({
spinner: state.graphSpinner
spinner: state.graphSpinner,
currentGraphSize: state.settings.chartHeight
})
export default connect(
mapStateToProps,
{ clearLoading }
{ clearLoading, setChartHeight }
)(withRouter(CreditAccumulationGraphHighCharts))
+19 -13
View File
@@ -4,10 +4,15 @@ import { func, bool, arrayOf, object } from 'prop-types'
import styles from './creditGraphTooltip.css'
const getCardHeader = title => (
<Card.Header className={styles.tooltipHeader}>
{title}
</Card.Header>
const getCardHeader = (title, isStudyModuleCredit) => (
isStudyModuleCredit ?
<Card.Header className={styles.tooltipHeader}>
{`${title} [Study Module]`}
</Card.Header>
:
<Card.Header className={styles.tooltipHeader}>
{title}
</Card.Header>
)
const getCardMeta = (name, date) => (
@@ -26,7 +31,7 @@ const getCardMeta = (name, date) => (
</Card.Meta>
)
const getCardDescription = (translate, credits, grade, passed) => (
const getCardDescription = (translate, credits, grade, passed, isStudyModuleCredit) => (
<Card.Description className={styles.tooltipBody}>
<div className={styles.tooltipBodyItem}>
<div className={styles.tooltipBodyTitle}>{translate('common.credits')}</div>
@@ -38,12 +43,14 @@ const getCardDescription = (translate, credits, grade, passed) => (
<div className={styles.tooltipBodyValue}>{grade}</div>
</div>
<div className={styles.tooltipBodyItem}>
<div className={styles.tooltipBodyTitle}>{translate('common.passed')}</div>
<div className={styles.tooltipBodyTitle}>{isStudyModuleCredit ? 'module' : translate('common.passed')}</div>
<div className={styles.tooltipBodyValue}>
{
passed
? (<Icon name="check circle outline" color="green" />)
: (<Icon name="remove circle outline" color="red" />)
isStudyModuleCredit // eslint-disable-line
? (<Icon name="certificate" color="purple" />)
: passed
? (<Icon name="check circle outline" color="green" />)
: (<Icon name="remove circle outline" color="red" />)
}
</div>
</div>
@@ -56,15 +63,14 @@ const CreditGraphTooltip = (props) => {
const { payload } = props
const { name } = payload[0]
const {
title, credits, date, grade, passed
title, credits, date, grade, passed, isStudyModuleCredit
} = payload[0].payload
return (
<Card>
<Card.Content >
{getCardHeader(title)}
{getCardHeader(title, isStudyModuleCredit)}
{getCardMeta(name, date)}
{getCardDescription(translate, credits, grade, passed)}
{getCardDescription(translate, credits, grade, passed, isStudyModuleCredit)}
</Card.Content>
</Card>
)
+135 -83
View File
@@ -1,131 +1,183 @@
import React, { Component } from 'react'
import { Button, Dropdown, Grid, List } from 'semantic-ui-react'
import { withRouter, Redirect } from 'react-router-dom'
import { Button, Icon, Card, Header, Segment, Confirm } from 'semantic-ui-react'
import { connect } from 'react-redux'
import { func, shape, string, number, bool, arrayOf } from 'prop-types'
import { func, shape, string, bool, arrayOf } from 'prop-types'
import { getTranslate, getActiveLanguage } from 'react-localize-redux'
import styles from './enableUsers.css'
import { getUsers, enableUser, addUserUnit, removeUserUnit } from '../../redux/users'
import { getUsers, enableUser, addUserUnit, removeUserUnit, sendEmail } from '../../redux/users'
import { getUnits } from '../../redux/units'
import { makeSortUsers } from '../../selectors/users'
import { copyToClipboard } from '../../common'
import sharedStyles from '../../styles/shared'
import UserPageNew from '../UserPage'
class EnableUsers extends Component {
state = {
confirm: false,
email: ''
}
componentDidMount() {
this.props.getUsers()
this.props.getUnits()
}
enableUser = id => () => this.props.enableUser(id)
handleChange = user => (e, { value }) => {
if (!user.units.find(unit => unit.id === value)) {
this.props.addUserUnit(user.id, value)
enableUser = user => () => {
if (!user.is_enabled) {
this.setState({ confirm: true, email: user.email })
}
this.props.enableUser(user.id)
}
removeAccess = (uid, unit) => () => this.props.removeUserUnit(uid, unit)
openEditUserPage = userid => () => {
const { history } = this.props
history.push(`users/${userid}`)
}
renderUnitList = (units, user) => {
if (!units) return null
return (
<List divided verticalAlign="middle">
{units.map(unit => (
<List.Item key={unit.id}>
<List.Content floated="right">
<Button onClick={this.removeAccess(user, unit.id)}>Remove</Button>
</List.Content>
<List.Content>
{unit.name}
</List.Content>
</List.Item>
openUsersPage = () => {
const { history } = this.props
history.push('/users')
}
copyEmailsToClippoard = () => {
const clipboardString = this.props.users
.filter(u => u.is_enabled && u.email)
.map(u => u.email).join('; ')
copyToClipboard(clipboardString)
}
renderUserPage = (userid) => {
const { users } = this.props
const user = users.find(u => u.id === userid)
return !user
? <Redirect to="/users" />
: (
<UserPageNew
userid={userid}
user={user}
goBack={this.openUsersPage}
/>
)
}
renderUserSearchList = () => {
const { users, error } = this.props
return error ? null : (
<Card.Group itemsPerRow={4}>
{this.state.confirm ? <Confirm
style={{
marginTop: 'auto !important',
display: 'inline-block !important',
position: 'relative',
top: '20%',
left: '33%'
}}
open={this.state.confirm}
cancelButton="no"
confirmButton="send"
content="Do you want to notify this person by email?"
onCancel={() => { this.setState({ confirm: false }) }}
onConfirm={() => {
this.setState({ confirm: false })
this.props.sendEmail(this.state.email)
}}
size="small"
/> : null}
{users.map(user => (
<Card raised key={user.id} color={user.is_enabled ? 'green' : 'red'}>
<Card.Content>
<Card.Header content={user.full_name} />
<Card.Meta content={user.username} />
<Card.Meta content={user.email} />
<Card.Description>
{`Access to oodikone: ${user.is_enabled ? 'En' : 'Dis'}abled`}
</Card.Description>
</Card.Content>
<Card.Content extra>
<Button.Group compact widths={8}>
<Button animated basic size="mini" disabled={!user.is_enabled} onClick={this.openEditUserPage(user.id)}>
<Button.Content hidden>Edit</Button.Content>
<Button.Content visible>
<Icon name="wrench" />
</Button.Content>
</Button>
<Button animated basic onClick={this.enableUser(user)} size="mini">
<Button.Content hidden>{user.is_enabled ? 'Disable' : 'Enable'}</Button.Content>
<Button.Content visible>
<Icon color={user.is_enabled ? 'green' : 'red'} name={user.is_enabled ? 'check' : 'remove'} />
</Button.Content>
</Button>
</Button.Group>
</Card.Content>
</Card>
))}
</List>
</Card.Group>
)
}
render() {
const { users, error, units } = this.props
const unitOptions = units.map(unit => ({ key: unit.id, value: unit.id, text: unit.name }))
return error ? null : (
<div styles={styles.container}>
<h1>Enable or disable access to OodiKone</h1>
{users.map(u => (
<Grid key={u.id} divided="vertically">
<Grid.Row columns={2}>
<Grid.Column>
<h3>{u.full_name}, {u.username} - Access to oodikone: {u.is_enabled ? 'En' : 'Dis'}abled</h3>
<Dropdown
placeholder="Select unit"
options={unitOptions}
onChange={this.handleChange(u)}
fluid
search
selection
/>
</Grid.Column>
<Grid.Column>
<Button onClick={this.enableUser(u.id)}>Enable/Disable</Button>
{this.renderUnitList(u.units, u.id)}
</Grid.Column>
</Grid.Row>
</Grid>
))}
const { match } = this.props
const { userid } = match.params
return (
<div className={sharedStyles.segmentContainer}>
<Header className={sharedStyles.segmentTitle} size="large">
Enable or disable access to Oodikone
</Header>
<Segment className={sharedStyles.contentSegment}>
{!userid ? this.renderUserSearchList() : this.renderUserPage(userid)}
</Segment>
<Icon
link
name="envelope"
onClick={this.copyEmailsToClippoard}
style={{ float: 'right' }}
/>
</div>
)
}
}
EnableUsers.propTypes = {
match: shape({
params: shape({
studentNumber: string
})
}).isRequired,
getUsers: func.isRequired,
enableUser: func.isRequired,
addUserUnit: func.isRequired,
removeUserUnit: func.isRequired,
getUnits: func.isRequired,
units: arrayOf(shape({
id: number,
name: string
})).isRequired,
sendEmail: func.isRequired,
users: arrayOf(shape({
id: number,
id: string,
full_name: string,
is_enabled: bool,
username: string,
units: arrayOf(shape({
id: number,
name: string
id: string,
name: shape({}).isRequired
}))
})).isRequired,
error: bool.isRequired
error: bool.isRequired,
history: shape({}).isRequired
}
const sortUsers = makeSortUsers()
const mapStateToProps = ({ locale, users, units }) => ({
const mapStateToProps = ({ locale, users, units, settings }) => ({
language: settings.language,
translate: getTranslate(locale),
currentLanguage: getActiveLanguage(locale).value,
units: units.data,
users: sortUsers(users),
pending: users.pending,
pending: (typeof (users.pending) === 'boolean') ? users.pending : true,
error: users.error || false
})
const mapDispatchToProps = dispatch => ({
getUsers: () => {
dispatch(getUsers())
},
enableUser: (id) => {
dispatch(enableUser(id))
},
addUserUnit: (uid, unit) => {
dispatch(addUserUnit(uid, unit))
},
removeUserUnit: (uid, unit) => {
dispatch(removeUserUnit(uid, unit))
},
getUnits: () => {
dispatch(getUnits())
}
})
export default connect(mapStateToProps, mapDispatchToProps)(EnableUsers)
export default withRouter(connect(mapStateToProps, {
getUsers,
enableUser,
addUserUnit,
removeUserUnit,
getUnits,
sendEmail
})(EnableUsers))
+61
View File
@@ -0,0 +1,61 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Dropdown } from 'semantic-ui-react'
import PropTypes from 'prop-types'
import { getUserName, setUserLanguage, getUserLanguage } from '../../common'
import { switchLanguage, initLanguage } from '../../redux/settings'
const { func, shape, string } = PropTypes
const languages = [
{ key: 'fi', text: 'fi', value: 'fi' },
{ key: 'en', text: 'en', value: 'en' },
{ key: 'sv', text: 'sv', value: 'sv' }
]
class LanguageChooser extends Component {
async componentDidMount() {
const initialLanguage = await getUserLanguage()
this.props.initLanguage(initialLanguage)
}
change = () => async (e, { value }) => {
const name = await getUserName()
this.props.switchLanguage(name, value)
await setUserLanguage(value)
}
render() {
return (
<Dropdown
size="small"
className="icon"
floating
icon="world"
onChange={this.change()}
text={this.props.settings.language}
options={languages}
button
/>
)
}
}
const mapStateToProps = ({ settings }) => ({
settings
})
const mapDispatchToProps = dispatch => ({
switchLanguage: (username, language) =>
dispatch(switchLanguage(username, language)),
initLanguage: language =>
dispatch(initLanguage(language))
})
LanguageChooser.propTypes = {
settings: shape({ language: string.isRequired }).isRequired,
switchLanguage: func.isRequired,
initLanguage: func.isRequired
}
export default connect(mapStateToProps, mapDispatchToProps)(LanguageChooser)
+25 -8
View File
@@ -1,34 +1,41 @@
import React, { Component } from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import { localize } from 'react-localize-redux'
import { Loader } from 'semantic-ui-react'
import { Loader, Image, Transition } from 'semantic-ui-react'
import Header from '../Header'
import Populations from '../PopulationStatistics'
import DepartmentSuccess from '../DepartmentSuccess'
import WelcomePage from '../WelcomePage'
import StudentStatistics from '../StudentStatistics'
import CourseInstances from '../CourseInstances'
import CourseStatistics from '../CourseStatistics'
import EnableUsers from '../EnableUsers'
import Settings from '../Settings'
import ErrorContainer from '../ErrorContainer'
import { routes, BASE_PATH } from '../../constants'
import AccessDenied from '../AccessDenied'
import UsageStatistics from '../UsageStatistics'
import Teachers from '../Teachers'
import styles from './main.css'
import { userIsEnabled, log } from '../../common'
import { userIsEnabled, log, images } from '../../common'
class Main extends Component {
state = {
enabled: false,
hasError: false,
loaded: false
loaded: false,
easterEgg: false
}
async componentDidMount() {
const enabled = await userIsEnabled()
if (!enabled) {
log('Not enabled')
setTimeout(
() => this.setState({ easterEgg: true }),
Math.floor(Math.random() * 1800000) + 600000
)
}
this.setState({ enabled, loaded: true })
}
@@ -38,11 +45,19 @@ class Main extends Component {
}
render() {
console.log('Wed May 30 19:55:39 EEST 2018') // eslint-disable-line
if (!this.state.loaded) {
return <Loader active inline="centered" />
}
if (!this.state.enabled || this.state.hasError) {
return <AccessDenied itWasError={this.state.hasError} />
return (
<div>
<AccessDenied itWasError={this.state.hasError} />
<Transition visible={this.state.easterEgg} animation="fly up" duration={10000}>
<Image src={images.irtomikko} size="huge" verticalAlign="top" inline style={{ position: 'absolute', top: '350px', right: '10px' }} />
</Transition>
</div>
)
}
return (
<div className={styles.appContainer}>
@@ -51,12 +66,14 @@ class Main extends Component {
<Header />
<ErrorContainer />
<Switch>
<Route exact path={routes.index.route} component={DepartmentSuccess} />
<Route exact path={routes.index.route} component={WelcomePage} />
<Route exact path={routes.populations.route} component={Populations} />
<Route exact path={routes.students.route} component={StudentStatistics} />
<Route exact path={routes.courseInstances.route} component={CourseInstances} />
<Route exact path={routes.courseStatistics.route} component={CourseStatistics} />
<Route exact path={routes.settings.route} component={Settings} />
<Route exact path={routes.users.route} component={EnableUsers} />
<Route exact path={routes.teachers.route} component={Teachers} />
<Route exact path={routes.usage.route} component={UsageStatistics} />
</Switch>
</main>
</Router>
+11 -4
View File
@@ -5,7 +5,7 @@ import { func } from 'prop-types'
import { connect } from 'react-redux'
import { routes } from '../../constants'
import { userIsAdmin } from '../../common'
import { userIsAdmin, userIsCzar } from '../../common'
import styles from './navigationBar.css'
import { logout, swapDevUser } from '../../apiConnection'
@@ -18,9 +18,12 @@ class NavigationBar extends Component {
async componentDidMount() {
const navigationRoutes = { ...routes }
const adminRights = await userIsAdmin()
const czarRights = await userIsCzar()
Object.keys(navigationRoutes).forEach((key) => {
if (navigationRoutes[key].admin && !adminRights) {
delete navigationRoutes[key]
} else if (navigationRoutes[key].czar && (!adminRights && !czarRights)) {
delete navigationRoutes[key]
}
})
this.setState({ navigationRoutes })
@@ -39,7 +42,7 @@ class NavigationBar extends Component {
if (process.env.NODE_ENV === 'development') {
const testUsers = ['tktl']
return (
<Menu.Item as={Dropdown} style={{ backgroundColor: 'purple', color: 'white' }} text="Dev controls">
<Menu.Item as={Dropdown} style={{ backgroundColor: 'purple', color: 'white' }} text="Dev controls" tabIndex="-1">
<Dropdown.Menu>
{testUsers.map(user => (
<Dropdown.Item
@@ -60,7 +63,7 @@ class NavigationBar extends Component {
}
return (
<Menu.Item link onClick={logout} icon="log out">
<Menu.Item link onClick={logout} icon="log out" tabIndex="-1">
{translate('navigationBar.logout')}
</Menu.Item>
)
@@ -70,12 +73,12 @@ class NavigationBar extends Component {
const t = this.props.translate
const { navigationRoutes } = this.state
const menuWidth = Object.keys(navigationRoutes).length + 2
return (
<Menu stackable fluid widths={menuWidth} className={styles.navBar}>
<Menu.Item
as={Link}
to={navigationRoutes.index.route}
tabIndex="-1"
>
<span className={styles.logo}>
<h2 className={styles.logoText}>oodikone</h2>
@@ -84,6 +87,9 @@ class NavigationBar extends Component {
{
Object.values(navigationRoutes).map((value) => {
const viewableRoute = this.checkForOptionalParams(value.route)
if (value.route === '/') {
return null
}
return (
<Menu.Item
exact={viewableRoute === value.route}
@@ -91,6 +97,7 @@ class NavigationBar extends Component {
as={NavLink}
key={`menu-item-${viewableRoute}`}
to={this.checkForOptionalParams(viewableRoute)}
tabIndex="-1"
>
{t(`navigationBar.${value.translateId}`)}
</Menu.Item>
+361 -138
View File
@@ -1,42 +1,49 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Table, Form, Input } from 'semantic-ui-react'
import { func, arrayOf, object, number } from 'prop-types'
import { Table, Form, Input, Popup, Button, Icon } from 'semantic-ui-react'
import { func, arrayOf, object, number, shape, string } from 'prop-types'
import { getTranslate } from 'react-localize-redux'
import { setPopulationLimit, clearPopulationLimit } from '../../redux/populationLimit'
import { setLoading } from '../../redux/graphSpinner'
import _ from 'lodash'
import { withRouter } from 'react-router-dom'
import moment from 'moment'
import { setPopulationFilter, removePopulationFilterOfCourse } from '../../redux/populationFilters'
import { getMultipleCourseStatistics } from '../../redux/courseStatistics'
import { courseParticipation } from '../../populationFilters'
const formatGradeDistribution = grades => _.replace(JSON.stringify(_.sortBy(Object.entries(grades).map(([key, value]) => ({ [key]: value.count })), o => -Object.keys(o)), null, 1), /\[\n|{\n*|{\s|}|\s*}|]|"|,/g, '')
class PopulationCourseStats extends Component {
static propTypes = {
courses: arrayOf(object).isRequired,
courses: shape({
coursestatistics: arrayOf(object).isRequired,
coursetypes: shape({}).isRequired,
disciplines: shape({}).isRequired
}).isRequired,
translate: func.isRequired,
setPopulationLimit: func.isRequired,
setLoading: func.isRequired,
setPopulationFilter: func.isRequired,
populationSize: number.isRequired,
selected: object // eslint-disable-line
selectedCourses: arrayOf(object).isRequired,
removePopulationFilterOfCourse: func.isRequired,
history: shape({}).isRequired,
getMultipleCourseStatistics: func.isRequired,
language: string.isRequired,
query: shape({}).isRequired
}
state = {
sortBy: 'students',
reversed: false,
limit: parseInt(this.props.populationSize * 0.15, 10)
}
constructor(props) {
super(props)
limitPopulationToCourse = course => () => {
this.props.setLoading()
this.props.setPopulationLimit(course, 'all')
}
sortBy(criteria) {
return () => {
let { reversed } = this.state
if (this.state.sortBy === 'percentage' && criteria === 'percentage') {
reversed = !reversed
}
this.setState({ sortBy: criteria, reversed })
this.state = {
sortBy: 'students',
reversed: false,
limit: parseInt(this.props.populationSize * 0.15, 10),
showGradeDistribution: false
}
}
getSelectionStyle = criteria => (this.state.sortBy === criteria ? ({ background: 'darkgray' }) : ({ background: '#f9fafb' }))
criteria = () => (c1, c2) => {
const orderByCode = (code1, code2) =>
(code1.course.code < code2.course.code ? -1 : 1)
@@ -56,16 +63,316 @@ class PopulationCourseStats extends Component {
return course.stats.students >= this.state.limit
}
sortBy(criteria) {
return () => {
let { reversed } = this.state
if (this.state.sortBy === criteria) {
reversed = !reversed
}
this.setState({ sortBy: criteria, reversed })
}
}
limitPopulationToCourse = course => () => {
if (!this.active(course.course)) {
const params = { course, field: 'all' }
this.props.setPopulationFilter(courseParticipation(params))
} else {
this.props.removePopulationFilterOfCourse(course.course)
}
}
active = course =>
this.props.selectedCourses
.find(c => course.name === c.name && course.code === c.code) !== undefined
renderSortArrow = (criteria) => {
if (this.state.sortBy === criteria) {
if (this.state.reversed) {
return (<Icon name="angle up" />)
}
return (<Icon name="angle down" />)
}
return null
}
renderGradeDistributionTable = ({ translate, sortBy, courses, language }) => (
<Table celled sortable>
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan="2" >
{translate('populationCourses.course')}
</Table.HeaderCell>
<Table.HeaderCell>{translate('populationCourses.code')}</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'students' ? 'descending' : null}
onClick={this.sortBy('students')}
style={this.getSelectionStyle('students')}
>
Attempts
</Table.HeaderCell>
<Table.HeaderCell>
0
</Table.HeaderCell>
<Table.HeaderCell>
1
</Table.HeaderCell>
<Table.HeaderCell>
2
</Table.HeaderCell>
<Table.HeaderCell>
3
</Table.HeaderCell>
<Table.HeaderCell>
4
</Table.HeaderCell>
<Table.HeaderCell>
5
</Table.HeaderCell>
<Table.HeaderCell>
Other passed
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{courses.coursestatistics.sort(this.criteria()).filter(this.limit()).map(course => (
<Popup
key={course.course.code}
trigger={
<Table.Row active={this.active(course.course)}>
<Table.Cell onClick={this.limitPopulationToCourse(course)}>
{course.course.name[language]}
</Table.Cell>
<Table.Cell
icon="level up alternate"
onClick={() => {
this.props.history.push('/coursestatistics/')
this.props.getMultipleCourseStatistics({
codes: [course.course.code],
start: Number(this.props.query.year),
end: Number(moment(moment(this.props.query.year, 'YYYY').add(this.props.query.months, 'months')).format('YYYY')),
separate: false,
language: this.props.language
})
}}
style={{ borderLeft: '0px !important' }}
/>
<Table.Cell>{course.course.code}</Table.Cell>
<Table.Cell>
{course.grades ? _.sum(Object.values(course.grades).map(g => g.count)) || 0 : 0}
</Table.Cell>
<Table.Cell>
{course.grades ?
_.sum(Object.values(course.grades).filter(g =>
g.status.failingGrade).map(g => g.count))
|| 0 : 0}
</Table.Cell>
<Table.Cell>
{course.grades[1] ? course.grades[1].count || 0 : 0}
</Table.Cell>
<Table.Cell>
{course.grades[2] ? course.grades[2].count || 0 : 0}
</Table.Cell>
<Table.Cell>
{course.grades[3] ? course.grades[3].count || 0 : 0}
</Table.Cell>
<Table.Cell>
{course.grades[4] ? course.grades[4].count || 0 : 0}
</Table.Cell>
<Table.Cell>
{course.grades[5] ? course.grades[5].count || 0 : 0}
</Table.Cell>
<Table.Cell>
{course.grades ?
_.sum(Object.values(_.omit(course.grades, [1, 2, 3, 4, 5])).filter(g =>
g.status.passingGrade || g.status.improvedGrade).map(g => g.count)) : 0}
</Table.Cell>
</Table.Row>}
flowing
hoverable
inverted
position="top right"
hideOnScroll
content={course.grades ? <pre>{formatGradeDistribution(course.grades)}</pre> : 'Nothing to see here'}
/>
))}
</Table.Body>
</Table>
)
renderBasicTable = ({ translate, sortBy, direction, courses, language }) => (
<Table celled sortable>
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan="2">
{translate('populationCourses.course')}
</Table.HeaderCell>
<Table.HeaderCell
rowSpan="2"
sorted={sortBy === 'students' ? 'descending' : null}
onClick={this.sortBy('students')}
style={this.getSelectionStyle('students')}
>
{translate('populationCourses.students')}
{this.renderSortArrow('students')}
</Table.HeaderCell>
<Table.HeaderCell
colSpan="3"
>
{translate('populationCourses.passed')}
</Table.HeaderCell>
<Table.HeaderCell
colSpan="2"
>
{translate('populationCourses.failed')}
</Table.HeaderCell>
<Table.HeaderCell colSpan="2">{translate('populationCourses.attempts')}</Table.HeaderCell>
<Table.HeaderCell colSpan="2">{translate('populationCourses.percentageOfPopulation')}</Table.HeaderCell>
</Table.Row>
<Table.Row>
<Table.HeaderCell colSpan="1" >{translate('populationCourses.name')}</Table.HeaderCell>
<Table.HeaderCell>{translate('populationCourses.code')}</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'passed' ? 'descending' : null}
onClick={this.sortBy('passed')}
style={this.getSelectionStyle('passed')}
>
{translate('populationCourses.number')}
{this.renderSortArrow('passed')}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'retryPassed' ? 'descending' : null}
onClick={this.sortBy('retryPassed')}
style={this.getSelectionStyle('retryPassed')}
>
{translate('populationCourses.passedAfterRetry')}
{this.renderSortArrow('retryPassed')}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'percentage' ? direction : null}
onClick={this.sortBy('percentage')}
style={this.getSelectionStyle('percentage')}
>
{translate('populationCourses.percentage')}
{this.renderSortArrow('percentage')}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'failed' ? 'descending' : null}
onClick={this.sortBy('failed')}
style={this.getSelectionStyle('failed')}
>
{translate('populationCourses.number')}
{this.renderSortArrow('failed')}
</Table.HeaderCell>
<Table.HeaderCell
rowSpan="2"
sorted={sortBy === 'failedMany' ? 'descending' : null}
onClick={this.sortBy('failedMany')}
style={this.getSelectionStyle('failedMany')}
>
{translate('populationCourses.failedManyTimes')}
{this.renderSortArrow('failedMany')}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'attempts' ? 'descending' : null}
onClick={this.sortBy('attempts')}
style={this.getSelectionStyle('attempts')}
>
{translate('populationCourses.number')}
{this.renderSortArrow('attempts')}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'perStudent' ? 'descending' : null}
onClick={this.sortBy('perStudent')}
style={this.getSelectionStyle('perStudent')}
>
{translate('populationCourses.perStudent')}
{this.renderSortArrow('perStudent')}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'passedOfPopulation' ? direction : null}
onClick={this.sortBy('passedOfPopulation')}
style={this.getSelectionStyle('passedOfPopulation')}
>
{translate('populationCourses.passed')}
{this.renderSortArrow('passedOfPopulation')}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'triedOfPopulation' ? direction : null}
onClick={this.sortBy('triedOfPopulation')}
style={this.getSelectionStyle('triedOfPopulation')}
>
{translate('populationCourses.attempted')}
{this.renderSortArrow('triedOfPopulation')}
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{courses.coursestatistics.sort(this.criteria()).filter(this.limit()).map(course => (
<Table.Row key={course.course.code} active={this.active(course.course)}>
<Table.Cell onClick={this.limitPopulationToCourse(course)}>
{course.course.name[language]}
</Table.Cell>
<Table.Cell
icon="level up alternate"
onClick={() => {
this.props.history.push('/coursestatistics/')
this.props.getMultipleCourseStatistics({
codes: [course.course.code],
start: Number(this.props.query.year),
end: Number(moment(moment(this.props.query.year, 'YYYY').add(this.props.query.months, 'months')).format('YYYY')),
separate: false,
language: this.props.language
})
}}
style={{
borderLeft: '0px !important',
display: 'none'
}}
/>
<Table.Cell>{course.course.code}</Table.Cell>
<Table.Cell>
{course.stats.passed + course.stats.failed}
</Table.Cell>
<Table.Cell>
{course.stats.passed}
</Table.Cell>
<Table.Cell>
{course.stats.retryPassed}
</Table.Cell>
<Table.Cell>{course.stats.percentage} %</Table.Cell>
<Table.Cell>
{course.stats.failed}
</Table.Cell>
<Table.Cell>
{course.stats.failedMany}
</Table.Cell>
<Table.Cell>{course.stats.attempts}</Table.Cell>
<Table.Cell>
{course.stats.perStudent.toFixed(2)}
</Table.Cell>
<Table.Cell>{course.stats.passedOfPopulation} %</Table.Cell>
<Table.Cell>{course.stats.triedOfPopulation} %</Table.Cell>
</Table.Row>))}
</Table.Body>
</Table>
)
render() {
const { courses, translate, selected } = this.props
const { sortBy, reversed } = this.state
const { courses, translate } = this.props
const { reversed } = this.state
const direction = reversed ? 'descending' : 'ascending'
if (courses.length === 0) return null
const active = course =>
selected && (course.name === selected.course.name && course.code === selected.course.code)
return (
<div>
<Form>
@@ -75,119 +382,35 @@ class PopulationCourseStats extends Component {
value={this.state.limit}
onChange={e => this.setState({ limit: e.target.value })}
/>
<Button icon floated="right" onClick={() => this.setState({ showGradeDistribution: !this.state.showGradeDistribution })}>
<Icon color="black" size="big" name="chart bar" />
</Button>
</Form.Field>
</Form>
<Table celled sortable>
<Table.Header>
<Table.Row>
<Table.HeaderCell colSpan="2">{translate('populationCourses.course')}</Table.HeaderCell>
<Table.HeaderCell
rowSpan="2"
sorted={sortBy === 'students' ? 'descending' : null}
onClick={this.sortBy('students')}
>
{translate('populationCourses.students')}
</Table.HeaderCell>
<Table.HeaderCell
colSpan="3"
>
{translate('populationCourses.passed')}
</Table.HeaderCell>
<Table.HeaderCell
colSpan="2"
>
{translate('populationCourses.failed')}
</Table.HeaderCell>
<Table.HeaderCell colSpan="2">{translate('populationCourses.attempts')}</Table.HeaderCell>
<Table.HeaderCell colSpan="2">{translate('populationCourses.percentageOfPopulation')}</Table.HeaderCell>
</Table.Row>
<Table.Row>
<Table.HeaderCell>{translate('populationCourses.name')}</Table.HeaderCell>
<Table.HeaderCell>{translate('populationCourses.code')}</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'passed' ? 'descending' : null}
onClick={this.sortBy('passed')}
>
{translate('populationCourses.number')}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'retryPassed' ? 'descending' : null}
onClick={this.sortBy('retryPassed')}
>
{translate('populationCourses.passedAfterRetry')}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'percentage' ? direction : null}
onClick={this.sortBy('percentage')}
>
{translate('populationCourses.percentage')}
</Table.HeaderCell>
<Table.HeaderCell
sorted={sortBy === 'failed' ? 'descending' : null}
onClick={this.sortBy('failed')}
>
{translate('populationCourses.number')}
</Table.HeaderCell>
<Table.HeaderCell
rowSpan="2"
sorted={sortBy === 'failedMany' ? 'descending' : null}
onClick={this.sortBy('failedMany')}
>
{translate('populationCourses.failedManyTimes')}
</Table.HeaderCell>
<Table.HeaderCell>{translate('populationCourses.number')}</Table.HeaderCell>
<Table.HeaderCell>
{translate('populationCourses.perStudent')}
</Table.HeaderCell>
<Table.HeaderCell>{translate('populationCourses.passed')}</Table.HeaderCell>
<Table.HeaderCell>{translate('populationCourses.attempted')}</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{courses.sort(this.criteria()).filter(this.limit()).map(course => (
<Table.Row key={course.course.code} active={active(course.course)}>
<Table.Cell onClick={this.limitPopulationToCourse(course)}>
{course.course.name}
</Table.Cell>
<Table.Cell>{course.course.code}</Table.Cell>
<Table.Cell>
{course.stats.passed + course.stats.failed}
</Table.Cell>
<Table.Cell>
{course.stats.passed}
</Table.Cell>
<Table.Cell>
{course.stats.retryPassed}
</Table.Cell>
<Table.Cell>{course.stats.percentage} %</Table.Cell>
<Table.Cell>
{course.stats.failed}
</Table.Cell>
<Table.Cell>
{course.stats.failedMany}
</Table.Cell>
<Table.Cell>{course.stats.attempts}</Table.Cell>
<Table.Cell>
{(course.stats.attempts / (course.stats.passed + course.stats.failed)).toFixed(2)}
</Table.Cell>
<Table.Cell>{course.stats.passedOfPopulation} %</Table.Cell>
<Table.Cell>{course.stats.triedOfPopulation} %</Table.Cell>
</Table.Row>))}
</Table.Body>
</Table>
{this.state.showGradeDistribution ?
this.renderGradeDistributionTable(this.props, this.state)
:
this.renderBasicTable(this.props, this.state, direction)
}
</div>
)
}
}
const mapStateToProps = state => ({
translate: getTranslate(state.locale),
selected: state.populationLimit ? state.populationLimit.course : null,
populationSize: state.populations.length > 0 ? state.populations[0].data.length : 0
})
const mapStateToProps = (state) => {
const courseFilters = state.populationFilters.filters.filter(f => f.type === 'CourseParticipation')
const selectedCourses = courseFilters.map(f => f.params.course.course)
return {
language: state.settings.language,
translate: getTranslate(state.locale),
query: state.populations.query,
selectedCourses,
populationSize: state.populations.data.students.length > 0 ?
state.populations.data.students.length : 0
}
}
export default connect(
mapStateToProps,
{ setPopulationLimit, clearPopulationLimit, setLoading }
)(PopulationCourseStats)
{ setPopulationFilter, removePopulationFilterOfCourse, getMultipleCourseStatistics }
)(withRouter(PopulationCourseStats))
+4 -1
View File
@@ -3,6 +3,7 @@ import { connect } from 'react-redux'
import { func, arrayOf, object } from 'prop-types'
import { Segment, Header, Popup } from 'semantic-ui-react'
import { getTranslate } from 'react-localize-redux'
import SegmentDimmer from '../SegmentDimmer'
import PopulationCourseStats from '../PopulationCourseStats'
class PopulationCourses extends Component {
@@ -16,6 +17,7 @@ class PopulationCourses extends Component {
return null
}
const { samples, translate } = this.props
const { pending } = this.props.samples[0]
return (
<Segment>
<Popup
@@ -24,10 +26,11 @@ class PopulationCourses extends Component {
wide
position="top left"
/>
<SegmentDimmer translate={translate} isLoading={pending} />
{samples.map(sample => (
<PopulationCourseStats
key={sample.query.uuid}
courses={sample.data}
courses={sample.data || {}}
query={sample.query}
/>))}
</Segment>
@@ -0,0 +1,99 @@
import React from 'react'
import { connect } from 'react-redux'
import { func, arrayOf, object, number } from 'prop-types'
import { Progress } from 'semantic-ui-react'
import SearchResultTable from '../SearchResultTable'
import { getStudentTotalCredits } from '../../common'
import { creditsLessThan, creditsAtLeast } from '../../populationFilters'
import { setPopulationFilter, removePopulationFilter } from '../../redux/populationFilters'
const getTotal = students => students.map(student => getStudentTotalCredits(student))
const expectedAmountOfCredits = months => ([
[Math.ceil(months * (55 / 12))],
[Math.ceil(months * (40 / 12)), (Math.ceil(months * (55 / 12)))],
[Math.ceil(months * (30 / 12)), Math.ceil(months * (40 / 12))],
[Math.ceil(months * (20 / 12)), Math.ceil(months * (30 / 12))],
[Math.ceil(months * (10 / 12)), Math.ceil(months * (20 / 12))],
[1, Math.ceil(months * (10 / 12))],
[0, 0]
])
const filterStudents = (students, minCredits, maxCredits = Infinity) => {
if (minCredits === 0) {
return {
minCredits: '',
maxCredits,
amount: students.filter(s => s === minCredits).length
}
} else if (maxCredits === Infinity) {
return {
minCredits,
maxCredits: '',
amount: students.filter(s => s >= minCredits).length
}
}
return {
minCredits,
maxCredits,
amount: students.filter(s => s >= minCredits && s < maxCredits).length
}
}
const PopulationCreditGainTable = (props) => {
const setCreditFilter = (row) => {
props.filters.map(filter => props.removePopulationFilter(filter.id))
const credits = row[0].split('-').map(count => Number(count))
if (credits[0]) {
props.setPopulationFilter(creditsAtLeast({ credit: credits[0] }))
if (credits[1]) {
props.setPopulationFilter(creditsLessThan({ credit: credits[1] }))
}
} else {
props.setPopulationFilter(creditsLessThan({ credit: 1 }))
}
}
const { translate, sample, months } = props
const stats = getTotal(sample)
const limits = expectedAmountOfCredits(months)
const arr = limits.map(l => filterStudents(stats, ...l))
const rows = arr.map(a => [`${a.minCredits}-${a.maxCredits}`, a.amount, <Progress style={{ margin: '0px' }} percent={stats.length === 0 ? 0 : Math.round((a.amount / stats.length) * 100)} progress />])
const headers = [
`Credits gained during first ${months} months`,
`Students (all=${stats.length})`,
'Percentage of population'
]
return (
<SearchResultTable
headers={headers}
rows={rows}
selectable
rowClickFn={(e, row) => setCreditFilter(row)}
noResultText={translate('common.noResults')}
/>
)
}
const mapStateToProps = state => ({
months: state.populations.query.months,
filters: state.populationFilters.filters.filter(f => f.type === 'CreditsLessThan' || f.type === 'CreditsAtLeast')
})
PopulationCreditGainTable.propTypes = {
translate: func.isRequired,
sample: arrayOf(object).isRequired,
months: number.isRequired,
setPopulationFilter: func.isRequired,
removePopulationFilter: func.isRequired,
filters: arrayOf(object).isRequired
}
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(PopulationCreditGainTable)
+150 -61
View File
@@ -1,49 +1,80 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { func, object, string, arrayOf } from 'prop-types'
import { Segment, Header } from 'semantic-ui-react'
import { func, object, string, arrayOf, bool } from 'prop-types'
import { Segment, Header, Message, Button, Icon, Tab } from 'semantic-ui-react'
import { getTranslate } from 'react-localize-redux'
import _ from 'lodash'
import scrollToComponent from 'react-scroll-to-component'
import { makePopulationsToData } from '../../selectors/populationDetails'
import { setPopulationLimitField, clearPopulationLimit } from '../../redux/populationLimit'
import { getTotalCreditsFromCourses } from '../../common'
import PopulationFilters from '../PopulationFilters'
import CreditAccumulationGraphHighCharts from '../CreditAccumulationGraphHighCharts'
import CourseQuarters from '../CourseQuarters'
import PopulationLimiter from '../PopulationLimiter'
import PopulationStudents from '../PopulationStudents'
import PopulationCourses from '../PopulationCourses'
import PopulationCreditGainTable from '../PopulationCreditGainTable'
class PopulationDetails extends Component {
static propTypes = {
translate: func.isRequired,
samples: arrayOf(arrayOf(object)).isRequired,
selectedStudents: arrayOf(string).isRequired
samples: arrayOf(object).isRequired,
selectedStudents: arrayOf(string).isRequired,
queryIsSet: bool.isRequired,
isLoading: bool.isRequired
}
constructor() {
super()
this.chart = React.createRef()
this.courses = React.createRef()
this.students = React.createRef()
this.filters = React.createRef()
this.state = {
navigationVisible: false
}
}
isSamplesRenderable = () => {
const { samples } = this.props
return samples && samples.length > 0
}
renderCourseStatistics = () => {
const { samples, translate } = this.props
let statistics = []
let statistics = null
if (samples) {
statistics = samples.map((sample, i) => (
<CourseQuarters
key={`course-quarters-${i}`} // eslint-disable-line react/no-array-index-key
sample={sample.filter(s => this.props.selectedStudents.includes(s.studentNumber))}
translate={translate}
/>
))
statistics = (
<Tab
menu={{ pointing: true }}
panes={[
{
menuItem: 'Credits gained',
render: () => (
<Tab.Pane attached={false}>
<PopulationCreditGainTable
sample={samples.filter(s =>
this.props.selectedStudents.includes(s.studentNumber))}
translate={translate}
/>
</Tab.Pane>)
},
{
menuItem: 'Quarters',
render: () => (
<Tab.Pane attached={false}>
<CourseQuarters
sample={samples.filter(s =>
this.props.selectedStudents.includes(s.studentNumber))}
translate={translate}
/>
</Tab.Pane>
)
}]}
/>)
}
return (
<Segment>
<Header size="medium" dividing>
{translate('populationStatistics.creditStatisticsHeader')}
</Header>
<PopulationLimiter />
{statistics}
</Segment>
)
@@ -51,44 +82,85 @@ class PopulationDetails extends Component {
renderCreditGainGraphs = () => {
const { samples, translate } = this.props
const graphs = samples.map((sample, i) => (
const graphs = (
<CreditAccumulationGraphHighCharts
key={`credit-graph-${i}`} // eslint-disable-line react/no-array-index-key
students={sample}
title={`${translate('populationStatistics.sampleId')}: ${i}`}
students={samples}
title={`${translate('populationStatistics.sampleId')}`}
translate={translate}
label={sample.label}
maxCredits={sample.maxCredits}
label={samples.label}
maxCredits={samples.maxCredits}
selectedStudents={this.props.selectedStudents}
ref={this.chart}
/>
))
)
return (
<Segment>
<Header size="medium" dividing>
{translate('populationStatistics.graphSegmentHeader')}
</Header>
<PopulationLimiter />
{graphs}
{samples.length > 0 ? graphs : null}
</Segment>
)
}
renderNavigationPanel = () => (
<div>
<Segment className="navigationpanel" style={{ position: 'fixed', right: '2%', bottom: '2%' }}>
<Header size="medium" textAlign="center" >
Navigation
<Button className="navigationbuttonclose" icon basic floated="right" onClick={() => this.setState({ navigationVisible: false })} >
<Icon name="chevron right" />
</Button>
</Header>
<Button.Group vertical >
<Button onClick={() => scrollToComponent(this.filters.current, { align: 'top', offset: -40 })}>Go To Filters</Button>
<Button onClick={() => scrollToComponent(this.chart.current, { align: 'middle' })}>Go To Chart</Button>
<Button onClick={() => scrollToComponent(this.courses.current, { align: 'top', offset: -40 })}>Go To Course List</Button>
<Button onClick={() => scrollToComponent(this.students.current, { align: 'top', offset: -40 })}>Go To Student List</Button>
</Button.Group>
</Segment>
</div>
)
renderPopulationDetailsContent = () => (
<div>
<PopulationFilters ref={this.filters} samples={this.props.samples} />
{this.renderCreditGainGraphs()}
{this.renderCourseStatistics()}
<PopulationCourses ref={this.courses} />
<PopulationStudents
samples={this.props.samples}
selectedStudents={this.props.selectedStudents}
ref={this.students}
/>
</div>
)
render() {
if (!this.isSamplesRenderable()) {
const { samples, translate, queryIsSet, isLoading } = this.props
if (isLoading || !queryIsSet) {
return null
}
if (samples.length === 0) {
return (
<Message negative content={`${translate('populationStatistics.emptyQueryResult')}`} />
)
}
if (this.state.navigationVisible) {
return (
<div>
{this.renderPopulationDetailsContent()}
{this.renderNavigationPanel()}
</div>
)
}
return (
<div>
<PopulationFilters />
{this.renderCourseStatistics()}
{this.renderCreditGainGraphs()}
<PopulationStudents
samples={this.props.samples}
selectedStudents={this.props.selectedStudents}
/>
<PopulationCourses />
{this.renderPopulationDetailsContent()}
<div>
<Button className="navigationbuttonopen" icon basic onClick={() => this.setState({ navigationVisible: true })} style={{ position: 'fixed', right: '0.5%', bottom: '0.5%' }} >
<Icon name="bars" />
</Button>
</div>
</div>
)
}
@@ -97,35 +169,52 @@ class PopulationDetails extends Component {
const populationsToData = makePopulationsToData()
const mapStateToProps = (state) => {
const allSamples = populationsToData(state)
const samples = populationsToData(state)
const { complemented } = state.populationFilters
let selectedStudents = samples.length > 0 ? samples.map(s => s.studentNumber) : []
const allStudents = allSamples.length > 0 ? allSamples[0].map(s => s.studentNumber) : []
// TODO refactor to more functional approach where the whole sample is not tested for each filter
let selectedStudents = state.populationLimit
? state.populationLimit.course.students[state.populationLimit.field]
: allStudents
if (samples.length > 0 && state.populationFilters.filters.length > 0) {
const studentsForFilter = (f) => {
if (f.type === 'CourseParticipation') {
return Object.keys(f.studentsOfSelectedField)
}
return samples.filter(f.filter).map(s => s.studentNumber)
}
if (state.populationFilters.length > 0) {
const { filter } = state.populationFilters[0]
selectedStudents =
allSamples.length > 0 ? allSamples[0].filter(filter).map(s => s.studentNumber) : []
const matchingStudents = state.populationFilters.filters.map(studentsForFilter)
selectedStudents = _.intersection(...matchingStudents)
if (complemented) {
selectedStudents = _.difference(samples.map(s => s.studentNumber), selectedStudents)
}
}
// REFACTOR ??
if (samples.length > 0) {
const creditsAndDates = samples.map((s) => {
const passedCourses = s.courses.filter(c => c.passed)
const passedCredits = getTotalCreditsFromCourses(passedCourses)
const dates = passedCourses.map(c => c.date)
return { passedCredits, dates }
})
const credits = creditsAndDates.map(cd => cd.passedCredits)
let dates = creditsAndDates.map(cd => cd.dates)
dates = _.flattenDeep(dates).map(date => new Date(date).getTime())
samples.maxCredits = Math.max(...credits)
samples.maxDate = Math.max(...dates)
samples.minDate = Math.min(...dates)
}
return {
samples: allSamples.map((sample) => {
const credits = sample.map(s =>
s.courses.filter(c => c.passed).reduce((sum, c) => c.credits + sum, 0))
sample.maxCredits = Math.round(Math.max(...credits) / 10) * 10
return sample
}),
selected: state.populationLimit,
samples,
selectedStudents,
translate: getTranslate(state.locale)
complemented,
translate: getTranslate(state.locale),
queryIsSet: !!state.populations.query,
isLoading: state.populations.pending === true
}
}
export default connect(mapStateToProps, {
setPopulationLimitField,
clearPopulationLimit
})(PopulationDetails)
export default connect(mapStateToProps)(PopulationDetails)
@@ -0,0 +1,96 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Dropdown, Icon, Form, Segment, Button, Popup } from 'semantic-ui-react'
import { shape, func, arrayOf, string } from 'prop-types'
import infoTooltips from '../../common/infotooltips'
import { canceledStudyright } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
class CanceledStudyright extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
studyrights: arrayOf(string).isRequired,
language: string.isRequired
}
state = {
cancel: 'true'
}
options = [{ value: 'false', text: 'haven\'t' }, { value: 'true', text: 'have' }]
handleChange = (e, { value }) => {
this.setState({ cancel: value })
}
handleCancel = () => {
this.props.setPopulationFilter(canceledStudyright({
studyrights: this.props.studyrights,
cancel: this.state.cancel
}))
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
render() {
const { filter, language } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup content={infoTooltips.PopulationStatistics.Filters.CanceledStudyright[language]} trigger={<Icon style={{ float: 'right' }} name="info" />} />
<Form.Group inline>
<Form.Field>
<label>Filter students that </label>
</Form.Field>
<Form.Field>
<Dropdown
fluid
placeholder="have/haven't"
name="complemented"
onChange={this.handleChange}
options={this.options}
/>
</Form.Field>
<Form.Field>
<label> canceled this studyright </label>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleCancel}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
{filter.params.cancel === 'true' ? 'Showing students that have canceled this studyright' : <span><b>Excluded</b> students whose studyright is cancelled</span>}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ populations, settings }) => ({
studyrights: populations.query.studyRights,
language: settings.language
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(CanceledStudyright)
@@ -0,0 +1,83 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Popup } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import { removePopulationFilter, alterPopulationCourseFilter } from '../../redux/populationFilters'
class CourseParticipation extends Component {
static propTypes = {
language: string.isRequired,
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
alterPopulationCourseFilter: func.isRequired
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
selectField = field => () => {
const selectedField = this.props.filter.params.field
if (field !== selectedField) {
this.props.alterPopulationCourseFilter(this.props.filter.id, field)
}
}
renderFilterSegment = (type, text, active) =>
(
<Segment
inverted={active(type)}
secondary={active(type)}
onClick={this.selectField(type)}
style={{ width: 30, height: 60, fontSize: 13, textAlign: 'center', verticalAlign: 'middle', lineHeight: 1, paddingTop: 15 }}
>
{text}
</Segment>
)
render() {
const { filter, language } = this.props
if (filter.notSet) {
return null
}
const { course, field } = filter.params
const selectedField = field
const active = field2 =>
(selectedField === field2)
return (
<div>
<Segment.Group horizontal size="small">
<Popup
trigger={
<Segment
style={{ width: '30%', height: 40 }}
>
<em style={{ float: 'left', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', width: '80%' }}>{course.course.name[language]}</em>
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>}
content={course.course.name[language]}
/>
{this.renderFilterSegment('all', 'all', active)}
{this.renderFilterSegment('passed', 'passed', active)}
{this.renderFilterSegment('retryPassed', 'passed after fail', active)}
{this.renderFilterSegment('failed', 'failed', active)}
{this.renderFilterSegment('failedMany', 'failed many', active)}
{this.renderFilterSegment('notParticipated', 'not participated', active)}
{this.renderFilterSegment('notParticipatedOrFailed', 'not participated or failed', active)}
</Segment.Group>
</div>
)
}
}
const mapStateToProps = ({ settings }) => ({
language: settings.language
})
export default connect(mapStateToProps, {
removePopulationFilter, alterPopulationCourseFilter
})(CourseParticipation)
@@ -0,0 +1,26 @@
import React from 'react'
import { connect } from 'react-redux'
import { arrayOf, object } from 'prop-types'
import CourseParticipation from './CourseParticipation'
const CourseParticipationFilters = ({ filters }) => {
if (filters.length === 0) {
return null
}
return (
<div>
{filters.map(f => <CourseParticipation key={f.id} filter={f} />)}
</div>
)
}
CourseParticipationFilters.propTypes = {
filters: arrayOf(object).isRequired
}
const mapStateToProps = state => ({
filters: state.populationFilters.filters.filter(f => f.type === 'CourseParticipation')
})
export default connect(mapStateToProps)(CourseParticipationFilters)
@@ -0,0 +1,103 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Form, Input, Button } from 'semantic-ui-react'
import { shape, func } from 'prop-types'
import { courseParticipationNTimes } from '../../populationFilters'
import { removePopulationFilter, alterPopulationCourseFilter, setPopulationFilter } from '../../redux/populationFilters'
class CourseParticipationNTimes extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
populationCourses: shape({}).isRequired
}
state = {
participationAmount: 0,
code: '375063'
}
handleChange = (e) => {
this.setState({ participationAmount: e.target.value })
}
handleLimit = () => {
this.props.setPopulationFilter(courseParticipationNTimes({
amount: this.state.participationAmount, course: this.state.code
}))
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
courseInPopulation = () => {
const { populationCourses } = this.props
if (populationCourses[0]) {
return populationCourses[0].query.studyRights.includes('MH30_001')
}
return false
}
render() {
const { filter } = this.props
const { participationAmount } = this.state
if (!this.courseInPopulation()) {
return null
}
if (filter.notSet) {
return (
<Segment>
<Form>
<Form.Group inline>
<Form.Field>
<label>
Show only students that have participated in International Progress Test
</label>
</Form.Field>
<Form.Field>
<Input
type="number"
onChange={this.handleChange}
value={participationAmount}
/>
</Form.Field>
<Form.Field>
<label>times</label>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleLimit}
disabled={participationAmount === 0}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Participated in
<i> International Progress Test </i>
at least <b>{filter.params.amount}</b> times
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ populationCourses }) => ({
populationCourses
})
export default connect(mapStateToProps, {
removePopulationFilter, alterPopulationCourseFilter, setPopulationFilter
})(CourseParticipationNTimes)
@@ -0,0 +1,86 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Input, Button, Form, Popup } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import { creditsAtLeast } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
import infoTooltips from '../../common/infotooltips'
class CreditsAtLeast extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
language: string.isRequired
}
state = {
limit: ''
}
handleChange = (e) => {
this.setState({ limit: e.target.value })
}
handleLimit = () => {
this.props.setPopulationFilter(creditsAtLeast({ credit: this.state.limit }))
this.setState({ limit: '' })
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup content={infoTooltips.PopulationStatistics.Filters.CreditsAtLeast[this.props.language]} trigger={<Icon style={{ float: 'right' }} name="info" />} />
<Form.Group inline>
<Form.Field>
<label>Show only students with credits at least</label>
</Form.Field>
<Form.Field>
<Input
type="number"
onChange={this.handleChange}
value={this.state.limit}
/>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleLimit}
disabled={this.state.limit.length === 0}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Credits at least {filter.params.credit}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ settings }) => ({
language: settings.language
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(CreditsAtLeast)
@@ -1,16 +1,18 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Input, Button, Form } from 'semantic-ui-react'
import { shape, func } from 'prop-types'
import { Segment, Icon, Input, Button, Form, Popup } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import infoTooltips from '../../common/infotooltips'
import { creditsLessThan } from '../../populationFilters'
import { clearPopulationFilters, setPopulationFilter } from '../../redux/populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
class CreditsLessThan extends Component {
static propTypes = {
filter: shape({}).isRequired,
clearPopulationFilters: func.isRequired,
setPopulationFilter: func.isRequired
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
language: string.isRequired
}
state = {
@@ -22,12 +24,12 @@ class CreditsLessThan extends Component {
}
handleLimit = () => {
this.props.setPopulationFilter(creditsLessThan(this.state.limit))
this.props.setPopulationFilter(creditsLessThan({ credit: this.state.limit }))
this.setState({ limit: '' })
}
clearFilter = () => {
this.props.clearPopulationFilters()
this.props.removePopulationFilter(this.props.filter.id)
}
render() {
@@ -37,6 +39,7 @@ class CreditsLessThan extends Component {
return (
<Segment>
<Form>
<Popup content={infoTooltips.PopulationStatistics.Filters.CreditsLessThan[this.props.language]} trigger={<Icon style={{ float: 'right' }} name="info" />} />
<Form.Group inline>
<Form.Field>
<label>Show only students with credits less than</label>
@@ -55,6 +58,7 @@ class CreditsLessThan extends Component {
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
@@ -64,7 +68,7 @@ class CreditsLessThan extends Component {
return (
<Segment>
Credits less than {filter.params[0]}
Credits less than {filter.params.credit}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
@@ -72,8 +76,10 @@ class CreditsLessThan extends Component {
)
}
}
const mapStateToProps = ({ settings }) => ({
language: settings.language
})
export default connect(
null,
{ setPopulationFilter, clearPopulationFilters }
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(CreditsLessThan)
@@ -0,0 +1,148 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Dropdown, Button, Form, Input, Popup } from 'semantic-ui-react'
import { shape, func, arrayOf, object, string } from 'prop-types'
import _ from 'lodash'
import infoTooltips from '../../common/infotooltips'
import { courseParticipation } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
class DisciplineTypes extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
courseTypes: shape({}).isRequired,
disciplines: shape({}).isRequired,
courses: arrayOf(object).isRequired,
language: string.isRequired
}
state = {
discipline: '',
courseType: '',
atleast: 0
}
handleChange = (e, data) => {
this.setState({ [data.name]: data.value })
}
handleFilter = () => {
const courses = this.props.courses.filter(({ course, stats }) =>
course.disciplines[this.state.discipline] &&
course.coursetypes[this.state.courseType] &&
stats.students >= this.state.atleast)
courses.forEach(course => this.props.setPopulationFilter(courseParticipation({ field: 'all', course })))
this.setState({ discipline: '', courseType: '' })
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
render() {
const { filter, courseTypes, disciplines } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup content={infoTooltips.PopulationStatistics.Filters.DisciplineTypes[this.props.language]} trigger={<Icon style={{ float: 'right' }} name="info" />} />
<Form.Group inline>
<Form.Field>
<label>Filter by </label>
</Form.Field>
<Form.Field
style={{ width: 135 }}
>
<Dropdown
search
fluid
icon={null}
name="courseType"
placeholder="course type"
onChange={this.handleChange}
value={this.state.courseType}
options={_.sortBy(Object.entries(courseTypes).map(([value, text]) => ({
text: text.fi,
value
})), entry => entry.text)}
/>
</Form.Field>
<Form.Field>
<label> of </label>
</Form.Field>
<Form.Field
style={{ width: 200 }}
>
<Dropdown
search
icon={null}
fluid
name="discipline"
placeholder="discipline"
onChange={this.handleChange}
value={this.state.discipline}
options={_.sortBy(Object.entries(disciplines).map(([value, text]) => ({
text: text.fi,
value
})), entry => entry.text)}
/>
</Form.Field>
<Form.Field>
<label> and at least </label>
</Form.Field>
<Form.Field>
<Input
transparent
maxLength="3"
style={{ width: 30 }}
name="atleast"
onChange={this.handleChange}
value={this.state.atleast}
/>
</Form.Field>
<Form.Field>
<label> participants</label>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleFilter}
disabled={this.state.discipline.length === 0 ||
this.state.courseType.length === 0 ||
!String(this.state.atleast).match(/^[0-9]+$/)}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Participants of {filter.params.discipline} {filter.params.courseType}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ populationCourses, settings }) => ({
courseTypes: populationCourses[0].data.coursetypes,
disciplines: populationCourses[0].data.disciplines,
courses: populationCourses[0].data.coursestatistics,
language: settings.language
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(DisciplineTypes)
@@ -0,0 +1,129 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { shape, func, object, arrayOf, string } from 'prop-types'
import moment from 'moment'
import _ from 'lodash'
import infoTooltips from '../../common/infotooltips'
import { enrollmentStatus } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
class EnrollmentStatus extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
samples: arrayOf(object).isRequired,
language: string.isRequired
}
state = {
semesters: [],
enrolled: true
}
componentDidMount() {
if (this.props.filter.notSet) {
const options = this.createPossibleOptions(this.props.samples)
this.setState({ options })
}
}
formatSemesterCodeToText = (semester) => {
const text = moment(moment('1950', 'YYYY').add((semester * 6), 'months')).format('YYYY.MM')
const split = text.split('.')
const formatted = split[1] > 6 ? `Fall ${split[0]}` : `Spring ${split[0]}`
return formatted
}
createPossibleOptions = ({ minDate, maxDate }) => {
const firstSemester = Math.floor(moment(minDate).diff(moment('1950', 'YYYY'), 'months') / 6)
const lastSemester = Math.floor(moment(maxDate).diff(moment('1950', 'YYYY'), 'months') / 6)
const allSemesters = _.range(firstSemester, lastSemester + 1)
const options = allSemesters.map(semester =>
({ key: semester, text: this.formatSemesterCodeToText(semester), value: semester }))
return options
}
handleTime = (e, data) => {
this.setState({ semesters: data.value })
}
handleFilter = () => {
this.props.setPopulationFilter(enrollmentStatus({
semesters: this.state.semesters, enrolled: this.state.enrolled
}))
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup content={infoTooltips.PopulationStatistics.Filters.EnrollmentStatus[this.props.language]} trigger={<Icon style={{ float: 'right' }} name="info" />} />
<Form.Group inline>
<Form.Field>
<label>Select students that were </label>
</Form.Field>
<Form.Field>
<Dropdown
placeholder="select status"
onChange={(e, data) => this.setState({ enrolled: data.value })}
options={[{ key: 1, text: 'present', value: 1 }, { key: 2, text: 'absent', value: 2 }]}
/>
</Form.Field>
<Form.Field>
<label> during </label>
</Form.Field>
<Form.Field>
<Dropdown
placeholder="select semesters"
fluid
multiple
options={this.state.options}
onChange={this.handleTime}
/>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleFilter}
disabled={this.state.semesters.length === 0}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
Students that were {filter.params.enrolled === 1 ? 'present' : 'absent'} during {filter.params.semesters.map(semester => this.formatSemesterCodeToText(semester)).join(', ')}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ settings }) => ({
language: settings.language
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(EnrollmentStatus)
@@ -0,0 +1,140 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { shape, func, arrayOf, object, string } from 'prop-types'
import _ from 'lodash'
import infoTooltips from '../../common/infotooltips'
import { extentGraduated } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
class ExtentGraduated extends Component {
static propTypes = {
language: string.isRequired,
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
extents: arrayOf(object).isRequired
}
state = {
extentcode: undefined,
graduated: undefined,
complemented: 'false'
}
graduationOptions = [
{ value: 'grad', text: 'graduated' },
{ value: 'either', text: 'studying' }
] // illegal to pass boolean values as Dropdown options value :(
complementedOptions = [{ value: 'false', text: 'are' }, { value: 'true', text: 'are not' }]
handleChange = (e, data) => {
this.setState({ [data.name]: data.value })
}
handleLimit = () => {
const { extentcode, graduated, complemented } = this.state
this.props.setPopulationFilter(extentGraduated({ extentcode, graduated, complemented }))
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
renderSetText = (extents, filter) => {
const { language } = this.props
const { extentcode, graduated, complemented } = filter.params
let returnText = ''
if (complemented === 'true') {
returnText = returnText.concat('Not')
}
if (graduated === 'grad') {
returnText = returnText.concat(' graduated from')
} else if (graduated === 'either') {
returnText = returnText.concat(' studying')
}
returnText = returnText.concat(` ${extents.find(extent => extent.extentcode === extentcode).name[language]} `)
if (returnText === 'Not studying Määräaikaiset ulkomaalaiset opiskelijat ') {
return <span><b>Excluded</b> exchange students</span>
}
return returnText
}
render() {
const { filter, extents, language } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup content={infoTooltips.PopulationStatistics.Filters.ExtentGraduated[language]} trigger={<Icon style={{ float: 'right' }} name="info" />} />
<Form.Group inline>
<Form.Field>
Students that
</Form.Field>
<Form.Field>
<Dropdown
fluid
placeholder="are/not"
name="complemented"
onChange={this.handleChange}
options={this.complementedOptions}
/>
</Form.Field>
<Form.Field>
<Dropdown
search
fluid
name="graduated"
placeholder="select graduation"
onChange={this.handleChange}
options={this.graduationOptions}
/>
</Form.Field>
<Form.Field>
<Dropdown
search
fluid
name="extentcode"
placeholder="select extent"
onChange={this.handleChange}
options={_.sortBy(
Object.values(extents).map(({
extentcode, name
}) =>
({ value: extentcode, text: name[language] })),
entry => entry.text
)}
/>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleLimit}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
<label>
{this.renderSetText(extents, filter)}
</label>
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ settings }) => ({ language: settings.language })
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(ExtentGraduated)
@@ -0,0 +1,74 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Radio, Icon, Form, Segment, Button } from 'semantic-ui-react'
import { shape, func } from 'prop-types'
import { matriculationFilter } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
class MatriculationFilter extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired
}
state = {
matr: undefined
}
handleChange = (e, { value }) => {
this.setState({ matr: value })
}
handleMatr = () => {
this.props.setPopulationFilter(matriculationFilter(this.state.matr))
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Form.Group inline>
<Form.Field>
<label>Filter by matriculation examination</label>
</Form.Field>
<Form.Field>
<Radio name="matr" onChange={this.handleChange} value label="Yes" checked={this.state.matr === true} />
<Radio name="matr" onChange={this.handleChange} value={false} label="No" checked={this.state.matr === false} />
</Form.Field>
<Form.Field>
<Button
onClick={this.handleMatr}
disabled={this.state.matr === undefined}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
{filter.params.matriculationexamination ? 'Showing students that have done matriculation examination' : 'Showing students that have not done matriculation examination'}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
export default connect(
null,
{ setPopulationFilter, removePopulationFilter }
)(MatriculationFilter)
+113
View File
@@ -0,0 +1,113 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Icon, Form, Segment, Button, Confirm, Grid } from 'semantic-ui-react'
import { shape, func } from 'prop-types'
import { removePopulationFilter, setPopulationFilter, deletePopulationFilter } from '../../redux/populationFilters'
import { presetFilter } from '../../populationFilters'
class Preset extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
deletePopulationFilter: func.isRequired,
destroy: func.isRequired
}
state = { open: false }
handleSetFilter = (filter) => {
this.props.filter.notSet = false
this.props.setPopulationFilter(presetFilter(filter))
}
clearFilter = (destroy = false) => {
this.props.filter.notSet = true
if (destroy) {
this.props.destroy(this.props.filter.id)
}
this.props.removePopulationFilter(this.props.filter.id)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Form.Group inline>
<Form.Field>
<label>{filter.name}</label>
<em>
{filter.description}
</em>
</Form.Field>
<Form.Field>
<Button onClick={() => this.handleSetFilter(filter)}>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment >
<Grid>
<Grid.Column floated="left" width={12}>
<Form>
<Form.Field inline>
<label>
{filter.name}
</label>
<em>
{filter.description}
</em>
</Form.Field>
</Form>
</Grid.Column>
<Grid.Column floated="right">
<Form style={{ float: 'right' }}>
<Form.Group inline>
<Form.Field>
<Icon
name="trash"
onClick={() => this.setState({ open: true })}
/>
<Confirm
style={{
marginTop: 'auto !important',
display: 'inline-block !important',
position: 'relative',
top: '20%',
left: '33%'
}}
open={this.state.open}
cancelButton="Just remove from use"
confirmButton="Delete for good"
content="Are you sure you want to delete this filter?"
onCancel={() => { this.setState({ open: false }); this.clearFilter() }}
onConfirm={() => {
this.clearFilter(true)
this.props.deletePopulationFilter(filter)
this.setState({ open: false })
}}
size="small"
/>
</Form.Field>
<Form.Field >
<Icon name="remove" onClick={() => this.clearFilter(false)} />
</Form.Field>
</Form.Group>
</Form>
</Grid.Column>
</Grid>
</Segment >
)
}
}
export default connect(
null,
{ removePopulationFilter, setPopulationFilter, deletePopulationFilter }
)(Preset)
@@ -0,0 +1,156 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
import { shape, func, arrayOf, string } from 'prop-types'
import infoTooltips from '../../common/infotooltips'
import { priorityStudyright } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
class PriorityStudyright extends Component {
static propTypes = {
language: string.isRequired,
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
studyrights: arrayOf({}).isRequired
}
state = {
prioritycode: undefined,
degree: undefined,
programme: undefined
}
graduationOptions = [
{ value: 'grad', text: 'graduated' },
{ value: 'either', text: 'studying' }
] // illegal to pass boolean values as Dropdown options value :(
priorityOptions = [{ value: 1, text: 'primary studies' }, { value: 2, text: 'non-primary studies' }]
handleChange = (e, data) => {
this.setState({ [data.name]: data.value })
}
handleLimit = () => {
const { prioritycode, degree, programme } = this.state
this.props.setPopulationFilter(priorityStudyright({
prioritycode, degree, programme
}))
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
renderSetText = (filter, studyrights) => {
const { language } = this.props
const { prioritycode, degree, programme } = filter.params
let returnText = 'Showing students that has '
if (degree && programme) {
const degreeText = studyrights.degrees.find(sr => sr.code === degree)
const programmeText = studyrights.programmes.find(sr => sr.code === programme)
returnText = returnText.concat(`${degreeText ? degreeText.name[language] : ''} and ${programmeText ? programmeText.name[language] : ''}`)
} else if (degree && !programme) {
const degreeText = studyrights.degrees.find(sr => sr.code === degree)
returnText = returnText.concat(`${degreeText ? degreeText.name[language] : ''}`)
} else if (!degree && programme) {
const programmeText = studyrights.programmes.find(sr => sr.code === programme)
returnText = returnText.concat(`${programmeText ? programmeText.name[language] : ''}`)
}
returnText = returnText.concat(` as ${this.priorityOptions.find(option => option.value === prioritycode).text} `)
return returnText
}
render() {
const { filter, language, studyrights } = this.props
let degreeOptions = studyrights.degrees.map(sr =>
({ value: sr.code, text: sr.name[language] }))
degreeOptions = [{ value: 'anyDegree', text: 'any degree' }, ...degreeOptions]
let programmeOptions = studyrights.programmes.map(sr =>
({ value: sr.code, text: sr.name[language] }))
programmeOptions = [{ value: 'anyProgramme', text: 'any programme' }, ...programmeOptions]
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup content={infoTooltips.PopulationStatistics.Filters.ExtentGraduated[language]} trigger={<Icon style={{ float: 'right' }} name="info" />} />
<Form.Group inline>
<Form.Field>
<label>Students that has</label>
</Form.Field>
<Form.Field>
<Dropdown
search
fluid
name="degree"
placeholder="degree"
onChange={this.handleChange}
options={degreeOptions}
/>
</Form.Field>
<Form.Field>
<label>
and
</label>
</Form.Field>
<Form.Field>
<Dropdown
search
fluid
name="programme"
placeholder="programme"
onChange={this.handleChange}
options={programmeOptions}
/>
</Form.Field>
<Form.Field>
<label>as</label>
</Form.Field>
<Form.Field>
<Dropdown
fluid
placeholder="priority"
name="prioritycode"
onChange={this.handleChange}
options={this.priorityOptions}
/>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleLimit}
disabled={this.state.prioritycode === undefined ||
(this.state.degree === undefined && this.state.programme === undefined)}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
<label>
{this.renderSetText(filter, studyrights)}
</label>
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ settings, populations }) => ({
language: settings.language,
studyrights: populations.data.studyrights
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(PriorityStudyright)
@@ -0,0 +1,75 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Radio, Icon, Form, Segment, Button } from 'semantic-ui-react'
import { shape, func } from 'prop-types'
import { sexFilter } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
class SexFilter extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired
}
state = {
sex: ''
}
handleChange = (e, { value }) => {
this.setState({ sex: value })
}
handleSex = () => {
this.props.setPopulationFilter(sexFilter({ gender: this.state.sex }))
this.setState({ sex: '' })
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
render() {
const { filter } = this.props
if (filter.notSet) {
return (
<Segment>
<Form>
<Form.Group inline>
<Form.Field>
<label>Filter by gender</label>
</Form.Field>
<Form.Field>
<Radio name="sex" onChange={this.handleChange} value="male" label="Male" checked={this.state.sex === 'male'} />
<Radio name="sex" onChange={this.handleChange} value="female" label="Female" checked={this.state.sex === 'female'} />
</Form.Field>
<Form.Field>
<Button
onClick={this.handleSex}
disabled={this.state.sex === ''}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
{`Showing only ${filter.params.sex} students.`}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
export default connect(
null,
{ setPopulationFilter, removePopulationFilter }
)(SexFilter)
@@ -0,0 +1,84 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Radio, Button, Form, Popup } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import infoTooltips from '../../common/infotooltips'
import { startingThisSemester } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
class StartingThisSemester extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
language: string.isRequired
}
state = {
starting: true
}
handleRadio = () => {
this.props.setPopulationFilter(startingThisSemester({ starting: this.state.starting }))
this.setState({ starting: true })
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
render() {
const { filter } = this.props
const toggleLabel = this.state.starting
? 'started this semester'
: 'had started before this semester'
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup content={infoTooltips.PopulationStatistics.Filters.StartingThisSemester[this.props.language]} trigger={<Icon style={{ float: 'right' }} name="info" />} />
<Form.Group inline>
<Form.Field>
<label>Show only students that</label>
</Form.Field>
<Form.Field>
<Radio
toggle
label={toggleLabel}
checked={this.state.starting}
onChange={() => this.setState({ starting: !this.state.starting })}
/>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleRadio}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
{filter.params.starting ? 'Started this semester' : 'Had started before this semester'}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ settings }) => ({
language: settings.language
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(StartingThisSemester)
@@ -0,0 +1,155 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Icon, Dropdown, Button, Form, Popup } from 'semantic-ui-react'
import { shape, func, string } from 'prop-types'
import _ from 'lodash'
import infoTooltips from '../../common/infotooltips'
import { transferFilter } from '../../populationFilters'
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
class TransferFilter extends Component {
static propTypes = {
filter: shape({}).isRequired,
removePopulationFilter: func.isRequired,
setPopulationFilter: func.isRequired,
transfers: shape({}).isRequired,
language: string.isRequired
}
state = {
selectedSource: '',
selectedTarget: ''
}
getName = (code) => {
const { transfers } = this.props
const mergedTransfers = { ...Object.values(transfers)[0], ...Object.values(transfers)[1] }
return mergedTransfers[code].name.fi
}
handleChange = (e, data) => {
this.setState({ [data.name]: data.value })
}
handleFilter = () => {
const { selectedSource, selectedTarget } = this.state
this.props.setPopulationFilter(transferFilter({
source: selectedSource,
target: selectedTarget
}))
}
clearFilter = () => {
this.props.removePopulationFilter(this.props.filter.id)
}
renderFilterText = (filter) => {
const sourceName = this.getName(filter.params.source)
const targetName = this.getName(filter.params.target)
let returnText = 'Showing students that transferred'
if (sourceName !== 'Anywhere') {
returnText = returnText.concat(` from ${sourceName}`)
}
if (targetName !== 'Anywhere') {
returnText = returnText.concat(` to ${targetName}`)
}
return returnText
}
render() {
const { filter, transfers } = this.props
const { sources, targets } = transfers
sources.anywhere = { name: { en: 'Anywhere', fi: 'Anywhere' }, targets }
targets.anywhere = { name: { en: 'Anywhere', fi: 'Anywhere' }, sources }
let filteredTargets = targets
let filteredSources = sources
if (this.state.selectedSource !== '') {
filteredTargets = { ...sources[this.state.selectedSource].targets, anywhere: { name: { en: 'Anywhere', fi: 'Anywhere' } } }
}
if (this.state.selectedTarget !== '') {
filteredSources = { ...targets[this.state.selectedTarget].sources, anywhere: { name: { en: 'Anywhere', fi: 'Anywhere' } } }
}
if (filter.notSet) {
return (
<Segment>
<Form>
<Popup content={infoTooltips.PopulationStatistics.Filters.TransferFilter[this.props.language]} trigger={<Icon style={{ float: 'right' }} name="info" />} />
<Form.Group inline>
<Form.Field>
<label>Students that transferred from </label>
</Form.Field>
<Form.Field
style={{ width: 300 }}
>
<Dropdown
search
fluid
icon={null}
name="selectedSource"
placeholder="select source"
onChange={this.handleChange}
value={this.state.courseType}
options={_.sortBy(Object.entries(filteredSources).map(([value, text]) => ({
value,
text: text.name.fi
})), entry => entry.text)}
/>
</Form.Field>
<Form.Field>
<label> to </label>
</Form.Field>
<Form.Field
style={{ width: 300 }}
>
<Dropdown
search
icon={null}
fluid
name="selectedTarget"
placeholder="select target"
onChange={this.handleChange}
value={this.state.discipline}
options={_.sortBy(Object.entries(filteredTargets).map(([value, text]) => ({
value,
text: text.name.fi
})), entry => entry.text)}
/>
</Form.Field>
<Form.Field>
<Button
onClick={this.handleFilter}
disabled={this.state.selectedSource === '' ||
this.state.selectedTarget === ''
}
>
set filter
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
)
}
return (
<Segment>
{this.renderFilterText(filter)}
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={this.clearFilter} />
</span>
</Segment>
)
}
}
const mapStateToProps = ({ populationCourses, settings }) => ({
courseTypes: populationCourses[0].data.coursetypes,
disciplines: populationCourses[0].data.disciplines,
courses: populationCourses[0].data.coursestatistics,
language: settings.language
})
export default connect(
mapStateToProps,
{ setPopulationFilter, removePopulationFilter }
)(TransferFilter)
+298 -27
View File
@@ -1,61 +1,332 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Segment, Header, Button } from 'semantic-ui-react'
import { arrayOf, object } from 'prop-types'
import { Segment, Header, Button, Form, Radio, Modal, Icon, TextArea, Input } from 'semantic-ui-react'
import { object, func, arrayOf, bool, string } from 'prop-types'
import _ from 'lodash'
import uuidv4 from 'uuid/v4'
import { getTranslate } from 'react-localize-redux'
import CreditsLessThan from './CreditsLessThan'
import CreditsAtLeast from './CreditsAtLeast'
import StartingThisSemester from './StartingThisSemester'
import CourseParticipation from './CourseParticipation'
import CourseParticipationNTimes from './CourseParticipationNTimes'
import ExtentGraduated from './ExtentGraduated'
import Preset from './Preset'
import DisciplineTypes from './DisciplineTypes'
import EnrollmentStatus from './EnrollmentStatus'
import TransferFilter from './TransferFilter'
import CanceledStudyright from './CanceledStudyright'
import PriorityStudyright from './PriorityStudyright'
import { clearPopulationFilters, setComplementFilter, savePopulationFilters, setPopulationFilter } from '../../redux/populationFilters'
import { presetFilter, getFilterFunction } from '../../populationFilters'
const componentFor = {
CreditsAtLeast,
CreditsLessThan,
StartingThisSemester,
CourseParticipationNTimes,
DisciplineTypes,
EnrollmentStatus,
CourseParticipation,
ExtentGraduated,
TransferFilter,
CanceledStudyright,
PriorityStudyright
}
const advancedFilters = { // Filters that are too hard to use for common folk
DisciplineTypes,
TransferFilter,
ExtentGraduated,
PriorityStudyright
}
const persistantFilters = { // Filters that can be duplicated with different values
ExtentGraduated,
TransferFilter
}
class PopulationFilters extends Component {
static propTypes = {
populationFilters: arrayOf(object).isRequired
filters: arrayOf(object).isRequired,
complemented: bool.isRequired,
clearPopulationFilters: func.isRequired,
setComplementFilter: func.isRequired,
savePopulationFilters: func.isRequired,
setPopulationFilter: func.isRequired,
studyRights: arrayOf(string).isRequired,
populationFilters: object.isRequired, //eslint-disable-line
populationCourses: object.isRequired //eslint-disable-line
}
state = {
visible: false
visible: false,
presetName: '',
presetDescription: '',
presetFilters: [],
firstRenderKludge: true,
advancedUser: false,
modalOpen: false
}
componentDidUpdate(prevProps) {
if (this.state.firstRenderKludge || (this.props.populationCourses.pending === false
&& prevProps.populationCourses.pending === true)
|| (this.props.populationFilters.filtersFromBackend.length
!== prevProps.populationFilters.filtersFromBackend.length)) {
this.updateFilterList(this.props.populationFilters.filtersFromBackend)
this.setState({ firstRenderKludge: !this.state.firstRenderKludge }) // eslint-disable-line react/no-did-update-set-state,max-len
/* You may call setState() immediately in componentDidUpdate()
but note that it must be wrapped in a condition */
}
}
formatFilter = (filter) => {
let filterToSave = {}
if (filter.type === 'Preset') {
filterToSave = {
...filter,
filters: filter.filters.map(f => this.formatFilter(f))
}
} else {
filterToSave = {
...filter,
params: filter.type === 'CourseParticipation' ?
{
field: filter.params.field,
course: {
course: {
name: filter.params.course.course.name,
code: filter.params.course.course.code
}
}
}
:
filter.params
}
}
return filterToSave
}
handleSavePopulationFilters = () => {
const preset = {
id: uuidv4(),
name: this.state.presetName,
description: this.state.presetDescription,
population: this.props.studyRights,
filters: this.props.filters
}
this.setState({ presetName: '', presetDescription: '' })
const presetToSave = {
...preset,
filters: preset.filters.map(filter => this.formatFilter(filter))
}
this.props.savePopulationFilters(presetToSave)
this.updateFilterList([preset])
this.props.clearPopulationFilters()
this.props.setPopulationFilter(presetFilter(preset))
}
render() {
if (this.props.populationFilters.length === 0 && !this.state.visible) {
updateFilterList(filtersToCreate) {
// sorry for the uglyness but it kinda works (I think)
const regenerateFilterFunctions = filters => /* eslint-disable */
filters.map(f => f.type === 'Preset' ?
getFilterFunction(f.type, { ...f, filters: regenerateFilterFunctions(f.filters) },
this.props.populationCourses.data)
:
getFilterFunction(f.type, f.params, this.props.populationCourses.data))
if (filtersToCreate) {
const newFilters = filtersToCreate.map(newFilter =>
({
...newFilter,
filters: regenerateFilterFunctions(newFilter.filters)
}))
this.setState({ presetFilters: this.state.presetFilters.concat(newFilters) })
}
}
destroyFromAllFilters = id => this.setState({ presetFilters: this.state.presetFilters.filter(filter => filter.id !== id) })
renderAddFilters() {
const { extents, transfers } = this.props
const allFilters = _.union(Object.keys(componentFor).filter(f =>
!(Object.keys(advancedFilters).includes(f) && !this.state.advancedUser)).map(f =>
String(f)), this.state.presetFilters.map(f => f.id).filter(f => this.state.advancedUser))
const setFilters = _.union(
this.props.filters.map(f => f.type),
this.props.filters.filter(f => f.type === 'Preset').map(f => f.id),
)
const unsetFilters = _.uniq(_.difference(allFilters, setFilters.filter(setFilter => !Object.keys(persistantFilters).includes(setFilter))))
if (unsetFilters.length === 0) {
return null
}
if (!this.state.visible) {
return (
<Segment>
<Header>Filters</Header>
<Header>Add filters</Header>
<Button onClick={() => this.setState({ visible: true })}>add</Button>
</Segment>
)
}
if (this.props.populationFilters.length === 0) {
return (
<Segment>
<Header>Filters</Header>
<div>
<em>
Note that filters does not work yet when population is limited by students
that have participated a specific course
</em>
</div>
<CreditsLessThan filter={{ notSet: true }} />
<Button onClick={() => this.setState({ visible: false })}>cancel</Button>
</Segment>
)
}
return (
<Segment>
<Header>Add filters</Header>
<div>
<Radio
toggle
label="Advanced filters"
checked={this.state.advancedUser}
onChange={() => this.setState({ advancedUser: !this.state.advancedUser })}
/>
</div>
{unsetFilters.map(filterName => {//eslint-disable-line
if (componentFor[filterName]) { // THIS IS KINDA HACKED SOLUTION PLS FIX
return React.createElement(componentFor[filterName], {
filter: { notSet: true }, key: filterName, samples: this.props.samples, transfers, extents
})
}
else {
return React.createElement(Preset, {
filter: {
...this.state.presetFilters.find(f => f.id === filterName),
notSet: true
},
key: filterName,
destroy: this.destroyFromAllFilters
})
}
})
}
<Button onClick={() => this.setState({ visible: false })}>cancel</Button>
</Segment>
)
}
const filter = this.props.populationFilters[0]
renderSetFilters(handleSave) {
const setFilters = this.props.filters.map(f => f.type)
if (setFilters.length === 0) {
return null
}
return (
<Segment>
<Header>Filters</Header>
<CreditsLessThan filter={filter} />
<Form>
<Form.Group inline>
<Form.Field>
<label>Showing students that</label>
</Form.Field>
<Form.Field>
<Radio
toggle
checked={this.props.complemented}
onClick={this.props.setComplementFilter}
/>
</Form.Field>
<Form.Field>
<label>{!this.props.complemented ? ' are included in the filters.' : ' are in excluded by the filters.'}</label>
</Form.Field>
</Form.Group>
</Form>
{this.props.filters.map(filter => {
if (filter.type !== 'Preset') {
return React.createElement(componentFor[filter.type], { filter, key: filter.id, samples: this.props.samples, transfers: this.props.transfers, extents: this.props.extents })
}
return React.createElement(Preset, {
filter, key: filter.id, destroy: this.destroyFromAllFilters
})
})}
<Button onClick={this.props.clearPopulationFilters}>clear all filters</Button>
{this.state.advancedUser ?
<Modal
style={{
marginTop: 'auto !important',
display: 'inline-block !important',
position: 'relative',
top: '20%',
left: '33%'
}}
trigger={<Button onClick={() => this.setState({ modalOpen: true })}>Save filters as preset</Button>}
open={this.state.modalOpen}
onClose={() => this.setState({ modalOpen: false })}
size='small'
>
<Header />
<Modal.Content>
<Form>
<Form.Field>
<h2> Save current filters as preset </h2>
<em> This filter is saved in this population for future use </em>
<Input placeholder="Name..." maxLength={40} onChange={e => this.setState({ presetName: e.target.value })} />
</Form.Field>
<Form.Field>
<em> explain what your filter is doing here </em>
<TextArea placeholder="Description..." maxLength={160} onChange={e => this.setState({ presetDescription: e.target.value })} />
</Form.Field>
</Form>
</Modal.Content>
<Modal.Actions>
<Button
negative
onClick={() => this.setState({ modalOpen: false })}
>Cancel
</Button>
<Button
disabled={this.state.presetName === ''}
color='green'
onClick={() => {
handleSave()
this.setState({ modalOpen: false })
}}
inverted>
<Icon name='checkmark' /> Save
</Button>
</Modal.Actions>
</Modal>
: null
}
</Segment>
)
}
render() {
return (
<div>
{this.renderAddFilters()}
{this.renderSetFilters(this.handleSavePopulationFilters)}
</div>
)
}
}
const mapStateToProps = ({ populationFilters, locale, graphSpinner }) => ({
const mapStateToProps = ({
populationFilters,
locale,
graphSpinner,
populations,
populationCourses
}) => ({
populationCourses: populationCourses[0],
populationFilters,
filters: populationFilters.filters,
complemented: populationFilters.complemented,
translate: getTranslate(locale),
loading: graphSpinner
loading: graphSpinner,
studyRights: populations.query.studyRights,
extents: populations.data.extents,
transfers: populations.data.transfers
})
export default connect(mapStateToProps)(PopulationFilters)
export default connect(mapStateToProps, {
clearPopulationFilters, setComplementFilter, savePopulationFilters, setPopulationFilter
})(PopulationFilters)
@@ -1,89 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { func, object } from 'prop-types'
import { Segment, Icon } from 'semantic-ui-react'
import { setPopulationLimitField, clearPopulationLimit } from '../../redux/populationLimit'
import { setLoading } from '../../redux/graphSpinner'
class PopulationLimiter extends Component {
static propTypes = {
clearPopulationLimit: func.isRequired,
setPopulationLimitField: func.isRequired,
setLoading: func.isRequired,
selected: object // eslint-disable-line
}
selectField = field => () => {
if (field !== this.props.selected.field) {
this.props.setLoading()
setTimeout(() => {
this.props.setPopulationLimitField(field)
}, 0)
}
}
render() {
const { selected } = this.props
const active = field =>
(selected && selected.field === field)
if (!selected) return null
return (
<div>
<Segment.Group horizontal>
<Segment>
<em>{selected.course.course.name}</em>
<span style={{ float: 'right' }}>
<Icon name="remove" onClick={() => this.props.clearPopulationLimit()} />
</span>
</Segment>
<Segment
inverted={active('all')}
secondary={active('all')}
onClick={this.selectField('all')}
>
all
</Segment>
<Segment
inverted={active('passed')}
secondary={active('passed')}
onClick={this.selectField('passed')}
>
passed
</Segment>
<Segment
inverted={active('retryPassed')}
secondary={active('retryPassed')}
onClick={this.selectField('retryPassed')}
>
passed after fail
</Segment>
<Segment
inverted={active('failed')}
secondary={active('failed')}
onClick={this.selectField('failed')}
>
failed
</Segment>
<Segment
inverted={active('failedMany')}
secondary={active('failedMany')}
onClick={this.selectField('failedMany')}
>
failed many
</Segment>
</Segment.Group>
</div>
)
}
}
const mapStateToProps = state => ({
selected: state.populationLimit
})
export default connect(mapStateToProps, {
setPopulationLimitField, clearPopulationLimit, setLoading
})(PopulationLimiter)
+94 -32
View File
@@ -1,39 +1,96 @@
import React from 'react'
import { func, arrayOf, object, shape, string } from 'prop-types'
import { Card, Icon } from 'semantic-ui-react'
import { connect } from 'react-redux'
import { func, arrayOf, object, shape, string, bool } from 'prop-types'
import { Card, Icon, Button } from 'semantic-ui-react'
import _ from 'lodash'
import styles from './populationQueryCard.css'
import { DISPLAY_DATE_FORMAT } from '../../constants'
import { reformatDate } from '../../common'
const PopulationQueryCard = ({ translate, population, query, removeSampleFn, unit }) => {
const { uuid, year, semester } = query
return (
<Card className={styles.cardContainer}>
<Card.Header className={styles.cardHeader}>
<div>
{unit.name}
</div>
<Icon
name="remove"
className={styles.controlIcon}
onClick={() => removeSampleFn(uuid)}
/>
</Card.Header>
<Card.Meta>
<div className={styles.dateItem}>
<Icon name="calendar" size="small" /> {`${translate(`populationStatistics.${semester}`)}/${year}`}
</div>
<div>
{`${translate('populationStatistics.sampleSize', { amount: population.length })} `}
</div>
</Card.Meta>
</Card>
)
}
const PopulationQueryCard =
({ translate,
population,
query,
removeSampleFn,
units,
updateStudentsFn,
updating,
language
}) => {
const { uuid, year, semesters, months } = query
const { students } = population
if (students.length > 0) {
return (
<Card className={styles.cardContainer}>
<Card.Header className={styles.cardHeader}>
<div>{Object.values(units).map(u => u.name[language]).join(', ')}</div>
<Icon
name="remove"
className={styles.controlIcon}
onClick={() => removeSampleFn(uuid)}
/>
</Card.Header>
<Card.Meta>
<div className={styles.dateItem}>
<Icon name="calendar" size="small" /> {`${semesters.map(s => translate(`populationStatistics.${s}`))}/${year}-${Number(year) + 1}, showing ${months} months.`}
</div>
<div>
{`${translate('populationStatistics.sampleSize', { amount: students.length })} `}
</div>
<div>
{`Updated at ${reformatDate(_.minBy(students, 'updatedAt').updatedAt, DISPLAY_DATE_FORMAT)} `}
</div>
{updating ?
<Button disabled compact floated="left" size="medium" labelPosition="left" onClick={updateStudentsFn} >
<Icon loading name="refresh" />
update population
</Button>
:
<Button compact floated="left" size="medium" labelPosition="left" onClick={updateStudentsFn} >
<Icon name="refresh" />
update population
</Button>
}
</Card.Meta>
</Card>
)
}
return (
<Card className={styles.cardContainer}>
<Card.Header className={styles.cardHeader}>
<div>{Object.values(units).map(u => u.name[language]).join(', ')}</div>
<Icon
name="remove"
className={styles.controlIcon}
onClick={() => removeSampleFn(uuid)}
/>
</Card.Header>
<Card.Meta>
<div className={styles.dateItem}>
<Icon name="calendar" size="small" /> {`${semesters.map(s => translate(`populationStatistics.${s}`))}/${year}-${Number(year) + 1}, showing ${months} months.`}
</div>
<div>
{`${translate('populationStatistics.sampleSize', { amount: students.length })} `}
</div>
{updating ?
<Button disabled compact floated="left" size="medium" labelPosition="left" onClick={updateStudentsFn} >
<Icon loading name="refresh" />
update population
</Button>
:
<Button compact floated="left" size="medium" labelPosition="left" onClick={updateStudentsFn} >
<Icon name="refresh" />
update population
</Button>
}
</Card.Meta>
</Card>)
}
PopulationQueryCard.propTypes = {
language: string.isRequired,
translate: func.isRequired,
population: arrayOf(object).isRequired,
population: shape({ students: arrayOf(object), extents: arrayOf(object) }).isRequired,
query: shape({
year: string,
semester: string,
@@ -41,7 +98,12 @@ PopulationQueryCard.propTypes = {
uuid: string
}).isRequired,
removeSampleFn: func.isRequired,
unit: object // eslint-disable-line
units: arrayOf(object).isRequired,
unit: object, // eslint-disable-line
updateStudentsFn: func.isRequired,
updating: bool.isRequired
}
export default PopulationQueryCard
const mapStateToProps = ({ settings }) => ({ language: settings.language })
export default connect(mapStateToProps, null)(PopulationQueryCard)
+354 -87
View File
@@ -1,20 +1,22 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { func, arrayOf, object } from 'prop-types'
import { Form, Button, Message, Radio, Dropdown } from 'semantic-ui-react'
import { func, arrayOf, shape, bool, string, object } from 'prop-types'
import { Form, Button, Message, Dropdown, Icon, Checkbox } from 'semantic-ui-react'
import { getTranslate } from 'react-localize-redux'
import uuidv4 from 'uuid/v4'
import Datetime from 'react-datetime'
import { isEqual } from 'lodash'
import _ from 'lodash'
import moment from 'moment'
import { getPopulationStatistics, clearPopulations } from '../../redux/populations'
import { getPopulationCourses } from '../../redux/populationCourses'
import { getUnits } from '../../redux/units'
import { isInDateFormat, momentFromFormat, reformatDate, isValidYear } from '../../common'
import { makeMapRightsToDropDown } from '../../selectors/populationSearchForm'
import { setLoading } from '../../redux/graphSpinner'
import { getPopulationFilters, setPopulationFilter } from '../../redux/populationFilters'
import { extentGraduated, canceledStudyright } from '../../populationFilters'
import { getDegreesAndProgrammes } from '../../redux/populationDegreesAndProgrammes'
import { isInDateFormat, momentFromFormat, reformatDate, isValidYear, textAndDescriptionSearch } from '../../common'
import { setLoading } from '../../redux/graphSpinner'
import LanguageChooser from '../LanguageChooser'
import style from './populationSearchForm.css'
import { dropdownType } from '../../constants/types'
@@ -22,14 +24,21 @@ const YEAR_DATE_FORMAT = 'YYYY'
class PopulationSearchForm extends Component {
static propTypes = {
language: string.isRequired,
translate: func.isRequired,
getUnits: func.isRequired,
getDegreesAndProgrammes: func.isRequired,
getPopulationStatistics: func.isRequired,
getPopulationCourses: func.isRequired,
getPopulationFilters: func.isRequired,
setPopulationFilter: func.isRequired,
clearPopulations: func.isRequired,
queries: arrayOf(object).isRequired,
studyProgrammes: arrayOf(dropdownType).isRequired,
setLoading: func.isRequired
queries: shape({}).isRequired,
studyProgrammes: shape({}), //eslint-disable-line
degrees: arrayOf(dropdownType), //eslint-disable-line
studyTracks: arrayOf(dropdownType), //eslint-disable-line
setLoading: func.isRequired,
extents: arrayOf(object).isRequired,
pending: bool //eslint-disable-line
}
constructor() {
@@ -37,7 +46,7 @@ class PopulationSearchForm extends Component {
const INITIAL_QUERY = {
year: '2017',
semester: 'FALL',
semesters: ['FALL', 'SPRING'],
studyRights: [],
months: this.months('2017', 'FALL')
}
@@ -51,38 +60,50 @@ class PopulationSearchForm extends Component {
componentDidMount() {
const { studyProgrammes } = this.props
if (studyProgrammes.length === 0) {
this.props.getUnits()
if (!studyProgrammes || studyProgrammes.length === 0) {
this.props.getDegreesAndProgrammes()
}
}
months(year, term) { // eslint-disable-line
const start = term === 'FALL' ? `${year}-08-01` : `${year}-01-01`
months(year, term) { // eslint-disable-line
const start = term === 'FALL' ? `${year}-08-01` : moment(`${year}-01-01`).add(1, 'years')
return Math.ceil(moment.duration(moment().diff(moment(start))).asMonths())
}
validateQuery = () => {
const { queries } = this.props
const { query } = this.state
return queries.some((q) => {
const compare = { ...q }
delete compare.uuid
return isEqual(compare, query)
})
const compare = { ...queries }
delete compare.uuid
return _.isEqual(compare, query)
}
clearPopulations = () => this.props.clearPopulations()
fetchPopulation = () => {
const { query } = this.state
let queryCodes = []
queryCodes = [...Object.values(query.studyRights)]
const backendQuery = { ...query, studyRights: queryCodes }
const uuid = uuidv4()
const request = { ...query, uuid }
const request = { ...backendQuery, uuid }
this.setState({ isLoading: true })
this.props.setLoading()
Promise.all([
this.props.getPopulationStatistics(request),
this.props.getPopulationCourses(request)
]).then(() => this.setState({ isLoading: false }))
this.props.getPopulationCourses(request),
this.props.getPopulationFilters(request)
]).then(() => {
if (this.props.extents.map(e => e.extentcode).includes(7)) {
this.props.setPopulationFilter(extentGraduated({ extentcode: 7, graduated: 'either', complemented: 'true' }))
}
if (this.props.extents.map(e => e.extentcode).includes(34)) {
this.props.setPopulationFilter(extentGraduated({ extentcode: 34, graduated: 'either', complemented: 'true' }))
}
this.props.setPopulationFilter(canceledStudyright({ studyrights: queryCodes, canceled: 'false' }))
this.setState({ isLoading: false })
})
}
handleYearSelection = (year) => {
@@ -94,7 +115,7 @@ class PopulationSearchForm extends Component {
query: {
...query,
year: reformatDate(year, YEAR_DATE_FORMAT),
months: this.months(reformatDate(year, YEAR_DATE_FORMAT), this.state.query.semester)
months: this.months(reformatDate(year, YEAR_DATE_FORMAT), this.state.query.semesters.includes('FALL') ? 'FALL' : 'SPRING')
}
})
} else {
@@ -116,118 +137,357 @@ class PopulationSearchForm extends Component {
handleSemesterSelection = (e, { value }) => {
const { query } = this.state
const semesters = query.semesters.includes(value) ?
query.semesters.filter(s => s !== value) : [...query.semesters, value]
this.setState({
query: {
...query,
semester: value,
months: this.months(this.state.query.year, value)
semesters,
months: this.months(this.state.query.year, semesters.includes('FALL') ? 'FALL' : 'SPRING')
}
})
}
handleStudyRightChange = (e, { value }) => {
handleDegreeChange = (e, { value }) => {
const { query } = this.state
const studyRights = value.length > 0 ? [value[0]] : []
const degree = value
this.setState({
query: {
...query,
studyRights
studyRights: {
...query.studyRights,
degree
}
}
})
}
handleProgrammeChange = (e, { value }) => {
const { query } = this.state
const programme = value
this.setState({
query: {
...query,
studyRights: {
programme
}
}
})
}
handleStudyTrackChange = (e, { value }) => {
const { query } = this.state
const studyTrack = value
this.setState({
query: {
...query,
studyRights: {
...query.studyRights,
studyTrack
}
}
})
}
handleMonthsChange = (e, { value }) => {
handleMonthsChange = (value) => {
const { query } = this.state
const months = this.getMonths(query.year, value, this.state.query.semesters.includes('FALL') ? 'FALL' : 'SPRING')
this.setState({
query: {
...query,
months
}
})
}
handleClear = (type) => {
const { query } = this.state
this.setState({
query: {
...query,
months: value
studyRights: {
...query.studyRights,
[type]: undefined
}
}
})
}
// (Potential?) issue with using Math.ceil with months.
getMonths = (year, end, term) => {
const lastDayOfMonth = moment(end).endOf('month')
const start = term === 'FALL' ? `${year}-08-01` : `${year}-01-01`
return Math.ceil(moment.duration(moment(lastDayOfMonth).diff(moment(start))).asMonths())
}
getMinSelection = (year, semester) => (semester === 'FALL' ? `${year}-08-01` : `${year}-01-01`)
renderableList = (list) => {
const { language } = this.props
return list.map((sp) => {
const shh = {}
Object.assign(shh, sp)
shh.text = sp.name[language]
shh.description = sp.code
shh.value = sp.code
return shh
})
}
renderEnrollmentDateSelector = () => {
const { translate } = this.props
const { query, validYear } = this.state
const { semester, year, months } = query
const semesters = ['FALL', 'SPRING']
const { semesters, year } = query
return (
<Form.Group key="year" className={style.enrollmentSelectorGroup}>
<Form.Field error={!validYear} className={style.yearSelect}>
<label>{translate('populationStatistics.enrollmentYear')}</label>
<label>Enrollment</label>
<Datetime
className={style.yearSelectInput}
control={Datetime}
dateFormat={YEAR_DATE_FORMAT}
timeFormat={false}
renderYear={(props, selectableYear) => <td {...props}>{`${selectableYear}-${selectableYear + 1}`}</td>}
closeOnSelect
value={year}
value={`${year}-${moment(year).add(1, 'years').format('YYYY')}`}
isValidDate={isValidYear}
onChange={this.handleYearSelection}
/>
</Form.Field>
<Form.Field className={style.yearControl}>
<Button.Group basic vertical className={style.yearControlButtonGroup}>
<Button icon="plus" className={style.yearControlButton} onClick={this.addYear} />
<Button icon="minus" className={style.yearControlButton} onClick={this.subtractYear} />
<Button icon="plus" className={style.yearControlButton} onClick={this.addYear} tabIndex="-1" />
<Button icon="minus" className={style.yearControlButton} onClick={this.subtractYear} tabIndex="-1" />
</Button.Group>
</Form.Field>
<Form.Field>
<label>{translate('populationStatistics.semester')}</label>
{semesters.map(s => (
<Radio
className={style.semesterRadio}
key={s}
label={translate(`populationStatistics.${s}`)}
value={s}
name="semesterGroup"
checked={semester === s}
onChange={this.handleSemesterSelection}
/>
))}
<label>Semesters</label>
<Checkbox
className={style.semesterRadio}
key="FALL"
label={translate(`populationStatistics.${'FALL'}`)}
value="FALL"
name="semesterGroup"
checked={semesters.includes('FALL')}
onChange={this.handleSemesterSelection}
/>
<Checkbox
className={style.semesterRadio}
key="SPRING"
label={translate(`populationStatistics.${'SPRING'}`)}
value="SPRING"
name="semesterGroup"
checked={semesters.includes('SPRING')}
onChange={this.handleSemesterSelection}
/>
</Form.Field>
<Form.Field>
<label>{translate('populationStatistics.months')}</label>
<Form.Input type="number" onChange={this.handleMonthsChange} value={months} />
</Form.Field>
</Form.Group>
)
}
renderStudyGroupSelector = () => {
const { studyProgrammes, translate } = this.props
const { studyRights } = this.state.query
return (
<Form.Group id="rightGroup">
<Form.Field width={8}>
<label htmlFor="rightGroup">{translate('populationStatistics.studyRights')}</label>
<Dropdown
placeholder={translate('populationStatistics.selectStudyRights')}
multiple
search
selection
noResultsMessage={translate('populationStatistics.noSelectableStudyRights')}
value={studyRights}
options={studyProgrammes}
onChange={this.handleStudyRightChange}
closeOnChange
<label>Statistics until</label>
<Datetime
dateFormat="MMMM YYYY"
defaultValue={moment()}
onChange={value => this.handleMonthsChange(value)}
isValidDate={current => current.isBefore(moment()) &&
current.isAfter(this.getMinSelection(year, semesters[1] || semesters[0]))}
/>
</Form.Field>
</Form.Group>
)
}
renderStudyProgrammeDropdown = (studyRights, programmesToRender) => (
<Form.Field
width={6}
style={{
position: 'relative'
}}
>
<label>Study programme</label>
<Icon
link
name="close"
style={{
position: 'absolute',
top: 35,
bottom: 0,
right: '2.5em',
lineHeight: 1,
zIndex: 1
}}
onClick={() => this.handleClear('programme')}
/>
<Dropdown
placeholder="Select study programme"
search={textAndDescriptionSearch}
selection
noResultsMessage="No selectable study programmes"
value={studyRights.programme}
options={programmesToRender}
onChange={this.handleProgrammeChange}
closeOnChange
/>
</Form.Field>
)
renderAdditionalDegreeOrStudyTrackDropdown = (studyRights, studyTracksToRender, degreesToRender) => { //eslint-disable-line
const renderableDegrees = (
<Form.Field
style={{
position: 'relative',
minWidth: '22em',
width: 'auto'
}}
>
<Icon
link
name="close"
style={{
position: 'absolute',
top: 35,
bottom: 0,
right: '2.5em',
lineHeight: 1,
zIndex: 1
}}
onClick={() => this.handleClear('degree')}
/>
<label>Degree (Optional)</label>
<Dropdown
placeholder="Select degree"
search={textAndDescriptionSearch}
floating
selection
noResultsMessage="No selectable degrees"
value={studyRights.degree}
options={degreesToRender}
onChange={this.handleDegreeChange}
closeOnChange
/>
</Form.Field>)
const renderableTracks = (
<Form.Field
style={{
position: 'relative',
minWidth: '22em',
width: 'auto'
}}
>
<Icon
link
name="close"
style={{
position: 'absolute',
top: 35,
bottom: 0,
right: '2.5em',
lineHeight: 1,
zIndex: 1
}}
onClick={() => this.handleClear('studyTrack')}
/>
<label>Study Track (Optional)</label>
<Dropdown
placeholder="Select study track"
search={textAndDescriptionSearch}
floating
selection
noResultsMessage="No selectable study track"
value={studyRights.studyTrack}
options={studyTracksToRender}
onChange={this.handleStudyTrackChange}
closeOnChange
/>
</Form.Field>)
if (studyRights.programme && degreesToRender.length > 1 && studyTracksToRender.length > 1) {
return (
<Form.Group>
{renderableDegrees}
{renderableTracks}
</Form.Group>
)
} else if (studyRights.programme && degreesToRender.length > 1) {
return (
<div>
{renderableDegrees}
</div>
)
} else if (studyRights.programme && studyTracksToRender.length > 1) {
return (
<div>
{renderableTracks}
</div>
)
}
return null
}
renderStudyGroupSelector = () => {
const { studyProgrammes, language } = this.props
const { studyRights } = this.state.query
if (this.props.pending) {
return (
<Icon name="spinner" loading size="big" color="black" style={{ marginLeft: '45%' }} />
)
}
if (!studyProgrammes && !this.props.pending) {
return <Message error color="red" header="You have no rights to access any data. If you should have access please contact grp-toska@helsinki.fi" />
}
let sortedStudyProgrammes = studyProgrammes
let programmesToRender
if (studyProgrammes) {
sortedStudyProgrammes = _.sortBy(sortedStudyProgrammes, s => s.name[language])
programmesToRender = this.renderableList(sortedStudyProgrammes)
}
let degreesToRender
if (studyRights.programme) {
const sortedStudyDegrees = _.sortBy(studyProgrammes[studyRights.programme].associations['10'], s => s.name[language])
degreesToRender = this.renderableList(sortedStudyDegrees)
}
let studyTracksToRender
if (studyRights.programme) {
const sortedStudyTracks = _.sortBy(studyProgrammes[studyRights.programme].associations['30'], s => s.name[language])
studyTracksToRender = this.renderableList(sortedStudyTracks)
}
return (
<Form.Group horizontal="true" widths={2} >
<Form.Field
width={1}
style={{
position: 'relative',
display: 'inline-block'
}}
>
<label>Language</label>
<LanguageChooser />
</Form.Field>
{this.renderStudyProgrammeDropdown(studyRights, programmesToRender)}
{this.renderAdditionalDegreeOrStudyTrackDropdown(
studyRights,
studyTracksToRender,
degreesToRender
)}
</Form.Group>
)
}
shouldRenderSearchForm = () => {
const queryIsEmpty = Object.getOwnPropertyNames(this.props.queries).length > 0
return !queryIsEmpty
}
render() {
if (this.props.queries.length > 0) {
if (!this.shouldRenderSearchForm()) {
return null
}
const { translate } = this.props
const { isLoading, validYear, query } = this.state
let errorText = translate('populationStatistics.alreadyFetched')
let isQueryInvalid = this.validateQuery()
@@ -240,7 +500,6 @@ class PopulationSearchForm extends Component {
isQueryInvalid = true
errorText = translate('populationStatistics.selectStudyRights')
}
return (
<Form error={isQueryInvalid} loading={isLoading}>
{this.renderEnrollmentDateSelector()}
@@ -256,18 +515,26 @@ class PopulationSearchForm extends Component {
}
}
const mapRightsToDropdown = makeMapRightsToDropDown()
const mapStateToProps = ({ populations, units, locale }) => ({
queries: populations.map(population => population.query),
translate: getTranslate(locale),
studyProgrammes: mapRightsToDropdown(units)
})
const mapStateToProps = ({ settings, populations, populationDegreesAndProgrammes, locale }) => {
const studyRights = populationDegreesAndProgrammes.data || {}
const { pending } = populationDegreesAndProgrammes
return ({
language: settings.language,
queries: populations.query || {},
translate: getTranslate(locale),
studyProgrammes: studyRights['20'],
pending,
extents: populations.data.extents || []
})
}
const mapDispatchToProps = dispatch => ({
getPopulationStatistics: request => dispatch(getPopulationStatistics(request)),
getPopulationCourses: request => dispatch(getPopulationCourses(request)),
getUnits: () => dispatch(getUnits()),
getPopulationFilters: request => dispatch(getPopulationFilters(request)),
setPopulationFilter: filter => dispatch(setPopulationFilter(filter)),
getDegreesAndProgrammes: () => dispatch(getDegreesAndProgrammes()),
clearPopulations: () => dispatch(clearPopulations()),
setLoading: () => dispatch(setLoading())
})
@@ -1,12 +1,12 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { func, shape, arrayOf, object, bool } from 'prop-types'
import { func, shape, object, bool } from 'prop-types'
import { getTranslate } from 'react-localize-redux'
import _ from 'lodash'
import PopulationQueryCard from '../PopulationQueryCard'
import { removePopulation } from '../../redux/populations'
import { removePopulation, updatePopulationStudents } from '../../redux/populations'
import { clearPopulationCourses } from '../../redux/populationCourses'
import { clearPopulationLimit } from '../../redux/populationLimit'
import { clearPopulationFilters } from '../../redux/populationFilters'
import styles from './populationSearchHistory.css'
@@ -15,31 +15,37 @@ class PopulationSearchHistory extends Component {
static propTypes = {
translate: func.isRequired,
removePopulation: func.isRequired,
populations: arrayOf(shape({
populations: shape({
pending: bool,
error: bool,
data: arrayOf(object),
data: shape({}),
query: object
})).isRequired,
units: object // eslint-disable-line
}).isRequired,
units: object, // eslint-disable-line
updatePopulationStudents: func.isRequired
}
removePopulation = uuid => this.props.removePopulation(uuid)
renderQueryCards = () => {
const { populations, translate, units } = this.props
return populations.map((population, i) => (
population.query ?
<PopulationQueryCard
key={`population-${population.query.uuid}`}
translate={translate}
population={population.data}
query={population.query}
queryId={i}
unit={units.data.find(u => u.id === population.query.studyRights[0])}
removeSampleFn={this.removePopulation}
/> : null
))
if (!populations.data.students) {
return null
}
const studentNumberList = (populations.data.students.map(s => s.studentNumber))
return populations.query ? (<PopulationQueryCard
key={`population-${populations.query.uuid}`}
translate={translate}
population={populations.data}
query={populations.query}
queryId={0}
unit={units.data[20][populations.query.studyRights[0]]} // Possibly deprecated
units={_.flattenDeep(Object.values(units.data).map(u =>
Object.values(u))).filter(u => populations.query.studyRights.includes(u.code))}
removeSampleFn={this.removePopulation}
updateStudentsFn={() => this.props.updatePopulationStudents(studentNumberList)}
updating={populations.updating}
/>) : null
}
render() {
@@ -51,9 +57,9 @@ class PopulationSearchHistory extends Component {
}
}
const mapStateToProps = ({ populations, units, locale }) => ({
const mapStateToProps = ({ populations, populationDegreesAndProgrammes, locale }) => ({
populations,
units,
units: populationDegreesAndProgrammes,
translate: getTranslate(locale)
})
@@ -61,9 +67,10 @@ const mapDispatchToProps = dispatch => ({
removePopulation: (uuid) => {
dispatch(removePopulation(uuid))
dispatch(clearPopulationCourses())
dispatch(clearPopulationLimit())
dispatch(clearPopulationFilters())
}
},
updatePopulationStudents: students => dispatch(updatePopulationStudents(students))
})
export default connect(mapStateToProps, mapDispatchToProps)(PopulationSearchHistory)
@@ -54,7 +54,7 @@ const mapStateToProps = ({ locale, populations }) => ({
translate: getTranslate(locale),
currentLanguage: getActiveLanguage(locale).value,
populationFound: populations.length > 0,
loading: populations.length > 0 && populations[0].pending
loading: populations.pending
})
export default connect(mapStateToProps)(PopulationStatistics)
+78 -13
View File
@@ -1,21 +1,39 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { string, arrayOf, object, func, bool, shape } from 'prop-types'
import { Header, Segment, Table, Button } from 'semantic-ui-react'
import { Header, Segment, Table, Button, Icon, Popup } from 'semantic-ui-react'
import { withRouter } from 'react-router-dom'
import { getStudentTotalCredits, copyToClipboard } from '../../common'
import { toggleStudentListVisibility } from '../../redux/settings'
import StudentNameVisibilityToggle from '../StudentNameVisibilityToggle'
const popupTimeoutLength = 1000
class PopulationStudents extends Component {
state = {}
handlePopupOpen = (id) => {
this.setState({ [id]: true })
this.timeout = setTimeout(() => {
this.setState({ [id]: false })
}, popupTimeoutLength)
}
handlePopupClose = (id) => {
this.setState({ [id]: false })
clearTimeout(this.timeout)
}
renderStudentTable() {
if (!this.props.showList) {
return null
}
const students = this.props.samples[0].reduce((obj, s) => {
const students = this.props.samples.reduce((obj, s) => {
obj[s.studentNumber] = s
return obj
}, {})
@@ -23,12 +41,17 @@ class PopulationStudents extends Component {
const byName = (s1, s2) =>
(students[s1].lastname < students[s2].lastname ? -1 : 1)
const creditsSinceStart = studentNumber => students[studentNumber].courses
.filter(c => c.passed)
.reduce((s, c) => s + c.credits, 0)
const creditsSinceStart = studentNumber => getStudentTotalCredits(students[studentNumber])
const pushToHistoryFn = studentNumber => this.props.history.push(`/students/${studentNumber}`)
const copyToClipboardAll = () => {
const studentsInfo = this.props.selectedStudents.map(number => students[number])
const emails = studentsInfo.filter(s => s.email).map(s => s.email)
const clipboardString = emails.join('; ')
copyToClipboard(clipboardString)
}
return (
<div>
<StudentNameVisibilityToggle />
@@ -54,30 +77,72 @@ class PopulationStudents extends Component {
<Table.HeaderCell>
all credits
</Table.HeaderCell>
{this.props.showNames ? (
<Table.HeaderCell>
email
<Popup
trigger={
<Icon
link
name="copy"
onClick={copyToClipboardAll}
style={{ float: 'right' }}
/>}
content="Copied email list!"
on="click"
open={this.state['0']}
onClose={() => this.handlePopupClose('0')}
onOpen={() => this.handlePopupOpen('0')}
position="top right"
/>
</Table.HeaderCell>
) : null}
</Table.Row>
</Table.Header>
<Table.Body>
{this.props.selectedStudents.sort(byName).map(studentNumber => (
<Table.Row key={studentNumber} onClick={() => pushToHistoryFn(studentNumber)}>
<Table.Cell>
<Table.Row key={studentNumber} >
<Table.Cell onClick={() => pushToHistoryFn(studentNumber)}>
{studentNumber}
</Table.Cell>
{ this.props.showNames ? (
<Table.Cell>
{this.props.showNames ? (
<Table.Cell onClick={() => pushToHistoryFn(studentNumber)}>
{students[studentNumber].lastname}
</Table.Cell>
) : null }
) : null}
{this.props.showNames ? (
<Table.Cell>
<Table.Cell onClick={() => pushToHistoryFn(studentNumber)}>
{students[studentNumber].firstnames}
</Table.Cell>
) : null}
<Table.Cell>
<Table.Cell onClick={() => pushToHistoryFn(studentNumber)}>
{creditsSinceStart(studentNumber)}
</Table.Cell>
<Table.Cell>
{students[studentNumber].credits}
</Table.Cell>
{this.props.showNames ? (
<Table.Cell>
{students[studentNumber].email}
{students[studentNumber].email
? <Popup
trigger={
<Icon
link
name="copy outline"
onClick={() => copyToClipboard(students[studentNumber].email)}
style={{ float: 'right' }}
/>}
content="Email copied!"
on="click"
open={this.state[studentNumber]}
onClose={() => this.handlePopupClose(studentNumber)}
onOpen={() => this.handlePopupOpen(studentNumber)}
position="top right"
/>
: null}
</Table.Cell>
) : null}
</Table.Row>
))}
</Table.Body>
@@ -107,7 +172,7 @@ class PopulationStudents extends Component {
}
PopulationStudents.propTypes = {
samples: arrayOf(arrayOf(object)).isRequired,
samples: arrayOf(object).isRequired,
selectedStudents: arrayOf(string).isRequired,
toggleStudentListVisibility: func.isRequired,
showNames: bool.isRequired,
+35 -20
View File
@@ -1,5 +1,5 @@
import React from 'react'
import { Table } from 'semantic-ui-react'
import { Table, Icon } from 'semantic-ui-react'
import { arrayOf, string, object, func, bool, oneOfType, array } from 'prop-types'
import styles from './searchResultsTable.css'
@@ -8,12 +8,12 @@ const getHeaderRow = headers => (
<Table.Header>
<Table.Row>
{
headers.map(header => (
<Table.HeaderCell key={`header-${header}`}>
{header}
</Table.HeaderCell>
))
}
headers.map(header => (
<Table.HeaderCell key={`header-${header}`}>
{header}
</Table.HeaderCell>
))
}
</Table.Row>
</Table.Header>
)
@@ -21,20 +21,35 @@ const getHeaderRow = headers => (
const getTableBody = (rows, rowClickFn, selectable) => (
<Table.Body>
{
rows.map((row, i) => (
<Table.Row
className={(selectable ? styles.selectableRow : '')}
key={`row-${i}`} // eslint-disable-line react/no-array-index-key
onClick={e => rowClickFn(e, row)}
>
{
Object.values(row).map((value, index) => (
<Table.Cell key={`cell-${index}`}>{value}</Table.Cell> // eslint-disable-line react/no-array-index-key
))
rows.map((row, i) => (
<Table.Row
className={(selectable ? styles.selectableRow : '')}
key={`row-${i}`} // eslint-disable-line react/no-array-index-key
onClick={e => rowClickFn(e, row)}
>
{
Object.values(row).map((value, index) => {
if (index === 2 && row.date) {
return (
<Table.Cell
key={`cell-${index}`} // eslint-disable-line react/no-array-index-key
>
{row.passed || value.includes('Yes') ?
<Icon name="check circle outline" color="green" />
:
<Icon name="remove circle outline" color="red" />
}
{value}
</Table.Cell>
)
}
</Table.Row>
))
}
return <Table.Cell key={`cell-${index}`}>{value}</Table.Cell> // eslint-disable-line react/no-array-index-key
})
}
</Table.Row>
))
}
</Table.Body>
)
+40
View File
@@ -0,0 +1,40 @@
import React, { Component } from 'react'
import { Header, Button, Message } from 'semantic-ui-react'
import CourseCodeMapper from '../CourseCodeMapper'
import sharedStyles from '../../styles/shared'
class Settings extends Component {
state = {
selected: 1
}
getSetting = () => {
if (this.state.selected === 0) {
return <Header>None</Header>
} else if (this.state.selected === 1) {
return <CourseCodeMapper />
}
return []
}
select = (which) => {
this.setState({ selected: which })
}
render() {
const selectedComponent = this.getSetting()
return (
<div className={sharedStyles.segmentContainer} >
<Message content="Visible only for admins for now" />
<Header className={sharedStyles.segmentTitle} size="large">Settings</Header>
<Button.Group >
<Button onClick={() => this.select(1)}>Course code Mapping</Button>
</Button.Group>
{selectedComponent}
</div>
)
}
}
export default Settings
+37
View File
@@ -0,0 +1,37 @@
import React, { Component } from 'react'
import Highcharts from 'highcharts'
import ReactHighcharts from 'react-highcharts'
import { shape, arrayOf, object } from 'prop-types'
class StackedBarChart extends Component {
state = {
graphOptions: {}
}
componentDidMount() {
const { options, series } = this.props
this.setState({ graphOptions: { ...options, series } })
}
componentDidUpdate(prevProps) {
if (this.props.series !== prevProps.series) {
const { options, series } = this.props
this.setState({ graphOptions: { ...options, series } }) //eslint-disable-line
}
}
render() {
return (
<div>
<ReactHighcharts
highcharts={Highcharts}
config={this.state.graphOptions}
/>
</div>
)
}
}
StackedBarChart.propTypes = {
options: shape({}).isRequired,
series: arrayOf(object).isRequired
}
export default StackedBarChart
+114 -11
View File
@@ -1,9 +1,10 @@
import React, { Component } from 'react'
import { func, shape, string, boolean, arrayOf, integer } from 'prop-types'
import { connect } from 'react-redux'
import { Segment } from 'semantic-ui-react'
import { isEmpty } from 'lodash'
import { Segment, Table, Icon } from 'semantic-ui-react'
import { isEmpty, sortBy } from 'lodash'
import { withRouter } from 'react-router-dom'
import { getStudent, removeStudentSelection, resetStudent } from '../../redux/students'
import StudentInfoCard from '../StudentInfoCard'
import CreditAccumulationGraph from '../CreditAccumulationGraph'
import SearchResultTable from '../SearchResultTable'
@@ -12,6 +13,20 @@ import { removeInvalidCreditsFromStudent, byDateDesc, reformatDate } from '../..
import sharedStyles from '../../styles/shared'
class StudentDetails extends Component {
componentDidMount() {
this.props.history.listen((location, action) => {
if (action === 'POP' || location.pathname !== '/students') {
this.props.resetStudent()
this.props.removeStudentSelection()
}
})
}
componentDidUpdate() {
if (isEmpty(this.props.student) && this.props.studentNumber) {
this.props.getStudent(this.props.studentNumber)
}
}
renderCreditsGraph = () => {
const { translate, student } = this.props
@@ -28,7 +43,7 @@ class StudentDetails extends Component {
}
renderCourseParticipation = () => {
const { translate, student } = this.props
const { translate, student, language } = this.props
const courseHeaders = [
translate('common.date'),
@@ -38,10 +53,10 @@ class StudentDetails extends Component {
]
const courseRows = student.courses.map((c) => {
const {
date, grade, credits, course
date, grade, credits, course, isStudyModuleCredit, passed
} = c
return {
date, course: `${course.name} (${course.code})`, grade, credits
date, course: `${isStudyModuleCredit ? `${course.name[language]} [Study Module]` : course.name[language]} (${course.code})`, grade, credits, passed
}
})
return (
@@ -56,8 +71,81 @@ class StudentDetails extends Component {
)
}
renderStudyRights = () => {
const { student, language } = this.props
const studyRightHeaders = ['Degree', 'Programme', 'Graduated']
const studyRightRows = student.studyrights.map((studyright) => {
const degrees = sortBy(studyright.studyrightElements, 'enddate').filter(e => e.element_detail.type === 10)
.map(degree => ({
startdate: degree.startdate,
enddate: degree.enddate,
name: degree.element_detail.name[language],
graduateionDate: degree.graduationDate,
canceldate: degree.canceldate
}))
const programmes = sortBy(studyright.studyrightElements, 'enddate').filter(e => e.element_detail.type === 20)
.map(programme => ({
startdate: programme.startdate,
enddate: programme.enddate,
name: programme.element_detail.name[language]
}))
return {
studyrightid: studyright.studyrightid,
graduated: studyright.graduated,
canceldate: studyright.canceldate,
enddate: studyright.enddate,
elements: { degrees, programmes }
}
})
return (
<Table>
<Table.Header>
<Table.Row>
{studyRightHeaders.map((header, index) => (
<Table.HeaderCell key={index}/* eslint-disable-line */>
{header}
</Table.HeaderCell>
))
}
</Table.Row>
</Table.Header>
<Table.Body>
{studyRightRows.map(c => (
<Table.Row key={c.studyrightid}>
<Table.Cell verticalAlign="middle">
{c.elements.degrees.map(degree => (
<p key={degree.name}>{`${degree.name} (${reformatDate(degree.startdate, 'DD.MM.YYYY')} - ${reformatDate(degree.enddate, 'DD.MM.YYYY')})`} <br /> </p>
))}
</Table.Cell>
<Table.Cell>
{c.elements.programmes.map(programme => (
<p key={programme.name}>{`${programme.name} (${reformatDate(programme.startdate, 'DD.MM.YYYY')} - ${reformatDate(programme.enddate, 'DD.MM.YYYY')})`}<br /> </p>
))}
</Table.Cell>
<Table.Cell>
{c.canceldate ? // eslint-disable-line
<div><p style={{ color: 'red', fontWeight: 'bold' }}>CANCELED</p></div>
:
c.graduated ?
<div><Icon name="check circle outline" color="green" /><p>{reformatDate(c.enddate, 'DD.MM.YYYY')}</p></div>
:
<div><Icon name="remove circle outline" color="red" /><p>{reformatDate(c.enddate, 'DD.MM.YYYY')}</p></div>
}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
)
}
render() {
const { translate, student } = this.props
const { translate, student, studentNumber } = this.props
if (!studentNumber) {
return null
}
if (isEmpty(student)) {
return null
}
@@ -68,6 +156,7 @@ class StudentDetails extends Component {
translate={translate}
/>
{this.renderCreditsGraph()}
{this.renderStudyRights()}
{this.renderCourseParticipation()}
</Segment>
)
@@ -75,12 +164,18 @@ class StudentDetails extends Component {
}
StudentDetails.propTypes = {
language: string.isRequired,
getStudent: func.isRequired,
history: shape({}).isRequired,
resetStudent: func.isRequired,
removeStudentSelection: func.isRequired,
studentNumber: string,
translate: func.isRequired,
student: shape({
courses: arrayOf(shape({
course: shape({
code: string,
name: string
name: Object
}),
credits: integer,
date: string,
@@ -96,12 +191,20 @@ StudentDetails.propTypes = {
}
StudentDetails.defaultProps = {
student: {}
student: {},
studentNumber: ''
}
const mapStateToProps = ({ students }) => ({
const mapStateToProps = ({ students, settings }) => ({
language: settings.language,
student: students.data.find(student =>
student.studentNumber === students.selected)
})
const mapDispatchToProps = dispatch => ({
removeStudentSelection: () => dispatch(removeStudentSelection()),
resetStudent: () => dispatch(resetStudent()),
getStudent: studentNumber =>
dispatch(getStudent(studentNumber))
})
export default connect(mapStateToProps)(StudentDetails)
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(StudentDetails))
+36 -7
View File
@@ -1,7 +1,8 @@
import React from 'react'
import { func, bool } from 'prop-types'
import { Card, Icon } from 'semantic-ui-react'
import { func, bool, shape } from 'prop-types'
import { Card, Icon, Button } from 'semantic-ui-react'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { reformatDate } from '../../common'
import { studentDetailsType } from '../../constants/types'
@@ -10,16 +11,20 @@ import { DISPLAY_DATE_FORMAT } from '../../constants'
import styles from './studentInfoCard.css'
import { removeStudentSelection, resetStudent } from '../../redux/students'
import { updatePopulationStudents } from '../../redux/populations'
const StudentInfoCard = (props) => {
const { student, translate, showName } = props
const { student, translate, showName, updating } = props
const name = showName ? `${student.name}, ` : ''
const email = showName && student.email ? `${student.email}` : ''
const onRemove = () => {
props.history.push('/students')
props.resetStudent()
props.removeStudentSelection()
}
return (
<Card fluid>
<Card.Content>
@@ -30,15 +35,33 @@ const StudentInfoCard = (props) => {
className={styles.controlIcon}
onClick={onRemove}
/>
</Card.Header>
<Card.Meta>
<div className={styles.startDate}>
{`${translate('common.started')}: ${reformatDate(student.started, DISPLAY_DATE_FORMAT)}`}
</div>
<div className={styles.startDate}>
{email}
</div>
</Card.Meta>
<Card.Description>
{`${translate('common.credits')}: ${student.credits || 0}`}
<p style={{ fontSize: 14 }}>{`Updated at ${reformatDate(student.updatedAt, DISPLAY_DATE_FORMAT)}`}</p>
</Card.Description>
<div style={{ paddingTop: '4px' }}>
{updating ?
<Button disabled compact size="medium" labelPosition="left" onClick={() => props.updatePopulationStudents([student.studentNumber])} >
<Icon loading name="refresh" />
update student
</Button>
:
<Button compact floated="left" size="medium" labelPosition="left" onClick={() => props.updatePopulationStudents([student.studentNumber])} >
<Icon name="refresh" />
update student
</Button>
}
</div>
</Card.Content>
</Card>
)
@@ -49,12 +72,18 @@ StudentInfoCard.propTypes = {
translate: func.isRequired,
showName: bool.isRequired,
removeStudentSelection: func.isRequired,
resetStudent: func.isRequired
resetStudent: func.isRequired,
history: shape({}).isRequired,
updating: bool.isRequired,
updatePopulationStudents: func.isRequired
}
const mapStateToProps = state => ({
showName: state.settings.namesVisible
showName: state.settings.namesVisible,
updating: state.populations.updating
})
export default connect(mapStateToProps, { removeStudentSelection, resetStudent })(StudentInfoCard)
export default withRouter(connect(mapStateToProps, {
removeStudentSelection, resetStudent, updatePopulationStudents
})(StudentInfoCard))
+15 -8
View File
@@ -1,5 +1,6 @@
import React, { Component } from 'react'
import { func, string, arrayOf, object, bool } from 'prop-types'
import { withRouter } from 'react-router-dom'
import { func, string, arrayOf, object, bool, shape } from 'prop-types'
import { connect } from 'react-redux'
import { Search, Segment } from 'semantic-ui-react'
@@ -25,6 +26,7 @@ class StudentSearch extends Component {
componentDidMount() {
const { studentNumber } = this.props
if (studentNumber && containsOnlyNumbers(studentNumber)) {
this.setState({ isLoading: true })
this.props.getStudent(studentNumber).then(() => this.resetComponent())
@@ -49,6 +51,7 @@ class StudentSearch extends Component {
handleSearchSelect = (e, student) => {
const { studentNumber } = student
this.props.history.push(`/students/${studentNumber}`, { selected: studentNumber })
const studentObject = this.props.students.find(person =>
person.studentNumber === studentNumber)
const fetched = studentObject ? studentObject.fetched : false
@@ -62,6 +65,9 @@ class StudentSearch extends Component {
}
fetchStudentList = (searchStr) => {
if (searchStr.length < 4 || (Number(searchStr) && searchStr.length < 6)) {
return
}
this.props.setTimeout('fetch', () => {
this.setState({ isLoading: true })
}, 250)
@@ -88,7 +94,8 @@ class StudentSearch extends Component {
}
if (!showResults) {
return null
// so that the loading spinner doesn't go on top of the search box
return <div style={{ margin: 100 }} />
}
const headers = [
translate('common.studentNumber'),
@@ -112,11 +119,12 @@ class StudentSearch extends Component {
)
}
render() {
const { translate, selected } = this.props
const { translate, studentNumber } = this.props
if (selected !== null) {
if (studentNumber) {
return null
}
@@ -151,11 +159,10 @@ StudentSearch.propTypes = {
setTimeout: func.isRequired,
clearTimeout: func.isRequired,
showNames: bool.isRequired,
selected: string
history: shape({}).isRequired
}
StudentSearch.defaultProps = {
studentNumber: undefined,
selected: null
studentNumber: undefined
}
const formatStudentRows = makeFormatStudentRows()
@@ -176,4 +183,4 @@ const mapDispatchToProps = dispatch => ({
})
export default connect(mapStateToProps, mapDispatchToProps)(Timeout(StudentSearch))
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Timeout(StudentSearch)))
+23 -6
View File
@@ -1,17 +1,21 @@
import React, { PureComponent } from 'react'
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { func, shape, string } from 'prop-types'
import { getTranslate, getActiveLanguage } from 'react-localize-redux'
import { Segment, Header } from 'semantic-ui-react'
import { withRouter } from 'react-router-dom'
import { findStudents, getStudent, selectStudent } from '../../redux/students'
import StudentSearch from '../StudentSearch'
import StudentDetails from '../StudentDetails'
import StudentNameVisibilityToggle from '../StudentNameVisibilityToggle'
import LanguageChooser from '../LanguageChooser'
import { toggleStudentNameVisibility } from '../../redux/settings'
import sharedStyles from '../../styles/shared'
class StudentStatistics extends PureComponent {
class StudentStatistics extends Component { //eslint-disable-line
render() {
const { translate, match } = this.props
const { studentNumber } = match.params
@@ -21,10 +25,11 @@ class StudentStatistics extends PureComponent {
<Header className={sharedStyles.segmentTitle} size="large">
{translate('studentStatistics.header')}
</Header>
<LanguageChooser />
<StudentNameVisibilityToggle />
<Segment className={sharedStyles.contentSegment}>
<StudentSearch translate={translate} studentNumber={studentNumber} />
<StudentDetails translate={translate} />
<StudentDetails translate={translate} studentNumber={studentNumber} />
</Segment>
</div>
)
@@ -46,9 +51,21 @@ StudentStatistics.defaultProps = {
}
}
const mapStateToProps = ({ locale }) => ({
const mapStateToProps = ({ locale, students }) => ({
translate: getTranslate(locale),
currentLanguage: getActiveLanguage(locale).value
currentLanguage: getActiveLanguage(locale).value,
student: students.data.find(student =>
student.studentNumber === students.selected)
})
const mapDispatchToProps = dispatch => ({
toggleStudentNameVisibility,
findStudents: searchStr =>
dispatch(findStudents(searchStr)),
getStudent: studentNumber =>
dispatch(getStudent(studentNumber)),
selectStudent: studentNumber =>
dispatch(selectStudent(studentNumber))
})
export default connect(mapStateToProps, { toggleStudentNameVisibility })(StudentStatistics)
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(StudentStatistics))
@@ -0,0 +1,52 @@
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { getActiveLanguage, getTranslate } from 'react-localize-redux'
import { func, bool } from 'prop-types'
import { Header, Segment, Divider } from 'semantic-ui-react'
import PopulationSearchForm from '../PopulationSearchForm'
import PopulationSearchHistory from '../PopulationSearchHistory'
import SegmentDimmer from '../SegmentDimmer'
import sharedStyles from '../../styles/shared'
class StudyrightStatistics extends PureComponent {
static propTypes = {
translate: func.isRequired,
loading: bool.isRequired
}
renderPopulationSearch = () => {
const { translate, loading } = this.props
return (
<Segment>
<PopulationSearchForm />
<Divider />
<PopulationSearchHistory />
<SegmentDimmer translate={translate} isLoading={loading} />
</Segment>
)
}
render() {
const { translate } = this.props
return (
<div className={sharedStyles.segmentContainer}>
<Header className={sharedStyles.segmentTitle} size="large">{translate('studyrightStatistics.header')}</Header>
<Segment className={sharedStyles.contentSegment}>
{ this.renderPopulationSearch() }
</Segment>
</div>
)
}
}
const mapStateToProps = ({ locale, populations }) => ({
translate: getTranslate(locale),
currentLanguage: getActiveLanguage(locale).value,
populationFound: populations.length > 0,
loading: populations.pending
})
export default connect(mapStateToProps)(StudyrightStatistics)
@@ -0,0 +1,150 @@
import React, { Component } from 'react'
import { Tab, Form } from 'semantic-ui-react'
import { shape, string, arrayOf, func, number, oneOfType } from 'prop-types'
import TeacherStatisticsTable from '../TeacherStatisticsTable'
const CourseStatsTab = ({ statistics, options, doSelect, selected }) => (
<div>
<Form>
<Form.Dropdown
options={options}
placeholder="Select..."
selection
search
value={selected}
onChange={(_, { value }) => doSelect(value)}
/>
</Form>
{selected && <TeacherStatisticsTable statistics={statistics} onClickFn={null} />}
</div>
)
CourseStatsTab.propTypes = {
options: arrayOf(shape({})).isRequired,
statistics: arrayOf(shape({})).isRequired,
doSelect: func.isRequired,
selected: oneOfType([string, number])
}
CourseStatsTab.defaultProps = {
selected: null
}
class CoursesTab extends Component {
state={
selectedSemester: null,
selectedCourse: null,
semesterOptions: [],
courseOptions: []
}
componentDidMount() {
const { courses } = this.props
const semesterOptions = this.semesterOptions()
const courseOptions = this.courseOptions()
const courseWithMostCredits = Object.values(courses)
.reduce((c1, c2) => (c1.stats.credits > c2.stats.credits ? c1 : c2))
this.setState({
semesterOptions,
courseOptions,
selectedSemester: semesterOptions[0].value,
selectedCourse: courseWithMostCredits.id
})
}
setCourse = selectedCourse => this.setState({ selectedCourse })
setSemester = selectedSemester => this.setState({ selectedSemester })
getCourseStats(courseid) {
if (!courseid) {
return []
}
const { courses, semesters } = this.props
const course = courses[courseid]
return Object.entries(course.semesters).map(([semesterid, stats]) => ({
id: semesterid,
name: semesters[semesterid].name.fi,
...stats
}))
}
getSemesterStats(semesterid) {
if (!semesterid) {
return []
}
const { courses } = this.props
return Object.values(courses)
.filter(course => !!course.semesters[semesterid])
.map(({ id, name, semesters }) => ({
id,
name: name.fi,
...semesters[semesterid]
}))
}
semesterOptions() {
const { semesters } = this.props
return Object.values(semesters)
.map(({ name, id }) => ({
key: id,
value: id,
text: name.fi
}))
.sort((s1, s2) => s2.value - s1.value)
}
courseOptions() {
const courses = Object.values(this.props.courses)
return courses.map(({ name, id }) => ({
key: id,
value: id,
description: id,
text: name.fi
}))
}
dropdownOptions = () => ({
courses: this.courseOptions(),
semesters: this.semesterOptions()
})
render = () => {
const { selectedCourse, selectedSemester, courseOptions, semesterOptions } = this.state
return (
<Tab
menu={{ secondary: true, pointing: true }}
panes={[
{
menuItem: 'Semester',
render: () => (
<CourseStatsTab
options={semesterOptions}
doSelect={this.setSemester}
selected={selectedSemester}
statistics={this.getSemesterStats(selectedSemester)}
/>
)
},
{
menuItem: 'Course',
render: () => (
<CourseStatsTab
options={courseOptions}
statistics={this.getCourseStats(selectedCourse)}
doSelect={this.setCourse}
selected={selectedCourse}
/>
)
}
]}
/>
)
}
}
CoursesTab.propTypes = {
courses: shape({}).isRequired,
semesters: shape({}).isRequired
}
export default CoursesTab
+69
View File
@@ -0,0 +1,69 @@
import React, { Component } from 'react'
import { shape } from 'prop-types'
import _ from 'lodash'
import { Card, Tab, Icon } from 'semantic-ui-react'
import { withRouter } from 'react-router'
import TeacherStatisticsTable from '../TeacherStatisticsTable'
import CoursesTab from './CoursesTab'
import styles from '../PopulationQueryCard/populationQueryCard.css'
const statisticsTableTab = (title, statistics) => ({
menuItem: title,
render: () => <TeacherStatisticsTable statistics={statistics} onClickFn={null} />
})
const formatStatisticsForTable = (statistics) => {
if (!statistics) {
return []
}
return Object.values(statistics).map(({ name, stats, ...rest }) => ({
...rest,
...stats,
name: _.isString(name) ? name : name.fi
}))
}
class TeacherDetails extends Component {
state={}
render() {
const { teacher } = this.props
const { courses, years, semesters } = teacher.statistics
const panes = [
{
menuItem: 'Courses',
render: () => <CoursesTab courses={courses} semesters={semesters} />
},
statisticsTableTab('Semesters', formatStatisticsForTable(semesters)),
statisticsTableTab('Years', formatStatisticsForTable(years))
]
return (
<div>
<Card fluid className={styles.cardContainer}>
<Card.Content>
<Card.Header className={styles.cardHeader}>
{teacher.name}
<Icon
name="remove"
className={styles.controlIcon}
onClick={() => this.props.history.goBack()}
/>
</Card.Header>
<Card.Meta content={teacher.code} />
<Card.Meta content={teacher.id} />
</Card.Content>
</Card>
<Tab panes={panes} style={{ paddingTop: '0.5rem' }} />
</div>
)
}
}
TeacherDetails.propTypes = {
teacher: shape({}).isRequired,
history: shape({}).isRequired
}
export default withRouter(TeacherDetails)
@@ -0,0 +1,80 @@
import React, { Component } from 'react'
import { Segment, Form } from 'semantic-ui-react'
import { connect } from 'react-redux'
import { func, arrayOf, shape, string, any } from 'prop-types'
import { getTopTeachers } from '../../redux/teachersTop'
class LeaderForm extends Component {
state={
selectedyear: null,
selectedcategory: null
}
componentDidMount() {
const { year, category } = this.defaultValues()
if (year && category) {
this.updateAndSubmitForm({
selectedyear: year,
selectedcategory: category
})
}
}
updateAndSubmitForm = (args) => {
this.setState(args)
const { selectedyear, selectedcategory } = { ...this.state, ...args }
this.props.getTopTeachers(selectedyear, selectedcategory)
}
defaultValues = () => {
const { yearoptions, categoryoptions } = this.props
const [defaultyear = {}] = yearoptions
const [defaultcategory = {}] = categoryoptions
return {
year: defaultyear.value,
category: defaultcategory.value
}
}
handleChange = (e, { value, name }) => this.updateAndSubmitForm({ [name]: value })
render() {
const { yearoptions, categoryoptions } = this.props
return (
<Segment>
<Form>
<Form.Group widths="equal">
<Form.Dropdown
name="selectedyear"
label="Academic year"
placeholder="Academic year"
options={yearoptions}
selection
search
value={this.state.selectedyear}
onChange={this.handleChange}
/>
<Form.Dropdown
name="selectedcategory"
label="Category"
placeholder="Category"
options={categoryoptions}
selection
search
value={this.state.selectedcategory}
onChange={this.handleChange}
/>
</Form.Group>
</Form>
</Segment>
)
}
}
LeaderForm.propTypes = {
yearoptions: arrayOf(shape({})).isRequired,
categoryoptions: arrayOf(shape({ key: any, value: any, text: string })).isRequired,
getTopTeachers: func.isRequired
}
export default connect(null, { getTopTeachers })(LeaderForm)
@@ -0,0 +1,76 @@
import React, { Component } from 'react'
import { Segment, Message } from 'semantic-ui-react'
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
import { func, arrayOf, bool, shape, any, string } from 'prop-types'
import { getTopTeachersCategories } from '../../redux/teachersTopCategories'
import TeacherStatisticsTable from '../TeacherStatisticsTable'
import LeaderForm from './LeaderForm'
class TeacherLeaderBoard extends Component {
state={}
componentDidMount() {
this.props.getTopTeachersCategories()
}
render() {
const { statistics, isLoading, yearoptions, categoryoptions } = this.props
return (
<div>
{ isLoading
? <Segment basic loading={isLoading} />
: (
<div>
<Message
header="Teacher leaderboard"
content="Teachers who have produced the most credits from all departments."
/>
<LeaderForm yearoptions={yearoptions} categoryoptions={categoryoptions} />
<Segment>
<TeacherStatisticsTable
statistics={statistics}
onClickFn={e => this.props.history.push(`/teachers/${e.target.innerText}`)}
/>
</Segment>
</div>
)
}
</div>
)
}
}
TeacherLeaderBoard.propTypes = {
isLoading: bool.isRequired,
statistics: arrayOf(shape({})).isRequired,
yearoptions: arrayOf(shape({})).isRequired,
history: shape({}).isRequired,
getTopTeachersCategories: func.isRequired,
categoryoptions: arrayOf(shape({ key: any, text: string, value: any })).isRequired
}
const mapStateToProps = ({ teachersTop, teachersTopCategories }) => {
const { data } = teachersTop
const { pending, data: yearsAndCategories } = teachersTopCategories
const { years = {}, categories = {} } = yearsAndCategories
return {
isLoading: pending,
statistics: data,
yearoptions: Object.values(years)
.map(({ yearcode, yearname }) => ({
key: yearcode,
value: yearcode,
text: yearname
})).sort((y1, y2) => y2.value - y1.value),
categoryoptions: Object.values(categories).map(({ id, name }) => ({
key: id,
value: id,
text: name
}))
}
}
export default connect(mapStateToProps, {
getTopTeachersCategories
})(withRouter(TeacherLeaderBoard))
+55
View File
@@ -0,0 +1,55 @@
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import { shape, string, func, bool } from 'prop-types'
import { connect } from 'react-redux'
import { Segment } from 'semantic-ui-react'
import { getTeacher } from '../../redux/teachers'
import TeacherDetails from '../TeacherDetails'
class TeacherPage extends Component {
state={
initialized: false
}
async componentDidMount() {
const { teacher, teacherid } = this.props
if (!teacher) {
await this.props.getTeacher(teacherid)
}
this.setState({ initialized: true })
}
render() {
const { teacher, isLoading } = this.props
if (!this.state.initialized || isLoading) {
return <Segment basic loading={isLoading} />
} else if (!teacher) {
return <Redirect to="/teachers" />
}
return (
<TeacherDetails teacher={teacher} />
)
}
}
TeacherPage.propTypes = {
teacher: shape({}),
teacherid: string.isRequired,
getTeacher: func.isRequired,
isLoading: bool.isRequired
}
TeacherPage.defaultProps = {
teacher: undefined
}
const mapStateToProps = (state, props) => {
const { teacherid } = props
const { items, pending } = state.teachers
return {
teacher: items[teacherid],
isLoading: pending
}
}
export default connect(mapStateToProps, { getTeacher })(TeacherPage)
+92
View File
@@ -0,0 +1,92 @@
import React, { Component } from 'react'
import { Search, Segment, Message } from 'semantic-ui-react'
import { func, arrayOf, object, shape } from 'prop-types'
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
import styles from './teacherSearch.css'
import sharedStyles from '../../styles/shared'
import Timeout from '../Timeout'
import { findTeachers } from '../../redux/teachers'
import SearchResultTable from '../SearchResultTable'
const DEFAULT_STATE = {
searchterm: '',
displayResults: false
}
class TeacherSearch extends Component {
state=DEFAULT_STATE
resetComponent = () => {
this.setState(DEFAULT_STATE)
}
fetchTeachers = (searchterm) => {
this.props.setTimeout('fetch', () => {
}, 250)
this.props.findTeachers(searchterm).then(() => {
this.setState({ displayResults: true })
this.props.clearTimeout('fetch')
})
}
handleSearchChange = (e, { value }) => {
this.props.clearTimeout('search')
if (value.length > 0) {
this.setState({ searchterm: value })
this.props.setTimeout('search', () => {
this.fetchTeachers(value)
}, 250)
} else {
this.resetComponent()
}
}
render() {
return (
<div >
<Message
header="Teacher search"
content="Search for a teacher and click the search result to view their individual statistics from their entire career. "
/>
<div className={styles.searchContainer}>
<Search
className={styles.searchInput}
input={{ fluid: true }}
placeholder="Search by entering a username, id or name"
value={this.state.searchterm}
onSearchChange={this.handleSearchChange}
showNoResults={false}
/>
{ this.state.displayResults && (
<Segment className={sharedStyles.contentSegment}>
<SearchResultTable
headers={['Teacher ID', 'Username', 'Name']}
rows={this.props.teachers}
rowClickFn={(_, teacher) => this.props.history.push(`/teachers/${teacher.id}`)}
noResultText="No teachers matched your search"
/>
</Segment>
)}
</div>
</div>
)
}
}
TeacherSearch.propTypes = {
setTimeout: func.isRequired,
clearTimeout: func.isRequired,
teachers: arrayOf(object).isRequired,
findTeachers: func.isRequired,
history: shape({}).isRequired
}
const mapStateToProps = ({ teachers }) => {
const { list } = teachers
return {
teachers: list
}
}
export default withRouter(connect(mapStateToProps, { findTeachers })(Timeout(TeacherSearch)))
@@ -0,0 +1,13 @@
.searchContainer {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
}
.searchInput {
width: 100%;
max-width: 750px;
padding-top: 1rem;
padding-bottom: 1rem;
}
+150
View File
@@ -0,0 +1,150 @@
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
import { shape, func, arrayOf, bool } from 'prop-types'
import { Form, Segment, Dropdown, Button, Message } from 'semantic-ui-react'
import { getProviders } from '../../redux/providers'
import { getSemesters } from '../../redux/semesters'
import { getTeacherStatistics } from '../../redux/teacherStatistics'
import TeacherStatisticsTable from '../TeacherStatisticsTable'
const initial = {
semesterStart: null,
semesterEnd: null,
providers: [],
display: false
}
class TeacherStatistics extends Component {
state=initial
componentDidMount() {
this.props.getProviders()
this.props.getSemesters()
}
setStartSemester = (_, { value }) => {
const { semesterEnd } = this.state
this.setState({ semesterStart: value })
if (semesterEnd <= value) {
this.setState({
semesterEnd: value + 1
})
}
}
handleChange = (_, { name, value }) => {
this.setState({ [name]: value })
}
handleSubmit = async () => {
const { semesterStart, semesterEnd, providers } = this.state
await this.props.getTeacherStatistics(semesterStart, semesterEnd, providers)
this.setState({ display: true })
}
render() {
const { semesters, providers, statistics, pending } = this.props
const { display, semesterStart, semesterEnd } = this.state
const invalidQueryParams = this.state.providers.length === 0 || !semesterStart
return (
<div>
<Message header="Teacher statistics by course providers" content="Statistics for teachers that admitted credits during and between the given semesters for one of the given course providers." />
<Segment>
<Form loading={pending}>
<Form.Group widths="equal">
<Form.Dropdown
name="semesterStart"
placeholder="Semester"
label="Start semester"
selection
search
options={semesters}
value={semesterStart}
onChange={this.setStartSemester}
/>
<Form.Dropdown
name="semesterEnd"
placeholder="Semester"
label="End semester"
selection
search
options={semesters.filter(semester => semester.value > semesterStart)}
disabled={!semesterStart}
value={semesterEnd}
onChange={this.handleChange}
/>
</Form.Group>
<Form.Field>
<label>Course providers</label>
<Dropdown
name="providers"
placeholder="Providers"
multiple
selection
search
options={providers}
value={this.state.providers}
onChange={this.handleChange}
/>
</Form.Field>
<Button fluid content="Search" onClick={this.handleSubmit} disabled={invalidQueryParams} />
</Form>
</Segment>
{ display && !pending && (
<Segment>
<TeacherStatisticsTable statistics={statistics} onClickFn={e => this.props.history.push(`/teachers/${e.target.innerText}`)} />
</Segment>
)}
</div>
)
}
}
TeacherStatistics.propTypes = {
providers: arrayOf(shape({})).isRequired,
semesters: arrayOf(shape({})).isRequired,
statistics: arrayOf(shape({})).isRequired,
getSemesters: func.isRequired,
getProviders: func.isRequired,
getTeacherStatistics: func.isRequired,
pending: bool.isRequired,
history: shape({}).isRequired
}
const mapStateToProps = (state) => {
const { providers, teacherStatistics } = state
const { semesters } = state.semesters.data
const providerOptions = providers.data.map(p => ({
key: p.providercode,
value: p.providercode,
text: p.name.fi || p.name.en
}))
const semesterOptions = !semesters
? []
: Object.values(semesters).reverse().map(({ semestercode, name }, idx) => ({
key: idx,
value: semestercode,
text: name.en
}))
const statistics = Object.values(teacherStatistics.data).map(teacher => ({
id: teacher.id,
name: teacher.name,
credits: teacher.stats.credits,
passed: teacher.stats.passed,
failed: teacher.stats.failed
}))
return {
providers: providerOptions,
semesters: semesterOptions,
statistics,
pending: teacherStatistics.pending,
error: teacherStatistics.error
}
}
export default connect(mapStateToProps, {
getProviders,
getSemesters,
getTeacherStatistics
})(withRouter(TeacherStatistics))
@@ -0,0 +1,96 @@
import React, { Component } from 'react'
import { Table, Segment } from 'semantic-ui-react'
import { shape, arrayOf, any, string, number } from 'prop-types'
import _ from 'lodash'
const calculatePassrate = (pass, fail) => (100 * pass) / (pass + fail)
class TeacherStatisticsTable extends Component {
state={
selected: 'credits',
direction: 'descending'
}
handleSort = column => () => {
const { selected, direction } = this.state
if (selected === column) {
this.setState({
direction: direction === 'ascending' ? 'descending' : 'ascending'
})
} else {
this.setState({
selected: column,
direction: 'descending'
})
}
}
sortStatistics = (statistics) => {
const { selected, direction } = this.state
const formatted = statistics.map(stat => ({
...stat,
passrate: calculatePassrate(stat.passed, stat.failed)
}))
const sorted = _.sortBy(formatted, selected)
return direction === 'ascending' ? sorted : sorted.reverse()
}
render() {
const { statistics } = this.props
const { selected, direction } = this.state
const sortDirection = name => (selected === name ? direction : null)
return (statistics.length === 0 ? <Segment content="No statistics found for the given query." /> :
<Table structured celled textAlign="center" sortable>
<Table.Header>
<Table.Row>
<Table.HeaderCell
content="ID"
onClick={this.handleSort('id')}
sorted={sortDirection('id')}
/>
<Table.HeaderCell
content="Name"
textAlign="left"
onClick={this.handleSort('name')}
sorted={sortDirection('name')}
/>
<Table.HeaderCell
content="Credits"
onClick={this.handleSort('credits')}
sorted={sortDirection('credits')}
/>
<Table.HeaderCell
content="Passed"
onClick={this.handleSort('passrate')}
sorted={sortDirection('passrate')}
/>
</Table.Row>
</Table.Header>
<Table.Body>
{ this.sortStatistics(statistics).map(({ id, name, credits, passrate }) => (
<Table.Row key={id}>
<Table.Cell content={id} width={1} onClick={this.props.onClickFn} /> {/* eslint-disable-line*/}
<Table.Cell content={name} textAlign="left" />
<Table.Cell content={credits} width={2} />
<Table.Cell content={`${parseFloat(passrate).toFixed(2)} %`} width={2} />
</Table.Row>
))}
</Table.Body>
</Table>
)
}
}
TeacherStatisticsTable.propTypes = {
statistics: arrayOf(shape({
id: any,
name: string,
credits: any,
failed: number,
passed: number
})).isRequired
}
export default TeacherStatisticsTable
+65
View File
@@ -0,0 +1,65 @@
import React, { Component } from 'react'
import { shape, string } from 'prop-types'
import { Header, Segment, Tab } from 'semantic-ui-react'
import { withRouter } from 'react-router-dom'
import sharedStyles from '../../styles/shared'
import TeacherSearch from '../TeacherSearch'
import TeacherPage from '../TeacherPage'
import TeacherStatistics from '../TeacherStatistics'
import TeacherLeaderBoard from '../TeacherLeaderBoard'
const pane = (title, Content, icon) => ({
menuItem: { key: title, content: title, icon },
render: () => (
<Tab.Pane style={{ borderWidth: '0' }}>
<Content />
</Tab.Pane>
)
})
const TeachersTabs = () => (
<Tab
menu={{ attached: false, borderless: true, tabular: true }}
panes={[
pane('Statistics', TeacherStatistics, 'table'),
pane('Leaderboard', TeacherLeaderBoard, 'trophy'),
pane('Search', TeacherSearch, 'user')
]}
/>
)
class Teachers extends Component {
state = {}
render() {
const { match } = this.props
const { teacherid } = match.params
return (
<div className={sharedStyles.segmentContainer}>
<Header className={sharedStyles.segmentTitle} size="large" content="Teacher statistics" />
<Segment className={sharedStyles.contentSegment}>
{ teacherid
? <TeacherPage teacherid={teacherid} />
: <TeachersTabs />
}
</Segment>
</div>
)
}
}
Teachers.propTypes = {
match: shape({
params: shape({
teacherid: string
})
})
}
Teachers.defaultProps = {
match: {
params: { teacherid: undefined }
}
}
export default withRouter(Teachers)
+129
View File
@@ -0,0 +1,129 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Container, Header, Table, Icon } from 'semantic-ui-react'
import { callApi } from '../../apiConnection'
import styles from '../StudentInfoCard/studentInfoCard.css'
class UsageStatistics extends Component { //eslint-disable-line
state = null
componentWillMount() {
callApi('/usage').then(({ data }) => {
this.setState(data)
})
}
renderStats() {
if (this.state === null) {
return null
}
const byCount = (x, y) => y.count - x.count
const users = Object.keys(this.state.byUser).map(user => ({
name: user,
count: this.state.byUser[user].length
}))
const endpoints = Object.keys(this.state.byEndpoint).map(endpoint => ({
name: endpoint,
count: this.state.byEndpoint[endpoint].length
}))
if (this.state.user) {
const entries = this.state.all
.filter(e => e.username === this.state.user)
.sort((e1, e2) => e2.time - e1.time)
const usersName = entries[0].name
const toTime = (stamp) => {
const zeroed = v => (v < 10 ? `0${v}` : v)
const date = new Date(stamp * 1000)
return `${date.getFullYear()}${zeroed(date.getUTCMonth() + 1)}${zeroed(date.getDate())} ${zeroed(date.getHours())}:${zeroed(date.getMinutes())}:${zeroed(date.getSeconds())}`
}
return (
<div>
<h3>
{usersName} ({this.state.user})
<Icon
onClick={() => this.setState({ user: null })}
name="remove"
className={styles.controlIcon}
/>
</h3>
<Table celled>
<Table.Body>
{entries.map(entry => (
<Table.Row key={entry.id}>
<Table.Cell>
{entry.method}
</Table.Cell>
<Table.Cell>
{entry.URL}
</Table.Cell>
<Table.Cell>
{toTime(entry.time)}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</div>
)
}
return (
<div>
<h3>by user</h3>
<Table celled>
<Table.Body>
{users.sort(byCount).map(user => (
<Table.Row key={user.name}>
<Table.Cell onClick={() => this.setState({ user: user.name })}>
{user.name}
</Table.Cell>
<Table.Cell>
{user.count}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
<h3>by endpoint</h3>
<Table celled>
<Table.Body>
{endpoints.sort(byCount).map(endpoint => (
<Table.Row key={endpoint}>
<Table.Cell>
{endpoint.name}
</Table.Cell>
<Table.Cell>
{endpoint.count}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</div>
)
}
render() {
return (
<div>
<Container text style={{ paddingTop: 50 }}>
<Header as="h1" textAlign="center">Usage statistics</Header>
{this.renderStats()}
</Container>
</div>
)
}
}
export default connect()(UsageStatistics)
+277
View File
@@ -0,0 +1,277 @@
import React, { Component } from 'react'
import { Button, Card, Divider, Image, Form, List, Icon } from 'semantic-ui-react'
import { connect } from 'react-redux'
import _ from 'lodash'
import { string, number, shape, bool, arrayOf, func } from 'prop-types'
import { textAndDescriptionSearch } from '../../common'
import LanguageChooser from '../LanguageChooser'
import { toggleCzar, addUserUnits, removeUserUnit } from '../../redux/users'
import { getStudyrightElements } from '../../redux/studyrightElements'
const formatToDropdown = elements => Object.values(elements).map(e => ({
associations: {
20: e.associations[20] ? Object.keys(e.associations[20]) : []
},
key: e.code,
value: e.code,
description: e.code,
text: e.name.fi || e.name.en || e.name.sv
}))
class UserPage extends Component {
state={
degree: undefined,
programme: undefined,
specializations: []
}
componentDidMount() {
const { studyrightElements } = this.props
if (Object.keys(studyrightElements).length === 0) {
this.props.getStudyrightElements()
}
}
getDisabledUnits = (units, enabled) => {
const enabledIds = new Set(enabled.map(element => element.code))
return units.filter(u => !enabledIds.has(u.id))
}
handleChange = (e, { name, value }) => this.setState({ [name]: value })
enableAccessRightToUser = userid => async () => {
const { degree, programme, specializations } = this.state
const codes = [degree, programme, ...specializations].filter(e => !!e)
await this.props.addUserUnits(userid, codes)
this.setState({
degree: undefined,
programme: undefined,
specializations: []
})
}
handleCoronation = user => async () => {
await this.props.toggleCzar(user.id)
}
removeAccess = (uid, unit) => () => this.props.removeUserUnit(uid, unit)
degreeOptions = () => {
const { 10: deg } = this.props.studyrightElements
const degrees = !deg ? [] : formatToDropdown(deg)
return degrees
}
programmeOptions = () => {
const { 10: deg, 20: prog } = this.props.studyrightElements
const { degree } = this.state
const all = !prog ? [] : formatToDropdown(prog)
if (deg && degree) {
const data = deg[degree]
const assocs = data.associations[20] || {}
const filtered = all.filter(({ key }) => !!assocs[key])
return filtered
}
return all
}
specializationOptions = () => {
const { 20: prog, 30: specs } = this.props.studyrightElements
const { programme } = this.state
const all = !specs ? [] : formatToDropdown(specs)
if (prog && programme) {
const data = prog[programme]
const assocs = data.associations[30] || {}
const filtered = all.filter(({ key }) => !!assocs[key])
return filtered
}
return all
}
studyelementOptions = () => ({
degrees: this.degreeOptions(),
programmes: this.programmeOptions(),
specializations: this.specializationOptions()
})
renderUnitList = (elementdetails, user) => {
const { language } = this.props
const nameInLanguage = element =>
element.name[language]
|| element.name.fi
|| element.name.en
|| element.name.sv
const byCode = (a, b) => {
const codeA = a.type === 30 && a.associations ? `${a.associations[0]}${nameInLanguage(a)}` : a.code
const codeB = b.type === 30 && b.associations ? `${b.associations[0]}${nameInLanguage(b)}` : b.code
return codeA < codeB ? -1 : 1
}
if (!elementdetails) return null
return (
<List divided>
{elementdetails.sort(byCode).map(element => (
<List.Item key={element.code}>
<List.Content floated="right">
<Button basic negative floated="right" onClick={this.removeAccess(user.id, element.code)} content="Remove" size="tiny" />
</List.Content>
<List.Content>
{element.type === 30 ? <Icon name="minus" /> : null} {`${nameInLanguage(element)} (${element.code})`}
</List.Content>
</List.Item>
))}
</List>
)
}
render() {
const { user, pending, studyrightElements } = this.props
// ugly trick to add associations to study tracks, should be moved to backend
if (studyrightElements[30]) {
const programmes = user.elementdetails.filter(e => e.type === 20).map(e => e.code)
user.elementdetails = user.elementdetails.map((element) => {
const e = Object.assign(element)
e.associations = studyrightElements && (element.type === 30) ?
_.intersection(
programmes,
Object.keys(studyrightElements[30][element.code].associations[20])
) :
[]
return e
})
}
const options = this.studyelementOptions()
return (
<div>
<Button icon="arrow circle left" content="Back" onClick={this.props.goBack} />
<LanguageChooser />
<Divider />
<Card.Group>
<Card fluid>
<Card.Content>
<Card.Header>
<Image onClick={this.handleCoronation(user)} src={user.czar ? 'https://i.pinimg.com/originals/06/7a/20/067a20e4ae1edcee790601ce9b9927df.jpg' : 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS6uJPJLxePjb5u1omdG2kOLfE0BwNjvvJ9accK922xSVwKlR8_'} avatar />
{user.full_name}
</Card.Header>
<Card.Meta content={user.czar ? `tsaari ${user.username}` : `${user.username}`} />
<Card.Meta content={user.email} />
<Card.Description>
{`Access to oodikone: ${user.is_enabled ? 'En' : 'Dis'}abled`} <br />
</Card.Description>
<Divider />
</Card.Content>
</Card>
<Card fluid>
<Card.Content>
<Card.Header content="Enable access" />
<Card.Description>
<Form loading={pending}>
<Form.Dropdown
name="degree"
label="Degree (optional)"
placeholder="Select specialization"
options={options.degrees}
value={this.state.degree}
onChange={this.handleChange}
fluid
search={textAndDescriptionSearch}
selection
/>
<Divider />
<Form.Group widths="equal">
<Form.Dropdown
name="programme"
label="Study programme"
placeholder="Select unit"
options={options.programmes}
value={this.state.programmes}
onChange={this.handleChange}
fluid
search={textAndDescriptionSearch}
selection
/>
<Form.Dropdown
label="Specialization"
name="specializations"
placeholder="Select specialization"
options={options.specializations}
value={this.state.specializations}
onChange={this.handleChange}
fluid
search={textAndDescriptionSearch}
multiple
selection
/>
</Form.Group>
<Button
basic
fluid
positive
content="Enable"
onClick={this.enableAccessRightToUser(user.id)}
/>
</Form>
</Card.Description>
</Card.Content>
</Card>
<Card fluid>
<Card.Content>
<Card.Header content="Access rights" />
<Card.Description>
{user.czar ?
<p style={{
fontSize: '34px',
fontFamily: 'Comic Sans',
color: 'darkred',
border: '1px'
}}
>everything!
</p> : this.renderUnitList(user.elementdetails, user)}
</Card.Description>
</Card.Content>
</Card>
</Card.Group>
</div >
)
}
}
UserPage.propTypes = {
user: shape({
id: string,
full_name: string,
is_enabled: bool,
elementdetails: arrayOf(shape({
code: string,
name: shape({}),
type: number
}))
}).isRequired,
toggleCzar: func.isRequired,
addUserUnits: func.isRequired,
removeUserUnit: func.isRequired,
language: string.isRequired,
goBack: func.isRequired,
getStudyrightElements: func.isRequired,
studyrightElements: shape({}).isRequired,
pending: bool.isRequired
}
const mapStateToProps = state => ({
language: state.settings.language,
units: state.units.data,
studyrightElements: state.studyrightElements.data,
pending: state.studyrightElements.pending
})
export default connect(mapStateToProps, {
toggleCzar,
addUserUnits,
removeUserUnit,
getStudyrightElements
})(UserPage)
+58
View File
@@ -0,0 +1,58 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Container, Header, Image, Divider } from 'semantic-ui-react'
import { images } from '../../common'
class WelcomePage extends Component { //eslint-disable-line
render() {
return (
<div>
<Container text style={{ paddingTop: 50 }}>
<Header as="h1" textAlign="center">oodikone</Header>
<h3 style={{ textAlign: 'center' }}>
a tool for explorative research on student data
</h3>
<Divider section />
<h4>
Population Statistics
</h4>
<p>
Query a student population specified by starting year and studyright.
Oodikone will give you study statistics and visualizations of the population,
which you can interactively filter and explore.
</p>
<Divider section />
<h4>
Student Statistics
</h4>
<p>
Shows detailed information and visualizations of a queried student.
</p>
<Divider section />
<h4>
Course Statistics
</h4>
<p>
Shows student results of a course over specified years.
</p>
<Divider section />
{/* <h4>
Settings
</h4>
<p>
Here you can modify settings of oodikone.
For example you can create custom course code correspondences.
</p>
<Divider section /> */}
</Container>
<Image src={images.toskaLogo} size="medium" centered style={{ bottom: 0 }} />
</div>
)
}
}
export default connect()(WelcomePage)
+109 -5
View File
@@ -36,18 +36,29 @@ import {
chart35
} from '../styles/variables'
/*
lightgreen: '#90EE90',
chartblue: '#178aa5',
chartdarkg: '#367a1c',
chartlgreen: '#80e061',
chartdarkred: '#a03530',
chartlred: '#e2726d'
*/
import { chartblue, red, green, chartdarkg, chartlgreen, chartdarkred, chartlred } from '../styles/variables/colors'
export const routes = {
index: { route: '/', translateId: 'departmentSuccess' },
index: { route: '/' },
populations: { route: '/populations', translateId: 'populations' },
courseInstances: { route: '/courseinstances', translateId: 'courseinstances' },
students: { route: '/students/:studentNumber?', translateId: 'students' },
courseStatistics: { route: '/coursestatistics', translateId: 'courseStatistics' },
users: { route: '/users', translateId: 'users', admin: true }
teachers: { route: '/teachers/:teacherid?', translateId: 'teachers', czar: true },
users: { route: '/users/:userid?', translateId: 'users', admin: true },
settings: { route: '/settings', translateId: 'settings', admin: true },
usage: { route: '/usage', translateId: 'usage', admin: true }
}
const assumeBasename = () => {
const POSSIBLE_BASENAMES = ['staging']
const POSSIBLE_BASENAMES = ['staging', 'testing']
const haystack = window.location.pathname.split('/')
const needle = haystack.find(path => POSSIBLE_BASENAMES.includes(path))
return needle ? `/${needle}/` : '/'
@@ -63,7 +74,100 @@ export const DEFAULT_LANG = 'en'
export const DISPLAY_DATE_FORMAT = 'DD.MM.YYYY'
export const API_DATE_FORMAT = 'YYYY.MM.DD'
export const TOKEN_NAME = window.location.pathname.includes('/staging') ? 'staging_token' : 'token'
export const TOKEN_NAME = window.location.pathname.includes('/staging') ? 'staging_token' : window.location.pathname.includes('/testing') ? 'testing_token' : 'token' //eslint-disable-line
export const passRateCumGraphOptions = (categories, max) => ({
chart: {
type: 'column'
},
colors: [chartblue, green, red],
title: {
text: 'Pass rate chart'
},
xAxis: {
categories
},
yAxis: {
allowDecimals: false,
title: {
text: 'Number of Students'
},
max,
floor: -max
},
plotOptions: {
column: {
stacking: 'normal',
borderRadius: 3
}
}
})
export const passRateStudGraphOptions = (categories, max) => ({
chart: {
type: 'column'
},
colors: [chartblue, chartlgreen, chartdarkg, chartlred, chartdarkred],
title: {
text: 'Pass rate chart'
},
xAxis: {
categories
},
yAxis: {
allowDecimals: false,
title: {
text: 'Number of Students'
},
max,
floor: -max
},
plotOptions: {
column: {
stacking: 'normal',
borderRadius: 1
}
}
})
export const gradeGraphOptions = (categories, max) => ({
chart: {
type: 'column'
},
colors: [red, chartblue, chartblue, chartblue, chartblue, chartblue, chartblue],
title: {
text: 'Grades'
},
xAxis: {
categories
},
yAxis: {
allowDecimals: false,
title: {
text: 'Number of Students'
},
max,
floor: -max
},
plotOptions: {
column: {
stacking: 'normal',
borderRadius: 2
}
}
})
export const CHART_COLORS = [
chart1,
+2 -2
View File
@@ -19,12 +19,12 @@ export const displayableDateType = shape({
export const studentDetailsType = shape({
studentNumber: string.isRequired,
started: string.isRequired,
started: string,
credits: number
})
export const dropdownType = shape({
key: oneOfType([string, number]).isRequired,
value: oneOfType([string, number]).isRequired,
text: string.isRequired
text: shape({}).isRequired
})
+250 -77
View File
@@ -1,100 +1,273 @@
{
"common": {
"ok": ["Ok"],
"cancel": ["Cancel"],
"example" : ["react-localize-redux example text"],
"loading": ["Loading"],
"noResults": ["No search results or search term is not accurate enough"],
"studentNumber": ["Student number"],
"started": ["Started"],
"credits": ["Credits"],
"date": ["Date"],
"course": ["Course"],
"grade": ["Grade"],
"passed": ["Passed"]
"ok": [
"Ok"
],
"cancel": [
"Cancel"
],
"example": [
"react-localize-redux example text"
],
"loading": [
"Loading"
],
"noResults": [
"No search results or search term is not accurate enough"
],
"studentNumber": [
"Student number"
],
"started": [
"Started"
],
"credits": [
"Credits"
],
"date": [
"Date"
],
"course": [
"Course"
],
"grade": [
"Grade"
],
"passed": [
"Passed"
]
},
"error": {
"connectionError" : ["Connection error"],
"errorInResponseTo" : ["Error for API call"]
"connectionError": [
"Connection error"
],
"errorInResponseTo": [
"Error for API call"
]
},
"graphs": {
"referenceCredits": ["Reference credits"]
"referenceCredits": [
"Reference credits"
]
},
"navigationBar" : {
"departmentSuccess" : ["Department success"],
"populations" : ["Population statistics"],
"students" : ["Student statistics"],
"courseinstances" : ["Course Instances"],
"teachers" : ["Teachers"],
"courseStatistics" : ["Course statistics"],
"users" : ["Users"],
"logout": ["Logout"]
"navigationBar": {
"departmentSuccess": [
"Department success"
],
"welcome": [
"Starting page"
],
"populations": [
"Population statistics"
],
"students": [
"Student statistics"
],
"courseinstances": [
"Course Instances"
],
"teachers": [
"Teachers"
],
"courseStatistics": [
"Course statistics"
],
"users": [
"Users"
],
"settings": [
"Settings"
],
"logout": [
"Logout"
],
"usage" : [
"Usage"
]
},
"populationCourses": {
"header": ["Courses of Population"],
"course": ["Course"],
"students": ["Students"],
"passed": ["Passed"],
"failed": ["Failed"],
"passedAfterRetry": ["after retry"],
"failedManyTimes": ["many times"],
"attempts": ["Attempts"],
"percentageOfPopulation": ["percentage of population"],
"name": ["name"],
"code": ["code"],
"number": ["n"],
"limit": ["Limit to courses where student number at least"],
"percentage": ["percentage"],
"perStudent": ["per student"],
"attempted": ["attempted"]
"header": [
"Courses of Population"
],
"course": [
"Course"
],
"students": [
"Students"
],
"passed": [
"Passed"
],
"failed": [
"Failed"
],
"passedAfterRetry": [
"after retry"
],
"failedManyTimes": [
"many times"
],
"attempts": [
"Attempts"
],
"percentageOfPopulation": [
"percentage of population"
],
"name": [
"name"
],
"code": [
"code"
],
"number": [
"n"
],
"limit": [
"Limit to courses where student number at least"
],
"percentage": [
"percentage"
],
"perStudent": [
"per student"
],
"attempted": [
"attempted"
]
},
"populationStatistics" : {
"header": ["Population statistics"],
"months": ["Months from beginning"],
"graphSegmentHeader" : ["Credit accumulation"],
"creditStatisticsHeader" : ["Credit statistics"],
"sampleId": ["Id"],
"sampleSize" : ["Sample size: ${ amount } students"],
"allPopulations" : ["All populations"],
"alreadyFetched": ["Selected population already in analysis"],
"selectValidYear": ["Select valid year."],
"addPopulation": ["See population"],
"clearPopulations": ["Remove population"],
"searchTitle": ["Search for population"],
"foundTitle": ["Population"],
"studyRights": ["Study rights"],
"enrollmentYear": ["Enrollment year"],
"semester" : ["Semester"],
"SPRING" : ["Spring"],
"FALL" : ["Fall"],
"selectStudyRights": ["Select study rights"],
"noSelectableStudyRights": ["No selectable study rights"]
"welcome": {
"header": [
"Welcome Page"
]
},
"populationStatistics": {
"header": [
"Population statistics"
],
"months": [
"End of search"
],
"graphSegmentHeader": [
"Credit accumulation"
],
"creditStatisticsHeader": [
"Credit statistics"
],
"sampleId": [
"Id"
],
"sampleSize": [
"Sample size: ${ amount } students"
],
"allPopulations": [
"All populations"
],
"alreadyFetched": [
"Selected population already in analysis"
],
"selectValidYear": [
"Select valid year."
],
"addPopulation": [
"See population"
],
"clearPopulations": [
"Remove population"
],
"searchTitle": [
"Search for population"
],
"foundTitle": [
"Population"
],
"studyRights": [
"Study rights"
],
"enrollmentYear": [
"Enrollment year"
],
"semester": [
"Semester"
],
"SPRING": [
"Spring"
],
"FALL": [
"Fall"
],
"selectStudyRights": [
"Select study rights"
],
"noSelectableStudyRights": [
"No selectable study rights"
],
"emptyQueryResult": [
"No statistics found for the given query. "
]
},
"departmentSuccess": {
"header": ["Department Success"],
"chartTitle": ["Average credit gains after 13 months for BSc students starting"]
"header": [
"Department Success"
],
"chartTitle": [
"Average credit gains after 13 months for BSc students starting"
]
},
"studentStatistics": {
"header" : ["Student statistics"],
"searchPlaceholder": ["Search with a student number or name (surename firstname)"],
"chartTitle" : ["Credit Accumulation"]
"header": [
"Student statistics"
],
"searchPlaceholder": [
"Search with a student number or name (surname firstname)"
],
"chartTitle": [
"Credit Accumulation"
]
},
"studyrightStatistis": {
"header": [
"Studyright statistics"
],
"navigationBar": [
"Studyright statistics"
]
},
"courseStatistics": {
"header" : ["Student statistics"],
"searchPlaceholder": ["Search with a student number or name (surename firstname)"],
"chartTitle" : ["Credit Accumulation"]
"header": [
"Student statistics"
],
"searchPlaceholder": [
"Search with a student number or name (surname firstname)"
],
"chartTitle": [
"Credit Accumulation"
]
},
"tags": {
"addTag": ["Add tag"],
"searchAndAdd": ["Search and select a new tag"],
"confirmDelete" : ["Are you sure you want to remove tag"]
"addTag": [
"Add tag"
],
"searchAndAdd": [
"Search and select a new tag"
],
"confirmDelete": [
"Are you sure you want to remove tag"
]
},
"studyrights": {
"studyright": ["Studyright"],
"searchPlaceholder": ["Search with a studyright name"]
"studyright": [
"Studyright"
],
"searchPlaceholder": [
"Search with a studyright name"
]
},
"teacherStatistics": {
"header" : ["Teacher statistics"],
"searchTitle" : ["Search for teachers"]
"header": [
"Teacher statistics"
],
"searchTitle": [
"Search for teachers"
]
}
}
}
+222 -12
View File
@@ -1,24 +1,234 @@
import uuidv4 from 'uuid/v4'
import { getStudentTotalCredits } from '../common'
export const creditsLessThan = credit =>
({
export const creditsLessThan = (params) => {
const { credit } = params
return ({
id: uuidv4(),
type: 'creditsLessThan',
params: [credit],
type: 'CreditsLessThan',
params: {
credit
},
filter: (student) => {
const creditsOfStudent = student.courses
.filter(c => c.passed)
.reduce((s, c) => s + c.credits, 0)
const creditsOfStudent = getStudentTotalCredits(student)
return credit > creditsOfStudent
}
})
}
export const sexFilter = (params) => {
const { gender } = params
return ({
id: uuidv4(),
type: 'SexFilter',
params: {
gender
},
filter: student => student.gender === gender
})
}
export const matriculationFilter = (params) => {
const { matriculationexamination } = params
return ({
id: uuidv4(),
type: 'HasMatriculation',
params: {
matriculationexamination
},
filter: student => student.matriculationexamination === matriculationexamination
})
}
export const creditsAtLeast = (params) => {
const { credit } = params
return ({
id: uuidv4(),
type: 'CreditsAtLeast',
params: {
credit
},
filter: (student) => {
const creditsOfStudent = getStudentTotalCredits(student)
return credit <= creditsOfStudent
}
})
}
export const enrollmentStatus = (params) => {
const { semesters, enrolled } = params
return ({
id: uuidv4(),
type: 'EnrollmentStatus',
params: {
semesters,
enrolled
},
filter: student => student.semesterenrollments.filter(({ semestercode }) =>
semesters.includes(semestercode)).map(({ enrollmenttype }) =>
enrollmenttype === enrolled).every(b => b === true)
})
}
export const transferFilter = (params) => {
const { source, target } = params
return ({
id: uuidv4(),
type: 'TransferFilter',
params: {
source,
target
},
filter: student => student.transfers.map(transfer =>
(source === 'anywhere' || transfer.source.code === source) && (target === 'anywhere' || transfer.target.code === target)).some(b => b === true)
export const placeHolder = () => () =>
})
}
export const startingThisSemester = (params) => {
const { starting } = params
return ({
id: uuidv4(),
type: 'StartingThisSemester',
params: {
starting
},
filter: student =>
student.starting === starting
})
}
export const courseParticipation = ({ field, course }) =>
({
id: uuidv4(),
type: 'placeHolder',
params: [],
filter: () =>
null
type: 'CourseParticipation',
params: {
field,
course
},
studentsOfSelectedField: course.students[field],
filter: student =>
course.students[field][student.studentNumber] === true
})
export const extentGraduated = (params) => {
const { extentcode, graduated, complemented } = params
return ({
id: uuidv4(),
type: 'ExtentGraduated',
params: {
extentcode,
graduated,
complemented
},
filter: (student) => {
if (complemented === 'true' && graduated === 'either') {
return !student.studyrights.map(sr =>
sr.extentcode).includes(extentcode)
} else if (complemented === 'true' && graduated === 'grad') {
return !student.studyrights.filter(sr =>
sr.extentcode === extentcode && sr.graduated).map(sr =>
sr.extentcode).includes(extentcode)
} else if (complemented === 'false' && graduated === 'either') {
return student.studyrights.map(sr => sr.extentcode).includes(extentcode)
}
return student.studyrights.filter(sr =>
sr.extentcode === extentcode && sr.graduated).map(sr =>
sr.extentcode).includes(extentcode)
}
})
}
export const courseParticipationNTimes = (params) => {
const { amount, course } = params
return ({
id: uuidv4(),
type: 'CourseParticipationNTimes',
params: {
amount,
course
},
filter: student => student.courses.filter(cr => cr.course.code === course).length >= amount
})
}
export const canceledStudyright = (params) => {
const { studyrights, cancel } = params
return ({
id: uuidv4(),
type: 'CanceledStudyright',
params: {
studyrights,
cancel
},
filter: (student) => {
if (cancel === 'true') {
return student.studyrights.filter(sr =>
sr.studyrightElements.some(e => studyrights.includes(e.code))).every(sr => sr.canceldate)
}
return !student.studyrights.filter(sr =>
sr.studyrightElements.some(e => studyrights.includes(e.code))).every(sr => sr.canceldate)
}
})
}
export const priorityStudyright = (params) => {
const { prioritycode, degree, programme } = params
return ({
id: uuidv4(),
type: 'PriorityStudyright',
params: {
prioritycode,
degree,
programme
},
filter: student =>
student.studyrights.some((sr) => {
if (sr.prioritycode === prioritycode) {
const elements = sr.studyrightElements.map(e => e.code)
let bools = []
if (degree) {
bools = bools.concat(elements.includes(degree) || degree === 'anyDegree')
}
if (programme) {
bools = bools.concat(elements.includes(programme) || programme === 'anyProgramme')
}
return bools.length > 0 ? bools.every(b => b === true) : false
}
return false
})
})
}
export const presetFilter = preset => ({
id: preset.id,
type: 'Preset',
name: preset.name,
description: preset.description,
filters: preset.filters,
filter: student => preset.filters.map(f => f.filter(student)).every(b => b === true)
})
const typeList = {
CreditsLessThan: creditsLessThan,
CreditsAtLeast: creditsAtLeast,
HasMatriculation: matriculationFilter,
SexFilter: sexFilter,
CourseParticipation: courseParticipation,
StartingThisSemester: startingThisSemester,
ExtentGraduated: extentGraduated,
EnrollmentStatus: enrollmentStatus,
TransferFilter: transferFilter,
CourseParticipationNTimes: courseParticipationNTimes,
CanceledStudyright: canceledStudyright,
Preset: presetFilter,
PriorityStudyright: priorityStudyright
}
export const getFilterFunction = (type, params, populationCourses) => {
switch (type) {
case 'CourseParticipation':
return courseParticipation({
field: params.field,
course: populationCourses.coursestatistics.filter(c =>
c.course.code === params.course.course.code)[0]
})
default:
return typeList[type](params)
}
}
+32
View File
@@ -0,0 +1,32 @@
const formreducer = (prefix, initvals = {}) => {
const initial = { ...initvals }
const actions = {
CLEAR: `${prefix}_CLEAR`,
SET: `${prefix}_SET_VALUE`
}
const reducer = (state = { ...initial }, action) => {
switch (action.type) {
case actions.SET:
return {
...state,
[action.name]: action.value
}
case actions.CLEAR:
return { ...initial }
default:
return state
}
}
const setValue = (name, value) => ({ type: actions.SET, name, value })
const clear = () => ({ type: actions.CLEAR })
return {
reducer,
setValue,
clear
}
}
export default formreducer
+57
View File
@@ -0,0 +1,57 @@
import { actionTypes } from '../../apiConnection'
const additionalTypes = prefix => ({
clear: `${prefix}_CLEAR`
})
const itemreducer = (prefix, initial) => {
const types = {
...actionTypes(prefix),
...additionalTypes(prefix)
}
const initialState = {
pending: false,
error: false,
data: [],
...initial
}
return (state = initialState, action) => {
switch (action.type) {
case types.attempt:
return {
...state,
error: false,
pending: true
}
case types.failure:
return {
...state,
pending: false,
error: true
}
case types.success:
return {
...state,
pending: false,
error: false,
data: action.response
}
case types.clear:
return {
...state,
data: initialState.data
}
default:
return state
}
}
}
export const actions = (prefix) => {
const types = additionalTypes(prefix)
return {
clear: () => ({ type: types.clear })
}
}
export default itemreducer
+63
View File
@@ -0,0 +1,63 @@
import { actionTypes } from '../../apiConnection'
const additionalTypes = prefix => ({
reset: `${prefix}_CLEAR`
})
const listreducer = (prefix, resToObject, rewriteData = true) => {
const types = {
...actionTypes(prefix),
...additionalTypes(prefix)
}
const initialState = {
pending: false,
error: false,
data: {},
query: undefined
}
return (state = initialState, action) => {
switch (action.type) {
case types.attempt:
return {
...state,
error: false,
pending: true,
query: undefined
}
case types.failure:
return {
...state,
error: true,
pending: false,
query: undefined
}
case types.success: {
const additions = resToObject ? resToObject(action.response) : action.response
const data = !rewriteData ? { ...state.data, ...additions } : additions
return {
...state,
error: false,
pending: false,
data,
query: action.query
}
}
case types.reset: {
return {
...initialState
}
}
default:
return state
}
}
}
export const actions = (prefix) => {
const types = additionalTypes(prefix)
return {
reset: () => ({ type: types.reset })
}
}
export default listreducer
+78 -11
View File
@@ -1,11 +1,30 @@
import { callController } from '../apiConnection'
export const getCourseStatistics = ({ code, start, end, separate }) => {
const route = `/courseyearlystats/?start=${start}&code=${code}&end=${end}&separate=${separate}`
export const getCourseStatistics = ({ code, start, end, separate, language }) => {
const route = `/courseyearlystats/?start=${start}&code=${code}&end=${end}&separate=${separate}&language=${language}`
const prefix = 'FIND_COURSE_STATISTICS_'
return callController(route, prefix)
}
export const getMultipleCourseStatistics = ({ codes, start, end, separate, language }) => {
const route = `/v2/courseyearlystats/?start=${start}&codes=${JSON.stringify(codes)}&end=${end}&separate=${separate}&language=${language}`
const prefix = 'FIND_COURSE_STATISTICS_'
return callController(route, prefix)
}
export const getCourseTypes = () => {
const route = '/coursetypes'
const prefix = 'FIND_COURSE_TYPES_'
return callController(route, prefix)
}
export const getCourseDisciplines = () => {
const route = '/courseDisciplines'
const prefix = 'FIND_COURSE_DISCIPLINES_'
return callController(route, prefix)
}
export const clearCouresStatistics = () => ({
type: 'CLEAR_COURSE_STATISTICS'
})
@@ -22,12 +41,14 @@ const reducer = (state = { data: [], selected: [] }, action) => {
switch (action.type) {
case 'FIND_COURSE_STATISTICS_ATTEMPT':
return {
...state,
pending: true,
selected: state.selected,
data: state.data
}
case 'FIND_COURSE_STATISTICS_FAILURE':
return {
...state,
pending: false,
error: true,
selected: state.selected,
@@ -35,19 +56,21 @@ const reducer = (state = { data: [], selected: [] }, action) => {
}
case 'FIND_COURSE_STATISTICS_SUCCESS':
return {
...state,
pending: false,
error: false,
selected: [...state.selected, {
code: action.response.code,
start: action.response.start,
end: action.response.end,
separate: action.response.separate,
name: action.response.name
}],
data: [...state.data, action.response]
selected: [...state.selected, ...action.response.map(res => ({
code: res.code,
start: res.start,
end: res.end,
separate: res.separate,
name: res.name
}))],
data: [...action.response, ...state.data]
}
case 'REMOVE_COURSE_STATISTICS':
return {
...state,
pending: false,
error: false,
selected: [...state.selected.filter(query => query.code !== action.code ||
@@ -59,8 +82,52 @@ const reducer = (state = { data: [], selected: [] }, action) => {
query.end !== action.end ||
query.separate !== action.separate)]
}
case 'FIND_COURSE_TYPES_ATTEMPT':
return {
...state,
types_pending: true,
selected: state.selected,
data: state.data
}
case 'FIND_COURSE_TYPES_FAILURE':
return {
...state,
types_pending: false,
error: true,
selected: state.selected,
data: state.data
}
case 'FIND_COURSE_TYPES_SUCCESS':
return {
...state,
types_pending: false,
error: false,
courseTypes: action.response
}
case 'FIND_COURSE_DISCIPLINES_ATTEMPT':
return {
...state,
disciplines_pending: true,
selected: state.selected,
data: state.data
}
case 'FIND_COURSE_DISCIPLINES_FAILURE':
return {
...state,
disciplines_pending: false,
error: true,
selected: state.selected,
data: state.data
}
case 'FIND_COURSE_DISCIPLINES_SUCCESS':
return {
...state,
disciplines_pending: false,
error: false,
courseDisciplines: action.response
}
case 'CLEAR_COURSE_STATISTICS':
return { data: [], selected: [] }
return { ...state, data: [], selected: [] }
default:
return state
}
+91
View File
@@ -0,0 +1,91 @@
import { callController } from '../apiConnection'
export const getDuplicates = () => {
const route = '/courses/duplicatecodes/all'
const prefix = 'GET_ALL_DUPLICATES_'
return callController(route, prefix)
}
export const addDuplicate = (code1, code2) => {
const route = `/courses/duplicatecodes/${code1}/${code2}`
const prefix = 'ADD_DUPLICATE_'
const data = { code1, code2 }
const method = 'post'
return callController(route, prefix, data, method)
}
export const removeDuplicate = (code1, code2) => {
const route = `/courses/duplicatecodes/${code1}/${code2}`
const prefix = 'REMOVE_DUPLICATE_'
const method = 'delete'
return callController(route, prefix, {}, method)
}
const reducer = (state = { data: [] }, action) => {
switch (action.type) {
case 'GET_ALL_DUPLICATES_ATTEMPT':
return {
pending: true,
selected: state.selected,
data: state.data
}
case 'GET_ALL_DUPLICATES_FAILURE':
return {
pending: false,
error: true,
selected: state.selected,
data: state.data
}
case 'GET_ALL_DUPLICATES_SUCCESS':
return {
pending: false,
error: false,
selected: action.response.code,
data: action.response
}
case 'ADD_DUPLICATE_ATTEMPT':
return {
pending: true,
selected: state.selected,
data: state.data
}
case 'ADD_DUPLICATE_FAILURE':
return {
pending: false,
error: true,
selected: state.selected,
data: state.data
}
case 'ADD_DUPLICATE_SUCCESS':
return {
pending: false,
error: false,
selected: action.response.code,
data: action.response
}
case 'REMOVE_DUPLICATE_ATTEMPT':
return {
pending: true,
selected: state.selected,
data: state.data
}
case 'REMOVE_DUPLICATE_FAILURE':
return {
pending: false,
error: true,
selected: state.selected,
data: state.data
}
case 'REMOVE_DUPLICATE_SUCCESS':
return {
pending: false,
error: false,
selected: action.response.code,
data: action.response
}
default:
return state
}
}
export default reducer
+10
View File
@@ -0,0 +1,10 @@
import itemreducer from './common/itemreducer'
import { callController } from '../apiConnection/index'
const prefix = 'GET_COURSE_DISCIPLINES_'
export const getCourseDisciplines = () => callController('/courseDisciplines', prefix)
const reducer = itemreducer(prefix)
export default reducer
+54 -11
View File
@@ -1,34 +1,77 @@
import { callController } from '../apiConnection'
export const findCourses = (searchStr) => {
const route = `/courses/?name=${searchStr}`
export const findCourses = (searchStr, language) => {
const route = `/courses/?name=${searchStr}&language=${language}`
const prefix = 'FIND_COURSES_'
return callController(route, prefix)
}
const reducer = (state = { data: [] }, action) => {
export const findMultipleCourses = ({ searchStr, type, discipline }, language = 'fi') => {
const route = `/coursesmulti/?${searchStr ? `name=${searchStr}` : ''}${type ? `&type=${type}` : ''}${discipline ? `&discipline=${discipline}` : ''}&language=${language}`
const prefix = 'FIND_COURSES_MULTI_'
return callController(route, prefix)
}
export const emptyCourseSearch = () => ({
type: 'CLEAR_COURSE_SEARCH'
})
export const toggleCourseSelect = code => ({
type: 'TOGGLE_COURSE_SELECT',
code
})
const reducer = (state = { data: [], selected: [] }, action) => {
switch (action.type) {
case 'FIND_COURSES_ATTEMPT':
return {
pending: true,
selected: state.selected,
data: state.data
...state,
pending: true
}
case 'FIND_COURSES_FAILURE':
return {
...state,
pending: false,
error: true,
selected: state.selected,
data: state.data
error: true
}
case 'FIND_COURSES_SUCCESS':
return {
...state,
pending: false,
error: false,
selected: action.response.code,
data: action.response
}
case 'FIND_COURSES_MULTI_ATTEMPT':
return {
...state,
pending: true
}
case 'FIND_COURSES_MULTI_FAILURE':
return {
...state,
pending: false,
error: true
}
case 'FIND_COURSES_MULTI_SUCCESS':
return {
...state,
pending: false,
error: false,
data: action.response
}
case 'CLEAR_COURSE_SEARCH':
return {
pending: false,
data: [],
selected: []
}
case 'TOGGLE_COURSE_SELECT':
return { // eslint-disable-line no-return-assign
...state,
selected: state.selected.find(course => course.code === action.code) ?
state.selected.filter(cr => cr.code !== action.code) :
[...state.selected, state.data.find(course => course.code === action.code)]
}
default:
return state
}
+16
View File
@@ -0,0 +1,16 @@
import formreducer from './common/formreducer'
import selector from '../selectors/courseStats'
const { ALL } = selector
const prefix = 'COURSE_SUMMARY_FORM'
export const fields = {
programme: 'programme'
}
const reducer = formreducer(prefix, { [fields.programme]: ALL.value })
export const { setValue, clear } = reducer
export default reducer.reducer
+18
View File
@@ -0,0 +1,18 @@
import itemreducer, { actions } from './common/itemreducer'
import { callController } from '../apiConnection/index'
const prefix = 'GET_COURSE_SEARCH_RESULT_'
const storeActions = actions(prefix)
export const clearCourses = storeActions.clear
export const findCourses = ({ name, type, discipline }, language = 'fi') => {
const route = '/coursesmulti'
const params = { name, type, discipline, language }
return callController(route, prefix, [], 'get', params, params)
}
const reducer = itemreducer(prefix)
export default reducer

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