Compare commits

...
83 Commits
Author SHA1 Message Date
Otto AhoniemiandGitHub 5cb8601cc9 Merge pull request #3145 from UniversityOfHelsinkiCS/trunk
Goodbye faculties
2021-08-16 10:09:39 +03:00
Otto Ahoniemi 74b16f7e50 Remove faculty stuff from new analyticsservice 2021-08-16 09:31:25 +03:00
Otto Ahoniemi ec2b1fe6a8 Remove old faculty stuff from events and services 2021-08-16 09:30:22 +03:00
Otto Ahoniemi a8ec3c2257 Change faculty fetching to work with sis-db 2021-08-16 09:26:51 +03:00
Otto Ahoniemi d9b4aa58f1 Remove unused facultystats route 2021-08-16 09:07:34 +03:00
Otto Ahoniemi f29c33cd60 Remove old providers logic 2021-08-16 09:02:05 +03:00
Otto Ahoniemi fccd0d7769 Cleanup unused stuff from faculties reducer 2021-08-14 17:38:04 +03:00
Otto Ahoniemi 5e59956485 Remove faculty component 2021-08-14 17:27:02 +03:00
Otto Ahoniemi 5c3fe43473 Remove faculties from frontend visible parts 2021-08-14 17:13:18 +03:00
Otto Ahoniemi 3fbcbbf0d2 Merge branch 'master' into trunk 2021-08-14 17:05:01 +03:00
Otto AhoniemiandGitHub 3d8d6e7a00 Merge pull request #3144 from UniversityOfHelsinkiCS/trunk
Fix provider stuff in teachers and add regular trendsv2 updates
2021-08-13 15:49:06 +03:00
Otto Ahoniemi b040bac675 Cleanup references to V2, since old oodi V1 versions are not used in events 2021-08-13 15:38:48 +03:00
Otto Ahoniemi c1689253ea Remove old oodikone cool data science stuff from events 2021-08-13 15:34:17 +03:00
Otto Ahoniemi 110abe4baf Skip courses test in popstats for now 2021-08-13 15:32:06 +03:00
Otto Ahoniemi 9ac9ed298c Don't intercept, just wait a lot with courses 2021-08-13 15:17:51 +03:00
Otto Ahoniemi 261b3f1384 Add trendsv2 refreshing to events.js 2021-08-13 15:16:38 +03:00
Otto Ahoniemi 515e3ea4c1 Fix protoc update failing when values not available 2021-08-13 15:15:43 +03:00
Otto Ahoniemi 3e81490f37 Moar waitingggg 2021-08-13 14:40:00 +03:00
Otto Ahoniemi 5b53233dc3 Remove extra only from filters 2021-08-13 14:39:12 +03:00
Otto Ahoniemi 2b04de96d2 Fix missing key & value for provider dropdown 2021-08-13 14:27:28 +03:00
Otto Ahoniemi abd13df96d Fix parsing provider in teacher stats 2021-08-13 10:29:28 +03:00
Otto Ahoniemi 4e28efd608 Use organisation v2 models to return correct providers 2021-08-12 09:53:13 +03:00
Otto Ahoniemi 63e72895f6 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-12 09:00:27 +03:00
Petri PalmuandGitHub 74baf4ea47 Merge pull request #3140 from UniversityOfHelsinkiCS/trunk
Trunk
2021-08-12 08:48:31 +03:00
Petri Palmu 1563566e62 i gave more wait time for cypress 2021-08-11 15:56:14 +03:00
Petri Palmu 921b6a0e07 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-11 14:13:28 +03:00
Petri Palmu 3717bed980 few new tests for course statistics 2021-08-11 14:13:19 +03:00
Otto AhoniemiandGitHub b646d540fe Merge pull request #3139 from UniversityOfHelsinkiCS/trunk
Correctly set sentry release in github action step
2021-08-11 11:27:54 +03:00
Otto AhoniemiandGitHub e5e246d826 Merge branch 'master' into trunk 2021-08-11 11:27:21 +03:00
Otto Ahoniemi 286b26d484 Remove old ooodikone stuff package.jsons to avoid dependabot spam 2021-08-11 11:26:23 +03:00
Otto Ahoniemi f84b05d804 Correctly set sentry release in github action step 2021-08-11 11:23:31 +03:00
Otto AhoniemiandGitHub b30124dc2e Merge pull request #3136 from UniversityOfHelsinkiCS/trunk
acually fix setting env vars in frontend
2021-08-11 11:09:55 +03:00
Otto Ahoniemi ac1f7039a4 Move all envs to start of docker build phase 2021-08-11 10:54:19 +03:00
Otto Ahoniemi 18943759a1 Remove unused env vars 2021-08-11 10:50:22 +03:00
Otto Ahoniemi 8c388119f3 Prevent linting failing because of file passed as an argument
If linting command is ran through npm scripts, linting just works.
See: https://github.com/okonet/lint-staged/issues/174
2021-08-11 10:49:30 +03:00
Otto Ahoniemi c486bbbbcf Ignore passed docker-compose file in lint-staged
Lint-staged file that is being linted as an argument, so our linting
command (`docker-compose config -q`) thinks argument is a docker-compose
service. This commit will ignore all arguments passed to command.

