mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19fe7fd817 | ||
|
|
99b3f7756c | ||
|
|
51475be0db | ||
|
|
a8eb68e317 | ||
|
|
8281564fef | ||
|
|
4373065fdd | ||
|
|
8dbbf00b51 | ||
|
|
584a1b474a | ||
|
|
8151c4f463 | ||
|
|
bb818ee9ac | ||
|
|
cd3b9bb843 | ||
|
|
aaa012894c | ||
|
|
4d06eb7558 | ||
|
|
b563b29a36 | ||
|
|
617e234798 | ||
|
|
754d525f87 | ||
|
|
a0e6d342b9 | ||
|
|
60f500c9e7 | ||
|
|
dc9ce89aa7 | ||
|
|
8a1fb2725b | ||
|
|
0ca6bca243 | ||
|
|
7457bc412b | ||
|
|
3d2e8fb1ca | ||
|
|
1d980795c6 | ||
|
|
6bc6a28127 | ||
|
|
a2f84931cd | ||
|
|
1f1bae8557 | ||
|
|
c714c61269 | ||
|
|
e85deff947 | ||
|
|
8c87135f58 | ||
|
|
7eac6c686e | ||
|
|
11172f7d77 | ||
|
|
be29486cdd | ||
|
|
04cbec19a5 | ||
|
|
7c3cfb56b5 | ||
|
|
090305be3b |
@@ -35,7 +35,6 @@ jobs:
|
||||
context: services/backend
|
||||
push: true
|
||||
tags: toska/oodikone2-backend:latest
|
||||
target: production
|
||||
|
||||
build_and_release_frontend:
|
||||
name: 'Build and release frontend'
|
||||
@@ -71,7 +70,6 @@ jobs:
|
||||
context: services/frontend
|
||||
push: true
|
||||
tags: toska/oodikone2-frontend:latest
|
||||
target: production
|
||||
- name: 'Create a container from built image '
|
||||
run: docker create --name frontend toska/oodikone2-frontend:latest
|
||||
- name: 'Copy assets from the container'
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
name: 'Test oodikone and release to staging'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- '.github/**'
|
||||
- 'cypress/**'
|
||||
- 'cypress.config.json'
|
||||
- 'services/**'
|
||||
- '!services/backend/src/rapodiff.js'
|
||||
- 'docker-compose.ci.yml'
|
||||
- 'docker-compose.test.yml'
|
||||
- 'nginx.conf'
|
||||
- 'package*.json'
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
secrets:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
|
||||
release_frontend:
|
||||
name: 'Build and release frontend'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build frontend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-frontend
|
||||
tags: staging ${{ github.sha }}
|
||||
context: services/frontend
|
||||
containerfiles: |
|
||||
services/frontend/Dockerfile
|
||||
build-args: |
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
BASE_PATH=/oodikone
|
||||
GIT_SHA=${{ github.sha }}
|
||||
STAGING=true
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
with:
|
||||
image: ${{ steps.build-image.outputs.image }}
|
||||
tags: ${{ steps.build-image.outputs.tags }}
|
||||
registry: quay.io/toska
|
||||
username: toska+github
|
||||
password: ${{ secrets.QUAY_IO_TOKEN }}
|
||||
|
||||
release_backend:
|
||||
name: 'Build and release backend'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build backend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-backend
|
||||
tags: staging ${{ github.sha }}
|
||||
context: services/backend
|
||||
containerfiles: |
|
||||
services/backend/Dockerfile
|
||||
build-args: |
|
||||
BASE_PATH=/oodikone
|
||||
GIT_SHA=${{ github.sha }}
|
||||
STAGING=true
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
with:
|
||||
image: ${{ steps.build-image.outputs.image }}
|
||||
tags: ${{ steps.build-image.outputs.tags }}
|
||||
registry: quay.io/toska
|
||||
username: toska+github
|
||||
password: ${{ secrets.QUAY_IO_TOKEN }}
|
||||
@@ -1,25 +1,17 @@
|
||||
name: 'Test oodikone and release to staging'
|
||||
name: Lint and test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- '.github/actions/**'
|
||||
- 'cypress/**'
|
||||
- 'cypress.config.json'
|
||||
- 'services/**'
|
||||
- '!services/backend/src/rapodiff.js'
|
||||
- 'docker-compose.ci.yml'
|
||||
- 'docker-compose.test.yml'
|
||||
- 'nginx.conf'
|
||||
- 'package*.json'
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
workflow_call:
|
||||
secrets:
|
||||
username:
|
||||
required: true
|
||||
description: 'Toska registry username'
|
||||
password:
|
||||
required: true
|
||||
description: 'Toska registry password'
|
||||
|
||||
jobs:
|
||||
# === Static tests / linting ===
|
||||
|
||||
lint_backend:
|
||||
name: 'Lint backend'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -62,13 +54,11 @@ jobs:
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=staging
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-backend:staging
|
||||
type=registry,ref=toska/oodikone2-backend:latest
|
||||
type=registry,ref=quay.io/toska/oodikone-backend:staging
|
||||
cache-to: type=inline
|
||||
context: services/backend
|
||||
push: false
|
||||
tags: toska/oodikone2-backend:staging
|
||||
target: production
|
||||
tags: oodikone-backend:staging
|
||||
outputs: type=docker,dest=/tmp/backend.tar
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -92,13 +82,11 @@ jobs:
|
||||
SENTRY_ENVIRONMENT=staging
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-frontend:staging
|
||||
type=registry,ref=toska/oodikone2-frontend:latest
|
||||
type=registry,ref=toska/oodikone-frontend:staging
|
||||
cache-to: type=inline
|
||||
context: services/frontend
|
||||
push: false
|
||||
tags: toska/oodikone2-frontend:staging
|
||||
target: production
|
||||
tags: oodikone-frontend:staging
|
||||
outputs: type=docker,dest=/tmp/frontend.tar
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -121,8 +109,8 @@ jobs:
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
- name: 'Let sis-updater-worker run migrations, then push new version of image to toska hub'
|
||||
if: steps.check_migrations.outputs.migrations == 'true'
|
||||
run: |
|
||||
@@ -154,8 +142,8 @@ jobs:
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
- name: 'Let backend run migrations, then push new version of image to toska hub'
|
||||
if: steps.check_migrations.outputs.migrations == 'true'
|
||||
run: |
|
||||
@@ -187,8 +175,8 @@ jobs:
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
- name: 'Let backend run migrations, then push new version of image to toska hub'
|
||||
if: steps.check_migrations.outputs.migrations == 'true'
|
||||
run: |
|
||||
@@ -211,8 +199,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Course_statistics.js
|
||||
|
||||
test_custom_population:
|
||||
@@ -229,8 +217,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Custom_population.js
|
||||
|
||||
test_population_statistics:
|
||||
@@ -247,8 +235,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Population_statistics.js
|
||||
|
||||
test_filters:
|
||||
@@ -265,8 +253,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Filters.js
|
||||
|
||||
test_student_statistics:
|
||||
@@ -283,9 +271,10 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Student_statistics.js
|
||||
|
||||
test_completed_courses:
|
||||
name: 'Test completed courses'
|
||||
needs:
|
||||
@@ -300,9 +289,10 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Completed_courses.js
|
||||
|
||||
test_studyprogramme_overview:
|
||||
name: 'Test studyprogramme overview'
|
||||
needs:
|
||||
@@ -317,8 +307,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Studyprogramme_overview.js
|
||||
|
||||
test_teachers_and_users:
|
||||
@@ -335,8 +325,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: |
|
||||
cypress/e2e/Teachers.js
|
||||
cypress/e2e/Users.js
|
||||
@@ -355,8 +345,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Faculty_statistics.js
|
||||
|
||||
test_open_uni_search:
|
||||
@@ -373,12 +363,11 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Openuni.js
|
||||
|
||||
# === Other tests ===
|
||||
|
||||
test_service_exit_statuses:
|
||||
name: 'Check services run correctly'
|
||||
needs:
|
||||
@@ -402,8 +391,8 @@ jobs:
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Check that exit statuses are not errors'
|
||||
@@ -416,100 +405,3 @@ jobs:
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
|
||||
# === Release ===
|
||||
|
||||
release_images:
|
||||
name: 'Release built images'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- lint_backend
|
||||
- lint_frontend
|
||||
- test_course_statistics
|
||||
- test_custom_population
|
||||
- test_filters
|
||||
- test_population_statistics
|
||||
- test_student_statistics
|
||||
- test_completed_courses
|
||||
- test_studyprogramme_overview
|
||||
- test_teachers_and_users
|
||||
- test_faculty_statistics
|
||||
- test_open_uni_search
|
||||
- test_service_exit_statuses
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 'Create a container from frontend'
|
||||
run: docker create --name frontend toska/oodikone2-frontend:staging
|
||||
- name: 'Copy assets from the container'
|
||||
run: docker cp frontend:/usr/src/app/build ./build
|
||||
- name: 'Create Sentry release'
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: toska
|
||||
SENTRY_PROJECT: oodikone
|
||||
with:
|
||||
environment: staging
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ github.sha }}
|
||||
- name: 'Push to docker hub'
|
||||
run: |
|
||||
docker push toska/oodikone2-backend:staging
|
||||
docker push toska/oodikone2-frontend:staging
|
||||
- name: 'Send deployment success notification to slack'
|
||||
if: success()
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment
|
||||
softa-url: https://oodikone-staging.cs.helsinki.fi/
|
||||
deployment-target: staging
|
||||
- name: 'Send deployment failure notification to slack'
|
||||
if: failure()
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment-failure
|
||||
deployment-target: staging
|
||||
|
||||
report_test_failure:
|
||||
runs-on: ubuntu-latest
|
||||
name: 'Report test failure to oodikone notifications'
|
||||
needs:
|
||||
- lint_backend
|
||||
- lint_frontend
|
||||
- test_course_statistics
|
||||
- test_custom_population
|
||||
- test_filters
|
||||
- test_population_statistics
|
||||
- test_student_statistics
|
||||
- test_completed_courses
|
||||
- test_studyprogramme_overview
|
||||
- test_teachers_and_users
|
||||
- test_faculty_statistics
|
||||
- test_open_uni_search
|
||||
- test_service_exit_statuses
|
||||
if: failure()
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: test-failure
|
||||
+1
-1
@@ -7,7 +7,7 @@ module.exports = {
|
||||
'{services,updater}/**/*.{js,jsx}': files => `eslint --fix ${files.join(' ')}`,
|
||||
'*.{js,json,md,yml,yaml,html}': files => `prettier --write ${files.join(' ')}`,
|
||||
'*.css': files => `stylelint --fix ${files.join(' ')}`,
|
||||
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.7.0 hadolint ${relativeFilePaths(files)}`,
|
||||
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.12.0 hadolint ${relativeFilePaths(files)}`,
|
||||
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck:v0.7.2 ${relativeFilePaths(files)} -x`,
|
||||
'.github/{workflows,actions}/*': files => `npm run actionlint ${relativeFilePaths(files)}`,
|
||||
'docker-compose*': files => {
|
||||
|
||||
@@ -8,15 +8,18 @@ const deleteTag = name => {
|
||||
}
|
||||
|
||||
const getEmptyYears = isAcademicYear => {
|
||||
const thisYear = new Date().getFullYear()
|
||||
const today = new Date()
|
||||
const latestYear = isAcademicYear && today.getMonth() < 7 ? today.getFullYear() - 1 : today.getFullYear()
|
||||
|
||||
const years = []
|
||||
for (let year = thisYear; year > 2021; year--) {
|
||||
for (let year = latestYear; year > 2021; year--) {
|
||||
if (isAcademicYear) {
|
||||
years.push(`${year} - ${year + 1}`)
|
||||
} else {
|
||||
years.push(year)
|
||||
}
|
||||
}
|
||||
console.log({ years, latestYear, isAcademicYear })
|
||||
return years
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ services:
|
||||
SECRET_TOKEN: dev
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
|
||||
IMPORTER_DB_API_URL: 'https://importer.cs.helsinki.fi/api/importer'
|
||||
JAMI_URL: 'http://jami:3003/'
|
||||
image: toska/oodikone2-backend:staging
|
||||
|
||||
frontend:
|
||||
|
||||
+9
-7
@@ -4,7 +4,7 @@ x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
x-build_defaults: &build_defaults
|
||||
target: development
|
||||
dockerfile: dev.Dockerfile
|
||||
|
||||
services:
|
||||
adminer:
|
||||
@@ -39,13 +39,15 @@ services:
|
||||
SECRET_TOKEN: dev
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
|
||||
IMPORTER_DB_API_URL: 'https://importer.cs.helsinki.fi/api/importer'
|
||||
JAMI_URL: 'http://jami:3003/'
|
||||
ports:
|
||||
- 8080:8080
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- backend-nodemod:/usr/src/app/node_modules
|
||||
- ./services/backend/:/usr/src/app
|
||||
- backend-nodemod:/opt/app-root/src/node_modules
|
||||
- ./services/backend/:/opt/app-root/src
|
||||
|
||||
frontend:
|
||||
<<: *defaults
|
||||
@@ -58,8 +60,8 @@ services:
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- frontend-nodemod:/usr/src/app/node_modules
|
||||
- ./services/frontend/:/usr/src/app
|
||||
- frontend-nodemod:/opt/app-root/src/node_modules
|
||||
- ./services/frontend/:/opt/app-root/src
|
||||
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
@@ -108,7 +110,7 @@ services:
|
||||
sis-updater-scheduler:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
target: development
|
||||
context: updater/sis-updater-scheduler
|
||||
container_name: sis-updater-scheduler
|
||||
depends_on:
|
||||
@@ -137,7 +139,7 @@ services:
|
||||
sis-updater-worker:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
target: development
|
||||
context: updater/sis-updater-worker
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
+16
-18
@@ -1,23 +1,21 @@
|
||||
FROM node:14.17-alpine3.13 as base
|
||||
FROM registry.access.redhat.com/ubi8/nodejs-14-minimal
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM base as development
|
||||
ENV NODE_ENV=development
|
||||
COPY ./package* /usr/src/app/
|
||||
RUN npm ci
|
||||
COPY . /usr/src/app/
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
||||
FROM base as production
|
||||
ENV NODE_ENV=production
|
||||
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 && apk add --no-cache vim curl nano
|
||||
COPY --chown=node:node . /usr/src/app/
|
||||
USER node
|
||||
|
||||
ARG BASE_PATH
|
||||
ENV PUBLIC_URL=$BASE_PATH
|
||||
|
||||
ARG STAGING
|
||||
ENV REACT_APP_STAGING=$STAGING
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci --omit-dev
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM node:14.17-alpine3.13
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENV NODE_ENV=development
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
@@ -1,7 +1,7 @@
|
||||
// Node env to use
|
||||
const isStaging = process.env.NODE_ENV === 'staging'
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const isStaging = process.env.REACT_APP_STAGING === 'true'
|
||||
const isProduction = !isStaging && process.env.NODE_ENV === 'production'
|
||||
|
||||
// Sentry
|
||||
const sentryRelease = process.env.SENTRY_RELEASE || ''
|
||||
@@ -17,9 +17,10 @@ const requiredGroup = isStaging
|
||||
const pateToken = process.env.PATE_API_TOKEN || ''
|
||||
|
||||
// Jami
|
||||
const jamiUrl = isDev || runningInCI ? 'http://jami:3003/' : 'https://importer.cs.helsinki.fi/api/auth'
|
||||
const jamiUrl = process.env.JAMI_URL || ''
|
||||
|
||||
// Importer client
|
||||
const importerUrl = process.env.IMPORTER_DB_API_URL || ''
|
||||
const importerToken = process.env.IMPORTER_DB_API_TOKEN || ''
|
||||
|
||||
// Networking: Urls & ports
|
||||
@@ -86,5 +87,6 @@ module.exports = {
|
||||
baseUrl,
|
||||
frontUrl,
|
||||
backendPort,
|
||||
importerUrl,
|
||||
importerToken,
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ const currentUserMiddleware = async (req, _res, next) => {
|
||||
}
|
||||
|
||||
const iamGroups = parseIamGroups(hygroupcn)
|
||||
const { access = {}, specialGroup = {} } = await getOrganizationAccess({ iamGroups })
|
||||
const { access = {}, specialGroup = {} } = await getOrganizationAccess({ id: sisId, iamGroups })
|
||||
const iamRights = Object.keys(access)
|
||||
|
||||
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
|
||||
|
||||
@@ -239,7 +239,13 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
|
||||
res.json(filterPersonalTags(result, user.id))
|
||||
} else {
|
||||
const result = await Population.optimizedStatisticsOf({ ...req.query, studyRights })
|
||||
// If two studyprogrammes are given, query does not work just right
|
||||
// This solution is temporal
|
||||
const result = await Population.optimizedStatisticsOf({
|
||||
...req.query,
|
||||
studyRights: { programme: studyRights.programme },
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).end()
|
||||
|
||||
@@ -525,7 +525,6 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
graduatedStudents
|
||||
) => {
|
||||
// eslint-disable-line
|
||||
|
||||
// db startdate is formatted to utc so need to change it when querying
|
||||
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
|
||||
|
||||
|
||||
@@ -103,7 +103,9 @@ const getYearsArray = (since, isAcademicYear, yearsCombined) => {
|
||||
const years = []
|
||||
const allYears = 'Total'
|
||||
if (yearsCombined) years.push(allYears)
|
||||
for (let i = since; i <= new Date().getFullYear(); i++) {
|
||||
const today = new Date()
|
||||
const until = isAcademicYear && today.getMonth() < 7 ? today.getFullYear() - 1 : today.getFullYear()
|
||||
for (let i = since; i <= until; i++) {
|
||||
const year = isAcademicYear ? `${i} - ${i + 1}` : i
|
||||
years.push(year)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const axios = require('axios')
|
||||
const { importerToken } = require('../conf-backend')
|
||||
const { importerUrl, importerToken } = require('../conf-backend')
|
||||
const logger = require('./logger')
|
||||
|
||||
const importerClient = axios.create({
|
||||
headers: {
|
||||
token: importerToken,
|
||||
},
|
||||
baseURL: 'https://importer.cs.helsinki.fi/api/importer',
|
||||
baseURL: importerUrl,
|
||||
})
|
||||
|
||||
const getImporterClient = () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const Sentry = require('@sentry/node')
|
||||
const Tracing = require('@sentry/tracing')
|
||||
|
||||
const { sentryRelease, sentryEnvironment, runningInCI } = require('../conf-backend')
|
||||
const { sentryRelease, sentryEnvironment, runningInCI, isStaging } = require('../conf-backend')
|
||||
|
||||
const initializeSentry = app => {
|
||||
if (!sentryRelease || !sentryEnvironment || runningInCI) return
|
||||
if (!sentryRelease || !sentryEnvironment || runningInCI || isStaging) return
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://020b79f0cbb14aad94cc9d69a1ea9d52@sentry.cs.helsinki.fi/2',
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
FROM node:14.17-alpine3.13 as base
|
||||
FROM registry.access.redhat.com/ubi8/nodejs-14-minimal
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
FROM base as development
|
||||
COPY ./package* /usr/src/app/
|
||||
RUN npm ci
|
||||
COPY . /usr/src/app/
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
ARG BASE_PATH
|
||||
ENV PUBLIC_URL=$BASE_PATH
|
||||
|
||||
FROM base as production
|
||||
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
|
||||
EXPOSE 5000
|
||||
|
||||
ARG STAGING
|
||||
ENV REACT_APP_STAGING=$STAGING
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci --omit-dev
|
||||
COPY . .
|
||||
|
||||
RUN npm run build && npm install -g serve@12.0.0
|
||||
|
||||
CMD ["serve", "--single", "--listen", "5000", "build"]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM node:14.17-alpine3.13
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "start"]
|
||||
@@ -17,6 +17,7 @@ export default {
|
||||
Opiskelijoiden kumulatiiviset opintopistekertymät opiskelijakohtaisina kuvaajina.
|
||||
Kun hiiren vie kuvaajien päälle, saa näkyviin jokaista kuvaajaa vastaavan opiskelijanumeron.
|
||||
Opiskelijanumeron yhteydessä olevaa ympyrää klikkaamalla siirtyy ”student statistics” puolelle ja näkee valitun opiskelijan opintotiedot.
|
||||
Timantin muotoinen ikoni kertoo opiskelijan valmistuneen valitusta koulutusohjelmasta kyseisellä hetkellä.
|
||||
|
||||
X-akselin skaalaa voi säätää harmaista painikkeista ”1m”, ”3m”, ”6m”, ”YTD”, ”1y” ja ”ALL” tai kuvaajan alla olevan sinisen näkymän harmaita ”kahvoja” siirtämällä.
|
||||
Y-akselin skaalaa voi säätää harmaista painikkeista ”small” ”medium” ja "large”`,
|
||||
@@ -25,8 +26,10 @@ export default {
|
||||
|
||||
Taulukko kertoo opintopistekertymän valitulle opiskelijapopulaatiolle. Opintopisteluokat on suhteutettu tarkasteltavan populaation aloitusvuoteen.
|
||||
Ylin luokka kertoo siksi aina tavoiteajassa etenevien määrän ja suhteellisen osuuden.
|
||||
|
||||
Huom! nollasuorittajissa on mukana myös poissaolevaksi ilmoittautuneet, ellei heitä ole erikseen suodatettu populaatiosta pois.
|
||||
|
||||
Opintopisteet sisältävät opiskelijoiden **kaikki opintopisteet**, eli siis myös ennen valitun tutkinnon alkamista suoritetut opinnot.
|
||||
|
||||
Huom! Nollasuorittajissa on mukana myös poissaolevaksi ilmoittautuneet, ellei heitä ole erikseen suodatettu populaatiosta pois.
|
||||
|
||||
Kategorioita voi myös itsessään käyttää suodattimina. Klikkaamalla rivin vasemmassa reunassa olevaa suodatinta jäävät näkyviin vain sen kategorian opiskelijat.
|
||||
Suodatin aukeaa samalla ikkunan vasempaan laitaan, josta sitä voi käyttää vapaavalintaisilla arvoilla.
|
||||
|
||||
@@ -287,6 +287,36 @@ const resolveStudyRightElement = ({ studyright_elements }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const filterGraduations = (student, selectedStudyRight) => {
|
||||
const graduated = student.studyrights.filter(({ graduated }) => graduated)
|
||||
// eslint-disable-next-line camelcase
|
||||
if (!selectedStudyRight)
|
||||
return graduated.map(({ enddate, studyright_elements }) => {
|
||||
const studyrightElem = studyright_elements
|
||||
? studyright_elements.filter(ele => ele.element_detail?.type === 20)
|
||||
: []
|
||||
let elemName = ''
|
||||
if (studyrightElem.length > 0) {
|
||||
elemName = getTextIn(studyrightElem[0].element_detail?.name)
|
||||
}
|
||||
return {
|
||||
value: new Date(enddate).getTime(),
|
||||
studyright: elemName,
|
||||
}
|
||||
})
|
||||
const selectedGraduation = graduated.find(({ studyrightid }) => studyrightid === selectedStudyRight.studyrightid)
|
||||
if (!selectedGraduation) return []
|
||||
const element = selectedGraduation.studyright_elements
|
||||
? selectedGraduation.studyright_elements.filter(ele => ele.element_detail?.type === 20)
|
||||
: []
|
||||
return [
|
||||
{
|
||||
value: new Date(selectedGraduation.enddate).getTime(),
|
||||
studyright: element.length > 0 ? getTextIn(element[0].element_detail?.name) : '',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const createStudentCreditLines = (
|
||||
students,
|
||||
singleStudent,
|
||||
@@ -322,6 +352,32 @@ const createStudentCreditLines = (
|
||||
courses => courses.reduce(reduceCreditsToPoints, { credits: 0, points: [], singleStudent })
|
||||
)(student.courses)
|
||||
|
||||
const graduation =
|
||||
points.length === 0
|
||||
? null
|
||||
: student.studyrights
|
||||
.filter(({ graduated }) => graduated)
|
||||
.find(({ studyright_elements }) =>
|
||||
studyright_elements.find(
|
||||
({ element_detail }) => element_detail.code === programmeCode && element_detail.type === 20
|
||||
)
|
||||
)
|
||||
|
||||
if (graduation) {
|
||||
const graduationX = new Date(graduation.enddate).getTime()
|
||||
const index = points.findIndex(point => point.x > graduationX)
|
||||
let graduationY
|
||||
if (index <= 0) {
|
||||
graduationY = graduationX > points[0].x ? points[points.length - 1].y : points[0].y
|
||||
} else {
|
||||
const yBefore = points[index - 1].y
|
||||
const yAfter = points[index].y
|
||||
graduationY = Math.round((yBefore + yAfter) / 2)
|
||||
}
|
||||
points.push({ x: graduationX, y: graduationY, marker: { enabled: true, radius: 10, symbol: 'diamond' } })
|
||||
points.sort((a, b) => a.x - b.x)
|
||||
}
|
||||
|
||||
return {
|
||||
name: student.studentNumber,
|
||||
data: points,
|
||||
@@ -333,40 +389,14 @@ const createStudentCreditLines = (
|
||||
}
|
||||
})
|
||||
|
||||
const filterGraduations = (student, selectedStudyRight) => {
|
||||
const graduated = student.studyrights.filter(({ graduated }) => graduated)
|
||||
// eslint-disable-next-line camelcase
|
||||
if (!selectedStudyRight)
|
||||
return graduated.map(({ enddate, studyright_elements }) => {
|
||||
const studyrightElem = studyright_elements
|
||||
? studyright_elements.filter(ele => ele.element_detail?.type === 20)
|
||||
: []
|
||||
let elemName = ''
|
||||
if (studyrightElem.length > 0) {
|
||||
elemName = getTextIn(studyrightElem[0].element_detail?.name)
|
||||
}
|
||||
return {
|
||||
value: new Date(enddate).getTime(),
|
||||
studyright: elemName,
|
||||
}
|
||||
})
|
||||
const selectedGraduation = graduated.find(({ studyrightid }) => studyrightid === selectedStudyRight.studyrightid)
|
||||
if (!selectedGraduation) return []
|
||||
const element = selectedGraduation.studyright_elements
|
||||
? selectedGraduation.studyright_elements.filter(ele => ele.element_detail?.type === 20)
|
||||
: []
|
||||
return [
|
||||
{
|
||||
value: new Date(selectedGraduation.enddate).getTime(),
|
||||
studyright: element.lenght > 0 ? getTextIn(element[0].element_detail?.name) : '',
|
||||
},
|
||||
]
|
||||
}
|
||||
const findTransferName = (student, transfer) =>
|
||||
student.studyrights.flatMap(right => right.studyright_elements).find(elem => elem.code === transfer.targetcode)
|
||||
.element_detail.name
|
||||
|
||||
const filterTransfers = student => {
|
||||
const filterTransfers = (student, language) => {
|
||||
const transferTimes = student.transfers.map(transfer => ({
|
||||
value: new Date(transfer.transferdate).getTime(),
|
||||
studyright: `From ${transfer.sourcecode} to ${transfer.targetcode}`,
|
||||
studyright: `to ${getTextIn(findTransferName(student, transfer), language)}`,
|
||||
}))
|
||||
const removeOverlapping = transferTimes.filter((transfer, i, a) => a.indexOf(transfer) === i)
|
||||
return removeOverlapping
|
||||
@@ -468,9 +498,8 @@ const CreditAccumulationGraphHighCharts = ({
|
||||
return Math.min(..._.flatten(students.map(({ courses }) => courses.map(({ date }) => new Date(date).getTime()))))
|
||||
return studyRightStart
|
||||
}
|
||||
|
||||
const graduations = singleStudent ? filterGraduations(students[0], selectedStudyRight) : []
|
||||
const transfers = singleStudent ? filterTransfers(students[0]) : []
|
||||
const transfers = singleStudent ? filterTransfers(students[0], language) : []
|
||||
const studyRightStartLine =
|
||||
!singleStudent && studyPlanFilterIsActive
|
||||
? [new Date(customStudyStartYear || students[0].studyrightStart).getTime()]
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ const CreditsGainedTable = ({ filteredStudents, type, year, creditDateFilterOpti
|
||||
months = getMonths(start, endDate)
|
||||
title = `${moment(start).format('DD.MM.YYYY')} and ${moment(endDate).format('DD.MM.YYYY')}`
|
||||
creditList = filteredStudents.map(student =>
|
||||
student.courses.lenght > 0
|
||||
student.courses.length > 0
|
||||
? student.courses.reduce(
|
||||
(results, course) =>
|
||||
results + (course.passed && endDate.isSameOrAfter(course.date, 'day')) ? course.credits : 0,
|
||||
|
||||
@@ -87,10 +87,6 @@ const PopulationDetails = ({
|
||||
return null
|
||||
}
|
||||
|
||||
/* if (filteredStudents.length === 0) {
|
||||
return <Message negative content="No statistics found for the given query." />
|
||||
} */
|
||||
|
||||
const onlyIamRights = !authLoading && !isAdmin && !rights.includes(query?.studyRights?.programme)
|
||||
|
||||
const panels = [
|
||||
|
||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import useFilters from 'components/FilterView/useFilters'
|
||||
import { hopsFilter } from 'components/FilterView/filters'
|
||||
import Toggle from 'components/StudyProgramme/Toggle'
|
||||
import PopulationSearchForm from './PopulationSearchForm'
|
||||
import PopulationSearchHistory from './PopulationSearchHistory'
|
||||
import ProgressBar from '../ProgressBar'
|
||||
@@ -11,7 +12,7 @@ import InfoBox from '../Info/InfoBox'
|
||||
import { useProgress } from '../../common/hooks'
|
||||
import infotoolTips from '../../common/InfoToolTips'
|
||||
|
||||
const PopulationSearch = ({ populationFound, history, location, loading }) => {
|
||||
const PopulationSearch = ({ populationFound, history, location, loading, unifiedProgramme }) => {
|
||||
const { onProgress, progress } = useProgress(loading)
|
||||
const { filterDispatch, useFilterSelector } = useFilters()
|
||||
|
||||
@@ -49,7 +50,17 @@ const PopulationSearch = ({ populationFound, history, location, loading }) => {
|
||||
label="Show only credits included in study plan"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
{unifiedProgramme && unifiedProgramme.combinedProgrammeCode && (
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="programmeToggle"
|
||||
firstLabel={`Filter by graduated and studyplan credits by ${unifiedProgramme.combinedProgrammeCode} `}
|
||||
secondLabel={unifiedProgramme.programmeCode}
|
||||
value={unifiedProgramme.filterByBachelor}
|
||||
setValue={unifiedProgramme.setFilterByBachelor}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<PopulationSearchHistory history={history} />
|
||||
</Form>
|
||||
)}
|
||||
|
||||
@@ -175,6 +175,7 @@ export default ({ students, programmeCode }) => {
|
||||
}),
|
||||
'phone number': s.phoneNumber,
|
||||
email: s.email,
|
||||
secondaryEmail: s.secondaryEmail,
|
||||
'transferred from': s.transferredStudyright ? transferFrom(s) : '',
|
||||
priority: priorityText(s.studyrights),
|
||||
extent: extentCodes(s.studyrights),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useLocation, useHistory } from 'react-router-dom'
|
||||
import { Header, Segment } from 'semantic-ui-react'
|
||||
@@ -43,23 +43,25 @@ const PopulationStatistics = () => {
|
||||
const { query, queryIsSet, isLoading, selectedStudentsByYear, samples } = useSelector(
|
||||
populationToData.makePopulationsToData
|
||||
)
|
||||
|
||||
// Option to set other Study programme in cases of combined view (e.g., eläinlääkkis)
|
||||
const [filterByBachelor, setFilterByBachelor] = useState(true)
|
||||
useTitle('Class statistics')
|
||||
|
||||
const { data: allSemesters } = useGetSemestersQuery()
|
||||
|
||||
const programmeCode = query?.studyRights?.programme
|
||||
const combinedProgrammeCode = query?.studyRights?.combinedProgramme
|
||||
const programmes = useSelector(store => store.populationProgrammes?.data?.programmes)
|
||||
const programmeName = programmes && programmeCode ? programmes[programmeCode]?.name : ''
|
||||
|
||||
const combinedProgrammeName = programmes && combinedProgrammeCode ? programmes[combinedProgrammeCode]?.name : ''
|
||||
const chosenProgrammeCode = filterByBachelor || !combinedProgrammeCode ? programmeCode : combinedProgrammeCode
|
||||
const filters = [
|
||||
studentNumberFilter,
|
||||
hopsFilter({ programmeCode }),
|
||||
hopsFilter({ programmeCode: chosenProgrammeCode }),
|
||||
genderFilter,
|
||||
ageFilter,
|
||||
courseFilter({ courses }),
|
||||
creditsEarnedFilter,
|
||||
graduatedFromProgrammeFilter({ code: programmeCode }),
|
||||
graduatedFromProgrammeFilter({ code: chosenProgrammeCode }),
|
||||
transferredToProgrammeFilter,
|
||||
startYearAtUniFilter,
|
||||
tagsFilter,
|
||||
@@ -68,14 +70,14 @@ const PopulationStatistics = () => {
|
||||
allSemesters: allSemesters?.semesters ?? [],
|
||||
language,
|
||||
}),
|
||||
studyTrackFilter({ code: programmeCode }),
|
||||
studyrightStatusFilter({ code: programmeCode }),
|
||||
studyTrackFilter({ code: chosenProgrammeCode }),
|
||||
studyrightStatusFilter({ code: chosenProgrammeCode }),
|
||||
]
|
||||
|
||||
if (parseInt(query?.year, 10) >= 2020) {
|
||||
filters.push(
|
||||
admissionTypeFilter({
|
||||
programme: programmeCode,
|
||||
programme: chosenProgrammeCode,
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -84,9 +86,9 @@ const PopulationStatistics = () => {
|
||||
if (!samples) {
|
||||
return []
|
||||
}
|
||||
|
||||
return samples.map(student => {
|
||||
const hopsCredits = student.studyplans.find(plan => plan.programme_code === programmeCode)?.completed_credits ?? 0
|
||||
const hopsCredits =
|
||||
student.studyplans?.find(plan => plan.programme_code === chosenProgrammeCode)?.completed_credits ?? 0
|
||||
const studyrightStartDate = new Date(student.studyrightStart)
|
||||
const courses = student.courses.filter(({ date }) => new Date(date) >= studyrightStartDate)
|
||||
const credits = getStudentTotalCredits({ courses })
|
||||
@@ -97,10 +99,23 @@ const PopulationStatistics = () => {
|
||||
credits,
|
||||
}
|
||||
})
|
||||
}, [samples, programmeCode])
|
||||
}, [samples, chosenProgrammeCode])
|
||||
const unifyProgrammeName = (bachelor, masterLisenciate) => {
|
||||
if (language === 'fi')
|
||||
return `${bachelor} ja ${
|
||||
masterLisenciate?.includes('lisensiaatin') ? 'lisensiaatin koulutusojelma' : 'maisterin koulutusohjelma'
|
||||
}`
|
||||
if (language === 'en') return `${bachelor?.split(' ')[0]} and ${masterLisenciate}`
|
||||
if (language === 'sv') return `${bachelor?.split('programmet')[0]}- och ${masterLisenciate}`
|
||||
return bachelor
|
||||
}
|
||||
|
||||
const programmeText =
|
||||
query?.studyRights?.combinedProgramme !== undefined
|
||||
? unifyProgrammeName(getTextIn(programmeName, language), getTextIn(combinedProgrammeName, language))
|
||||
: getTextIn(programmeName, language)
|
||||
const title = location.search === '' ? 'Class statistics' : `${programmeText} ${getYearText(query?.year)}`
|
||||
|
||||
const title =
|
||||
location.search === '' ? 'Class statistics' : `${getTextIn(programmeName, language)} ${getYearText(query?.year)}`
|
||||
return (
|
||||
<FilterView
|
||||
name="PopulationStatistics"
|
||||
@@ -131,13 +146,23 @@ const PopulationStatistics = () => {
|
||||
</Header>
|
||||
|
||||
<Segment className="contentSegment">
|
||||
<PopulationSearch history={history} location={location} />
|
||||
<PopulationSearch
|
||||
history={history}
|
||||
location={location}
|
||||
unifiedProgramme={{ programmeCode, combinedProgrammeCode, filterByBachelor, setFilterByBachelor }}
|
||||
/>
|
||||
{location.search !== '' ? (
|
||||
<PopulationDetails
|
||||
queryIsSet={queryIsSet}
|
||||
query={query}
|
||||
isLoading={isLoading}
|
||||
dataExport={<DataExport students={filteredStudents} programmeCode={query?.studyRights?.programme} />}
|
||||
dataExport={
|
||||
<DataExport
|
||||
students={filteredStudents}
|
||||
programmeCode={query?.studyRights?.programme}
|
||||
combinedProgrammeCode={combinedProgrammeCode}
|
||||
/>
|
||||
}
|
||||
allStudents={samples}
|
||||
selectedStudentsByYear={selectedStudentsByYear}
|
||||
filteredStudents={filteredStudents}
|
||||
|
||||
@@ -156,7 +156,7 @@ const ProgressTable = ({ criteria, students, months, programme, studyGuidanceGro
|
||||
|
||||
const renderSemester = ({ enrollmenttype }) => {
|
||||
const { className } = enrolmentTypes[enrollmenttype]
|
||||
return <div className={`enrollment-label ${className}`}> </div>
|
||||
return <div className={`enrollment-label-no-margin ${className}`}> </div>
|
||||
}
|
||||
|
||||
const createContent = (labels, year, start, end, enrollStatusIdx) => {
|
||||
@@ -599,9 +599,10 @@ const ProgressTable = ({ criteria, students, months, programme, studyGuidanceGro
|
||||
<Icon fitted name="minus" color="grey" />: Student has enrolled, but has not received any grade from the
|
||||
course.
|
||||
<br />
|
||||
<span className="enrollment-label label-present" />: Student has an active semester enrollment. <br />
|
||||
<span className="enrollment-label label-absent" />: Student has enrolled as absent. <br />
|
||||
<span className="enrollment-label label-passive" />: Student has no semester enrollment.
|
||||
<span className="enrollment-label-no-margin label-present" />: Student has an active semester enrollment.{' '}
|
||||
<br />
|
||||
<span className="enrollment-label-no-margin label-absent" />: Student has enrolled as absent. <br />
|
||||
<span className="enrollment-label-no-margin label-passive" />: Student has no semester enrollment.
|
||||
</p>
|
||||
</Message>
|
||||
<Tab.Pane>
|
||||
|
||||
@@ -513,11 +513,14 @@ const GeneralTab = ({
|
||||
/>
|
||||
</>
|
||||
),
|
||||
textTitle: 'Email',
|
||||
getRowVal: s => s.email,
|
||||
},
|
||||
{
|
||||
key: 'copyEmail',
|
||||
getRowVal: s =>
|
||||
textTitle: 'Secondary email',
|
||||
getRowVal: s => s.secondaryEmail,
|
||||
getRowContent: s =>
|
||||
s.email && !s.obfuscated ? (
|
||||
<Popup
|
||||
trigger={
|
||||
|
||||
+10
@@ -38,6 +38,16 @@
|
||||
min-height: 15px;
|
||||
}
|
||||
|
||||
.enrollment-label-no-margin {
|
||||
border-radius: 0.28rem;
|
||||
box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5;
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
min-width: 15px;
|
||||
min-height: 15px;
|
||||
}
|
||||
|
||||
.label-present {
|
||||
background-color: #03cc0d;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ import InfotoolTips from '../../../common/InfoToolTips'
|
||||
import '../studyprogramme.css'
|
||||
|
||||
const isNewProgramme = code => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code)
|
||||
const getGraduatedText = code => {
|
||||
if (code.slice(0, 1) === 'T' || code.slice(0, 3) === 'LIS') {
|
||||
return 'Graduated of the programme'
|
||||
}
|
||||
return 'Graduated and thesis writers of the programme'
|
||||
}
|
||||
|
||||
const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYear, setAcademicYear }) => {
|
||||
const [showMedian, setShowMedian] = useState(false)
|
||||
@@ -147,7 +153,7 @@ const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYea
|
||||
)}
|
||||
{graduations.isSuccess && graduations.data && (
|
||||
<>
|
||||
{getDivider('Graduated and thesis writers of the programme', 'GraduatedAndThesisWritersOfTheProgramme')}
|
||||
{getDivider(getGraduatedText(studyprogramme), 'GraduatedAndThesisWritersOfTheProgramme')}
|
||||
<div className="section-container">
|
||||
<BarChart cypress="GraduatedAndThesisWritersOfTheProgramme" data={graduations?.data} />
|
||||
<DataTable
|
||||
|
||||
+40
-12
@@ -9,15 +9,47 @@ const getMonths = year => {
|
||||
const start = `${year}-08-01`
|
||||
return Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(start))).asMonths())
|
||||
}
|
||||
const getTotalPopulationLink = (studytrack, studyprogramme, combinedProgramme, months, yearsString) => {
|
||||
if (studytrack) {
|
||||
return (
|
||||
`/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"studyTrack"%3A"${studytrack}"%7D&year=All&years=${yearsString}`
|
||||
)
|
||||
}
|
||||
if (combinedProgramme) {
|
||||
return (
|
||||
`/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"combinedProgramme"%3A"${combinedProgramme}"%7D&year=All&years=${yearsString}`
|
||||
)
|
||||
}
|
||||
return (
|
||||
`/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=All&years=${yearsString}`
|
||||
)
|
||||
}
|
||||
|
||||
const PopulationStatisticsLink = ({ studytrack, studyprogramme, year }) => {
|
||||
const getPopulationLink = (studytrack, studyprogramme, combinedProgramme, startYear, months) => {
|
||||
if (studytrack) {
|
||||
return (
|
||||
`/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"studyTrack"%3A"${studytrack}"%7D&year=${startYear}`
|
||||
)
|
||||
}
|
||||
if (combinedProgramme) {
|
||||
return (
|
||||
`/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"combinedProgramme"%3A"${combinedProgramme}"%7D&year=${startYear}`
|
||||
)
|
||||
}
|
||||
return (
|
||||
`/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=${startYear}`
|
||||
)
|
||||
}
|
||||
const PopulationStatisticsLink = ({ studytrack, studyprogramme, combinedProgramme, year }) => {
|
||||
const startYear = Number(year.slice(0, 4))
|
||||
const months = Math.ceil(moment.duration(moment().diff(`${startYear}-08-01`)).asMonths())
|
||||
const href = studytrack
|
||||
? `/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"studyTrack"%3A"${studytrack}"%7D&year=${startYear}`
|
||||
: `/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=${startYear}`
|
||||
const href = getPopulationLink(studytrack, studyprogramme, combinedProgramme, startYear, months)
|
||||
return (
|
||||
<Link title={`Population statistics of class ${year}`} to={href}>
|
||||
<Icon name="level up alternate" />
|
||||
@@ -25,14 +57,10 @@ const PopulationStatisticsLink = ({ studytrack, studyprogramme, year }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const TotalPopulationLink = ({ studytrack, studyprogramme, years }) => {
|
||||
const TotalPopulationLink = ({ studytrack, studyprogramme, combinedProgramme, years }) => {
|
||||
const yearsString = years.join('&years=')
|
||||
const months = getMonths(Math.min(...years.map(year => Number(year))))
|
||||
const href = studytrack
|
||||
? `/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"studyTrack"%3A"${studytrack}"%7D&year=All&years=${yearsString}`
|
||||
: `/populations?months=${months}&semesters=FALL&semesters=` +
|
||||
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=All&years=${yearsString}`
|
||||
const href = getTotalPopulationLink(studytrack, studyprogramme, combinedProgramme, years, months, yearsString)
|
||||
return (
|
||||
<Link title="Population statistics of all years" to={href}>
|
||||
<Icon name="level up alternate" />
|
||||
|
||||
Reference in New Issue
Block a user