mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56528b6fec | ||
|
|
1fb642fe7e | ||
|
|
cd855dfc32 | ||
|
|
f5200906d3 | ||
|
|
57fd9a5434 | ||
|
|
1311999637 | ||
|
|
c3411112b2 | ||
|
|
52e9d5dc51 | ||
|
|
5347670a0a | ||
|
|
133c65d147 | ||
|
|
5cb8601cc9 | ||
|
|
74b16f7e50 | ||
|
|
ec2b1fe6a8 | ||
|
|
a8ec3c2257 | ||
|
|
d9b4aa58f1 | ||
|
|
f29c33cd60 | ||
|
|
fccd0d7769 | ||
|
|
5e59956485 | ||
|
|
5c3fe43473 | ||
|
|
3fbcbbf0d2 | ||
|
|
3d8d6e7a00 | ||
|
|
b040bac675 | ||
|
|
c1689253ea | ||
|
|
110abe4baf | ||
|
|
9ac9ed298c | ||
|
|
261b3f1384 | ||
|
|
515e3ea4c1 | ||
|
|
3e81490f37 | ||
|
|
5b53233dc3 | ||
|
|
2b04de96d2 | ||
|
|
abd13df96d | ||
|
|
4e28efd608 | ||
|
|
63e72895f6 | ||
|
|
74baf4ea47 | ||
|
|
1563566e62 | ||
|
|
921b6a0e07 | ||
|
|
3717bed980 | ||
|
|
b646d540fe | ||
|
|
e5e246d826 | ||
|
|
286b26d484 | ||
|
|
f84b05d804 | ||
|
|
b30124dc2e | ||
|
|
ac1f7039a4 | ||
|
|
18943759a1 | ||
|
|
8c388119f3 | ||
|
|
c486bbbbcf | ||
|
|
76b28928a7 | ||
|
|
4389af86f5 | ||
|
|
e776c29aa4 | ||
|
|
511f262d8b | ||
|
|
9c48fe888b | ||
|
|
bd54d4f4e1 | ||
|
|
2daa1c674d | ||
|
|
d544220436 | ||
|
|
fcd0ad23cc | ||
|
|
1e94accc55 | ||
|
|
326da589de | ||
|
|
804a7f3f63 | ||
|
|
178d0d921c | ||
|
|
1cefe9e410 | ||
|
|
b1cfc82aa2 | ||
|
|
a8b76c7253 | ||
|
|
c6c2d6f314 | ||
|
|
15142e3750 | ||
|
|
45b1d4cb7a | ||
|
|
3a7575b052 | ||
|
|
f180480b5c | ||
|
|
997a6e36ef | ||
|
|
0cbbaea27b | ||
|
|
748e8b31ae | ||
|
|
74b80e75df | ||
|
|
8039bd369b | ||
|
|
2312a8d662 | ||
|
|
c25431e918 | ||
|
|
53865cafd3 | ||
|
|
b0a8ce30a2 | ||
|
|
2c1eb0c90e | ||
|
|
48878b961c | ||
|
|
149cbbcf19 | ||
|
|
749a52a375 | ||
|
|
999ffaaa9f | ||
|
|
65cb7a99df | ||
|
|
14486e1116 | ||
|
|
9922708975 | ||
|
|
bf7588ba28 | ||
|
|
5c92532aef | ||
|
|
6acba8f523 | ||
|
|
77c1a733c2 | ||
|
|
d3f6c72906 | ||
|
|
3d8b6d214a | ||
|
|
16b1ff5c59 | ||
|
|
43b08bc8e9 | ||
|
|
bc42dbee1a |
@@ -48,12 +48,16 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: "Get current time"
|
||||
id: get_current_time
|
||||
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
|
||||
- name: "Build and push"
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=staging
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-frontend:trunk
|
||||
type=registry,ref=toska/oodikone2-frontend:staging
|
||||
@@ -79,6 +83,7 @@ jobs:
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/oodikone/static/js'
|
||||
version: ${{ github.sha }}
|
||||
|
||||
build_and_release_userservice:
|
||||
name: "Build and release userservice"
|
||||
|
||||
@@ -49,6 +49,9 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: "Get current time"
|
||||
id: get_current_time
|
||||
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
|
||||
- name: "Extract release version"
|
||||
# Github context (e.g. github.ref) only has ref formatted as /refs/tags/vX.X.X
|
||||
# This steps extracts release version from GITHUB_REF env var
|
||||
@@ -61,6 +64,7 @@ jobs:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-frontend:trunk
|
||||
type=registry,ref=toska/oodikone2-frontend:staging
|
||||
@@ -86,6 +90,7 @@ jobs:
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/oodikone/static/js'
|
||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
build_and_release_userservice:
|
||||
name: "Build and release userservice"
|
||||
|
||||
@@ -3,7 +3,5 @@
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"defaultCommandTimeout": 30000,
|
||||
"requestTimeout": 30000,
|
||||
"viewportHeight": 1080,
|
||||
"viewportWidth": 1920,
|
||||
"videoCompression": false
|
||||
}
|
||||
|
||||
@@ -119,6 +119,42 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('AYTKT10004 Avoin yo: Tietokantojen perusteet')
|
||||
})
|
||||
|
||||
it('Searching course by name displays right courses, 10 credit courses', () => {
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('Search for courses')
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
|
||||
|
||||
cy.contains('Tietorakenteet ja algoritmit')
|
||||
cy.contains('Avoin yo: Tietorakenteet ja algoritmit')
|
||||
cy.contains('AYTKT20001')
|
||||
cy.contains('TKT20001, 58131')
|
||||
cy.contains('td', /^TKT20001/).click()
|
||||
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
|
||||
cy.contains('TKT20001, 58131 Tietorakenteet ja algoritmit')
|
||||
cy.get('.right').click()
|
||||
cy.contains('No results')
|
||||
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
|
||||
cy.contains('td', /^AYTKT20001/).click()
|
||||
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
cy.contains('AYTKT20001 Avoin yo: Tietorakenteet ja algoritmit')
|
||||
|
||||
cy.get('.right').click()
|
||||
cy.contains('No results')
|
||||
cy.get("input[name='unifyOpenUniCourses']").parent().click()
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
|
||||
cy.contains('td', 'TKT20001, 58131, AYTKT20001').click()
|
||||
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
cy.contains('TKT20001, 58131, AYTKT20001 Tietorakenteet ja algoritmit')
|
||||
})
|
||||
|
||||
it('Can find course population', () => {
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('Search for courses')
|
||||
@@ -138,6 +174,18 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('010431753')
|
||||
})
|
||||
|
||||
it('Population of course shows grades for each student', () => {
|
||||
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
|
||||
cy.contains(/^TKT20001$/).click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('TKT20001, 58131 Tietorakenteet ja algoritmit')
|
||||
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
|
||||
cy.contains('Population of course Tietorakenteet ja algoritmit 2018-2019')
|
||||
cy.contains('Students (197)').click()
|
||||
cy.contains('td', '010262566').siblings().contains('4')
|
||||
cy.contains('td', '010674989').siblings().contains('1')
|
||||
})
|
||||
|
||||
describe('When searching unified course stats', () => {
|
||||
beforeEach(() => {
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
const baseUrl = Cypress.config().baseUrl
|
||||
// const baseUrl = Cypress.config().baseUrl
|
||||
|
||||
const checkFilteringResult = studentCount => {
|
||||
cy.contains(`Students (${studentCount})`)
|
||||
@@ -130,12 +130,9 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: this fails in CI, but not locally. Investigate and fix
|
||||
// plz fix
|
||||
it.skip('Courses filter works', () => {
|
||||
// courses takes some time to load, wait for it to complete
|
||||
cy.intercept({ path: '**/courses' }).as('coursesOfPopulation')
|
||||
cy.wait('@coursesOfPopulation')
|
||||
// moar waiting hack
|
||||
cy.wait(10000)
|
||||
runTestStepWithPreAndPostParts('courseFilter-header', () => {
|
||||
cy.cs('courseFilter-course-dropdown').click()
|
||||
|
||||
@@ -73,9 +73,9 @@ describe('Population Statistics tests', () => {
|
||||
|
||||
cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(3000) // a bit hacky way, wait until ui is ready
|
||||
cy.cs('expand-TKT1').click()
|
||||
cy.cs('coursestats-link-TKT10002').click()
|
||||
cy.wait(10000) // a bit hacky way, wait until ui is ready
|
||||
cy.get('[data-cy=expand-TKT1]').click()
|
||||
cy.get('[data-cy=coursestats-link-TKT10002]').click()
|
||||
cy.wait('@coursePage')
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('TKT10002, 581325 Ohjelmoinnin perusteet')
|
||||
|
||||
@@ -185,7 +185,6 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
cy.get('.ui > label').click()
|
||||
cy.contains('Create new tag').click()
|
||||
cy.get('.purple')
|
||||
cy.contains(name)
|
||||
deleteTag(name)
|
||||
})
|
||||
|
||||
@@ -50,6 +50,8 @@ Cypress.Commands.add('init', (path = '', userId = 'basic') => {
|
||||
if (!headersToUse) throw Error(`${userId} is not valid user id!`)
|
||||
req.headers = headersToUse
|
||||
})
|
||||
// Hide SIS warning flag to avoid screen size problems when running headlessly
|
||||
window.localStorage.setItem('SIS_WARNING_FLAG', 'true')
|
||||
const baseUrl = Cypress.config().baseUrl
|
||||
cy.visit(baseUrl.concat(path))
|
||||
})
|
||||
|
||||
@@ -29,8 +29,6 @@ services:
|
||||
frontend:
|
||||
<<: *defaults
|
||||
container_name: frontend
|
||||
environment:
|
||||
BACKEND_ADDR: backend
|
||||
image: toska/oodikone2-frontend:trunk
|
||||
|
||||
kone-db:
|
||||
|
||||
@@ -53,13 +53,6 @@ services:
|
||||
<<: *build_defaults
|
||||
context: services/frontend
|
||||
container_name: frontend
|
||||
environment:
|
||||
ADMINER_URL: http://localhost:5050/?pgsql=oodi-db&username=postgres
|
||||
BACKEND_ADDR: backend
|
||||
KONE_ADMINER_URL: http://localhost:5050/?pgsql=kone-db&username=postgres
|
||||
SIS_ADMINER_URL: http://localhost:5050/?pgsql=sis-db&username=postgres
|
||||
SIS_IMPORTER_ADMINER_URL: http://localhost:5050/?pgsql=sis-importer-db&username=postgres
|
||||
USER_ADMINER_URL: http://localhost:5050/?pgsql=user-db&username=postgres
|
||||
ports:
|
||||
- 3000:3000
|
||||
profiles:
|
||||
|
||||
-3749
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "updater_api",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "npm run lint",
|
||||
"lint": "eslint .",
|
||||
"lintfix": "prettier --write **/*.{js,json} && eslint --fix .",
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"dotenv": "^7.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^5.1.1",
|
||||
"moment": "^2.29.1",
|
||||
"node-nats-streaming": "^0.2.6",
|
||||
"nodemon": "^1.19.4",
|
||||
"winston": "^3.3.3",
|
||||
"winston-log2gelf": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"prettier": "^1.19.1",
|
||||
"prettier-eslint": "^9.0.2"
|
||||
}
|
||||
}
|
||||
-4305
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "updater_scheduler",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "npm run lint",
|
||||
"lint": "eslint .",
|
||||
"lintfix": "prettier --write **/*.{js,json} && eslint --fix .",
|
||||
"start": "node --max_old_space_size=4096 index.js",
|
||||
"dev": "nodemon --max_old_space_size=4096 index.js",
|
||||
"scheduleActiveStudents": "NATS_CLIENTID=manualActiveSchedule node --max_old_space_size=4096 -e 'require(\"./src/schedule_students.js\").scheduleActiveStudents()'",
|
||||
"scheduleMeta": "NATS_CLIENTID=manualAllSchedule node --max_old_space_size=4096 -e 'require(\"./src/schedule_students.js\").scheduleMeta()'",
|
||||
"getOldestTasks": "node --max_old_space_size=4096 -e 'require(\"./src/SchedulingStatistics.js\").getOldestTasks()'"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cron": "^1.8.2",
|
||||
"express": "^4.17.1",
|
||||
"mongoose": "^5.12.3",
|
||||
"node-nats-streaming": "^0.2.6",
|
||||
"nodemon": "^1.19.4",
|
||||
"winston": "^3.3.3",
|
||||
"winston-log2gelf": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"prettier": "^1.19.1",
|
||||
"prettier-eslint": "^9.0.2"
|
||||
}
|
||||
}
|
||||
-15012
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "updater_writer",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "npm run lint && npm run jest",
|
||||
"jest": "NODE_ENV=test jest --runInBand --verbose",
|
||||
"test_docker": "docker exec -t updater_writer /bin/bash -c 'npm test'",
|
||||
"lint": "eslint .",
|
||||
"lintfix": "prettier --write **/*.{js,json} && eslint --fix .",
|
||||
"start": "node 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",
|
||||
"dependencies": {
|
||||
"dotenv": "^8.0.0",
|
||||
"jest": "^24.8.0",
|
||||
"lodash": "^4.17.19",
|
||||
"node-nats-streaming": "^0.2.6",
|
||||
"pg": "^8.6.0",
|
||||
"sequelize": "^6.6.2",
|
||||
"umzug": "^2.2.0",
|
||||
"winston": "^3.2.1",
|
||||
"winston-log2gelf": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.0.3",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-prettier": "^6.3.0",
|
||||
"eslint-plugin-prettier": "^3.1.0",
|
||||
"nodemon": "^1.19.4",
|
||||
"prettier": "^1.18.2",
|
||||
"prettier-eslint": "^9.0.0"
|
||||
}
|
||||
}
|
||||
Generated
+70
-96
@@ -7,23 +7,23 @@
|
||||
"": {
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.14.7",
|
||||
"@babel/eslint-parser": "^7.15.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.14.5",
|
||||
"concurrently": "^6.2.0",
|
||||
"cypress": "^8.0.0",
|
||||
"eslint": "^7.31.0",
|
||||
"concurrently": "^6.2.1",
|
||||
"cypress": "^8.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-cypress": "^2.11.3",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-import": "^2.24.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prefer-object-spread": "^1.2.1",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"husky": "^7.0.1",
|
||||
"lint-staged": "^11.0.1",
|
||||
"lint-staged": "^11.1.2",
|
||||
"prettier": "^2.3.2",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
@@ -101,9 +101,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/eslint-parser": {
|
||||
"version": "7.14.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.14.7.tgz",
|
||||
"integrity": "sha512-6WPwZqO5priAGIwV6msJcdc9TsEPzYeYdS/Xuoap+/ihkgN6dzHp2bcAAwyWZ5bLzk0vvjDmKvRwkqNaiJ8BiQ==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.15.0.tgz",
|
||||
"integrity": "sha512-+gSPtjSBxOZz4Uh8Ggqu7HbfpB8cT1LwW0DnVVLZEJvzXauiD0Di3zszcBkRmfGGrLdYeHUwcflG7i3tr9kQlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eslint-scope": "^5.1.1",
|
||||
@@ -1446,9 +1446,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.2.0.tgz",
|
||||
"integrity": "sha512-v9I4Y3wFoXCSY2L73yYgwA9ESrQMpRn80jMcqMgHx720Hecz2GZAvTI6bREVST6lkddNypDKRN22qhK0X8Y00g==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.2.1.tgz",
|
||||
"integrity": "sha512-emgwhH+ezkuYKSHZQ+AkgEpoUZZlbpPVYCVv7YZx0r+T7fny1H03r2nYRebpi2DudHR4n1Rgbo2YTxKOxVJ4+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
@@ -1580,9 +1580,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-8.0.0.tgz",
|
||||
"integrity": "sha512-Hhbc7FtbeCSg5Ui2zxXQLynk7IYGIygG8NqTauS4EtCWyp2k6s4g8P4KUZXwRbhuryN/+/dCd1kPtFbhBx8MuQ==",
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-8.2.0.tgz",
|
||||
"integrity": "sha512-jg7S5VxxslwsgEyAkCE9ZCkFADxOUY1bSWScp1cWnga88K0TZgFQ0zdxyG9Mw/4spLGuvkriIZ62am+TR6C04w==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
@@ -1719,12 +1719,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
||||
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
|
||||
@@ -1965,9 +1959,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "7.31.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.31.0.tgz",
|
||||
"integrity": "sha512-vafgJpSh2ia8tnTkNUkwxGmnumgckLh5aAbLa1xRmIn9+owi8qBNGKL+B881kNKNTy7FFqTEkpNkUvmw0n6PkA==",
|
||||
"version": "7.32.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
|
||||
"integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.12.11",
|
||||
@@ -2073,34 +2067,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz",
|
||||
"integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==",
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.5.tgz",
|
||||
"integrity": "sha512-XMoPKjSpXbkeJ7ZZ9icLnJMTY5Mc1kZbCakHquaFsXPpyWOwK0TK6CODO+0ca54UoM9LKOxyUNnoVZRl8TeaAg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^2.6.9",
|
||||
"resolve": "^1.13.1"
|
||||
"debug": "^3.2.7",
|
||||
"resolve": "^1.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/eslint-module-utils": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.1.tgz",
|
||||
"integrity": "sha512-ZXI9B8cxAJIH4nfkhTwcRTEAnrVfobYqwjWy/QMCZ8rHkZHFjf9yO4BzpiF9kCSfNlMG54eKigISHpX0+AaT4A==",
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz",
|
||||
"integrity": "sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^3.2.7",
|
||||
@@ -2156,17 +2144,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import": {
|
||||
"version": "2.23.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.23.4.tgz",
|
||||
"integrity": "sha512-6/wP8zZRsnQFiR3iaPFgh5ImVRM1WN5NUWfTIRqwOdeiGJlBcSk82o1FEVq8yXmy4lkIzTo7YhHCIxlU/2HyEQ==",
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.0.tgz",
|
||||
"integrity": "sha512-Kc6xqT9hiYi2cgybOc0I2vC9OgAYga5o/rAFinam/yF/t5uBqxQbauNPMC6fgb640T/89P0gFoO27FOilJ/Cqg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.3",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"debug": "^2.6.9",
|
||||
"doctrine": "^2.1.0",
|
||||
"eslint-import-resolver-node": "^0.3.4",
|
||||
"eslint-module-utils": "^2.6.1",
|
||||
"eslint-import-resolver-node": "^0.3.5",
|
||||
"eslint-module-utils": "^2.6.2",
|
||||
"find-up": "^2.0.0",
|
||||
"has": "^1.0.3",
|
||||
"is-core-module": "^2.4.0",
|
||||
@@ -3788,9 +3776,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lint-staged": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.0.1.tgz",
|
||||
"integrity": "sha512-RkTA1ulE6jAGFskxpGAwxfVRXjHp7D9gFg/+KMARUWMPiVFP0t28Em2u0gL8sA0w3/ck3TC57F2v2RNeQ5XPnw==",
|
||||
"version": "11.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.1.2.tgz",
|
||||
"integrity": "sha512-6lYpNoA9wGqkL6Hew/4n1H6lRqF3qCsujVT0Oq5Z4hiSAM7S6NksPJ3gnr7A7R52xCtiZMcEUNNQ6d6X5Bvh9w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.1",
|
||||
@@ -3798,7 +3786,6 @@
|
||||
"commander": "^7.2.0",
|
||||
"cosmiconfig": "^7.0.0",
|
||||
"debug": "^4.3.1",
|
||||
"dedent": "^0.7.0",
|
||||
"enquirer": "^2.3.6",
|
||||
"execa": "^5.0.0",
|
||||
"listr2": "^3.8.2",
|
||||
@@ -6612,9 +6599,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/eslint-parser": {
|
||||
"version": "7.14.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.14.7.tgz",
|
||||
"integrity": "sha512-6WPwZqO5priAGIwV6msJcdc9TsEPzYeYdS/Xuoap+/ihkgN6dzHp2bcAAwyWZ5bLzk0vvjDmKvRwkqNaiJ8BiQ==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.15.0.tgz",
|
||||
"integrity": "sha512-+gSPtjSBxOZz4Uh8Ggqu7HbfpB8cT1LwW0DnVVLZEJvzXauiD0Di3zszcBkRmfGGrLdYeHUwcflG7i3tr9kQlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-scope": "^5.1.1",
|
||||
@@ -7650,9 +7637,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"concurrently": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.2.0.tgz",
|
||||
"integrity": "sha512-v9I4Y3wFoXCSY2L73yYgwA9ESrQMpRn80jMcqMgHx720Hecz2GZAvTI6bREVST6lkddNypDKRN22qhK0X8Y00g==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.2.1.tgz",
|
||||
"integrity": "sha512-emgwhH+ezkuYKSHZQ+AkgEpoUZZlbpPVYCVv7YZx0r+T7fny1H03r2nYRebpi2DudHR4n1Rgbo2YTxKOxVJ4+g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
@@ -7749,9 +7736,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"cypress": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-8.0.0.tgz",
|
||||
"integrity": "sha512-Hhbc7FtbeCSg5Ui2zxXQLynk7IYGIygG8NqTauS4EtCWyp2k6s4g8P4KUZXwRbhuryN/+/dCd1kPtFbhBx8MuQ==",
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-8.2.0.tgz",
|
||||
"integrity": "sha512-jg7S5VxxslwsgEyAkCE9ZCkFADxOUY1bSWScp1cWnga88K0TZgFQ0zdxyG9Mw/4spLGuvkriIZ62am+TR6C04w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^2.88.5",
|
||||
@@ -7862,12 +7849,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dedent": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
||||
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
|
||||
"dev": true
|
||||
},
|
||||
"deep-is": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
|
||||
@@ -8067,9 +8048,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"eslint": {
|
||||
"version": "7.31.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.31.0.tgz",
|
||||
"integrity": "sha512-vafgJpSh2ia8tnTkNUkwxGmnumgckLh5aAbLa1xRmIn9+owi8qBNGKL+B881kNKNTy7FFqTEkpNkUvmw0n6PkA==",
|
||||
"version": "7.32.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
|
||||
"integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.12.11",
|
||||
@@ -8161,36 +8142,30 @@
|
||||
"requires": {}
|
||||
},
|
||||
"eslint-import-resolver-node": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz",
|
||||
"integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==",
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.5.tgz",
|
||||
"integrity": "sha512-XMoPKjSpXbkeJ7ZZ9icLnJMTY5Mc1kZbCakHquaFsXPpyWOwK0TK6CODO+0ca54UoM9LKOxyUNnoVZRl8TeaAg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^2.6.9",
|
||||
"resolve": "^1.13.1"
|
||||
"debug": "^3.2.7",
|
||||
"resolve": "^1.20.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-module-utils": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.1.tgz",
|
||||
"integrity": "sha512-ZXI9B8cxAJIH4nfkhTwcRTEAnrVfobYqwjWy/QMCZ8rHkZHFjf9yO4BzpiF9kCSfNlMG54eKigISHpX0+AaT4A==",
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz",
|
||||
"integrity": "sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^3.2.7",
|
||||
@@ -8235,17 +8210,17 @@
|
||||
}
|
||||
},
|
||||
"eslint-plugin-import": {
|
||||
"version": "2.23.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.23.4.tgz",
|
||||
"integrity": "sha512-6/wP8zZRsnQFiR3iaPFgh5ImVRM1WN5NUWfTIRqwOdeiGJlBcSk82o1FEVq8yXmy4lkIzTo7YhHCIxlU/2HyEQ==",
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.0.tgz",
|
||||
"integrity": "sha512-Kc6xqT9hiYi2cgybOc0I2vC9OgAYga5o/rAFinam/yF/t5uBqxQbauNPMC6fgb640T/89P0gFoO27FOilJ/Cqg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-includes": "^3.1.3",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"debug": "^2.6.9",
|
||||
"doctrine": "^2.1.0",
|
||||
"eslint-import-resolver-node": "^0.3.4",
|
||||
"eslint-module-utils": "^2.6.1",
|
||||
"eslint-import-resolver-node": "^0.3.5",
|
||||
"eslint-module-utils": "^2.6.2",
|
||||
"find-up": "^2.0.0",
|
||||
"has": "^1.0.3",
|
||||
"is-core-module": "^2.4.0",
|
||||
@@ -9425,9 +9400,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"lint-staged": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.0.1.tgz",
|
||||
"integrity": "sha512-RkTA1ulE6jAGFskxpGAwxfVRXjHp7D9gFg/+KMARUWMPiVFP0t28Em2u0gL8sA0w3/ck3TC57F2v2RNeQ5XPnw==",
|
||||
"version": "11.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.1.2.tgz",
|
||||
"integrity": "sha512-6lYpNoA9wGqkL6Hew/4n1H6lRqF3qCsujVT0Oq5Z4hiSAM7S6NksPJ3gnr7A7R52xCtiZMcEUNNQ6d6X5Bvh9w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.1",
|
||||
@@ -9435,7 +9410,6 @@
|
||||
"commander": "^7.2.0",
|
||||
"cosmiconfig": "^7.0.0",
|
||||
"debug": "^4.3.1",
|
||||
"dedent": "^0.7.0",
|
||||
"enquirer": "^2.3.6",
|
||||
"execa": "^5.0.0",
|
||||
"listr2": "^3.8.2",
|
||||
|
||||
+15
-11
@@ -19,7 +19,11 @@
|
||||
"diff:courses": "docker exec backend node /usr/src/app/src/sisoodi_diff/courses/index.js",
|
||||
"diff:transfers": "docker exec backend node /usr/src/app/src/sisoodi_diff/transfers.js",
|
||||
"diff:trends": "docker exec backend node /usr/src/app/src/sisoodi_diff/trends.js",
|
||||
"prepare": "husky install"
|
||||
"prepare": "husky install",
|
||||
"lint:docker-compose": "docker-compose config -q && :",
|
||||
"lint:docker-compose:real": "docker-compose -f docker-compose.yml -f docker-compose.real.yml config -q && :",
|
||||
"lint:docker-compose:ci": "docker-compose -f docker-compose.ci.yml config -q && :",
|
||||
"lint:docker-compose:test": "docker-compose -f docker-compose.test.yml config -q && :"
|
||||
},
|
||||
"lint-staged": {
|
||||
"{services,updater}/**/*.{js,jsx}": "eslint --fix",
|
||||
@@ -28,10 +32,10 @@
|
||||
".github/workflows/*": "actionlint",
|
||||
"Dockerfile": "hadolint",
|
||||
"*.sh": "shellcheck -x",
|
||||
"docker-compose.yml": "docker-compose config -q",
|
||||
"docker-compose.real.yml": "docker-compose -f docker-compose.yml -f docker-compose.real.yml config -q",
|
||||
"docker-compose.ci.yml": "docker-compose -f docker-compose.ci.yml config -q",
|
||||
"docker-compose.test.yml": "docker-compose -f docker-compose.test.yml config -q"
|
||||
"docker-compose.yml": "npm run lint:docker-compose",
|
||||
"docker-compose.real.yml": "npm run lint:docker-compose:real",
|
||||
"docker-compose.ci.yml": "npm run lint:docker-compose:ci",
|
||||
"docker-compose.test.yml": "npm run lint:docker-compose:test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -43,23 +47,23 @@
|
||||
},
|
||||
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.14.7",
|
||||
"@babel/eslint-parser": "^7.15.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.14.5",
|
||||
"concurrently": "^6.2.0",
|
||||
"cypress": "^8.0.0",
|
||||
"eslint": "^7.31.0",
|
||||
"concurrently": "^6.2.1",
|
||||
"cypress": "^8.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-cypress": "^2.11.3",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-import": "^2.24.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prefer-object-spread": "^1.2.1",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"husky": "^7.0.1",
|
||||
"lint-staged": "^11.0.1",
|
||||
"lint-staged": "^11.1.2",
|
||||
"prettier": "^2.3.2",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
|
||||
@@ -141,9 +141,9 @@ set_up_oodikone_from_scratch() {
|
||||
|
||||
infomsg "Cleaning up docker setup and node_modules from possible previous installations"
|
||||
local compose_down_args="--remove-orphans --volumes --rmi all"
|
||||
"$PROJECT_ROOT"/run.sh both down "$compose_down_args"
|
||||
docker-compose --file "$PROJECT_ROOT"/docker-compose.test.yml "$compose_down_args"
|
||||
docker-compose --file "$PROJECT_ROOT"/docker-compose.ci.yml "$compose_down_args"
|
||||
"$PROJECT_ROOT"/run.sh both down "$compose_down_args" || infomsg "Cleaning errored, but will continue"
|
||||
docker-compose --file "$PROJECT_ROOT"/docker-compose.test.yml "$compose_down_args" || infomsg "Cleaning errored, but will continue"
|
||||
docker-compose --file "$PROJECT_ROOT"/docker-compose.ci.yml "$compose_down_args" || infomsg "Cleaning errored, but will continue"
|
||||
for folder in "${folders_to_set_up[@]}"; do
|
||||
cd "$folder" || die "Couldn't change directory to folder $folder"
|
||||
rm -rf node_modules|| die "Couldn't remove node_modules in folder $folder"
|
||||
|
||||
@@ -12,12 +12,12 @@ CMD ["npm", "run", "dev"]
|
||||
|
||||
FROM base as production
|
||||
ENV NODE_ENV=production
|
||||
COPY --chown=node:node ./package* /usr/src/app/
|
||||
RUN npm ci --production
|
||||
COPY --chown=node:node . /usr/src/app/
|
||||
USER node
|
||||
ARG SENTRY_ENVIRONMENT
|
||||
ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
COPY --chown=node:node ./package* /usr/src/app/
|
||||
RUN npm ci --production
|
||||
COPY --chown=node:node . /usr/src/app/
|
||||
USER node
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
const { createCourseGroup } = require('../src/services/courseGroups')
|
||||
|
||||
const run = async () => {
|
||||
await Promise.all(groups.map(name => createCourseGroup(name)))
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const groups = [
|
||||
'Yleinen ja aikuiskasvatustiede',
|
||||
'Kasvatustiede, yleinen didaktiikka',
|
||||
'Kasvatuspsykologia',
|
||||
'Varhaiskasvatus (VO+VAKAMO)',
|
||||
'Erityispedagogiikka',
|
||||
'Kotitaloustiede',
|
||||
'Käsityötiede',
|
||||
'Aikuisopetus',
|
||||
'Biologia ja maantiede',
|
||||
'Historia ja yhteiskuntaoppi',
|
||||
'Uskonto',
|
||||
'Elämänkatsomustieto',
|
||||
'Suomen kieli ja kirjallisuus',
|
||||
'Vieraat kielet',
|
||||
'Matematiikka',
|
||||
'Fysiikka ja kemia',
|
||||
'Musiikki',
|
||||
'Kuvataide',
|
||||
'Liikunta',
|
||||
'Draama',
|
||||
'Mediakasvatus',
|
||||
'Tiedekeskuspedagogiikka',
|
||||
'Svenska studier',
|
||||
]
|
||||
|
||||
run()
|
||||
@@ -1,14 +1,6 @@
|
||||
const { NODE_ENV } = process.env
|
||||
const isTest = process.env.NODE_ENV === 'test'
|
||||
let DB_URL = process.env.DB_URL
|
||||
let DB_URL_KONE = process.env.DB_URL_KONE
|
||||
if (isTest) {
|
||||
DB_URL = process.env.TEST_DB
|
||||
DB_URL_KONE = process.env.TEST_DB_KONE
|
||||
} else if (NODE_ENV === 'anon') {
|
||||
DB_URL = process.env.ANON_DB
|
||||
DB_URL_KONE = process.env.ANON_DB_KONE
|
||||
}
|
||||
|
||||
let DB_MAX_CONNECTIONS = parseInt(process.env.DB_MAX_CONNECTIONS, 10)
|
||||
if (isNaN(DB_MAX_CONNECTIONS)) {
|
||||
@@ -49,17 +41,6 @@ const formatURL = url => {
|
||||
|
||||
const addSlashToEnd = url => (url.endsWith('/') ? url : url + '/')
|
||||
|
||||
if (process.env.NODE_ENV === 'development' && process.env.FEATURES) {
|
||||
const toggled = process.env.FEATURES.split(',')
|
||||
toggled
|
||||
.map(toggle => toggle.trim())
|
||||
.forEach(feature => {
|
||||
if (FEATURES[feature] !== undefined) {
|
||||
FEATURES[feature] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const OODI = {
|
||||
test: 'http://localhost',
|
||||
anon: process.env.OODI_ADDR_ANON,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('teacher_course_groups')
|
||||
await queryInterface.dropTable('course_groups')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.sequelize.query('DROP SEQUENCE "course_groups_id_seq"')
|
||||
await queryInterface.sequelize.query('DROP SEQUENCE "teacher_course_group_id_seq"')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -7,58 +7,35 @@ const {
|
||||
refreshUber,
|
||||
refreshProtoCProgramme,
|
||||
getStartYears,
|
||||
} = require('./services/coolDataScience')
|
||||
const { refreshAssociationsInRedis: refreshAssociationsInRedisV2 } = require('./servicesV2/studyrights')
|
||||
} = require('./servicesV2/trends')
|
||||
const { refreshAssociationsInRedis } = require('./servicesV2/studyrights')
|
||||
const { getAllProgrammes, nonGraduatedStudentsOfElementDetail } = require('./servicesV2/studyrights')
|
||||
const { productivityStatsForStudytrack, throughputStatsForStudytrack } = require('./servicesV2/studyprogramme')
|
||||
const { findAndSaveTeachers } = require('./servicesV2/topteachers')
|
||||
const {
|
||||
getAllProgrammes: getAllProgrammesV2,
|
||||
nonGraduatedStudentsOfElementDetail: nonGraduatedStudentsOfElementDetailV2,
|
||||
} = require('./servicesV2/studyrights')
|
||||
|
||||
const {
|
||||
productivityStatsForStudytrack: productivityStatsForStudytrackV2,
|
||||
throughputStatsForStudytrack: throughputStatsForStudytrackV2,
|
||||
} = require('./servicesV2/studyprogramme')
|
||||
|
||||
const topteachersV2 = require('./servicesV2/topteachers')
|
||||
const { isNewHYStudyProgramme } = require('./util')
|
||||
|
||||
const { calculateFacultyYearlyStats } = require('./services/faculties')
|
||||
const { patchFacultyYearlyStats } = require('./servicesV2/analyticsService')
|
||||
|
||||
const {
|
||||
setProductivity: setProductivityV2,
|
||||
setThroughput: setThroughputV2,
|
||||
patchProductivity: patchProductivityV2,
|
||||
patchThroughput: patchThroughputV2,
|
||||
patchNonGraduatedStudents: patchNonGraduatedStudentsV2,
|
||||
setProductivity,
|
||||
setThroughput,
|
||||
patchProductivity,
|
||||
patchThroughput,
|
||||
patchNonGraduatedStudents,
|
||||
} = require('./servicesV2/analyticsService')
|
||||
const { isNewHYStudyProgramme } = require('./util')
|
||||
|
||||
const schedule = (cronTime, func) => new CronJob({ cronTime, onTick: func, start: true, timeZone: 'Europe/Helsinki' })
|
||||
|
||||
// This is currently done with old data, update to use sis db
|
||||
const refreshFacultyYearlyStats = async () => {
|
||||
try {
|
||||
console.log('Refreshing faculty yearly stats...')
|
||||
const data = await calculateFacultyYearlyStats()
|
||||
await patchFacultyYearlyStats(data)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshStudyrightAssociationsV2 = async () => {
|
||||
const refreshStudyrightAssociations = async () => {
|
||||
try {
|
||||
console.log('Refreshing studyright associations...')
|
||||
await refreshAssociationsInRedisV2()
|
||||
await refreshAssociationsInRedis()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshOverviewV2 = async () => {
|
||||
const refreshOverview = async () => {
|
||||
try {
|
||||
console.log('Refreshing throughput and productivity for programmes...')
|
||||
const codes = (await getAllProgrammesV2()).map(p => p.code)
|
||||
const codes = (await getAllProgrammes()).map(p => p.code)
|
||||
let ready = 0
|
||||
for (const code of codes) {
|
||||
let programmeStatsSince = new Date('2017-07-31')
|
||||
@@ -68,12 +45,12 @@ const refreshOverviewV2 = async () => {
|
||||
programmeStatsSince = new Date('2000-07-31')
|
||||
}
|
||||
try {
|
||||
await patchThroughputV2({ [code]: { status: 'RECALCULATING' } })
|
||||
const data = await throughputStatsForStudytrackV2(code, programmeStatsSince.getFullYear())
|
||||
await setThroughputV2(data)
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
const data = await throughputStatsForStudytrack(code, programmeStatsSince.getFullYear())
|
||||
await setThroughput(data)
|
||||
} catch (e) {
|
||||
try {
|
||||
await patchThroughputV2({ [code]: { status: 'RECALCULATION ERRORED' } })
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATION ERRORED' } })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
@@ -81,12 +58,12 @@ const refreshOverviewV2 = async () => {
|
||||
console.log(`Failed to update throughput stats for code: ${code}, reason: ${e.message}`)
|
||||
}
|
||||
try {
|
||||
await patchProductivityV2({ [code]: { status: 'RECALCULATING' } })
|
||||
const data = await productivityStatsForStudytrackV2(code, programmeStatsSince)
|
||||
await setProductivityV2(data)
|
||||
await patchProductivity({ [code]: { status: 'RECALCULATING' } })
|
||||
const data = await productivityStatsForStudytrack(code, programmeStatsSince)
|
||||
await setProductivity(data)
|
||||
} catch (e) {
|
||||
try {
|
||||
await patchProductivityV2({
|
||||
await patchProductivity({
|
||||
[code]: { status: 'RECALCULATION ERRORED' },
|
||||
})
|
||||
} catch (e) {
|
||||
@@ -103,20 +80,20 @@ const refreshOverviewV2 = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const refreshTeacherLeaderboardV2 = async () => {
|
||||
const refreshTeacherLeaderboard = async () => {
|
||||
try {
|
||||
const startyearcode = new Date().getFullYear() - 1950
|
||||
const endyearcode = startyearcode + 1
|
||||
console.log('Refreshing teacher leaderboard...')
|
||||
await topteachersV2.findAndSaveTeachers(startyearcode, endyearcode)
|
||||
await findAndSaveTeachers(startyearcode, endyearcode)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshNonGraduatedStudentsOfOldProgrammesV2 = async () => {
|
||||
const refreshNonGraduatedStudentsOfOldProgrammes = async () => {
|
||||
try {
|
||||
const oldProgrammeCodes = (await getAllProgrammesV2()).map(p => p.code).filter(c => !isNewHYStudyProgramme(c))
|
||||
const oldProgrammeCodes = (await getAllProgrammes()).map(p => p.code).filter(c => !isNewHYStudyProgramme(c))
|
||||
let i = 0
|
||||
console.log('Refreshing non-graduated students of old programmes...')
|
||||
await Promise.all(
|
||||
@@ -124,8 +101,8 @@ const refreshNonGraduatedStudentsOfOldProgrammesV2 = async () => {
|
||||
c =>
|
||||
new Promise(async res => {
|
||||
try {
|
||||
const [nonGraduatedStudents, studentnumbers] = await nonGraduatedStudentsOfElementDetailV2(c)
|
||||
await patchNonGraduatedStudentsV2({ [c]: { formattedData: nonGraduatedStudents, studentnumbers } })
|
||||
const [nonGraduatedStudents, studentnumbers] = await nonGraduatedStudentsOfElementDetail(c)
|
||||
await patchNonGraduatedStudents({ [c]: { formattedData: nonGraduatedStudents, studentnumbers } })
|
||||
console.log(`${++i}/${oldProgrammeCodes.length}`)
|
||||
} catch (e) {
|
||||
console.log(`Failed refreshing non-graduated students of programme ${c}!`)
|
||||
@@ -193,8 +170,9 @@ const refreshUberToRedis = async () => {
|
||||
|
||||
const refreshProtoCProgrammeToRedis = async () => {
|
||||
try {
|
||||
const codes = (await getAllProgrammesV2()).map(p => p.code).filter(code => code.includes('KH'))
|
||||
const codes = (await getAllProgrammes()).map(p => p.code).filter(code => code.includes('KH'))
|
||||
codes.forEach(async code => {
|
||||
console.log('refreshing code', code)
|
||||
const defaultQuery = { include_old_attainments: 'false', exclude_non_enrolled: 'false', code }
|
||||
const onlyOld = { include_old_attainments: 'true', exclude_non_enrolled: 'false', code }
|
||||
const onlyEnr = { include_old_attainments: 'false', exclude_non_enrolled: 'true', code }
|
||||
@@ -210,31 +188,30 @@ const refreshProtoCProgrammeToRedis = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const refreshStatisticsV2 = async () => {
|
||||
await refreshStudyrightAssociationsV2()
|
||||
await refreshOverviewV2()
|
||||
await refreshNonGraduatedStudentsOfOldProgrammesV2()
|
||||
await refreshTeacherLeaderboardV2()
|
||||
await refreshFacultyYearlyStats() // using old data
|
||||
const refreshStatistics = async () => {
|
||||
await refreshStudyrightAssociations()
|
||||
await refreshOverview()
|
||||
await refreshNonGraduatedStudentsOfOldProgrammes()
|
||||
await refreshTeacherLeaderboard()
|
||||
}
|
||||
|
||||
const startCron = () => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
schedule('0 6 * * *', async () => {
|
||||
await refreshStatisticsV2()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const refreshCDS = async () => {
|
||||
const refreshTrends = async () => {
|
||||
await refreshProtoCtoRedis()
|
||||
await refreshStatusToRedis()
|
||||
await refreshUberToRedis()
|
||||
await refreshProtoCProgrammeToRedis()
|
||||
}
|
||||
|
||||
const startCron = () => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
schedule('0 6 * * *', async () => {
|
||||
await refreshStatistics()
|
||||
await refreshTrends()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startCron,
|
||||
refreshStatisticsV2,
|
||||
refreshCDS,
|
||||
refreshStatistics,
|
||||
}
|
||||
|
||||
@@ -6,21 +6,6 @@ const ThesisTypeEnums = {
|
||||
BACHELOR: 'BACHELOR',
|
||||
}
|
||||
|
||||
const CourseGroup = sequelizeKone.define('course_groups', {
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true,
|
||||
},
|
||||
name: {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
},
|
||||
programmeid: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
})
|
||||
|
||||
const ExcludedCourse = sequelizeKone.define(
|
||||
'excluded_courses',
|
||||
{
|
||||
@@ -47,15 +32,6 @@ const ExcludedCourse = sequelizeKone.define(
|
||||
}
|
||||
)
|
||||
|
||||
const TeacherCourseGroup = sequelizeKone.define('teacher_course_groups', {
|
||||
course_group_id: {
|
||||
type: Sequelize.BIGINT,
|
||||
},
|
||||
teacher_id: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
})
|
||||
|
||||
const MandatoryCourse = sequelizeKone.define('mandatory_courses', {
|
||||
course_code: {
|
||||
type: Sequelize.STRING,
|
||||
@@ -248,26 +224,15 @@ MandatoryCourseLabels.hasMany(MandatoryCourse, {
|
||||
sourceKey: 'id',
|
||||
})
|
||||
|
||||
TeacherCourseGroup.belongsTo(CourseGroup, {
|
||||
foreignKey: 'course_group_id',
|
||||
sourceKey: 'id',
|
||||
})
|
||||
CourseGroup.hasMany(TeacherCourseGroup, {
|
||||
foreignKey: 'course_group_id',
|
||||
sourceKey: 'id',
|
||||
})
|
||||
|
||||
TagStudent.belongsTo(Tag, { foreignKey: 'tag_id', sourceKey: 'tag_id' })
|
||||
Tag.hasMany(TagStudent, { foreignKey: 'tag_id', sourceKey: 'tag_id' })
|
||||
|
||||
module.exports = {
|
||||
TeacherCourseGroup,
|
||||
MandatoryCourse,
|
||||
TagStudent,
|
||||
Tag,
|
||||
Filters,
|
||||
UsageStatistic,
|
||||
CourseGroup,
|
||||
CourseDuplicates,
|
||||
ThesisCourse,
|
||||
ThesisTypeEnums,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { sequelize, sequelizeKone } = require('../database/connection')
|
||||
const { sequelize } = require('../database/connection')
|
||||
|
||||
const getStudentNumbers = () =>
|
||||
sequelize.query(
|
||||
@@ -9,164 +9,6 @@ const getStudentNumbers = () =>
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
)
|
||||
|
||||
const getCurrentAcademicYear = () =>
|
||||
sequelize.query(
|
||||
`select
|
||||
semestercode,
|
||||
yearname
|
||||
from semesters
|
||||
where startdate <= now()
|
||||
and date_part('month', startdate) = 7
|
||||
order by startdate desc
|
||||
fetch first row only`,
|
||||
{ type: sequelize.QueryTypes.SELECT }
|
||||
)
|
||||
|
||||
const getAcademicYearsFrom = startSemesterCode =>
|
||||
sequelize.query(
|
||||
`select
|
||||
distinct on (yearname) yearname,
|
||||
semestercode
|
||||
from semesters
|
||||
where semestercode >= :startSemesterCode
|
||||
and startdate <= now()
|
||||
order by yearname, semestercode`,
|
||||
{
|
||||
replacements: { startSemesterCode },
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
)
|
||||
|
||||
const getAcademicYearStatistics = (teacherIds, startSemester) => {
|
||||
const endSemester = startSemester + 1
|
||||
return sequelize.query(
|
||||
`select
|
||||
count(distinct c.course_code) as courses,
|
||||
sum(credits) as credits,
|
||||
count(distinct student_studentnumber) as students
|
||||
from credit_teachers ct
|
||||
left join credit c on ct.credit_id = c.id
|
||||
where
|
||||
ct.teacher_id in (:teacherIds)
|
||||
and
|
||||
c.semestercode >= :startSemester
|
||||
and
|
||||
c.semestercode <= :endSemester`,
|
||||
{
|
||||
replacements: { teacherIds, startSemester, endSemester },
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getTeacherAcademicYearStatisticsByIds = (teacherIds, startSemester) => {
|
||||
const endSemester = startSemester + 1
|
||||
return sequelize.query(
|
||||
`select
|
||||
ct.teacher_id as id,
|
||||
count(distinct c.course_code) as courses,
|
||||
sum(credits) as credits,
|
||||
count(distinct student_studentnumber) as students
|
||||
from credit_teachers ct
|
||||
left join credit c on ct.credit_id = c.id
|
||||
where
|
||||
c.semestercode >= :startSemester
|
||||
and
|
||||
c.semestercode <= :endSemester
|
||||
and
|
||||
ct.teacher_id in (:teacherIds)
|
||||
group by ct.teacher_id`,
|
||||
{
|
||||
replacements: { teacherIds, startSemester, endSemester },
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getAcademicYearCoursesByTeacherIds = (teacherIds, startSemester) => {
|
||||
const endSemester = startSemester + 1
|
||||
return sequelize.query(
|
||||
`select
|
||||
course_code as coursecode,
|
||||
co.name as coursenames,
|
||||
t.code as teachercode,
|
||||
t.name as teachername,
|
||||
sum(credits) as credits,
|
||||
count(distinct student_studentnumber) as students
|
||||
from credit_teachers ct
|
||||
left join credit c on ct.credit_id = c.id
|
||||
left join teacher t on ct.teacher_id = t.id
|
||||
left join course co on c.course_code = co.code
|
||||
where
|
||||
c.semestercode >= :startSemester
|
||||
and
|
||||
c.semestercode <= :endSemester
|
||||
and
|
||||
ct.teacher_id in (:teacherIds)
|
||||
group by course_code, t.name, t.code, co.name`,
|
||||
{
|
||||
replacements: { teacherIds, startSemester, endSemester },
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getAcademicYearStatisticsForStudyProgramme = async (programmeid, startSemester) => {
|
||||
const endSemester = startSemester + 1
|
||||
const courseGroupData = await sequelizeKone.query(
|
||||
`
|
||||
SELECT cg.id AS id, cg.name AS name, tcg.teacher_id as teacher_id, cg.programmeid as programmeid
|
||||
FROM course_groups cg
|
||||
left join teacher_course_groups tcg
|
||||
ON tcg.course_group_id = cg.id
|
||||
WHERE cg.programmeid = :programmeid
|
||||
`,
|
||||
{
|
||||
replacements: { programmeid },
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
)
|
||||
|
||||
const courseGroupIdToCGData = courseGroupData.reduce((acc, cgd) => {
|
||||
acc[cgd.id] = acc[cgd.id] || { name: cgd.name, id: cgd.id, programmeid: cgd.programmeid, teacherids: [] }
|
||||
if (cgd.teacher_id) acc[cgd.id].teacherids.push(cgd.teacher_id)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const resultPromises = Object.keys(courseGroupIdToCGData).map(async cgid => {
|
||||
const { name, id, programmeid, teacherids } = courseGroupIdToCGData[cgid]
|
||||
if (teacherids.length === 0) return { name, id, programmeid, courses: 0, credits: 0, students: 0 }
|
||||
const stats = await sequelize.query(
|
||||
`
|
||||
SELECT
|
||||
COUNT(DISTINCT c.course_code) AS courses,
|
||||
COALESCE(SUM(credits), 0) AS credits,
|
||||
COUNT(DISTINCT student_studentnumber) AS students
|
||||
FROM credit_teachers ct
|
||||
left join credit c
|
||||
ON c.id = ct.credit_id
|
||||
AND c.semestercode >= :startSemester
|
||||
AND c.semestercode <= :endSemester
|
||||
WHERE ct.teacher_id IN (:teacherids)
|
||||
`,
|
||||
{
|
||||
replacements: { startSemester, endSemester, teacherids },
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
)
|
||||
if (stats.length !== 1) throw 'expecting single row'
|
||||
const { courses, credits, students } = stats[0]
|
||||
return { name, id, programmeid, courses, credits, students }
|
||||
})
|
||||
return Promise.all(resultPromises)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getStudentNumbers,
|
||||
getCurrentAcademicYear,
|
||||
getAcademicYearsFrom,
|
||||
getAcademicYearStatistics,
|
||||
getAcademicYearStatisticsForStudyProgramme,
|
||||
getTeacherAcademicYearStatisticsByIds,
|
||||
getAcademicYearCoursesByTeacherIds,
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ const auth = require('./middleware/auth')
|
||||
const teachers = require('./routes/teachers')
|
||||
const providers = require('./routes/providers')
|
||||
const faculties = require('./routes/faculties')
|
||||
const facultyStats = require('./routes/facultyStats')
|
||||
const semesters = require('./routes/semesters')
|
||||
const courseGroups = require('./routes/courseGroups')
|
||||
const mandatoryCourses = require('./routes/mandatorycourses')
|
||||
const mandatoryCourseLabels = require('./routes/mandatorycourselabels')
|
||||
const ping = require('./routes/ping')
|
||||
@@ -56,12 +54,10 @@ module.exports = (app, url) => {
|
||||
app.use(url, programmeModules)
|
||||
app.use(url, diff)
|
||||
app.use(`${url}/faculties`, faculties)
|
||||
app.use(`${url}/facultystats`, auth.roles(['faculties']), facultyStats)
|
||||
app.use(`${url}/updater`, auth.roles(['admin']), updater)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
app.use(`${url}/users`, auth.roles(['users']), users)
|
||||
app.use(`${url}/feedback`, feedback)
|
||||
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)
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
const router = require('express').Router()
|
||||
const CourseGroupService = require('../services/courseGroups')
|
||||
|
||||
router.get('/programme/:programmeId/:force?', async (req, res) => {
|
||||
const { programmeId, force } = req.params
|
||||
const { rights, roles } = req
|
||||
const semesterCode = req.query.semester
|
||||
if (rights.includes(programmeId) || (roles && roles.includes('admin'))) {
|
||||
const courseGroups = await CourseGroupService.getCourseGroupsWithTotals(
|
||||
programmeId,
|
||||
semesterCode,
|
||||
force === 'force'
|
||||
)
|
||||
return res.json(courseGroups)
|
||||
}
|
||||
return res.status(403).json({ error: 'No right to view programme data' })
|
||||
})
|
||||
|
||||
router.get('/academic-years', async (req, res) => {
|
||||
const academicYears = await CourseGroupService.getAcademicYears()
|
||||
return res.json(academicYears)
|
||||
})
|
||||
|
||||
router.get('/courses', async (req, res) => {
|
||||
const { teacherIds: ids, semester } = req.query
|
||||
const teacherIds = ids && JSON.parse(ids)
|
||||
|
||||
if (!teacherIds || !Array.isArray(teacherIds)) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
if (teacherIds.length === 0) return res.send([])
|
||||
|
||||
const courses = await CourseGroupService.getCoursesByTeachers(teacherIds, semester)
|
||||
|
||||
return courses ? res.send(courses) : res.sendStatus(404)
|
||||
})
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const { semester } = req.query
|
||||
const groupData = await CourseGroupService.getCourseGroup(Number(id), Number(semester))
|
||||
|
||||
return groupData ? res.send(groupData) : res.sendStatus(404)
|
||||
})
|
||||
|
||||
router.post('/:id/add/:teacherid', async (req, res) => {
|
||||
const { id, teacherid } = req.params
|
||||
const success = await CourseGroupService.addTeacher(Number(id), teacherid)
|
||||
|
||||
return success ? res.status(200).json('added') : res.sendStatus(404)
|
||||
})
|
||||
|
||||
router.post('/:id/remove/:teacherid', async (req, res) => {
|
||||
const { id, teacherid } = req.params
|
||||
const success = await CourseGroupService.removeTeacher(Number(id), teacherid)
|
||||
|
||||
return success ? res.status(200).json('removed') : res.sendStatus(404)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,9 +1,10 @@
|
||||
const router = require('express').Router()
|
||||
const { all } = require('../services/organisations')
|
||||
const { faculties } = require('../servicesV2/organisations')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const faculties = await all()
|
||||
res.json(faculties)
|
||||
const facultyList = await faculties()
|
||||
console.log('faculties')
|
||||
res.json(facultyList)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
const router = require('express').Router()
|
||||
const { all } = require('../services/organisations')
|
||||
const { byType: getElementDetailsByType } = require('../services/elementdetails')
|
||||
const { getFacultyYearlyStats } = require('../servicesV2/analyticsService')
|
||||
|
||||
router.get('/faculties', async (req, res) => {
|
||||
const { roles, faculties } = req
|
||||
const allFaculties = await all()
|
||||
res.json(roles.includes('admin') ? allFaculties : allFaculties.filter(({ code }) => faculties.has(code)))
|
||||
})
|
||||
|
||||
router.get('/yearlystats', async (req, res) => {
|
||||
try {
|
||||
const { roles, faculties } = req
|
||||
const facultyYearlyStats = await getFacultyYearlyStats()
|
||||
res.json(roles.includes('admin') ? facultyYearlyStats : facultyYearlyStats.filter(({ id }) => faculties.has(id)))
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/programmes', async (req, res) => {
|
||||
try {
|
||||
const { roles } = req
|
||||
const isAdmin = roles.includes('admin')
|
||||
const facultyProgrammes = await getElementDetailsByType(20)
|
||||
const userRightsSet = new Set(req.rights)
|
||||
res.json(facultyProgrammes.filter(({ code }) => userRightsSet.has(code) || isAdmin))
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e })
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,5 +1,5 @@
|
||||
const router = require('express').Router()
|
||||
const { getAllProviders } = require('../services/providers')
|
||||
const { getAllProviders } = require('../servicesV2/providers')
|
||||
|
||||
router.get('/providers', async (req, res) => {
|
||||
const providers = await getAllProviders()
|
||||
|
||||
@@ -9,7 +9,7 @@ const {
|
||||
abort,
|
||||
updateCoursesByCourseCode,
|
||||
} = require('../services/sisUpdaterService')
|
||||
const { refreshStatisticsV2 } = require('../events')
|
||||
const { refreshStatistics } = require('../events')
|
||||
|
||||
router.get('/update/v2/meta', async (req, res) => {
|
||||
const { roles } = req
|
||||
@@ -117,7 +117,7 @@ router.get('/refresh_redis_cache', async (req, res) => {
|
||||
|
||||
router.post('/refresh_statistic_v2', async (req, res) => {
|
||||
try {
|
||||
refreshStatisticsV2()
|
||||
refreshStatistics()
|
||||
res.status(200).json('Refreshing sis statistics')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
const { redisClient } = require('../services/redis')
|
||||
const { Teacher } = require('../models')
|
||||
const { CourseGroup, TeacherCourseGroup } = require('../models/models_kone')
|
||||
const { Op } = require('sequelize')
|
||||
const { flatMap } = require('lodash')
|
||||
|
||||
const {
|
||||
getCurrentAcademicYear,
|
||||
getAcademicYearsFrom,
|
||||
getAcademicYearStatistics,
|
||||
getAcademicYearStatisticsForStudyProgramme,
|
||||
getTeacherAcademicYearStatisticsByIds,
|
||||
getAcademicYearCoursesByTeacherIds,
|
||||
} = require('../models/queries')
|
||||
|
||||
const ACADEMIC_YEAR_START_SEMESTER = 111 // academic year 2005-06
|
||||
|
||||
const COURSE_GROUP_STATISTICS_KEY = (programmeId, semesterCode) =>
|
||||
`course_group_statistics_${programmeId}_${semesterCode}`
|
||||
const TEACHER_COURSES_KEY = (teacherId, semesterCode) => `course_group_courses_${teacherId}_${semesterCode}`
|
||||
const REDIS_CACHE_TTL = 12 * 60 * 60
|
||||
|
||||
const getAcademicYears = async () => getAcademicYearsFrom(ACADEMIC_YEAR_START_SEMESTER)
|
||||
|
||||
const getCurrentAcademicYearSemesterCode = async () => {
|
||||
const academicYear = await getCurrentAcademicYear()
|
||||
return academicYear[0].semestercode
|
||||
}
|
||||
|
||||
const getTeachersForCourseGroup = async courseGroupId => {
|
||||
const courseGroups = await CourseGroup.findAll({
|
||||
model: CourseGroup,
|
||||
include: {
|
||||
model: TeacherCourseGroup,
|
||||
required: true,
|
||||
},
|
||||
where: {
|
||||
id: courseGroupId,
|
||||
},
|
||||
})
|
||||
const teacherIds = flatMap(courseGroups, e => e.teacher_course_groups.map(tcg => tcg.teacher_id))
|
||||
const teachers = await Teacher.findAll({
|
||||
attributes: ['name', 'code', 'id'],
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: teacherIds,
|
||||
},
|
||||
},
|
||||
})
|
||||
return teachers
|
||||
}
|
||||
|
||||
const createCourseGroup = name => CourseGroup.create({ name })
|
||||
|
||||
const getCourseGroupsWithTotals = async (programmeId, semesterCode, forceRefresh = false) => {
|
||||
if (!forceRefresh) {
|
||||
const cachedStats = await redisClient.getAsync(COURSE_GROUP_STATISTICS_KEY(programmeId, semesterCode))
|
||||
if (cachedStats) {
|
||||
return JSON.parse(cachedStats)
|
||||
}
|
||||
}
|
||||
const startSemester = semesterCode || (await getCurrentAcademicYearSemesterCode())
|
||||
const result = await getAcademicYearStatisticsForStudyProgramme(programmeId, startSemester)
|
||||
|
||||
await redisClient.setAsync(
|
||||
COURSE_GROUP_STATISTICS_KEY(programmeId, semesterCode),
|
||||
JSON.stringify(result),
|
||||
'EX',
|
||||
6 * 4 * 7 * 24 * 60 * 60 // 6 months in seconds
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
const getCourseGroup = async (courseGroupId, semesterCode) => {
|
||||
const startSemester = semesterCode || (await getCurrentAcademicYearSemesterCode())
|
||||
const teacherBasicInfo = await getTeachersForCourseGroup(courseGroupId)
|
||||
const courseGroup = await CourseGroup.findByPk(courseGroupId)
|
||||
|
||||
if (!courseGroup) return
|
||||
if (!teacherBasicInfo) return
|
||||
|
||||
if (teacherBasicInfo.length === 0) {
|
||||
return {
|
||||
id: courseGroupId,
|
||||
name: courseGroup.name,
|
||||
teachers: [],
|
||||
totalCredits: 0,
|
||||
totalStudents: 0,
|
||||
totalCourses: 0,
|
||||
semester: startSemester,
|
||||
}
|
||||
}
|
||||
|
||||
const teacherIds = teacherBasicInfo.map(t => t.id)
|
||||
const statistics = await getAcademicYearStatistics(teacherIds, startSemester)
|
||||
const teacherStats = await getTeacherAcademicYearStatisticsByIds(teacherIds, startSemester)
|
||||
|
||||
const { credits, students, courses } = statistics[0]
|
||||
|
||||
const mergedTeachers = [...teacherBasicInfo.map(e => e.get()), ...teacherStats].reduce((acc, cur) => {
|
||||
if (acc[cur.id]) {
|
||||
acc[cur.id] = { ...cur, ...acc[cur.id] }
|
||||
} else {
|
||||
acc[cur.id] = cur
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const teachers = Object.values(mergedTeachers).map(t => ({
|
||||
...t,
|
||||
courses: t.courses ? Number(t.courses) : 0,
|
||||
credits: t.credits ? Number(t.credits) : 0,
|
||||
students: t.students ? Number(t.students) : 0,
|
||||
}))
|
||||
|
||||
const courseGroupInfo = {
|
||||
id: courseGroupId,
|
||||
name: courseGroup.name,
|
||||
teachers,
|
||||
totalCredits: credits || 0,
|
||||
totalStudents: Number(students),
|
||||
totalCourses: Number(courses),
|
||||
semester: startSemester,
|
||||
}
|
||||
|
||||
return courseGroupInfo
|
||||
}
|
||||
|
||||
const getCoursesByTeachers = async (teacherIds, semesterCode) => {
|
||||
const startSemester = semesterCode || (await getCurrentAcademicYearSemesterCode())
|
||||
const cacheKey = TEACHER_COURSES_KEY(teacherIds, startSemester)
|
||||
const cachedData = await redisClient.getAsync(cacheKey)
|
||||
|
||||
if (cachedData) {
|
||||
return JSON.parse(cachedData)
|
||||
}
|
||||
|
||||
const courses = await getAcademicYearCoursesByTeacherIds(teacherIds, startSemester)
|
||||
|
||||
// Sequelize returns credis sums as string
|
||||
courses.forEach(course => {
|
||||
course.credits = Number(course.credits)
|
||||
course.students = Number(course.students)
|
||||
})
|
||||
|
||||
await redisClient.setAsync(cacheKey, JSON.stringify(courses), 'EX', REDIS_CACHE_TTL)
|
||||
|
||||
return courses
|
||||
}
|
||||
|
||||
const addTeacher = async (courseGroupId, teacherid) => {
|
||||
const teacher = await Teacher.findByPk(teacherid)
|
||||
if (!teacher) return
|
||||
return TeacherCourseGroup.create({ teacher_id: teacherid, course_group_id: courseGroupId })
|
||||
}
|
||||
|
||||
const removeTeacher = (courseGroupId, teacherid) => {
|
||||
return TeacherCourseGroup.destroy({ where: { teacher_id: teacherid, course_group_id: courseGroupId } })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createCourseGroup,
|
||||
getAcademicYears,
|
||||
getCourseGroupsWithTotals,
|
||||
getCourseGroup,
|
||||
getCoursesByTeachers,
|
||||
addTeacher,
|
||||
removeTeacher,
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
const axios = require('axios')
|
||||
const moment = require('moment')
|
||||
const eachLimit = require('async/eachLimit')
|
||||
const { USERSERVICE_URL, DB_MAX_CRON_CONNECTIONS } = require('../conf-backend')
|
||||
const { Credit, StudyrightElement } = require('../models')
|
||||
const { lruMemoize } = require('../util')
|
||||
|
||||
const client = axios.create({ baseURL: USERSERVICE_URL, headers: { secret: process.env.USERSERVICE_SECRET } })
|
||||
|
||||
const calculateFacultyYearlyStats = async () => {
|
||||
const { data: facultyProgrammes } = await client.get('/faculty_programmes')
|
||||
|
||||
const getFilteredStudentCredits = lruMemoize(
|
||||
studentNumber =>
|
||||
Credit.findAll({
|
||||
where: {
|
||||
student_studentnumber: studentNumber,
|
||||
credittypecode: [4, 10],
|
||||
isStudyModule: false,
|
||||
},
|
||||
}),
|
||||
args => args[0],
|
||||
{
|
||||
max: 10000,
|
||||
}
|
||||
)
|
||||
|
||||
const res = {}
|
||||
const studentSets = {}
|
||||
|
||||
let amountDone = 0
|
||||
// Don't try to make this fast with Promise.all without taking into consideration that
|
||||
// we only have a limited amount of database connections
|
||||
for (const { faculty_code, programme_code } of facultyProgrammes) {
|
||||
if (!res[faculty_code]) {
|
||||
res[faculty_code] = {}
|
||||
studentSets[faculty_code] = {}
|
||||
}
|
||||
if (!res[faculty_code][programme_code]) {
|
||||
res[faculty_code][programme_code] = {}
|
||||
studentSets[faculty_code][programme_code] = {}
|
||||
}
|
||||
|
||||
const facultyStudents = await StudyrightElement.findAll({
|
||||
where: {
|
||||
code: programme_code,
|
||||
},
|
||||
attributes: ['studentnumber'],
|
||||
})
|
||||
|
||||
await eachLimit(facultyStudents, DB_MAX_CRON_CONNECTIONS, async student => {
|
||||
const filteredStudentCredits = await getFilteredStudentCredits(student.studentnumber)
|
||||
|
||||
filteredStudentCredits.forEach(c => {
|
||||
const attainmentYear = moment(c.attainment_date).year()
|
||||
if (!res[faculty_code][attainmentYear] && !res[faculty_code][programme_code][attainmentYear]) {
|
||||
const facultyYearStats = {}
|
||||
facultyYearStats.studentCredits = 0
|
||||
facultyYearStats.coursesPassed = 0
|
||||
facultyYearStats.coursesFailed = 0
|
||||
facultyYearStats.students = 0
|
||||
facultyYearStats.studentArray = []
|
||||
res[faculty_code][programme_code][attainmentYear] = facultyYearStats
|
||||
studentSets[faculty_code][programme_code][attainmentYear] = new Set()
|
||||
}
|
||||
const facultyYearStats = res[faculty_code][programme_code][attainmentYear]
|
||||
const studentSet = studentSets[faculty_code][programme_code][attainmentYear]
|
||||
if (!studentSet.has(c.student_studentnumber)) {
|
||||
facultyYearStats.students++
|
||||
facultyYearStats.studentArray.push(c.student_studentnumber)
|
||||
studentSet.add(c.student_studentnumber)
|
||||
}
|
||||
if (c.credittypecode === 4) {
|
||||
facultyYearStats.studentCredits += c.credits
|
||||
facultyYearStats.coursesPassed += 1
|
||||
} else {
|
||||
facultyYearStats.coursesFailed += 1
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
amountDone += 1
|
||||
console.log(`Faculty programmes done ${amountDone}/${facultyProgrammes.length}`)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
calculateFacultyYearlyStats,
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
const { Provider } = require('../models/index')
|
||||
|
||||
const getAllProviders = async () =>
|
||||
Provider.findAll({
|
||||
attributes: ['providercode', 'name'],
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
getAllProviders,
|
||||
}
|
||||
@@ -242,7 +242,7 @@ const getStudyrightElementsAndAssociationsForUser = async username => {
|
||||
}
|
||||
|
||||
const getAllDegreesAndProgrammes = async () => {
|
||||
const elementDetails = ElementDetails.findAll({
|
||||
const elementDetails = await ElementDetails.findAll({
|
||||
where: {
|
||||
type: {
|
||||
[Op.in]: [10, 20],
|
||||
|
||||
@@ -88,21 +88,6 @@ const patchThroughput = async data => {
|
||||
}
|
||||
}
|
||||
|
||||
const patchFacultyYearlyStats = async data => {
|
||||
const redisKey = 'FACULTY_YEARLY_STATS'
|
||||
const dataToPatch = Object.entries(data).reduce((acc, [id, data]) => [...acc, { id, data }], [])
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToPatch))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToPatch
|
||||
}
|
||||
|
||||
const getFacultyYearlyStats = async () => {
|
||||
const redisKey = 'FACULTY_YEARLY_STATS'
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const patchNonGraduatedStudents = async data => {
|
||||
const [id, dataToPatch] = Object.entries(data)[0]
|
||||
const redisKey = createRedisKeyForNonGraduatedStudents(id)
|
||||
@@ -135,8 +120,6 @@ module.exports = {
|
||||
getThroughput,
|
||||
setThroughput,
|
||||
patchThroughput,
|
||||
patchFacultyYearlyStats,
|
||||
getFacultyYearlyStats,
|
||||
patchNonGraduatedStudents,
|
||||
getNonGraduatedStudents,
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ const { parseCredit } = require('./parseCredits')
|
||||
const Op = Sequelize.Op
|
||||
const { CourseYearlyStatsCounter } = require('../servicesV2/course_yearly_stats_counter')
|
||||
const _ = require('lodash')
|
||||
// const { CourseProvider } = require('../models')
|
||||
|
||||
const byNameOrCode = (searchTerm, language) =>
|
||||
Course.findAll({
|
||||
@@ -356,50 +357,75 @@ const codeLikeTerm = code =>
|
||||
}
|
||||
|
||||
const byNameAndOrCodeLike = async (name, code) => {
|
||||
let courses = await Course.findAll({
|
||||
raw: true,
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
'code',
|
||||
'latest_instance_date',
|
||||
'startdate',
|
||||
'enddate',
|
||||
'max_attainment_date',
|
||||
'min_attainment_date',
|
||||
'substitutions',
|
||||
],
|
||||
let rawCourses = await Course.findAll({
|
||||
include: {
|
||||
model: Organization,
|
||||
required: true,
|
||||
},
|
||||
where: {
|
||||
...nameLikeTerm(name),
|
||||
...codeLikeTerm(code),
|
||||
},
|
||||
})
|
||||
|
||||
const courses = rawCourses
|
||||
.map(
|
||||
({
|
||||
id,
|
||||
code,
|
||||
name,
|
||||
latest_instance_date,
|
||||
is_study_module,
|
||||
coursetypecode,
|
||||
startdate,
|
||||
max_attainment_date,
|
||||
min_attainment_date,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
substitutions,
|
||||
organizations,
|
||||
}) => {
|
||||
return {
|
||||
id,
|
||||
code,
|
||||
name,
|
||||
latest_instance_date,
|
||||
is_study_module,
|
||||
coursetypecode,
|
||||
startdate,
|
||||
max_attainment_date,
|
||||
min_attainment_date,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
substitutions,
|
||||
organizations: organizations.map(o => o.id),
|
||||
}
|
||||
}
|
||||
)
|
||||
.sort(a => (a.code.match(/^[A-Za-z]{3}[0-9]{1}/) ? -1 : 1))
|
||||
|
||||
let substitutionGroupIndex = 0
|
||||
const subsGroups = {}
|
||||
const visited = []
|
||||
const dfs = async courseId => {
|
||||
if (visited.includes(courseId)) return
|
||||
visited.push(courseId)
|
||||
const course = courses.find(course => course.id === courseId)
|
||||
|
||||
if (!course) return
|
||||
const organizeSubgroups = course => {
|
||||
if (visited.includes(course.code)) return
|
||||
|
||||
subsGroups[course.code] = substitutionGroupIndex
|
||||
course.subsId = substitutionGroupIndex
|
||||
|
||||
for (const courseId of course.substitutions) {
|
||||
dfs(courseId)
|
||||
}
|
||||
let temp = courses.filter(c => (course.substitutions ? course.substitutions.includes(c.id) : false))
|
||||
temp.unshift(course)
|
||||
temp.forEach(cu => {
|
||||
if (visited.includes(course.code)) return
|
||||
visited.push(cu.id)
|
||||
cu.subsId = substitutionGroupIndex
|
||||
})
|
||||
}
|
||||
|
||||
for (const course of courses) {
|
||||
courses.forEach(course => {
|
||||
if (!visited.includes(course.id)) {
|
||||
substitutionGroupIndex++
|
||||
dfs(course.id)
|
||||
organizeSubgroups(course)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
console.log(courses)
|
||||
return { courses }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Organization } = require('../modelsV2')
|
||||
const Op = Sequelize.Op
|
||||
|
||||
// Have facultyfetching to work like it worked during oodi-db time
|
||||
const facultiesInOodi = [
|
||||
'H10',
|
||||
'H20',
|
||||
'H30',
|
||||
'H40',
|
||||
'H50',
|
||||
'H55',
|
||||
'H57',
|
||||
'H60',
|
||||
'H70',
|
||||
'H74',
|
||||
'H80',
|
||||
'H90',
|
||||
'H92',
|
||||
'H930',
|
||||
'H99',
|
||||
'Y',
|
||||
'Y01',
|
||||
]
|
||||
|
||||
const faculties = () => {
|
||||
return Organization.findAll({
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: facultiesInOodi,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
faculties,
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
const { Organization } = require('../modelsV2/index')
|
||||
|
||||
const getAllProviders = async () =>
|
||||
Organization.findAll({
|
||||
attributes: ['code', 'name'],
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
getAllProviders,
|
||||
}
|
||||
@@ -102,10 +102,13 @@ const calculateProtoCProgramme = async query => {
|
||||
includeOldAttainments: query.include_old_attainments === 'true',
|
||||
excludeNonEnrolled: query.exclude_non_enrolled === 'true',
|
||||
})
|
||||
|
||||
const programmeData = data.find(d => d.programmeType === 20)
|
||||
const studytrackData = data.filter(d => d.programmeType !== 20)
|
||||
|
||||
// mankel through studytracks
|
||||
if (!programmeData) return {}
|
||||
|
||||
// mankel through studytracks if studyrightdata available
|
||||
const studytrackMankelid = _(studytrackData)
|
||||
// seems to return the numerical columns as strings, parse them first
|
||||
.map(programmeRow => ({
|
||||
@@ -122,7 +125,7 @@ const calculateProtoCProgramme = async query => {
|
||||
const studytrackToBachelorProgrammes = Object.keys(associations.programmes).reduce((acc, curr) => {
|
||||
if (!curr.includes('KH')) return acc
|
||||
const studytracksForProgramme = studytrackMankelid.reduce((acc2, studytrackdata) => {
|
||||
if (associations.programmes[curr].studytracks.includes(studytrackdata.programmeCode)) {
|
||||
if (studytrackdata && associations.programmes[curr].studytracks.includes(studytrackdata.programmeCode)) {
|
||||
acc2.push({
|
||||
code: studytrackdata.programmeCode,
|
||||
name: studytrackdata.programmeName,
|
||||
|
||||
@@ -17,67 +17,57 @@ const ignores = {
|
||||
KH60_001: {
|
||||
2018: {
|
||||
oodi: [
|
||||
'014323074' // https://jira.it.helsinki.fi/browse/DOO-4545 // koulutus puuttuu
|
||||
]
|
||||
'014323074', // https://jira.it.helsinki.fi/browse/DOO-4545 // koulutus puuttuu
|
||||
],
|
||||
},
|
||||
2020: {
|
||||
oodi: [
|
||||
'013466013', // https://jira.it.helsinki.fi/browse/DOO-4545 // koulutus puuttuu
|
||||
'014340963',
|
||||
'014179998',
|
||||
'013743299'
|
||||
]
|
||||
}
|
||||
'013743299',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
MH40_002: {
|
||||
2020: {
|
||||
sis: ['014840890'] // https://jira.it.helsinki.fi/browse/DOO-4572
|
||||
}
|
||||
sis: ['014840890'], // https://jira.it.helsinki.fi/browse/DOO-4572
|
||||
},
|
||||
},
|
||||
MH40_005: {
|
||||
2019: {
|
||||
oodi: ['015095686'] // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2786
|
||||
}
|
||||
},
|
||||
MH40_014: {
|
||||
2020: {
|
||||
oodi: ['013349512'] // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2831
|
||||
}
|
||||
},
|
||||
MH40_015: {
|
||||
2020: {
|
||||
oodi: ['014143791'] // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2831
|
||||
}
|
||||
oodi: ['015095686'], // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2786
|
||||
},
|
||||
},
|
||||
MH55_001: {
|
||||
2019: {
|
||||
oodi: ['014744501'] // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2844
|
||||
}
|
||||
oodi: ['014744501'], // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2844
|
||||
},
|
||||
},
|
||||
MH70_008: {
|
||||
2017: {
|
||||
oodi: ['014404139'] // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2850
|
||||
}
|
||||
oodi: ['014404139'], // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2850
|
||||
},
|
||||
},
|
||||
MH70_007: {
|
||||
2020: {
|
||||
sis: ['014918968', '014934081', '014954922', '014887918'] // https://jira.it.helsinki.fi/browse/DOO-4572
|
||||
}
|
||||
sis: ['014918968', '014934081', '014954922', '014887918'], // https://jira.it.helsinki.fi/browse/DOO-4572
|
||||
},
|
||||
},
|
||||
MH80_001: {
|
||||
2020: {
|
||||
oodi: ['014343643'] // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2827
|
||||
}
|
||||
oodi: ['014343643'], // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2827
|
||||
},
|
||||
},
|
||||
MH60_001: {
|
||||
2017: {
|
||||
sis: [
|
||||
'014741193' // https://jira.it.helsinki.fi/browse/DOO-4570
|
||||
]
|
||||
'014741193', // https://jira.it.helsinki.fi/browse/DOO-4570
|
||||
],
|
||||
},
|
||||
2019: {
|
||||
oodi: ['014323074'] // https://jira.it.helsinki.fi/browse/DOO-4570
|
||||
oodi: ['014323074'], // https://jira.it.helsinki.fi/browse/DOO-4570
|
||||
},
|
||||
2020: {
|
||||
sis: [
|
||||
@@ -85,13 +75,13 @@ const ignores = {
|
||||
'014590027', // https://jira.it.helsinki.fi/browse/DOO-4571
|
||||
'014590807', // https://jira.it.helsinki.fi/browse/DOO-4571
|
||||
'014734511', // https://jira.it.helsinki.fi/browse/DOO-4571
|
||||
'013299358' // https://jira.it.helsinki.fi/browse/DOO-4571
|
||||
'013299358', // https://jira.it.helsinki.fi/browse/DOO-4571
|
||||
],
|
||||
oodi: [
|
||||
'014741193', // https://jira.it.helsinki.fi/browse/DOO-4570
|
||||
'014954744' // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3046
|
||||
]
|
||||
}
|
||||
'014954744', // https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3046
|
||||
],
|
||||
},
|
||||
},
|
||||
// there are many inconsistencies in masters, so they're grouped by the reason, not
|
||||
// by program for now
|
||||
@@ -276,7 +266,7 @@ const ignores = {
|
||||
'014863767', // MH60_001
|
||||
'014920231', // MH60_001
|
||||
'014936487', // MH60_001
|
||||
'015159111' // MH60_001
|
||||
'015159111', // MH60_001
|
||||
],
|
||||
// in oodi, but not in sis
|
||||
oodi: [
|
||||
@@ -338,21 +328,21 @@ const ignores = {
|
||||
'010664397', // KH50_005 opintoaika loppunut/luopunut
|
||||
'014769816', // KH55_001 luopunut
|
||||
'014845154', // KH60_001 oodissa väärä loppumispvm
|
||||
'014821974' // MH60_001 alotusv väärin oodissa
|
||||
]
|
||||
}
|
||||
'014821974', // MH60_001 alotusv väärin oodissa
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// For collecting all diff studentnumbers and printing them by reason
|
||||
let allGrouped = {
|
||||
oodiOnly: {
|
||||
cancelledInSis: [],
|
||||
unknown: []
|
||||
unknown: [],
|
||||
},
|
||||
sisOnly: {
|
||||
cancelledInOodi: [],
|
||||
unknown: []
|
||||
}
|
||||
unknown: [],
|
||||
},
|
||||
}
|
||||
|
||||
const populationDiff = async (programme, year) => {
|
||||
@@ -364,7 +354,7 @@ const populationDiff = async (programme, year) => {
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months,
|
||||
studyRights: { programme },
|
||||
year
|
||||
year,
|
||||
}
|
||||
|
||||
const resultSis = await populationsSis.optimizedStatisticsOf(query)
|
||||
@@ -445,19 +435,19 @@ const checkIfCancelledInOodi = async (code, sisOnly) => {
|
||||
const cancelledInOodi = await Studyright.findAll({
|
||||
where: {
|
||||
canceldate: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
[Op.ne]: null,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
student_studentnumber: {
|
||||
[Op.in]: sisOnly
|
||||
}
|
||||
[Op.in]: sisOnly,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: { code }
|
||||
}
|
||||
where: { code },
|
||||
},
|
||||
})
|
||||
return cancelledInOodi
|
||||
}
|
||||
@@ -466,19 +456,19 @@ const checkIfCancelledInSis = async (code, oodiOnly) => {
|
||||
const cancelledInSis = await SISStudyright.findAll({
|
||||
where: {
|
||||
canceldate: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
[Op.ne]: null,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
student_studentnumber: {
|
||||
[Op.in]: oodiOnly
|
||||
}
|
||||
[Op.in]: oodiOnly,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
model: SISStudyrightElement,
|
||||
required: true,
|
||||
where: { code }
|
||||
}
|
||||
where: { code },
|
||||
},
|
||||
})
|
||||
return cancelledInSis
|
||||
}
|
||||
@@ -494,14 +484,14 @@ const weirdInSIS = async (oodiOnly, resultOodi, code) => {
|
||||
const sisRights = await SISStudyright.findAll({
|
||||
where: {
|
||||
student_studentnumber: {
|
||||
[Op.in]: oodiOnly
|
||||
}
|
||||
[Op.in]: oodiOnly,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
model: SISStudyrightElement,
|
||||
required: true,
|
||||
where: { code }
|
||||
}
|
||||
where: { code },
|
||||
},
|
||||
}).reduce((acc, curr) => ({ ...acc, [curr.studentStudentnumber]: curr }), {})
|
||||
|
||||
const notInSisProgramme = oodiOnly.filter(sn => !sisRights[sn])
|
||||
@@ -511,10 +501,10 @@ const weirdInSIS = async (oodiOnly, resultOodi, code) => {
|
||||
where: {
|
||||
targetcode: code,
|
||||
studentnumber: {
|
||||
[Op.in]: notInSisProgramme
|
||||
}
|
||||
[Op.in]: notInSisProgramme,
|
||||
},
|
||||
},
|
||||
raw: true
|
||||
raw: true,
|
||||
})
|
||||
|
||||
let studentNumbersTransferredToThisProgramme = transferredToThisProgramme.map(s => s.studentnumber)
|
||||
@@ -548,7 +538,7 @@ const weirdInSIS = async (oodiOnly, resultOodi, code) => {
|
||||
cancelledstudents,
|
||||
transferredInPakkoSiirto,
|
||||
transferredAtSomeOtherDate,
|
||||
notInProgramme: _.difference(notInSisProgramme, studentNumbersTransferredToThisProgramme)
|
||||
notInProgramme: _.difference(notInSisProgramme, studentNumbersTransferredToThisProgramme),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,11 +567,11 @@ const masterCodes = async () => {
|
||||
attributes: ['code'],
|
||||
where: {
|
||||
code: {
|
||||
[Op.like]: 'MH%'
|
||||
}
|
||||
[Op.like]: 'MH%',
|
||||
},
|
||||
},
|
||||
group: ['code'],
|
||||
order: ['code']
|
||||
order: ['code'],
|
||||
})
|
||||
).map(s => s.code)
|
||||
}
|
||||
@@ -592,11 +582,11 @@ const bscCodes = async () => {
|
||||
attributes: ['code'],
|
||||
where: {
|
||||
code: {
|
||||
[Op.like]: 'KH%'
|
||||
}
|
||||
[Op.like]: 'KH%',
|
||||
},
|
||||
},
|
||||
group: ['code'],
|
||||
order: ['code']
|
||||
order: ['code'],
|
||||
})
|
||||
).map(s => s.code)
|
||||
}
|
||||
@@ -703,5 +693,5 @@ main()
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
ignores
|
||||
ignores,
|
||||
}
|
||||
|
||||
@@ -3,21 +3,23 @@ FROM node:14.17-alpine3.13 as base
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
FROM base as development
|
||||
EXPOSE 3000
|
||||
COPY ./package* /usr/src/app/
|
||||
RUN npm ci
|
||||
COPY . /usr/src/app/
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
|
||||
FROM base as production
|
||||
EXPOSE 5000
|
||||
ARG SENTRY_ENVIRONMENT
|
||||
ENV REACT_APP_SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||
ARG SENTRY_RELEASE
|
||||
ENV REACT_APP_SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
ARG BUILT_AT
|
||||
ENV REACT_APP_BUILT_AT=$BUILT_AT
|
||||
COPY --chown=node:node ./package* /usr/src/app/
|
||||
RUN npm ci --production
|
||||
COPY --chown=node:node . /usr/src/app/
|
||||
RUN npm run build && npm install -g serve@12.0.0 && chown -R node:node /usr/src/app/build
|
||||
USER node
|
||||
ARG SENTRY_ENVIRONMENT
|
||||
ENV REACT_APP_SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||
ARG SENTRY_RELEASE
|
||||
ENV REACT_APP_SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
EXPOSE 5000
|
||||
CMD ["serve", "--single", "--listen", "5000", "build"]
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import { Header, Segment, Icon, Button } from 'semantic-ui-react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { func, shape, string } from 'prop-types'
|
||||
|
||||
import { getCompiledPath } from '../../../common'
|
||||
|
||||
import { callApi } from '../../../apiConnection'
|
||||
import SortableTable from '../../SortableTable'
|
||||
import CourseGroupAddTeacher from '../CourseGroupAddTeacher'
|
||||
import CourseGroup from '../CourseGroup'
|
||||
|
||||
const getCourseGroupPath = (courseGroupId, studyProgrammeId) =>
|
||||
getCompiledPath('/study-programme/:studyProgrammeId/course-group/:courseGroupId', {
|
||||
courseGroupId,
|
||||
studyProgrammeId,
|
||||
})
|
||||
|
||||
class AggregateView extends Component {
|
||||
state = {
|
||||
isLoading: true,
|
||||
editedCourseGroup: null,
|
||||
courseGroups: [],
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
callApi(`/course-groups/programme/${this.props.programmeId}`)
|
||||
.then(res => {
|
||||
this.setState({
|
||||
courseGroups: res.data,
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.message.toLowerCase() === 'network error') {
|
||||
window.location.reload(true)
|
||||
return
|
||||
}
|
||||
this.setState({ courseGroups: [], isLoading: false })
|
||||
})
|
||||
}
|
||||
|
||||
handleForceRefresh() {
|
||||
this.setState({ isLoading: true })
|
||||
callApi(`/course-groups/programme/${this.props.programmeId}/force`)
|
||||
.then(res => {
|
||||
this.setState({
|
||||
courseGroups: res.data,
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.message.toLowerCase() === 'network error') {
|
||||
window.location.reload(true)
|
||||
return
|
||||
}
|
||||
this.setState({ courseGroups: [], isLoading: false })
|
||||
})
|
||||
}
|
||||
|
||||
handleNavigationCourseGroup = (e, courseGroupId, programmeId) => {
|
||||
e.preventDefault()
|
||||
this.props.history.push(getCourseGroupPath(courseGroupId, programmeId))
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, courseGroups, editedCourseGroup } = this.state
|
||||
const { programmeId, courseGroupId } = this.props
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'Course group',
|
||||
title: 'Course group',
|
||||
getRowVal: cg => cg.name,
|
||||
getRowContent: courseGroup => (
|
||||
<>
|
||||
<a
|
||||
href={getCourseGroupPath(courseGroup.id, programmeId)}
|
||||
onClick={e => this.handleNavigationCourseGroup(e, courseGroup.id, programmeId)}
|
||||
>
|
||||
{courseGroup.name}
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{ key: 'Credits', title: 'Credits', getRowVal: cg => cg.credits },
|
||||
{ key: 'Students', title: 'Students', getRowVal: cg => cg.students },
|
||||
{
|
||||
key: 'Edit',
|
||||
title: '',
|
||||
getRowVal: cg => <Icon name="edit" onClick={() => this.setState({ editedCourseGroup: cg.id })} link />,
|
||||
headerProps: { onClick: null, sorted: null },
|
||||
cellProps: { collapsing: true },
|
||||
},
|
||||
]
|
||||
|
||||
const renderViewCourseGroup = () => (
|
||||
<>
|
||||
<CourseGroup groupId={courseGroupId} studyProgrammeId={programmeId} />
|
||||
</>
|
||||
)
|
||||
|
||||
const renderEditCourseGroup = () => (
|
||||
<>
|
||||
<Header size="medium">
|
||||
Edit group
|
||||
<Icon name="reply" onClick={() => this.setState({ editedCourseGroup: null })} link />
|
||||
</Header>
|
||||
<CourseGroupAddTeacher groupId={editedCourseGroup} />
|
||||
</>
|
||||
)
|
||||
|
||||
const renderCourseGroups = () => (
|
||||
<>
|
||||
<Header size="medium">
|
||||
Group statistics
|
||||
<Button
|
||||
content="Recalculate"
|
||||
floated="right"
|
||||
size="small"
|
||||
style={{ marginBottom: '10px' }}
|
||||
onClick={() => {
|
||||
this.handleForceRefresh()
|
||||
}}
|
||||
/>
|
||||
</Header>
|
||||
{courseGroups.length === 0 ? (
|
||||
<Segment>No course groups defined</Segment>
|
||||
) : (
|
||||
<SortableTable getRowKey={gc => gc.id} tableProps={{ celled: false }} columns={columns} data={courseGroups} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Segment loading={isLoading}>
|
||||
{courseGroupId && renderViewCourseGroup()}
|
||||
{editedCourseGroup && renderEditCourseGroup()}
|
||||
{!editedCourseGroup && !courseGroupId && renderCourseGroups()}
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AggregateView.propTypes = {
|
||||
history: shape({
|
||||
push: func.isRequired,
|
||||
}).isRequired,
|
||||
programmeId: string.isRequired,
|
||||
courseGroupId: string,
|
||||
}
|
||||
|
||||
AggregateView.defaultProps = {
|
||||
courseGroupId: null,
|
||||
}
|
||||
|
||||
export default withRouter(AggregateView)
|
||||
@@ -1,161 +0,0 @@
|
||||
import React, { Fragment, Component } from 'react'
|
||||
import { Header, List, Loader, Placeholder, Icon } from 'semantic-ui-react'
|
||||
import { arrayOf, string, number, shape } from 'prop-types'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import { callApi } from '../../../../apiConnection/index'
|
||||
import '../courseGroup.css'
|
||||
import { CG_API_BASE_PATH } from '../util'
|
||||
|
||||
const courseColumnTypes = {
|
||||
TEACHER: 'teachername',
|
||||
CODE: 'coursecode',
|
||||
NAME: 'coursenames',
|
||||
CREDITS: 'credits',
|
||||
STUDENTS: 'students',
|
||||
}
|
||||
|
||||
const CourseItem = ({ course }) => {
|
||||
const { coursecode, teachername, credits, students, coursenames } = course
|
||||
const coursename = coursenames.fi
|
||||
return (
|
||||
<List.Item>
|
||||
<List.Content className="courseInfoContent">
|
||||
<div className="courseInfoName">{coursename}</div>
|
||||
<div className="courseInfoCode">{coursecode}</div>
|
||||
<div className="courseInfoTeacher">{teachername}</div>
|
||||
<div className="courseInfoItem">{credits}</div>
|
||||
<div className="courseInfoItem">{students}</div>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
)
|
||||
}
|
||||
CourseItem.propTypes = {
|
||||
course: shape({
|
||||
coursecode: string,
|
||||
teachername: string,
|
||||
credits: number,
|
||||
students: number,
|
||||
coursenames: shape({}),
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
class Index extends Component {
|
||||
state = {
|
||||
isLoading: true,
|
||||
courses: [],
|
||||
sortColumn: courseColumnTypes.TEACHER,
|
||||
sortReverse: false,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchCourses()
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { teacherIds, semesterCode } = this.props
|
||||
const isNewTeachers = prevProps.teacherIds.length !== teacherIds.length
|
||||
const isNewSemester = prevProps.semesterCode !== semesterCode
|
||||
if (isNewTeachers || isNewSemester) {
|
||||
this.fetchCourses()
|
||||
}
|
||||
}
|
||||
|
||||
onCourseHeaderClick = columnName => {
|
||||
const { sortColumn, sortReverse } = this.state
|
||||
const isSortColumn = columnName === sortColumn
|
||||
|
||||
this.setState({
|
||||
sortColumn: columnName,
|
||||
sortReverse: isSortColumn ? !sortReverse : sortReverse,
|
||||
})
|
||||
}
|
||||
|
||||
fetchCourses = () => {
|
||||
const { teacherIds, semesterCode } = this.props
|
||||
|
||||
let path = `${CG_API_BASE_PATH}/courses/?teacherIds=${JSON.stringify(teacherIds)}`
|
||||
if (semesterCode) {
|
||||
path = `${path}&semester=${semesterCode}`
|
||||
}
|
||||
|
||||
this.setState({ isLoading: true, courses: [] }, () =>
|
||||
callApi(path)
|
||||
.then(({ data: courses }) => this.setState({ isLoading: false, courses }))
|
||||
.catch(e => {
|
||||
if (e.message.toLowerCase() === 'network error') {
|
||||
window.location.reload(true)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
renderPlaceholder = () => {
|
||||
const placeholderKeys = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
return (
|
||||
<Placeholder fluid>
|
||||
{placeholderKeys.map(k => (
|
||||
<Placeholder.Line key={k} length="full" />
|
||||
))}
|
||||
</Placeholder>
|
||||
)
|
||||
}
|
||||
|
||||
renderListHeader = () => {
|
||||
const { sortColumn, sortReverse } = this.state
|
||||
|
||||
const getHeader = (className, label, columnName) => {
|
||||
const isActive = columnName === sortColumn
|
||||
return (
|
||||
<div className={className} onClick={() => this.onCourseHeaderClick(columnName)}>
|
||||
{label}
|
||||
{isActive ? <Icon name={`caret ${sortReverse ? 'down' : 'up'}`} /> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<List.Header>
|
||||
<List.Content className="courseHeaderContent">
|
||||
{getHeader('courseHeaderName', 'Name', courseColumnTypes.NAME)}
|
||||
{getHeader('courseHeaderCode', 'Code', courseColumnTypes.CODE)}
|
||||
{getHeader('courseHeaderTeacher', 'Teacher', courseColumnTypes.TEACHER)}
|
||||
{getHeader('courseHeaderItem', 'Credits', courseColumnTypes.CREDITS)}
|
||||
{getHeader('courseHeaderItem', 'Students', courseColumnTypes.STUDENTS)}
|
||||
</List.Content>
|
||||
</List.Header>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, courses, sortColumn, sortReverse } = this.state
|
||||
const sortedCourses = sortBy(courses, sortColumn)
|
||||
|
||||
if (sortReverse) {
|
||||
sortedCourses.reverse()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header size="medium" content="Courses" />
|
||||
<Loader active={isLoading} inline="centered" />
|
||||
{isLoading ? (
|
||||
this.renderPlaceholder()
|
||||
) : (
|
||||
<List celled>
|
||||
{this.renderListHeader()}
|
||||
{sortedCourses.map(c => (
|
||||
<CourseItem key={`${c.coursecode}-${c.teachercode}`} course={c} />
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Index.propTypes = {
|
||||
teacherIds: arrayOf(string).isRequired,
|
||||
semesterCode: number.isRequired,
|
||||
}
|
||||
|
||||
export default Index
|
||||
@@ -1,79 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Statistic, Placeholder } from 'semantic-ui-react'
|
||||
import { number, bool, arrayOf } from 'prop-types'
|
||||
import { teacherType } from '../util'
|
||||
import '../courseGroup.css'
|
||||
|
||||
const getStatistic = (label, value) => (
|
||||
<Statistic className="groupStatistic">
|
||||
<Statistic.Label>{label}</Statistic.Label>
|
||||
<Statistic.Value>{value}</Statistic.Value>
|
||||
</Statistic>
|
||||
)
|
||||
|
||||
const Index = ({ totalStudents, totalCourses, totalCredits, totalTeachers, activeTeachers, isLoading }) => {
|
||||
if (isLoading) {
|
||||
const lineKeys = [1, 2, 3, 4, 5]
|
||||
return (
|
||||
<Placeholder>
|
||||
{lineKeys.map(k => (
|
||||
<Placeholder.Line key={k} length="full" />
|
||||
))}
|
||||
</Placeholder>
|
||||
)
|
||||
}
|
||||
let teacherAmount = totalTeachers
|
||||
let studentAmount = totalStudents
|
||||
let coursesAmount = totalCourses
|
||||
let creditAmount = totalCredits
|
||||
|
||||
if (activeTeachers.length > 0) {
|
||||
const accumulator = {
|
||||
credits: 0,
|
||||
students: 0,
|
||||
courses: 0,
|
||||
}
|
||||
|
||||
const filteredStatistics = activeTeachers.reduce((acc, cur) => {
|
||||
acc.credits += cur.credits
|
||||
acc.students += cur.students
|
||||
acc.courses += cur.courses
|
||||
return acc
|
||||
}, accumulator)
|
||||
|
||||
const { credits, students, courses } = filteredStatistics
|
||||
|
||||
teacherAmount = activeTeachers.length
|
||||
studentAmount = students
|
||||
creditAmount = credits
|
||||
coursesAmount = courses
|
||||
}
|
||||
|
||||
return (
|
||||
<Statistic.Group className="groupStatistics">
|
||||
{getStatistic('Total teachers', teacherAmount)}
|
||||
{getStatistic('Total students', studentAmount)}
|
||||
{getStatistic('Total courses', coursesAmount)}
|
||||
{getStatistic('Total credits', creditAmount)}
|
||||
</Statistic.Group>
|
||||
)
|
||||
}
|
||||
|
||||
Index.propTypes = {
|
||||
totalStudents: number,
|
||||
totalCourses: number,
|
||||
totalCredits: number,
|
||||
totalTeachers: number,
|
||||
activeTeachers: arrayOf(teacherType),
|
||||
isLoading: bool.isRequired,
|
||||
}
|
||||
|
||||
Index.defaultProps = {
|
||||
totalTeachers: undefined,
|
||||
totalStudents: undefined,
|
||||
totalCourses: undefined,
|
||||
totalCredits: undefined,
|
||||
activeTeachers: undefined,
|
||||
}
|
||||
|
||||
export default Index
|
||||
@@ -1,130 +0,0 @@
|
||||
import React, { Fragment, useState, useEffect } from 'react'
|
||||
import { Header, List, Button, Radio, Icon } from 'semantic-ui-react'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import { bool, func, arrayOf, number, shape } from 'prop-types'
|
||||
import { teacherType } from '../util'
|
||||
|
||||
import '../courseGroup.css'
|
||||
|
||||
const teacherColumnTypes = {
|
||||
NAME: 'name',
|
||||
CODE: 'code',
|
||||
COURSES: 'courses',
|
||||
CREDITS: 'credits',
|
||||
}
|
||||
|
||||
const TeacherItem = ({ teacher, isActive, handleFilterClick }) => {
|
||||
const { name, code, id, courses, credits } = teacher
|
||||
|
||||
return (
|
||||
<List.Item className={`${isActive ? 'teacherActiveItem' : ''}`}>
|
||||
<List.Content className="teacherItemStatistics">
|
||||
<div className="teacherItemBasicInfo">
|
||||
<div className="teacherName">{name}</div>
|
||||
<div className="teacherCode">{`(${code})`}</div>
|
||||
</div>
|
||||
<div className="teacherStatisticNumber">{courses}</div>
|
||||
<div className="teacherStatisticNumber">{credits}</div>
|
||||
<div className="statisticControlItem">
|
||||
<Button
|
||||
icon="filter"
|
||||
className={`${isActive ? 'activeIconButton' : 'iconButton'}`}
|
||||
onClick={() => handleFilterClick(id)}
|
||||
circular
|
||||
/>
|
||||
</div>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
)
|
||||
}
|
||||
|
||||
TeacherItem.propTypes = {
|
||||
handleFilterClick: func.isRequired,
|
||||
teacher: teacherType.isRequired,
|
||||
isActive: bool.isRequired,
|
||||
}
|
||||
|
||||
const Index = ({ teachers, showOnlyActiveTeachers, activeTeacherIds, handleFilterClick, handleActiveToggleChange }) => {
|
||||
const [viewableTeachers, setViewableTeachers] = useState([])
|
||||
const [sortColumn, setSortColumn] = useState(teacherColumnTypes.NAME)
|
||||
const [sortReverse, setSortReverse] = useState(false)
|
||||
const [activeTeacherCount, setActiveTeacherCount] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const activeTeachers = teachers.filter(t => activeTeacherIds.includes(t.id))
|
||||
setActiveTeacherCount(activeTeachers.length)
|
||||
setViewableTeachers(showOnlyActiveTeachers ? activeTeachers : teachers)
|
||||
})
|
||||
|
||||
const handleTeacherHeaderClick = columnName => {
|
||||
const isSortColumn = columnName === sortColumn
|
||||
setSortColumn(columnName)
|
||||
setSortReverse(isSortColumn ? !sortReverse : sortReverse)
|
||||
}
|
||||
|
||||
const renderListHeader = () => {
|
||||
const getHeader = (className, label, columnName) => {
|
||||
const isActive = columnName === sortColumn
|
||||
|
||||
return (
|
||||
<div className={className} onClick={() => handleTeacherHeaderClick(columnName)}>
|
||||
{label}
|
||||
{isActive ? <Icon name={`caret ${sortReverse ? 'down' : 'up'}`} /> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<List.Header>
|
||||
<List.Content className="courseHeaderContent">
|
||||
{getHeader('teacherHeaderName', 'Name', teacherColumnTypes.NAME)}
|
||||
{getHeader('teacherHeaderItem', 'Courses', teacherColumnTypes.COURSES)}
|
||||
{getHeader('teacherHeaderItem', 'Credits', teacherColumnTypes.CREDITS)}
|
||||
<div className="teacherHeaderFilter">Filter</div>
|
||||
</List.Content>
|
||||
</List.Header>
|
||||
)
|
||||
}
|
||||
|
||||
const toggleId = 'toggle'
|
||||
const sortedTeachers = sortBy(viewableTeachers, sortColumn)
|
||||
|
||||
if (sortReverse) {
|
||||
sortedTeachers.reverse()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header size="medium" className="headerWithControl">
|
||||
Teachers
|
||||
<div className="activeToggleContainer">
|
||||
<label htmlFor={toggleId}>Show only active</label>
|
||||
<Radio
|
||||
id={toggleId}
|
||||
toggle
|
||||
checked={showOnlyActiveTeachers}
|
||||
onChange={handleActiveToggleChange}
|
||||
disabled={activeTeacherCount === 0}
|
||||
/>
|
||||
</div>
|
||||
</Header>
|
||||
<List celled>
|
||||
{renderListHeader()}
|
||||
{sortedTeachers.map(t => {
|
||||
const isActive = activeTeacherIds.includes(t.id)
|
||||
return <TeacherItem key={t.id} teacher={t} isActive={isActive} handleFilterClick={handleFilterClick} />
|
||||
})}
|
||||
</List>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Index.propTypes = {
|
||||
teachers: arrayOf(shape({})).isRequired,
|
||||
activeTeacherIds: arrayOf(number).isRequired,
|
||||
handleFilterClick: func.isRequired,
|
||||
handleActiveToggleChange: func.isRequired,
|
||||
showOnlyActiveTeachers: bool.isRequired,
|
||||
}
|
||||
|
||||
export default Index
|
||||
@@ -1,69 +0,0 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import { Dropdown, Button } from 'semantic-ui-react'
|
||||
import { number, arrayOf, func } from 'prop-types'
|
||||
import { academicYearType } from './util'
|
||||
|
||||
import './courseGroup.css'
|
||||
|
||||
const AcademicYearFilter = ({ academicYears, semesterCode, handleSemesterCodeChangeFn }) => {
|
||||
if (!academicYears) {
|
||||
return null
|
||||
}
|
||||
|
||||
const academicYearSelectOptions = academicYears.map(ac => ({
|
||||
key: ac.semestercode,
|
||||
value: ac.semestercode,
|
||||
text: ac.yearname,
|
||||
}))
|
||||
|
||||
const semesterCodeDifference = 2
|
||||
const academicYearSelectId = 'academicYearSelect'
|
||||
const semesterCodes = academicYears.map(ac => ac.semestercode)
|
||||
const isMaxSemesterCode = semesterCode === Math.max(...semesterCodes)
|
||||
const isMinSemesterCode = semesterCode === Math.min(...semesterCodes)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="academicYearFilterContainer">
|
||||
<Button
|
||||
type="button"
|
||||
icon="angle left"
|
||||
className={isMinSemesterCode ? 'hiddenButton' : 'filterButton'}
|
||||
onClick={e => handleSemesterCodeChangeFn(e, { value: semesterCode - semesterCodeDifference })}
|
||||
/>
|
||||
<div className="academicYearSelectContainer">
|
||||
<Dropdown
|
||||
id={academicYearSelectId}
|
||||
search
|
||||
fluid
|
||||
selection
|
||||
options={academicYearSelectOptions}
|
||||
value={semesterCode}
|
||||
onChange={handleSemesterCodeChangeFn}
|
||||
selectOnBlur={false}
|
||||
selectOnNavigation={false}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
icon="angle right"
|
||||
className={isMaxSemesterCode ? 'hiddenButton' : 'filterButton'}
|
||||
onClick={e => handleSemesterCodeChangeFn(e, { value: semesterCode + semesterCodeDifference })}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AcademicYearFilter
|
||||
|
||||
AcademicYearFilter.propTypes = {
|
||||
semesterCode: number,
|
||||
academicYears: arrayOf(academicYearType),
|
||||
handleSemesterCodeChangeFn: func.isRequired,
|
||||
}
|
||||
|
||||
AcademicYearFilter.defaultProps = {
|
||||
semesterCode: undefined,
|
||||
academicYears: undefined,
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
.headerWithControl {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1em !important;
|
||||
}
|
||||
|
||||
.headerFilterContainer {
|
||||
font-size: 1.2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.headerIconButton,
|
||||
.iconButton {
|
||||
font-size: 1em !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.headerIconButton {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.activeIconButton {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.academicYearFilterContainer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
font-size: 0.65em;
|
||||
margin-top: 0.4em;
|
||||
}
|
||||
|
||||
.academicYearSelectContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.academicYearSelectContainer > label {
|
||||
color: var(--darkSilver);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hiddenButton,
|
||||
.filterButton {
|
||||
height: 1.9em;
|
||||
margin-right: 0 !important;
|
||||
background: none !important;
|
||||
padding: 0 !important;
|
||||
font-size: 1.5em !important;
|
||||
}
|
||||
|
||||
.groupStatistics {
|
||||
display: flex;
|
||||
justify-content: space-around !important;
|
||||
align-items: flex-end !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.groupStatistic {
|
||||
flex: 1 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.teacherActiveItem {
|
||||
background: var(--lightSilver);
|
||||
}
|
||||
|
||||
.courseInfoContent,
|
||||
.teacherItemStatistics,
|
||||
.courseHeaderContent {
|
||||
padding-top: 0.3em;
|
||||
padding-bottom: 0.3em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.courseHeaderContent {
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.courseHeaderContent > div {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.teacherName {
|
||||
font-weight: 700;
|
||||
font-size: 1.075em;
|
||||
}
|
||||
|
||||
.teacherCount {
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
|
||||
.teacherCount,
|
||||
.teacherCode {
|
||||
color: var(--darkSilver);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.teacherItemBasicInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 33%;
|
||||
}
|
||||
|
||||
.courseInfoItem,
|
||||
.courseInfoCode,
|
||||
.courseHeaderCode,
|
||||
.courseHeaderItem {
|
||||
flex: 0 0 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.courseHeaderCode,
|
||||
.courseInfoCode {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.courseInfoName,
|
||||
.courseInfoTeacher,
|
||||
.courseHeaderTeacher,
|
||||
.courseHeaderName,
|
||||
.teacherHeaderName {
|
||||
flex: 0 0 33%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.teacherHeaderFilter {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.statisticControlItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.teacherStatisticNumber,
|
||||
.teacherHeaderItem {
|
||||
flex: 0 0 20%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.teacherStatisticNumber {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
flex: 0 0 20%;
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activeToggleContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.activeToggleContainer > label {
|
||||
color: var(--darkSilver);
|
||||
font-size: 0.65em;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.hiddenButton {
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import { Segment, Header, Button, Placeholder } from 'semantic-ui-react'
|
||||
import { string, func, shape } from 'prop-types'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
|
||||
import { getCompiledPath } from '../../../common'
|
||||
import { callApi } from '../../../apiConnection'
|
||||
import Teachers from './Teachers'
|
||||
import Courses from './Courses'
|
||||
import Statistics from './Statistics'
|
||||
|
||||
import './courseGroup.css'
|
||||
|
||||
import AcademicYearFilter from './academicYearFilter'
|
||||
import { CG_API_BASE_PATH } from './util'
|
||||
|
||||
class CourseGroup extends Component {
|
||||
static propTypes = {
|
||||
groupId: string.isRequired,
|
||||
history: shape({
|
||||
push: func.isRequired,
|
||||
}).isRequired,
|
||||
studyProgrammeId: string.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
isLoading: true,
|
||||
teachers: [],
|
||||
activeTeacherIds: [],
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
try {
|
||||
const { groupId } = this.props
|
||||
const [courseGroup, academicYears] = await Promise.all([
|
||||
callApi(`${CG_API_BASE_PATH}/${groupId}`),
|
||||
callApi(`${CG_API_BASE_PATH}/academic-years`),
|
||||
])
|
||||
|
||||
const { name, totalCredits, totalStudents, totalCourses, teachers, semester } = courseGroup.data
|
||||
|
||||
this.setState({
|
||||
academicYears: academicYears.data,
|
||||
semesterCode: semester,
|
||||
name,
|
||||
totalCredits,
|
||||
totalStudents,
|
||||
totalCourses,
|
||||
teachers,
|
||||
isLoading: false,
|
||||
showOnlyActiveTeachers: false,
|
||||
})
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase() === 'network error') {
|
||||
window.location.reload(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSemesterCodeChange = (e, { value }) => {
|
||||
const { groupId } = this.props
|
||||
|
||||
this.setState({ semesterCode: value, isLoading: true }, () =>
|
||||
callApi(`${CG_API_BASE_PATH}/${groupId}/?semester=${value}`)
|
||||
.then(({ data }) => {
|
||||
const { totalCredits, totalStudents, totalCourses, teachers } = data
|
||||
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
totalCredits,
|
||||
totalStudents,
|
||||
totalCourses,
|
||||
teachers,
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.message.toLowerCase() === 'network error') {
|
||||
window.location.reload(true)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
handleTeacherActiveToggleChange = () => {
|
||||
const { showOnlyActiveTeachers } = this.state
|
||||
this.setState({ showOnlyActiveTeachers: !showOnlyActiveTeachers })
|
||||
}
|
||||
|
||||
handleTeacherFilterClick = teacherId => {
|
||||
const { showOnlyActiveTeachers, activeTeacherIds } = this.state
|
||||
this.setState({ isLoading: true }, () => {
|
||||
const isActiveTeacher = activeTeacherIds.includes(teacherId)
|
||||
|
||||
const newActiveTeachers = isActiveTeacher
|
||||
? activeTeacherIds.filter(id => id !== teacherId)
|
||||
: [...activeTeacherIds, teacherId]
|
||||
|
||||
const resetActiveTeachers = showOnlyActiveTeachers && newActiveTeachers.length === 0
|
||||
|
||||
this.setState({
|
||||
activeTeacherIds: newActiveTeachers,
|
||||
isLoading: false,
|
||||
showOnlyActiveTeachers: resetActiveTeachers ? false : showOnlyActiveTeachers,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
renderTeachersAndCourses = () => {
|
||||
const { teachers, isLoading, showOnlyActiveTeachers, semesterCode, activeTeacherIds } = this.state
|
||||
if (isLoading) {
|
||||
const lineKeys = [1, 2, 3, 4]
|
||||
return (
|
||||
<Placeholder>
|
||||
{lineKeys.map(k => (
|
||||
<Placeholder.Line key={k} length="full" />
|
||||
))}
|
||||
</Placeholder>
|
||||
)
|
||||
}
|
||||
|
||||
const getTeacherIds = teach => teach.map(t => t.id)
|
||||
|
||||
const hasActiveTeachers = activeTeacherIds.length > 0
|
||||
const teacherIds = hasActiveTeachers ? activeTeacherIds : getTeacherIds(teachers)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Teachers
|
||||
activeTeacherIds={activeTeacherIds}
|
||||
teachers={teachers}
|
||||
handleFilterClick={this.handleTeacherFilterClick}
|
||||
handleActiveToggleChange={this.handleTeacherActiveToggleChange}
|
||||
showOnlyActiveTeachers={showOnlyActiveTeachers}
|
||||
/>
|
||||
<Courses teacherIds={teacherIds} semesterCode={semesterCode} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { history, studyProgrammeId } = this.props
|
||||
const {
|
||||
totalStudents,
|
||||
totalCourses,
|
||||
totalCredits,
|
||||
teachers,
|
||||
activeTeacherIds,
|
||||
isLoading,
|
||||
name,
|
||||
semesterCode,
|
||||
academicYears,
|
||||
} = this.state
|
||||
|
||||
const navigateTo = programme =>
|
||||
history.push(
|
||||
getCompiledPath('/study-programme/:programme', {
|
||||
programme,
|
||||
})
|
||||
)
|
||||
const statisticsTeachers = teachers.filter(t => activeTeacherIds.includes(t.id))
|
||||
|
||||
return (
|
||||
<Segment loading={isLoading}>
|
||||
<Header size="medium" className="headerWithControl">
|
||||
<div className="headerFilterContainer">
|
||||
{name}
|
||||
<AcademicYearFilter
|
||||
semesterCode={semesterCode}
|
||||
academicYears={academicYears}
|
||||
handleSemesterCodeChangeFn={this.handleSemesterCodeChange}
|
||||
/>
|
||||
</div>
|
||||
<Button icon="reply" onClick={() => navigateTo(studyProgrammeId)} className="headerIconButton" />
|
||||
</Header>
|
||||
<Statistics
|
||||
isLoading={isLoading}
|
||||
totalCourses={totalCourses}
|
||||
totalTeachers={teachers.length}
|
||||
activeTeachers={statisticsTeachers}
|
||||
totalStudents={totalStudents}
|
||||
totalCredits={totalCredits}
|
||||
/>
|
||||
{this.renderTeachersAndCourses()}
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(CourseGroup)
|
||||
@@ -1,18 +0,0 @@
|
||||
import { bool, number, shape, string } from 'prop-types'
|
||||
|
||||
export const teacherType = shape({
|
||||
name: string,
|
||||
code: string,
|
||||
id: string,
|
||||
isActive: bool,
|
||||
courses: number,
|
||||
credits: number,
|
||||
students: number,
|
||||
})
|
||||
|
||||
export const academicYearType = shape({
|
||||
semestercode: number.isRequired,
|
||||
yearname: string.isRequired,
|
||||
})
|
||||
|
||||
export const CG_API_BASE_PATH = 'course-groups'
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import { Header, Search, Segment, Icon } from 'semantic-ui-react'
|
||||
import { func, arrayOf, object, string } from 'prop-types'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import './courseGroupAddTeacher.css'
|
||||
|
||||
import Timeout from '../../Timeout'
|
||||
import { findTeachers } from '../../../redux/teachers'
|
||||
import { callApi } from '../../../apiConnection'
|
||||
import SortableTable from '../../SortableTable'
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
courseGroup: undefined,
|
||||
isLoading: true,
|
||||
searchterm: '',
|
||||
displayResults: false,
|
||||
}
|
||||
|
||||
class CourseGroupAddTeacher extends Component {
|
||||
state = DEFAULT_STATE
|
||||
|
||||
componentDidMount() {
|
||||
callApi(`/course-groups/${this.props.groupId}`)
|
||||
.then(res => {
|
||||
this.setState({
|
||||
courseGroup: res.data,
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.message.toLowerCase() === 'network error') {
|
||||
window.location.reload(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
resetComponent = () => {
|
||||
this.setState(DEFAULT_STATE)
|
||||
}
|
||||
|
||||
fetchTeachers = searchterm => {
|
||||
if (searchterm.length <= 3 || (Number(searchterm) && searchterm.length < 6)) {
|
||||
return
|
||||
}
|
||||
this.props.setTimeout('fetch', () => {}, 250)
|
||||
this.props.findTeachers(searchterm).then(() => {
|
||||
this.setState({ displayResults: true })
|
||||
this.props.clearTimeout('fetch')
|
||||
})
|
||||
}
|
||||
|
||||
handleSearchChange = (e, { value }) => {
|
||||
this.props.clearTimeout('search')
|
||||
if (value.length > 0) {
|
||||
this.setState({ searchterm: value })
|
||||
this.props.setTimeout(
|
||||
'search',
|
||||
() => {
|
||||
this.fetchTeachers(value)
|
||||
},
|
||||
250
|
||||
)
|
||||
} else {
|
||||
this.resetComponent()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, courseGroup } = this.state
|
||||
|
||||
const searchResultColumns = [
|
||||
{ key: 'teacherid', title: 'Teacher ID', getRowVal: s => s.id, headerProps: { onClick: null, sorted: null } },
|
||||
{ key: 'username', title: 'Username', getRowVal: s => s.code, headerProps: { onClick: null, sorted: null } },
|
||||
{ key: 'name', title: 'Name', getRowVal: s => s.name, headerProps: { onClick: null, sorted: null, colSpan: 2 } },
|
||||
{
|
||||
key: 'icon',
|
||||
getRowContent: () => <Icon name="add user" />,
|
||||
cellProps: { collapsing: true },
|
||||
headerProps: { onClick: null, sorted: null },
|
||||
},
|
||||
]
|
||||
|
||||
const groupColumns = [
|
||||
{ key: 'Teacher ID', title: 'Teacher ID', getRowVal: t => t.id },
|
||||
{ key: 'Username', title: 'Username', getRowVal: t => t.code },
|
||||
{ key: 'Name', title: 'Name', getRowVal: t => t.name },
|
||||
{
|
||||
key: 'Remove',
|
||||
title: 'Remove',
|
||||
getRowVal: () => '',
|
||||
getCellProps: t => ({
|
||||
collapsing: true,
|
||||
icon: (
|
||||
<Icon
|
||||
name="remove user"
|
||||
link
|
||||
onClick={() => {
|
||||
this.setState({ isLoading: true })
|
||||
callApi(`/course-groups/${this.props.groupId}/remove/${t.id}`, 'post')
|
||||
.then(() => {
|
||||
callApi(`/course-groups/${this.props.groupId}`).then(res => {
|
||||
this.setState({
|
||||
courseGroup: res.data,
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.message.toLowerCase() === 'network error') {
|
||||
window.location.reload(true)
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
),
|
||||
textAlign: 'center',
|
||||
}),
|
||||
headerProps: { onClick: null, sorted: null },
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Segment loading={isLoading}>
|
||||
{!courseGroup ? null : (
|
||||
<>
|
||||
<Header size="large">{courseGroup.name}</Header>
|
||||
<Segment>
|
||||
<Header size="medium">Add teacher</Header>
|
||||
<Search
|
||||
input={{ fluid: true }}
|
||||
placeholder="Search by entering a username, id or name"
|
||||
value={this.state.searchterm}
|
||||
onSearchChange={this.handleSearchChange}
|
||||
showNoResults={false}
|
||||
/>
|
||||
{this.state.displayResults && (
|
||||
<>
|
||||
{this.props.teachers.length <= 0 ? (
|
||||
<div>No teachers matched your search</div>
|
||||
) : (
|
||||
<SortableTable
|
||||
getRowKey={s => s.id}
|
||||
getRowProps={teacher => ({
|
||||
className: 'clickable',
|
||||
onClick: () => {
|
||||
this.setState({ isLoading: true })
|
||||
callApi(`/course-groups/${this.props.groupId}/add/${teacher.id}`, 'post')
|
||||
.then(() => {
|
||||
callApi(`/course-groups/${this.props.groupId}`).then(res => {
|
||||
this.setState({
|
||||
courseGroup: res.data,
|
||||
isLoading: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.message.toLowerCase() === 'network error') {
|
||||
window.location.reload(true)
|
||||
}
|
||||
})
|
||||
},
|
||||
})}
|
||||
tableProps={{ celled: false, sortable: false }}
|
||||
columns={searchResultColumns}
|
||||
data={this.props.teachers}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Segment>
|
||||
<Segment>
|
||||
<Header size="medium">Teachers in group</Header>
|
||||
<SortableTable getRowKey={gc => gc.id} columns={groupColumns} data={courseGroup.teachers} />
|
||||
</Segment>
|
||||
</>
|
||||
)}
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CourseGroupAddTeacher.propTypes = {
|
||||
groupId: string.isRequired,
|
||||
setTimeout: func.isRequired,
|
||||
clearTimeout: func.isRequired,
|
||||
teachers: arrayOf(object).isRequired,
|
||||
findTeachers: func.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ teachers }) => {
|
||||
const { list } = teachers
|
||||
return {
|
||||
teachers: list,
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps, { findTeachers })(Timeout(CourseGroupAddTeacher)))
|
||||
@@ -122,6 +122,8 @@ const CoursePopulation = ({
|
||||
}, [semesters])
|
||||
|
||||
const header = courseData ? `${getTextIn(courseData.name, language)} ${headerYears}` : null
|
||||
const coursecode = courseData ? courseData.alternatives : []
|
||||
|
||||
const subHeader = codes.join(', ')
|
||||
|
||||
if (!dateFrom || !dateTo) return null
|
||||
@@ -259,6 +261,7 @@ const CoursePopulation = ({
|
||||
filteredStudents={studentData.students.filter(stu => selectedStudents.includes(stu.studentNumber))}
|
||||
coursePopulation
|
||||
language={language}
|
||||
coursecode={coursecode}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { orderBy } from 'lodash'
|
||||
|
||||
// handle special case where the course's actual code starts with "A" so it is mistakenly taken as Open Uni course
|
||||
const isAvoin = code => !!code.match(/^[A][0-9]|^AY/)
|
||||
// const isAvoin = code => !!code.match(/^[A][0-9]|^AY/)
|
||||
const isAvoinTwo = course => course.organizations.some(c => c === 'hy-org-48645785')
|
||||
|
||||
const sortAlternatives = alternatives => {
|
||||
const result = orderBy(
|
||||
@@ -30,7 +31,7 @@ const filterCourseSearchResults = (courses, unifyOpenUniCourses) => {
|
||||
const mergedCourses = {}
|
||||
const sortedCourses = sortAlternatives(courses)
|
||||
sortedCourses.forEach(course => {
|
||||
const groupId = isAvoin(course.code) && !unifyOpenUniCourses ? course.code : course.subsId
|
||||
const groupId = isAvoinTwo(course) && !unifyOpenUniCourses ? course.code : course.subsId
|
||||
|
||||
if (!(course.max_attainment_date && course.min_attainment_date)) {
|
||||
return
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { arrayOf, shape, number, func } from 'prop-types'
|
||||
import { calculateTotalPassedCourses, calculateTotalFailedCourses } from '../facultyUtils'
|
||||
import { getTextIn } from '../../../common'
|
||||
import SortableTable from '../../SortableTable'
|
||||
import FacultyStatsGraph from '../FacultyStatsGraph'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
|
||||
const FacultySelector = ({ faculties, facultyYearlyStats, fromYear, toYear, handleSelect }) => {
|
||||
const { language } = useLanguage()
|
||||
const calculateYearlyStatsForFaculty = facultyCode => {
|
||||
const res = {}
|
||||
const yearData = {
|
||||
studentCredits: 0,
|
||||
coursesPassed: 0,
|
||||
coursesFailed: 0,
|
||||
students: [],
|
||||
}
|
||||
|
||||
const faculty = facultyYearlyStats.find(f => f.id === facultyCode)
|
||||
if (!faculty) return res
|
||||
Object.values(faculty.data).forEach(programmeYearlyStats => {
|
||||
Object.entries(programmeYearlyStats).forEach(([year, stat]) => {
|
||||
if (!res[year]) res[year] = { ...yearData }
|
||||
res[year].studentCredits += stat.studentCredits
|
||||
res[year].coursesPassed += stat.coursesPassed
|
||||
res[year].coursesFailed += stat.coursesFailed
|
||||
res[year].students = [...new Set(res[year].students.concat(stat.studentArray))]
|
||||
})
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const calculateTotalStatsForFaculty = facultyCode => {
|
||||
const initial = {
|
||||
totalStudentCredits: 0,
|
||||
totalCoursesPassed: 0,
|
||||
totalCoursesFailed: 0,
|
||||
totalStudents: [],
|
||||
}
|
||||
return Object.entries(calculateYearlyStatsForFaculty(facultyCode))
|
||||
.filter(([year]) => year >= fromYear && year <= toYear)
|
||||
.reduce(
|
||||
(res, [, curr]) => ({
|
||||
totalStudentCredits: res.totalStudentCredits + curr.studentCredits,
|
||||
totalCoursesPassed: res.totalCoursesPassed + curr.coursesPassed,
|
||||
totalCoursesFailed: res.totalCoursesFailed + curr.coursesFailed,
|
||||
totalStudents: [...new Set(res.totalStudents.concat(curr.students))],
|
||||
}),
|
||||
{ ...initial }
|
||||
)
|
||||
}
|
||||
|
||||
const totalStats = useMemo(
|
||||
() =>
|
||||
faculties.reduce((res, { code }) => {
|
||||
res[code] = calculateTotalStatsForFaculty(code)
|
||||
return { ...res }
|
||||
}, {}),
|
||||
[fromYear, toYear]
|
||||
)
|
||||
|
||||
const graphData = useMemo(
|
||||
() =>
|
||||
faculties.map(({ code, name }) => ({
|
||||
name: getTextIn(name, language),
|
||||
data: calculateYearlyStatsForFaculty(code),
|
||||
})),
|
||||
[]
|
||||
)
|
||||
|
||||
const headers = [
|
||||
{
|
||||
key: 'name',
|
||||
title: 'name',
|
||||
getRowVal: faculty => getTextIn(faculty.name, language),
|
||||
},
|
||||
{
|
||||
key: 'code',
|
||||
title: 'code',
|
||||
getRowVal: faculty => faculty.code,
|
||||
},
|
||||
{
|
||||
key: 'studentCredits',
|
||||
title: 'Student credits',
|
||||
getRowVal: ({ code }) => Math.round(totalStats[code].totalStudentCredits),
|
||||
},
|
||||
{
|
||||
key: 'coursesPassed',
|
||||
title: 'Courses passed',
|
||||
getRowVal: ({ code }) => calculateTotalPassedCourses(totalStats[code]),
|
||||
getRowContent: ({ code }) => `${calculateTotalPassedCourses(totalStats[code]).toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
key: 'coursesFailed',
|
||||
title: 'Courses failed',
|
||||
getRowVal: ({ code }) => calculateTotalFailedCourses(totalStats[code]),
|
||||
getRowContent: ({ code }) => `${calculateTotalFailedCourses(totalStats[code]).toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
key: 'students',
|
||||
title: 'Students',
|
||||
getRowVal: ({ code }) => totalStats[code].totalStudents.length,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<SortableTable
|
||||
columns={headers}
|
||||
getRowKey={faculty => faculty.code}
|
||||
getRowProps={faculty => ({ onClick: () => handleSelect(faculty.code), style: { cursor: 'pointer' } })}
|
||||
data={faculties}
|
||||
/>
|
||||
<FacultyStatsGraph data={graphData} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
FacultySelector.propTypes = {
|
||||
faculties: arrayOf(shape({})).isRequired,
|
||||
facultyYearlyStats: arrayOf(shape({})).isRequired,
|
||||
fromYear: number.isRequired,
|
||||
toYear: number.isRequired,
|
||||
handleSelect: func.isRequired,
|
||||
}
|
||||
|
||||
export default FacultySelector
|
||||
@@ -1,119 +0,0 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { string, arrayOf, shape, number } from 'prop-types'
|
||||
import { Segment, Icon, Header, Item } from 'semantic-ui-react'
|
||||
import { getTextIn } from '../../../common'
|
||||
import { calculateStatsForProgramme, calculateTotalPassedCourses, calculateTotalFailedCourses } from '../facultyUtils'
|
||||
import SortableTable from '../../SortableTable'
|
||||
import FacultyStatsGraph from '../FacultyStatsGraph'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
|
||||
const ShowProgrammeOverView = ({ code }) => {
|
||||
return (
|
||||
<Item as={Link} title={`Overview of study programme ${code}`} to={`/study-programme/${code}`}>
|
||||
<Icon name="level up alternate" />
|
||||
</Item>
|
||||
)
|
||||
}
|
||||
|
||||
ShowProgrammeOverView.propTypes = {
|
||||
code: string.isRequired,
|
||||
}
|
||||
|
||||
const FacultyStats = ({ facultyProgrammes, selectedFacultyProgrammesStats, fromYear, toYear }) => {
|
||||
const { language } = useLanguage()
|
||||
const totalStats = useMemo(
|
||||
() =>
|
||||
Object.entries(selectedFacultyProgrammesStats).reduce((res, [code, stats]) => {
|
||||
res[code] = calculateStatsForProgramme(stats, fromYear, toYear)
|
||||
return { ...res }
|
||||
}, {}),
|
||||
[fromYear, toYear]
|
||||
)
|
||||
|
||||
const getNameOfProgramme = code => {
|
||||
const foundProgramme = facultyProgrammes.find(p => p.code === code)
|
||||
return foundProgramme ? getTextIn(foundProgramme.name, language) : code
|
||||
}
|
||||
|
||||
const graphData = useMemo(
|
||||
() =>
|
||||
Object.entries(selectedFacultyProgrammesStats).map(([code, data]) => ({ name: getNameOfProgramme(code), data })),
|
||||
[]
|
||||
)
|
||||
|
||||
if (!Object.keys(selectedFacultyProgrammesStats).length) {
|
||||
return <Segment textAlign="center">No data</Segment>
|
||||
}
|
||||
|
||||
/* eslint-disable react/prop-types */
|
||||
const headers = [
|
||||
{
|
||||
key: 'name',
|
||||
title: 'name',
|
||||
getRowVal: ({ code }) => code,
|
||||
getRowContent: ({ code }) => (
|
||||
<div>
|
||||
{' '}
|
||||
{getNameOfProgramme(code)} <ShowProgrammeOverView code={code} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'code',
|
||||
title: 'code',
|
||||
getRowVal: ({ code }) => code,
|
||||
},
|
||||
{
|
||||
key: 'studentCredits',
|
||||
title: 'Student credits',
|
||||
getRowVal: ({ code }) => Math.round(totalStats[code].totalStudentCredits),
|
||||
},
|
||||
{
|
||||
key: 'coursesPassed',
|
||||
title: 'Courses passed',
|
||||
getRowVal: ({ code }) => calculateTotalPassedCourses(totalStats[code]),
|
||||
getRowContent: ({ code }) => `${calculateTotalPassedCourses(totalStats[code]).toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
key: 'coursesFailed',
|
||||
title: 'Courses failed',
|
||||
getRowVal: ({ code }) => calculateTotalFailedCourses(totalStats[code]),
|
||||
getRowContent: ({ code }) => `${calculateTotalFailedCourses(totalStats[code]).toFixed(2)}%`,
|
||||
},
|
||||
{
|
||||
key: 'students',
|
||||
title: 'Students',
|
||||
getRowVal: ({ code }) => totalStats[code].totalStudents.length,
|
||||
},
|
||||
]
|
||||
/* eslint-enable react/prop-types */
|
||||
|
||||
const data = Object.entries(selectedFacultyProgrammesStats).map(([code, stats]) => ({ code, stats }))
|
||||
const bachelors = data.filter(programme => programme.code.includes('KH'))
|
||||
const masters = data.filter(programme => programme.code.includes('MH'))
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>Bachelor degrees</Header>
|
||||
<SortableTable columns={headers} getRowKey={({ code }) => code} data={bachelors} />
|
||||
<Header>Masters degrees</Header>
|
||||
<SortableTable columns={headers} getRowKey={({ code }) => code} data={masters} />
|
||||
<FacultyStatsGraph data={graphData} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
FacultyStats.propTypes = {
|
||||
facultyProgrammes: arrayOf(shape({})).isRequired,
|
||||
selectedFacultyProgrammesStats: shape({}).isRequired,
|
||||
fromYear: number.isRequired,
|
||||
toYear: number.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ faculties }) => ({
|
||||
facultyProgrammes: faculties.facultyProgrammes,
|
||||
})
|
||||
|
||||
export default connect(mapStateToProps)(FacultyStats)
|
||||
@@ -1,88 +0,0 @@
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { arrayOf, string, number, shape } from 'prop-types'
|
||||
import { Segment, Button } from 'semantic-ui-react'
|
||||
import Highcharts from 'highcharts/highstock'
|
||||
import ReactHighstock from 'react-highcharts/ReactHighstock'
|
||||
|
||||
const FacultyStatsGraph = ({ data }) => {
|
||||
const [mode, setMode] = useState('studentCredits')
|
||||
const defaultStartTimestamp = new Date(2000, 0, 1).getTime()
|
||||
|
||||
const series = useMemo(
|
||||
() =>
|
||||
data.map(({ name, data: entryData }) => ({
|
||||
name,
|
||||
data: Object.entries(entryData).map(([year, stats]) => ({
|
||||
x: new Date(parseInt(year, 10), 0, 1, 0, 0, 0, 0).getTime(),
|
||||
y: Math.round(
|
||||
['coursesPassed', 'coursesFailed'].includes(mode)
|
||||
? (stats[mode] / (stats.coursesPassed + stats.coursesFailed)) * 100
|
||||
: stats[mode].length || stats[mode]
|
||||
),
|
||||
})),
|
||||
})),
|
||||
[data, mode]
|
||||
)
|
||||
|
||||
const { min, max } = useMemo(
|
||||
() =>
|
||||
series.reduce(
|
||||
(res, { data }) => {
|
||||
data.forEach(({ x }) => {
|
||||
res.min = Math.min(res.min, x)
|
||||
res.max = Math.max(res.max, x)
|
||||
})
|
||||
return res
|
||||
},
|
||||
{ min: Infinity, max: -Infinity }
|
||||
),
|
||||
[series]
|
||||
)
|
||||
|
||||
const options = {
|
||||
findNearestPointBy: 'xy',
|
||||
series,
|
||||
xAxis: {
|
||||
min: Math.max(min, defaultStartTimestamp),
|
||||
max,
|
||||
ordinal: false,
|
||||
},
|
||||
}
|
||||
|
||||
const handleClick = (_, { name }) => setMode(name)
|
||||
|
||||
return (
|
||||
<Segment>
|
||||
<ReactHighstock highcharts={Highcharts} constructorType="stockChart" config={options} />
|
||||
<Button active={mode === 'studentCredits'} onClick={handleClick} name="studentCredits">
|
||||
Total credits
|
||||
</Button>
|
||||
<Button active={mode === 'coursesPassed'} onClick={handleClick} name="coursesPassed">
|
||||
% of courses passed
|
||||
</Button>
|
||||
<Button active={mode === 'coursesFailed'} onClick={handleClick} name="coursesFailed">
|
||||
% of courses failed
|
||||
</Button>
|
||||
<Button active={mode === 'students'} onClick={handleClick} name="students">
|
||||
Amount of students
|
||||
</Button>
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
|
||||
FacultyStatsGraph.propTypes = {
|
||||
data: arrayOf(
|
||||
shape({
|
||||
name: string,
|
||||
data: shape({
|
||||
year: shape({
|
||||
studentCredits: number,
|
||||
coursesPassed: number,
|
||||
coursesFailed: number,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
).isRequired,
|
||||
}
|
||||
|
||||
export default FacultyStatsGraph
|
||||
@@ -1,30 +0,0 @@
|
||||
export const calculateStatsForProgramme = (stats, fromYear, toYear) => {
|
||||
const res = {
|
||||
totalStudentCredits: 0,
|
||||
totalCoursesPassed: 0,
|
||||
totalCoursesFailed: 0,
|
||||
totalStudents: [],
|
||||
}
|
||||
|
||||
Object.entries(stats)
|
||||
.filter(([year]) => year >= fromYear && year <= toYear)
|
||||
.forEach(([, stat]) => {
|
||||
const { studentCredits, coursesPassed, coursesFailed, studentArray } = stat
|
||||
res.totalStudentCredits += studentCredits
|
||||
res.totalCoursesPassed += coursesPassed
|
||||
res.totalCoursesFailed += coursesFailed
|
||||
res.totalStudents = [...new Set(res.totalStudents.concat(studentArray))]
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
export const calculateTotalPassedCourses = ({ totalCoursesPassed, totalCoursesFailed }) => {
|
||||
const ratio = (totalCoursesPassed / (totalCoursesPassed + totalCoursesFailed)) * 100
|
||||
return Number.isNaN(ratio) ? 0 : ratio
|
||||
}
|
||||
|
||||
export const calculateTotalFailedCourses = ({ totalCoursesPassed, totalCoursesFailed }) => {
|
||||
const totalPassedRatio = calculateTotalPassedCourses({ totalCoursesPassed, totalCoursesFailed })
|
||||
if (totalPassedRatio === 0) return 0
|
||||
return 100 - totalPassedRatio
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { Header, Segment, Form } from 'semantic-ui-react'
|
||||
import { uniq } from 'lodash'
|
||||
import { arrayOf, shape, func, bool } from 'prop-types'
|
||||
import { getTextIn } from '../../common'
|
||||
import { useTitle } from '../../common/hooks'
|
||||
import { getUserFaculties, getFacultiesYearlyStats, getFacultyProgrammes } from '../../redux/faculties'
|
||||
import YearFilter from '../CourseStatistics/SearchForm/YearFilter'
|
||||
import FacultySelector from './FacultySelector'
|
||||
import FacultyStats from './FacultyStats'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
|
||||
const Faculty = ({
|
||||
getUserFaculties,
|
||||
getFacultiesYearlyStats,
|
||||
getFacultyProgrammes,
|
||||
faculties,
|
||||
pending,
|
||||
error,
|
||||
facultyYearlyStats,
|
||||
history,
|
||||
match,
|
||||
}) => {
|
||||
const { language } = useLanguage()
|
||||
const [selectedFaculty, setSelectedFaculty] = useState(null)
|
||||
const [fromYear, setFromYear] = useState(-1)
|
||||
const [toYear, setToYear] = useState(-1)
|
||||
const [years, setYears] = useState([])
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
useTitle('Faculties')
|
||||
|
||||
const facultyCodes = faculties.map(({ code }) => code)
|
||||
const selectedFacultyProgrammesStats = facultyYearlyStats.find(({ id }) => id === selectedFaculty)
|
||||
const hasLoaded = faculties.length > 0 && facultyYearlyStats.length > 0
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFaculty) {
|
||||
history.push(`/faculties/${selectedFaculty}`)
|
||||
}
|
||||
}, [selectedFaculty])
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
params: { facultyid },
|
||||
} = match
|
||||
if (facultyid) setSelectedFaculty(facultyid)
|
||||
else setSelectedFaculty(null)
|
||||
}, [history.location.pathname])
|
||||
|
||||
useEffect(() => {
|
||||
getUserFaculties()
|
||||
getFacultiesYearlyStats()
|
||||
getFacultyProgrammes()
|
||||
setInitialized(true)
|
||||
}, [])
|
||||
|
||||
const getYearFilterData = () => {
|
||||
const filterYears = uniq(
|
||||
facultyYearlyStats
|
||||
.filter(f => facultyCodes.includes(f.id))
|
||||
.map(({ data }) => Object.values(data).reduce((res, curr) => [...res, ...Object.keys(curr)], []))
|
||||
.reduce((acc, curr) => [...acc, ...curr], [])
|
||||
)
|
||||
.sort((a, b) => b - a)
|
||||
.map(year => parseInt(year, 10))
|
||||
|
||||
return {
|
||||
fromYear: filterYears[filterYears.length - 1],
|
||||
toYear: filterYears[0],
|
||||
years: filterYears.map(y => ({ key: y, text: y, value: y })),
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (faculties.length && facultyYearlyStats.length) {
|
||||
const { fromYear: newFromYear, toYear: newToYear, years: newYears } = getYearFilterData()
|
||||
if (fromYear < newFromYear || fromYear > newToYear) setFromYear(Math.max(newFromYear, 2000))
|
||||
if (toYear < newFromYear || toYear > newToYear) setToYear(newToYear)
|
||||
setYears(newYears)
|
||||
}
|
||||
}, [faculties, facultyYearlyStats, selectedFaculty])
|
||||
|
||||
const handleYearChange = (e, target) => {
|
||||
const { name, value } = target
|
||||
if (name === 'fromYear') setFromYear(value)
|
||||
else setToYear(value)
|
||||
}
|
||||
|
||||
const getTitle = () =>
|
||||
selectedFaculty && hasLoaded
|
||||
? getTextIn(faculties.find(({ code }) => code === selectedFaculty).name, language)
|
||||
: 'Faculties'
|
||||
|
||||
if (!(faculties.length || pending) && initialized) {
|
||||
return !error ? (
|
||||
<Header textAlign="center" content="No access to any faculties!" as="h1" />
|
||||
) : (
|
||||
<Header textAlign="center" content="Not authorized for faculties!" as="h1" />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="segmentContainer">
|
||||
<Header className="segmentTitle" size="large">
|
||||
{hasLoaded && getTitle()}
|
||||
</Header>
|
||||
<Segment className="contentSegment" loading={!hasLoaded}>
|
||||
{!hasLoaded && null}
|
||||
{hasLoaded && (
|
||||
<>
|
||||
<Segment>
|
||||
<Form>
|
||||
<Header content="Filter by time range" as="h4" />
|
||||
<YearFilter fromYear={fromYear} toYear={toYear} years={years} handleChange={handleYearChange} />
|
||||
</Form>
|
||||
</Segment>
|
||||
{!selectedFaculty ? (
|
||||
<FacultySelector
|
||||
faculties={faculties}
|
||||
facultyYearlyStats={facultyYearlyStats}
|
||||
fromYear={fromYear}
|
||||
toYear={toYear}
|
||||
handleSelect={setSelectedFaculty}
|
||||
/>
|
||||
) : (
|
||||
<FacultyStats
|
||||
selectedFacultyProgrammesStats={
|
||||
selectedFacultyProgrammesStats ? selectedFacultyProgrammesStats.data : {}
|
||||
}
|
||||
history={history}
|
||||
fromYear={fromYear}
|
||||
toYear={toYear}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Segment>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Faculty.propTypes = {
|
||||
getUserFaculties: func.isRequired,
|
||||
getFacultiesYearlyStats: func.isRequired,
|
||||
getFacultyProgrammes: func.isRequired,
|
||||
faculties: arrayOf(shape({})).isRequired,
|
||||
facultyYearlyStats: arrayOf(shape({})).isRequired,
|
||||
history: shape({}).isRequired,
|
||||
match: shape({}).isRequired,
|
||||
pending: bool.isRequired,
|
||||
error: bool.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ faculties }) => ({
|
||||
faculties: faculties.data,
|
||||
facultyYearlyStats: faculties.yearlyStats,
|
||||
pending: faculties.userFacultiesPending,
|
||||
error: faculties.userFacultiesError,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = {
|
||||
getUserFaculties,
|
||||
getFacultiesYearlyStats,
|
||||
getFacultyProgrammes,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Faculty))
|
||||
@@ -11,6 +11,7 @@ import Grade from './filters/Grade'
|
||||
import Date from './filters/Date'
|
||||
import Age from './filters/Age'
|
||||
import AdmissionType from './filters/AdmissionType'
|
||||
import Tags from './filters/Tags'
|
||||
|
||||
export const PopulationStatisticsFilters = () => {
|
||||
const analytics = useAnalytics()
|
||||
@@ -30,6 +31,7 @@ export const PopulationStatisticsFilters = () => {
|
||||
<StartYearAtUni />
|
||||
<AdmissionType />
|
||||
<Courses />
|
||||
<Tags />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Form, Dropdown } from 'semantic-ui-react'
|
||||
|
||||
import ClearFilterButton from './common/ClearFilterButton'
|
||||
import FilterCard from './common/FilterCard'
|
||||
import useFilters from '../useFilters'
|
||||
import useAnalytics from '../useAnalytics'
|
||||
|
||||
export default () => {
|
||||
const { addFilter, removeFilter, withoutFilter, activeFilters } = useFilters()
|
||||
const analytics = useAnalytics()
|
||||
|
||||
const [value, setValue] = useState([])
|
||||
const name = 'tagsFilter'
|
||||
const isActive = () => value.length > 0
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive()) {
|
||||
removeFilter(name)
|
||||
analytics.clearFilter(name)
|
||||
} else {
|
||||
addFilter(name, student =>
|
||||
value.some(tag => {
|
||||
const studentTags = student.tags ? student.tags.map(studentTag => studentTag.tag.tagname) : []
|
||||
return studentTags.includes(tag)
|
||||
})
|
||||
)
|
||||
analytics.setFilter(name, value.join(', '))
|
||||
}
|
||||
}, [value])
|
||||
|
||||
const countsByTag = {}
|
||||
withoutFilter(name).forEach(student => {
|
||||
student.tags.forEach(tag => {
|
||||
const name = tag.tag.tagname
|
||||
countsByTag[name] = countsByTag[name] ? countsByTag[name] + 1 : 1
|
||||
})
|
||||
})
|
||||
|
||||
const options = Object.keys(countsByTag).map(tag => ({
|
||||
key: `tag-${tag}`,
|
||||
text: `${tag} (${countsByTag[tag]})`,
|
||||
value: tag,
|
||||
}))
|
||||
|
||||
return (
|
||||
<FilterCard
|
||||
title="Tags"
|
||||
contextKey="tagsFilter"
|
||||
footer={<ClearFilterButton disabled={!isActive()} onClick={() => setValue([])} name={name} />}
|
||||
active={Object.keys(activeFilters).includes(name)}
|
||||
name={name}
|
||||
>
|
||||
<div className="card-content">
|
||||
<Form>
|
||||
<Dropdown
|
||||
multiple
|
||||
selection
|
||||
fluid
|
||||
options={options}
|
||||
button
|
||||
className="mini"
|
||||
placeholder="Choose Tags to include"
|
||||
onChange={(_, { value: inputValue }) => setValue(inputValue)}
|
||||
value={value}
|
||||
data-cy={`${name}-dropdown`}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
</FilterCard>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import propTypes from 'prop-types'
|
||||
import { isEqual } from 'lodash'
|
||||
import { images, getUserRoles, checkUserAccess } from '../common'
|
||||
import { useTitle } from '../common/hooks'
|
||||
import { builtAt } from '../conf'
|
||||
|
||||
const FrontPage = props => {
|
||||
const { userRoles, rights } = props
|
||||
@@ -72,7 +73,7 @@ const FrontPage = props => {
|
||||
</p>
|
||||
|
||||
<Divider section />
|
||||
<p>Oodikone was last updated on: {moment(process.env.BUILT_AT).toDate().toLocaleString()}</p>
|
||||
{builtAt ? <p>Oodikone was last updated on: {moment(builtAt).toDate().toLocaleString()}</p> : null}
|
||||
</Container>
|
||||
<Image src={images.toskaLogo} size="medium" centered style={{ bottom: 0 }} />
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,6 @@ const allNavigationItems = {
|
||||
teachers: { path: '/teachers', key: 'teachers', label: 'Teachers', reqRights: ['teachers'] },
|
||||
users: { path: '/users', key: 'users', label: 'Users', reqRights: ['users'] },
|
||||
trends: { path: '/trends', key: 'trends', label: 'Trends' },
|
||||
faculty: { path: '/faculties', key: 'faculty', label: 'Faculty', reqRights: ['faculties'] },
|
||||
updater: { path: '/updater', key: 'updater', label: 'Updater', reqRights: ['dev', 'admin'] },
|
||||
sandbox: { path: '/sandbox', key: 'sandbox', label: 'Sandbox', reqRights: ['dev'] },
|
||||
feedback: { path: '/feedback', key: 'feedback', label: 'Give feedback' },
|
||||
|
||||
@@ -28,6 +28,7 @@ const GeneralTab = ({
|
||||
queryStudyrights,
|
||||
isAdmin,
|
||||
studentToTargetCourseDateMap,
|
||||
coursecode,
|
||||
}) => {
|
||||
const { language } = useLanguage()
|
||||
const { filteredStudents } = useFilters()
|
||||
@@ -246,6 +247,21 @@ const GeneralTab = ({
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (coursecode) {
|
||||
columns.push({
|
||||
key: 'grade for single course',
|
||||
title: 'Grade',
|
||||
getRowVal: s => {
|
||||
const grade = s.courses.filter(c => coursecode.includes(c.course_code))
|
||||
if (grade) {
|
||||
return grade[grade.length - 1].grade
|
||||
}
|
||||
return ''
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
columns.push({
|
||||
key: 'all credits',
|
||||
title: 'All Credits',
|
||||
@@ -419,6 +435,7 @@ GeneralTab.defaultProps = {
|
||||
studentToTargetCourseDateMap: null,
|
||||
customPopulation: false,
|
||||
coursePopulation: false,
|
||||
coursecode: '',
|
||||
}
|
||||
|
||||
GeneralTab.propTypes = {
|
||||
@@ -429,6 +446,7 @@ GeneralTab.propTypes = {
|
||||
queryStudyrights: arrayOf(string).isRequired,
|
||||
isAdmin: bool.isRequired,
|
||||
studentToTargetCourseDateMap: shape({}),
|
||||
coursecode: string,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
|
||||
@@ -265,7 +265,7 @@ class PopulationStudents extends Component {
|
||||
const mandatoryCourseData = [totals, ...this.props.filteredStudents]
|
||||
|
||||
// FIXME: here only for refactorment
|
||||
const { showNames, studentToTargetCourseDateMap } = this.props
|
||||
const { showNames, studentToTargetCourseDateMap, coursecode } = this.props
|
||||
const panes = [
|
||||
{
|
||||
menuItem: 'General',
|
||||
@@ -276,6 +276,7 @@ class PopulationStudents extends Component {
|
||||
coursePopulation={coursePopulation}
|
||||
customPopulation={customPopulation}
|
||||
studentToTargetCourseDateMap={studentToTargetCourseDateMap}
|
||||
coursecode={coursecode}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
),
|
||||
@@ -408,6 +409,7 @@ PopulationStudents.defaultProps = {
|
||||
customPopulation: false,
|
||||
coursePopulation: false,
|
||||
dataExport: null,
|
||||
coursecode: '',
|
||||
}
|
||||
|
||||
PopulationStudents.propTypes = {
|
||||
@@ -431,6 +433,7 @@ PopulationStudents.propTypes = {
|
||||
customPopulation: bool,
|
||||
filteredStudents: arrayOf(shape({})).isRequired,
|
||||
dataExport: node,
|
||||
coursecode: string,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
|
||||
@@ -34,7 +34,6 @@ const StudyProgramme = React.lazy(() => retry(() => import('../StudyProgramme'))
|
||||
const Teachers = React.lazy(() => retry(() => import('../Teachers')))
|
||||
const Sandbox = React.lazy(() => retry(() => import('../Sandbox')))
|
||||
const Feedback = React.lazy(() => retry(() => import('../Feedback')))
|
||||
const Faculty = React.lazy(() => retry(() => import('../Faculty')))
|
||||
const CoursePopulation = React.lazy(() => retry(() => import('../CoursePopulation')))
|
||||
const CustomPopulation = React.lazy(() => retry(() => import('../CustomPopulation')))
|
||||
const Updater = React.lazy(() => retry(() => import('../Updater')))
|
||||
@@ -45,7 +44,6 @@ const routes = {
|
||||
courseStatistics: '/coursestatistics',
|
||||
teachers: '/teachers/:teacherid?',
|
||||
users: '/users/:userid?',
|
||||
faculty: '/faculties/:facultyid?',
|
||||
sandbox: '/sandbox',
|
||||
feedback: '/feedback',
|
||||
coursepopulation: '/coursepopulation',
|
||||
@@ -60,10 +58,8 @@ const Routes = () => (
|
||||
<Route exact path="/" component={FrontPage} />
|
||||
<Route exact path="/populations" component={Populations} />
|
||||
<Route exact path="/study-programme/:studyProgrammeId?" component={StudyProgramme} />
|
||||
<Route exact path="/study-programme/:studyProgrammeId/course-group/:courseGroupId" component={StudyProgramme} />
|
||||
<Route exact path={routes.students} component={StudentStatistics} />
|
||||
<Route exact path={routes.courseStatistics} component={CourseStatistics} />
|
||||
<Route exact path={routes.faculty} component={Faculty} />
|
||||
<Route exact path={routes.users} component={EnableUsers} />
|
||||
<Route exact path={routes.teachers} component={Teachers} />
|
||||
<Route exact path={routes.sandbox} component={Sandbox} />
|
||||
|
||||
@@ -110,6 +110,8 @@ const ThroughputTable = ({
|
||||
const CANCELLED_FEATURE_TOGGLED_ON = userRoles.includes('admin')
|
||||
|
||||
const genders = data.length > 0 ? uniq(flatten(data.map(year => Object.keys(year.genders)))) : []
|
||||
genders.sort()
|
||||
|
||||
const renderGenders = genders.length > 0
|
||||
|
||||
const calculateTotalNationalities = () =>
|
||||
@@ -265,7 +267,7 @@ const ThroughputTable = ({
|
||||
|
||||
<Table.Row>
|
||||
{renderGenders || renderRatioOfFinns ? <Table.HeaderCell content="Total" /> : null}
|
||||
{[...genders].sort().map(gender => (
|
||||
{genders.map(gender => (
|
||||
<Table.HeaderCell key={gender} content={gender} />
|
||||
))}
|
||||
{renderRatioOfFinns ? <Table.HeaderCell content="Finnish" /> : null}
|
||||
@@ -346,13 +348,15 @@ const ThroughputTable = ({
|
||||
) : null}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell>{throughput.totals.students}</Table.HeaderCell>
|
||||
{Object.keys(throughput.totals.genders).map(genderKey => (
|
||||
<Table.HeaderCell key={`${genderKey}total`}>
|
||||
{`${throughput.totals.genders[genderKey]} (${Math.floor(
|
||||
(throughput.totals.genders[genderKey] / throughput.totals.students) * 100
|
||||
)}%)`}
|
||||
</Table.HeaderCell>
|
||||
))}
|
||||
{Object.keys(throughput.totals.genders)
|
||||
.sort()
|
||||
.map(genderKey => (
|
||||
<Table.HeaderCell key={`${genderKey}total`}>
|
||||
{`${throughput.totals.genders[genderKey]} (${Math.floor(
|
||||
(throughput.totals.genders[genderKey] / throughput.totals.students) * 100
|
||||
)}%)`}
|
||||
</Table.HeaderCell>
|
||||
))}
|
||||
{renderRatioOfFinns ? (
|
||||
<Table.HeaderCell>
|
||||
{`${throughput.totals.nationalities.Finland || 0} (${
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { connect } from 'react-redux'
|
||||
import { shape, string, arrayOf, func, bool } from 'prop-types'
|
||||
import { shape, string, func, bool } from 'prop-types'
|
||||
import { Header, Segment, Tab, Button } from 'semantic-ui-react'
|
||||
import { isEqual, uniqBy } from 'lodash'
|
||||
import DegreeCoursesTable from './DegreeCourses'
|
||||
@@ -9,7 +9,6 @@ import CourseCodeMapper from '../CourseCodeMapper'
|
||||
import StudyProgrammeSelector from './StudyProgrammeSelector'
|
||||
import Overview from './Overview'
|
||||
import StudyTrackOverview from './StudyTrackOverview'
|
||||
import AggregateView from '../CourseGroups/AggregateView'
|
||||
import ThesisCourses from './ThesisCourses'
|
||||
import PresentStudents from './PresentStudents'
|
||||
import '../PopulationQueryCard/populationQueryCard.css'
|
||||
@@ -28,7 +27,7 @@ import useLanguage from '../LanguagePicker/useLanguage'
|
||||
|
||||
const StudyProgramme = props => {
|
||||
const { language } = useLanguage()
|
||||
const [tab, setTab] = useTabs('p_tab', props.match.params.courseGroupId ? 2 : 0, props.history)
|
||||
const [tab, setTab] = useTabs('p_tab', 0, props.history)
|
||||
useTitle('Study programmes')
|
||||
|
||||
useEffect(() => {
|
||||
@@ -63,8 +62,8 @@ const StudyProgramme = props => {
|
||||
}
|
||||
|
||||
const getPanes = () => {
|
||||
const { match, rights, userRoles, programmes } = props
|
||||
const { studyProgrammeId, courseGroupId } = match.params
|
||||
const { match, programmes } = props
|
||||
const { studyProgrammeId } = match.params
|
||||
const filteredStudytracks = programmes[studyProgrammeId]
|
||||
? Object.values(programmes[studyProgrammeId].enrollmentStartYears).reduce((acc, curr) => {
|
||||
acc.push(...Object.values(curr.studyTracks))
|
||||
@@ -91,13 +90,6 @@ const StudyProgramme = props => {
|
||||
},
|
||||
{ menuItem: 'Code Mapper', render: () => <CourseCodeMapper studyprogramme={studyProgrammeId} /> }
|
||||
)
|
||||
|
||||
if ((userRoles.includes('coursegroups') && rights.includes(studyProgrammeId)) || userRoles.includes('admin')) {
|
||||
panes.push({
|
||||
menuItem: 'Course Groups',
|
||||
render: () => <AggregateView programmeId={studyProgrammeId} courseGroupId={courseGroupId} />,
|
||||
})
|
||||
}
|
||||
panes.push({
|
||||
menuItem: 'Thesis Courses',
|
||||
render: () => <ThesisCourses studyprogramme={studyProgrammeId} />,
|
||||
@@ -188,13 +180,10 @@ StudyProgramme.propTypes = {
|
||||
match: shape({
|
||||
params: shape({
|
||||
studyProgrammeId: string,
|
||||
courseGroupId: string,
|
||||
}),
|
||||
}),
|
||||
programmes: shape({}),
|
||||
history: shape({}).isRequired,
|
||||
rights: arrayOf(string).isRequired,
|
||||
userRoles: arrayOf(string).isRequired,
|
||||
getProductivityDispatch: func.isRequired,
|
||||
getThroughputDispatch: func.isRequired,
|
||||
isAdmin: bool.isRequired,
|
||||
|
||||
@@ -51,6 +51,9 @@ const TeacherStatistics = ({
|
||||
KH57_001
|
||||
500-K001
|
||||
--------
|
||||
KH74_001
|
||||
740-K001
|
||||
--------
|
||||
KH80_003
|
||||
800-K003
|
||||
--------
|
||||
@@ -58,13 +61,12 @@ const TeacherStatistics = ({
|
||||
*/
|
||||
const mapToProviders = rights =>
|
||||
rights.map(r => {
|
||||
const isNumber = str => !Number.isNaN(Number(str))
|
||||
if (r.includes('_')) {
|
||||
let newPrefix = ''
|
||||
let newSuffix = ''
|
||||
const split = r.split('_')
|
||||
newPrefix = `${split[0][2]}00`
|
||||
newSuffix = `${split[0][0]}${split[1]}`
|
||||
const providercode = `${newPrefix}-${newSuffix}`
|
||||
const [left, right] = r.split('_')
|
||||
const prefix = [...left].filter(isNumber).join('')
|
||||
const suffix = `${left[0]}${right}`
|
||||
const providercode = `${prefix}0-${suffix}`
|
||||
return providercode
|
||||
}
|
||||
return r
|
||||
@@ -85,7 +87,7 @@ const TeacherStatistics = ({
|
||||
|
||||
const userProviders = mapToProviders(rights)
|
||||
const invalidQueryParams = provs.length === 0 || !semesterStart
|
||||
const providerOptions = isAdmin ? providers : providers.filter(p => userProviders.includes(p.value))
|
||||
const providerOptions = isAdmin ? providers : providers.filter(p => userProviders.includes(p.code))
|
||||
const localizedProviderOptions = providerOptions.map(({ name, ...rest }) => ({
|
||||
...rest,
|
||||
text: getTextIn(name, language),
|
||||
@@ -184,11 +186,7 @@ const mapStateToProps = state => {
|
||||
},
|
||||
} = state
|
||||
const { semesters } = state.semesters.data
|
||||
const providerOptions = providers.data.map(p => ({
|
||||
key: p.providercode,
|
||||
value: p.providercode,
|
||||
name: p.name,
|
||||
}))
|
||||
const providerOptions = providers.data.map(prov => ({ key: prov.code, value: prov.code, name: prov.name }))
|
||||
const semesterOptions = !semesters
|
||||
? []
|
||||
: Object.values(semesters)
|
||||
|
||||
@@ -12,15 +12,19 @@ const databaseNames = ['kone-db', 'oodi-db', 'sis-db', 'sis-importer-db', 'user-
|
||||
const adminerUrls = databaseNames.map(db => ({ url: `${adminerBaseUrl}/?pgsql=${db}&username=postgres`, text: db }))
|
||||
|
||||
// Base paths
|
||||
const basePath = process.env.PUBLIC_URL || ''
|
||||
const basePath = process.env.REACT_APP_PUBLIC_URL || ''
|
||||
const apiBasePath = `${basePath}/api`
|
||||
|
||||
// Update time for frontpage
|
||||
const builtAt = process.env.REACT_APP_BUILT_AT || ''
|
||||
|
||||
module.exports = {
|
||||
adminerUrls,
|
||||
isProduction,
|
||||
isDev,
|
||||
basePath,
|
||||
apiBasePath,
|
||||
builtAt,
|
||||
SENTRY_RELEASE,
|
||||
SENTRY_ENVIRONMENT,
|
||||
}
|
||||
|
||||
@@ -6,24 +6,6 @@ export const getFaculties = () => {
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
export const getUserFaculties = () => {
|
||||
const route = '/facultystats/faculties'
|
||||
const prefix = 'GET_USER_FACULTIES_'
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
export const getFacultiesYearlyStats = () => {
|
||||
const route = '/facultystats/yearlystats'
|
||||
const prefix = 'GET_FACULTIES_YEARLY_STATS_'
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
export const getFacultyProgrammes = () => {
|
||||
const route = '/facultystats/programmes'
|
||||
const prefix = 'GET_FACULTY_PROGRAMMES_'
|
||||
return callController(route, prefix)
|
||||
}
|
||||
|
||||
const reducer = (
|
||||
state = {
|
||||
data: [],
|
||||
@@ -56,57 +38,6 @@ const reducer = (
|
||||
pending: false,
|
||||
error: true,
|
||||
}
|
||||
case 'GET_USER_FACULTIES_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
userFacultiesPending: true,
|
||||
userFacultiesError: false,
|
||||
}
|
||||
case 'GET_USER_FACULTIES_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
userFacultiesPending: false,
|
||||
userFacultiesError: true,
|
||||
}
|
||||
case 'GET_USER_FACULTIES_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
data: action.response,
|
||||
userFacultiesPending: false,
|
||||
userFacultiesError: false,
|
||||
}
|
||||
case 'GET_FACULTIES_YEARLY_STATS_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: true,
|
||||
}
|
||||
case 'GET_FACULTIES_YEARLY_STATS_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
yearlyStats: action.response,
|
||||
pending: false,
|
||||
error: true,
|
||||
}
|
||||
case 'GET_FACULTY_PROGRAMMES_ATTEMPT':
|
||||
return {
|
||||
...state,
|
||||
pending: true,
|
||||
error: false,
|
||||
}
|
||||
case 'GET_FACULTY_PROGRAMMES_FAILURE':
|
||||
return {
|
||||
...state,
|
||||
pending: false,
|
||||
error: true,
|
||||
}
|
||||
case 'GET_FACULTY_PROGRAMMES_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
facultyProgrammes: action.response,
|
||||
pending: false,
|
||||
error: false,
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
up: queryInterface => {
|
||||
return queryInterface.bulkDelete('access_groups', [
|
||||
{
|
||||
group_code: 'faculties',
|
||||
},
|
||||
])
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -220,7 +220,7 @@ const courseProviderMapper =
|
||||
|
||||
const timify = t => new Date(t).getTime()
|
||||
|
||||
const courseMapper = courseIdToAttainments => (groupedCourse, substitutions) => {
|
||||
const courseMapper = courseIdToAttainments => (groupedCourse, substitutions, organisation) => {
|
||||
const [groupId, courses] = groupedCourse
|
||||
const { code, name, study_level: coursetypecode } = courses[0]
|
||||
|
||||
@@ -263,6 +263,7 @@ const courseMapper = courseIdToAttainments => (groupedCourse, substitutions) =>
|
||||
enddate,
|
||||
is_study_module: false, // VALIDATE THIS PLS
|
||||
substitutions,
|
||||
responsible_organisation: organisation,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,14 @@ const updateCourses = async (courseIdToAttainments, groupIdToCourse) => {
|
||||
}, [])
|
||||
),
|
||||
]
|
||||
// take responsible organisation from all course units
|
||||
const organisation = [
|
||||
...new Set(
|
||||
courses.reduce((acc, curr) => {
|
||||
return [...acc, ...flatten(curr.organisations).map(({ organisationId }) => organisationId)]
|
||||
}, [])
|
||||
),
|
||||
][0]
|
||||
|
||||
// Take organisation for the newest course_unit of this groupid
|
||||
const coursesSortedByStartDate = sortBy(courses, 'validity_period.startDate')
|
||||
@@ -59,8 +67,9 @@ const updateCourses = async (courseIdToAttainments, groupIdToCourse) => {
|
||||
.filter(({ roleUrn }) => roleUrn === 'urn:code:organisation-role:responsible-organisation')
|
||||
.map(mapCourseProvider)
|
||||
)
|
||||
console.log('courseProviders: ', courseProviders)
|
||||
|
||||
return mapCourse(groupedCourse, substitutions)
|
||||
return mapCourse(groupedCourse, substitutions, organisation)
|
||||
})
|
||||
|
||||
await bulkCreate(Course, courses)
|
||||
|
||||
@@ -13,12 +13,27 @@ const {
|
||||
const { selectFromByIds, selectFromSnapshotsByIds, bulkCreate, getCourseUnitsByCodes } = require('../../db')
|
||||
const { getEducation, getUniOrgId, loadMapsIfNeeded } = require('../shared')
|
||||
const { studentMapper, mapTeacher, creditMapper, semesterEnrollmentMapper, courseProviderMapper } = require('../mapper')
|
||||
|
||||
const { dbConnections } = require('../../db/connection')
|
||||
const { isBaMa } = require('../../utils')
|
||||
const { updateStudyRights, updateStudyRightElements, updateElementDetails } = require('./studyRightUpdaters')
|
||||
const { getAttainmentsToBeExcluded } = require('./excludedPartialAttainments')
|
||||
|
||||
// Accepted selection path is not available when degree programme doesn't have
|
||||
// studytrack or major subject. This is a known bug on SIS and has been reported
|
||||
// to funidata.
|
||||
// In these cases, degree programmes module group id must be fetched from education.
|
||||
const addSelectionPaths = studyrightSnapshots =>
|
||||
studyrightSnapshots.map(snapshot =>
|
||||
Object.keys(snapshot.accepted_selection_path).length === 0
|
||||
? {
|
||||
...snapshot,
|
||||
accepted_selection_path: {
|
||||
educationPhase1GroupId: getEducation(snapshot.education_id).structure.phase1.options[0].moduleGroupId,
|
||||
},
|
||||
}
|
||||
: snapshot
|
||||
)
|
||||
|
||||
// Group snapshots by studyright id and find out when studyrights have begun
|
||||
const groupStudyrightSnapshots = studyrightSnapshots => {
|
||||
const snapshotsBystudyright = Object.entries(
|
||||
@@ -45,12 +60,20 @@ const groupStudyrightSnapshots = studyrightSnapshots => {
|
||||
|
||||
const groupedByPhases = groupBy(orderedSnapshots, byPhases)
|
||||
|
||||
// Get snapshot date time from oldest snapshot where date time is available. If no date
|
||||
// times are available (e.g. all are null), take study start date of the oldest one
|
||||
const parseSnapshotDateTime = snapshots => {
|
||||
let oldestFirst = [...snapshots]
|
||||
oldestFirst.reverse()
|
||||
const firstWithSnapshotDateTime = oldestFirst.find(s => !!s.snapshot_date_time)
|
||||
if (firstWithSnapshotDateTime) return firstWithSnapshotDateTime.snapshot_date_time
|
||||
return oldestFirst[0].study_start_date
|
||||
}
|
||||
|
||||
const snapshotsWithRightDate = Object.keys(groupedByPhases).map(key => {
|
||||
const snapshots = groupedByPhases[key]
|
||||
const most_recent = snapshots[0]
|
||||
const the_first = snapshots[snapshots.length - 1]
|
||||
most_recent.first_snapshot_date_time = the_first.snapshot_date_time
|
||||
|
||||
most_recent.first_snapshot_date_time = parseSnapshotDateTime(snapshots)
|
||||
return most_recent
|
||||
})
|
||||
|
||||
@@ -132,7 +155,9 @@ const updateStudents = async personIds => {
|
||||
selectFromByIds('study_right_primalities', personIds, 'student_id'),
|
||||
])
|
||||
|
||||
const groupedStudyRightSnapshots = groupStudyrightSnapshots(studyRightSnapshots)
|
||||
const studyRightSnapshotsWithAddedSelectionPaths = addSelectionPaths(studyRightSnapshots)
|
||||
|
||||
const groupedStudyRightSnapshots = groupStudyrightSnapshots(studyRightSnapshotsWithAddedSelectionPaths)
|
||||
|
||||
const personIdToStudentNumber = students.reduce((res, curr) => {
|
||||
res[curr.id] = curr.student_number
|
||||
@@ -147,7 +172,9 @@ const updateStudents = async personIds => {
|
||||
|
||||
const attainmentsToBeExluced = getAttainmentsToBeExcluded()
|
||||
|
||||
const mappedStudents = students.map(studentMapper(attainments, studyRightSnapshots, attainmentsToBeExluced))
|
||||
const mappedStudents = students.map(
|
||||
studentMapper(attainments, studyRightSnapshotsWithAddedSelectionPaths, attainmentsToBeExluced)
|
||||
)
|
||||
await bulkCreate(Student, mappedStudents)
|
||||
|
||||
const [moduleGroupIdToCode, formattedStudyRights] = await Promise.all([
|
||||
@@ -167,7 +194,7 @@ const updateStudents = async personIds => {
|
||||
),
|
||||
updateAttainments(attainments, personIdToStudentNumber, attainmentsToBeExluced),
|
||||
updateTermRegistrations(termRegistrations, personIdToStudentNumber),
|
||||
await bulkCreate(Transfer, mappedTransfers),
|
||||
await bulkCreate(Transfer, mappedTransfers, null, ['studyrightid']),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ const studentsThatNeedToBeFixed = [
|
||||
{ id: 'hy-hlo-57634714', started: '2018-05-02' },
|
||||
{ id: 'hy-hlo-114941473', started: '2019-01-22' },
|
||||
{ id: 'hy-hlo-97547785', started: '2019-12-18' },
|
||||
// new entries
|
||||
{ id: 'hy-hlo-62121710', started: '2020-12-19' },
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
|
||||
Reference in New Issue
Block a user