See:
https://github.com/okonet/lint-staged/issues/174#issuecomment-437468837
2021-08-11 10:46:13 +03:00
Otto Ahoniemi 76b28928a7 Remove console.logs used for debugging 2021-08-11 10:29:44 +03:00
Otto AhoniemiandGitHub 4389af86f5 Merge pull request #3134 from UniversityOfHelsinkiCS/trunk
env var mayyhem
2021-08-11 10:08:25 +03:00
Otto Ahoniemi e776c29aa4 Set env variables in frontend before building 2021-08-11 10:07:38 +03:00
Otto AhoniemiandGitHub 511f262d8b Merge pull request #3133 from UniversityOfHelsinkiCS/trunk
moar debugging
2021-08-11 09:54:57 +03:00
Otto Ahoniemi 9c48fe888b Merge branch 'master' into trunk 2021-08-11 09:53:40 +03:00
Otto Ahoniemi bd54d4f4e1 Log whole process env for staging debug 2021-08-11 09:52:28 +03:00
Otto AhoniemiandGitHub 2daa1c674d Merge pull request #3132 from UniversityOfHelsinkiCS/trunk
Debug in staging
2021-08-11 09:27:26 +03:00
Otto AhoniemiandGitHub d544220436 Merge branch 'master' into trunk 2021-08-11 09:26:55 +03:00
Otto Ahoniemi fcd0ad23cc Add some console.logs for staging debug 2021-08-11 09:26:10 +03:00
Petri Palmu 1e94accc55 Merge branch 'feature/org' into trunk 2021-08-11 08:50:46 +03:00
Petri Palmu 326da589de changed substitutions 2021-08-11 08:05:40 +03:00
Otto Ahoniemi 804a7f3f63 Acually take snapshot date time for first snapshot 2021-08-10 14:39:50 +03:00
Otto Ahoniemi 178d0d921c Add kludge to find correct snapshot date time timestamp 2021-08-10 13:34:56 +03:00
Otto Ahoniemi 1cefe9e410 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-10 11:39:48 +03:00
Otto Ahoniemi b1cfc82aa2 Filter out snapshots with missing snapshot date time when trying to get first date time 2021-08-10 11:38:58 +03:00
Otto Ahoniemi a8b76c7253 Enrich missing accepted selection paths to snapshots 2021-08-10 11:38:28 +03:00
Otto AhoniemiandGitHub c6c2d6f314 Merge pull request #3131 from UniversityOfHelsinkiCS/trunk
Fix setting current time in actions
2021-08-10 10:54:57 +03:00
Otto AhoniemiandGitHub 15142e3750 Merge branch 'master' into trunk 2021-08-10 10:53:33 +03:00
Otto Ahoniemi 45b1d4cb7a Fix setting current time in actions 2021-08-10 10:52:44 +03:00
Otto AhoniemiandGitHub 3a7575b052 Merge pull request #3130 from UniversityOfHelsinkiCS/trunk
Add correct built at time + test fixes
2021-08-10 10:42:50 +03:00
Otto Ahoniemi f180480b5c Merge branch 'master' into trunk 2021-08-10 10:33:41 +03:00
Otto Ahoniemi 997a6e36ef Add correct built at time to oodikone frontpage 2021-08-10 10:23:39 +03:00
Otto Ahoniemi 0cbbaea27b Hide SIS warning flag to avoid screen size problems when running headlessly 2021-08-10 09:46:43 +03:00
Otto Ahoniemi 748e8b31ae Just get tag name instead of looking containing element first 2021-08-10 09:46:21 +03:00
Otto Ahoniemi 74b80e75df Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-09 20:51:46 +03:00
Otto Ahoniemi 8039bd369b Change viewports to defaults to speed up runtime when running headlessly
See: https://github.com/cypress-io/cypress/issues/15990
2021-08-09 20:51:18 +03:00
Matti Luukkainen 2312a8d662 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-09 19:57:53 +03:00
Matti Luukkainen c25431e918 a fix in kasvatustiede kandi start date 2021-08-09 19:55:47 +03:00
Petri PalmuandGitHub 53865cafd3 Merge pull request #3127 from UniversityOfHelsinkiCS/trunk
Trunk
2021-08-09 16:03:55 +03:00
Otto Ahoniemi b0a8ce30a2 Upgrade packages 2021-08-09 10:25:18 +03:00
Otto Ahoniemi 2c1eb0c90e Upgrade cypress 2021-08-09 10:01:53 +03:00
Petri Palmu 48878b961c Merge branch 'trunk' into feature/org 2021-08-09 09:09:25 +03:00
Otto Ahoniemi 149cbbcf19 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-09 09:06:28 +03:00
Otto Ahoniemi 749a52a375 Continue setup even if compose cleanup fails 2021-08-09 09:06:05 +03:00
Petri Palmu 999ffaaa9f added test for student grades 2021-08-06 16:49:47 +03:00
Petri Palmu 65cb7a99df Merge branch 'feature/grade' into trunk 2021-08-06 11:49:50 +03:00
Petri Palmu 14486e1116 changed course grades so that old courses are included 2021-08-06 11:49:28 +03:00
Petri Palmu 9922708975 Merge branch 'feature/grade' into trunk 2021-08-05 17:25:58 +03:00
Petri Palmu bf7588ba28 changed studyprogramme total gender right 2021-08-05 17:24:05 +03:00
Petri Palmu 5c92532aef studyprogramme view gender are now in right order 2021-08-05 17:02:27 +03:00
Petri Palmu 6acba8f523 another little change to backend substitutions 2021-08-05 15:37:03 +03:00
Petri Palmu 77c1a733c2 more minor changes to substitutions 2021-08-05 15:27:22 +03:00
Petri Palmu d3f6c72906 changed the way subgroups are made in backend 2021-08-05 15:02:54 +03:00
Petri Palmu 3d8b6d214a added grades for students in coursestats issue #3039 2021-08-04 16:18:35 +03:00
Petri Palmu 16b1ff5c59 added responsible_organisation to the course model and used it in frontend 2021-08-04 12:04:37 +03:00
Petri Palmu 43b08bc8e9 little change to cypress tests to get working in gh actions 2021-08-02 14:33:21 +03:00
Petri Palmu bc42dbee1a fixed issue #3109, user navigation responded 500 2021-08-02 11:00:05 +03:00
56 changed files with 491 additions and 24285 deletions
@@ -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"
+5
View File
@@ -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"
-2
View File
@@ -3,7 +3,5 @@
"baseUrl": "http://localhost:3000",
"defaultCommandTimeout": 30000,
"requestTimeout": 30000,
"viewportHeight": 1080,
"viewportWidth": 1920,
"videoCompression": false
}
+48
View File
@@ -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')
+2 -5
View File
@@ -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()
+3 -3
View File
@@ -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)
})
+2
View File
@@ -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))
})
-2
View File
@@ -29,8 +29,6 @@ services:
frontend:
<<: *defaults
container_name: frontend
environment:
BACKEND_ADDR: backend
image: toska/oodikone2-frontend:trunk
kone-db:
-7
View File
@@ -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:
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"
}
}
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"
}
}
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"
}
}
+70 -96
View File
@@ -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
View File
@@ -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",
+3 -3
View File
@@ -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"
+4 -4
View File
@@ -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"]
+47 -70
View File
@@ -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,
}
-2
View File
@@ -13,7 +13,6 @@ 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')
@@ -56,7 +55,6 @@ 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)
+4 -3
View File
@@ -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 -1
View File
@@ -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()
+2 -2
View File
@@ -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,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,
}
+1 -1
View File
@@ -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,
}
+55 -29
View File
@@ -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,
}
+8 -6
View File
@@ -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"]
@@ -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))
@@ -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',
@@ -63,7 +61,6 @@ const Routes = () => (
<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} (${
@@ -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)
+5 -1
View File
@@ -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,
}
-69
View File
@@ -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
}
@@ -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([
@@ -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 = {