mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9102a90262 | ||
|
|
da32b25d78 | ||
|
|
3bf9125b2c | ||
|
|
fd80179799 | ||
|
|
285e722ef9 | ||
|
|
fbfe0ac0cd | ||
|
|
b88036036e | ||
|
|
dc0b02fe4a | ||
|
|
31df156381 | ||
|
|
5309bdb621 | ||
|
|
538947c0cb | ||
|
|
8a7538b855 | ||
|
|
5ffde2d37c | ||
|
|
e627039176 | ||
|
|
c5d4caefcf | ||
|
|
6ceca872e6 | ||
|
|
81259ef675 | ||
|
|
c987e6dbd8 | ||
|
|
2e2a06e627 | ||
|
|
d10af5ce9f | ||
|
|
a3b6265568 | ||
|
|
0448d0b75c | ||
|
|
1d3eb52e68 | ||
|
|
ad8b6ca0a1 | ||
|
|
461ce9d431 | ||
|
|
122e518481 | ||
|
|
45b402d157 | ||
|
|
f6446a608a | ||
|
|
2bb2f44d31 | ||
|
|
67940704bd | ||
|
|
b537848311 | ||
|
|
c8d0b697b5 | ||
|
|
a975292fa6 | ||
|
|
8b344e8c25 | ||
|
|
ba1f208225 | ||
|
|
9bd39a37cf | ||
|
|
6700edf65f | ||
|
|
cd5a457abb | ||
|
|
0d8fe732e8 | ||
|
|
dffccec9f3 | ||
|
|
84ec4d46de | ||
|
|
ff49e720dc | ||
|
|
9edcce3bf2 | ||
|
|
947a0de0a6 | ||
|
|
d00182c150 | ||
|
|
d30529ab72 | ||
|
|
435f102599 | ||
|
|
6d3b46dfc3 | ||
|
|
4615f4a8e6 | ||
|
|
bfcd48dbb0 | ||
|
|
ee1ade9d8f | ||
|
|
45c09874b1 | ||
|
|
f6e09b8316 | ||
|
|
45ec804c2e | ||
|
|
349597cc29 | ||
|
|
977188a4e2 | ||
|
|
c763ae8727 | ||
|
|
b14dfed31e | ||
|
|
7dc93fc41f | ||
|
|
53e5fb4bc5 | ||
|
|
68dc31ef21 | ||
|
|
6dc63eea68 | ||
|
|
53e0afdd56 | ||
|
|
a5f7986d97 | ||
|
|
af3716f94b | ||
|
|
351b7d0567 | ||
|
|
88394cb2e1 | ||
|
|
2813ed2eaa | ||
|
|
029c2a4bb0 | ||
|
|
1c5c5761c5 | ||
|
|
a73e7369ff | ||
|
|
ea4cf8651f | ||
|
|
b35ac28bf3 | ||
|
|
fd124bdcad | ||
|
|
68cca6f431 | ||
|
|
24a5bdd777 | ||
|
|
0364a34ee0 | ||
|
|
3a63c5317c | ||
|
|
d63481b0d9 | ||
|
|
c84fb51f7d | ||
|
|
1c4a81819b | ||
|
|
2a0edc71e4 | ||
|
|
88c4c3a738 | ||
|
|
a7f9e59703 | ||
|
|
f9042bf3b8 | ||
|
|
5a26bd18c0 | ||
|
|
e12bccdd13 | ||
|
|
dd22e385cc | ||
|
|
d32a863a05 | ||
|
|
61c8886922 | ||
|
|
f274ee47bc | ||
|
|
78c8a505f0 | ||
|
|
9261b00e8e | ||
|
|
2c27010ebf | ||
|
|
1ca1d85e9a | ||
|
|
2a19138c5c | ||
|
|
1453f4ff76 | ||
|
|
ee432fdf5a | ||
|
|
155769e2e2 | ||
|
|
4ac09ac052 | ||
|
|
abd0ebaee5 | ||
|
|
319fecb54e | ||
|
|
dbd81caf9a | ||
|
|
a8bf3f7eb1 | ||
|
|
0bc3099407 | ||
|
|
82e39a2215 | ||
|
|
d66b66aa3a | ||
|
|
13886b62cd | ||
|
|
9ce0ce1529 | ||
|
|
d1b52ce005 | ||
|
|
d90b19ba8f | ||
|
|
2fb735a149 | ||
|
|
cac6ca9c50 | ||
|
|
f9b123aa1b | ||
|
|
bd1b453b22 | ||
|
|
8869de5e3c | ||
|
|
75e85874c7 | ||
|
|
9b833b0dd5 | ||
|
|
324afad820 | ||
|
|
e9305392e5 | ||
|
|
34370af90a | ||
|
|
7d2841b586 | ||
|
|
e1cc9fa6d1 | ||
|
|
24ba49b5be | ||
|
|
f09b2441ff | ||
|
|
604503f778 | ||
|
|
766f6f1ff9 | ||
|
|
c190110e82 | ||
|
|
93b0d50aa4 | ||
|
|
7f4d177ffc | ||
|
|
fde131ab8e | ||
|
|
23e875f783 | ||
|
|
9c93054e9a | ||
|
|
48259304b6 | ||
|
|
9edb2df3b5 | ||
|
|
61b353682e | ||
|
|
8f5f075309 | ||
|
|
5492ed21ea | ||
|
|
4803b8f1fb | ||
|
|
b8867fadba | ||
|
|
b77a04ef33 | ||
|
|
a9b2b1816d | ||
|
|
04230a85b6 | ||
|
|
74da624b9e | ||
|
|
20b70f02c4 | ||
|
|
9acde13a84 | ||
|
|
c7e742f86c | ||
|
|
8be1f6a1c4 | ||
|
|
0e9d63d982 | ||
|
|
9202c7d765 | ||
|
|
af528092e9 | ||
|
|
2b998a9270 | ||
|
|
0abcfe54af | ||
|
|
1a3cd1f234 | ||
|
|
66ca9518cf | ||
|
|
7071c43a45 | ||
|
|
8fa1293462 | ||
|
|
3a8ad9eab0 | ||
|
|
fb3c8bb074 | ||
|
|
6cad4bc191 | ||
|
|
d128b1abe4 | ||
|
|
2385d85d45 | ||
|
|
e5938d59cf | ||
|
|
e49f986282 | ||
|
|
2509701a95 | ||
|
|
e290695902 | ||
|
|
49a0302974 | ||
|
|
e5d8a82670 | ||
|
|
768c242427 | ||
|
|
d2b7180184 | ||
|
|
5421d80925 | ||
|
|
1bfecb7311 | ||
|
|
975c5ffe6c | ||
|
|
3b165258c8 | ||
|
|
b5a1953ce2 | ||
|
|
d6558a93e9 | ||
|
|
693d677349 | ||
|
|
079c4f787d | ||
|
|
08f6bd6bfd | ||
|
|
f17ce53e4b | ||
|
|
9c2a270475 | ||
|
|
c310ccc5c4 | ||
|
|
4491bd5225 | ||
|
|
1ca6031201 | ||
|
|
3dc67b7a28 | ||
|
|
1de6b809f2 |
+2
-1
@@ -25,4 +25,5 @@ services/updater_scheduler/debug.log
|
||||
services/updater_scheduler/active_student_numbers.txt
|
||||
services/oodikone2-backend/debug.log
|
||||
datastore
|
||||
services/updater_api/debug.log
|
||||
services/updater_api/debug.log
|
||||
services/backend/updater_writer/dumps
|
||||
+3
-3
@@ -30,15 +30,15 @@ jobs:
|
||||
- docker ps
|
||||
- cat nginx/log
|
||||
- docker-compose logs -f &
|
||||
- until $(curl --output /dev/null --silent --fail http://localhost:1337/staging/); do printf '.'; sleep 5; done
|
||||
- until $(curl --output /dev/null --silent --fail http://localhost:1337/); do printf '.'; sleep 5; done
|
||||
- time npm run test_services
|
||||
- time CYPRESS_baseUrl=http://localhost:1337/staging/ npm run cypress:record
|
||||
- time CYPRESS_baseUrl=http://localhost:1337/ npm run cypress:record
|
||||
- docker ps -a
|
||||
- if [[ $(docker ps --all | grep -Eo '(Exited|Restarting) \([0-9]+\)' | grep -Eo '[0-9]+' | awk '{ sum += $1 } END { print sum }') != '0' ]]; then echo 'Some process had nonzero exit code'; exit 1; fi
|
||||
deploy: # deploy is always skipped for PR
|
||||
provider: script
|
||||
skip_cleanup: true
|
||||
script: "docker-compose -f docker-compose.lateste2e.yml push && curl --fail -k -X POST -H \"Authorization: token ${TRAVIS_ACCESS_TOKEN}\" https://toska.cs.helsinki.fi/toskawatch/release/ && curl -X POST -H 'Content-type: application/json' --data \"{\\\"text\\\":\\\"Staging auto deployment started!\\\"}\" ${SLACKBOT_URL}"
|
||||
script: "docker-compose -f docker-compose.lateste2e.yml push && curl -X POST -H 'Content-type: application/json' --data \"{\\\"text\\\":\\\"Staging auto deployment started!\\\"}\" ${SLACKBOT_URL}"
|
||||
on:
|
||||
branch: master
|
||||
- # in parallel
|
||||
|
||||
@@ -18,13 +18,10 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
|
||||
it('Searching single course having duplicate mappings shows course statistics', () => {
|
||||
cy.contains("Fetch statistics").should('be.disabled')
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains("Search for courses")
|
||||
cy.get("input[placeholder='Search by entering a course code']").type('TKT20003')
|
||||
cy.contains("tr", "TKT20003").within(($row) => {
|
||||
cy.get('button').click()
|
||||
})
|
||||
cy.contains("tr", "TKT20003").click()
|
||||
cy.contains("Fetch statistics").should('be.enabled').click()
|
||||
cy.contains("Search for courses").should('not.exist')
|
||||
|
||||
@@ -51,16 +48,11 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
|
||||
it('Searching multiple courses having duplicate mappings shows course statistics', () => {
|
||||
cy.contains("Fetch statistics").should('be.disabled')
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains("Search for courses")
|
||||
cy.get("input[placeholder='Search by entering a course code']").type('TKT')
|
||||
cy.contains("tr", "TKT20003").within(($row) => {
|
||||
cy.get('button').click()
|
||||
})
|
||||
cy.contains("tr", "TKT10002").within(($row) => {
|
||||
cy.get('button').click()
|
||||
})
|
||||
cy.contains("tr", "TKT20003").click()
|
||||
cy.contains("tr", "TKT10002").click()
|
||||
cy.contains("Fetch statistics").should('be.enabled').click()
|
||||
cy.contains("Search for courses").should('not.exist')
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('Population Statistics tests', () => {
|
||||
|
||||
cy.contains("add").click()
|
||||
cy.contains("Add filters").siblings().within(() => {
|
||||
cy.get(".form").should('have.length', 9)
|
||||
cy.get(".form").should('have.length', 10)
|
||||
})
|
||||
|
||||
checkAmountOfStudents(26)
|
||||
|
||||
@@ -56,6 +56,14 @@ describe('Student Statistics tests', () => {
|
||||
cy.contains('Credits').should('not.exist')
|
||||
})
|
||||
|
||||
it('Can jump to course', () => {
|
||||
cy.get('.prompt').type('Oinonen')
|
||||
cy.contains('011143561').click()
|
||||
cy.contains('Toinen kotimainen kieli (40061)').siblings().within(() => { cy.get('.level').click() })
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('Toinen kotimainen kieli')
|
||||
})
|
||||
|
||||
it('Searching with bad inputs doesnt yield results', () => {
|
||||
cy.get('.prompt').type('Oin')
|
||||
cy.contains('Student number').should('not.exist')
|
||||
|
||||
@@ -75,7 +75,7 @@ services:
|
||||
REDIS: redis
|
||||
DB_URL: postgres://postgres@db:5432/tkt_oodi
|
||||
TEST_DB: postgres://postgres@db:5432/tkt_oodi_test
|
||||
FRONT_URL: http://localhost:1337/staging
|
||||
FRONT_URL: http://localhost:1337
|
||||
TOKEN_SECRET: IWannaBeTheVeryBest
|
||||
OODILEARN_URL: http://oodilearn:5000
|
||||
USERSERVICE_URL: http://userservice:4567
|
||||
@@ -97,7 +97,7 @@ services:
|
||||
build:
|
||||
context: services/oodikone2-frontend
|
||||
args:
|
||||
BASE_PATH: /staging/
|
||||
BASE_PATH: /
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
@@ -105,7 +105,7 @@ services:
|
||||
BACKEND_ADDR: backend
|
||||
ADMINER_URL: http://localhost:5050/?pgsql=db&username=postgres
|
||||
container_name: frontend
|
||||
|
||||
|
||||
userservice:
|
||||
image: toska/oodikone2-userservice:staging
|
||||
build:
|
||||
|
||||
@@ -132,6 +132,8 @@ services:
|
||||
- ./services/backend/oodikone2-backend/:/usr/src/app
|
||||
- ./services/backend/shared/models:/usr/src/app/src/models
|
||||
- ./services/backend/shared/migrations:/usr/src/app/src/database/migrations
|
||||
- ./services/backend/shared/migrations_kone:/usr/src/app/src/database/migrations_kone
|
||||
|
||||
container_name: backend
|
||||
depends_on:
|
||||
- db
|
||||
@@ -245,6 +247,8 @@ services:
|
||||
- ./services/backend/updater_writer/:/usr/src/app
|
||||
- ./services/backend/shared/models:/usr/src/app/models
|
||||
- ./services/backend/shared/migrations:/usr/src/app/database/migrations
|
||||
- ./services/backend/shared/migrations_kone:/usr/src/app/database/migrations_kone
|
||||
|
||||
depends_on:
|
||||
- nats
|
||||
- db
|
||||
@@ -254,6 +258,8 @@ services:
|
||||
DB_URL: postgres://postgres@db:5432/tkt_oodi
|
||||
TEST_DB: postgres://postgres@db:5432/tkt_oodi_test
|
||||
TEST_DB_SCHEMA: updater_test
|
||||
DB_SCHEMA_KONE: kone_data
|
||||
|
||||
|
||||
|
||||
scheduler_mongo:
|
||||
|
||||
+2
-2
@@ -8,11 +8,11 @@ http {
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location /staging/ {
|
||||
location / {
|
||||
proxy_pass http://frontend:5000/;
|
||||
}
|
||||
|
||||
location /staging/api/ {
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8080/;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"cypress:open": "CYPRESS_baseUrl=http://localhost:8081 cypress open",
|
||||
"cypress:run": "CYPRESS_baseUrl=http://localhost:8081 cypress run --config video=false -P ./",
|
||||
"cypress:record": "cypress run --config videoUploadOnPasses=false --record -P ./",
|
||||
"cypress:record": "cypress run --config videoUploadOnPasses=false -P ./",
|
||||
"concurrently": "concurrently",
|
||||
"test": "./run_all_tests.sh",
|
||||
"test_services": "./run_service_tests.sh"
|
||||
|
||||
@@ -52,11 +52,9 @@ do
|
||||
;;
|
||||
"Download latest anon db")
|
||||
get_anon_oodikone
|
||||
unpack_oodikone_server_backup
|
||||
;;
|
||||
"Download latest real db")
|
||||
get_oodikone_server_backup
|
||||
unpack_oodikone_server_backup
|
||||
;;
|
||||
"Full reset")
|
||||
purge
|
||||
|
||||
+18
-44
@@ -1,9 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
DIR_PATH=$(dirname "$0")
|
||||
ANONDB_DIR=anonyymioodi
|
||||
BACKUP_DIR=backups
|
||||
PSQL_DB_BACKUP="$BACKUP_DIR/anon.bak"
|
||||
USER_DB_BACKUP="$BACKUP_DIR/user-dump.bak"
|
||||
PSQL_DB_BACKUP="$ANONDB_DIR/anon.sqz"
|
||||
USER_DB_BACKUP="$ANONDB_DIR/user-dump.sqz"
|
||||
PSQL_REAL_DB_BACKUP="$BACKUP_DIR/latest-pg.sqz"
|
||||
USER_REAL_DB_BACKUP="$BACKUP_DIR/latest-user-pg.sqz"
|
||||
|
||||
@@ -51,31 +52,21 @@ get_anon_oodikone() {
|
||||
fi
|
||||
rm -rf anonyymioodi
|
||||
GIT_SSH_COMMAND='ssh -i private.key' git clone git@github.com:UniversityOfHelsinkiCS/anonyymioodi.git
|
||||
mv anonyymioodi/anon.bak.bz2 ./$BACKUP_DIR/anon.bak.bz2
|
||||
mv anonyymioodi/user-dump.bak.bz2 ./$BACKUP_DIR/user-dump.bak.bz2
|
||||
|
||||
}
|
||||
|
||||
unpack_oodikone_server_backup() {
|
||||
bunzip2 --force --decompress --verbose ./$BACKUP_DIR/*.bz2
|
||||
}
|
||||
|
||||
restore_psql_from_backup () {
|
||||
cat $PSQL_DB_BACKUP | docker exec -i -u postgres oodi_db psql -d tkt_oodi
|
||||
time pg_restore -U postgres -h localhost -p 5421 --no-owner -F c --dbname=tkt_oodi -j4 $PSQL_DB_BACKUP
|
||||
}
|
||||
|
||||
restore_real_psql_from_backup () {
|
||||
ping_psql "oodi_db" "tkt_oodi_real"
|
||||
time pg_restore -U postgres -h localhost -p 5421 --no-owner -F c --dbname=tkt_oodi_real -j4 $PSQL_REAL_DB_BACKUP
|
||||
}
|
||||
|
||||
restore_userdb_from_backup () {
|
||||
docker exec -u postgres oodi_user_db psql -c "CREATE DATABASE user_db"
|
||||
cat $USER_DB_BACKUP | docker exec -i -u postgres oodi_user_db psql -d user_db
|
||||
time pg_restore -U postgres -h localhost -p 5422 --no-owner -F c --dbname=user_db -j4 $USER_DB_BACKUP
|
||||
}
|
||||
|
||||
restore_real_userdb_from_backup () {
|
||||
docker exec -u postgres oodi_user_db psql -c "CREATE DATABASE user_db_real"
|
||||
time pg_restore -U postgres -h localhost -p 5422 --no-owner -F c --dbname=user_db_real -j4 $USER_REAL_DB_BACKUP
|
||||
}
|
||||
|
||||
@@ -84,12 +75,6 @@ restore_real_userdb_from_backup () {
|
||||
# docker exec -t mongo_db mongorestore -d oodilearn "/dump"
|
||||
# }
|
||||
|
||||
db_oodikone_reset () {
|
||||
docker exec -u postgres oodi_db dropdb "tkt_oodi" || echo "dbdrop of tkt_oodi failed. "
|
||||
docker exec -u postgres oodi_db createdb "tkt_oodi" || echo "createdb of tkt_oodi failed. "
|
||||
restore_psql_from_backup
|
||||
}
|
||||
|
||||
ping_psql () {
|
||||
echo "Pinging psql in container $1 with db name $2"
|
||||
retry docker exec -u postgres $1 pg_isready --dbname=$2
|
||||
@@ -156,10 +141,6 @@ show_instructions () {
|
||||
cat ./assets/instructions.txt
|
||||
}
|
||||
|
||||
docker_restart_backend () {
|
||||
docker-compose restart backend userservice
|
||||
}
|
||||
|
||||
run_full_setup () {
|
||||
echo "Setup npm packages"
|
||||
install_cli_npm_packages
|
||||
@@ -169,15 +150,13 @@ run_full_setup () {
|
||||
get_oodikone_server_backup
|
||||
echo "Getting anon backups from the private repository. "
|
||||
get_anon_oodikone
|
||||
echo "Unpacking compressed files"
|
||||
unpack_oodikone_server_backup
|
||||
echo "Building images, starting containers"
|
||||
docker_build
|
||||
echo "Building images"
|
||||
docker-compose build
|
||||
echo "Setup oodikone db from dump."
|
||||
docker-compose up -d db user_db
|
||||
db_setup_full
|
||||
db_anon_setup_full
|
||||
echo "Restarting Docker backend containers to run migrations, etc."
|
||||
docker_restart_backend
|
||||
docker-compose down
|
||||
show_instructions
|
||||
}
|
||||
|
||||
@@ -188,14 +167,12 @@ run_anon_full_setup () {
|
||||
init_dirs
|
||||
echo "Getting anon backups from the private repository. "
|
||||
get_anon_oodikone
|
||||
echo "Unpacking compressed files"
|
||||
unpack_oodikone_server_backup
|
||||
echo "Building images, starting containers"
|
||||
docker_build
|
||||
echo "Building images"
|
||||
docker-compose build
|
||||
echo "Setup oodikone db from dump."
|
||||
docker-compose up -d db user_db
|
||||
db_anon_setup_full
|
||||
echo "Restarting Docker backend containers to run migrations, etc."
|
||||
docker_restart_backend
|
||||
docker-compose down
|
||||
show_instructions
|
||||
}
|
||||
|
||||
@@ -206,14 +183,11 @@ run_e2e_setup () {
|
||||
init_dirs
|
||||
echo "Getting anon backups from the private repository. "
|
||||
get_anon_oodikone
|
||||
echo "Unpacking compressed files"
|
||||
unpack_oodikone_server_backup
|
||||
echo "Building images, starting containers"
|
||||
docker-compose -f $1 build && docker-compose -f $1 up -d
|
||||
echo "Building images"
|
||||
docker-compose -f $1 build
|
||||
echo "Setup oodikone db from dump."
|
||||
docker-compose -f $1 up -d db user_db
|
||||
db_anon_setup_full
|
||||
echo "Restarting Docker backend containers to run migrations, etc."
|
||||
docker-compose -f $1 restart backend userservice
|
||||
echo "Restarting Docker nginx because it has old backend IP otherwise"
|
||||
docker-compose -f $1 restart nginx
|
||||
echo "Starting services."
|
||||
docker-compose -f $1 up -d
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ FROM node:10.15-alpine
|
||||
COPY ./oodikone2-backend /usr/src/app
|
||||
COPY ./shared/models /usr/src/app/src/models
|
||||
COPY ./shared/migrations /usr/src/app/src/database/migrations
|
||||
COPY ./shared/migrations_kone /usr/src/app/src/database/migrations_kone
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
RUN npm ci
|
||||
|
||||
|
||||
+56
-24
@@ -1702,7 +1702,7 @@
|
||||
},
|
||||
"chalk": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "http://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz",
|
||||
"integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -1713,7 +1713,7 @@
|
||||
},
|
||||
"pretty-ms": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "http://registry.npmjs.org/pretty-ms/-/pretty-ms-0.2.2.tgz",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-0.2.2.tgz",
|
||||
"integrity": "sha1-2oeaaC/zOjcBEEbxPWJ/Z8c7hPY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -1722,7 +1722,7 @@
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz",
|
||||
"integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=",
|
||||
"dev": true
|
||||
}
|
||||
@@ -2403,12 +2403,19 @@
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
|
||||
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
|
||||
"version": "0.19.0",
|
||||
"resolved": "http://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
|
||||
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.3.0",
|
||||
"is-buffer": "^1.1.5"
|
||||
"follow-redirects": "1.5.10",
|
||||
"is-buffer": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-buffer": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
|
||||
"integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"babel-code-frame": {
|
||||
@@ -3858,7 +3865,8 @@
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
||||
"optional": true
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.2",
|
||||
@@ -5128,7 +5136,8 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@@ -5149,12 +5158,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -5169,17 +5180,20 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -5296,7 +5310,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -5308,6 +5323,7 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -5322,6 +5338,7 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -5329,12 +5346,14 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -5353,6 +5372,7 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -5433,7 +5453,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -5445,6 +5466,7 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -5530,7 +5552,8 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -5566,6 +5589,7 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -5585,6 +5609,7 @@
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -5628,12 +5653,14 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -10593,7 +10620,7 @@
|
||||
},
|
||||
"onetime": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
|
||||
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -11250,6 +11277,7 @@
|
||||
"version": "0.1.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"kind-of": "^3.0.2",
|
||||
"longest": "^1.0.1",
|
||||
@@ -11600,7 +11628,8 @@
|
||||
"is-buffer": {
|
||||
"version": "1.1.6",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"is-builtin-module": {
|
||||
"version": "1.0.0",
|
||||
@@ -11711,6 +11740,7 @@
|
||||
"version": "3.2.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-buffer": "^1.1.5"
|
||||
}
|
||||
@@ -11757,7 +11787,8 @@
|
||||
"longest": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "4.1.3",
|
||||
@@ -12023,7 +12054,8 @@
|
||||
"repeat-string": {
|
||||
"version": "1.6.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"author": "Matti Luukkainen",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"axios": "^0.19.0",
|
||||
"bluebird": "^3.5.3",
|
||||
"body-parser": "^1.18.3",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -4,22 +4,20 @@ const conf = require('../conf-backend')
|
||||
|
||||
const sequelize = new Sequelize(conf.DB_URL, {
|
||||
schema: conf.DB_SCHEMA,
|
||||
searchPath: conf.DB_SCHEMA,
|
||||
logging: false,
|
||||
operatorsAliases: false,
|
||||
dialectOptions: {
|
||||
prependSearchPath: true
|
||||
}
|
||||
operatorsAliases: false
|
||||
})
|
||||
sequelize.query(`SET SESSION search_path to ${conf.DB_SCHEMA}`)
|
||||
|
||||
const sequelizeKone = new Sequelize(conf.DB_URL, {
|
||||
schema: conf.DB_SCHEMA_KONE,
|
||||
logging: false,
|
||||
operatorsAliases: false,
|
||||
searchPath: conf.DB_SCHEMA_KONE,
|
||||
dialectOptions: {
|
||||
prependSearchPath: true
|
||||
}
|
||||
logging: false,
|
||||
operatorsAliases: false
|
||||
})
|
||||
sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
|
||||
// See https://github.com/sequelize/sequelize/issues/10875
|
||||
|
||||
const runMigrations = async () => {
|
||||
try {
|
||||
@@ -37,6 +35,7 @@ const runMigrations = async () => {
|
||||
],
|
||||
path: `${process.cwd()}/src/database/migrations`,
|
||||
pattern: /\.js$/,
|
||||
schema: conf.DB_SCHEMA
|
||||
}
|
||||
})
|
||||
const migrations = await migrator.up()
|
||||
@@ -60,12 +59,11 @@ const runMigrationsKone = async () => {
|
||||
migrations: {
|
||||
params: [
|
||||
sequelizeKone.getQueryInterface(),
|
||||
sequelizeKone
|
||||
Sequelize
|
||||
],
|
||||
path: `${process.cwd()}/src/database/migrations_kone`,
|
||||
pattern: /\.js$/,
|
||||
schema: conf.DB_SCHEMA_KONE,
|
||||
wrap: fn => () => fn(sequelizeKone.getQueryInterface(), sequelizeKone)
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
},
|
||||
})
|
||||
const migrations = await migrator.up()
|
||||
|
||||
@@ -12,14 +12,17 @@ const auth = require('./middleware/auth')
|
||||
const teachers = require('./routes/teachers')
|
||||
const usage = require('./routes/usage')
|
||||
const providers = require('./routes/providers')
|
||||
const faculties = require('./routes/faculties')
|
||||
const semesters = require('./routes/semesters')
|
||||
const oodilearn = require('./routes/oodilearn')
|
||||
const courseGroups = require('./routes/courseGroups')
|
||||
const mandatoryCourses = require('./routes/mandatorycourses')
|
||||
const mandatoryCourseLabels = require('./routes/mandatorycourselabels')
|
||||
const ping = require('./routes/ping')
|
||||
const oodi = require('./routes/oodi')
|
||||
const task = require('./routes/tasks')
|
||||
const feedback = require('./routes/feedback')
|
||||
const tags = require('./routes/tags')
|
||||
|
||||
module.exports = (app, url) => {
|
||||
app.use(url, log)
|
||||
@@ -33,14 +36,17 @@ module.exports = (app, url) => {
|
||||
app.use(url, population)
|
||||
app.use(url, language)
|
||||
app.use(url, providers)
|
||||
app.use(url, faculties)
|
||||
app.use(url, semesters)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
app.use(`${url}/users`, auth.roles(['users']), users)
|
||||
app.use(`${url}/feedback`, auth.roles(['users']), feedback)
|
||||
app.use(`${url}/feedback`, feedback)
|
||||
app.use(`${url}/usage`, auth.roles(['usage']), usage)
|
||||
app.use(`${url}/oodilearn`, auth.roles(['oodilearn']), oodilearn)
|
||||
app.use(`${url}/course-groups`, auth.roles(['coursegroups']), courseGroups)
|
||||
app.use(`${url}/mandatory_courses`, mandatoryCourses),
|
||||
app.use(`${url}/mandatory-course-labels`, mandatoryCourseLabels),
|
||||
app.use(`${url}/oodi`, auth.roles(['dev']), oodi)
|
||||
app.use(url, auth.roles(['dev', 'admin']), task)
|
||||
app.use(url, tags)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const router = require('express').Router()
|
||||
const { all } = require('../services/organisations')
|
||||
|
||||
router.get('/faculties', async (req, res) => {
|
||||
const faculties = await all()
|
||||
res.json(faculties)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -5,10 +5,9 @@ const userService = require('../services/userService')
|
||||
router.post('/email', async (req, res) => {
|
||||
const { content } = req.body
|
||||
const { uid } = req.headers
|
||||
const { email } = await userService.byUsername(uid)
|
||||
|
||||
const { email, full_name } = await userService.byUsername(uid)
|
||||
const formattedEmail = content.split('\n\n').map(line => `<p> ${line} </p>`).join('')
|
||||
const feedback = mailservice.feedback(formattedEmail, uid, email)
|
||||
const feedback = mailservice.feedback(formattedEmail, uid, email, full_name)
|
||||
|
||||
if (process.env.SMTP !== undefined && content) {
|
||||
await mailservice.transporter.sendMail(feedback, (error) => {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
const router = require('express').Router()
|
||||
const MandatoryCourseLabels = require('../services/mandatoryCourseLabels')
|
||||
|
||||
router.get('/:programme/labels', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
if (programme) {
|
||||
try {
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:programme/labels', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
const { label } = req.body
|
||||
if (programme && label) {
|
||||
try {
|
||||
await MandatoryCourseLabels.destroy(programme, label)
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:programme/move', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
const { label, direction } = req.body
|
||||
if (programme && label) {
|
||||
try {
|
||||
await MandatoryCourseLabels.move(programme, label, direction)
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:programme/labels', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
const { label } = req.body
|
||||
if (programme && label) {
|
||||
try {
|
||||
await MandatoryCourseLabels.create(programme, label)
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -46,7 +46,7 @@ router.post('/:programme/label/:course', async (req, res) => {
|
||||
.json({
|
||||
name: updatedCourse.course.name,
|
||||
code: updatedCourse.course_code,
|
||||
label: updatedCourse.label
|
||||
label: updatedCourse.mandatory_course_label
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
const router = require('express').Router()
|
||||
const Tags = require('../services/tags')
|
||||
const TagStudent = require('../services/tagstudent')
|
||||
|
||||
router.get('/tags', async (req, res) => {
|
||||
try {
|
||||
const tags = await Tags.findTags()
|
||||
res.status(200).json(tags)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(400).json(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/tags/:studytrack', async (req, res) => {
|
||||
const { studytrack } = req.params
|
||||
try {
|
||||
const tags = await Tags.findTagsByStudytrack(studytrack)
|
||||
res.status(200).json(tags)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(400).json(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/tags', async (req, res) => {
|
||||
const { tag } = req.body
|
||||
try {
|
||||
const result = await Tags.createNewTag(tag)
|
||||
res.status(200).json(result)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/tags', async (req, res) => {
|
||||
const { tag } = req.body
|
||||
try {
|
||||
const result = await Tags.deleteTag(tag)
|
||||
res.status(200).json(result)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(400).json(err)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
router.get('/studenttags', async (req, res) => {
|
||||
try {
|
||||
const result = await TagStudent.getStudentTags()
|
||||
res.status(200).json(result)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(400).json(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/studenttags/:studytrack', async (req, res) => {
|
||||
const { studytrack } = req.params
|
||||
try {
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(result)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(400).json(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/studenttags/:studentnumber', async (req, res) => {
|
||||
const { studentnumber } = req.params
|
||||
try {
|
||||
const result = await TagStudent.getStudentTagsByStudentnumber(studentnumber)
|
||||
console.log(result)
|
||||
res.status(200).json(result)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(400).json(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/studenttags', async (req, res) => {
|
||||
const { tag } = req.body
|
||||
try {
|
||||
const result = await TagStudent.createStudentTag(tag)
|
||||
res.status(200).json(result)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(400).json(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/studenttags', async (req, res) => {
|
||||
const { id } = req.body
|
||||
try {
|
||||
const result = await TagStudent.deleteStudentTag(id)
|
||||
res.status(200).json(result)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(400).json(err)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -63,4 +63,12 @@ router.delete('/:uid/elements', async (req, res) => {
|
||||
res.json(user)
|
||||
})
|
||||
|
||||
router.post('/:uid/faculties', async (req, res) => {
|
||||
const { uid } = req.params
|
||||
const { faculties } = req.body
|
||||
const user = await userService.setFaculties(uid, faculties)
|
||||
if (user && user.username != req.decodedToken.userId) await blacklist.addUserToBlacklist(user.username)
|
||||
res.json(user)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -62,14 +62,14 @@ const message2 = email => {
|
||||
<img style="max-width: 13.5%;height: auto;" src="https://i.imgur.com/tnNDAJk.png" /> `,
|
||||
})
|
||||
}
|
||||
const feedback = (content, user, email) => {
|
||||
const feedback = (content, user, email, full_name) => {
|
||||
return ({
|
||||
to: 'Toska <grp-toska@helsinki.fi>',
|
||||
replyTo: email,
|
||||
subject: `New message from Oodikone feedback`,
|
||||
text: `New message from user ${user}`,
|
||||
html: `${content}
|
||||
<p>sent by user: ${user}, email: ${email}</p>
|
||||
<p>sent by ${full_name}, userid: ${user}, email: ${email}</p>
|
||||
<img src="cid:toskalogoustcid"/>`,
|
||||
attachments: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const _ = require('lodash')
|
||||
const { MandatoryCourseLabels } = require('../models')
|
||||
const { Op } = Sequelize
|
||||
const {
|
||||
sequelize
|
||||
} = require('../models/index')
|
||||
|
||||
const create = async (studyprogramme_id, { label }) => {
|
||||
const labels = await labelsByStudyprogramme(studyprogramme_id)
|
||||
await MandatoryCourseLabels.create({ studyprogramme_id, label, orderNumber: labels.length+1 })
|
||||
}
|
||||
|
||||
const destroy = (studyprogramme_id, { id }) => {
|
||||
return MandatoryCourseLabels.destroy({ where: { studyprogramme_id, id } })
|
||||
}
|
||||
|
||||
const move = async (studyprogramme_id, { id }, direction) => {
|
||||
const labels = await labelsByStudyprogramme(studyprogramme_id).map(e => e.get())
|
||||
const orderedlabels = _.sortBy(labels, ['orderNumber'])
|
||||
const indexToMove = orderedlabels.findIndex(e => e.id === id)
|
||||
const indexToSwap = direction === 'up' ? indexToMove-1 : indexToMove+1
|
||||
if (indexToSwap < 0 || indexToSwap >= orderedlabels.length) return null;
|
||||
[orderedlabels[indexToMove], orderedlabels[indexToSwap]] = [orderedlabels[indexToSwap], orderedlabels[indexToMove]]
|
||||
const newOrder = orderedlabels.map((e, index) => ({ ...e, orderNumber: index }))
|
||||
await sequelize.transaction(async transaction => {
|
||||
for (let e of newOrder) {
|
||||
await MandatoryCourseLabels.update(
|
||||
{orderNumber: e.orderNumber}, { where: { studyprogramme_id, id: e.id }, transaction }
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const find = (studyprogramme_id, { id }) => {
|
||||
return MandatoryCourseLabels.findOne({
|
||||
attributes: ['id', 'label', 'orderNumber'],
|
||||
where: {
|
||||
studyprogramme_id: {
|
||||
[Op.eq]: studyprogramme_id,
|
||||
},
|
||||
id: {
|
||||
[Op.eq]: id,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const labelsByStudyprogramme = (studyProgrammeId) => {
|
||||
return MandatoryCourseLabels.findAll({
|
||||
attributes: ['id', 'label', 'orderNumber'],
|
||||
where: {
|
||||
studyprogramme_id: {
|
||||
[Op.eq]: studyProgrammeId
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
labelsByStudyprogramme,
|
||||
create,
|
||||
destroy,
|
||||
move,
|
||||
find
|
||||
}
|
||||
@@ -1,24 +1,31 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Course, MandatoryCourse } = require('../models')
|
||||
const { Course, MandatoryCourse, MandatoryCourseLabels } = require('../models')
|
||||
const { Op } = Sequelize
|
||||
|
||||
const byStudyprogramme = (studyProgrammeId) => {
|
||||
const courses = MandatoryCourse.findAll({
|
||||
attributes: ['course_code', 'label'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['name'],
|
||||
},
|
||||
attributes: ['course_code'],
|
||||
include: [
|
||||
{
|
||||
model: Course,
|
||||
attributes: ['name'],
|
||||
},
|
||||
{
|
||||
model: MandatoryCourseLabels,
|
||||
attributes: ['id', 'label', 'orderNumber'],
|
||||
}
|
||||
],
|
||||
where: {
|
||||
studyprogramme_id: {
|
||||
[Op.eq]: studyProgrammeId
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return courses.map(course => ({
|
||||
name: course.course.name,
|
||||
code: course.course_code,
|
||||
label: course.label
|
||||
label: course.mandatory_course_label
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -32,10 +39,16 @@ const create = (studyProgrammeId, code) => {
|
||||
const find = (studyProgrammeId, code) => {
|
||||
return MandatoryCourse.findOne({
|
||||
attributes: ['course_code', 'label'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['name'],
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Course,
|
||||
attributes: ['name'],
|
||||
},
|
||||
{
|
||||
model: MandatoryCourseLabels,
|
||||
attributes: ['id', 'label', 'orderNumber'],
|
||||
}
|
||||
],
|
||||
where: {
|
||||
studyprogramme_id: {
|
||||
[Op.eq]: studyProgrammeId
|
||||
@@ -56,11 +69,10 @@ const remove = (studyProgrammeId, code) => {
|
||||
})
|
||||
}
|
||||
|
||||
const updateLabel = (studyProgrammeId, code, label) => {
|
||||
if (label != null) label = label.trim()
|
||||
const updateLabel = (studyProgrammeId, code, { id }) => {
|
||||
return MandatoryCourse.update(
|
||||
{
|
||||
label: label
|
||||
label: id
|
||||
},
|
||||
{
|
||||
where: {
|
||||
|
||||
@@ -3,7 +3,8 @@ const moment = require('moment')
|
||||
const { orderBy } = require('lodash')
|
||||
const {
|
||||
Student, Credit, Course, sequelize, Studyright, StudyrightExtent, ElementDetails,
|
||||
Discipline, CourseType, SemesterEnrollment, Semester, Transfers, StudyrightElement
|
||||
Discipline, CourseType, SemesterEnrollment, Semester, Transfers, StudyrightElement,
|
||||
Tag, TagStudent
|
||||
} = require('../models')
|
||||
const { getMainCodesMap, byName } = require('./courses')
|
||||
const { CourseStatsCounter } = require('./course_stats_counter')
|
||||
@@ -24,8 +25,9 @@ const formatStudentForPopulationStatistics = ({
|
||||
firstnames, lastname, studentnumber, dateofuniversityenrollment, creditcount,
|
||||
matriculationexamination, credits, abbreviatedname, email, studyrights,
|
||||
semester_enrollments, transfers, updatedAt, createdAt, gender_code,
|
||||
gender_fi, gender_sv, gender_en
|
||||
gender_fi, gender_sv, gender_en, tag_students
|
||||
}, startDate, endDate) => {
|
||||
|
||||
const toCourse = ({ grade, attainment_date, credits, course, credittypecode, isStudyModule }) => {
|
||||
course = course.get()
|
||||
|
||||
@@ -97,7 +99,7 @@ const formatStudentForPopulationStatistics = ({
|
||||
email,
|
||||
semesterenrollments,
|
||||
updatedAt: updatedAt || createdAt,
|
||||
tags: [],
|
||||
tags: tag_students || [],
|
||||
studyrightStart: startDate,
|
||||
starting: moment(started).isBetween(startDate, endDate, null, '[]')
|
||||
}
|
||||
@@ -136,7 +138,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
|
||||
const creditsOfStudent = ['320001', 'MH30_001'].includes(studyright[0]) ?
|
||||
creditsOfStudentLaakis : creditsOfStudentOther
|
||||
|
||||
|
||||
const students = await Student.findAll({
|
||||
attributes: ['firstnames', 'lastname', 'studentnumber',
|
||||
'dateofuniversityenrollment', 'creditcount', 'matriculationexamination',
|
||||
@@ -210,7 +212,18 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
model: TagStudent,
|
||||
attributes: ['id'],
|
||||
include: [
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['tag_id','tagname']
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
],
|
||||
where: {
|
||||
studentnumber: {
|
||||
@@ -352,18 +365,18 @@ const formatStudentsForApi = async (students, startDate, endDate, { studyRights
|
||||
stats.students.push(formatStudentForPopulationStatistics(student, startDate, endDate))
|
||||
return stats
|
||||
}, {
|
||||
students: [],
|
||||
extents: {},
|
||||
semesters: {},
|
||||
transfers: {
|
||||
targets: {},
|
||||
sources: {}
|
||||
},
|
||||
studyrights: {
|
||||
degrees: [],
|
||||
programmes: []
|
||||
}
|
||||
})
|
||||
students: [],
|
||||
extents: {},
|
||||
semesters: {},
|
||||
transfers: {
|
||||
targets: {},
|
||||
sources: {}
|
||||
},
|
||||
studyrights: {
|
||||
degrees: [],
|
||||
programmes: []
|
||||
}
|
||||
})
|
||||
|
||||
const transferredStudyright = (s) => {
|
||||
const studyright = s.studyrights.find(s => s.studyrightElements
|
||||
|
||||
@@ -21,6 +21,65 @@ const formatCredit = credit => {
|
||||
return { id, year, credits }
|
||||
}
|
||||
|
||||
const getTransferredCredits = (provider, since) => Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'credittypecode'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['code'],
|
||||
required: true,
|
||||
where: {
|
||||
is_study_module: false
|
||||
},
|
||||
include: {
|
||||
model: Provider,
|
||||
attributes: [],
|
||||
required: true,
|
||||
where: {
|
||||
providercode: provider
|
||||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
credittypecode: {
|
||||
[Op.eq]: [9]
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.gte]: since
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const getCreditsForMajors = (provider, since, studentnumbers) => Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'student_studentnumber'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['code'],
|
||||
required: true,
|
||||
where: {
|
||||
is_study_module: false
|
||||
},
|
||||
include: {
|
||||
model: Provider,
|
||||
attributes: [],
|
||||
required: true,
|
||||
where: {
|
||||
providercode: provider
|
||||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
credittypecode: {
|
||||
[Op.notIn]: [10, 9]
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.gte]: since
|
||||
},
|
||||
student_studentnumber: {
|
||||
[Op.in]: studentnumbers
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const getCreditsForProvider = (provider, since) => Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'credits', 'attainment_date'],
|
||||
include: {
|
||||
@@ -98,26 +157,10 @@ const graduatedStatsFromStudyrights = studyrights => {
|
||||
stats[year] = {
|
||||
graduated: graduated + 1,
|
||||
timesToGraduation: stats[year] ?
|
||||
[...stats[year].timesToGraduation, timeToGraduation || 0] : [ timeToGraduation || 0]
|
||||
[...stats[year].timesToGraduation, timeToGraduation || 0] : [timeToGraduation || 0]
|
||||
}
|
||||
graduationTimes = [...graduationTimes, timeToGraduation || 0]
|
||||
})
|
||||
const median = (values) => {
|
||||
if (values.length === 0) return 0
|
||||
|
||||
values.sort((a, b) => a - b)
|
||||
|
||||
var half = Math.floor(values.length / 2)
|
||||
|
||||
if (values.length % 2)
|
||||
return values[half]
|
||||
|
||||
return (values[half - 1] + values[half]) / 2.0
|
||||
}
|
||||
Object.keys(stats).forEach(year => {
|
||||
stats[year].medianGraduationTime = median(stats[year].timesToGraduation)
|
||||
})
|
||||
stats['medianGraduationTime'] = median(graduationTimes)
|
||||
return stats
|
||||
}
|
||||
|
||||
@@ -171,32 +214,40 @@ const thesisProductivityForStudytrack = async code => {
|
||||
return thesisProductivityFromCredits(credits)
|
||||
}
|
||||
|
||||
const combineStatistics = (creditStats, studyrightStats, thesisStats) => {
|
||||
const combineStatistics = (creditStats, studyrightStats, thesisStats, creditsForMajors, transferredCredits) => {
|
||||
const stats = { ...creditStats }
|
||||
Object.keys(stats).forEach(year => {
|
||||
const thesis = thesisStats[year] || {}
|
||||
stats[year].graduated = studyrightStats[year] ? studyrightStats[year].graduated : 0
|
||||
stats[year].medianGraduationTime = studyrightStats[year] ? studyrightStats[year].medianGraduationTime : 0
|
||||
// stats[year].medianGraduationTime = studyrightStats[year] ? studyrightStats[year].medianGraduationTime : 0
|
||||
stats[year].bThesis = thesis.bThesis || 0
|
||||
stats[year].mThesis = thesis.mThesis || 0
|
||||
stats[year].creditsForMajors = creditsForMajors[year] || 0
|
||||
stats[year].transferredCredits = transferredCredits[year] || 0
|
||||
})
|
||||
return Object.values(stats)
|
||||
}
|
||||
|
||||
const productivityStatsForStudytrack = async (studytrack, since) => {
|
||||
const providercode = studytrackToProviderCode(studytrack)
|
||||
const year = new Date(since).getFullYear()
|
||||
const startDate = `${year}-${semesterStart['FALL']}`
|
||||
const endDate = `${moment(since, 'YYYY').add(1, 'years').format('YYYY')}-${semesterEnd['SPRING']}`
|
||||
const studentnumbers = await studentnumbersWithAllStudyrightElements([studytrack], startDate, endDate, false, false)
|
||||
const promises = [
|
||||
graduatedStatsForStudytrack(studytrack, since),
|
||||
productivityStatsForProvider(providercode, since),
|
||||
thesisProductivityForStudytrack(studytrack)
|
||||
thesisProductivityForStudytrack(studytrack),
|
||||
productivityCreditsFromStudyprogrammeStudents(studytrack, since, studentnumbers),
|
||||
transferredCreditsForProductivity(studytrack, since)
|
||||
]
|
||||
const [studyrightStats, creditStats, thesisStats] = await Promise.all(
|
||||
const [studyrightStats, creditStats, thesisStats, creditsForMajors, transferredCredits] = await Promise.all(
|
||||
promises
|
||||
)
|
||||
return {
|
||||
id: studytrack,
|
||||
status: null,
|
||||
data: combineStatistics(creditStats, studyrightStats, thesisStats)
|
||||
data: combineStatistics(creditStats, studyrightStats, thesisStats, creditsForMajors, transferredCredits)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,13 +392,78 @@ const tranferredToStudyprogram = async (studentnumbers, startDate, studytrack, e
|
||||
})
|
||||
}
|
||||
|
||||
const productivityStats = async (studentnumbers, startDate, studytrack, endDate) => {
|
||||
return Promise.all([creditsAfter(studentnumbers, startDate),
|
||||
const endedStudyright = async (studentnumbers, startDate, studytrack, endDate) => {
|
||||
const enddate = new Date() < new Date(endDate) ? new Date() : new Date(endDate)
|
||||
return Studyright.findAndCountAll({
|
||||
include: {
|
||||
include: {
|
||||
model: ElementDetails,
|
||||
where: {
|
||||
type: {
|
||||
[Op.eq]: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: {
|
||||
[Op.eq]: studytrack
|
||||
},
|
||||
enddate: {
|
||||
[Op.gte]: new Date(startDate),
|
||||
[Op.lt]: enddate
|
||||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
student_studentnumber: {
|
||||
[Op.in]: studentnumbers
|
||||
},
|
||||
graduated: {
|
||||
[Op.eq]: 0
|
||||
},
|
||||
extentcode: {
|
||||
[Op.eq]: 5
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formatCreditsForProductivity = (credits) => {
|
||||
return credits.map(formatCredit).reduce(function (acc, curr) {
|
||||
var key = curr['year']
|
||||
if (!acc[key]) {
|
||||
acc[key] = []
|
||||
}
|
||||
acc[key] = Number(acc[key]) + Number(curr.credits)
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const productivityCreditsFromStudyprogrammeStudents = async (studytrack, startDate, studentnumbers) => {
|
||||
const providercode = studytrackToProviderCode(studytrack)
|
||||
const credits = await getCreditsForMajors(providercode, startDate, studentnumbers)
|
||||
const formattedStudentCredits = formatCreditsForProductivity(credits)
|
||||
return formattedStudentCredits
|
||||
}
|
||||
|
||||
const transferredCreditsForProductivity = async (studytrack, since) => {
|
||||
const providercode = studytrackToProviderCode(studytrack)
|
||||
const credits = await getTransferredCredits(providercode, since)
|
||||
const formattedCredits = formatCreditsForProductivity(credits)
|
||||
return formattedCredits
|
||||
}
|
||||
|
||||
const statsForClass = async (studentnumbers, startDate, studytrack, endDate) => {
|
||||
return Promise.all([
|
||||
creditsAfter(studentnumbers, startDate),
|
||||
graduationsFromClass(studentnumbers, studytrack),
|
||||
thesesFromClass(studentnumbers, startDate, studytrack),
|
||||
gendersFromClass(studentnumbers),
|
||||
countriesFromClass(studentnumbers),
|
||||
tranferredToStudyprogram(studentnumbers, startDate, studytrack, endDate)
|
||||
tranferredToStudyprogram(studentnumbers, startDate, studytrack, endDate),
|
||||
endedStudyright(studentnumbers, startDate, studytrack, endDate)
|
||||
])
|
||||
}
|
||||
|
||||
@@ -380,16 +496,33 @@ const throughputStatsForStudytrack = async (studytrack, since) => {
|
||||
const years = getYears(since)
|
||||
// studyprogramme starts with K if bachelors and M if masters
|
||||
const graduationTimeLimit = studytrack[0] === 'K' ? 36 : 24
|
||||
const median = (values) => {
|
||||
if (values.length === 0) return 0
|
||||
|
||||
values.sort((a, b) => a - b)
|
||||
|
||||
var half = Math.floor(values.length / 2)
|
||||
|
||||
if (values.length % 2)
|
||||
return values[half]
|
||||
|
||||
return (values[half - 1] + values[half]) / 2.0
|
||||
}
|
||||
let allGraduationTimes = []
|
||||
const arr = await Promise.all(years.map(async year => {
|
||||
const startDate = `${year}-${semesterStart['FALL']}`
|
||||
const endDate = `${moment(year, 'YYYY').add(1, 'years').format('YYYY')}-${semesterEnd['SPRING']}`
|
||||
const studentnumbers = await studentnumbersWithAllStudyrightElements([studytrack], startDate, endDate, false, false)
|
||||
const [credits, graduated, theses, genders, countries, transferred] =
|
||||
await productivityStats(studentnumbers, startDate, studytrack, endDate)
|
||||
const creditsForStudyprogramme =
|
||||
await productivityCreditsFromStudyprogrammeStudents(studytrack, startDate, studentnumbers)
|
||||
|
||||
const [credits, graduated, theses, genders, countries, transferredTo, endedStudyright] =
|
||||
await statsForClass(studentnumbers, startDate, studytrack, endDate)
|
||||
//console.log(year)
|
||||
//console.log(transferredFrom.rows.map(r => r.get({ plain: true })))
|
||||
// theres so much shit in the data that transefferFrom doesnt rly mean anything
|
||||
delete genders[null]
|
||||
delete countries[null]
|
||||
|
||||
const creditValues = credits.reduce((acc, curr) => {
|
||||
acc.mte30 = curr >= 30 ? acc.mte30 + 1 : acc.mte30
|
||||
acc.mte60 = curr >= 60 ? acc.mte60 + 1 : acc.mte60
|
||||
@@ -411,26 +544,33 @@ const throughputStatsForStudytrack = async (studytrack, since) => {
|
||||
totals.countries[countryKey] + Number(countries[countryKey]) :
|
||||
Number(countries[countryKey])
|
||||
})
|
||||
const inTargetTime = graduated.filter(g =>
|
||||
moment(g.enddate).diff(g.startstududate, 'months') <= graduationTimeLimit).length
|
||||
const graduationTimes = graduated.map(g => moment(g.enddate).diff(g.studystartdate, 'months'))
|
||||
const inTargetTime = graduationTimes.filter(time =>
|
||||
time <= graduationTimeLimit).length
|
||||
allGraduationTimes = [...allGraduationTimes, ...graduationTimes]
|
||||
|
||||
totals.thesisM = theses.MASTER ? totals.thesisM + theses.MASTER : totals.thesisM
|
||||
totals.thesisB = theses.BACHELOR ? totals.thesisB + theses.BACHELOR : totals.thesisB
|
||||
totals.students = totals.students + credits.length
|
||||
totals.graduated = totals.graduated + graduated.length,
|
||||
totals.inTargetTime = totals.inTargetTime + inTargetTime,
|
||||
totals.transferred = totals.transferred + transferred.count
|
||||
totals.ended = totals.ended + endedStudyright.count,
|
||||
totals.medianGraduationTime = median(allGraduationTimes)
|
||||
totals.inTargetTime = totals.inTargetTime + inTargetTime
|
||||
totals.transferred = totals.transferred + transferredTo.count
|
||||
return {
|
||||
year: `${year}-${year + 1}`,
|
||||
credits: credits.map(cr => cr === null ? 0 : cr),
|
||||
creditsForStudyprogramme: creditsForStudyprogramme,
|
||||
graduated: graduated.length,
|
||||
medianGraduationTime: median(graduationTimes),
|
||||
inTargetTime,
|
||||
thesisM: theses.MASTER || 0,
|
||||
thesisB: theses.BACHELOR || 0,
|
||||
genders,
|
||||
countries,
|
||||
creditValues,
|
||||
transferred: transferred.count
|
||||
transferred: transferredTo.count,
|
||||
ended: endedStudyright.count
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
@@ -125,8 +125,8 @@ test('graduatedStatsFromStudyrights calculates stats correctly', () => {
|
||||
]
|
||||
const stats = graduatedStatsFromStudyrights(studyrights)
|
||||
expect(stats).toMatchObject({
|
||||
2015: { graduated: 2., medianGraduationTime: 0, timesToGraduation: [ 0, 0 ]},
|
||||
2014: { graduated: 1., medianGraduationTime: 0, timesToGraduation: [ 0 ]}
|
||||
2015: { graduated: 2. },
|
||||
2014: { graduated: 1. }
|
||||
})
|
||||
})
|
||||
|
||||
@@ -137,21 +137,31 @@ test('combineStatistics returns correctly formatted array', () => {
|
||||
2014: { year: 2014, credits: 20 }
|
||||
}
|
||||
const studyrightStats = {
|
||||
2015: { graduated: 2, medianGraduationTime: 1.5, timesToGraduation: [1, 2] },
|
||||
2016: { graduated: 1, medianGraduationTime: 0, timesToGraduation: [ 0 ] }
|
||||
2015: { graduated: 2 },
|
||||
2016: { graduated: 1 }
|
||||
}
|
||||
const thesisStats = {
|
||||
2014: { mThesis: 1 },
|
||||
2015: { mThesis: 2, bThesis: 1 }
|
||||
}
|
||||
const stats = combineStatistics(creditStats, studyrightStats, thesisStats)
|
||||
const creditsForMajors = {
|
||||
2014: 10,
|
||||
2015: 22
|
||||
}
|
||||
const transferredCredits = {
|
||||
2014: 2,
|
||||
2015: 4
|
||||
}
|
||||
|
||||
const stats = combineStatistics(creditStats, studyrightStats, thesisStats, creditsForMajors, transferredCredits)
|
||||
expect(stats).toContainEqual({
|
||||
year: 2015,
|
||||
mThesis: 2,
|
||||
bThesis: 1,
|
||||
credits: 40,
|
||||
medianGraduationTime: 1.5,
|
||||
graduated: 2
|
||||
graduated: 2,
|
||||
creditsForMajors: 22,
|
||||
transferredCredits: 4
|
||||
})
|
||||
expect(stats).toContainEqual({
|
||||
year: 2014,
|
||||
@@ -159,7 +169,8 @@ test('combineStatistics returns correctly formatted array', () => {
|
||||
bThesis: 0,
|
||||
credits: 20,
|
||||
graduated: 0,
|
||||
medianGraduationTime: 0
|
||||
creditsForMajors: 10,
|
||||
transferredCredits: 2
|
||||
})
|
||||
expect(stats).toContainEqual({
|
||||
year: 2016,
|
||||
@@ -167,7 +178,8 @@ test('combineStatistics returns correctly formatted array', () => {
|
||||
bThesis: 0,
|
||||
credits: 5,
|
||||
graduated: 1,
|
||||
medianGraduationTime: 0
|
||||
creditsForMajors: 0,
|
||||
transferredCredits: 0
|
||||
})
|
||||
})
|
||||
|
||||
@@ -179,7 +191,8 @@ test('productivityStatsForStudytrack integrates', async () => {
|
||||
bThesis: 0,
|
||||
mThesis: 1,
|
||||
credits: 40,
|
||||
medianGraduationTime: 0
|
||||
creditsForMajors: 0,
|
||||
transferredCredits: 0
|
||||
})
|
||||
expect(stats.data).toContainEqual({
|
||||
year: 2016,
|
||||
@@ -187,7 +200,8 @@ test('productivityStatsForStudytrack integrates', async () => {
|
||||
mThesis: 0,
|
||||
bThesis: 0,
|
||||
credits: 5,
|
||||
medianGraduationTime: 0
|
||||
creditsForMajors: 0,
|
||||
transferredCredits: 0
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Tag } = require('../models')
|
||||
|
||||
const Op = Sequelize.Op
|
||||
|
||||
const findTags = () => Tag.findAll({})
|
||||
|
||||
const findTagsByStudytrack = (studytrack) => {
|
||||
return Tag.findAll({
|
||||
where: {
|
||||
studytrack: {
|
||||
[Op.eq]: studytrack
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const createNewTag = async (tag) => {
|
||||
return Tag.create(tag)
|
||||
}
|
||||
|
||||
const deleteTag = async (tag) => {
|
||||
return Tag.destroy({
|
||||
where: {
|
||||
tag_id: {
|
||||
[Op.eq]: tag.tag_id
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createNewTag,
|
||||
findTags,
|
||||
findTagsByStudytrack,
|
||||
deleteTag
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { TagStudent, Tag } = require('../models')
|
||||
|
||||
const Op = Sequelize.Op
|
||||
|
||||
const getStudentTags = () => TagStudent.findAll({})
|
||||
|
||||
const getStudentTagsByStudytrack = (studytrack) => {
|
||||
return TagStudent.findAll({
|
||||
include: {
|
||||
model: Tag,
|
||||
attributes: ['tag_id', 'tagname'],
|
||||
where: {
|
||||
studytrack: {
|
||||
[Op.eq]: studytrack
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getStudentTagsByStudentnumber = async (studentnumber) => {
|
||||
return TagStudent.findAll({
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.eq]: studentnumber
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const createStudentTag = async (tag) => {
|
||||
return TagStudent.create(tag)
|
||||
}
|
||||
|
||||
const deleteStudentTag = async (id) => {
|
||||
return TagStudent.destroy({
|
||||
where: {
|
||||
id: {
|
||||
[Op.eq]: id
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getStudentTags,
|
||||
getStudentTagsByStudentnumber,
|
||||
getStudentTagsByStudytrack,
|
||||
createStudentTag,
|
||||
deleteStudentTag
|
||||
}
|
||||
@@ -22,7 +22,7 @@ const findAllEnabled = async () => {
|
||||
|
||||
const login = async (uid, full_name, hyGroups, affiliations, email) => {
|
||||
const response = await client.post('/login', {
|
||||
uid, full_name, hyGroups, affiliations, email,
|
||||
uid, full_name, hyGroups, affiliations, email,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
@@ -83,6 +83,11 @@ const removeElementDetails = async (uid, codes) => {
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
const setFaculties = async (uid, faculties) => {
|
||||
const response = await client.post('/set_faculties', { uid, faculties })
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
const getUnitsFromElementDetails = async username => {
|
||||
const url = `/user/elementdetails/${username}`
|
||||
const response = await client.get(url)
|
||||
@@ -116,5 +121,6 @@ module.exports = {
|
||||
modifyAccess,
|
||||
getAccessGroups,
|
||||
getUnitsFromElementDetails,
|
||||
getRolesFor
|
||||
getRolesFor,
|
||||
setFaculties
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
const conf = require('../../conf-backend')
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
return queryInterface.sequelize.transaction(async transaction => {
|
||||
await queryInterface.createTable(
|
||||
'mandatory_course_labels',
|
||||
{
|
||||
studyprogramme_id: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true
|
||||
},
|
||||
label: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
orderNumber: {
|
||||
type: Sequelize.INTEGER
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
},
|
||||
{ transaction }
|
||||
)
|
||||
await queryInterface.addIndex('mandatory_course_labels', ['studyprogramme_id'], { transaction })
|
||||
// Temporarily hardcode schema here since sequelize has issues:
|
||||
// https://github.com/sequelize/sequelize/issues/10875
|
||||
await queryInterface.removeColumn({ tableName: 'mandatory_courses', schema: conf.DB_SCHEMA_KONE },
|
||||
'label', { transaction, }
|
||||
)
|
||||
await queryInterface.addColumn(
|
||||
{ tableName: 'mandatory_courses', schema: 'kone_data' },
|
||||
'label',
|
||||
{
|
||||
type: Sequelize.BIGINT,
|
||||
references: {
|
||||
model: 'mandatory_course_labels',
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
{ transaction }
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
down: async () => {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface) => {
|
||||
await queryInterface.dropTable('tag')
|
||||
await queryInterface.dropTable('tag_student')
|
||||
},
|
||||
down: async () => {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('tag', {
|
||||
createdAt: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
tagname: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
tag_id: {
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
studytrack: {
|
||||
type: Sequelize.STRING,
|
||||
primaryKey: true,
|
||||
}
|
||||
})
|
||||
},
|
||||
down: async () => {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn({ tableName: 'tag', schema: 'kone_data' }, 'tag_id')
|
||||
await queryInterface.addColumn({ tableName: 'tag', schema: 'kone_data' }, 'tag_id', { type: Sequelize.BIGINT, primaryKey: true, allowNull: false, autoIncrement: true, unique:true })
|
||||
},
|
||||
down: async () => {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('tag_student', {
|
||||
createdAt: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
tag_id: {
|
||||
type: Sequelize.BIGINT,
|
||||
primaryKey: true,
|
||||
reference: {
|
||||
model: 'tag',
|
||||
key: 'tag_id'
|
||||
}
|
||||
},
|
||||
studentnumber: {
|
||||
type: Sequelize.STRING
|
||||
}
|
||||
})
|
||||
},
|
||||
down: async () => {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn({ tableName: 'tag_student', schema: 'kone_data' }, 'tag_id')
|
||||
await queryInterface.addColumn({ tableName: 'tag_student', schema: 'kone_data' }, 'id', { type: Sequelize.BIGINT, primaryKey: true, allowNull: false, autoIncrement: true })
|
||||
await queryInterface.sequelize.query(`ALTER TABLE tag_student ADD PRIMARY KEY (id);`)
|
||||
await queryInterface.addColumn({ tableName: 'tag_student', schema: 'kone_data' }, 'tag_id', {
|
||||
type: Sequelize.BIGINT,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'tag',
|
||||
key: 'tag_id'
|
||||
},
|
||||
})
|
||||
},
|
||||
down: async () => {
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,9 @@ const CourseGroup = sequelize.define('course_groups', {
|
||||
unique: true
|
||||
}
|
||||
},
|
||||
{
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
})
|
||||
{
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
})
|
||||
|
||||
const TeacherCourseGroup = sequelize.define('teacher_course_groups', {
|
||||
course_group_id: {
|
||||
@@ -30,9 +30,9 @@ const TeacherCourseGroup = sequelize.define('teacher_course_groups', {
|
||||
type: Sequelize.STRING,
|
||||
}
|
||||
},
|
||||
{
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
})
|
||||
{
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
})
|
||||
|
||||
const MandatoryCourse = sequelize.define('mandatory_courses', {
|
||||
course_code: {
|
||||
@@ -42,12 +42,12 @@ const MandatoryCourse = sequelize.define('mandatory_courses', {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
label: {
|
||||
type: Sequelize.STRING
|
||||
type: Sequelize.BIGINT
|
||||
}
|
||||
},
|
||||
{
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
})
|
||||
{
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
})
|
||||
|
||||
const Filters = sequelize.define('filters',
|
||||
{
|
||||
@@ -104,33 +104,69 @@ const UsageStatistic = sequelize.define('usage_statistics', {
|
||||
},
|
||||
|
||||
}, {
|
||||
timestamps: false,
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
})
|
||||
timestamps: false,
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
})
|
||||
|
||||
const TagStudent = sequelize.define('tag_student',
|
||||
{
|
||||
taggedstudents_studentnumber: { type: Sequelize.STRING },
|
||||
tags_tagname: { type: Sequelize.STRING },
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true
|
||||
},
|
||||
studentnumber: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
tag_id: {
|
||||
type: Sequelize.BIGINT,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'tag_student',
|
||||
timestamps: false,
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
}
|
||||
)
|
||||
TagStudent.removeAttribute('id')
|
||||
|
||||
const Tag = sequelize.define('tag',
|
||||
{
|
||||
tagname: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
tag_id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true
|
||||
},
|
||||
studytrack: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING
|
||||
}
|
||||
},
|
||||
{
|
||||
tableName: 'tag',
|
||||
timestamps: false,
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
}
|
||||
)
|
||||
|
||||
const MandatoryCourseLabels = sequelize.define('mandatory_course_labels',
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true
|
||||
},
|
||||
studyprogramme_id: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
label: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
orderNumber: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
},
|
||||
{
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
}
|
||||
)
|
||||
@@ -141,10 +177,10 @@ const MigrationKone = sequelize.define('migrationsKone', {
|
||||
primaryKey: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'migrations',
|
||||
timestamps: false,
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
})
|
||||
tableName: 'migrations',
|
||||
timestamps: false,
|
||||
schema: conf.DB_SCHEMA_KONE
|
||||
})
|
||||
|
||||
const Student = sequelize.define('student',
|
||||
{
|
||||
@@ -463,16 +499,16 @@ const SemesterEnrollment = sequelize.define('semester_enrollment', {
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
}, {
|
||||
indexes: [
|
||||
{
|
||||
fields: ['semestercode', 'studentnumber'],
|
||||
unique: true
|
||||
}, {
|
||||
fields: ['studentnumber'],
|
||||
name: 'semester_enrollment_studentnumber'
|
||||
}
|
||||
]
|
||||
})
|
||||
indexes: [
|
||||
{
|
||||
fields: ['semestercode', 'studentnumber'],
|
||||
unique: true
|
||||
}, {
|
||||
fields: ['studentnumber'],
|
||||
name: 'semester_enrollment_studentnumber'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const Provider = sequelize.define('provider', {
|
||||
providercode: {
|
||||
@@ -489,13 +525,13 @@ const Transfers = sequelize.define('transfers', {
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
}, {
|
||||
indexes: [
|
||||
{
|
||||
fields: ['studentnumber'],
|
||||
name: 'transfers_studentnumber'
|
||||
}
|
||||
]
|
||||
})
|
||||
indexes: [
|
||||
{
|
||||
fields: ['studentnumber'],
|
||||
name: 'transfers_studentnumber'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const CourseProvider = sequelize.define('course_providers', {}, {
|
||||
indexes: [
|
||||
@@ -549,9 +585,9 @@ const Migration = sequelize.define('migrations', {
|
||||
primaryKey: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'migrations',
|
||||
timestamps: false
|
||||
})
|
||||
tableName: 'migrations',
|
||||
timestamps: false
|
||||
})
|
||||
|
||||
const CreditTeacher = sequelize.define('credit_teacher', {
|
||||
credit_id: {
|
||||
@@ -617,9 +653,15 @@ Credit.belongsTo(Course, { foreignKey: 'course_code' })
|
||||
Course.hasMany(Credit, { foreignKey: 'course_code' })
|
||||
|
||||
MandatoryCourse.belongsTo(Course, { foreignKey: 'course_code' })
|
||||
MandatoryCourse.belongsTo(MandatoryCourseLabels, { foreignKey: 'label', sourceKey: 'id' })
|
||||
MandatoryCourseLabels.hasMany(MandatoryCourse, { foreignKey: 'label', sourceKey: 'id' })
|
||||
|
||||
Student.hasMany(TagStudent, { foreignKey: 'taggedstudents_studentnumber', sourceKey: 'studentnumber' })
|
||||
Tag.hasMany(TagStudent, { foreignKey: 'tags_tagname', sourceKey: 'tagname' })
|
||||
TagStudent.belongsTo(Tag, { foreignKey: 'tag_id', sourceKey: 'tag_id' })
|
||||
TagStudent.belongsTo(Student, { foreignKey: 'studentnumber' })
|
||||
Student.hasMany(TagStudent, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
Tag.hasOne(ElementDetails, { foreignKey: 'code', sourceKey: 'studytrack' })
|
||||
Tag.hasMany(TagStudent, { foreignKey: 'tag_id', sourceKey: 'tag_id' })
|
||||
|
||||
Studyright.belongsTo(Student, { foreignKey: 'student_studentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(Studyright, { foreignKey: 'student_studentnumber', sourceKey: 'studentnumber' })
|
||||
@@ -723,5 +765,6 @@ module.exports = {
|
||||
ThesisCourse,
|
||||
CourseDuplicates,
|
||||
ThesisTypeEnums,
|
||||
MandatoryCourseLabels,
|
||||
ErrorData
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
FROM node:10.15-alpine
|
||||
|
||||
RUN apk add postgresql-client
|
||||
|
||||
COPY ./updater_writer /usr/src/app
|
||||
COPY ./shared/models /usr/src/app/models
|
||||
COPY ./shared/migrations /usr/src/app/database/migrations
|
||||
COPY ./shared/migrations_kone /usr/src/app/database/migrations_kone
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
RUN npm ci
|
||||
|
||||
COPY ./updater_writer/package.json /usr/src/cache/package.json
|
||||
COPY ./updater_writer/package-lock.json /usr/src/cache/package-lock.json
|
||||
|
||||
|
||||
@@ -1,26 +1,43 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const conf = require('../conf-backend')
|
||||
|
||||
console.log('SCHEMA', conf.DB_SCHEMA)
|
||||
|
||||
const sequelize = new Sequelize(conf.DB_URL, {
|
||||
schema: conf.DB_SCHEMA,
|
||||
searchPath: conf.DB_SCHEMA,
|
||||
logging: false,
|
||||
operatorsAliases: false
|
||||
})
|
||||
sequelize.query(`SET SESSION search_path to ${conf.DB_SCHEMA}`)
|
||||
|
||||
const createSchemaIfNotExists = async schema => {
|
||||
try {
|
||||
await sequelize.createSchema(schema)
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
const sequelizeKone = new Sequelize(conf.DB_URL, {
|
||||
schema: conf.DB_SCHEMA_KONE,
|
||||
searchPath: conf.DB_SCHEMA_KONE,
|
||||
logging: false,
|
||||
operatorsAliases: false
|
||||
})
|
||||
sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
|
||||
// See https://github.com/sequelize/sequelize/issues/10875
|
||||
|
||||
}
|
||||
|
||||
const forceSyncDatabase = async () => {
|
||||
await createSchemaIfNotExists(conf.DB_SCHEMA)
|
||||
try {
|
||||
await sequelize.getQueryInterface().createSchema(conf.DB_SCHEMA)
|
||||
} catch (e) {
|
||||
// console.log(e)
|
||||
}
|
||||
try {
|
||||
await sequelizeKone.getQueryInterface().createSchema(conf.DB_SCHEMA_KONE)
|
||||
} catch (e) {
|
||||
// console.log(e)
|
||||
}
|
||||
console.log('OODI SYNC')
|
||||
await sequelize.sync({ force: true })
|
||||
console.log('KONE SYNC')
|
||||
await sequelizeKone.sync({ force: true })
|
||||
}
|
||||
|
||||
module.exports = { sequelize, forceSyncDatabase }
|
||||
module.exports = {
|
||||
sequelize,
|
||||
sequelizeKone,
|
||||
forceSyncDatabase
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
const spawn = require('child_process').spawn
|
||||
const fs = require('fs')
|
||||
const { sync } = require('./force_sync_database')
|
||||
const { DB_SCHEMA } = require('../conf-backend')
|
||||
|
||||
const dumpDatabase = async () => {
|
||||
const { DB_URL } = process.env
|
||||
const dumpOptions = ['-Fc', `-n${DB_SCHEMA}`, `${DB_URL}`]
|
||||
console.log(dumpOptions)
|
||||
const dumpProcess = spawn('pg_dump', dumpOptions, { stdio: ['ignore', 'pipe', 'inherit'] })
|
||||
dumpProcess.on('error', (err) => {
|
||||
console.log(' dump process error:', err)
|
||||
})
|
||||
const writeStream = fs.createWriteStream(`./dumps/oodidb_prod-${new Date().toISOString()}`)
|
||||
|
||||
dumpProcess.stdout.pipe(writeStream)
|
||||
|
||||
dumpProcess.on('exit', async (code) => {
|
||||
if (code !== 0) {
|
||||
throw new Error('pg_dump: Bad exit code (' + code + ')');
|
||||
} else {
|
||||
console.log('database dump created, force syncing database')
|
||||
await sync()
|
||||
}
|
||||
})
|
||||
}
|
||||
module.exports = { dumpDatabase }
|
||||
@@ -0,0 +1,17 @@
|
||||
const { seedAllMigrations } = require('./seed_migrations')
|
||||
const { forceSyncDatabase } = require('./connection')
|
||||
|
||||
const sync = async () => {
|
||||
try {
|
||||
await forceSyncDatabase()
|
||||
await seedAllMigrations()
|
||||
console.log('Force sync succeeded. ')
|
||||
process.exit(0)
|
||||
} catch (e) {
|
||||
console.log(`Force sync failed: ${e}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
//sync()
|
||||
module.exports = { sync }
|
||||
@@ -1,20 +1,27 @@
|
||||
const fs = require('fs')
|
||||
const { Migration } = require('../models/index')
|
||||
const { Migration, MigrationKone } = require('../models/index')
|
||||
|
||||
const DEFAULT_PATH = `${process.cwd()}/database/migrations`
|
||||
const DEFAULT_PATH_KONE = `${process.cwd()}/database/migrations_kone`
|
||||
|
||||
const saveMigrationsToDatabase = async filenames => Promise.all(filenames.map(name => Migration.upsert({ name })))
|
||||
const saveMigrationsToDatabase = Migration => async filenames =>
|
||||
Promise.all(filenames.map(name => Migration.upsert({ name })))
|
||||
|
||||
const seedMigrations = async (migrationfilepath=DEFAULT_PATH) => {
|
||||
const filenames = fs.readdirSync(migrationfilepath)
|
||||
await saveMigrationsToDatabase(filenames)
|
||||
await saveMigrationsToDatabase(Migration)(filenames)
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
const seedMigrationsKone = async (migrationfilepath=DEFAULT_PATH_KONE) => {
|
||||
const filenames = fs.readdirSync(migrationfilepath)
|
||||
await saveMigrationsToDatabase(MigrationKone)(filenames)
|
||||
}
|
||||
|
||||
const seedAllMigrations = async () => {
|
||||
await seedMigrations()
|
||||
await seedMigrationsKone()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
run,
|
||||
seedMigrations
|
||||
seedAllMigrations
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
const stan = require('node-nats-streaming').connect('updaterNATS', process.env.HOSTNAME, process.env.NATS_URI);
|
||||
const { updateStudent, updateMeta, updateAttainmentMeta } = require('./updater/database_updater')
|
||||
const { dumpDatabase } = require('./database/dump_database')
|
||||
const fs = require('fs')
|
||||
|
||||
console.log(`STARTING WITH ${process.env.HOSTNAME} as id`)
|
||||
@@ -13,11 +14,12 @@ stan.on('connect', function () {
|
||||
|
||||
const sub = stan.subscribe('UpdateWrite', 'updater.workers', opts)
|
||||
const attSub = stan.subscribe('UpdateAttainmentDates', opts)
|
||||
const dumpSub = stan.subscribe('DumpDatabase')
|
||||
|
||||
sub.on('message', async (msg) => {
|
||||
const data = JSON.parse(msg.getData())
|
||||
if (data.studentInfo) {
|
||||
await updateStudent(data)
|
||||
await updateStudent(data, stan)
|
||||
} else {
|
||||
await updateMeta(data)
|
||||
}
|
||||
@@ -28,5 +30,14 @@ stan.on('connect', function () {
|
||||
await updateAttainmentMeta()
|
||||
msg.ack()
|
||||
})
|
||||
|
||||
dumpSub.on('message', async (_) => {
|
||||
await dumpDatabase()
|
||||
stan.publish('ScheduleAll', null, (err, guid) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
} else {
|
||||
console.log('calling re-scheduling database')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,9 @@
|
||||
"test": "NODE_ENV=test jest --runInBand --verbose",
|
||||
"test_docker": "docker exec -t updater_writer /bin/bash -c 'npm test'",
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js"
|
||||
"dev": "nodemon index.js",
|
||||
"force_sync": "node -e \"require('./database/force_sync_database').sync()\"",
|
||||
"dump": "node -e \"require('./database/dump_database').dumpDatabase()\""
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
|
||||
@@ -51,7 +51,7 @@ const updateStudyRights = (studyRights, transaction) => studyRights.map(async ({
|
||||
])
|
||||
})
|
||||
|
||||
const updateStudent = async (student) => {
|
||||
const updateStudent = async (student, stan) => {
|
||||
const { studentInfo, studyAttainments, semesterEnrollments, studyRights } = student
|
||||
const transaction = await sequelize.transaction()
|
||||
try {
|
||||
@@ -72,6 +72,7 @@ const updateStudent = async (student) => {
|
||||
console.log('Transaction aborted')
|
||||
} else if (err.message === 'deadlock detected') {
|
||||
console.log('Deadlock suicide')
|
||||
stan.close()
|
||||
process.exit(1)
|
||||
} else {
|
||||
console.log(err.parent)
|
||||
|
||||
+67
-23
@@ -117,7 +117,7 @@
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
|
||||
},
|
||||
"ms": {
|
||||
@@ -5060,7 +5060,7 @@
|
||||
},
|
||||
"util": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
|
||||
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -5115,12 +5115,35 @@
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
|
||||
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
|
||||
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.3.0",
|
||||
"is-buffer": "^1.1.5"
|
||||
"follow-redirects": "1.5.10",
|
||||
"is-buffer": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||
"requires": {
|
||||
"debug": "=3.1.0"
|
||||
}
|
||||
},
|
||||
"is-buffer": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
|
||||
"integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"axobject-query": {
|
||||
@@ -6399,7 +6422,7 @@
|
||||
"dependencies": {
|
||||
"callsites": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
|
||||
"integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=",
|
||||
"dev": true
|
||||
}
|
||||
@@ -9542,6 +9565,7 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz",
|
||||
"integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^3.1.0"
|
||||
},
|
||||
@@ -9550,6 +9574,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@@ -9665,7 +9690,8 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@@ -9686,12 +9712,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -9706,17 +9734,20 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -9833,7 +9864,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -9845,6 +9877,7 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -9859,6 +9892,7 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -9866,12 +9900,14 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -9890,6 +9926,7 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -9970,7 +10007,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -9982,6 +10020,7 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -10067,7 +10106,8 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -10103,6 +10143,7 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -10122,6 +10163,7 @@
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -10165,12 +10207,14 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -16755,7 +16799,7 @@
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -16770,7 +16814,7 @@
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -17979,7 +18023,7 @@
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
@@ -17993,7 +18037,7 @@
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"@babel/preset-env": "^7.4.4",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@sentry/browser": "^4.5.3",
|
||||
"axios": "^0.18.0",
|
||||
"axios": "^0.18.1",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"dotenv": "^6.2.0",
|
||||
"highcharts": "^6.2.0",
|
||||
|
||||
@@ -151,9 +151,7 @@ export const handleRequest = store => next => async (action) => {
|
||||
}
|
||||
|
||||
export const logout = async () => {
|
||||
const stagingPath = '/staging'
|
||||
const returnUrl = window.location.pathname.includes(stagingPath) ?
|
||||
`${window.location.origin}${stagingPath}` : window.location.origin
|
||||
const returnUrl = window.location.origin
|
||||
const response = await getAxios().delete('/logout', { data: { returnUrl } })
|
||||
localStorage.removeItem(TOKEN_NAME)
|
||||
window.location = response.data.logoutUrl
|
||||
|
||||
@@ -165,9 +165,8 @@ export const getStudentTotalCredits = student => student.courses
|
||||
|
||||
export const getStudentGradeMean = (student) => {
|
||||
const gradedCourses = student.courses.filter(c => Number(c.grade))
|
||||
const gradeTotal = gradedCourses.reduce((a, b) => a + b.grade, 0)
|
||||
const gradeTotal = gradedCourses.reduce((a, b) => a + Number(b.grade), 0)
|
||||
const mean = gradeTotal / gradedCourses.length || 0
|
||||
|
||||
return mean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import _ from 'lodash'
|
||||
import { Segment, Table, Button } from 'semantic-ui-react'
|
||||
import { Segment, Table } from 'semantic-ui-react'
|
||||
import { func, arrayOf, shape, string, bool } from 'prop-types'
|
||||
import { getActiveYears } from '../courseStatisticsUtils'
|
||||
|
||||
import './courseTable.css'
|
||||
|
||||
const CourseTable = ({ courses, onSelectCourse, hidden, title, emptyListText, controlIcon, mandatory = false }) => {
|
||||
const CourseTable = ({ courses, onSelectCourse, hidden, title, emptyListText, mandatory = false }) => {
|
||||
const noContent = courses.length === 0
|
||||
const sortedCourses = !noContent && _.sortBy(courses, course => course.name)
|
||||
|
||||
@@ -17,33 +17,23 @@ const CourseTable = ({ courses, onSelectCourse, hidden, title, emptyListText, co
|
||||
)
|
||||
|
||||
const toCourseRow = course => (
|
||||
<Table.Row key={course.code}>
|
||||
<Table.Row style={{ cursor: 'pointer' }} key={course.code} onClick={() => (course.min_attainment_date || mandatory ? onSelectCourse(course) : null)} >
|
||||
<Table.Cell width={10}>
|
||||
<div>{course.name}</div>
|
||||
<div>{getActiveYears(course)}</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell content={course.code} />
|
||||
<Table.Cell>
|
||||
{course.min_attainment_date || mandatory ?
|
||||
<Button
|
||||
basic
|
||||
className="controlIcon"
|
||||
icon={controlIcon}
|
||||
onClick={() => onSelectCourse(course)}
|
||||
/> : null }
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
)
|
||||
|
||||
return (
|
||||
!hidden && (
|
||||
<Segment basic style={{ padding: '0' }} >
|
||||
<Table>
|
||||
<Table selectable>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell content={title} />
|
||||
<Table.HeaderCell content="Code" />
|
||||
<Table.HeaderCell content="Select" />
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ const getHeader = (categoryName) => {
|
||||
}
|
||||
|
||||
const CumulativeTable = ({ categoryName, data, onClickCourse }) => (
|
||||
<Table>
|
||||
<Table style={{ cursor: 'pointer' }} selectable>
|
||||
{getHeader(categoryName)}
|
||||
<Table.Body>
|
||||
{ data.map(course =>
|
||||
|
||||
+86
-46
@@ -1,59 +1,83 @@
|
||||
/* eslint-disable quotes */
|
||||
import React from 'react'
|
||||
import { Grid } from 'semantic-ui-react'
|
||||
import { bool } from 'prop-types'
|
||||
import { gradeGraphOptions } from '../../../../constants'
|
||||
import {
|
||||
viewModeNames,
|
||||
graphSeriesTypes,
|
||||
getDataObject,
|
||||
getMaxValueOfSeries,
|
||||
dataSeriesType,
|
||||
viewModeType,
|
||||
getGradeSpread,
|
||||
getThesisGradeSpread,
|
||||
isThesisSeries
|
||||
isThesisSeries,
|
||||
absoluteToRelative
|
||||
} from './util'
|
||||
import StackedBarChart from '../../../StackedBarChart'
|
||||
|
||||
const getGradeSeries = (series, seriesType) => {
|
||||
const { name, multiplier } = seriesType
|
||||
const getGradeSeries = (series) => {
|
||||
const isGradeSeries = !isThesisSeries(series)
|
||||
const newSeries = isGradeSeries
|
||||
? getGradeSpread(series, multiplier)
|
||||
: getThesisGradeSpread(series, multiplier)
|
||||
|
||||
const newSeries = isGradeSeries ?
|
||||
getGradeSpread(series)
|
||||
: getThesisGradeSpread(series)
|
||||
const sumAll = Object.values(newSeries)[0].map((_, idx) => Object.values(newSeries).map(serie => serie[idx]).reduce((a, b) => a + b, 0))
|
||||
return isGradeSeries
|
||||
? [
|
||||
getDataObject(`${name} 0`, newSeries[0], 'a'),
|
||||
getDataObject(`${name} 1`, newSeries[1], 'b'),
|
||||
getDataObject(`${name} 2`, newSeries[2], 'c'),
|
||||
getDataObject(`${name} 3`, newSeries[3], 'd'),
|
||||
getDataObject(`${name} 4`, newSeries[4], 'e'),
|
||||
getDataObject(`${name} 5`, newSeries[5], 'f'),
|
||||
getDataObject(`${name} Hyv.`, newSeries['Hyv.'], 'g')
|
||||
]
|
||||
: [
|
||||
getDataObject(`${name} I`, newSeries.I, 'a'),
|
||||
getDataObject(`${name} A`, newSeries.A, 'b'),
|
||||
getDataObject(`${name} NSLA`, newSeries.NSLA, 'c'),
|
||||
getDataObject(`${name} LUB`, newSeries.LUB, 'd'),
|
||||
getDataObject(`${name} CL`, newSeries.CL, 'e'),
|
||||
getDataObject(`${name} MCLA`, newSeries.MCLA, 'f'),
|
||||
getDataObject(`${name} ECLA`, newSeries.ECLA, 'g'),
|
||||
getDataObject(`${name} L`, newSeries.L, 'h')
|
||||
]
|
||||
? {
|
||||
absolute: [
|
||||
getDataObject('0', newSeries[0], 'a'),
|
||||
getDataObject('1', newSeries[1], 'b'),
|
||||
getDataObject('2', newSeries[2], 'c'),
|
||||
getDataObject('3', newSeries[3], 'd'),
|
||||
getDataObject('4', newSeries[4], 'e'),
|
||||
getDataObject('5', newSeries[5], 'f'),
|
||||
getDataObject('Hyv.', newSeries['Hyv.'], 'g')
|
||||
],
|
||||
relative: [
|
||||
getDataObject('0', newSeries[0].map(absoluteToRelative(sumAll)), 'a'),
|
||||
getDataObject('1', newSeries[1].map(absoluteToRelative(sumAll)), 'b'),
|
||||
getDataObject('2', newSeries[2].map(absoluteToRelative(sumAll)), 'c'),
|
||||
getDataObject('3', newSeries[3].map(absoluteToRelative(sumAll)), 'd'),
|
||||
getDataObject('4', newSeries[4].map(absoluteToRelative(sumAll)), 'e'),
|
||||
getDataObject('5', newSeries[5].map(absoluteToRelative(sumAll)), 'f'),
|
||||
getDataObject('Hyv.', newSeries['Hyv.'].map(absoluteToRelative(sumAll)), 'g')
|
||||
]
|
||||
}
|
||||
: {
|
||||
absolute: [
|
||||
getDataObject(' I', newSeries.I, 'a'),
|
||||
getDataObject('A', newSeries.A, 'b'),
|
||||
getDataObject('NSLA', newSeries.NSLA, 'c'),
|
||||
getDataObject('LUB', newSeries.LUB, 'd'),
|
||||
getDataObject('CL', newSeries.CL, 'e'),
|
||||
getDataObject('MCLA', newSeries.MCLA, 'f'),
|
||||
getDataObject('ECLA', newSeries.ECLA, 'g'),
|
||||
getDataObject('L', newSeries.L, 'h')
|
||||
],
|
||||
relative: [
|
||||
getDataObject(' I', newSeries.I.map(absoluteToRelative(sumAll)), 'a'),
|
||||
getDataObject('A', newSeries.A.map(absoluteToRelative(sumAll)), 'b'),
|
||||
getDataObject('NSLA', newSeries.NSLA.map(absoluteToRelative(sumAll)), 'c'),
|
||||
getDataObject('LUB', newSeries.LUB.map(absoluteToRelative(sumAll)), 'd'),
|
||||
getDataObject('CL', newSeries.CL.map(absoluteToRelative(sumAll)), 'e'),
|
||||
getDataObject('MCLA', newSeries.MCLA.map(absoluteToRelative(sumAll)), 'f'),
|
||||
getDataObject('ECLA', newSeries.ECLA.map(absoluteToRelative(sumAll)), 'g'),
|
||||
getDataObject('L', newSeries.L.map(absoluteToRelative(sumAll)), 'h')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const getGradeCumSeriesFromStats = (stats, seriesType) => {
|
||||
const getGradeCumSeriesFromStats = (stats) => {
|
||||
const series = stats.flatMap(s => s.cumulative.grades)
|
||||
return getGradeSeries(series, seriesType)
|
||||
return getGradeSeries(series)
|
||||
}
|
||||
|
||||
const getGradeStudSeriesFromStats = (stats, seriesType) => {
|
||||
const getGradeStudSeriesFromStats = (stats) => {
|
||||
const series = stats.flatMap(s => s.students.grades)
|
||||
return getGradeSeries(series, seriesType)
|
||||
return getGradeSeries(series)
|
||||
}
|
||||
|
||||
const Distribution = ({ primary, comparison, viewMode }) => {
|
||||
const Distribution = ({ primary, comparison, viewMode, isRelative }) => {
|
||||
const isCumulativeMode = viewMode === viewModeNames.CUMULATIVE
|
||||
const primaryStats = primary.stats
|
||||
const statYears = primaryStats.map(year => year.name)
|
||||
@@ -61,33 +85,49 @@ const Distribution = ({ primary, comparison, viewMode }) => {
|
||||
|
||||
const gradeGraphSerieFn = isCumulativeMode ? getGradeCumSeriesFromStats : getGradeStudSeriesFromStats
|
||||
|
||||
const gradeGraphSerie = [
|
||||
...gradeGraphSerieFn(primaryStats, graphSeriesTypes.PRIMARY),
|
||||
...gradeGraphSerieFn(comparisonStats, graphSeriesTypes.COMPARISON)
|
||||
]
|
||||
const gradeGraphSerie = gradeGraphSerieFn(primaryStats)
|
||||
const comparisonGraphSerie = gradeGraphSerieFn(comparisonStats)
|
||||
|
||||
const maxGradeValue = getMaxValueOfSeries(gradeGraphSerie)
|
||||
const maxGradeValue = isRelative ? 1 : getMaxValueOfSeries(gradeGraphSerie.absolute)
|
||||
|
||||
const distributionOptions = gradeGraphOptions(statYears, maxGradeValue)
|
||||
const primaryDistributionOptions = comparison ? gradeGraphOptions(statYears, maxGradeValue, 'Primary Grades') : gradeGraphOptions(statYears, maxGradeValue, 'Grades')
|
||||
const comparisonDistributionOptions = gradeGraphOptions(statYears, maxGradeValue, 'Comparison Grades')
|
||||
|
||||
return (
|
||||
<Grid.Column>
|
||||
<StackedBarChart
|
||||
options={distributionOptions}
|
||||
series={gradeGraphSerie}
|
||||
/>
|
||||
</Grid.Column>
|
||||
<>
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<StackedBarChart
|
||||
options={primaryDistributionOptions}
|
||||
series={isRelative ? gradeGraphSerie.relative : gradeGraphSerie.absolute}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
{comparison &&
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<StackedBarChart
|
||||
options={comparisonDistributionOptions}
|
||||
series={isRelative ? comparisonGraphSerie.relative : comparisonGraphSerie.absolute}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
}
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Distribution.propTypes = {
|
||||
primary: dataSeriesType.isRequired,
|
||||
comparison: dataSeriesType,
|
||||
viewMode: viewModeType.isRequired
|
||||
viewMode: viewModeType.isRequired,
|
||||
isRelative: bool
|
||||
}
|
||||
|
||||
Distribution.defaultProps = {
|
||||
comparison: undefined
|
||||
comparison: undefined,
|
||||
isRelative: false
|
||||
}
|
||||
|
||||
export default Distribution
|
||||
|
||||
+75
-41
@@ -1,39 +1,47 @@
|
||||
/* eslint-disable quotes */
|
||||
import React from 'react'
|
||||
import { Grid } from 'semantic-ui-react'
|
||||
import { bool } from 'prop-types'
|
||||
import StackedBarChart from '../../../StackedBarChart'
|
||||
import { passRateCumGraphOptions, passRateStudGraphOptions } from '../../../../constants'
|
||||
import {
|
||||
viewModeNames,
|
||||
graphSeriesTypes,
|
||||
getDataObject,
|
||||
getMaxValueOfSeries,
|
||||
dataSeriesType,
|
||||
viewModeType
|
||||
viewModeType,
|
||||
absoluteToRelative
|
||||
} from './util'
|
||||
|
||||
const getPassRateCumSeriesFromStats = (stats, seriesType) => {
|
||||
const { name, multiplier } = seriesType
|
||||
const getPassRateCumSeriesFromStats = (stats) => {
|
||||
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)
|
||||
all.push(p + f)
|
||||
passed.push(p)
|
||||
failed.push(f)
|
||||
})
|
||||
|
||||
return [
|
||||
getDataObject(`${name} all`, all, 'a'),
|
||||
getDataObject(`${name} passed`, passed, 'b'),
|
||||
getDataObject(`${name} failed`, failed, 'c')
|
||||
]
|
||||
return {
|
||||
absolute: [
|
||||
getDataObject(`all`, all, 'a'),
|
||||
getDataObject(`passed`, passed, 'b'),
|
||||
getDataObject(`failed`, failed, 'c')
|
||||
],
|
||||
relative: [
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getDataObject(`all`, all.map(_ => 1), 'a'),
|
||||
getDataObject(`passed`, passed.map(absoluteToRelative(all)), 'b'),
|
||||
getDataObject(`failed`, failed.map(absoluteToRelative(all)), 'c')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const getPassRateStudSeriesFromStats = (stats, seriesType) => {
|
||||
const { name, multiplier } = seriesType
|
||||
|
||||
const getPassRateStudSeriesFromStats = (stats) => {
|
||||
const all = []
|
||||
const failedFirst = []
|
||||
const failedRetry = []
|
||||
@@ -48,23 +56,33 @@ const getPassRateStudSeriesFromStats = (stats, seriesType) => {
|
||||
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)
|
||||
all.push((ff || 0) + (fr || 0) + (pf || 0) + (pr || 0))
|
||||
failedFirst.push(ff || 0)
|
||||
failedRetry.push(fr || 0)
|
||||
passedFirst.push(pf || 0)
|
||||
passedRetry.push(pr || 0)
|
||||
})
|
||||
|
||||
return [
|
||||
getDataObject(`${name} all`, all, 'a'),
|
||||
getDataObject(`${name} passed on first try`, passedFirst, 'b'),
|
||||
getDataObject(`${name} passed after retry`, passedRetry, 'b'),
|
||||
getDataObject(`${name} failed on first try`, failedFirst, 'c'),
|
||||
getDataObject(`${name} failed after retry`, failedRetry, 'c')
|
||||
]
|
||||
return {
|
||||
absolute: [
|
||||
getDataObject(`all`, all, 'a'),
|
||||
getDataObject(` passed on first try`, passedFirst, 'b'),
|
||||
getDataObject(`passed after retry`, passedRetry, 'b'),
|
||||
getDataObject(`failed on first try`, failedFirst, 'c'),
|
||||
getDataObject(`failed after retry`, failedRetry, 'c')
|
||||
],
|
||||
relative: [
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getDataObject(`all`, all.map(_ => 1), 'a'),
|
||||
getDataObject(` passed on first try`, passedFirst.map(absoluteToRelative(all)), 'b'),
|
||||
getDataObject(`passed after retry`, passedRetry.map(absoluteToRelative(all)), 'b'),
|
||||
getDataObject(`failed on first try`, failedFirst.map(absoluteToRelative(all)), 'c'),
|
||||
getDataObject(`failed after retry`, failedRetry.map(absoluteToRelative(all)), 'c')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const PassRate = ({ primary, comparison, viewMode }) => {
|
||||
const PassRate = ({ primary, comparison, viewMode, isRelative = false }) => {
|
||||
const isCumulativeMode = viewMode === viewModeNames.CUMULATIVE
|
||||
|
||||
const primaryStats = primary.stats
|
||||
@@ -72,33 +90,49 @@ const PassRate = ({ primary, comparison, viewMode }) => {
|
||||
const comparisonStats = comparison ? comparison.stats : []
|
||||
const passGraphSerieFn = isCumulativeMode ? getPassRateCumSeriesFromStats : getPassRateStudSeriesFromStats
|
||||
|
||||
const passGraphSerie = [
|
||||
...passGraphSerieFn(primaryStats, graphSeriesTypes.PRIMARY),
|
||||
...passGraphSerieFn(comparisonStats, graphSeriesTypes.COMPARISON)
|
||||
]
|
||||
const passGraphSerie = passGraphSerieFn(primaryStats)
|
||||
const comparisonGraphSerie = passGraphSerieFn(comparisonStats)
|
||||
|
||||
const maxPassRateVal = getMaxValueOfSeries(passGraphSerie)
|
||||
const maxPassRateVal = isRelative ? 1 : getMaxValueOfSeries(passGraphSerie.absolute)
|
||||
const graphOptionsFn = isCumulativeMode ? passRateCumGraphOptions : passRateStudGraphOptions
|
||||
const graphOptions = graphOptionsFn(statYears, maxPassRateVal)
|
||||
const primaryGraphOptions = comparison ? graphOptionsFn(statYears, maxPassRateVal, 'Primary pass rate chart') : graphOptionsFn(statYears, maxPassRateVal, 'Pass rate chart')
|
||||
const comparisonGraphOptions = graphOptionsFn(statYears, maxPassRateVal, 'Comparison pass rate chart')
|
||||
|
||||
return (
|
||||
<Grid.Column>
|
||||
<StackedBarChart
|
||||
options={graphOptions}
|
||||
series={passGraphSerie}
|
||||
/>
|
||||
</Grid.Column>
|
||||
<>
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<StackedBarChart
|
||||
options={primaryGraphOptions}
|
||||
series={isRelative ? passGraphSerie.relative : passGraphSerie.absolute}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
{
|
||||
comparison &&
|
||||
<Grid.Row>
|
||||
<Grid.Column>
|
||||
<StackedBarChart
|
||||
options={comparisonGraphOptions}
|
||||
series={isRelative ? comparisonGraphSerie.relative : comparisonGraphSerie.absolute}
|
||||
/>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
PassRate.propTypes = {
|
||||
primary: dataSeriesType.isRequired,
|
||||
comparison: dataSeriesType,
|
||||
viewMode: viewModeType.isRequired
|
||||
viewMode: viewModeType.isRequired,
|
||||
isRelative: bool
|
||||
}
|
||||
|
||||
PassRate.defaultProps = {
|
||||
comparison: undefined
|
||||
comparison: undefined,
|
||||
isRelative: false
|
||||
}
|
||||
|
||||
export default PassRate
|
||||
|
||||
+7
-11
@@ -6,11 +6,6 @@ export const viewModeNames = {
|
||||
GRADES: 'Grades'
|
||||
}
|
||||
|
||||
export const graphSeriesTypes = {
|
||||
PRIMARY: { name: 'primary', multiplier: 1 },
|
||||
COMPARISON: { name: 'comparison', multiplier: -1 }
|
||||
}
|
||||
|
||||
export const getDataObject = (name, data, stack) => ({ name, data, stack })
|
||||
|
||||
export const getMaxValueOfSeries = series => Object.values(series).reduce((acc, cur) => {
|
||||
@@ -33,7 +28,9 @@ export const isThesisGrades = grades => Object.keys(grades)
|
||||
|
||||
export const isThesisSeries = series => series && series.some(s => isThesisGrades(s))
|
||||
|
||||
export const getThesisGradeSpread = (series, multiplier = 1) => {
|
||||
export const absoluteToRelative = all => (p, i) => (p / all[i]) || 0
|
||||
|
||||
export const getThesisGradeSpread = (series) => {
|
||||
const thesisGradeAccumulator = {
|
||||
L: [],
|
||||
ECLA: [],
|
||||
@@ -48,7 +45,7 @@ export const getThesisGradeSpread = (series, multiplier = 1) => {
|
||||
const currentEntries = Object.entries(cur)
|
||||
|
||||
currentEntries.forEach(([k, v]) => {
|
||||
acc[k].push(v * multiplier)
|
||||
acc[k].push(v)
|
||||
})
|
||||
|
||||
Object.entries(acc)
|
||||
@@ -62,7 +59,7 @@ export const getThesisGradeSpread = (series, multiplier = 1) => {
|
||||
}, { ...thesisGradeAccumulator })
|
||||
}
|
||||
|
||||
export const getGradeSpread = (series, multiplier = 1) => {
|
||||
export const getGradeSpread = (series) => {
|
||||
const failedKeys = ['Eisa', 'Hyl.', '0', 'Luop']
|
||||
|
||||
const baseAccumalator = {
|
||||
@@ -82,10 +79,10 @@ export const getGradeSpread = (series, multiplier = 1) => {
|
||||
if (failedKeys.includes(k)) {
|
||||
failed += v
|
||||
} else {
|
||||
acc[k].push(v * multiplier)
|
||||
acc[k].push(v)
|
||||
}
|
||||
})
|
||||
acc[0].push(failed * multiplier)
|
||||
acc[0].push(failed)
|
||||
Object.entries(acc)
|
||||
.forEach(([k, v]) => {
|
||||
if (v.length < i + 1) {
|
||||
@@ -96,4 +93,3 @@ export const getGradeSpread = (series, multiplier = 1) => {
|
||||
return acc
|
||||
}, { ...baseAccumalator })
|
||||
}
|
||||
|
||||
|
||||
@@ -16,15 +16,17 @@ const paneViewIndex = {
|
||||
class ResultTabs extends Component {
|
||||
state = {
|
||||
activeIndex: paneViewIndex.TABLE,
|
||||
viewMode: viewModeNames.CUMULATIVE
|
||||
viewMode: viewModeNames.CUMULATIVE,
|
||||
isRelative: false
|
||||
}
|
||||
|
||||
getPanes = () => {
|
||||
const { primary, comparison } = this.props
|
||||
const { viewMode } = this.state
|
||||
const { viewMode, isRelative } = this.state
|
||||
|
||||
const paneMenuItems = [
|
||||
{ menuItem: { key: 'Table', icon: 'table', content: 'Table' },
|
||||
{
|
||||
menuItem: { key: 'Table', icon: 'table', content: 'Table' },
|
||||
renderFn: () =>
|
||||
(<Tables
|
||||
comparison={comparison}
|
||||
@@ -32,20 +34,24 @@ class ResultTabs extends Component {
|
||||
viewMode={viewMode}
|
||||
/>)
|
||||
},
|
||||
{ menuItem: { key: 'pass', icon: 'balance', content: 'Pass rate chart' },
|
||||
{
|
||||
menuItem: { key: 'pass', icon: 'balance', content: 'Pass rate chart' },
|
||||
renderFn: () =>
|
||||
(<PassRate
|
||||
comparison={comparison}
|
||||
primary={primary}
|
||||
viewMode={viewMode}
|
||||
isRelative={isRelative}
|
||||
/>)
|
||||
},
|
||||
{ menuItem: { key: 'grade', icon: 'chart bar', content: 'Grade distribution chart' },
|
||||
{
|
||||
menuItem: { key: 'grade', icon: 'chart bar', content: 'Grade distribution chart' },
|
||||
renderFn: () =>
|
||||
(<Distribution
|
||||
comparison={comparison}
|
||||
primary={primary}
|
||||
viewMode={viewMode}
|
||||
isRelative={isRelative}
|
||||
/>)
|
||||
}
|
||||
]
|
||||
@@ -59,9 +65,7 @@ class ResultTabs extends Component {
|
||||
<Grid.Row className="modeSelectorRow">
|
||||
{this.renderViewModeSelector()}
|
||||
</Grid.Row>
|
||||
<Grid.Row>
|
||||
{renderFn()}
|
||||
</Grid.Row>
|
||||
{renderFn()}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
@@ -105,15 +109,27 @@ class ResultTabs extends Component {
|
||||
const newMode = isToggleChecked ? viewModeNames.CUMULATIVE : viewModeNames.STUDENT
|
||||
const toggleId = 'viewModeToggle'
|
||||
return (
|
||||
<div className="toggleContainer">
|
||||
<label className="toggleLabel" htmlFor={toggleId}>{viewModeNames.CUMULATIVE}</label>
|
||||
<Radio
|
||||
id={toggleId}
|
||||
checked={isToggleChecked}
|
||||
toggle
|
||||
onChange={() => this.handleModeChange(newMode)}
|
||||
/>
|
||||
<label className="toggleLabel" htmlFor={toggleId}>{viewModeNames.STUDENT}</label>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div className="toggleContainer">
|
||||
<label className="toggleLabel" htmlFor={toggleId}>{viewModeNames.CUMULATIVE}</label>
|
||||
<Radio
|
||||
id={toggleId}
|
||||
checked={isToggleChecked}
|
||||
toggle
|
||||
onChange={() => this.handleModeChange(newMode)}
|
||||
/>
|
||||
<label className="toggleLabel" htmlFor={toggleId}>{viewModeNames.STUDENT}</label>
|
||||
</div>
|
||||
{this.props.comparison &&
|
||||
<div className="toggleContainer">
|
||||
<label className="toggleLabel">Absolute</label>
|
||||
<Radio
|
||||
toggle
|
||||
onChange={() => this.setState({ isRelative: !this.state.isRelative })}
|
||||
/>
|
||||
<label className="toggleLabel">Relative</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+3
-1
@@ -4,12 +4,13 @@ import { Form } from 'semantic-ui-react'
|
||||
|
||||
const YearFilter = ({ years, fromYear, toYear, handleChange, separate, onToggleCheckbox }) => (
|
||||
<Fragment>
|
||||
<Form.Group widths="equal">
|
||||
<Form.Group widths="equal" inline>
|
||||
<Form.Dropdown
|
||||
label="From:"
|
||||
name="fromYear"
|
||||
options={years}
|
||||
selection
|
||||
inline
|
||||
placeholder="Select academic year"
|
||||
onChange={handleChange}
|
||||
value={fromYear}
|
||||
@@ -18,6 +19,7 @@ const YearFilter = ({ years, fromYear, toYear, handleChange, separate, onToggleC
|
||||
label="To:"
|
||||
name="toYear"
|
||||
options={years}
|
||||
inline
|
||||
selection
|
||||
placeholder="Select academic year"
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { Component } from 'react'
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import { Segment, Header, Form } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router'
|
||||
import qs from 'query-string'
|
||||
import { func, arrayOf, shape, bool } from 'prop-types'
|
||||
import { getSemesters } from '../../../redux/semesters'
|
||||
import { clearCourses, findCoursesV2 } from '../../../redux/coursesearch'
|
||||
import { getCourseStats } from '../../../redux/coursestats'
|
||||
import { getCourseStats, clearCourseStats } from '../../../redux/coursestats'
|
||||
import AutoSubmitSearchInput from '../../AutoSubmitSearchInput'
|
||||
import CourseTable from '../CourseTable'
|
||||
import { getCourseSearchResults } from '../../../selectors/courses'
|
||||
@@ -30,8 +32,21 @@ class SearchForm extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.getSemesters()
|
||||
this.props.clearCourses()
|
||||
const { location } = this.props
|
||||
if (location.search) {
|
||||
this.fetchStatisticsFromUrlParams()
|
||||
} else {
|
||||
this.props.getSemesters()
|
||||
this.props.clearCourses()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props
|
||||
const queryParamsChanged = prevProps.location.search !== this.props.location.search
|
||||
if (location.search && queryParamsChanged) {
|
||||
this.fetchStatisticsFromUrlParams()
|
||||
}
|
||||
}
|
||||
|
||||
onSelectCourse = (course) => {
|
||||
@@ -69,7 +84,36 @@ class SearchForm extends Component {
|
||||
separate
|
||||
}
|
||||
|
||||
await this.props.getCourseStats(params)
|
||||
// await this.props.getCourseStats(params)
|
||||
this.pushQueryToUrl(params)
|
||||
}
|
||||
|
||||
fetchStatisticsFromUrlParams() {
|
||||
const query = this.parseQueryFromUrl()
|
||||
this.setState({ ...query, selectedcourses: query.courseCodes })
|
||||
this.props.getCourseStats(query)
|
||||
}
|
||||
|
||||
pushQueryToUrl = (query) => {
|
||||
const { history } = this.props
|
||||
const { courseCodes, ...rest } = query
|
||||
const queryObject = { ...rest, courseCodes: JSON.stringify(courseCodes) }
|
||||
const searchString = qs.stringify(queryObject)
|
||||
history.push({ search: searchString })
|
||||
}
|
||||
|
||||
parseQueryFromUrl = () => {
|
||||
const { location } = this.props
|
||||
const { courseCodes, fromYear, toYear, separate, ...rest } = qs.parse(location.search)
|
||||
const query = {
|
||||
...this.state.INITIAL,
|
||||
...rest,
|
||||
courseCodes: JSON.parse(courseCodes),
|
||||
fromYear: JSON.parse(fromYear),
|
||||
toYear: JSON.parse(toYear),
|
||||
separate: JSON.parse(separate)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
fetchCourses = () => {
|
||||
@@ -103,7 +147,6 @@ class SearchForm extends Component {
|
||||
coursename,
|
||||
coursecode
|
||||
} = this.state
|
||||
|
||||
const courses = matchingCourses.filter(c => !selectedcourses[c.code])
|
||||
|
||||
const disabled = (!fromYear || Object.keys(selectedcourses).length === 0) || isLoading
|
||||
@@ -114,45 +157,9 @@ class SearchForm extends Component {
|
||||
return (
|
||||
<Segment loading={isLoading}>
|
||||
<Form>
|
||||
<Header content="Search parameters" as="h3" />
|
||||
<YearFilter
|
||||
fromYear={fromYear}
|
||||
toYear={toYear}
|
||||
years={years}
|
||||
separate={separate}
|
||||
handleChange={this.handleChange}
|
||||
onToggleCheckbox={this.onToggleCheckbox}
|
||||
/>
|
||||
<CourseTable
|
||||
title="Selected courses"
|
||||
hidden={noSelectedCourses}
|
||||
courses={selected}
|
||||
onSelectCourse={this.onSelectCourse}
|
||||
controlIcon="remove"
|
||||
/>
|
||||
<Form.Button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
fluid
|
||||
basic
|
||||
positive
|
||||
content="Fetch statistics"
|
||||
onClick={this.onSubmitFormClick}
|
||||
/>
|
||||
<Header content="Search for courses" />
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<Form.Group widths="equal">
|
||||
<Form.Field>
|
||||
<label>Code:</label>
|
||||
<AutoSubmitSearchInput
|
||||
doSearch={this.fetchCourses}
|
||||
placeholder="Search by entering a course code"
|
||||
value={coursecode}
|
||||
onChange={cc => this.setState({ coursecode: cc })}
|
||||
loading={this.props.coursesLoading}
|
||||
minSearchLength={0}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Name:</label>
|
||||
<AutoSubmitSearchInput
|
||||
@@ -164,7 +171,48 @@ class SearchForm extends Component {
|
||||
minSearchLength={0}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>Code:</label>
|
||||
<AutoSubmitSearchInput
|
||||
doSearch={this.fetchCourses}
|
||||
placeholder="Search by entering a course code"
|
||||
value={coursecode}
|
||||
onChange={cc => this.setState({ coursecode: cc })}
|
||||
loading={this.props.coursesLoading}
|
||||
minSearchLength={0}
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
<CourseTable
|
||||
title="Selected courses"
|
||||
hidden={noSelectedCourses}
|
||||
courses={selected}
|
||||
onSelectCourse={this.onSelectCourse}
|
||||
controlIcon="remove"
|
||||
/>
|
||||
{!noSelectedCourses &&
|
||||
<Fragment>
|
||||
<YearFilter
|
||||
fromYear={fromYear}
|
||||
toYear={toYear}
|
||||
years={years}
|
||||
separate={separate}
|
||||
handleChange={this.handleChange}
|
||||
onToggleCheckbox={this.onToggleCheckbox}
|
||||
/>
|
||||
<Form.Button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
fluid
|
||||
size="huge"
|
||||
primary
|
||||
basic
|
||||
positive
|
||||
content="Fetch statistics"
|
||||
onClick={this.onSubmitFormClick}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
<CourseTable
|
||||
hidden={noQueryStrings || isLoading}
|
||||
courses={courses}
|
||||
@@ -187,7 +235,9 @@ SearchForm.propTypes = {
|
||||
matchingCourses: arrayOf(shape({})).isRequired,
|
||||
years: arrayOf(shape({})).isRequired,
|
||||
isLoading: bool.isRequired,
|
||||
coursesLoading: bool.isRequired
|
||||
coursesLoading: bool.isRequired,
|
||||
history: shape({}).isRequired,
|
||||
location: shape({}).isRequired
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
@@ -205,9 +255,10 @@ const mapStateToProps = (state) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
export default withRouter(connect(mapStateToProps, {
|
||||
getSemesters,
|
||||
getCourseStats,
|
||||
clearCourses,
|
||||
findCoursesV2
|
||||
})(SearchForm)
|
||||
findCoursesV2,
|
||||
clearCourseStats
|
||||
})(SearchForm))
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Header, Segment, Tab } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bool } from 'prop-types'
|
||||
import { withRouter } from 'react-router'
|
||||
import { bool, shape, func } from 'prop-types'
|
||||
import './courseStatistics.css'
|
||||
import SearchForm from './SearchForm'
|
||||
import SingleCourseTab from './SingleCourseTab'
|
||||
@@ -26,10 +27,10 @@ class CourseStatistics extends Component {
|
||||
return finishedGet ? { ...INITIAL } : { pending: props.pending }
|
||||
}
|
||||
|
||||
state={ ...INITIAL }
|
||||
state = { ...INITIAL }
|
||||
|
||||
getPanes = () => {
|
||||
const { singleCourseStats } = this.props
|
||||
const { singleCourseStats, clearCourseStats: clearaust, history } = this.props
|
||||
const panes = [
|
||||
{
|
||||
menuItem: MENU.SUM,
|
||||
@@ -44,7 +45,11 @@ class CourseStatistics extends Component {
|
||||
key: 'query',
|
||||
content: MENU.QUERY,
|
||||
icon: 'search',
|
||||
position: 'right'
|
||||
position: 'right',
|
||||
onClick: () => {
|
||||
history.push('/coursestatistics')
|
||||
clearaust()
|
||||
}
|
||||
},
|
||||
render: () => <SearchForm />
|
||||
}
|
||||
@@ -72,7 +77,7 @@ class CourseStatistics extends Component {
|
||||
Course Statistics
|
||||
</Header>
|
||||
<Segment className="contentSegment" >
|
||||
{ statsIsEmpty ? <SearchForm /> : (
|
||||
{statsIsEmpty ? <SearchForm /> : (
|
||||
<Tab
|
||||
menu={{ attached: false, borderless: false }}
|
||||
panes={panes}
|
||||
@@ -88,7 +93,9 @@ class CourseStatistics extends Component {
|
||||
|
||||
CourseStatistics.propTypes = {
|
||||
statsIsEmpty: bool.isRequired,
|
||||
singleCourseStats: bool.isRequired
|
||||
singleCourseStats: bool.isRequired,
|
||||
history: shape({}).isRequired,
|
||||
clearCourseStats: func.isRequired
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ courseStats }) => {
|
||||
@@ -101,4 +108,4 @@ const mapStateToProps = ({ courseStats }) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { clearCourseStats })(CourseStatistics)
|
||||
export default withRouter(connect(mapStateToProps, { clearCourseStats })(CourseStatistics))
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Segment, Icon, Button, Form, Dropdown, Popup } from 'semantic-ui-react'
|
||||
import { func, shape, arrayOf } from 'prop-types'
|
||||
import _ from 'lodash'
|
||||
|
||||
import { removePopulationFilter, setPopulationFilter } from '../../redux/populationFilters'
|
||||
import { tagFilter } from '../../populationFilters'
|
||||
|
||||
|
||||
|
||||
const TagFilter = ({ setPopulationFilterAction, removePopulationFilterAction, filter, samples }) => {
|
||||
const [options, setOptions] = useState([])
|
||||
const [selectedTag, setSelectedTag] = useState()
|
||||
|
||||
const createOptions = () => {
|
||||
const tags = samples.map(s => s.tags.map(t => ({ tagname: t.tag.tagname, tag_id: t.tag.tag_id })))
|
||||
const merge = tags.reduce((a, b) => a.concat(b), [])
|
||||
const uniqueTags = _.uniqBy(merge, 'tag_id')
|
||||
const createdOptions = uniqueTags.map(tag => ({ key: tag.tag_id, text: tag.tagname, value: tag.tag_id }))
|
||||
setOptions(createdOptions)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
createOptions()
|
||||
}, [])
|
||||
|
||||
const handleFilter = () => {
|
||||
setPopulationFilterAction(tagFilter({ tag: selectedTag }))
|
||||
}
|
||||
|
||||
const handleChange = (e, { value }) => {
|
||||
// any better solutions?
|
||||
const selection = options.filter(tag => tag.value === value)
|
||||
setSelectedTag(selection[0])
|
||||
}
|
||||
const clearFilter = () => {
|
||||
removePopulationFilterAction(filter.id)
|
||||
}
|
||||
|
||||
if (filter.notSet) {
|
||||
return (
|
||||
<Segment>
|
||||
<Form>
|
||||
<Popup
|
||||
trigger={<Icon style={{ float: 'right' }} name="info" />}
|
||||
/>
|
||||
<Form.Group inline>
|
||||
<Form.Field>
|
||||
<label>Select students that have tag </label>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Dropdown
|
||||
placeholder="select tag"
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
selectOnBlur={false}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Button onClick={handleFilter}>
|
||||
set filter
|
||||
</Button>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Segment>
|
||||
Students that have a tag {filter.params.text}
|
||||
<span style={{ float: 'right' }}>
|
||||
<Icon name="remove" onClick={clearFilter} />
|
||||
</span>
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
|
||||
TagFilter.propTypes = {
|
||||
setPopulationFilterAction: func.isRequired,
|
||||
removePopulationFilterAction: func.isRequired,
|
||||
filter: shape({}).isRequired,
|
||||
samples: arrayOf(shape({})).isRequired
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
{ setPopulationFilterAction: setPopulationFilter, removePopulationFilterAction: removePopulationFilter }
|
||||
)(TagFilter)
|
||||
@@ -17,6 +17,7 @@ import ExtentGraduated from './ExtentGraduated'
|
||||
import SimpleExtentGraduated from './SimpleExtentGraduated'
|
||||
import Preset from './Preset'
|
||||
import SexFilter from './SexFilter'
|
||||
import TagFilter from './TagFilter'
|
||||
import DisciplineTypes from './DisciplineTypes'
|
||||
import EnrollmentStatus from './EnrollmentStatus'
|
||||
import TransferFilter from './TransferFilter'
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
clearPopulationFilters, setComplementFilter, savePopulationFilters, setPopulationFilter
|
||||
} from '../../redux/populationFilters'
|
||||
import { presetFilter, getFilterFunction } from '../../populationFilters'
|
||||
import { getTextIn } from '../../common'
|
||||
import { getTextIn, userIsAdmin } from '../../common'
|
||||
|
||||
|
||||
const componentFor = {
|
||||
@@ -48,7 +49,8 @@ const componentFor = {
|
||||
CanceledStudyright,
|
||||
PriorityStudyright,
|
||||
TransferToStudyrightFilter,
|
||||
SimpleExtentGraduated
|
||||
SimpleExtentGraduated,
|
||||
TagFilter
|
||||
}
|
||||
|
||||
const advancedFilters = { // Filters that are too hard to use for common folk
|
||||
@@ -82,11 +84,14 @@ class PopulationFilters extends Component {
|
||||
presetDescription: '',
|
||||
presetFilters: [],
|
||||
advancedUser: false,
|
||||
modalOpen: false
|
||||
modalOpen: false,
|
||||
isAdmin: false
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.initialFilterLoading()
|
||||
const admin = await userIsAdmin()
|
||||
this.setState({ isAdmin: admin })
|
||||
}
|
||||
|
||||
initialFilterLoading = async () => {
|
||||
@@ -216,9 +221,17 @@ class PopulationFilters extends Component {
|
||||
</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, allStudyRights
|
||||
})
|
||||
// this is awful, shame on who ever wrote this, pls fix
|
||||
if (this.state.isAdmin) {
|
||||
return React.createElement(componentFor[filterName], {
|
||||
filter: { notSet: true }, key: filterName, samples: this.props.samples, transfers, extents, allStudyRights
|
||||
})
|
||||
}
|
||||
if (!this.state.isAdmin && filterName !== 'TagFilter') {
|
||||
return React.createElement(componentFor[filterName], {
|
||||
filter: { notSet: true }, key: filterName, samples: this.props.samples, transfers, extents, allStudyRights
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
return React.createElement(Preset, {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { string, arrayOf, object, func, bool, shape } from 'prop-types'
|
||||
import { Header, Segment, Button, Icon, Popup, Tab, Grid } from 'semantic-ui-react'
|
||||
import { Header, Segment, Button, Icon, Popup, Tab, Grid, Checkbox } from 'semantic-ui-react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import _ from 'lodash'
|
||||
import XLSX from 'xlsx'
|
||||
@@ -9,6 +9,7 @@ import { getStudentTotalCredits, copyToClipboard, userRoles, reformatDate, getTe
|
||||
import { PRIORITYCODE_TEXTS } from '../../constants'
|
||||
|
||||
import { toggleStudentListVisibility } from '../../redux/settings'
|
||||
import { getTagsByStudytrackAction } from '../../redux/tags'
|
||||
|
||||
import StudentNameVisibilityToggle from '../StudentNameVisibilityToggle'
|
||||
import '../PopulationCourseStats/populationCourseStats.css'
|
||||
@@ -16,18 +17,35 @@ import SortableTable from '../SortableTable'
|
||||
import InfoBox from '../InfoBox'
|
||||
import infotooltips from '../../common/InfoToolTips'
|
||||
import CheckStudentList from '../CheckStudentList'
|
||||
import TagStudent from '../TagStudent'
|
||||
import TagPopulation from '../TagPopulation'
|
||||
|
||||
const popupTimeoutLength = 1000
|
||||
|
||||
|
||||
class PopulationStudents extends Component {
|
||||
state = { containsStudyTracks: false, students: [] }
|
||||
state = {
|
||||
containsStudyTracks: false,
|
||||
students: [],
|
||||
checked: false,
|
||||
checkedStudents: []
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const roles = await userRoles()
|
||||
const admin = roles.includes('admin')
|
||||
|
||||
await this.props.getTagsByStudytrack(this.props.queryStudyrights[0])
|
||||
this.setState({ admin, containsStudyTracks: this.containsStudyTracks() })
|
||||
|
||||
const initialCheckedStudents = []
|
||||
this.props.selectedStudents.forEach((sn) => {
|
||||
const check = {
|
||||
studentnumber: sn,
|
||||
checked: false
|
||||
}
|
||||
initialCheckedStudents.push(check)
|
||||
})
|
||||
this.setState({ checkedStudents: initialCheckedStudents })
|
||||
}
|
||||
|
||||
containsStudyTracks = () => {
|
||||
@@ -68,6 +86,40 @@ class PopulationStudents extends Component {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
|
||||
handleAllCheck = () => {
|
||||
const newCheckedStudents = []
|
||||
this.props.selectedStudents.forEach((sn) => {
|
||||
const check = {
|
||||
studentnumber: sn,
|
||||
checked: !this.state.checked
|
||||
}
|
||||
newCheckedStudents.push(check)
|
||||
})
|
||||
this.setState({ checkedStudents: newCheckedStudents })
|
||||
this.setState({ checked: !this.state.checked })
|
||||
}
|
||||
|
||||
handleSingleCheck = (studentnumber) => {
|
||||
const checker = this.state.checkedStudents.find(check => check.studentnumber === studentnumber)
|
||||
const idx = this.state.checkedStudents.indexOf(checker)
|
||||
const tempArr = [...this.state.checkedStudents]
|
||||
tempArr.splice(idx, 1, ({ studentnumber: checker.studentnumber, checked: !checker.checked }))
|
||||
this.setState({ checkedStudents: tempArr })
|
||||
}
|
||||
|
||||
falsifyChecks = () => {
|
||||
const newCheckedStudents = []
|
||||
this.props.selectedStudents.forEach((sn) => {
|
||||
const check = {
|
||||
studentnumber: sn,
|
||||
checked: false
|
||||
}
|
||||
newCheckedStudents.push(check)
|
||||
})
|
||||
this.setState({ checkedStudents: newCheckedStudents })
|
||||
this.setState({ checked: false })
|
||||
}
|
||||
|
||||
renderStudentTable() {
|
||||
if (!this.props.showList) {
|
||||
return null
|
||||
@@ -93,12 +145,12 @@ class PopulationStudents extends Component {
|
||||
|
||||
const priorityText = (studyRights) => {
|
||||
const codes = this.studyrightCodes(studyRights, 'prioritycode')
|
||||
return codes.map(code => PRIORITYCODE_TEXTS[code] ? PRIORITYCODE_TEXTS[code] : code).join(', ') // eslint-disable-line
|
||||
return codes.map(code => (PRIORITYCODE_TEXTS[code] ? PRIORITYCODE_TEXTS[code] : code)).join(', ')
|
||||
}
|
||||
|
||||
const extentCodes = (studyRights) => {
|
||||
const codes = this.studyrightCodes(studyRights, 'extentcode')
|
||||
return codes.join(', ') // eslint-disable-line
|
||||
return codes.join(', ')
|
||||
}
|
||||
|
||||
const studytrack = studyrights => (
|
||||
@@ -277,37 +329,47 @@ class PopulationStudents extends Component {
|
||||
}
|
||||
)
|
||||
|
||||
const mandatoryCourseLabels = []
|
||||
|
||||
const labelToMandatoryCourses = this.props.mandatoryCourses.reduce((acc, e) => {
|
||||
const label = e.label || ''
|
||||
const label = e.label ? e.label.label : ''
|
||||
acc[label] = acc[label] || []
|
||||
acc[label].push(e)
|
||||
if (e.label) mandatoryCourseLabels.push(e.label)
|
||||
else mandatoryCourseLabels.push({ id: 'null', label: '' })
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const mandatoryCourseLabels = _.orderBy(
|
||||
Object.keys(labelToMandatoryCourses),
|
||||
[e => e.length === 0, e => e],
|
||||
['asc', 'asc']
|
||||
const sortedlabels = _.orderBy(
|
||||
_.uniqBy(mandatoryCourseLabels, l => l.label),
|
||||
[e => e.orderNumber],
|
||||
['asc']
|
||||
)
|
||||
|
||||
const labelColumns = []
|
||||
if (sortedlabels.filter(e => e.label !== '').length > 0) {
|
||||
labelColumns.push(
|
||||
{
|
||||
key: 'general',
|
||||
title: <b>Labels:</b>,
|
||||
parent: true,
|
||||
headerProps: { colSpan: nameColumns.length, style: { textAlign: 'right' } }
|
||||
},
|
||||
...sortedlabels.map(e => ({
|
||||
key: e.id,
|
||||
title: (
|
||||
<div style={{ overflowX: 'hidden' }}><div style={{ width: 0 }}>{e.label}</div></div>
|
||||
),
|
||||
parent: true,
|
||||
headerProps: { colSpan: labelToMandatoryCourses[e.label].length, title: e.label }
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
const mandatoryCourseColumns = [
|
||||
...nameColumns,
|
||||
...(mandatoryCourseLabels.reduce((acc, e) => acc + e.length, 0) > 0) ? [{
|
||||
key: 'general',
|
||||
title: <b>Labels:</b>,
|
||||
parent: true,
|
||||
headerProps: { colSpan: nameColumns.length, style: { textAlign: 'right' } }
|
||||
}] : [],
|
||||
...mandatoryCourseLabels.map(e => ({
|
||||
key: e,
|
||||
title: (
|
||||
<div style={{ overflowX: 'hidden' }}><div style={{ width: 0 }}>{e}</div></div>
|
||||
),
|
||||
parent: true,
|
||||
headerProps: { colSpan: labelToMandatoryCourses[e].length, width: labelToMandatoryCourses[e].length, title: e }
|
||||
})),
|
||||
..._.flatten(mandatoryCourseLabels.map(e => _.sortBy(
|
||||
labelToMandatoryCourses[e],
|
||||
...labelColumns,
|
||||
..._.flatten(sortedlabels.map(e => _.sortBy(
|
||||
labelToMandatoryCourses[e.label],
|
||||
[(m) => {
|
||||
const res = m.code.match(/\d+/)
|
||||
return res ? Number(res[0]) : Number.MAX_VALUE
|
||||
@@ -320,10 +382,30 @@ class PopulationStudents extends Component {
|
||||
getRowContent: s => (
|
||||
hasPassedMandatory(s.studentNumber, m.code) ? (<Icon fitted name="check" color="green" />) : (null)
|
||||
),
|
||||
child: true
|
||||
child: true,
|
||||
childOf: e.label
|
||||
}))))
|
||||
]
|
||||
|
||||
const tagRows = this.props.selectedStudents
|
||||
.map(sn => students[sn])
|
||||
.map((s) => {
|
||||
const check = this.state.checkedStudents.find(c => c.studentnumber === s.studentNumber) || false
|
||||
return (
|
||||
<div key={s.studentNumber}>
|
||||
<Checkbox
|
||||
checked={check.checked}
|
||||
onChange={() => this.handleSingleCheck(s.studentNumber)}
|
||||
/>
|
||||
<TagStudent
|
||||
tags={this.props.tags}
|
||||
studentnumber={s.studentNumber}
|
||||
studentstags={s.tags}
|
||||
studytrack={this.props.queryStudyrights[0]}
|
||||
/>
|
||||
</div>)
|
||||
})
|
||||
|
||||
const panes = [
|
||||
{
|
||||
menuItem: 'General',
|
||||
@@ -364,6 +446,8 @@ class PopulationStudents extends Component {
|
||||
singleLine: true,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
collapsingHeaders
|
||||
showNames={this.props.showNames}
|
||||
columns={mandatoryCourseColumns}
|
||||
data={this.props.selectedStudents.map(sn => students[sn])}
|
||||
/>
|
||||
@@ -375,6 +459,22 @@ class PopulationStudents extends Component {
|
||||
</div>
|
||||
</Tab.Pane>
|
||||
)
|
||||
},
|
||||
{
|
||||
menuItem: 'Tags',
|
||||
render: () => (
|
||||
<Tab.Pane>
|
||||
<TagPopulation
|
||||
allChecker={this.state.checked}
|
||||
handleAllCheck={() => this.handleAllCheck()}
|
||||
falsifyChecks={() => this.falsifyChecks()}
|
||||
tags={this.props.tags}
|
||||
checkedStudents={this.state.checkedStudents}
|
||||
studytrack={this.props.queryStudyrights[0]}
|
||||
/>
|
||||
{tagRows}
|
||||
</Tab.Pane>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
@@ -409,7 +509,6 @@ class PopulationStudents extends Component {
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet)
|
||||
return workbook
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Grid columns="two">
|
||||
@@ -421,7 +520,7 @@ class PopulationStudents extends Component {
|
||||
</Button>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
<Tab panes={panes} />
|
||||
{this.state.admin ? (<Tab panes={panes} />) : (<Tab panes={panes.slice(0, 2)} />)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -465,10 +564,12 @@ PopulationStudents.propTypes = {
|
||||
}).isRequired,
|
||||
code: string.isRequired
|
||||
})).isRequired,
|
||||
mandatoryPassed: shape({}).isRequired
|
||||
mandatoryPassed: shape({}).isRequired,
|
||||
tags: arrayOf(shape({ tag_id: string, tagname: string, studytrack: string })).isRequired,
|
||||
getTagsByStudytrack: func.isRequired
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ settings, populations, populationCourses, populationMandatoryCourses }) => {
|
||||
const mapStateToProps = ({ settings, populations, populationCourses, populationMandatoryCourses, tags }) => {
|
||||
const mandatoryCodes = populationMandatoryCourses.data.map(c => c.code)
|
||||
|
||||
let mandatoryPassed = {}
|
||||
@@ -488,11 +589,12 @@ const mapStateToProps = ({ settings, populations, populationCourses, populationM
|
||||
language: settings.language,
|
||||
queryStudyrights: Object.values(populations.query.studyRights),
|
||||
mandatoryCourses: populationMandatoryCourses.data,
|
||||
mandatoryPassed
|
||||
mandatoryPassed,
|
||||
tags: tags.data
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ toggleStudentListVisibility }
|
||||
{ toggleStudentListVisibility, getTagsByStudytrack: getTagsByStudytrackAction }
|
||||
)(withRouter(PopulationStudents))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react'
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import { Table } from 'semantic-ui-react'
|
||||
import { shape, arrayOf, string, func, bool, element, oneOfType } from 'prop-types'
|
||||
import _ from 'lodash'
|
||||
@@ -9,57 +9,95 @@ const DIRECTIONS = {
|
||||
}
|
||||
|
||||
class SortableTable extends Component {
|
||||
state={
|
||||
direction: this.props.defaultdescending ? DIRECTIONS.DESC : DIRECTIONS.ASC,
|
||||
selected: this.props.defaultsortkey == null ? this.props.columns[0].key : this.props.defaultsortkey
|
||||
}
|
||||
state = {
|
||||
direction: this.props.defaultdescending ? DIRECTIONS.DESC : DIRECTIONS.ASC,
|
||||
selected: this.props.defaultsortkey == null ? this.props.columns[0].key : this.props.defaultsortkey,
|
||||
collapsed: []
|
||||
}
|
||||
|
||||
handleSort = column => () => {
|
||||
const { selected, direction } = this.state
|
||||
if (selected === column) {
|
||||
this.setState({
|
||||
direction: direction === DIRECTIONS.ASC ? DIRECTIONS.DESC : DIRECTIONS.ASC
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
selected: column,
|
||||
direction: DIRECTIONS.DESC
|
||||
})
|
||||
handleSort = column => () => {
|
||||
const { selected, direction } = this.state
|
||||
if (selected === column) {
|
||||
this.setState({
|
||||
direction: direction === DIRECTIONS.ASC ? DIRECTIONS.DESC : DIRECTIONS.ASC
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
selected: column,
|
||||
direction: DIRECTIONS.DESC
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleCollapse = column => () => {
|
||||
const { collapsed } = this.state
|
||||
if (collapsed.map(c => c.headerProps.title).includes(column.headerProps.title)) {
|
||||
this.setState({ collapsed: [...collapsed.filter(c => c.headerProps.title !== column.headerProps.title)] })
|
||||
} else {
|
||||
this.setState({ collapsed: [...collapsed, column] })
|
||||
}
|
||||
}
|
||||
|
||||
sortedRows = () => {
|
||||
const { selected, direction } = this.state
|
||||
const column = this.props.columns.find(c => c.key === selected)
|
||||
if (!column) {
|
||||
return this.props.data
|
||||
}
|
||||
const { getRowVal } = column
|
||||
const sorted = _.sortBy(this.props.data, [getRowVal])
|
||||
return direction === DIRECTIONS.ASC ? sorted : sorted.reverse()
|
||||
}
|
||||
|
||||
verticalTitle = title => (
|
||||
// https://stackoverflow.com/a/41396815
|
||||
<div style={{ writingMode: 'vertical-rl', minWidth: '32px', textAlign: 'left' }}>
|
||||
{title}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
render() {
|
||||
const calculateSkippedColumns = () => {
|
||||
const { columns, showNames } = this.props
|
||||
const { collapsed } = this.state
|
||||
const defaultColumnCount = showNames ? 6 : 3
|
||||
if (collapsed) {
|
||||
const collapsedKeys = collapsed.map(c => c.key)
|
||||
return collapsedKeys.reduce((acc, curr) => {
|
||||
const previousCols = columns.filter(c => c.key < curr)
|
||||
const sumOfPreviousColSpans = previousCols.reduce((a, b) => a + b.headerProps.colSpan, 0)
|
||||
return [
|
||||
...acc,
|
||||
sumOfPreviousColSpans + columns.find(c => c.key === curr).headerProps.colSpan + defaultColumnCount]
|
||||
}, [])
|
||||
}
|
||||
return []
|
||||
}
|
||||
const { tableProps, getRowProps, columns, getRowKey, collapsingHeaders } = this.props
|
||||
const { selected, direction, collapsed } = this.state
|
||||
const columnsWithCollapsedHeaders = collapsingHeaders ? [...columns.filter(c => (c.headerProps && (!collapsed.map(cell => cell.headerProps.title).includes(c.headerProps.title) && !c.collapsed))), ...this.state.collapsed].sort((a, b) => a.key - b.key) : columns
|
||||
|
||||
sortedRows = () => {
|
||||
const { selected, direction } = this.state
|
||||
const column = this.props.columns.find(c => c.key === selected)
|
||||
if (!column) {
|
||||
return this.props.data
|
||||
}
|
||||
const { getRowVal } = column
|
||||
const sorted = _.sortBy(this.props.data, [getRowVal])
|
||||
return direction === DIRECTIONS.ASC ? sorted : sorted.reverse()
|
||||
}
|
||||
const sortDirection = name => (selected === name ? direction : null)
|
||||
|
||||
render() {
|
||||
const { tableProps, getRowProps, columns, getRowKey } = this.props
|
||||
const { selected, direction } = this.state
|
||||
const sortDirection = name => (selected === name ? direction : null)
|
||||
return (
|
||||
<Table sortable {...tableProps}>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{columns.filter(c => !c.child && !(c.title == null)).map(c => (
|
||||
<Table.HeaderCell
|
||||
key={c.key}
|
||||
content={c.title}
|
||||
onClick={c.parent ? undefined : this.handleSort(c.key)}
|
||||
sorted={c.parent ? undefined : sortDirection(c.key)}
|
||||
{...c.headerProps}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
{columns.filter(c => c.child && !(c.title == null)).map(c => (
|
||||
return (
|
||||
<Table sortable {...tableProps}>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{columnsWithCollapsedHeaders.filter(c => !c.child && !(c.title == null)).map(c => (
|
||||
<Table.HeaderCell
|
||||
key={c.key}
|
||||
content={c.title}
|
||||
onClick={c.parent && collapsingHeaders ? this.handleCollapse({ title: this.verticalTitle(<Fragment>{c.headerProps.title}</Fragment>), headerProps: { ...c.headerProps, colSpan: 1, rowSpan: 2 }, key: c.key, collapsed: true, parent: c.parent }) : this.handleSort(c.key)}
|
||||
sorted={c.parent ? undefined : sortDirection(c.key)}
|
||||
{...c.headerProps}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
{columns.filter(c => c.child && !(c.title == null) && !collapsed.map(cell => cell.headerProps.title).includes(c.childOf)).map(c =>
|
||||
(
|
||||
<Table.HeaderCell
|
||||
key={c.key}
|
||||
content={c.title}
|
||||
@@ -67,29 +105,40 @@ class SortableTable extends Component {
|
||||
sorted={sortDirection(c.key)}
|
||||
{...c.headerProps}
|
||||
/>
|
||||
))}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{ this.sortedRows().map(row => (
|
||||
<Table.Row
|
||||
key={getRowKey(row)}
|
||||
{...getRowProps && getRowProps(row)}
|
||||
>
|
||||
{columns.filter(c => !c.parent).map(c => (
|
||||
))
|
||||
}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{this.sortedRows().map(row => (
|
||||
<Table.Row
|
||||
key={getRowKey(row)}
|
||||
{...getRowProps && getRowProps(row)}
|
||||
>
|
||||
{columns.filter(c => !c.parent).map((c, i) => {
|
||||
if (collapsed.map(cell => cell.headerProps.title).includes(c.childOf)) {
|
||||
const skippedColumns = calculateSkippedColumns()
|
||||
if (skippedColumns.includes(i + 1)) {
|
||||
return <Table.Cell warning />
|
||||
}
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Table.Cell
|
||||
key={c.key}
|
||||
content={c.getRowContent ? c.getRowContent(row) : c.getRowVal(row)}
|
||||
{...c.cellProps}
|
||||
{...c.getCellProps && c.getCellProps(row)}
|
||||
/>
|
||||
))}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SortableTable.propTypes = {
|
||||
@@ -97,7 +146,7 @@ SortableTable.propTypes = {
|
||||
getRowKey: func.isRequired,
|
||||
getRowProps: func,
|
||||
columns: arrayOf(shape({
|
||||
key: string,
|
||||
key: string.isRequired,
|
||||
title: oneOfType([element, string]),
|
||||
headerProps: shape({}),
|
||||
getRowVal: func,
|
||||
@@ -109,14 +158,18 @@ SortableTable.propTypes = {
|
||||
})).isRequired,
|
||||
data: arrayOf(shape({})).isRequired,
|
||||
defaultdescending: bool,
|
||||
defaultsortkey: string
|
||||
defaultsortkey: string,
|
||||
collapsingHeaders: bool,
|
||||
showNames: bool
|
||||
}
|
||||
|
||||
SortableTable.defaultProps = {
|
||||
tableProps: undefined,
|
||||
getRowProps: undefined,
|
||||
defaultdescending: false,
|
||||
defaultsortkey: null
|
||||
defaultsortkey: null,
|
||||
collapsingHeaders: false,
|
||||
showNames: undefined
|
||||
}
|
||||
|
||||
export default SortableTable
|
||||
|
||||
@@ -3,12 +3,16 @@ import { func, shape, string, boolean, arrayOf, integer } from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Segment, Table, Icon } from 'semantic-ui-react'
|
||||
import { isEmpty, sortBy } from 'lodash'
|
||||
import moment from 'moment'
|
||||
import qs from 'query-string'
|
||||
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'
|
||||
import { byDateDesc, reformatDate, getTextIn } from '../../common'
|
||||
import { clearCourseStats } from '../../redux/coursestats'
|
||||
|
||||
|
||||
class StudentDetails extends Component {
|
||||
componentDidMount() {
|
||||
@@ -26,6 +30,16 @@ class StudentDetails extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
pushQueryToUrl = (query) => {
|
||||
const { history } = this.props
|
||||
const { courseCodes, ...rest } = query
|
||||
const queryObject = { ...rest, courseCodes: JSON.stringify(courseCodes) }
|
||||
const searchString = qs.stringify(queryObject)
|
||||
this.props.clearCourseStats()
|
||||
history.push('/coursestatistics/')
|
||||
history.push({ search: searchString })
|
||||
}
|
||||
|
||||
renderCreditsGraph = () => {
|
||||
const { translate, student } = this.props
|
||||
return (
|
||||
@@ -46,7 +60,8 @@ class StudentDetails extends Component {
|
||||
translate('common.date'),
|
||||
translate('common.course'),
|
||||
translate('common.grade'),
|
||||
translate('common.credits')
|
||||
translate('common.credits'),
|
||||
''
|
||||
]
|
||||
const courseRows = student.courses.sort(byDateDesc).map((c) => {
|
||||
const {
|
||||
@@ -60,11 +75,13 @@ class StudentDetails extends Component {
|
||||
} else {
|
||||
icon = <Icon name="circle outline" color="red" />
|
||||
}
|
||||
const year = -(moment(new Date('1.1.1950')).diff(new Date(date), 'years')) // :D
|
||||
return [
|
||||
reformatDate(date, 'DD.MM.YYYY'),
|
||||
`${isStudyModuleCredit ? `${getTextIn(course.name, language)} [Study Module]` : getTextIn(course.name, language)} (${course.code})`,
|
||||
<div>{icon}{grade}</div>,
|
||||
credits
|
||||
credits,
|
||||
<Icon style={{ cursor: 'pointer' }} name="level up" onClick={() => this.pushQueryToUrl({ courseCodes: [course.code], separate: false, fromYear: year - 1, toYear: year + 1 })} />
|
||||
]
|
||||
})
|
||||
return (
|
||||
@@ -204,6 +221,7 @@ StudentDetails.propTypes = {
|
||||
removeStudentSelection: func.isRequired,
|
||||
studentNumber: string,
|
||||
translate: func.isRequired,
|
||||
clearCourseStats: func.isRequired,
|
||||
student: shape({
|
||||
courses: arrayOf(shape({
|
||||
course: shape({
|
||||
@@ -235,6 +253,7 @@ const mapStateToProps = ({ students, settings }) => ({
|
||||
})
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
removeStudentSelection: () => dispatch(removeStudentSelection()),
|
||||
clearCourseStats: () => dispatch(clearCourseStats()),
|
||||
resetStudent: () => dispatch(resetStudent()),
|
||||
getStudent: studentNumber =>
|
||||
dispatch(getStudent(studentNumber))
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
import { Segment, Form, Table, Button, Modal, Header } from 'semantic-ui-react'
|
||||
import { string, func, shape, arrayOf } from 'prop-types'
|
||||
import {
|
||||
GetMandatoryCourseLabels,
|
||||
AddMandatoryCourseLabel,
|
||||
DeleteMandatoryCourseLabel,
|
||||
MoveMandatoryCourseLabel
|
||||
} from '../../../redux/mandatoryCourseLabels'
|
||||
|
||||
const MandatoryCourseLabels = ({
|
||||
studyProgramme,
|
||||
labels,
|
||||
getLabels,
|
||||
removeLabel,
|
||||
moveLabel,
|
||||
addLabel,
|
||||
getMandatoryCourses
|
||||
}) => {
|
||||
useEffect(() => { getLabels(studyProgramme) }, [])
|
||||
const [newLabel, setNewLabel] = useState('')
|
||||
return (
|
||||
<Segment basic>
|
||||
<Header>Labels</Header>
|
||||
<Form>
|
||||
<Form.Group>
|
||||
<Form.Input placeholder="Label text.." value={newLabel} onChange={(__, action) => setNewLabel(action.value)} />
|
||||
<Form.Button content="Add" disabled={newLabel.length === 0 || labels.some(e => newLabel.trim() === e.label)} onClick={() => addLabel(studyProgramme, { label: newLabel.trim() }) && setNewLabel('')} />
|
||||
</Form.Group>
|
||||
</Form>
|
||||
<Table compact>
|
||||
<Table.Body>
|
||||
{_.orderBy(labels, ['orderNumber']).map((label, index) => (
|
||||
<Table.Row key={label.id}>
|
||||
<Table.Cell>{label.label}</Table.Cell>
|
||||
<Table.Cell collapsing>
|
||||
<Button
|
||||
icon="angle up"
|
||||
disabled={index === 0}
|
||||
onClick={() => moveLabel(studyProgramme, label, 'up')}
|
||||
/>
|
||||
<Button
|
||||
icon="angle down"
|
||||
disabled={index === labels.length - 1}
|
||||
onClick={() => moveLabel(studyProgramme, label, 'down')}
|
||||
/>
|
||||
<Modal
|
||||
trigger={<Button icon="remove" negative />}
|
||||
header="Really remove label?"
|
||||
content={`Are you sure you want to remove the label "${
|
||||
label.label
|
||||
}"? This removes the label from all courses it is attached to.`}
|
||||
actions={[
|
||||
'Cancel',
|
||||
{
|
||||
key: 'remove',
|
||||
content: 'Remove',
|
||||
positive: true,
|
||||
onClick: () => removeLabel(studyProgramme, label).then(() => getMandatoryCourses(studyProgramme))
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
|
||||
MandatoryCourseLabels.propTypes = {
|
||||
studyProgramme: string.isRequired,
|
||||
getLabels: func.isRequired,
|
||||
addLabel: func.isRequired,
|
||||
removeLabel: func.isRequired,
|
||||
moveLabel: func.isRequired,
|
||||
labels: arrayOf(shape({ code: string, label: string, name: shape({}) })).isRequired,
|
||||
getMandatoryCourses: func.isRequired
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ mandatoryCourseLabels }) => ({
|
||||
labels: mandatoryCourseLabels.data
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
getLabels: studyProgramme => dispatch(GetMandatoryCourseLabels(studyProgramme)),
|
||||
addLabel: (studyProgramme, label) => dispatch(AddMandatoryCourseLabel(studyProgramme, label)),
|
||||
removeLabel: (studyProgramme, label) => dispatch(DeleteMandatoryCourseLabel(studyProgramme, label)),
|
||||
moveLabel: (studyProgramme, label, direction) => dispatch(MoveMandatoryCourseLabel(studyProgramme, label, direction))
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MandatoryCourseLabels)
|
||||
+74
-31
@@ -1,45 +1,78 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { sortBy } from 'lodash'
|
||||
import { arrayOf, string, shape, func } from 'prop-types'
|
||||
import { Button, Form } from 'semantic-ui-react'
|
||||
import { Button, Dropdown } from 'semantic-ui-react'
|
||||
import SortableTable from '../../SortableTable'
|
||||
import { getTextIn } from '../../../common'
|
||||
import {
|
||||
GetMandatoryCourseLabels
|
||||
} from '../../../redux/mandatoryCourseLabels'
|
||||
|
||||
const MandatoryCourseTable = ({
|
||||
studyProgramme,
|
||||
mandatoryCourses,
|
||||
language,
|
||||
deleteMandatoryCourse,
|
||||
setMandatoryCourseLabel,
|
||||
labels,
|
||||
getLabels
|
||||
}) => {
|
||||
useEffect(() => { getLabels(studyProgramme) }, [])
|
||||
|
||||
const MandatoryCourseTable = ({ studyProgramme, mandatoryCourses, language, deleteMandatoryCourse, setMandatoryCourseLabel }) => {
|
||||
const deleteButton = code => (
|
||||
<Button
|
||||
onClick={() => deleteMandatoryCourse(studyProgramme, code)}
|
||||
>
|
||||
<Button onClick={() => deleteMandatoryCourse(studyProgramme, code)}>
|
||||
Delete
|
||||
</Button>
|
||||
)
|
||||
|
||||
const initialLabels = mandatoryCourses.reduce((acc, e) => {
|
||||
acc[e.code] = e.label
|
||||
return acc
|
||||
}, {})
|
||||
const [labels, setLabels] = useState(initialLabels)
|
||||
const labelInput = code => (
|
||||
<Form>
|
||||
<Form.Group>
|
||||
<Form.Input defaultValue={initialLabels[code]} onChange={e => setLabels({ ...labels, [code]: e.target.value })} />
|
||||
<Form.Button
|
||||
disabled={initialLabels[code] === labels[code] || labels[code] == null || (initialLabels[code] == null && labels[code] === '')}
|
||||
onClick={() => setMandatoryCourseLabel(studyProgramme, code, labels[code])}
|
||||
>
|
||||
Save label
|
||||
</Form.Button>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
)
|
||||
|
||||
const idtolabel = labels.reduce((acc, e) => { acc[e.id] = e; return acc }, {})
|
||||
const options = sortBy(labels, ['label']).map(e => ({ key: e.id, text: e.label, value: e.id }))
|
||||
const columns = [
|
||||
{ key: 'name', title: 'Name', getRowVal: course => getTextIn(course.name, language) },
|
||||
{
|
||||
key: 'name',
|
||||
title: 'Name',
|
||||
getRowVal: course => getTextIn(course.name, language)
|
||||
},
|
||||
{ key: 'code', title: 'Code', getRowVal: course => course.code },
|
||||
{ key: 'label', title: 'Label', getRowVal: course => labels[course.code], getRowContent: course => labelInput(course.code) },
|
||||
{ key: 'delete', title: 'Delete', getRowVal: course => deleteButton(course.code) }
|
||||
{
|
||||
key: 'label',
|
||||
title: 'Label',
|
||||
getRowVal: course =>
|
||||
course.label &&
|
||||
idtolabel[course.label.id] &&
|
||||
idtolabel[course.label.id].label,
|
||||
getRowContent: course => (
|
||||
<Dropdown
|
||||
clearable
|
||||
selectOnBlur={false}
|
||||
selectOnNavigation={false}
|
||||
placeholder="select label"
|
||||
defaultValue={course.label && course.label.id}
|
||||
disabled={options.length === 0}
|
||||
options={options}
|
||||
onChange={(_, { value }) =>
|
||||
setMandatoryCourseLabel(studyProgramme, course.code, {
|
||||
id: value === '' ? null : value
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
title: 'Delete',
|
||||
getRowVal: course => deleteButton(course.code)
|
||||
}
|
||||
]
|
||||
|
||||
return <SortableTable columns={columns} data={mandatoryCourses} getRowKey={row => row.code} />
|
||||
return (
|
||||
<SortableTable
|
||||
columns={columns}
|
||||
data={mandatoryCourses}
|
||||
getRowKey={row => row.code}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
MandatoryCourseTable.propTypes = {
|
||||
@@ -47,7 +80,17 @@ MandatoryCourseTable.propTypes = {
|
||||
studyProgramme: string.isRequired,
|
||||
deleteMandatoryCourse: func.isRequired,
|
||||
setMandatoryCourseLabel: func.isRequired,
|
||||
language: string.isRequired
|
||||
language: string.isRequired,
|
||||
labels: arrayOf(shape({ code: string, label: string, name: shape({}) })).isRequired,
|
||||
getLabels: func.isRequired
|
||||
}
|
||||
|
||||
export default MandatoryCourseTable
|
||||
const mapStateToProps = ({ mandatoryCourseLabels }) => ({
|
||||
labels: mandatoryCourseLabels.data
|
||||
})
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
getLabels: studyProgramme => dispatch(GetMandatoryCourseLabels(studyProgramme))
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MandatoryCourseTable)
|
||||
|
||||
+31
-20
@@ -12,13 +12,20 @@ const ProductivityTable = ({ productivity, thesis, loading, error, studyprogramm
|
||||
if (thesis) {
|
||||
thesisTypes = thesis.map(t => t.thesisType)
|
||||
}
|
||||
const headerList = ['Year', 'Credits', thesisTypes.includes('MASTER') && 'Masters Thesis', thesisTypes.includes('BACHELOR') && 'Bachelors Thesis', 'Graduated', 'Graduation median time'].filter(_ => _)
|
||||
|
||||
const headerList = [
|
||||
'Year',
|
||||
'Credits',
|
||||
thesisTypes.includes('MASTER') && 'Masters Thesis',
|
||||
thesisTypes.includes('BACHELOR') && 'Bachelors Thesis',
|
||||
'Graduated',
|
||||
'Credits for studyprogramme majors',
|
||||
'Credits for non major students',
|
||||
'Hyväksiluettu (not included in Credits column)'
|
||||
].filter(_ => _)
|
||||
const refresh = () => {
|
||||
callApi('/v2/studyprogrammes/productivity/recalculate', 'get', null, { code: studyprogramme })
|
||||
.then(() => { dispatchGetProductivity(studyprogramme) })
|
||||
}
|
||||
console.log(productivity)
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Header>
|
||||
@@ -32,7 +39,7 @@ const ProductivityTable = ({ productivity, thesis, loading, error, studyprogramm
|
||||
productivity.lastUpdated
|
||||
? moment(productivity.lastUpdated).format('HH:mm:ss MM-DD-YYYY')
|
||||
: 'unknown'
|
||||
} ${productivity.status || ''}`}
|
||||
} ${productivity.status || ''}`}
|
||||
</Header.Subheader>
|
||||
)}
|
||||
</Grid.Column>
|
||||
@@ -58,21 +65,23 @@ const ProductivityTable = ({ productivity, thesis, loading, error, studyprogramm
|
||||
<Table.Body>
|
||||
{productivity && productivity.data
|
||||
? productivity.data
|
||||
.sort((year1, year2) => year2.year - year1.year)
|
||||
.map(year => (
|
||||
<Table.Row key={year.year}>
|
||||
<Table.Cell>{year.year}</Table.Cell>
|
||||
<Table.Cell>{Math.floor(year.credits)}</Table.Cell>
|
||||
{thesisTypes.includes('BACHELOR') && (
|
||||
<Table.Cell>{year.bThesis}</Table.Cell>
|
||||
)}
|
||||
{thesisTypes.includes('MASTER') && (
|
||||
<Table.Cell>{year.mThesis}</Table.Cell>
|
||||
)}
|
||||
<Table.Cell>{year.graduated}</Table.Cell>
|
||||
<Table.Cell>{year.medianGraduationTime} months</Table.Cell>
|
||||
</Table.Row>
|
||||
))
|
||||
.sort((year1, year2) => year2.year - year1.year)
|
||||
.map(year => (
|
||||
<Table.Row key={year.year}>
|
||||
<Table.Cell>{year.year}</Table.Cell>
|
||||
<Table.Cell>{Math.floor(year.credits)}</Table.Cell>
|
||||
{thesisTypes.includes('BACHELOR') && (
|
||||
<Table.Cell>{year.bThesis}</Table.Cell>
|
||||
)}
|
||||
{thesisTypes.includes('MASTER') && (
|
||||
<Table.Cell>{year.mThesis}</Table.Cell>
|
||||
)}
|
||||
<Table.Cell>{year.graduated}</Table.Cell>
|
||||
<Table.Cell>{year.creditsForMajors}</Table.Cell>
|
||||
<Table.Cell>{year.credits - year.creditsForMajors}</Table.Cell>
|
||||
<Table.Cell>{year.transferredCredits}</Table.Cell>
|
||||
</Table.Row>
|
||||
))
|
||||
: null}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
@@ -89,7 +98,9 @@ ProductivityTable.propTypes = {
|
||||
credits: number,
|
||||
mThesis: number,
|
||||
bThesis: number,
|
||||
graduated: number
|
||||
graduated: number,
|
||||
creditsForMajors: number,
|
||||
transferredCredits: number
|
||||
}))
|
||||
}),
|
||||
thesis: arrayOf(shape({
|
||||
|
||||
+38
-13
@@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { func, shape, string } from 'prop-types'
|
||||
import { Message } from 'semantic-ui-react'
|
||||
import { Message, Tab } from 'semantic-ui-react'
|
||||
import {
|
||||
getMandatoryCourses,
|
||||
addMandatoryCourse,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '../../../redux/populationMandatoryCourses'
|
||||
import MandatoryCourseTable from '../MandatoryCourseTable'
|
||||
import AddMandatoryCourses from '../AddMandatoryCourses'
|
||||
import MandatoryCourseLabels from '../MandatoryCourseLabels'
|
||||
|
||||
class StudyProgrammeMandatoryCourses extends Component {
|
||||
static propTypes = {
|
||||
@@ -24,7 +25,9 @@ class StudyProgrammeMandatoryCourses extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const { studyProgramme } = this.props
|
||||
if (studyProgramme) this.props.getMandatoryCourses(studyProgramme)
|
||||
if (studyProgramme) {
|
||||
this.props.getMandatoryCourses(studyProgramme)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -38,6 +41,38 @@ class StudyProgrammeMandatoryCourses extends Component {
|
||||
const { studyProgramme, mandatoryCourses, language } = this.props
|
||||
if (!studyProgramme) return null
|
||||
|
||||
const panes = [
|
||||
{
|
||||
menuItem: 'Mandatory courses',
|
||||
render: () => (
|
||||
<Tab.Pane>
|
||||
<AddMandatoryCourses
|
||||
addMandatoryCourse={this.props.addMandatoryCourse}
|
||||
studyProgramme={studyProgramme}
|
||||
/>
|
||||
<MandatoryCourseTable
|
||||
mandatoryCourses={mandatoryCourses.data}
|
||||
studyProgramme={studyProgramme}
|
||||
deleteMandatoryCourse={this.props.deleteMandatoryCourse}
|
||||
setMandatoryCourseLabel={this.props.setMandatoryCourseLabel}
|
||||
language={language}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
)
|
||||
},
|
||||
{
|
||||
menuItem: 'Group labels',
|
||||
render: () => (
|
||||
<Tab.Pane>
|
||||
<MandatoryCourseLabels
|
||||
studyProgramme={studyProgramme}
|
||||
getMandatoryCourses={this.props.getMandatoryCourses}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Message
|
||||
@@ -45,17 +80,7 @@ class StudyProgrammeMandatoryCourses extends Component {
|
||||
in the 'Study programme' > 'Search by class' results page. The label is used to group the courses in different views.
|
||||
You can for example use labels '1. year', '2. year' to define groups of 1st year and second year mandatory courses."
|
||||
/>
|
||||
<AddMandatoryCourses
|
||||
addMandatoryCourse={this.props.addMandatoryCourse}
|
||||
studyProgramme={studyProgramme}
|
||||
/>
|
||||
<MandatoryCourseTable
|
||||
mandatoryCourses={mandatoryCourses.data}
|
||||
studyProgramme={studyProgramme}
|
||||
deleteMandatoryCourse={this.props.deleteMandatoryCourse}
|
||||
setMandatoryCourseLabel={this.props.setMandatoryCourseLabel}
|
||||
language={language}
|
||||
/>
|
||||
<Tab panes={panes} />
|
||||
</React.Fragment >
|
||||
)
|
||||
}
|
||||
|
||||
+3
-4
@@ -34,12 +34,12 @@ class StudyProgrammeSelector extends Component {
|
||||
{
|
||||
key: 'name',
|
||||
title: 'name',
|
||||
getRowContent: prog => getTextIn(prog.name, language)
|
||||
getRowVal: prog => getTextIn(prog.name, language)
|
||||
},
|
||||
{
|
||||
key: 'code',
|
||||
title: 'code',
|
||||
getRowContent: prog => prog.code
|
||||
getRowVal: prog => prog.code
|
||||
}
|
||||
]
|
||||
if (studyprogrammes == null) {
|
||||
@@ -51,7 +51,6 @@ class StudyProgrammeSelector extends Component {
|
||||
getRowKey={programme => programme.code}
|
||||
getRowProps={programme => ({ onClick: () => this.props.handleSelect(programme.code), style: { cursor: 'pointer' } })}
|
||||
data={studyprogrammes}
|
||||
defaultdescending
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -63,7 +62,7 @@ const mapStateToProps = ({ populationDegreesAndProgrammes, settings }) => {
|
||||
|
||||
return {
|
||||
studyprogrammes: programmes ? Object.values(programmes).filter(programme =>
|
||||
programme.code.includes('_')) : null,
|
||||
programme.code.includes('_') || programme.code.startsWith('T92')) : null,
|
||||
language
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import { Button, Input, List, Label, Icon, Header } from 'semantic-ui-react'
|
||||
import { arrayOf, string, shape, func } from 'prop-types'
|
||||
|
||||
import { getTagsByStudytrackAction, createTagAction, deleteTagAction } from '../../../redux/tags'
|
||||
|
||||
const Tags = ({ createTag, deleteTag, getTagsByStudytrack, tags, studyprogramme }) => {
|
||||
const [tagname, setTagname] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
getTagsByStudytrack(studyprogramme)
|
||||
}, [])
|
||||
|
||||
const handleDeleteTag = (event, tag) => {
|
||||
event.preventDefault()
|
||||
deleteTag(tag)
|
||||
getTagsByStudytrack(studyprogramme)
|
||||
}
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault()
|
||||
const newTag = {
|
||||
tagname,
|
||||
studytrack: studyprogramme
|
||||
}
|
||||
createTag(newTag)
|
||||
setTagname('')
|
||||
getTagsByStudytrack(studyprogramme)
|
||||
}
|
||||
|
||||
const handleChange = ({ target }) => {
|
||||
setTagname(target.value)
|
||||
}
|
||||
|
||||
const rows = tags.map(tag => (
|
||||
<List.Item key={tag.tag_id}>
|
||||
<List.Content>
|
||||
<Label>
|
||||
{tag.tagname} <Icon name="delete" link onClick={event => handleDeleteTag(event, tag)} />
|
||||
</Label>
|
||||
</List.Content>
|
||||
</List.Item >
|
||||
))
|
||||
|
||||
return (
|
||||
<List>
|
||||
<Input onChange={handleChange} value={tagname} />
|
||||
<Button onClick={handleSubmit}>add new tag</Button>
|
||||
<Header size="medium">Study programme tags</Header>
|
||||
{rows}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ tags }) => ({
|
||||
tags: tags.data
|
||||
})
|
||||
|
||||
Tags.propTypes = {
|
||||
getTagsByStudytrack: func.isRequired,
|
||||
createTag: func.isRequired,
|
||||
deleteTag: func.isRequired,
|
||||
tags: arrayOf(shape({ tag_id: string, tagname: string, studytrack: string })).isRequired,
|
||||
studyprogramme: string.isRequired
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps, { createTag: createTagAction, deleteTag: deleteTagAction, getTagsByStudytrack: getTagsByStudytrackAction })(Tags))
|
||||
+37
-10
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
import moment from 'moment'
|
||||
import { Header, Loader, Table, Button, Grid, Icon } from 'semantic-ui-react'
|
||||
import { shape, number, arrayOf, bool, string, func } from 'prop-types'
|
||||
@@ -7,9 +7,17 @@ import { withRouter } from 'react-router'
|
||||
import { flatten, uniq } from 'lodash'
|
||||
import { callApi } from '../../../apiConnection'
|
||||
import { getThroughput } from '../../../redux/throughput'
|
||||
import { userRoles } from '../../../common'
|
||||
|
||||
const ThroughputTable = ({ history, throughput, thesis, loading, error, studyprogramme,
|
||||
dispatchGetThroughput }) => {
|
||||
const [roles, setRoles] = useState(undefined)
|
||||
const setFuckingRoles = async () => {
|
||||
setRoles(await userRoles())
|
||||
}
|
||||
useEffect(() => {
|
||||
setFuckingRoles()
|
||||
}, [])
|
||||
const showPopulationStatistics = (yearLabel) => {
|
||||
const year = Number(yearLabel.slice(0, 4))
|
||||
const months = Math.ceil(moment.duration(moment().diff(`${year}-08-01`)).asMonths())
|
||||
@@ -17,6 +25,10 @@ const ThroughputTable = ({ history, throughput, thesis, loading, error, studypro
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=${year}`)
|
||||
}
|
||||
if (error) return <h1>Oh no so error {error}</h1>
|
||||
let GRADUATED_FEATURE_TOGGLED_ON = false
|
||||
if (roles) {
|
||||
GRADUATED_FEATURE_TOGGLED_ON = roles.includes('dev')
|
||||
}
|
||||
const data = throughput && throughput.data ? throughput.data.filter(year => year.credits.length > 0) : []
|
||||
const genders = data.length > 0 ? uniq(flatten(data.map(year => Object.keys(year.genders)))) : []
|
||||
const countries = data.length > 0 && throughput.totals.countries ? uniq(flatten(data.map(year => Object.keys(year.countries)))).sort() : []
|
||||
@@ -67,7 +79,7 @@ const ThroughputTable = ({ history, throughput, thesis, loading, error, studypro
|
||||
<Table.HeaderCell colSpan={genders.length + 1}>Students</Table.HeaderCell> :
|
||||
<Table.HeaderCell rowSpan="2">Students</Table.HeaderCell>
|
||||
}
|
||||
<Table.HeaderCell colSpan="2">Graduated</Table.HeaderCell>
|
||||
<Table.HeaderCell colSpan={GRADUATED_FEATURE_TOGGLED_ON ? '3' : '1'}>Graduated</Table.HeaderCell>
|
||||
|
||||
<Table.HeaderCell rowSpan="2">Transferred to this program</Table.HeaderCell>
|
||||
{
|
||||
@@ -87,13 +99,18 @@ const ThroughputTable = ({ history, throughput, thesis, loading, error, studypro
|
||||
{renderGenders ? <Table.HeaderCell content="Total" /> : null}
|
||||
{genders.map(gender => <Table.HeaderCell key={gender} content={gender} />)}
|
||||
<Table.HeaderCell >Graduated overall</Table.HeaderCell>
|
||||
<Table.HeaderCell >Graduated in time</Table.HeaderCell>
|
||||
{GRADUATED_FEATURE_TOGGLED_ON &&
|
||||
<Fragment>
|
||||
<Table.HeaderCell >Graduated in time</Table.HeaderCell>
|
||||
<Table.HeaderCell >Graduation median time</Table.HeaderCell>
|
||||
</Fragment>
|
||||
}
|
||||
{renderCountries ? countries.map(country => <Table.HeaderCell key={country} content={country} />) : null}
|
||||
<Table.HeaderCell content=">= 30" />
|
||||
<Table.HeaderCell content=">= 60" />
|
||||
<Table.HeaderCell content=">= 90" />
|
||||
<Table.HeaderCell content=">= 120" />
|
||||
<Table.HeaderCell content=">= 150" />
|
||||
<Table.HeaderCell content="≥ 30" />
|
||||
<Table.HeaderCell content="≥ 60" />
|
||||
<Table.HeaderCell content="≥ 90" />
|
||||
<Table.HeaderCell content="≥ 120" />
|
||||
<Table.HeaderCell content="≥ 150" />
|
||||
{thesisTypes.includes('MASTER') && (
|
||||
<Table.HeaderCell content="Master" />
|
||||
)}
|
||||
@@ -120,7 +137,12 @@ const ThroughputTable = ({ history, throughput, thesis, loading, error, studypro
|
||||
</Table.Cell>
|
||||
))}
|
||||
<Table.Cell>{year.graduated}</Table.Cell>
|
||||
<Table.Cell>{year.inTargetTime}</Table.Cell>
|
||||
{GRADUATED_FEATURE_TOGGLED_ON &&
|
||||
<Fragment>
|
||||
<Table.Cell>{year.inTargetTime}</Table.Cell>
|
||||
<Table.Cell>{year.medianGraduationTime ? `${year.medianGraduationTime} months` : '∞'}</Table.Cell>
|
||||
</Fragment>
|
||||
}
|
||||
<Table.Cell>{year.transferred}</Table.Cell>
|
||||
{renderCountries ? countries.map(country => (
|
||||
<Table.Cell key={year.year + country}>
|
||||
@@ -151,7 +173,12 @@ const ThroughputTable = ({ history, throughput, thesis, loading, error, studypro
|
||||
</Table.HeaderCell>
|
||||
))}
|
||||
<Table.HeaderCell>{throughput.totals.graduated}</Table.HeaderCell>
|
||||
<Table.HeaderCell>{throughput.totals.inTargetTime}</Table.HeaderCell>
|
||||
{GRADUATED_FEATURE_TOGGLED_ON &&
|
||||
<Fragment>
|
||||
<Table.HeaderCell>{throughput.totals.inTargetTime}</Table.HeaderCell>
|
||||
<Table.HeaderCell>{throughput.totals.medianGraduationTime ? `${throughput.totals.medianGraduationTime} months` : '∞'}</Table.HeaderCell>
|
||||
</Fragment>
|
||||
}
|
||||
<Table.HeaderCell>{throughput.totals.transferred}</Table.HeaderCell>
|
||||
{renderCountries ? Object.keys(throughput.totals.countries).map(countryKey => (
|
||||
<Table.HeaderCell key={`${countryKey}total`}>
|
||||
|
||||
@@ -11,6 +11,7 @@ import AggregateView from '../CourseGroups/AggregateView'
|
||||
import ThesisCourses from './ThesisCourses'
|
||||
import '../PopulationQueryCard/populationQueryCard.css'
|
||||
import { getRolesWithoutRefreshToken, getRightsWithoutRefreshToken, getTextIn } from '../../common'
|
||||
import Tags from './Tags'
|
||||
|
||||
class StudyProgramme extends Component {
|
||||
static propTypes = {
|
||||
@@ -63,6 +64,12 @@ class StudyProgramme extends Component {
|
||||
menuItem: 'Thesis Courses',
|
||||
render: () => <ThesisCourses studyprogramme={studyProgrammeId} />
|
||||
})
|
||||
if (getRolesWithoutRefreshToken().includes('admin')) {
|
||||
panes.push({
|
||||
menuItem: 'Tags',
|
||||
render: () => <Tags studyprogramme={studyProgrammeId} />
|
||||
})
|
||||
}
|
||||
return panes
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import { Button, Dropdown, Checkbox } from 'semantic-ui-react'
|
||||
import { arrayOf, string, shape, func, bool } from 'prop-types'
|
||||
|
||||
import {
|
||||
createStudentTagAction,
|
||||
getStudentTagsByStudytrackAction
|
||||
} from '../../redux/tagstudent'
|
||||
|
||||
|
||||
const TagPopulation = ({
|
||||
allChecker,
|
||||
handleAllCheck,
|
||||
falsifyChecks,
|
||||
createStudentTag,
|
||||
tags,
|
||||
checkedStudents,
|
||||
getStudentTagsByStudytrack,
|
||||
studytrack,
|
||||
created }) => {
|
||||
const [options, setOptions] = useState([])
|
||||
const [selectedValue, setSelected] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const createdOptions = tags.map(tag => ({ key: tag.tag_id, text: tag.tagname, value: tag.tag_id }))
|
||||
setOptions(createdOptions)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (created) {
|
||||
getStudentTagsByStudytrack(studytrack)
|
||||
}
|
||||
}, [created])
|
||||
|
||||
const handleChange = (event, { value }) => {
|
||||
event.preventDefault()
|
||||
setSelected(value)
|
||||
}
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault()
|
||||
falsifyChecks()
|
||||
checkedStudents.forEach((student) => {
|
||||
if (student.checked) {
|
||||
const tag = {
|
||||
tag_id: selectedValue,
|
||||
studentnumber: student.studentnumber
|
||||
}
|
||||
createStudentTag(tag)
|
||||
setSelected('')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={allChecker}
|
||||
onChange={handleAllCheck}
|
||||
/>
|
||||
<Dropdown
|
||||
placeholder="Tag"
|
||||
search
|
||||
selection
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
value={selectedValue}
|
||||
/>
|
||||
<Button onClick={handleSubmit}>add tag to multiple students</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
TagPopulation.propTypes = {
|
||||
createStudentTag: func.isRequired,
|
||||
getStudentTagsByStudytrack: func.isRequired,
|
||||
checkedStudents: arrayOf(shape({ studentnumber: string, checked: bool })).isRequired,
|
||||
tags: arrayOf(shape({ tag_id: string, tagname: string, studytrack: string })).isRequired,
|
||||
studytrack: string.isRequired,
|
||||
created: bool.isRequired,
|
||||
falsifyChecks: func.isRequired,
|
||||
allChecker: bool.isRequired,
|
||||
handleAllCheck: func.isRequired
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ tagstudent }) => ({
|
||||
created: tagstudent.created
|
||||
})
|
||||
|
||||
|
||||
export default withRouter(connect(mapStateToProps, { createStudentTag: createStudentTagAction, getStudentTagsByStudytrack: getStudentTagsByStudytrackAction })(TagPopulation))
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import { Dropdown, List, Label, Icon } from 'semantic-ui-react'
|
||||
import { arrayOf, string, shape, func, bool } from 'prop-types'
|
||||
|
||||
import {
|
||||
createStudentTagAction,
|
||||
deleteStudentTagAction,
|
||||
getStudentTagsByStudytrackAction
|
||||
} from '../../redux/tagstudent'
|
||||
|
||||
const TagStudent = ({
|
||||
createStudentTag,
|
||||
deleteStudentTag,
|
||||
getStudentTagsByStudytrack,
|
||||
studentnumber,
|
||||
studentstags,
|
||||
tags,
|
||||
studytrack,
|
||||
success,
|
||||
data }) => {
|
||||
const [allTags, setTags] = useState([])
|
||||
const [studentsTagIds, setStudentsTagIds] = useState([])
|
||||
const [tagOptions, setTagOptions] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
setTags(tags)
|
||||
const tagIds = studentstags.map(t => ({ id: t.id, tag_id: t.tag.tag_id }))
|
||||
setStudentsTagIds(tagIds)
|
||||
const initialTagOptions = tags.filter(tag => !tagIds.includes(tag.tag_id)).map(tag => ({
|
||||
key: tag.tag_id,
|
||||
text: tag.tagname,
|
||||
value: tag.tag_id
|
||||
}))
|
||||
setTagOptions(initialTagOptions)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (success) {
|
||||
const studentData = data.filter(row => row.studentnumber === studentnumber)
|
||||
const newTagIds = studentData.map(t => t.tag_id)
|
||||
const filteredData = allTags.filter(tag => !newTagIds.includes(tag.tag_id)).map(tag => ({
|
||||
key: tag.tag_id,
|
||||
text: tag.tagname,
|
||||
value: tag.tag_id
|
||||
}))
|
||||
setStudentsTagIds(studentData)
|
||||
setTagOptions(filteredData)
|
||||
}
|
||||
}, [success])
|
||||
|
||||
const handleChange = async (event, { value }) => {
|
||||
event.preventDefault()
|
||||
const tag = {
|
||||
tag_id: value,
|
||||
studentnumber
|
||||
}
|
||||
await createStudentTag(tag)
|
||||
getStudentTagsByStudytrack(studytrack)
|
||||
}
|
||||
|
||||
const deleteTag = async (event, tag) => {
|
||||
event.preventDefault()
|
||||
const removableTag = studentsTagIds.find(t => t.tag_id === tag.tag_id)
|
||||
await deleteStudentTag(removableTag.id)
|
||||
getStudentTagsByStudytrack(studytrack)
|
||||
}
|
||||
|
||||
const studentsTags = allTags
|
||||
.filter(tag => studentsTagIds.map(t => t.tag_id).includes(tag.tag_id))
|
||||
.map(tag => (
|
||||
<List.Item key={tag.tag_id} >
|
||||
<List.Content>
|
||||
<Label>
|
||||
{tag.tagname} <Icon name="delete" link onClick={event => deleteTag(event, tag)} />
|
||||
</Label>
|
||||
</List.Content>
|
||||
</List.Item>))
|
||||
|
||||
return (
|
||||
<List horizontal>
|
||||
<List.Item >
|
||||
<List.Content>
|
||||
{studentnumber}
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
{studentsTags}
|
||||
<List.Item>
|
||||
<List.Content>
|
||||
<Dropdown
|
||||
placeholder="Tag"
|
||||
search
|
||||
selection
|
||||
options={tagOptions}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
TagStudent.propTypes = {
|
||||
createStudentTag: func.isRequired,
|
||||
deleteStudentTag: func.isRequired,
|
||||
getStudentTagsByStudytrack: func.isRequired,
|
||||
studentnumber: string.isRequired,
|
||||
studentstags: arrayOf(shape({ tag: shape({ tagname: string, tag_id: string }), id: string })).isRequired,
|
||||
tags: arrayOf(shape({ tag_id: string, tagname: string, studytrack: string })).isRequired,
|
||||
studytrack: string.isRequired,
|
||||
success: bool.isRequired,
|
||||
data: arrayOf(shape({ studentnumber: string, tag_id: string })).isRequired
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ tagstudent }) => ({
|
||||
success: tagstudent.success,
|
||||
data: tagstudent.data
|
||||
})
|
||||
|
||||
export default withRouter(connect(mapStateToProps, {
|
||||
createStudentTag: createStudentTagAction,
|
||||
deleteStudentTag: deleteStudentTagAction,
|
||||
getStudentTagsByStudytrack: getStudentTagsByStudytrackAction
|
||||
})(TagStudent))
|
||||
@@ -88,7 +88,7 @@ class TeacherLeaderBoard extends Component {
|
||||
|
||||
<TeacherStatisticsTable
|
||||
statistics={statistics}
|
||||
onClickFn={e => this.props.history.push(`/teachers/${e.target.innerText}`)}
|
||||
onClickFn={id => this.props.history.push(`/teachers/${id}`)}
|
||||
/>
|
||||
</Segment>
|
||||
</div>
|
||||
|
||||
@@ -134,8 +134,8 @@ class TeacherStatistics extends Component {
|
||||
<Segment>
|
||||
<TeacherStatisticsTable
|
||||
statistics={statistics}
|
||||
onClickFn={e =>
|
||||
this.props.history.push(`/teachers/${e.target.innerText}`)
|
||||
onClickFn={id =>
|
||||
this.props.history.push(`/teachers/${id}`)
|
||||
}
|
||||
/>
|
||||
</Segment>
|
||||
|
||||
@@ -1,55 +1,52 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Table, Segment } from 'semantic-ui-react'
|
||||
import { shape, arrayOf, any, string, number } from 'prop-types'
|
||||
import { shape, arrayOf, any, string, number, func } from 'prop-types'
|
||||
import _ from 'lodash'
|
||||
|
||||
const calculatePassrate = (pass, fail) => (100 * pass) / (pass + fail)
|
||||
|
||||
class TeacherStatisticsTable extends Component {
|
||||
state={
|
||||
selected: 'credits',
|
||||
direction: 'descending'
|
||||
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'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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, onClickFn } = this.props
|
||||
const { selected, direction } = this.state
|
||||
|
||||
render() {
|
||||
const { statistics } = this.props
|
||||
const { selected, direction } = this.state
|
||||
const sortDirection = name => (selected === name ? direction : null)
|
||||
|
||||
const sortDirection = name => (selected === name ? direction : null)
|
||||
|
||||
return (statistics.length === 0 ? <Segment basic content="No statistics found for the given query." /> :
|
||||
<Table structured celled textAlign="center" sortable>
|
||||
return (statistics.length === 0 ?
|
||||
<Segment basic content="No statistics found for the given query." />
|
||||
:
|
||||
<Table structured celled textAlign="center" sortable selectable>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell
|
||||
content="ID"
|
||||
onClick={this.handleSort('id')}
|
||||
sorted={sortDirection('id')}
|
||||
/>
|
||||
<Table.HeaderCell
|
||||
content="Name"
|
||||
textAlign="left"
|
||||
@@ -74,21 +71,19 @@ class TeacherStatisticsTable extends Component {
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{ this.sortStatistics(statistics).map(({ id, name, credits, passrate, transferred }) => (
|
||||
<Table.Row key={id}>
|
||||
<Table.Cell content={id} width={1} onClick={this.props.onClickFn} /> {/* eslint-disable-line*/}
|
||||
{this.sortStatistics(statistics).map(({ id, name, credits, passrate, transferred }) => (
|
||||
<Table.Row key={id} onClick={() => onClickFn(id)} style={{ cursor: 'pointer' }} >
|
||||
<Table.Cell content={name} textAlign="left" />
|
||||
<Table.Cell content={credits} width={2} />
|
||||
<Table.Cell content={transferred} width={2} />
|
||||
<Table.Cell content={`${parseFloat(passrate).toFixed(2)} %`} width={2} />
|
||||
</Table.Row>
|
||||
))}
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TeacherStatisticsTable.propTypes = {
|
||||
statistics: arrayOf(shape({
|
||||
id: any,
|
||||
@@ -97,7 +92,9 @@ TeacherStatisticsTable.propTypes = {
|
||||
credits: any,
|
||||
failed: number,
|
||||
passed: number
|
||||
})).isRequired
|
||||
})).isRequired,
|
||||
onClickFn: func.isRequired
|
||||
}
|
||||
|
||||
|
||||
export default TeacherStatisticsTable
|
||||
|
||||
@@ -7,7 +7,7 @@ import { modifyAccessGroups } from '../../redux/users'
|
||||
import { getIdWithoutRefreshToken, setToken } from '../../common'
|
||||
|
||||
class AccessGroups extends Component {
|
||||
state={
|
||||
state = {
|
||||
selected: this.props.userGroups
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Button, Card, Divider, List, Icon, Popup } from 'semantic-ui-react'
|
||||
import { Button, Card, Divider, List, Icon, Popup, Dropdown, Header } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
import { withRouter } from 'react-router'
|
||||
import { string, number, shape, bool, arrayOf, func, object } from 'prop-types'
|
||||
import { getTextIn, getRolesWithoutRefreshToken } from '../../common'
|
||||
import { removeUserUnits, getAccessGroups } from '../../redux/users'
|
||||
import { removeUserUnits, getAccessGroups, setFaculties } from '../../redux/users'
|
||||
import { getFaculties } from '../../redux/faculties'
|
||||
import { getDegreesAndProgrammesUnfiltered } from '../../redux/populationDegreesAndProgrammesUnfiltered'
|
||||
import { superLogin } from '../../apiConnection'
|
||||
import AccessRights from './AccessRights'
|
||||
@@ -33,6 +34,7 @@ class UserPage extends Component {
|
||||
this.props.getDegreesAndProgrammesUnfiltered()
|
||||
await this.props.getAccessGroups()
|
||||
}
|
||||
await this.props.getFaculties()
|
||||
}
|
||||
|
||||
getDisabledUnits = (units, enabled) => {
|
||||
@@ -85,20 +87,11 @@ class UserPage extends Component {
|
||||
|
||||
renderUnitList = (elementdetails, user) => {
|
||||
const { language } = this.props
|
||||
|
||||
const nameInLanguage = element => getTextIn(element.name, language)
|
||||
|
||||
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 => (
|
||||
{_.sortBy(elementdetails, 'code').map(element => (
|
||||
<List.Item key={element.code}>
|
||||
<List.Content floated="right">
|
||||
<Button
|
||||
@@ -120,28 +113,7 @@ class UserPage extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, associations } = this.props
|
||||
// ugly trick to add associations to study tracks, should be moved to backend
|
||||
if (associations.programmes) {
|
||||
const programmes = user.elementdetails.filter(e => e.type === 20).map(e => e.code)
|
||||
user.elementdetails = user.elementdetails.map((element) => {
|
||||
const e = Object.assign(element)
|
||||
if (associations && (element.type === 30)) {
|
||||
const studyright = associations.studyTracks[element.code]
|
||||
if (studyright && studyright.programmes) {
|
||||
e.associations = _.intersection(
|
||||
programmes,
|
||||
Object.keys(studyright.programmes)
|
||||
)
|
||||
} else {
|
||||
e.associations = programmes
|
||||
}
|
||||
} else {
|
||||
e.associations = []
|
||||
}
|
||||
return e
|
||||
})
|
||||
}
|
||||
const { user, language } = this.props
|
||||
return this.props.accessGroups ?
|
||||
<div>
|
||||
<Button icon="arrow circle left" content="Back" onClick={this.props.goBack} />
|
||||
@@ -191,9 +163,19 @@ class UserPage extends Component {
|
||||
color: 'darkred',
|
||||
border: '1px'
|
||||
}}
|
||||
>everything!
|
||||
>Admin access!
|
||||
</p> : null}
|
||||
{this.renderUnitList(user.elementdetails, user)}
|
||||
{this.renderUnitList(user.programme, user)}
|
||||
<Header content="Faculties" />
|
||||
<Dropdown
|
||||
placeholder="Select faculties"
|
||||
fluid
|
||||
multiple
|
||||
search
|
||||
value={this.props.user.faculty.map(f => f.faculty_code)}
|
||||
options={_.sortBy(this.props.faculties.map(f => ({ key: f.code, text: getTextIn(f.name, language), description: f.code, value: f.code })), ['text'])}
|
||||
onChange={(__, { value: facultycodes }) => this.props.setFaculties(user.id, facultycodes)}
|
||||
/>
|
||||
</Card.Description>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
@@ -213,9 +195,23 @@ UserPage.propTypes = {
|
||||
code: string,
|
||||
name: shape({}),
|
||||
type: number
|
||||
})),
|
||||
programme: arrayOf(shape({
|
||||
code: string,
|
||||
name: shape({}),
|
||||
type: number
|
||||
})),
|
||||
faculty: arrayOf(shape({
|
||||
faculty_code: string,
|
||||
programme: arrayOf(shape({
|
||||
code: string,
|
||||
name: shape({}),
|
||||
type: number
|
||||
}))
|
||||
}))
|
||||
}).isRequired,
|
||||
removeUserUnits: func.isRequired,
|
||||
setFaculties: func.isRequired,
|
||||
language: string.isRequired,
|
||||
goBack: func.isRequired,
|
||||
getDegreesAndProgrammesUnfiltered: func.isRequired,
|
||||
@@ -225,12 +221,15 @@ UserPage.propTypes = {
|
||||
push: func.isRequired
|
||||
}).isRequired,
|
||||
getAccessGroups: func.isRequired,
|
||||
getFaculties: func.isRequired,
|
||||
faculties: arrayOf(shape({ code: string, name: shape({}) })).isRequired,
|
||||
accessGroups: arrayOf(object).isRequired,
|
||||
isAdmin: bool.isRequired
|
||||
}
|
||||
const mapStateToProps = state => ({
|
||||
language: state.settings.language,
|
||||
units: state.units.data,
|
||||
faculties: state.faculties.data,
|
||||
associations: state.populationDegreesAndProgrammesUnfiltered.data,
|
||||
pending: !!state.populationDegreesAndProgrammesUnfiltered.pending,
|
||||
accessGroups: state.users.accessGroupsData || [],
|
||||
@@ -239,6 +238,8 @@ const mapStateToProps = state => ({
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
removeUserUnits,
|
||||
setFaculties,
|
||||
getDegreesAndProgrammesUnfiltered,
|
||||
getAccessGroups
|
||||
getAccessGroups,
|
||||
getFaculties
|
||||
})(withRouter(UserPage))
|
||||
|
||||
@@ -63,14 +63,14 @@ export const routes = {
|
||||
usage: { menuRoute: '/usage', route: '/usage', translateId: 'usage', reqRights: ['usage'] },
|
||||
sandbox: { menuRoute: '/sandbox', route: '/sandbox', translateId: 'sandbox', reqRights: ['dev'] },
|
||||
oodilearn: { menuRoute: '/oodilearn', route: '/oodilearn', translateId: 'oodilearn', reqRights: ['oodilearn'] },
|
||||
feedback: { menuRoute: '/feedback', route: '/feedback', translateId: 'feedback', reqRights: ['users'] }
|
||||
feedback: { menuRoute: '/feedback', route: '/feedback', translateId: 'feedback' }
|
||||
}
|
||||
|
||||
export const hiddenRoutes = {
|
||||
}
|
||||
|
||||
const assumeBasename = () => {
|
||||
const POSSIBLE_BASENAMES = ['staging', 'testing']
|
||||
const POSSIBLE_BASENAMES = ['testing']
|
||||
const haystack = window.location.pathname.split('/')
|
||||
const needle = haystack.find(path => POSSIBLE_BASENAMES.includes(path))
|
||||
return needle ? `/${needle}/` : '/'
|
||||
@@ -86,16 +86,16 @@ 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' : window.location.pathname.includes('/testing') ? 'testing_token' : 'token' //eslint-disable-line
|
||||
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) => ({
|
||||
export const passRateCumGraphOptions = (categories, max, title) => ({
|
||||
chart: {
|
||||
type: 'column'
|
||||
},
|
||||
colors: [chartblue, green, red],
|
||||
|
||||
title: {
|
||||
text: 'Pass rate chart'
|
||||
text: title
|
||||
},
|
||||
|
||||
xAxis: {
|
||||
@@ -119,14 +119,14 @@ export const passRateCumGraphOptions = (categories, max) => ({
|
||||
}
|
||||
})
|
||||
|
||||
export const passRateStudGraphOptions = (categories, max) => ({
|
||||
export const passRateStudGraphOptions = (categories, max, title) => ({
|
||||
chart: {
|
||||
type: 'column'
|
||||
},
|
||||
colors: [chartblue, chartlgreen, chartdarkg, chartlred, chartdarkred],
|
||||
|
||||
title: {
|
||||
text: 'Pass rate chart'
|
||||
text: title
|
||||
},
|
||||
|
||||
xAxis: {
|
||||
@@ -150,14 +150,14 @@ export const passRateStudGraphOptions = (categories, max) => ({
|
||||
}
|
||||
})
|
||||
|
||||
export const gradeGraphOptions = (categories, max) => ({
|
||||
export const gradeGraphOptions = (categories, max, title) => ({
|
||||
chart: {
|
||||
type: 'column'
|
||||
},
|
||||
colors: [red, chartblue, chartblue, chartblue, chartblue, chartblue, green, green],
|
||||
|
||||
title: {
|
||||
text: 'Grades'
|
||||
text: title
|
||||
},
|
||||
|
||||
legend: {
|
||||
|
||||
@@ -245,7 +245,6 @@ export const gradeMeanFilter = (params) => {
|
||||
},
|
||||
filter: (student) => {
|
||||
const gradeMeanOfStudent = getStudentGradeMean(student)
|
||||
|
||||
if (comparator === 'less') {
|
||||
return gradeMean > gradeMeanOfStudent
|
||||
}
|
||||
@@ -254,6 +253,22 @@ export const gradeMeanFilter = (params) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const tagFilter = (params) => {
|
||||
const { text, value } = params.tag
|
||||
return ({
|
||||
id: uuidv4(),
|
||||
type: 'TagFilter',
|
||||
params: {
|
||||
text,
|
||||
value
|
||||
},
|
||||
filter: (student) => {
|
||||
const studentTagIds = student.tags.map(t => t.tag.tag_id)
|
||||
return studentTagIds.includes(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const presetFilter = preset => ({
|
||||
id: preset.id,
|
||||
type: 'Preset',
|
||||
@@ -268,6 +283,7 @@ const typeList = {
|
||||
CreditsAtLeast: creditsAtLeast,
|
||||
HasMatriculation: matriculationFilter,
|
||||
SexFilter: sexFilter,
|
||||
GradeMeanFilter: gradeMeanFilter,
|
||||
CourseParticipation: courseParticipation,
|
||||
StartingThisSemester: startingThisSemester,
|
||||
ExtentGraduated: extentGraduated,
|
||||
@@ -278,7 +294,8 @@ const typeList = {
|
||||
CanceledStudyright: canceledStudyright,
|
||||
Preset: presetFilter,
|
||||
TransferToStudyrightFilter: transferTo,
|
||||
PriorityStudyright: priorityStudyright
|
||||
PriorityStudyright: priorityStudyright,
|
||||
TagFilter: tagFilter
|
||||
}
|
||||
export const getFilterFunction = (type, params, populationCourses) => {
|
||||
switch (type) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { callController } from '../apiConnection'
|
||||
|
||||
export const getFaculties = () => {
|
||||
const route = '/faculties'
|
||||
const prefix = 'GET_FACULTIES_'
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
const reducer = (state = { data: [], pending: false, error: false }, action) => {
|
||||
switch (action.type) {
|
||||
case 'GET_FACULTIES_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true,
|
||||
error: false
|
||||
}
|
||||
case 'GET_FACULTIES_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: true
|
||||
}
|
||||
case 'GET_FACULTIES_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
data: action.response,
|
||||
pending: false,
|
||||
error: true
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer
|
||||
@@ -10,6 +10,7 @@ import populationDegreesAndProgrammesUnfiltered from './populationDegreesAndProg
|
||||
import populationMandatoryCourses from './populationMandatoryCourses'
|
||||
import studyProgrammeProductivity from './productivity'
|
||||
import studyProgrammeThroughput from './throughput'
|
||||
import faculties from './faculties'
|
||||
import units from './units'
|
||||
import students from './students'
|
||||
import errors from './errors'
|
||||
@@ -42,11 +43,15 @@ import oodilearnPopulationCourseSelect from './oodilearnPopulationCourseSelect'
|
||||
import thesisCourses from './thesisCourses'
|
||||
import accessGroups from './accessGroups'
|
||||
import feedback from './feedback'
|
||||
import mandatoryCourseLabels from './mandatoryCourseLabels'
|
||||
import tags from './tags'
|
||||
import tagstudent from './tagstudent'
|
||||
|
||||
export default combineReducers({
|
||||
locale,
|
||||
actionHistory,
|
||||
users,
|
||||
mandatoryCourseLabels,
|
||||
populations,
|
||||
populationCourses,
|
||||
populationFilters,
|
||||
@@ -55,6 +60,7 @@ export default combineReducers({
|
||||
populationMandatoryCourses,
|
||||
studyProgrammeProductivity,
|
||||
studyProgrammeThroughput,
|
||||
faculties,
|
||||
units,
|
||||
students,
|
||||
errors,
|
||||
@@ -86,5 +92,7 @@ export default combineReducers({
|
||||
oodilearnPopulationCourseSelect,
|
||||
thesisCourses,
|
||||
accessGroups,
|
||||
feedback
|
||||
feedback,
|
||||
tags,
|
||||
tagstudent
|
||||
})
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { callController } from '../apiConnection/index'
|
||||
|
||||
export const GetMandatoryCourseLabels = (programme) => {
|
||||
const prefix = 'GET_MANDATORY_COURSE_LABELS_'
|
||||
const route = `/mandatory-course-labels/${programme}/labels`
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
export const AddMandatoryCourseLabel = (programme, label) => {
|
||||
const prefix = 'ADD_MANDATORY_COURSE_LABELS_'
|
||||
const route = `/mandatory-course-labels/${programme}/labels`
|
||||
const method = 'post'
|
||||
const data = { label }
|
||||
return callController(route, prefix, data, method)
|
||||
}
|
||||
|
||||
export const DeleteMandatoryCourseLabel = (programme, label) => {
|
||||
const prefix = 'DELETE_MANDATORY_COURSE_LABELS_'
|
||||
const route = `/mandatory-course-labels/${programme}/labels`
|
||||
const method = 'delete'
|
||||
const data = { label }
|
||||
return callController(route, prefix, data, method)
|
||||
}
|
||||
|
||||
export const MoveMandatoryCourseLabel = (programme, label, direction) => {
|
||||
const prefix = 'MOVE_MANDATORY_COURSE_LABELS_'
|
||||
const route = `/mandatory-course-labels/${programme}/move`
|
||||
const method = 'post'
|
||||
const data = { label, direction }
|
||||
return callController(route, prefix, data, method)
|
||||
}
|
||||
|
||||
const reducer = (state = { data: [] }, action) => {
|
||||
switch (action.type) {
|
||||
case 'GET_MANDATORY_COURSE_LABELS_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true
|
||||
}
|
||||
case 'GET_MANDATORY_COURSE_LABELS_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: true
|
||||
}
|
||||
case 'GET_MANDATORY_COURSE_LABELS_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: false,
|
||||
data: action.response
|
||||
}
|
||||
case 'ADD_MANDATORY_COURSE_LABELS_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true
|
||||
}
|
||||
case 'ADD_MANDATORY_COURSE_LABELS_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: true
|
||||
}
|
||||
case 'ADD_MANDATORY_COURSE_LABELS_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: false,
|
||||
data: action.response
|
||||
}
|
||||
case 'DELETE_MANDATORY_COURSE_LABELS_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true
|
||||
}
|
||||
case 'DELETE_MANDATORY_COURSE_LABELS_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: true
|
||||
}
|
||||
case 'DELETE_MANDATORY_COURSE_LABELS_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: false,
|
||||
data: action.response
|
||||
}
|
||||
case 'MOVE_MANDATORY_COURSE_LABELS_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true
|
||||
}
|
||||
case 'MOVE_MANDATORY_COURSE_LABELS_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: true
|
||||
}
|
||||
case 'MOVE_MANDATORY_COURSE_LABELS_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: false,
|
||||
data: action.response
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer
|
||||
@@ -39,6 +39,7 @@ const reducer = (state = { data: [] }, action) => {
|
||||
pending: false,
|
||||
error: true,
|
||||
selected: state.selected,
|
||||
lastSearch: state.lastSearch,
|
||||
data: state.data
|
||||
}
|
||||
case 'FIND_STUDENTS_SUCCESS':
|
||||
@@ -46,6 +47,7 @@ const reducer = (state = { data: [] }, action) => {
|
||||
pending: false,
|
||||
error: false,
|
||||
selected: state.selected,
|
||||
lastSearch: state.lastSearch,
|
||||
data: state.lastSearch === action.query ?
|
||||
[...state.data.filter(student => student.fetched), ...action.response] :
|
||||
state.data
|
||||
@@ -55,6 +57,7 @@ const reducer = (state = { data: [] }, action) => {
|
||||
pending: false,
|
||||
error: false,
|
||||
selected: action.response.studentNumber,
|
||||
lastSearch: state.lastSearch,
|
||||
data: [...state.data.filter(student =>
|
||||
student.studentNumber !== action.response.studentNumber),
|
||||
{ ...action.response, ...{ fetched: true } }
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { callController } from '../apiConnection'
|
||||
|
||||
export const getTagsAction = () => {
|
||||
const route = '/tags'
|
||||
const prefix = 'GET_TAGS_'
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
export const getTagsByStudytrackAction = (studytrack) => {
|
||||
const route = `/tags/${studytrack}`
|
||||
const prefix = 'GET_TAGS_BY_ST_'
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
export const createTagAction = (tag) => {
|
||||
const route = '/tags'
|
||||
const prefix = 'CREATE_TAG_'
|
||||
const method = 'post'
|
||||
const data = { tag }
|
||||
return callController(route, prefix, data, method)
|
||||
}
|
||||
|
||||
export const deleteTagAction = (tag) => {
|
||||
const route = '/tags'
|
||||
const prefix = 'DELETE_TAG_'
|
||||
const method = 'delete'
|
||||
const data = { tag }
|
||||
return callController(route, prefix, data, method)
|
||||
}
|
||||
|
||||
const reducer = (state = { data: [] }, action) => {
|
||||
switch (action.type) {
|
||||
case 'GET_TAGS_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true
|
||||
}
|
||||
case 'GET_TAGS_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
data: action.response || {}
|
||||
}
|
||||
case 'GET_TAGS_BY_ST_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true
|
||||
}
|
||||
case 'GET_TAGS_BY_ST_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
data: action.response || {}
|
||||
}
|
||||
case 'GET_TAGS_BY_ST_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
data: action.response || {}
|
||||
}
|
||||
case 'GET_TAGS_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false
|
||||
}
|
||||
case 'CREATE_TAG_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true
|
||||
}
|
||||
case 'CREATE_TAG_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false
|
||||
}
|
||||
case 'CREATE_TAG_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false
|
||||
}
|
||||
case 'DELETE_TAG_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true
|
||||
}
|
||||
case 'DELETE_TAG_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false
|
||||
}
|
||||
case 'DELETE_TAG_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer
|
||||
@@ -0,0 +1,129 @@
|
||||
import { callController } from '../apiConnection'
|
||||
|
||||
export const getStudentTagsAction = () => {
|
||||
const route = '/studenttags'
|
||||
const prefix = 'GET_STUDENT_TAGS_'
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
export const createStudentTagAction = (tag) => {
|
||||
const route = '/studenttags'
|
||||
const prefix = 'CREATE_STUDENT_TAG_'
|
||||
const method = 'post'
|
||||
const data = { tag }
|
||||
return callController(route, prefix, data, method)
|
||||
}
|
||||
|
||||
export const getStudentTagsByStudytrackAction = (studytrack) => {
|
||||
const route = `/studenttags/${studytrack}`
|
||||
const prefix = 'GET_STUDENT_TAGS_BY_ST_'
|
||||
const data = { studytrack }
|
||||
return callController(route, prefix, data)
|
||||
}
|
||||
|
||||
export const getStudentTagsByStudentnumberAction = (studentnumber) => {
|
||||
const route = `/studenttags/${studentnumber}`
|
||||
const prefix = 'GET_STUDENT_TAG_BY_SN_'
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
export const deleteStudentTagAction = (id) => {
|
||||
const route = '/studenttags'
|
||||
const prefix = 'DELETE_STUDENT_TAG_'
|
||||
const method = 'delete'
|
||||
const data = { id }
|
||||
return callController(route, prefix, data, method)
|
||||
}
|
||||
|
||||
const reducer = (state = { data: [], success: false, created: false }, action) => {
|
||||
switch (action.type) {
|
||||
case 'GET_STUDENT_TAGS_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true
|
||||
}
|
||||
case 'GET_STUDENT_TAGS_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
data: action.response || {}
|
||||
}
|
||||
case 'GET_STUDENT_TAGS_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false
|
||||
}
|
||||
case 'GET_STUDENT_TAGS_BY_ST_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true,
|
||||
success: false
|
||||
}
|
||||
case 'GET_STUDENT_TAGS_BY_ST_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
success: false
|
||||
}
|
||||
case 'GET_STUDENT_TAGS_BY_ST_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
data: action.response || [],
|
||||
success: true
|
||||
}
|
||||
case 'GET_STUDENT_TAG_BY_SN_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true
|
||||
}
|
||||
case 'GET_STUDENT_TAG_BY_SN_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false
|
||||
}
|
||||
case 'GET_STUDENT_TAG_BY_SN_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
data: action.response || []
|
||||
}
|
||||
case 'CREATE_STUDENT_TAG_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true,
|
||||
created: false
|
||||
}
|
||||
case 'CREATE_STUDENT_TAG_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
created: false
|
||||
}
|
||||
case 'CREATE_STUDENT_TAG_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
created: true
|
||||
}
|
||||
case 'DELETE_STUDENT_TAG_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true
|
||||
}
|
||||
case 'DELETE_STUDENT_TAG_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false
|
||||
}
|
||||
case 'DELETE_STUDENT_TAG_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
pending: false
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer
|
||||
@@ -39,6 +39,13 @@ export const removeUserUnits = (uid, codes) => {
|
||||
return callController(route, prefix, data, 'delete')
|
||||
}
|
||||
|
||||
export const setFaculties = (uid, faculties) => {
|
||||
const route = `/users/${uid}/faculties`
|
||||
const data = { faculties }
|
||||
const prefix = 'SET_USER_FACULTIES_'
|
||||
return callController(route, prefix, data, 'post')
|
||||
}
|
||||
|
||||
export const sendEmail = (email) => {
|
||||
const route = '/users/email'
|
||||
const prefix = 'SEND_EMAIL_'
|
||||
@@ -135,6 +142,23 @@ const reducer = (state = { data: [], enabledOnly: true }, action) => {
|
||||
data: state.data.filter(user => user.id !== action.response.id)
|
||||
.concat(action.response)
|
||||
}
|
||||
case 'SET_USER_FACULTIES_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
setfacultypending: true
|
||||
}
|
||||
case 'SET_USER_FACULTIES_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
setfacultypending: false
|
||||
}
|
||||
case 'SET_USER_FACULTIES_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
setfacultypending: false,
|
||||
data: state.data.filter(user => user.id !== action.response.id)
|
||||
.concat(action.response)
|
||||
}
|
||||
case 'SEND_EMAIL_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -85,9 +85,8 @@ module.exports = (env, args) => {
|
||||
port: devServerPort,
|
||||
proxy: [
|
||||
{
|
||||
context: ['/api', '/staging/api'],
|
||||
target: backendURL,
|
||||
pathRewrite: { '^/staging': '' }
|
||||
context: ['/api'],
|
||||
target: backendURL
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,22 +16,12 @@ app.get('/ping', (req, res) => res.json({ message: 'pong '}))
|
||||
|
||||
app.get('/findall', async (req, res) => {
|
||||
const users = await User.findAll()
|
||||
const returnedUsers = users.map(u => {
|
||||
const enabled = requiredGroup === null || u.hy_group.some(e => e.code === requiredGroup)
|
||||
return {...u.get(), is_enabled: enabled, hy_group: null}
|
||||
})
|
||||
res.json(returnedUsers)
|
||||
res.json(users.map(User.getUserData))
|
||||
})
|
||||
|
||||
app.get('/findallenabled', async (req, res) => {
|
||||
const users = await User.findAll()
|
||||
const returnedUsers = users.filter(u => (
|
||||
requiredGroup === null || u.hy_group.some(e => e.code === requiredGroup)
|
||||
)).map(u => {
|
||||
const enabled = requiredGroup === null || u.hy_group.some(e => e.code === requiredGroup)
|
||||
return {...u.get(), is_enabled: enabled, hy_group: null}
|
||||
})
|
||||
res.json(returnedUsers)
|
||||
res.json(users.map(User.getUserData).filter(u => u.is_enabled))
|
||||
})
|
||||
|
||||
app.get('/user/:uid', async (req, res) => {
|
||||
@@ -40,12 +30,13 @@ app.get('/user/:uid', async (req, res) => {
|
||||
|
||||
console.log(JSON.stringify(user))
|
||||
|
||||
res.json(user)
|
||||
res.json(User.getUserData(user))
|
||||
})
|
||||
|
||||
app.get('/user/elementdetails/:username', async (req, res) => {
|
||||
const username = req.params.username
|
||||
const elementdetails = await User.getUserElementDetails(username)
|
||||
const user = await byUsername(username)
|
||||
const elementdetails = User.getUserElementDetails(user)
|
||||
|
||||
console.log(JSON.stringify(elementdetails))
|
||||
|
||||
@@ -58,7 +49,7 @@ app.get('/user/id/:id', async (req, res) => {
|
||||
|
||||
console.log(JSON.stringify(user))
|
||||
|
||||
res.json(user)
|
||||
res.json(User.getUserData(user))
|
||||
})
|
||||
|
||||
app.post('/user', async (req, res) => {
|
||||
@@ -95,7 +86,7 @@ app.put('/user/:uid', async (req, res) => {
|
||||
}
|
||||
await User.updateUser(user, req.body)
|
||||
const returnedUser = await User.byUsername(uid)
|
||||
res.json(returnedUser)
|
||||
res.json(User.getUserData(returnedUser))
|
||||
})
|
||||
|
||||
app.post('/modifyaccess', async (req, res) => {
|
||||
@@ -103,7 +94,7 @@ app.post('/modifyaccess', async (req, res) => {
|
||||
try {
|
||||
await User.modifyRights(uid, accessgroups)
|
||||
const user = await User.byId(uid)
|
||||
res.status(200).json(user)
|
||||
res.status(200).json(User.getUserData(user))
|
||||
} catch (e) {
|
||||
res.status(400).json({ e })
|
||||
}
|
||||
@@ -115,7 +106,7 @@ app.post('/add_rights', async (req, res) => {
|
||||
try {
|
||||
await User.enableElementDetails(uid, codes)
|
||||
const user = await User.byId(uid)
|
||||
res.status(200).json({ user })
|
||||
res.status(200).json({ user: User.getUserData(user) })
|
||||
|
||||
} catch (e) {
|
||||
res.status(400).json({ e })
|
||||
@@ -127,9 +118,22 @@ app.post('/remove_rights', async (req, res) => {
|
||||
try {
|
||||
await User.removeElementDetails(uid, codes)
|
||||
const user = await User.byId(uid)
|
||||
res.status(200).json({ user })
|
||||
res.status(200).json({ user: User.getUserData(user) })
|
||||
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.status(400).json({ e })
|
||||
}
|
||||
})
|
||||
app.post('/set_faculties', async (req, res) => {
|
||||
const { uid, faculties } = req.body
|
||||
try {
|
||||
await User.setFaculties(uid, faculties)
|
||||
const user = await User.byId(uid)
|
||||
res.status(200).json({ user: User.getUserData(user) })
|
||||
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.status(400).json({ e })
|
||||
}
|
||||
})
|
||||
|
||||
+134
-74
@@ -780,7 +780,8 @@
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@@ -798,11 +799,13 @@
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -810,20 +813,22 @@
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.1.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -856,7 +861,6 @@
|
||||
"fs-minipass": {
|
||||
"version": "1.2.5",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.2.1"
|
||||
}
|
||||
@@ -926,7 +930,8 @@
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -936,6 +941,7 @@
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -948,13 +954,15 @@
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
@@ -967,7 +975,6 @@
|
||||
"minizlib": {
|
||||
"version": "1.2.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.2.1"
|
||||
}
|
||||
@@ -975,6 +982,7 @@
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -1047,7 +1055,8 @@
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -1057,6 +1066,7 @@
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -1162,6 +1172,7 @@
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -1179,6 +1190,7 @@
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -1188,20 +1200,6 @@
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.8",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"fs-minipass": "^1.2.5",
|
||||
"minipass": "^2.3.4",
|
||||
"minizlib": "^1.1.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
@@ -1217,7 +1215,8 @@
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
@@ -1232,6 +1231,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
|
||||
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
|
||||
"optional": true
|
||||
},
|
||||
"ci-info": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
|
||||
@@ -2137,6 +2142,15 @@
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz",
|
||||
"integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -2163,7 +2177,8 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@@ -2184,12 +2199,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -2197,24 +2214,25 @@
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -2252,8 +2270,6 @@
|
||||
"fs-minipass": {
|
||||
"version": "1.2.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.2.1"
|
||||
}
|
||||
@@ -2331,7 +2347,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -2343,6 +2360,7 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -2357,6 +2375,7 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -2364,12 +2383,12 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.2.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.1",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -2378,8 +2397,6 @@
|
||||
"minizlib": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.2.1"
|
||||
}
|
||||
@@ -2388,6 +2405,7 @@
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -2468,7 +2486,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -2480,6 +2499,7 @@
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -2564,8 +2584,7 @@
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"bundled": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -2601,6 +2620,7 @@
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -2620,6 +2640,7 @@
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -2630,21 +2651,6 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chownr": "^1.0.1",
|
||||
"fs-minipass": "^1.2.5",
|
||||
"minipass": "^2.2.4",
|
||||
"minizlib": "^1.1.0",
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.1",
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
@@ -2663,12 +2669,12 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"bundled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2816,12 +2822,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"handlebars": {
|
||||
"version": "4.0.12",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz",
|
||||
"integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
|
||||
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"async": "^2.5.0",
|
||||
"neo-async": "^2.6.0",
|
||||
"optimist": "^0.6.1",
|
||||
"source-map": "^0.6.1",
|
||||
"uglify-js": "^3.1.4"
|
||||
@@ -4315,9 +4321,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.12.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz",
|
||||
"integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==",
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
|
||||
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
@@ -4727,9 +4733,36 @@
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
|
||||
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
|
||||
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
|
||||
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"mixin-deep": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
|
||||
@@ -4753,7 +4786,6 @@
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
},
|
||||
@@ -4761,8 +4793,7 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||
"dev": true
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4819,6 +4850,12 @@
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
|
||||
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
|
||||
},
|
||||
"neo-async": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz",
|
||||
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==",
|
||||
"dev": true
|
||||
},
|
||||
"node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
@@ -6266,6 +6303,29 @@
|
||||
"integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=",
|
||||
"dev": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz",
|
||||
"integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"fs-minipass": "^1.2.5",
|
||||
"minipass": "^2.3.5",
|
||||
"minizlib": "^1.2.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
|
||||
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"term-size": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('user_faculties',
|
||||
{
|
||||
userId: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
faculty_code: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
})
|
||||
await queryInterface.createTable('faculty_programmes',
|
||||
{
|
||||
faculty_code: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
programme_code: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING,
|
||||
references: {
|
||||
model: 'element_details',
|
||||
key: 'code'
|
||||
}
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
down: async () => {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
const valtiotieteellisen_ohjelmat = [
|
||||
'KH40_001', // Filosofian kandiohjelma
|
||||
'KH70_001', // Politiikan ja viestinnän kandiohjelma
|
||||
'KH70_002', // Yhteiskunnallisen muutoksen kandiohjelma
|
||||
'KH70_003', // Sosiaalitieteiden kandiohjelma
|
||||
'KH70_004', // Taloustieteen kandiohjelma
|
||||
'MH50_001', // Matematiikan ja tilastotieteen maisteriohjelma
|
||||
'MH50_013', // Kaupunkitutkimuksen ja suunnittelun maisteriohjelma
|
||||
'MH57_005', // Ympäristömuutoksen ja globaalin kestävyyden maisteriohjelma
|
||||
'MH70_001', // Filosofian maisteriohjelma
|
||||
'MH70_002', // Politiikan ja viestinnän maisteriohjelma
|
||||
'MH70_003', // Globaalin politiikan ja kommunikaation maisteriohjelma
|
||||
'MH70_004', // Yhteiskunnallisen muutoksen maisteriohjelma
|
||||
'MH70_005', // Nyky-yhteiskunnan tutkimuksen maisteriohjelma
|
||||
'MH70_006', // Euroopan ja pohjoismaiden tutkimuksen maisteriohjelma (European and Nordic Studies)
|
||||
'MH70_007', // Yhteiskuntatieteiden maisteriohjelma
|
||||
'MH70_008', // Sosiaalitieteiden maisteriohjelma
|
||||
'MH70_009', // Taloustieteen maisteriohjelma
|
||||
'MH70_010' // International Masters in Economy, State & Society
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.bulkInsert(
|
||||
'faculty_programmes',
|
||||
valtiotieteellisen_ohjelmat.map(code => ({
|
||||
faculty_code: 'H70',
|
||||
programme_code: code,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}))
|
||||
)
|
||||
},
|
||||
down: async () => {}
|
||||
}
|
||||
@@ -37,6 +37,49 @@ const AccessGroup = sequelize.define('access_group',
|
||||
}
|
||||
})
|
||||
|
||||
const UserFaculties = sequelize.define('user_faculties',
|
||||
{
|
||||
userId: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
faculty_code: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
})
|
||||
const FacultyProgrammes = sequelize.define('faculty_programmes',
|
||||
{
|
||||
faculty_code: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
programme_code: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING,
|
||||
references: {
|
||||
model: 'element_details',
|
||||
key: 'code'
|
||||
}
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const User = sequelize.define('users',
|
||||
{
|
||||
@@ -109,7 +152,7 @@ const Migration = sequelize.define('migrations', {
|
||||
})
|
||||
|
||||
|
||||
User.belongsToMany(ElementDetails, { through: 'user_elementdetails', as: 'elementdetails' })
|
||||
User.belongsToMany(ElementDetails, { through: 'user_elementdetails', as: 'programme' })
|
||||
ElementDetails.belongsToMany(User, { through: 'user_elementdetails' })
|
||||
|
||||
User.belongsToMany(AccessGroup, { through: 'user_accessgroup', as: 'accessgroup' })
|
||||
@@ -121,11 +164,18 @@ HyGroup.belongsToMany(User, { through: 'user_hy_group' })
|
||||
User.belongsToMany(Affiliation, { through: 'user_affiliation', as: 'affiliation' })
|
||||
Affiliation.belongsToMany(User, { through: 'user_affiliation' })
|
||||
|
||||
User.hasMany(UserFaculties, { as: 'faculty', foreignKey: 'userId' })
|
||||
UserFaculties.hasMany(FacultyProgrammes, { as: 'programme', foreignKey: 'faculty_code', sourceKey: 'faculty_code' })
|
||||
FacultyProgrammes.hasOne(ElementDetails, { foreignKey: 'code', sourceKey: 'programme_code' })
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
ElementDetails,
|
||||
Migration,
|
||||
AccessGroup,
|
||||
HyGroup,
|
||||
Affiliation
|
||||
Affiliation,
|
||||
UserFaculties,
|
||||
FacultyProgrammes,
|
||||
sequelize
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const moment = require('moment')
|
||||
const { User, ElementDetails, AccessGroup, HyGroup, Affiliation } = require('../models')
|
||||
const { flatMap } = require('lodash')
|
||||
const { User, ElementDetails, AccessGroup, HyGroup, Affiliation, FacultyProgrammes, UserFaculties, sequelize } = require('../models')
|
||||
const ElementService = require('./studyelements')
|
||||
const AccessService = require('./accessgroups')
|
||||
const AffiliationService = require('./affiliations')
|
||||
@@ -11,15 +12,14 @@ const Op = Sequelize.Op
|
||||
|
||||
const TOKEN_VERSION = 1 // When token structure changes, increment in userservice, backend and frontend
|
||||
const generateToken = async (uid, mockedBy = null) => {
|
||||
let user = await byUsername(uid)
|
||||
const elementdetails = await getUserElementDetails(user.username)
|
||||
const elements = elementdetails.map(element => element.code)
|
||||
const enabled = requiredGroup === null || user.hy_group.some(e => e.code === requiredGroup)
|
||||
const user = await byUsername(uid)
|
||||
const userData = getUserData(user)
|
||||
const elements = userData.elementdetails.map(element => element.code)
|
||||
const payload = {
|
||||
id: user.id,
|
||||
userId: uid, // username
|
||||
name: user.full_name,
|
||||
enabled,
|
||||
enabled: userData.is_enabled,
|
||||
language: user.language,
|
||||
mockedBy,
|
||||
rights: elements,
|
||||
@@ -114,28 +114,57 @@ const superlogin = async (uid, asUser) => {
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const userIncludes = [
|
||||
{
|
||||
model: ElementDetails,
|
||||
as: 'programme'
|
||||
},
|
||||
{
|
||||
model: AccessGroup,
|
||||
as: 'accessgroup',
|
||||
attributes: ['id', 'group_code', 'group_info']
|
||||
},
|
||||
{
|
||||
model: HyGroup,
|
||||
as: 'hy_group'
|
||||
},
|
||||
{
|
||||
model: UserFaculties,
|
||||
as: 'faculty',
|
||||
include: {
|
||||
model: FacultyProgrammes,
|
||||
as: 'programme',
|
||||
include: {
|
||||
model: ElementDetails,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
model: Affiliation,
|
||||
as: 'affiliation'
|
||||
}
|
||||
]
|
||||
|
||||
const getUserData = user => {
|
||||
if (user == null) return null
|
||||
const newuser = user.get()
|
||||
newuser.elementdetails = getUserElementDetails(newuser)
|
||||
newuser.is_enabled = requiredGroup === null || newuser.hy_group.some(e => e.code === requiredGroup)
|
||||
newuser.hy_group = null
|
||||
return newuser
|
||||
}
|
||||
|
||||
const byUsername = async (username) => {
|
||||
return User.findOne({
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
username: {
|
||||
[Op.eq]: username
|
||||
}
|
||||
},
|
||||
include: [{
|
||||
model: ElementDetails,
|
||||
as: 'elementdetails'
|
||||
}, {
|
||||
model: AccessGroup,
|
||||
as: 'accessgroup',
|
||||
attributes: ['id', 'group_code', 'group_info']
|
||||
}, {
|
||||
model: HyGroup,
|
||||
as: 'hy_group'
|
||||
},{
|
||||
model: Affiliation,
|
||||
as: 'affiliation'
|
||||
}]
|
||||
include: userIncludes
|
||||
})
|
||||
return user
|
||||
}
|
||||
|
||||
const createUser = async (username, fullname, email) => {
|
||||
@@ -152,32 +181,26 @@ const updateUser = async (userObject, values) => {
|
||||
|
||||
|
||||
const byId = async (id) => {
|
||||
return User.findOne({
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
id: {
|
||||
[Op.eq]: id
|
||||
}
|
||||
},
|
||||
include: [{
|
||||
model: ElementDetails,
|
||||
as: 'elementdetails'
|
||||
}, {
|
||||
model: AccessGroup,
|
||||
as: 'accessgroup',
|
||||
attributes: ['id', 'group_code', 'group_info']
|
||||
}]
|
||||
include: userIncludes
|
||||
})
|
||||
return user
|
||||
}
|
||||
|
||||
const getUnitsFromElementDetails = async username => {
|
||||
const user = await byUsername(username)
|
||||
const elementDetails = await user.getElementdetails()
|
||||
return elementDetails.map(element => UnitService.parseUnitFromElement(element))
|
||||
const userData = getUserData(user)
|
||||
return userData.elementdetails.map(element => UnitService.parseUnitFromElement(element))
|
||||
}
|
||||
|
||||
const getUserElementDetails = async username => {
|
||||
const user = await byUsername(username)
|
||||
return await user.getElementdetails()
|
||||
const getUserElementDetails = user => {
|
||||
const elementdetails = [...user.programme, ...flatMap(user.faculty, f => f.programme.map(p => p.element_detail))]
|
||||
return elementdetails
|
||||
}
|
||||
const getUserAccessGroups = async username => {
|
||||
const user = await byUsername(username)
|
||||
@@ -185,32 +208,31 @@ const getUserAccessGroups = async username => {
|
||||
}
|
||||
|
||||
const findAll = async () => {
|
||||
return User.findAll({
|
||||
include: [{
|
||||
model: ElementDetails,
|
||||
as: 'elementdetails'
|
||||
},
|
||||
{
|
||||
model: AccessGroup,
|
||||
as: 'accessgroup'
|
||||
},
|
||||
{
|
||||
model: HyGroup,
|
||||
as: 'hy_group'
|
||||
}]
|
||||
const users = await User.findAll({
|
||||
include: userIncludes,
|
||||
})
|
||||
return users
|
||||
}
|
||||
|
||||
const enableElementDetails = async (uid, codes) => {
|
||||
const user = await byId(uid)
|
||||
const elements = await ElementService.byCodes(codes)
|
||||
await user.addElementdetails(elements)
|
||||
await user.addProgramme(elements)
|
||||
}
|
||||
|
||||
const removeElementDetails = async (uid, codes) => {
|
||||
const user = await byId(uid)
|
||||
const elements = await ElementService.byCodes(codes)
|
||||
await user.removeElementdetails(elements)
|
||||
await user.removeProgramme(elements)
|
||||
}
|
||||
|
||||
const setFaculties = async (uid, faculties) => {
|
||||
await sequelize.transaction(async transaction => {
|
||||
await UserFaculties.destroy({ where: { userId: uid }, transaction })
|
||||
for (const faculty of faculties) {
|
||||
await UserFaculties.create({ userId: uid, faculty_code: faculty }, { transaction })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const modifyRights = async (uid, rights) => {
|
||||
@@ -254,6 +276,7 @@ module.exports = {
|
||||
superlogin,
|
||||
modifyRights,
|
||||
getUserAccessGroups,
|
||||
getRoles
|
||||
|
||||
getRoles,
|
||||
getUserData,
|
||||
setFaculties
|
||||
}
|
||||
@@ -100,7 +100,7 @@ beforeAll(async () => {
|
||||
await admin_user.addHy_group(hygroup)
|
||||
const normal_user = await User.findOne({ where: { id: 665 }})
|
||||
const CS_ED = await ElementDetails.findOne({ where: { code: 'ELEMENT_CS' }})
|
||||
await normal_user.addElementdetail(CS_ED)
|
||||
await normal_user.addProgramme(CS_ED)
|
||||
await normal_user.addHy_group(hygroup)
|
||||
})
|
||||
afterAll(async () => {
|
||||
@@ -168,19 +168,23 @@ describe('user tests', async () => {
|
||||
|
||||
describe('user access right tests', async () => {
|
||||
test('adding and getting access rights works', async () => {
|
||||
const rights1 = await userService.getUserElementDetails('sasumaki')
|
||||
const user1 = await userService.byUsername('sasumaki')
|
||||
const rights1 = userService.getUserElementDetails(user1)
|
||||
expect(rights1.length).toBe(0)
|
||||
|
||||
await userService.enableElementDetails(69, ['Element_MATH'])
|
||||
const rights2 = await userService.getUserElementDetails('sasumaki')
|
||||
const user2 = await userService.byUsername('sasumaki')
|
||||
const rights2 = userService.getUserElementDetails(user2)
|
||||
expect(rights2[0].type).toBe(20)
|
||||
})
|
||||
test('removing and getting access rights works', async () => {
|
||||
const rights1 = await userService.getUserElementDetails('freeman')
|
||||
const user1 = await userService.byUsername('freeman')
|
||||
const rights1 = userService.getUserElementDetails(user1)
|
||||
expect(rights1.length).toBe(1)
|
||||
|
||||
await userService.removeElementDetails(665, ['ELEMENT_CS'])
|
||||
const rights2 = await userService.getUserElementDetails('freeman')
|
||||
const user2 = await userService.byUsername('freeman')
|
||||
const rights2 = userService.getUserElementDetails(user2)
|
||||
console.log(rights2)
|
||||
expect(rights2.length).toBe(0)
|
||||
})
|
||||
|
||||
@@ -23,19 +23,34 @@ const updateTask = async (task, status, type) => {
|
||||
}
|
||||
|
||||
stan.on('connect', async () => {
|
||||
cron.schedule('0 0 1 * *', async () => {
|
||||
// Update ALL students and meta every month
|
||||
scheduleAllStudentsAndMeta()
|
||||
}, { timezone })
|
||||
// cron.schedule('0 0 1 * *', async () => {
|
||||
// // Update ALL students and meta every month
|
||||
// scheduleAllStudentsAndMeta()
|
||||
// }, { timezone })
|
||||
|
||||
cron.schedule('20 4 1 1,3,8,10 *', async () => {
|
||||
// At 04:20 on day-of-month 1 in January, March, August, and October.”
|
||||
updateStudentNumberList()
|
||||
})
|
||||
cron.schedule('0 23 * * *', async () => {
|
||||
// Update ACTIVE students every night
|
||||
scheduleActiveStudents()
|
||||
}, { timezone })
|
||||
|
||||
cron.schedule('0 * * * *'), async () => {
|
||||
const allStudentTasks = await Schedule.find({ type: 'student' })
|
||||
if (allStudentTasks && allStudentTasks.every(task => task.status === 'DONE')) {
|
||||
|
||||
stan.publish('DumpDatabase', null, (err, guid) => {
|
||||
if (err) {
|
||||
console.log('publish failed', 'DumpDatabase')
|
||||
} else {
|
||||
console.log('published', 'DumpDatabase')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// cron.schedule('0 23 * * *', async () => {
|
||||
// // Update ACTIVE students every night
|
||||
// scheduleActiveStudents()
|
||||
// }, { timezone })
|
||||
|
||||
cron.schedule('*/5 * * * *', async () => {
|
||||
const oldestTasks = await getOldestTasks()
|
||||
const status = await getCurrentStatus()
|
||||
@@ -43,6 +58,7 @@ stan.on('connect', async () => {
|
||||
logger.info('updaterStatus', status)
|
||||
|
||||
}, { timezone })
|
||||
|
||||
cron.schedule('0 0-9 * * *', async () => {
|
||||
// Just log some statistics about updater during nights
|
||||
logger.info(`${updatedCount} TASKS DONE IN LAST HOUR\n ${scheduledCount} TASKS SCHEDULED IN LAST HOUR\n ${fetchedCount} TASKS FETCHED FROM API IN LAST HOUR`)
|
||||
@@ -50,31 +66,44 @@ stan.on('connect', async () => {
|
||||
fetchedCount = 0
|
||||
scheduledCount = 0
|
||||
}, { timezone })
|
||||
cron.schedule('0 7 * * *', async () => {
|
||||
stan.publish('RefreshOverview', null, (err, guid) => {
|
||||
if (err) {
|
||||
console.log('publish failed', 'RefreshOverview')
|
||||
} else {
|
||||
console.log('published', 'RefreshOverview')
|
||||
}
|
||||
})
|
||||
stan.publish('RefreshStudyrightAssociations', null, (err, guid) => {
|
||||
if (err) {
|
||||
console.log('publish failed', 'RefreshStudyrightAssociations')
|
||||
} else {
|
||||
console.log('published', 'RefreshStudyrightAssociations')
|
||||
}
|
||||
})
|
||||
stan.publish('updateAttainmentDates', null, (err, guid) => {
|
||||
if (err) {
|
||||
console.log('publish failed', 'UpdateAttainmentDates')
|
||||
} else {
|
||||
console.log('published', 'UpdateAttainmentDates')
|
||||
}
|
||||
})
|
||||
}, { timezone })
|
||||
|
||||
const statusSub = stan.subscribe('status')
|
||||
// cron.schedule('0 7 * * *', async () => {
|
||||
// stan.publish('RefreshOverview', null, (err, guid) => {
|
||||
// if (err) {
|
||||
// console.log('publish failed', 'RefreshOverview')
|
||||
// } else {
|
||||
// console.log('published', 'RefreshOverview')
|
||||
// }
|
||||
// })
|
||||
// stan.publish('RefreshStudyrightAssociations', null, (err, guid) => {
|
||||
// if (err) {
|
||||
// console.log('publish failed', 'RefreshStudyrightAssociations')
|
||||
// } else {
|
||||
// console.log('published', 'RefreshStudyrightAssociations')
|
||||
// }
|
||||
// })
|
||||
// stan.publish('updateAttainmentDates', null, (err, guid) => {
|
||||
// if (err) {
|
||||
// console.log('publish failed', 'UpdateAttainmentDates')
|
||||
// } else {
|
||||
// console.log('published', 'UpdateAttainmentDates')
|
||||
// }
|
||||
// })
|
||||
// }, { timezone })
|
||||
|
||||
const scheduleSub = stan.subscribe('ScheduleAll')
|
||||
|
||||
scheduleSub.on('message', async (_) => {
|
||||
scheduleAllStudentsAndMeta()
|
||||
})
|
||||
const opts = stan.subscriptionOptions();
|
||||
opts.setManualAckMode(true);
|
||||
opts.setAckWait(30 * 60 * 1000); // 1min
|
||||
opts.setDeliverAllAvailable()
|
||||
opts.setDurableName('durable')
|
||||
opts.setMaxInFlight(20)
|
||||
|
||||
const statusSub = stan.subscribe('status', opts)
|
||||
|
||||
statusSub.on('message', async (msg) => {
|
||||
const message = msg.getData().split(':')
|
||||
@@ -111,6 +140,7 @@ stan.on('connect', async () => {
|
||||
const isStudent = !!isValidStudentId(task)
|
||||
logger.info(`Status changed for ${task} to ${status}`, { task: task, status: status, student: isStudent })
|
||||
await updateTask(task, status, isStudent ? 'student' : 'other')
|
||||
msg.ack()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -22,14 +22,21 @@ const publish = async (tasks) => {
|
||||
await sleep(15 * rampup)
|
||||
}
|
||||
}
|
||||
const shuffleArray = (array) => {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array
|
||||
}
|
||||
const scheduleActiveStudents = async () => {
|
||||
const tasks = [...(await Schedule.find({ type: 'student', active: true }))]
|
||||
const tasks = [...shuffleArray(await Schedule.find({ type: 'student', active: true }))]
|
||||
console.log(tasks.length, 'tasks to schedule')
|
||||
publish(tasks)
|
||||
}
|
||||
const scheduleAllStudentsAndMeta = async () => {
|
||||
|
||||
const tasks = ['meta', ...(await Schedule.find({ type: 'student' }))]
|
||||
const tasks = [{ task:'meta', type: 'other', active: 'false' }, ...shuffleArray(await Schedule.find({ type: 'student' }))]
|
||||
console.log(tasks.length, 'tasks to schedule')
|
||||
publish(tasks)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user