mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a737d2cc7 | ||
|
|
099ee64639 | ||
|
|
73c491aaae | ||
|
|
dca980e6af | ||
|
|
3187c4bcee | ||
|
|
601107796a | ||
|
|
6df8ab6fa4 | ||
|
|
17387514c5 | ||
|
|
b36fcfc730 | ||
|
|
d1745003b4 | ||
|
|
e17fa0886f | ||
|
|
6633a2f2b4 | ||
|
|
c1ccb22470 | ||
|
|
cef98cca22 | ||
|
|
17a1095f00 | ||
|
|
94bb1d6b39 | ||
|
|
4cefe627fb | ||
|
|
28594af9a2 | ||
|
|
85905d98fa | ||
|
|
3b1faff253 | ||
|
|
c8e1db5f19 | ||
|
|
6fb102808d | ||
|
|
e910f8660b | ||
|
|
a0ba838f3b | ||
|
|
faac4aa816 | ||
|
|
48b8177133 | ||
|
|
f87800cbfc | ||
|
|
f2afd9e91a | ||
|
|
f94b3e18af | ||
|
|
5384074527 | ||
|
|
366abfc0d9 | ||
|
|
08cfb3de80 | ||
|
|
8c6326a668 | ||
|
|
626d3cf462 | ||
|
|
5c7931ac5c | ||
|
|
6bf613fb03 | ||
|
|
fec01a2c02 | ||
|
|
24e5be335f | ||
|
|
c19017b2bd | ||
|
|
eea6fb0918 | ||
|
|
674ea56f99 | ||
|
|
ce49e64e45 | ||
|
|
8b483cf4e8 | ||
|
|
bc8febe25e | ||
|
|
00ee23750c | ||
|
|
7634179d78 | ||
|
|
8c58ffd9e7 | ||
|
|
6208691147 | ||
|
|
19d7a53f1d | ||
|
|
11d039da25 | ||
|
|
527206981d | ||
|
|
57546e4455 | ||
|
|
39dbd37469 | ||
|
|
5c50088152 | ||
|
|
621f2e45e7 | ||
|
|
6808f0a5ae | ||
|
|
7f8df7acee | ||
|
|
be12a44bbf | ||
|
|
904058aa02 | ||
|
|
8331c8ef77 | ||
|
|
75686c3768 | ||
|
|
3509644f05 | ||
|
|
96095fa218 | ||
|
|
8aa2dc70d8 | ||
|
|
321a2da3ef | ||
|
|
8627ba85c6 | ||
|
|
6948e7c2df | ||
|
|
462af37d37 | ||
|
|
3b93d970de | ||
|
|
26fad4a031 | ||
|
|
ed28394566 | ||
|
|
f9467a8edb | ||
|
|
22fbfd6f29 | ||
|
|
a0ce5d5b0f | ||
|
|
5f252d5470 | ||
|
|
e232dc684e | ||
|
|
35b7c4e38e | ||
|
|
00db629958 | ||
|
|
307d87c3e6 | ||
|
|
265c24421e | ||
|
|
8269e4bce8 | ||
|
|
24c9e7b50f | ||
|
|
06a2b0655e | ||
|
|
fa55bcfa79 | ||
|
|
6b48e86848 | ||
|
|
8d131131f7 | ||
|
|
478079ac74 | ||
|
|
38f40c1dc5 | ||
|
|
983f110ffa | ||
|
|
a1be78fc18 | ||
|
|
5a8b36d028 | ||
|
|
bef5fe1881 | ||
|
|
1135d9ed99 | ||
|
|
747d2729e5 | ||
|
|
01dc6f183b | ||
|
|
0f68978909 | ||
|
|
b8235a88e2 | ||
|
|
a64b961a4b | ||
|
|
b058f19e47 | ||
|
|
7346e666c5 | ||
|
|
ad7de5c63d | ||
|
|
b54eef59af | ||
|
|
862a75dd7a | ||
|
|
a89daf5f4f | ||
|
|
51ed24bfdc | ||
|
|
87ff19e595 | ||
|
|
29e2654c33 | ||
|
|
2fca45c394 | ||
|
|
c915e305f0 | ||
|
|
e1585a77b1 | ||
|
|
1dabb7a26b | ||
|
|
ed6f0f1337 | ||
|
|
9cfdc42a8d | ||
|
|
200401b7be | ||
|
|
95d85cd24d | ||
|
|
52ef63d9e3 | ||
|
|
3a4c0df383 | ||
|
|
61d35cbd26 | ||
|
|
0737722e5a | ||
|
|
3b582ba5d3 | ||
|
|
6c31202923 | ||
|
|
0fa7670e94 | ||
|
|
586a257098 | ||
|
|
b010963b56 | ||
|
|
3591297247 | ||
|
|
7edc2ac5a8 | ||
|
|
25d5eb9561 | ||
|
|
53416146bd | ||
|
|
11c217a9ad | ||
|
|
fc63a895a5 | ||
|
|
9bf0ae3771 | ||
|
|
5a458120ff | ||
|
|
d8f2aa5bef | ||
|
|
31402e5b71 | ||
|
|
ac99125c2b | ||
|
|
b835c0938c | ||
|
|
61e252dabb | ||
|
|
f2a6955666 | ||
|
|
b52517891c | ||
|
|
082d1466a9 | ||
|
|
19d01bf2c9 | ||
|
|
d9a099f3b9 | ||
|
|
448cf2d14e | ||
|
|
a8b570e4bb | ||
|
|
fbdf23125b | ||
|
|
a0b2fae993 | ||
|
|
c11e588e13 | ||
|
|
65f7808c36 | ||
|
|
060297676d | ||
|
|
a3055e518d | ||
|
|
d2b5231c31 | ||
|
|
1d2c54fe4b | ||
|
|
433d57ff24 | ||
|
|
fb84c9f1e5 | ||
|
|
d1f87d0952 | ||
|
|
7e54071f07 | ||
|
|
df091f8645 | ||
|
|
776ac42cc6 | ||
|
|
6d36a1a37a | ||
|
|
939235051e | ||
|
|
320f31e224 | ||
|
|
1148fe7a0d | ||
|
|
4ce9f1c26f | ||
|
|
b51c8ef2ee | ||
|
|
a34aa5967e | ||
|
|
0db528f3a3 | ||
|
|
84d43be5d6 | ||
|
|
2cc928f24b | ||
|
|
e903e81cdb | ||
|
|
20a5a74bc0 | ||
|
|
6965b830f1 | ||
|
|
3f778e65ac | ||
|
|
8f3a144f30 | ||
|
|
bcf31de217 | ||
|
|
59a60dbc56 | ||
|
|
497c14fbf4 | ||
|
|
cb89c2fcc7 | ||
|
|
9544156a57 | ||
|
|
d8752e7f65 | ||
|
|
228e50a763 | ||
|
|
c28182101f | ||
|
|
b4742d3862 | ||
|
|
63a4a20103 | ||
|
|
bc5ca17fe5 | ||
|
|
a0f56461fd | ||
|
|
24c8c9eaec | ||
|
|
85df38552f | ||
|
|
9e970d7a9d | ||
|
|
e4f118b234 | ||
|
|
d0cdf4908c | ||
|
|
5276906203 | ||
|
|
9dedc05d6b | ||
|
|
3ce4eaca61 | ||
|
|
d084a41ffe | ||
|
|
3123ef88dd | ||
|
|
865eacc6d4 | ||
|
|
0d0107c9c3 | ||
|
|
3c91747283 | ||
|
|
6c18abb88b | ||
|
|
887d0a6ca3 | ||
|
|
0568756662 | ||
|
|
ad2c4104f8 | ||
|
|
fe0eab8a26 | ||
|
|
6251eeb297 | ||
|
|
4317383c9e | ||
|
|
65b73234e7 | ||
|
|
2932e0b631 | ||
|
|
745342d985 | ||
|
|
4245599321 | ||
|
|
a9e7cb714d | ||
|
|
37db94f2ad | ||
|
|
731aa5dfc3 | ||
|
|
f65cb9f8ed | ||
|
|
8b8fd233d5 | ||
|
|
da7587b871 | ||
|
|
8e21c47487 | ||
|
|
63f1f76e58 | ||
|
|
dea5126a5d | ||
|
|
bf38483d32 | ||
|
|
b8a87b7bd4 | ||
|
|
6e2718897f | ||
|
|
db41071f1b | ||
|
|
09b90bb6db | ||
|
|
972e26aa89 | ||
|
|
40d68a34a3 | ||
|
|
a3005fd325 | ||
|
|
e745fbe0f5 | ||
|
|
3bc8fd2ce8 | ||
|
|
6365104f97 | ||
|
|
69ce1554a7 | ||
|
|
2cec3f68be | ||
|
|
8fd8a962f4 | ||
|
|
c19c251c51 | ||
|
|
fe3c993143 | ||
|
|
88ac7c5c18 | ||
|
|
b81fb676a7 | ||
|
|
293d9d097b | ||
|
|
26826f626a | ||
|
|
4be9149bcd | ||
|
|
ee91d57e3a | ||
|
|
cf01e648e1 | ||
|
|
5a83811cb6 | ||
|
|
311a69a414 | ||
|
|
83fa615ff9 | ||
|
|
12508202d2 | ||
|
|
c4c02c43dd | ||
|
|
207d00579b | ||
|
|
9a2ae922f7 | ||
|
|
af896688d1 | ||
|
|
2d66cf80ba | ||
|
|
28559533f2 | ||
|
|
d6d5008d3f | ||
|
|
7b4e3d8dfa | ||
|
|
3a22617d8d | ||
|
|
359dbbe1c5 | ||
|
|
c6e5a33584 | ||
|
|
a7cd4d08b1 | ||
|
|
618590cb0c | ||
|
|
43087d7e61 | ||
|
|
ccd61eb27c | ||
|
|
f794a45ab6 | ||
|
|
df116a47d5 | ||
|
|
6aebc404be | ||
|
|
16c764df4e | ||
|
|
94d9a43dab | ||
|
|
44d6685af8 | ||
|
|
abc8189616 | ||
|
|
61b4b684d0 | ||
|
|
6b95d6a7b0 | ||
|
|
cce375a3fe | ||
|
|
8c6cc3c89a | ||
|
|
12cf733ae1 | ||
|
|
eb49fcde1a | ||
|
|
05682e973d | ||
|
|
9061a6a3a5 | ||
|
|
785cfd0780 | ||
|
|
ea9daef50b | ||
|
|
8ade9a003b | ||
|
|
78cec1dd89 | ||
|
|
50023d4ab5 | ||
|
|
fe7a0caa1f | ||
|
|
b04fa49df7 | ||
|
|
ba87890445 | ||
|
|
76e2980bb9 | ||
|
|
02b5fdc6f3 | ||
|
|
8ac3ff1786 | ||
|
|
991c92fdaf | ||
|
|
31d01d58de | ||
|
|
33118fc244 | ||
|
|
14496b7934 | ||
|
|
eff7e0a5fb | ||
|
|
dac69b38fd | ||
|
|
7424530d49 | ||
|
|
2f24cb8116 | ||
|
|
a1831d70e0 | ||
|
|
d1b30e415c | ||
|
|
ceaeaf03e8 | ||
|
|
83e95ec8d3 | ||
|
|
1c36563d1b | ||
|
|
3e6cd896cc | ||
|
|
86b2f559cc | ||
|
|
091127ee63 | ||
|
|
a57012d422 | ||
|
|
64a3c84d1b | ||
|
|
75713d4228 | ||
|
|
799e703750 | ||
|
|
e1b0db4a86 | ||
|
|
13e17cf084 | ||
|
|
dde753562d | ||
|
|
3f39e59826 | ||
|
|
4aaba75099 | ||
|
|
f9ed86fec7 | ||
|
|
1aff38613c | ||
|
|
a94f7f73b6 | ||
|
|
b7ac291a48 | ||
|
|
dd6ad7b84b | ||
|
|
e9d049eef9 | ||
|
|
0819c0599d | ||
|
|
4ce9379522 | ||
|
|
b593ee7d2b | ||
|
|
31623bea66 | ||
|
|
05ed218ec6 | ||
|
|
6e55ecac39 | ||
|
|
a91b44c47c | ||
|
|
13817e741f | ||
|
|
d2df706162 | ||
|
|
c9e3439087 | ||
|
|
8717e3cfce | ||
|
|
6a7e721722 |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"presets": [
|
||||
["latest", {
|
||||
"es2015": {
|
||||
"modules": false
|
||||
}
|
||||
}],
|
||||
"react"
|
||||
],
|
||||
"env": {
|
||||
"development": {
|
||||
"plugins": [
|
||||
"react-hot-loader/babel",
|
||||
"transform-object-rest-spread",
|
||||
"transform-class-properties"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"plugins": [
|
||||
"transform-async-to-generator",
|
||||
"inline-json-import",
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
# Populate and change this to .env
|
||||
# To get the crypt key, run `openssl rand -hex 32` or
|
||||
# `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`
|
||||
IMPORTER_DB_API_TOKEN=
|
||||
PATE_TOKEN=
|
||||
CRYPT_KEY=
|
||||
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
config/
|
||||
+44
-11
@@ -1,13 +1,46 @@
|
||||
{
|
||||
"ignorePatterns": [
|
||||
"eslint.config.mjs",
|
||||
"**/build/**",
|
||||
"**/dist/**",
|
||||
"**/node_modules/**",
|
||||
"**/instrumented/**",
|
||||
"**/coverage/**",
|
||||
"nyc-config.cjs",
|
||||
".lintstagedrc.mjs",
|
||||
"cypress.config.js"
|
||||
"root": true,
|
||||
"parser": "babel-eslint",
|
||||
"extends": "airbnb",
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"rules": {
|
||||
"comma-dangle": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"no-param-reassign": [
|
||||
2,
|
||||
{
|
||||
"props": false
|
||||
}
|
||||
],
|
||||
"import/no-unresolved": 2,
|
||||
"react/jsx-no-bind": 2,
|
||||
"react/no-did-mount-set-state": 0,
|
||||
"react/prefer-stateless-function": 2,
|
||||
"react/no-typos": 0,
|
||||
"jsx-a11y/no-static-element-interactions": 0,
|
||||
"jsx-a11y/click-events-have-key-events": 0,
|
||||
"jsx-a11y/label-has-for": 0,
|
||||
"object-curly-newline": "off",
|
||||
"prefer-object-spread/prefer-object-spread": 2,
|
||||
"prefer-arrow-callback": 2,
|
||||
"babel/no-invalid-this": 2,
|
||||
"babel/semi": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"semi": [
|
||||
2,
|
||||
"never"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"import",
|
||||
"prefer-object-spread",
|
||||
"babel"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
name: 'Lint code'
|
||||
description: 'Runs ESLint and Stylelint for frontend and backend'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
- name: 'Install dependencies'
|
||||
shell: bash
|
||||
run: npm ci && npm ci --prefix services/shared && npm ci --prefix services/frontend && npm ci --prefix services/backend
|
||||
- name: 'Run Stylelint for CSS files'
|
||||
shell: bash
|
||||
run: npm run stylelint "services/frontend/src/**/*.css"
|
||||
- name: 'Run ESLint for JS and TS files'
|
||||
shell: bash
|
||||
run: npm run eslint -- services
|
||||
@@ -1,50 +0,0 @@
|
||||
name: 'Run tests'
|
||||
description: 'Runs Cypress tests for individual spec'
|
||||
|
||||
inputs:
|
||||
username:
|
||||
required: true
|
||||
description: 'Toska registry username'
|
||||
password:
|
||||
required: true
|
||||
description: 'Toska registry password'
|
||||
spec:
|
||||
required: true
|
||||
description: 'Path to Cypress file'
|
||||
test_name:
|
||||
required: true
|
||||
description: 'Name of the test'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: 'Login to toska Docker registry'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
username: ${{ inputs.username }}
|
||||
password: ${{ inputs.password }}
|
||||
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
shell: bash
|
||||
run: docker compose --file docker-compose.ci.yml --profile oodikone up --detach --quiet-pull
|
||||
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v7
|
||||
with:
|
||||
browser: chrome
|
||||
spec: ${{ inputs.spec }}
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
name: 'Cypress videos - ${{ inputs.test_name }}'
|
||||
path: ./cypress/videos/*
|
||||
|
||||
- name: 'Print logs on failure'
|
||||
shell: bash
|
||||
if: failure()
|
||||
run: docker compose --file docker-compose.ci.yml --profile oodikone logs
|
||||
@@ -1,187 +0,0 @@
|
||||
name: 'Release to production'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
release_frontend:
|
||||
name: 'Build and release frontend'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: 'Get current time'
|
||||
id: get_current_time
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract release version
|
||||
id: extract_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build frontend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-frontend
|
||||
tags: production ${{ github.sha }}
|
||||
context: services
|
||||
containerfiles: |
|
||||
services/frontend/Dockerfile
|
||||
build-args: |
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
SENTRY_DSN=${{ secrets.OODIKONE_FRONTEND_SENTRY_DSN }}
|
||||
|
||||
- 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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Extract release version
|
||||
id: extract_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build backend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-backend
|
||||
tags: production ${{ github.sha }}
|
||||
context: services
|
||||
containerfiles: |
|
||||
services/backend/Dockerfile
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
|
||||
- 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 }}
|
||||
|
||||
sentry_release_frontend:
|
||||
name: 'Create Sentry release for frontend'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
- release_backend
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Extract release version
|
||||
id: extract_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Pull frontend image and create a container
|
||||
run: docker create --name frontend quay.io/toska/oodikone-frontend:production
|
||||
|
||||
- name: Copy assets from the frontend container
|
||||
run: docker cp frontend:/opt/app-root/frontend/build ./build
|
||||
|
||||
- name: Create Sentry release
|
||||
uses: getsentry/action-release@v3
|
||||
env:
|
||||
SENTRY_URL: https://toska.it.helsinki.fi/
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_TOKEN }}
|
||||
SENTRY_ORG: sentry
|
||||
SENTRY_PROJECT: oodikone-frontend
|
||||
with:
|
||||
environment: production
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/assets'
|
||||
url_prefix: '~/assets'
|
||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
sentry_release_backend:
|
||||
name: 'Create Sentry release for backend'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
- release_backend
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Extract release version
|
||||
id: extract_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Pull backend image and create a container
|
||||
run: docker create --name backend quay.io/toska/oodikone-backend:production
|
||||
|
||||
- name: Copy assets from the backend container
|
||||
run: docker cp backend:/opt/app-root/backend/dist ./dist
|
||||
|
||||
- name: Create Sentry release
|
||||
uses: getsentry/action-release@v3
|
||||
env:
|
||||
SENTRY_URL: https://toska.it.helsinki.fi/
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_TOKEN }}
|
||||
SENTRY_ORG: sentry
|
||||
SENTRY_PROJECT: oodikone-backend
|
||||
with:
|
||||
environment: production
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './dist'
|
||||
url_prefix: '~/opt/app-root/backend/dist/'
|
||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
report_success:
|
||||
name: 'Report deployment success to the "oodikone_notifications" channel'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
- release_backend
|
||||
- sentry_release_frontend
|
||||
- sentry_release_backend
|
||||
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: 'Send notification to Slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment
|
||||
softa-url: https://oodikone.helsinki.fi/
|
||||
deployment-target: production
|
||||
|
||||
report_failure:
|
||||
name: 'Report deployment failure to the "oodikone_notifications" channel'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
- release_backend
|
||||
- sentry_release_frontend
|
||||
- sentry_release_backend
|
||||
|
||||
if: failure()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: 'Send notification to Slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment-failure
|
||||
deployment-target: production
|
||||
@@ -1,98 +0,0 @@
|
||||
name: 'Run tests'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'cypress/**'
|
||||
- 'cypress.config.json'
|
||||
- 'services/**'
|
||||
- '!services/backend/src/rapodiff/**'
|
||||
- 'docker-compose.ci.yml'
|
||||
- 'package*.json'
|
||||
|
||||
jobs:
|
||||
lint_packages:
|
||||
name: 'Lint packages'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Lint backend, frontend and shared
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
test_api:
|
||||
needs:
|
||||
- lint_packages
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install node-24 for cypress
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
|
||||
- name: Start oodikone detached
|
||||
run: npm run oodikone:ci:setup
|
||||
|
||||
- name: Run api-tests
|
||||
run: npm run oodikone:ci:api
|
||||
|
||||
test_end_to_end:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- lint_packages
|
||||
- test_api
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: 'Test course statistics'
|
||||
spec: cypress/e2e/Course_statistics.js
|
||||
- name: 'Test custom population'
|
||||
spec: cypress/e2e/Custom_population.js
|
||||
- name: 'Test population statistics'
|
||||
spec: cypress/e2e/Population_statistics.js
|
||||
- name: 'Test filters'
|
||||
spec: cypress/e2e/Filters.js
|
||||
- name: 'Test students'
|
||||
spec: cypress/e2e/Students.js
|
||||
- name: 'Test completed courses'
|
||||
spec: cypress/e2e/Completed_courses.js
|
||||
- name: 'Test degree programme overview'
|
||||
spec: cypress/e2e/Degree_programme_overview.js
|
||||
- name: 'Test teachers and users'
|
||||
spec: |
|
||||
cypress/e2e/Teachers.js
|
||||
cypress/e2e/Users.js
|
||||
- name: 'Test faculty statistics'
|
||||
spec: cypress/e2e/Faculty_statistics.js
|
||||
- name: 'Test open uni search'
|
||||
spec: cypress/e2e/Openuni.js
|
||||
- name: 'Test language center view'
|
||||
spec: cypress/e2e/Language_center.js
|
||||
- name: 'Test study guidance groups'
|
||||
spec: cypress/e2e/Study_guidance_groups.js
|
||||
- name: 'Test university'
|
||||
spec: cypress/e2e/University.js
|
||||
name: ${{ matrix.name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
spec: ${{ matrix.spec }}
|
||||
test_name: ${{ matrix.name }}
|
||||
@@ -1,127 +0,0 @@
|
||||
name: 'Test and release to staging'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/actions/**'
|
||||
- '.github/workflows/staging.yaml'
|
||||
- '.github/workflows/test.yaml'
|
||||
- 'cypress/**'
|
||||
- 'cypress.config.json'
|
||||
- 'services/**'
|
||||
- '!services/backend/src/rapodiff/**'
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
- 'docker-compose.ci.yml'
|
||||
- 'package*.json'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: UniversityOfHelsinkiCS/oodikone/.github/workflows/test.yaml@master
|
||||
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@v6
|
||||
|
||||
- name: Get current time
|
||||
id: get_current_time
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build frontend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-frontend
|
||||
tags: staging ${{ github.sha }}
|
||||
context: services
|
||||
containerfiles: |
|
||||
services/frontend/Dockerfile
|
||||
build-args: |
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
BASE_PATH=/oodikone
|
||||
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@v6
|
||||
|
||||
- name: Build backend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-backend
|
||||
tags: staging ${{ github.sha }}
|
||||
context: services
|
||||
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 }}
|
||||
|
||||
report_success:
|
||||
name: 'Report deployment success to the "oodikone_notifications" channel'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
- release_backend
|
||||
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: 'Send notification to Slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment
|
||||
softa-url: https://toska-staging.cs.helsinki.fi/oodikone
|
||||
deployment-target: staging
|
||||
|
||||
report_failure:
|
||||
name: 'Report deployment failure to the "oodikone_notifications" channel'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
- release_backend
|
||||
|
||||
if: failure()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: 'Send notification to Slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment-failure
|
||||
deployment-target: staging
|
||||
@@ -1,334 +0,0 @@
|
||||
name: 'Lint and test'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
username:
|
||||
required: true
|
||||
description: 'Toska registry username'
|
||||
password:
|
||||
required: true
|
||||
description: 'Toska registry password'
|
||||
|
||||
jobs:
|
||||
# === Static tests / linting ===
|
||||
lint_packages:
|
||||
name: 'Lint packages'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Lint backend, frontend and shared
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
# === Build database images ===
|
||||
build_backend:
|
||||
name: 'Build backend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
- name: 'Build image and export it'
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=staging
|
||||
context: services
|
||||
file: services/backend/Dockerfile
|
||||
push: false
|
||||
tags: oodikone-backend:staging
|
||||
outputs: type=docker,dest=/tmp/backend.tar
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: backend-image
|
||||
path: /tmp/backend.tar
|
||||
|
||||
build_frontend:
|
||||
name: 'Build frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Get current time
|
||||
id: get_current_time
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
- name: Build frontend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-frontend
|
||||
tags: staging ${{ github.sha }}
|
||||
context: services
|
||||
containerfiles: |
|
||||
services/frontend/Dockerfile
|
||||
build-args: |
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
BASE_PATH=/oodikone
|
||||
STAGING=true
|
||||
|
||||
check_migration_files:
|
||||
name: Check if new migrations have been added
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
kone_db_migrations: ${{ steps.check_migrations.outputs.kone_db_migrations }}
|
||||
user_db_migrations: ${{ steps.check_migrations.outputs.user_db_migrations }}
|
||||
sis_db_migrations: ${{ steps.check_migrations.outputs.sis_db_migrations }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Check if new migrations have been added
|
||||
id: check_migrations
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
kone_db_migrations:
|
||||
- 'services/backend/src/database/migrations_kone/**'
|
||||
- '.github/workflows/test.yaml'
|
||||
user_db_migrations:
|
||||
- 'services/backend/src/database/migrations_user/**'
|
||||
- '.github/workflows/test.yaml'
|
||||
sis_db_migrations:
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
- '.github/workflows/test.yaml'
|
||||
|
||||
build_and_push_anon_sis_db:
|
||||
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
|
||||
needs: check_migration_files
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check_migration_files.outputs.sis_db_migrations == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
- name: 'Let sis-updater-worker run migrations, then push new version of image to toska hub'
|
||||
run: |
|
||||
docker compose --file docker-compose.ci.yml up --detach sis-updater-worker
|
||||
sleep 30 # wait for sis-updater-worker to start and run migrations
|
||||
./anonyymioodi/commit_and_push_to_toska_hub.sh sis-db
|
||||
|
||||
build_and_push_anon_kone_db:
|
||||
name: 'Build anon kone-db and push to toska hub if new migrations have been added'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- check_migration_files
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check_migration_files.outputs.kone_db_migrations == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: 'Download backend image'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: backend-image
|
||||
- name: 'Load built backend image'
|
||||
run: docker load --input backend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
- name: 'Let backend run migrations, then push new version of image to toska hub'
|
||||
run: |
|
||||
docker compose --file docker-compose.ci.yml up --detach backend
|
||||
sleep 30 # wait for backend to start and run migrations
|
||||
./anonyymioodi/commit_and_push_to_toska_hub.sh kone-db
|
||||
|
||||
build_and_push_anon_user_db:
|
||||
name: 'Build anon user-db and push to toska hub if new migrations have been added'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- check_migration_files
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check_migration_files.outputs.user_db_migrations == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: check_migrations
|
||||
with:
|
||||
filters: |
|
||||
migrations:
|
||||
- 'services/backend/src/database/migrations_user/**'
|
||||
- '.github/workflows/test.yaml'
|
||||
- name: 'Download backend image'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: backend-image
|
||||
- name: 'Load built backend image'
|
||||
run: docker load --input backend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
- name: 'Let backend run migrations, then push new version of image to toska hub'
|
||||
run: |
|
||||
docker compose --file docker-compose.ci.yml up --detach backend
|
||||
sleep 30 # wait for backend to start and run migrations
|
||||
./anonyymioodi/commit_and_push_to_toska_hub.sh user-db
|
||||
|
||||
# === Other tests ===
|
||||
test_service_exit_statuses:
|
||||
name: 'Check services run correctly'
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
- lint_packages
|
||||
if: always() && !cancelled() && needs.lint_packages.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker compose --file docker-compose.ci.yml --profile oodikone up --detach --quiet-pull
|
||||
|
||||
- name: 'Check that exit statuses are not errors'
|
||||
run: |
|
||||
docker ps -a
|
||||
if [[ $(docker ps --all | grep -Eo '(Exited|Restarting) \([0-9]+\)' | grep -Eo '[0-9]+' | awk 'BEGIN {sum=0} { sum += $1 } END { print sum }') != '0' ]]; then
|
||||
echo 'Some process had nonzero exit code';
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
- name: 'Save Docker images for downstream jobs'
|
||||
run: |
|
||||
docker save \
|
||||
registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/kone-db \
|
||||
registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-db \
|
||||
registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/user-db \
|
||||
redis:7.2.6 \
|
||||
postgres:15.10 \
|
||||
"quay.io/toska/jami:production" \
|
||||
nginx:latest \
|
||||
| zstd -T0 -3 -o /tmp/docker-images.tar.zst
|
||||
|
||||
- name: 'Upload Docker images artifact'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: docker-images
|
||||
path: /tmp/docker-images.tar.zst
|
||||
retention-days: 1
|
||||
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker compose --file docker-compose.ci.yml --profile oodikone logs
|
||||
|
||||
# === Backend api-tests ===
|
||||
test_api:
|
||||
needs:
|
||||
- test_service_exit_statuses
|
||||
if: always() && !cancelled() && needs.test_service_exit_statuses.result == 'success'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install node-24 for cypress
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: 'Download Docker images'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: docker-images
|
||||
path: /tmp
|
||||
|
||||
- name: 'Load Docker images'
|
||||
run: zstd -d /tmp/docker-images.tar.zst --stdout | docker load
|
||||
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
|
||||
- name: Start oodikone (without frontend and backend)
|
||||
run: npm run oodikone:ci:setup
|
||||
|
||||
- name: Run api-tests
|
||||
run: npm run oodikone:ci:api
|
||||
|
||||
# === End-to-end tests ===
|
||||
test_end_to_end:
|
||||
needs:
|
||||
- test_service_exit_statuses
|
||||
- test_api
|
||||
if: always() && !cancelled() && needs.test_service_exit_statuses.result == 'success'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: 'Test course statistics'
|
||||
spec: cypress/e2e/Course_statistics.js
|
||||
- name: 'Test custom population'
|
||||
spec: cypress/e2e/Custom_population.js
|
||||
- name: 'Test population statistics'
|
||||
spec: cypress/e2e/Population_statistics.js
|
||||
- name: 'Test filters'
|
||||
spec: cypress/e2e/Filters.js
|
||||
- name: 'Test students'
|
||||
spec: cypress/e2e/Students.js
|
||||
- name: 'Test completed courses'
|
||||
spec: cypress/e2e/Completed_courses.js
|
||||
- name: 'Test degree programme overview'
|
||||
spec: cypress/e2e/Degree_programme_overview.js
|
||||
- name: 'Test teachers and users'
|
||||
spec: |
|
||||
cypress/e2e/Teachers.js
|
||||
cypress/e2e/Users.js
|
||||
- name: 'Test faculty statistics'
|
||||
spec: cypress/e2e/Faculty_statistics.js
|
||||
- name: 'Test open uni search'
|
||||
spec: cypress/e2e/Openuni.js
|
||||
- name: 'Test language center view'
|
||||
spec: cypress/e2e/Language_center.js
|
||||
- name: 'Test study guidance groups'
|
||||
spec: cypress/e2e/Study_guidance_groups.js
|
||||
- name: 'Test university'
|
||||
spec: cypress/e2e/University.js
|
||||
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install node-24 for cypress
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: 'Download Docker images'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: docker-images
|
||||
path: /tmp
|
||||
|
||||
- name: 'Load Docker images'
|
||||
run: zstd -d /tmp/docker-images.tar.zst --stdout | docker load
|
||||
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: ${{ matrix.spec }}
|
||||
test_name: ${{ matrix.name }}
|
||||
@@ -1,170 +0,0 @@
|
||||
name: 'Test (and release) Updater'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
paths:
|
||||
- '.github/workflows/updater.yaml'
|
||||
- 'updater/sis-updater-worker/**'
|
||||
- 'updater/sis-updater-scheduler/**'
|
||||
|
||||
jobs:
|
||||
lint_scheduler_and_worker:
|
||||
name: 'Lint scheduler and worker'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
- name: 'Install dependencies'
|
||||
run: npm ci && npm ci --prefix updater/sis-updater-scheduler && npm ci --prefix updater/sis-updater-worker
|
||||
- name: 'Run linter'
|
||||
run: npm run eslint -- updater
|
||||
|
||||
build_worker:
|
||||
name: 'Build Updater worker'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: updater/sis-updater-worker/
|
||||
push: false
|
||||
outputs: type=docker,dest=/tmp/worker.tar
|
||||
tags: toska/sis-updater-worker:latest
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: updater-worker-image
|
||||
path: /tmp/worker.tar
|
||||
|
||||
build_scheduler:
|
||||
name: 'Build Updater scheduler'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: updater/sis-updater-scheduler/
|
||||
push: false
|
||||
outputs: type=docker,dest=/tmp/scheduler.tar
|
||||
tags: toska/sis-updater-scheduler:latest
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: updater-scheduler-image
|
||||
path: /tmp/scheduler.tar
|
||||
|
||||
test_updater:
|
||||
name: 'Test Updater'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- lint_scheduler_and_worker
|
||||
- build_worker
|
||||
- build_scheduler
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Download built images'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: 'updater-{worker,scheduler}-image'
|
||||
merge-multiple: true
|
||||
- name: 'Load worker image'
|
||||
run: docker load --input worker.tar
|
||||
- name: 'Load scheduler image'
|
||||
run: docker load --input scheduler.tar
|
||||
- name: 'Start worker'
|
||||
run: docker compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker
|
||||
- name: 'Run hourly jobs'
|
||||
run: docker compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: hourly
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
- name: 'Run weekly jobs'
|
||||
run: docker compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: weekly
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
- name: 'Run prepurge'
|
||||
run: docker compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: prepurge
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
- name: 'Run purge'
|
||||
run: docker compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: purge
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
|
||||
release_worker:
|
||||
name: 'Build and release Updater worker'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test_updater
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Build worker image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: updater-worker
|
||||
tags: staging production ${{ github.sha }}
|
||||
context: updater/sis-updater-worker
|
||||
containerfiles: |
|
||||
updater/sis-updater-worker/Dockerfile
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
|
||||
- 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_scheduler:
|
||||
name: 'Build and release Updater scheduler'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test_updater
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Build scheduler image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: updater-scheduler
|
||||
tags: staging production ${{ github.sha }}
|
||||
context: updater/sis-updater-scheduler
|
||||
containerfiles: |
|
||||
updater/sis-updater-scheduler/Dockerfile
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
|
||||
- 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 }}
|
||||
+26
-66
@@ -1,70 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
dist
|
||||
|
||||
# IDE metadata
|
||||
.vscode
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Production
|
||||
dist/
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
node_modules/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Misc
|
||||
*.sqz
|
||||
|
||||
# Environment variable files
|
||||
.env
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# Oodikone related stuff
|
||||
.databasedumps/
|
||||
|
||||
# Cypress
|
||||
cypress/downloads/
|
||||
cypress/screenshots/
|
||||
cypress/videos/
|
||||
|
||||
# Code coverage
|
||||
.nyc_output/
|
||||
coverage/
|
||||
instrumented/
|
||||
|
||||
# Pycharm
|
||||
.idea
|
||||
|
||||
# VS Code
|
||||
.vscode/settings.json
|
||||
.vscode/tasks.json
|
||||
.vscode/launch.json
|
||||
.vscode/terminals.json
|
||||
|
||||
# Target of shared symlink
|
||||
services/**/src/shared
|
||||
|
||||
#Stats
|
||||
stats.json
|
||||
@@ -1,9 +0,0 @@
|
||||
npx lint-staged
|
||||
|
||||
if git grep -qE --cached "(it|describe)\.only" $(git diff --cached --name-only --diff-filter=ACMR | xargs) ./cypress; then
|
||||
echo "\e[0;31mDo not commit .only-tests."
|
||||
exit 1
|
||||
else
|
||||
echo "No .only-tests found"
|
||||
exit 0
|
||||
fi
|
||||
@@ -1,30 +0,0 @@
|
||||
import { relative } from 'path'
|
||||
|
||||
const cwd = process.cwd()
|
||||
const dockerCmdBase = `docker run --rm --volume ${cwd}:/oodikone --workdir /oodikone`
|
||||
const relativeFilePaths = files => [...files.map(file => relative(cwd, file))].join(' ')
|
||||
|
||||
export default {
|
||||
'{services,updater}/**/*.{js,jsx,ts,tsx}': files =>
|
||||
`eslint --fix ${files.join(' ')} --report-unused-disable-directives`,
|
||||
'services/backend/**/*.{ts,tsx}': () => 'npx tsc --noEmit --project services/backend/tsconfig.json',
|
||||
'services/frontend/**/*.{ts,tsx}': () => 'npx tsc --noEmit --project services/frontend/tsconfig.json',
|
||||
'*.{js,jsx,ts,tsx,json,md,yml,yaml,html,css}': files => `prettier --write ${files.join(' ')}`,
|
||||
'*.css': files => `stylelint --fix ${files.join(' ')}`,
|
||||
Dockerfile: files =>
|
||||
`${dockerCmdBase} hadolint/hadolint:v2.12.0 hadolint --ignore DL3006 ${relativeFilePaths(files)}`,
|
||||
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck:v0.7.2 ${relativeFilePaths(files)} -x`,
|
||||
'docker-compose*': files => {
|
||||
const composeFiles = file => {
|
||||
if (['docker-compose.ci.yml', 'docker-compose.test.yml'].some(f => file.includes(f))) {
|
||||
return `--file ${file}`
|
||||
}
|
||||
if (file.includes('docker-compose.real.yml')) {
|
||||
return `--file ${cwd}/docker-compose.yml --file ${file}`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
return files.map(file => `docker-compose ${composeFiles(file)} config --quiet`)
|
||||
},
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
+12
-2
@@ -1,6 +1,16 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"selector-class-pattern": "^(?:[a-z][a-zA-Z-]*|Mui[A-Za-z-]+)$"
|
||||
"selector-type-no-unknown": [true, {
|
||||
"ignoreTypes": ["/^--/", "--"]
|
||||
}],
|
||||
"selector-pseudo-class-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignorePseudoClasses": [
|
||||
"global"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '8'
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"aaron-bond.better-comments",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"github.vscode-github-actions",
|
||||
"stylelint.vscode-stylelint"
|
||||
]
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
FROM node:8.9.4
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN npm i
|
||||
|
||||
RUN npm run dist
|
||||
RUN npm install -g serve
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["serve", "-s", "dist"]
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2024 University of Helsinki
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,249 +1,31 @@
|
||||
# Oodikone
|
||||
[](https://qlty.sh/gh/UniversityOfHelsinkiCS/projects/oodikone)
|
||||
# OodiKone2-Frontend
|
||||
Master:
|
||||
[](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone2-frontend)
|
||||
|
||||
An application for analyzing university data, running at [https://oodikone.helsinki.fi/](https://oodikone.helsinki.fi/).
|
||||
Trunk:
|
||||
[](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone2-frontend)
|
||||
|
||||
## ✔️ Requirements
|
||||
|
||||
To run Oodikone locally, you will need the following:
|
||||
### Development
|
||||
fetch dependencies: `npm install`
|
||||
|
||||
1. Applications
|
||||
- [Docker](https://docs.docker.com/install/) (version `20.10+`),
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (version `1.29+`)
|
||||
- [npm](https://docs.npmjs.com/cli/v7) (version `7+`)
|
||||
2. Access to Toska Docker Hub
|
||||
3. For real data setup, access to Toska's production servers
|
||||
4. For some features to work, access to Toska's service tokens
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
Pre-installation steps:
|
||||
|
||||
- Clone this repo
|
||||
- `git@github.com:UniversityOfHelsinkiCS/oodikone.git`
|
||||
- Login to Toska's Docker Hub (see Toska's GitLab for more information)
|
||||
- Copy the `.env.template` file to `.env` and populate with correct tokens
|
||||
- `cp .env.template .env`
|
||||
|
||||
Then launch the CLI and follow the instructions:
|
||||
|
||||
```bash
|
||||
npm run cli
|
||||
```
|
||||
|
||||
_Please use a terminal at least 80 characters wide, the CLI is a bit rudimentary_ 😊
|
||||
|
||||
What different CLI options do:
|
||||
|
||||
1. Set up Oodikone from scratch
|
||||
- Cleans up any previous installations
|
||||
- Installs all needed npm packages locally
|
||||
- Sets up pre-commit linting hooks
|
||||
- Pulls and builds all needed Docker images and sets up dockerized development environment
|
||||
- Setup happens with anonymized data, which developers should use by default
|
||||
- **Note:** Running this option cleans up all real data too, so please don' run option 2 before this.
|
||||
2. Reset all real data
|
||||
- Cleans up any previous real data databases
|
||||
- Downloads needed database dumps from production servers.
|
||||
- Creates real data databases and populates them with downloaded dumps.
|
||||
3. Reset a single database
|
||||
- Like option 2, but only for single database
|
||||
- Leaves other databases untouched
|
||||
4. Restore data from dumps
|
||||
- Like option 2, but skips downloading, and simply restores databases from the dumps downloaded earlier
|
||||
5. Docker system prune
|
||||
- Runs `docker system prune -a && docker volume prune -a`
|
||||
- This will remove all your Docker data, also related to any other applications than Oodikone!
|
||||
- Sometimes necessary after strange errors due to caching
|
||||
|
||||
## ⌨️ Development
|
||||
|
||||
### Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Oodikone & Updater
|
||||
subgraph Oodikone
|
||||
client[React app] --> server[NodeJS server]
|
||||
server --> ok_redis(Redis)
|
||||
konedb[(kone-db)]
|
||||
userdb[(user-db)]
|
||||
end
|
||||
server -->|Read| sisdb[(sis-db)]
|
||||
subgraph Updater
|
||||
updaterscheduler[Scheduler] --> |Push BullMQ jobs| redis(Redis)
|
||||
updaterworker[Worker] -->|Write| sisdb
|
||||
updaterworker --> |Fetch BullMQ jobs| redis
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Common Toska services
|
||||
jami[JAMI]
|
||||
pate[Pate]
|
||||
importer[Importer]
|
||||
end
|
||||
|
||||
updaterworker -->|Read db| importer
|
||||
updaterscheduler -->|Read db| importer
|
||||
importer -->|Fetch data| sisu[Sisu export APIs]
|
||||
server -->|Get IAM access| jami
|
||||
server -->|Send mail| pate
|
||||
|
||||
subgraph Analytics
|
||||
direction TB
|
||||
sentry[Sentry]
|
||||
graylog[Graylog]
|
||||
grafana[Grafana]
|
||||
end
|
||||
```
|
||||
|
||||
In development, backend and updater use the same Redis, but in production they are separate.
|
||||
|
||||
See also the [older graph](documentation/img/oodikone.png) for more information.
|
||||
|
||||
To read a bit more details about what updater-scheduler and updater-worker do, see their own READMEs:
|
||||
|
||||
- [updater-scheduler's README](./updater/sis-updater-scheduler/README.md)
|
||||
- [updater-worker's README](./updater/sis-updater-worker/README.md)
|
||||
|
||||
### Documentation
|
||||
|
||||
The `documentation` directory contains some notes on the sis-db schema and is meant as a place for storing useful information on Oodikone to make life easier for future developers. Feel free to add anything relevant!
|
||||
|
||||
For internal meeting notes and sensitive information, see Toska GitLab.
|
||||
|
||||
### Basics
|
||||
|
||||
The development environment is entirely configured in the `docker-compose.yml` file located in this repository. The file defines the services for Oodikone's two main components: **oodikone** and **updater**.
|
||||
|
||||
Running Oodikone with real data requires separated databases and redis, which are defined in `docker-compose.real.yml` file. Otherwise real data development environment uses the configuration as anonymized data development environment.
|
||||
|
||||
Some useful commands are defined in `package.json` and can be run with `npm run <command>` as follows:
|
||||
|
||||
- `npm run oodikone`: starts Oodikone with anonymized data
|
||||
- `npm run oodikone:real`: starts Oodikone with real data
|
||||
- `npm run updater`: starts updater with anonymized data
|
||||
- `npm run updater:real`: starts updater with real data
|
||||
- `npm run both`: starts Oodikone and updater with anonymized data
|
||||
- `npm run both:real`: starts Oodikone and updater with real data
|
||||
- `npm run docker:down`: stops the whole environment
|
||||
- `npm run testupdater`: runs tests for updater
|
||||
- `npm run flushredis`: clears Redis, which forces big calculations to be redone in e.g. studyprogramme overview and faculty views
|
||||
- `npm run rapodiff`: runs [Rapodiff](./services/backend/src/rapodiff/)
|
||||
- `npm run install:local`: installs `node_modules` for each package (backend, frontend, sis-updater-scheduler and sis-updater-worker) locally
|
||||
- `npm run install:docker`: installs `node_modules` for each package inside containers
|
||||
- `npm run install:both`: runs both `npm run install:local` and `npm run install:docker`
|
||||
|
||||
Once you have ran setup for Oodikone , you can just execute the first one (`npm run oodikone`). After starting and waiting for a while for containers to compile, Oodikone can be accessed at [http://localhost:3000](http://localhost:3000/) and Adminer (database investigation tool) at [http://localhost:5050](http://localhost:5050/). Adminer requires you to login with username `postgres` and with any password you choose (for example `p`).
|
||||
|
||||
### Run.sh script
|
||||
|
||||
As said, the development environment runs entirely inside Docker containers. To keep `package.json` clean and not filled with predefined scripts, we have created a simple helper script called `./run.sh`. The script allows you to use docker-compose commands without the need to write long list of parameters. Try to run `./run.sh` in the root of the project and see what happens!
|
||||
|
||||
`./run.sh` is simply a wrapper script to run Oodikone, updater or both services in either anon or real mode. If you take a look at `package.json`, you can see that most of the predefined scripts above use `./run.sh` under the hood.
|
||||
|
||||
It is recommended to spend some time to become familiar with `docker` and `docker compose` cli commands. You can then use them directly or with `./run.sh` wrapper. Here is some examples for day-to-day development situations:
|
||||
|
||||
- `./run.sh oodikone anon pull`: pull all images related to Oodikone development
|
||||
- `./run.sh updater real up --build --force-recreate --detach`: start updater detached (in the background) in real data mode, but build new images before starting
|
||||
- `docker compose ps`: view the containers in the running environment
|
||||
- `docker compose logs frontend`: print logs for just frontend
|
||||
- `docker compose logs --follow --tail 100 backend`: print last hundred rows of backend logs and begin to follow them in your terminal window
|
||||
- `docker exec -it backend sh`: open bash terminal inside backend container
|
||||
- `docker exec -it sis-db psql -U postgres sis-db-real`: open psql client to investigate sis real data database
|
||||
|
||||
### User types for development
|
||||
|
||||
By default, you're login as `mluukkai` dev user when running Oodikone in development mode. If you want to debug with certain type of user, you can use mocking: go to the "Users" page, click "edit" on the user you want to use and then click the icon on the right corner of user card. Another way is to mock user headers - see frontend's API configuration for how this is done.
|
||||
|
||||
Anonymous data contains some preset user types, corresponding to most usual user types in real data. These are used in testing too (see "Testing" below for more info).
|
||||
local development: `npm run dev`
|
||||
|
||||
### Linting
|
||||
Install eslint: `npm install -g eslint`
|
||||
|
||||
Cli script sets up pre-commit hooks that are used to lint and fix files before committing. If your `package.json` file doesn't include "lint-staged" -key, please set hooks manually with `npm run prepare`.
|
||||
lint js & css: `npm run lint`
|
||||
lint only js: `npm run lint:js`
|
||||
lint only css: `npm run lint:css`
|
||||
lint fix for js: `npm run lintfix`
|
||||
|
||||
For more information on how files are tested, take a look at "lint-staged" in `package.json`. Some files (e.g. github action files, dockerfiles, shell scripts) are checked with external tools and may require you to install those tools in case you're modifying files in question.
|
||||
|
||||
Pre-commit hooks will fix auto-fixable problems. To set up quick formatting: In VSCode you can go to command palette, select "ESLint: Fix all auto-fixable problems" and assign a hotkey for it, like `SHIFT + CTRL + '.'`.
|
||||
### Building
|
||||
`npm run dist`
|
||||
|
||||
## 🔨 Testing & CI
|
||||
Builds optimized production package to /dist
|
||||
|
||||
We use cypress for end-to-end testing. No unit tests are currently used.
|
||||
### Deployment
|
||||
|
||||
Cypress
|
||||
Read part deployment from [backend README](https://github.com/UniversityOfHelsinkiCS/oodikone2-backend/)
|
||||
|
||||
- Can be launched in interactive mode with `npm run cypress open`. `package.json` defines entrypoint `npm run cypress` so you can basically run cypress with any arguments you want
|
||||
- Are defined in cypress -folder and cypress.config.json
|
||||
- The entire cypress test stack takes about 15 to 20 mins to run. Since tests are ran in our Github actions CI pipe, you're encouraged to take advantage of this instead of running all tests locally.
|
||||
- Running tests locally sometimes causes problems after running the whole tests suite due to some login-related issue. Also for this reason, it is usually best practice to only run single views locally, and let the CI run the entire test suite.
|
||||
- There are some different user types and cypress commands defined for testing. Take a look at these when debugging tests.
|
||||
|
||||
Continuous integration (CI) works with Github actions and is defined in workflow files in `.github/workflows` folder:
|
||||
|
||||
- Oodikone setup for cypress and other tests in CI is defined in `docker-compose.ci.yml`. Take a look at this too if debugging github action workflows.
|
||||
- Tests are run on every push
|
||||
- After a successful test run, Oodikone is deployed to staging
|
||||
- After creating a release, Oodikone is deployed to production
|
||||
- Updater is deployed to production when pushed if tests pass
|
||||
- Test databases can be updated, instructions in [anonyymioodi README](anonyymioodi/README.md)
|
||||
|
||||
[Rapodiff](./services/backend/src/rapodiff/) can be used to check differences between Rapo and Oodikone data.
|
||||
|
||||
## ❓FAQ
|
||||
|
||||
### Modules are missing after updating package.json
|
||||
|
||||
You should always install the dependencies **inside** the container to have the application **inside** the container access them. Module might be missing for example when someone else installs a new library and you only pull the changes in package.json. Use `docker exec <service> npm ci` to install modules inside the container or `npm run install:docker` to install modules inside all packages (backend, frontend, sis-updater-scheduler and sis-updater-worker).
|
||||
|
||||
### Study guidance groups don't work on my machine
|
||||
|
||||
Make sure you have your VPN on.
|
||||
|
||||
### Everything is broken, can't get oodikone running, data is not there etc.
|
||||
|
||||
First: Try `npm run cli` option 1: _Set up oodikone from scratch_ and then option 2 to download and restore databases (or option 4 to skip downloading, if you've done it recently).
|
||||
|
||||
If that does not help, try option 5: _Docker system prune_. Notice that this clears up **all Docker data**, including data related to other projects. If this is not ok, you'll need to identify the Oodikone-specific Docker volumes yourself and caches and clear those invididually.
|
||||
|
||||
### How to run tests locally faster
|
||||
|
||||
Vite simplifies development but can be **very slow** when running tests. If you're curious about the reasons for this, you can read more in [this GitHub issue](https://github.com/cypress-io/cypress/issues/22968).
|
||||
|
||||
To speed up testing, you can use Vite's preview mode. This will make the tests run faster, but keep in mind that you must rebuild the frontend code every time you make changes to it. To use the preview mode, follow these steps:
|
||||
|
||||
1. Run `npm run build` in the `services/frontend` directory.
|
||||
2. Run `npm run preview` in the `services/frontend` directory and keep it running.
|
||||
3. Update the `baseUrl` in [cypress.config.js](./cypress.config.js) to `http://localhost:4173`.
|
||||
4. Start Oodikone with `npm run oodikone`.
|
||||
|
||||
After these steps, you can run your tests:
|
||||
|
||||
- Use `npm run cypress open` to open the Cypress UI and run the tests interactively.
|
||||
|
||||
Tests can also be run in headless mode, similar to how they are run in the CI/CD pipeline. To do this, use:
|
||||
|
||||
```bash
|
||||
npm run cypress:run -- --spec <file path>
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
npm run cypress:run -- --spec "cypress/e2e/Language_center.js"
|
||||
```
|
||||
|
||||
Running tests in headless mode may also help if your browser keeps crashing when running tests interactively. If you want to run all tests, use:
|
||||
|
||||
```bash
|
||||
npm run cypress:run
|
||||
```
|
||||
|
||||
Remember to revert the `baseUrl` in [cypress.config.js](./cypress.config.js) to its original value when you're done.
|
||||
|
||||
## ✌🏼 Maintainers and contribution
|
||||
|
||||
[Toska](https://toska.dev) - University of Helsinki
|
||||
|
||||
## 🛡️ Tietosuoja / data protection
|
||||
|
||||
See [tietosuoja.md](./documentation/tietosuoja.md).
|
||||
|
||||
Binary file not shown.
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
require_once('plugins/login-password-less.php');
|
||||
|
||||
/** Set allowed password
|
||||
* @param string result of password_hash
|
||||
*/
|
||||
return new AdminerLoginPasswordLess(
|
||||
$password_hash = password_hash("postgres", PASSWORD_DEFAULT)
|
||||
);
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
require_once('plugins/login-servers.php');
|
||||
$driver = "pgsql";
|
||||
|
||||
/** Set supported servers
|
||||
* @param array array($description => array("server" => , "driver" => "server|pgsql|sqlite|..."))
|
||||
*/
|
||||
return new AdminerLoginServers(
|
||||
$servers = array(
|
||||
"kone-db" => array("server" => "kone-db", "driver" => $driver),
|
||||
"sis-db" => array("server" => "sis-db", "driver" => $driver),
|
||||
"sis-importer-db" => array("server" => "sis-importer-db", "driver" => $driver),
|
||||
"user-db" => array("server" => "user-db", "driver" => $driver)
|
||||
)
|
||||
);
|
||||
@@ -1,72 +0,0 @@
|
||||
# Anonyymioodi - test database setup
|
||||
|
||||
This folder contains a guide and scripts to modify or create anonymous database images for Oodikone, mainly used for testing.
|
||||
|
||||
The quick guide should be enough for most purposes: updating schema, data, or the PostgreSQL version. If you need to generate completely new `sis-importer-db` and `sis-db` images (which shouldn't happen too often), follow the instructions in the section [Generate new sis-importer-db and sis-db completely from scratch](#generate-new-sis-importer-db-and-sis-db-completely-from-scratch).
|
||||
|
||||
## Quick guide to updating a test database
|
||||
|
||||
1. Ensure you have the latest test-db. Just pulling images might not replace your existing volume, so you may need to remove it manually. This command might work: `docker volume rm oodikone-sis-db-data` (replace `oodikone-sis-db-data` with the correct volume name). The new image, including data, should be pulled the next time you launch Oodikone normally.
|
||||
|
||||
2. Do your dev work as usual with the test database, for example, using `npm run both`. Edit data as needed, (for example, run the updater via the frontend), and then run tests to see if they pass (follow the instructions in the [README](../README.md) to run the tests more quickly).
|
||||
|
||||
3. If you're updating `sis-db`:
|
||||
|
||||
- Modify the scheduler limit in [config.js](../updater/sis-updater-scheduler/src/config.js) by setting `module.exports.DEV_SCHEDULE_COUNT = null`.
|
||||
- Follow the `sis-updater-worker` logs to ensure everything is running smoothly (`docker logs --follow oodikone-sis-updater-worker`).
|
||||
|
||||
4. When you're satisfied with the database, create a dump:
|
||||
|
||||
- Use the command: `docker exec -i <dbname> pg_dump -Fc -U postgres <dbname> > anonyymioodi/<dbname>.sqz`.
|
||||
|
||||
5. Shut down the environment with `npm run docker:down`.
|
||||
|
||||
6. Navigate to this folder: `cd anonyymioodi`.
|
||||
|
||||
7. If you need to update the PostgreSQL version, do it now by changing the version in this directory's `docker-compose.yml`. The version should match the one used on the server to avoid compatibility issues.
|
||||
|
||||
8. Create a new container from the dump: `./create_container_from_dump.sh <dbname>`.
|
||||
|
||||
9. Commit and push it to the Toska registry: `./commit_and_push_to_toska_hub.sh <dbname>`.
|
||||
|
||||
10. Bring down the environment with `docker compose down`.
|
||||
|
||||
## Generate new sis-importer-db and sis-db completely from scratch
|
||||
|
||||
> [!CAUTION]
|
||||
> The more the data in `sis-importer-db` and `sis-db` changes, the more tests might break. Because of this, it's highly recommended to make as few changes as possible (for example, avoid changing the degree programs whose students are used in the test data).
|
||||
>
|
||||
> If you only need to make small changes, follow the quick guide above.
|
||||
|
||||
### Sis-importer-db
|
||||
|
||||
1. To generate a sample dump from test Sisu data, follow the instructions at [importer-db-staging-sampletaker](https://github.com/UniversityOfHelsinkiCS/sis-importer/tree/master/importer-db-staging-sampletaker) inside the `sis-importer` repo.
|
||||
|
||||
2. Copy `sis-importer-db.sqz` into this folder.
|
||||
|
||||
3. Run the following commands (in this folder) to create a new image from the dump and push it to the Toska registry:
|
||||
|
||||
```bash
|
||||
./create_container_from_dump.sh sis-importer-db
|
||||
./commit_and_push_to_toska_hub.sh sis-importer-db
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Sis-db
|
||||
|
||||
1. To ensure the newest `sis-importer-db` image is used:
|
||||
|
||||
- Delete all `sis-importer-db` images you have locally (list images with `docker images`, delete with `docker rmi <image ID>`).
|
||||
- Also, delete the old `sis-importer-db` volume (`docker volume rm sis-importer-db-data`).
|
||||
|
||||
2. Start Oodikone and Updater with `npm run both` (this will pull the newest `sis-importer-db` image you just pushed in the previous step).
|
||||
|
||||
3. Truncate all tables in `sis-db` **_except for `migrations` and `semesters`_** (the easiest way is to use Adminer).
|
||||
|
||||
4. Modify the scheduler limit in [config.js](../updater/sis-updater-scheduler/src/config.js) by setting `module.exports.DEV_SCHEDULE_COUNT = null`.
|
||||
|
||||
5. Navigate to <http://localhost:3000/updater> and click the three buttons (Update meta, Update students, Update curriculums) one by one. This will populate `sis-db` with data from the new `sis-importer-db`. You might want to do each update a couple of times, as the database tables affect each other, and not all necessary data might be generated on the first try.
|
||||
|
||||
6. Follow the `sis-updater-worker` logs to ensure everything is running smoothly (`docker logs --follow oodikone-sis-updater-worker`). If you see any errors, try clicking the buttons again.
|
||||
|
||||
7. Run all the tests to ensure everything is working as expected (follow the instructions in the [README](../README.md) to run the tests more quickly). If any tests fail, update the test files accordingly.
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# === Config ===
|
||||
|
||||
# Quit if failing
|
||||
set -euo pipefail
|
||||
|
||||
# Print messages and logs that are not script output to stderr
|
||||
msg() {
|
||||
echo >&2 -e "${1-}"
|
||||
}
|
||||
|
||||
# Some special types of messages with colours
|
||||
successmsg() {
|
||||
msg "${GREEN}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
errormsg() {
|
||||
msg "${RED}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
infomsg() {
|
||||
msg "${BLUE}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
warningmsg() {
|
||||
msg "${ORANGE}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
|
||||
# Setup colors for messages if running interactive shell
|
||||
if [[ -t 2 ]] && [[ "${TERM-}" != "dumb" ]]; then
|
||||
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m'
|
||||
else
|
||||
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE=''
|
||||
fi
|
||||
|
||||
args=("$@")
|
||||
if [[ ${#args[@]} -ne 1 ]]; then
|
||||
errormsg "No database given, exiting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
infomsg "Committing and pushing to registry"
|
||||
database=${args[0]}
|
||||
docker commit "$database" "registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/$database"
|
||||
docker push "registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/$database"
|
||||
successmsg "Succesfully pushed"
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# === Config ===
|
||||
|
||||
# Quit if failing
|
||||
set -euo pipefail
|
||||
|
||||
# Print messages and logs that are not script output to stderr
|
||||
msg() {
|
||||
echo >&2 -e "${1-}"
|
||||
}
|
||||
|
||||
# Some special types of messages with colours
|
||||
successmsg() {
|
||||
msg "${GREEN}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
errormsg() {
|
||||
msg "${RED}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
infomsg() {
|
||||
msg "${BLUE}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
warningmsg() {
|
||||
msg "${ORANGE}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
|
||||
# Setup colors for messages if running interactive shell
|
||||
if [[ -t 2 ]] && [[ "${TERM-}" != "dumb" ]]; then
|
||||
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m'
|
||||
else
|
||||
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE=''
|
||||
fi
|
||||
|
||||
# === Create images ===
|
||||
|
||||
retry () {
|
||||
sleep 5
|
||||
for i in {1..60}; do
|
||||
"$@" && break || warningmsg "Retry attempt $i failed, waiting..." && sleep 5;
|
||||
done
|
||||
}
|
||||
|
||||
args=("$@")
|
||||
if [[ ${#args[@]} -ne 1 ]]; then
|
||||
errormsg "No database given, exiting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
infomsg "Restoring PostgreSQL dumps from backups. This might take a while."
|
||||
database=${args[0]}
|
||||
docker-compose down --remove-orphans
|
||||
docker-compose up -d "$database"
|
||||
|
||||
database_dump="$database.sqz"
|
||||
infomsg "Attempting to restore database $database from dump $database_dump"
|
||||
retry docker-compose exec "$database" pg_isready --dbname="$database"
|
||||
docker exec -i "$database" /bin/bash -c "pg_restore --username=postgres \
|
||||
--format=custom --dbname=$database --no-owner" < "$database_dump"
|
||||
successmsg "Database $database succesfully created"
|
||||
@@ -1,40 +0,0 @@
|
||||
x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
services:
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
container_name: kone-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: kone-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.10
|
||||
|
||||
sis-db:
|
||||
<<: *defaults
|
||||
container_name: sis-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.10
|
||||
shm_size: 2gb
|
||||
|
||||
sis-importer-db:
|
||||
<<: *defaults
|
||||
container_name: sis-importer-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-importer-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.10
|
||||
|
||||
user-db:
|
||||
<<: *defaults
|
||||
container_name: user-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: user-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.10
|
||||
@@ -1,88 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script is used to setup oodikone with interactive cli. Base for script:
|
||||
# https://betterdev.blog/minimal-safe-bash-script-template/
|
||||
|
||||
# === Config ===
|
||||
|
||||
# Fail immediately if script fails, unbound variables are referenced
|
||||
# or command inside pipe fails. -E ensures cleanup trap fires in rare ERR cases.
|
||||
set -euoE pipefail
|
||||
|
||||
# Set up constants
|
||||
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
||||
|
||||
# Source functions for setup
|
||||
source "$PROJECT_ROOT"/scripts/functions_for_setup.sh
|
||||
|
||||
# Try to run cleanup function if things fail.
|
||||
trap cleanup SIGINT SIGTERM ERR EXIT
|
||||
|
||||
# Run docker-compose down on cleanup
|
||||
cleanup() {
|
||||
trap - SIGINT SIGTERM ERR EXIT
|
||||
warningmsg "Trying to run docker-compose down and remove orphans"
|
||||
docker-compose --profile oodikone --profile updater down --remove-orphans
|
||||
}
|
||||
|
||||
# === CLI ===
|
||||
|
||||
show_welcome() {
|
||||
local cashmoneyyellow
|
||||
cashmoneyyellow=$(tput setaf 221)
|
||||
local normal
|
||||
normal=$(tput sgr0)
|
||||
if [ "$(tput cols)" -gt "76" ]; then
|
||||
while IFS="" read -r p || [ -n "$p" ]; do
|
||||
printf '%40s\n' "${cashmoneyyellow}$p${normal}"
|
||||
done < "$PROJECT_ROOT"/scripts/assets/logo.txt
|
||||
fi
|
||||
infomsg "Welcome to Oodikone CLI!"
|
||||
msg "This tool helps you in managing the project configuration. If you are new to
|
||||
Oodikone development, you should probably run \"Set up oodikone from scratch.\" which
|
||||
will take care of setting up Oodikone for you. See README for more details."
|
||||
msg ""
|
||||
}
|
||||
|
||||
set_custom_select_prompt() {
|
||||
PS3="Please enter your choice: "
|
||||
|
||||
options=(
|
||||
"Set up Oodikone from scratch"
|
||||
"Reset all real data"
|
||||
"Reset single database"
|
||||
"Restore data from dumps"
|
||||
"Docker system prune"
|
||||
"Quit"
|
||||
)
|
||||
}
|
||||
|
||||
# Run scripts and set the prompt
|
||||
show_welcome
|
||||
init_dirs
|
||||
get_s3_config
|
||||
set_custom_select_prompt
|
||||
|
||||
while true; do
|
||||
select opt in "${options[@]}"; do
|
||||
case $opt in
|
||||
"Set up Oodikone from scratch")
|
||||
set_up_oodikone_from_scratch;;
|
||||
"Reset all real data")
|
||||
reset_all_real_data;;
|
||||
"Reset single database")
|
||||
reset_single_database
|
||||
set_custom_select_prompt # Set prompt back to initial values
|
||||
;;
|
||||
"Restore data from dumps")
|
||||
restore_data_from_dumps;;
|
||||
"Docker system prune")
|
||||
docker_prune;;
|
||||
"Quit")
|
||||
break 2;;
|
||||
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
|
||||
";;
|
||||
esac
|
||||
break
|
||||
done
|
||||
done
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable import-x/no-extraneous-dependencies */
|
||||
/* eslint-disable import-x/no-default-export */
|
||||
const { defineConfig } = require('cypress')
|
||||
|
||||
module.exports = defineConfig({
|
||||
projectId: 'c3jsph',
|
||||
defaultCommandTimeout: 30000,
|
||||
requestTimeout: 30000,
|
||||
videoCompression: false,
|
||||
viewportWidth: 1920,
|
||||
viewportHeight: 1080,
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
require('./cypress/plugins/index.js')(on, config)
|
||||
return config
|
||||
},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
},
|
||||
video: true,
|
||||
})
|
||||
@@ -1,179 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const openCompletedCoursesModal = () => {
|
||||
cy.contains('button', 'Search completed courses of students').click()
|
||||
}
|
||||
|
||||
const hasLanded = () => {
|
||||
cy.contains('Search completed courses')
|
||||
cy.contains(
|
||||
'Here you can search by a list of student and course numbers to see whether students have completed certain courses yet'
|
||||
)
|
||||
}
|
||||
|
||||
const openSearch = () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
}
|
||||
|
||||
const selectSavedCourselist = name => {
|
||||
cy.cs('history-search').click()
|
||||
cy.cs('history-search').type(name)
|
||||
cy.cs('history-search').type('{downarrow}')
|
||||
cy.cs('history-search').type('{enter}')
|
||||
}
|
||||
|
||||
const createCourseList = (courseCodes, courseListName) => {
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
cy.cs('course-list-input').type(courseCodes.join('\n'))
|
||||
cy.cs('search-name').type(courseListName)
|
||||
cy.cs('save-courselist').click()
|
||||
}
|
||||
|
||||
const generateCourseListName = () => `TEST-course-list-${new Date().getTime()}`
|
||||
|
||||
const deleteAllPreviousSearches = () => {
|
||||
cy.cs('history-search').click()
|
||||
|
||||
cy.get('.MuiAutocomplete-popper li').then($options => {
|
||||
if ($options.length > 0) {
|
||||
cy.wrap($options).each(($option, index, $list) => {
|
||||
cy.wrap($option).click()
|
||||
cy.cs('delete-courselist').click()
|
||||
if (index < $list.length - 1) {
|
||||
cy.cs('history-search').click()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
cy.contains('You have no previous searches.')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('When search modal is opened', () => {
|
||||
after(() => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
cy.url().should('include', '/completedcoursessearch')
|
||||
cy.contains('Search completed courses of students')
|
||||
cy.cs('open-completed-courses-modal-button').click()
|
||||
})
|
||||
|
||||
it('Modal opens correctly', () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.contains('Search completed courses of students')
|
||||
cy.contains('Insert one or more student numbers, separated by a space, a newline, a comma, or a semicolon.')
|
||||
cy.contains('Insert one or more courses, separated by a space, a newline, a comma, or a semicolon.')
|
||||
cy.contains('Insert name for this course list if you wish to save it')
|
||||
})
|
||||
|
||||
it('Modal gets the correct course codes and student numbers from the URL', () => {
|
||||
cy.init('/completedcoursessearch?courseList=TKT10001&courseList=TKT10002&studentList=433237&studentList=457144')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.cs('student-no-input').contains('433237, 457144')
|
||||
cy.cs('course-list-input').contains('TKT10001, TKT10002')
|
||||
})
|
||||
|
||||
describe('When a search is executed with invalid input', () => {
|
||||
it('Returns empty table and notification', () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.cs('student-no-input').type('1')
|
||||
cy.cs('course-list-input').type('1')
|
||||
cy.cs('completed-courses-search-button').click()
|
||||
cy.cs('rights-notification').should(
|
||||
'contain.text',
|
||||
'The information for the following students could not be displayed'
|
||||
)
|
||||
cy.cs('rights-notification').contains('1')
|
||||
cy.cs('completed-courses-table-div').contains('Student number')
|
||||
cy.cs('completed-courses-table-div').contains('1').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('When a search with correct data is executed', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
|
||||
cy.cs('student-no-input').type(studentSet1.join('\n'))
|
||||
cy.cs('course-list-input').type(coursesSet1.join('\n'))
|
||||
cy.cs('completed-courses-search-button').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Pushes the query to url', () => {
|
||||
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
|
||||
cy.url().should(
|
||||
'include',
|
||||
`/completedcoursessearch?courseList=${coursesSet1.join('&courseList=')}&studentList=${studentSet1.join('&studentList=')}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('Finds correct students and courses', () => {
|
||||
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
|
||||
for (const studentNumber of studentSet1) {
|
||||
cy.cs('completed-courses-table-div').contains(studentNumber)
|
||||
}
|
||||
for (const courseCode of coursesSet1) {
|
||||
cy.cs('completed-courses-table-div').contains(courseCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Courselist saving-related functions work', () => {
|
||||
beforeEach(() => {
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find('[data-cy=history-search]').length > 0) {
|
||||
deleteAllPreviousSearches()
|
||||
} else {
|
||||
cy.contains('You have no previous searches.')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Course list can be saved', () => {
|
||||
const courseList = generateCourseListName()
|
||||
const courses = ['CSM14204', 'TKT10004']
|
||||
createCourseList(courses, courseList)
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseList)
|
||||
cy.cs('course-list-input').contains(courses.join(', '))
|
||||
})
|
||||
|
||||
it('Course list can be deleted', () => {
|
||||
const courseList = generateCourseListName()
|
||||
createCourseList(['CSM14204', 'TKT10004'], courseList)
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseList)
|
||||
cy.cs('delete-courselist').click()
|
||||
cy.contains('You have no previous searches.')
|
||||
})
|
||||
|
||||
it('Course list can be updated', () => {
|
||||
const courseList = generateCourseListName()
|
||||
createCourseList(['CSM14204', 'TKT10004'], courseList)
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseList)
|
||||
cy.cs('course-list-input').type(',TKT10001')
|
||||
cy.cs('save-courselist').click()
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseList)
|
||||
cy.cs('course-list-input').contains('CSM14204, TKT10004, TKT10001')
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,192 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const fillName = () => {
|
||||
const name = `TEST-${new Date().getTime()}`
|
||||
cy.contains('Insert a name for this custom population if you wish to save it')
|
||||
cy.cs('custom-population-name-input').type(name)
|
||||
return name
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
cy.get('button').contains('Save').click()
|
||||
}
|
||||
|
||||
const selectSavedPopulation = name => {
|
||||
cy.cs('history-search').type(`${name}{enter}`)
|
||||
}
|
||||
|
||||
const deleteAllSearches = () => {
|
||||
cy.cs('history-search').click()
|
||||
cy.get('.MuiAutocomplete-popper > .MuiPaper-root')
|
||||
.find('li[role=option]')
|
||||
.then($lis => {
|
||||
const labels = [...$lis].map(li => li.textContent?.trim() || '')
|
||||
|
||||
labels.forEach((label, index) => {
|
||||
if (index !== 0) cy.cs('history-search').click()
|
||||
cy.contains('.MuiAutocomplete-popper li[role=option]', label).click()
|
||||
cy.contains('button', 'Delete').click()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const fillForm = (content, separator) => {
|
||||
cy.cs('student-number-input').type(content.join(separator))
|
||||
}
|
||||
|
||||
const search = () => {
|
||||
cy.cs('search-button').click()
|
||||
}
|
||||
|
||||
const searchFor = (studentnumbers, separator) => {
|
||||
fillForm(studentnumbers, separator)
|
||||
search()
|
||||
}
|
||||
|
||||
const hasLanded = () => {
|
||||
cy.contains('Credit accumulation')
|
||||
cy.contains('Programme distribution')
|
||||
cy.contains('Courses of population')
|
||||
cy.contains('Students')
|
||||
}
|
||||
|
||||
const containsAmountOfStudents = (amount = 0) => {
|
||||
cy.contains(`Credit accumulation (for ${amount} students)`)
|
||||
}
|
||||
|
||||
const containsSpecificStudents = (studentnumbers = []) => {
|
||||
cy.contains(`Students (${studentnumbers.length})`)
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains(`Students (${studentnumbers.length})`).click()
|
||||
})
|
||||
|
||||
studentnumbers.forEach(s => cy.contains(s))
|
||||
}
|
||||
|
||||
const checkRightsNotification = studentNumbers => {
|
||||
cy.cs('rights-notification').within(() => {
|
||||
cy.contains(
|
||||
'The following students information could not be displayed. This could be either because they do not exist, or you do not have the right to view their information.'
|
||||
)
|
||||
cy.get('ul').within(() => {
|
||||
studentNumbers.forEach(number => cy.get('li').contains(number))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('Custom population tests', () => {
|
||||
const nonExistentStudentNumbers = ['123', 'X', '-']
|
||||
beforeEach(() => {
|
||||
cy.init('/custompopulation')
|
||||
cy.url().should('include', '/custompopulation')
|
||||
cy.contains('Custom population')
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.visit('/custompopulation')
|
||||
cy.url().should('include', '/custompopulation')
|
||||
cy.contains('Custom population')
|
||||
deleteAllSearches()
|
||||
})
|
||||
|
||||
describe('Custom population searching', () => {
|
||||
it('Finds a proper population', () => {
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1: students }) => {
|
||||
searchFor(students, '\n')
|
||||
hasLanded()
|
||||
containsAmountOfStudents(students.length)
|
||||
containsSpecificStudents(students)
|
||||
})
|
||||
})
|
||||
|
||||
it("Doesn't return non-existing students", () => {
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1: students }) => {
|
||||
searchFor([...students, ...nonExistentStudentNumbers], ' ')
|
||||
hasLanded()
|
||||
containsAmountOfStudents(students.length)
|
||||
containsSpecificStudents(students)
|
||||
checkRightsNotification(nonExistentStudentNumbers)
|
||||
})
|
||||
})
|
||||
|
||||
it("Doesn't find empty custom population", () => {
|
||||
searchFor(nonExistentStudentNumbers, ',')
|
||||
checkRightsNotification(nonExistentStudentNumbers)
|
||||
cy.contains('Credit accumulation').should('not.exist')
|
||||
cy.contains('Programme distribution').should('not.exist')
|
||||
cy.contains('Courses of population').should('not.exist')
|
||||
cy.get('button').contains('Back to search form')
|
||||
})
|
||||
|
||||
it("Doesn't return students user has no right to", () => {
|
||||
// These students exist in the database, but the user doesn't have the right to view them
|
||||
const studentsForEduBachStudents = ['014990067', '013069465', '014853890']
|
||||
// Two semicolons on purpose, the page should be able to handle it
|
||||
searchFor(studentsForEduBachStudents, ';;')
|
||||
checkRightsNotification(studentsForEduBachStudents)
|
||||
cy.contains('Credit accumulation').should('not.exist')
|
||||
cy.contains('Programme distribution').should('not.exist')
|
||||
cy.contains('Courses of population').should('not.exist')
|
||||
cy.get('button').contains('Back to search form')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom population search saving', () => {
|
||||
it('Saves a custom population search', { retries: 2 }, () => {
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1: students }) => {
|
||||
cy.contains(
|
||||
'Insert student numbers to use for the population. Each student number needs to be separated with a comma, a semicolon, a space, or a line break.'
|
||||
)
|
||||
const name = fillName()
|
||||
// A comma and a newline on purpose, the page should be able to handle it
|
||||
fillForm(students, ',\n')
|
||||
save()
|
||||
|
||||
// Round 1
|
||||
selectSavedPopulation(name)
|
||||
search()
|
||||
cy.contains(`Custom population: ${name}`)
|
||||
containsAmountOfStudents(students.length)
|
||||
containsSpecificStudents(students)
|
||||
|
||||
// Round 2
|
||||
cy.visit('/custompopulation')
|
||||
cy.contains('Custom population')
|
||||
selectSavedPopulation(name)
|
||||
search()
|
||||
cy.contains(`Custom population: ${name}`)
|
||||
containsAmountOfStudents(students.length)
|
||||
containsSpecificStudents(students)
|
||||
})
|
||||
})
|
||||
|
||||
it('Updates a custom population search', () => {
|
||||
cy.fixture('customPopulations').then(
|
||||
({ studentNumbersForCSStudentsSet1: students1, studentNumbersForCSStudentsSet2: students2 }) => {
|
||||
const name = fillName()
|
||||
fillForm(students1, ' ')
|
||||
save()
|
||||
|
||||
selectSavedPopulation(name)
|
||||
search()
|
||||
cy.contains(`Custom population: ${name}`)
|
||||
containsAmountOfStudents(students1.length)
|
||||
containsSpecificStudents(students1)
|
||||
cy.contains('Back to search form').click()
|
||||
selectSavedPopulation(name)
|
||||
fillForm(['\n', ...students2], ', ')
|
||||
save()
|
||||
|
||||
cy.visit('/custompopulation')
|
||||
cy.contains('Custom population')
|
||||
selectSavedPopulation(name)
|
||||
search()
|
||||
cy.contains(`Custom population: ${name}`)
|
||||
containsAmountOfStudents(students1.length + students2.length)
|
||||
containsSpecificStudents([...students1, ...students2])
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,910 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const getEmptyYears = isAcademicYear => {
|
||||
const today = new Date()
|
||||
const latestYear = isAcademicYear && today.getMonth() < 7 ? today.getFullYear() - 1 : today.getFullYear()
|
||||
|
||||
const years = []
|
||||
for (let year = latestYear; year >= 2024; year--) {
|
||||
if (isAcademicYear) {
|
||||
years.push(`${year} - ${year + 1}`)
|
||||
} else {
|
||||
years.push(year)
|
||||
}
|
||||
}
|
||||
return years
|
||||
}
|
||||
|
||||
const tagName = `tag-${new Date().getTime()}`
|
||||
|
||||
const selectYear = year => {
|
||||
// ? Testing the MUI X DatePicker seems very difficult. It does not detect data-cy and the tests fail in CI/CD
|
||||
cy.contains('Associated start year (optional)').click({ force: true })
|
||||
cy.get('[placeholder=YYYY]').type(year)
|
||||
}
|
||||
|
||||
const deleteTag = tagName => {
|
||||
cy.cs(`delete-tag-${tagName}-button`).click()
|
||||
cy.contains('Delete tag')
|
||||
cy.contains('Are you sure you want to delete tag')
|
||||
cy.cs('confirm-delete-tag-button').click()
|
||||
}
|
||||
|
||||
describe('Degree programme overview', () => {
|
||||
describe('Degree programme selector', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme', 'admin')
|
||||
})
|
||||
|
||||
it('Correct categories', () => {
|
||||
cy.contains('Bachelor programmes')
|
||||
cy.contains('Master programmes')
|
||||
cy.contains('Combined programmes')
|
||||
cy.contains('Doctoral programmes')
|
||||
})
|
||||
|
||||
it('Degree programme search filter', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').should('exist')
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma').should('exist')
|
||||
cy.cs('study-programme-filter').type('Tietojenkäsittelytieteen')
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').should('exist')
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma').should('not.exist')
|
||||
})
|
||||
|
||||
it('Old and specialized programmes filter', () => {
|
||||
cy.contains('Biotekniikka (ylempi)').should('not.exist')
|
||||
cy.cs('filter-old-programmes-toggle').click()
|
||||
cy.contains('Biotekniikka (ylempi)').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Basic information tab works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click({ force: true })
|
||||
})
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Basic information tab loads', () => {
|
||||
cy.cs('students-of-the-study-programme-section')
|
||||
cy.cs('credits-produced-by-the-study-programme-section')
|
||||
cy.cs('graduated-and-thesis-writers-of-the-programme-section')
|
||||
cy.cs('programmes-before-or-after-section')
|
||||
cy.cs('average-graduation-times-section')
|
||||
})
|
||||
|
||||
it('Basic information contains correct students', () => {
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Started studying, Accepted, Graduated, Transferred Away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
[2023, 8, 8, 26, 0, 0],
|
||||
[2022, 25, 26, 48, 1, 4],
|
||||
[2021, 29, 32, 47, 0, 2],
|
||||
[2020, 26, 27, 12, 1, 3],
|
||||
[2019, 28, 34, 1, 0, 1],
|
||||
[2018, 40, 45, 0, 0, 0],
|
||||
[2017, 41, 47, 0, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'students-of-the-study-programme')
|
||||
})
|
||||
|
||||
it('Basic information contains correct credits', () => {
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
[2023, 1519, 1519, 0, 0, 0, 0, 0, 222, 0],
|
||||
[2022, 3235, 3205, 0, 0, 30, 0, 0, 209, 0],
|
||||
[2021, 5133, 5108, 0, 0, 25, 0, 0, 428, 25],
|
||||
[2020, 5801, 5796, 0, 0, 5, 0, 0, 94, 0],
|
||||
[2019, 5305, 5305, 0, 0, 0, 0, 0, 162, 0],
|
||||
[2018, 3442, 3432, 0, 0, 10, 0, 0, 21, 0],
|
||||
[2017, 1211, 1211, 0, 0, 0, 0, 0, 189, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'credits-produced-by-the-study-programme')
|
||||
})
|
||||
|
||||
it('Basic information contains correct thesis writers and graduates', () => {
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2023, 26, 5],
|
||||
[2022, 48, 13],
|
||||
[2021, 47, 76],
|
||||
[2020, 12, 23],
|
||||
[2019, 1, 19],
|
||||
[2018, 0, 0],
|
||||
[2017, 0, 1],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'graduated-and-graduations-of-the-programme')
|
||||
})
|
||||
|
||||
it('Special study rights can be excluded and basic data changes accordingly', () => {
|
||||
cy.cs('study-right-toggle').click()
|
||||
const years = getEmptyYears()
|
||||
const studentTableContents = [
|
||||
// [Year, Started studying, Accepted, Graduated]
|
||||
...years.map(year => [year, 0, 0, 0]),
|
||||
[2023, 8, 8, 24],
|
||||
[2022, 25, 26, 44],
|
||||
[2021, 29, 32, 46],
|
||||
[2020, 26, 27, 11],
|
||||
[2019, 28, 34, 1],
|
||||
[2018, 40, 45, 0],
|
||||
[2017, 41, 47, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(studentTableContents, 'students-of-the-study-programme')
|
||||
|
||||
const graduatedTableContents = [
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2023, 24, 5],
|
||||
[2022, 44, 12],
|
||||
[2021, 46, 69],
|
||||
[2020, 11, 22],
|
||||
[2019, 1, 18],
|
||||
[2018, 0, 0],
|
||||
[2017, 0, 1],
|
||||
]
|
||||
|
||||
cy.checkTableStats(graduatedTableContents, 'graduated-and-graduations-of-the-programme')
|
||||
})
|
||||
|
||||
it('Year can be changed to academic year, and data changes accordingly', () => {
|
||||
cy.cs('year-toggle').click()
|
||||
const isAcademicYear = true
|
||||
const years = getEmptyYears(isAcademicYear)
|
||||
const studentTableContents = [
|
||||
// [Year, Started studying, Accepted, Graduated, Transferred away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 8, 8, 4, 0, 0],
|
||||
['2022 - 2023', 25, 26, 36, 0, 0],
|
||||
['2021 - 2022', 29, 32, 57, 1, 6],
|
||||
['2020 - 2021', 26, 27, 30, 0, 3],
|
||||
['2019 - 2020', 28, 34, 7, 1, 1],
|
||||
['2018 - 2019', 40, 45, 0, 0, 0],
|
||||
['2017 - 2018', 41, 47, 0, 0, 0],
|
||||
]
|
||||
cy.checkTableStats(studentTableContents, 'students-of-the-study-programme')
|
||||
|
||||
const creditTableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 160, 160, 0, 0, 0, 0, 0, 67, 0],
|
||||
['2022 - 2023', 2725, 2720, 0, 0, 5, 0, 0, 337, 0],
|
||||
['2021 - 2022', 4092, 4042, 0, 0, 50, 0, 0, 198, 0],
|
||||
['2020 - 2021', 5420, 5415, 0, 0, 5, 0, 0, 321, 25],
|
||||
['2019 - 2020', 6043, 6043, 0, 0, 0, 0, 0, 101, 0],
|
||||
['2018 - 2019', 4856, 4851, 0, 0, 5, 0, 0, 107, 0],
|
||||
['2017 - 2018', 2350, 2345, 0, 0, 5, 0, 0, 26, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'credits-produced-by-the-study-programme')
|
||||
cy.cs('year-toggle').click()
|
||||
})
|
||||
|
||||
it('Basic information graphs render', () => {
|
||||
cy.cs('students-of-the-study-programme-line-graph-section')
|
||||
.should('contain', 'Started studying')
|
||||
.should('contain', 'Accepted')
|
||||
.should('contain', 'Graduated')
|
||||
.should('contain', 'Transferred away')
|
||||
.should('contain', 'Transferred to')
|
||||
|
||||
cy.cs('credits-produced-by-the-study-programme-stacked-bar-chart-section')
|
||||
.should('contain', 'Degree students')
|
||||
.should('contain', 'Transferred')
|
||||
.should('contain', 5796)
|
||||
.should('contain', 428)
|
||||
|
||||
cy.cs('graduated-and-thesis-writers-of-the-programme-bar-chart-section')
|
||||
.should('contain', 'Graduated students')
|
||||
.should('contain', 'Wrote thesis')
|
||||
.should('contain', 47)
|
||||
.should('contain', 76)
|
||||
|
||||
cy.cs('bachelor-breakdown-bar-chart-section')
|
||||
cy.cs('graduation-time-toggle').click()
|
||||
cy.cs('bachelor-median-time-bar-chart-section').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2022')
|
||||
cy.contains('48 graduated').trigger('mouseover')
|
||||
cy.contains('48 students graduated in year 2022')
|
||||
cy.contains('median study time: 6 semesters')
|
||||
cy.contains('26 graduated on time')
|
||||
cy.contains('10 graduated max year overtime')
|
||||
cy.contains('12 graduated over year late')
|
||||
})
|
||||
|
||||
cy.cs('programmes-before-or-after-stacked-bar-chart-section')
|
||||
.should('contain', 'Tietojenkäsittelytieteen maisteriohjelma')
|
||||
.should('contain', 'Datatieteen maisteriohjelma')
|
||||
.should('contain', 'Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
.should('contain', 32)
|
||||
.should('contain', 21)
|
||||
.should('contain', 17)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Graduation times of master programmes', () => {
|
||||
it('are split into two graphs', () => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Matematiikan ja tilastotieteen maisteriohjelma').click({ force: true })
|
||||
|
||||
cy.cs('bachelor-breakdown-bar-chart-section')
|
||||
cy.cs('master-breakdown-bar-chart-section')
|
||||
|
||||
cy.cs('graduation-time-toggle').click()
|
||||
cy.cs('master-median-time-bar-chart-section').within(() => {
|
||||
cy.contains('Master study right')
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2021')
|
||||
cy.contains('2 graduated').trigger('mouseover')
|
||||
cy.contains('2 students graduated in year 2021')
|
||||
cy.contains('median study time: 4.5 semesters')
|
||||
cy.contains('1 graduated on time')
|
||||
cy.contains('1 graduated max year overtime')
|
||||
cy.contains('0 graduated over year late')
|
||||
})
|
||||
|
||||
cy.cs('bachelor-median-time-bar-chart-section').within(() => {
|
||||
cy.contains('Bachelor + master study right')
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2023')
|
||||
cy.contains('11 graduated').trigger('mouseover')
|
||||
cy.contains('11 students graduated in year 2023')
|
||||
cy.contains('median study time: 12 semesters')
|
||||
cy.contains('4 graduated on time')
|
||||
cy.contains('6 graduated max year overtime')
|
||||
cy.contains('1 graduated over year late')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Study tracks and class statistics tab works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(
|
||||
'GET',
|
||||
'api/v2/studyprogrammes/KH50_001/studytrackstats?special_groups=SPECIAL_INCLUDED&combined_programme='
|
||||
).as('stQuery')
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click()
|
||||
cy.cs('StudyTracksAndClassStatisticsTab').click()
|
||||
})
|
||||
|
||||
describe('Info boxes', () => {
|
||||
it('Study track overview section', () => {
|
||||
cy.cs('study-track-overview-info-box-button').click()
|
||||
cy.cs('study-track-overview-info-box-content').contains('Opiskelijat, joiden')
|
||||
})
|
||||
|
||||
it('Progress of students section', () => {
|
||||
cy.cs('progress-of-students-info-box-button').click()
|
||||
cy.cs('progress-of-students-info-box-content').contains('Kuvaa koulutusohjelmassa')
|
||||
})
|
||||
|
||||
it('Average graduation times section', () => {
|
||||
cy.cs('average-graduation-times-info-box-button').click()
|
||||
cy.cs('average-graduation-times-info-box-content').contains('Yksittäinen palkki')
|
||||
})
|
||||
})
|
||||
|
||||
it('All sections are visible', () => {
|
||||
cy.cs('study-track-selector-section')
|
||||
cy.cs('study-track-overview-section')
|
||||
cy.cs('progress-of-students-section')
|
||||
cy.cs('average-graduation-times-section')
|
||||
})
|
||||
|
||||
it('Students of the degree programme are shown correctly', () => {
|
||||
const tableContents = [
|
||||
// [Year, All, Started studying, Present, Absent, Passive, Graduated, Men, Women, Other/Unknown, Finland, Other]
|
||||
['2023 - 2024', 8, 8, 0, 0, 8, 0, 5, 3, 0, 8, 1],
|
||||
['2022 - 2023', 26, 25, 0, 0, 24, 2, 19, 7, 0, 25, 1],
|
||||
['2021 - 2022', 38, 29, 0, 0, 33, 5, 29, 9, 0, 36, 4],
|
||||
['2020 - 2021', 30, 26, 0, 0, 11, 19, 15, 15, 0, 29, 3],
|
||||
['2019 - 2020', 35, 28, 0, 0, 8, 27, 22, 13, 0, 35, 2],
|
||||
['2018 - 2019', 45, 40, 0, 0, 6, 39, 26, 19, 0, 44, 1],
|
||||
['2017 - 2018', 47, 41, 0, 0, 5, 42, 31, 16, 0, 47, 0],
|
||||
['Total', 229, 197, 0, 0, 95, 134, 147, 82, 0, 224, 12],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'study-tracks-and-class-statistics')
|
||||
})
|
||||
|
||||
it.skip('Years in the students table can be expanded and study track data will be shown', () => {
|
||||
// TODO: Fix this test
|
||||
cy.cs('study-tracks-and-class-statistics-data-table').within(() => {
|
||||
cy.get('tbody tr.header-row')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get('td i.angle.right.icon').click()
|
||||
})
|
||||
|
||||
const dataForStudyTracks = [
|
||||
['Ekonometria, MAT-EKO', 2, 2, 0, 0, 0, 2, 1, 1, 0, 2, 0],
|
||||
['Matematiikka, MAT-MAT', 13, 10, 0, 0, 1, 12, 7, 6, 0, 12, 3],
|
||||
['Tietojenkäsittelyteoria, MAT-TIE', 2, 1, 0, 0, 1, 1, 2, 0, 0, 2, 0],
|
||||
['Tilastotiede, MAT-TIL', 4, 4, 0, 0, 0, 4, 1, 3, 0, 4, 0],
|
||||
]
|
||||
|
||||
dataForStudyTracks.forEach((data, index) => {
|
||||
cy.get('tbody tr.regular-row')
|
||||
.eq(index)
|
||||
.within(() => {
|
||||
cy.get('td').each((cell, i) => {
|
||||
cy.wrap(cell).contains(data[i])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('tbody tr.header-row')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get('td i.angle.down.icon').click()
|
||||
})
|
||||
|
||||
cy.get('tbody tr.regular-row').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Population link button works for', () => {
|
||||
it('a single year', () => {
|
||||
cy.cs('2023-population-link-button').first().click()
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2023 - 2024')
|
||||
cy.contains('Class size 8 students')
|
||||
})
|
||||
|
||||
it('total', () => {
|
||||
cy.cs('total-population-link-button').click()
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('Class size 227 students')
|
||||
})
|
||||
|
||||
it('Links to class statistics page with study track info included work', () => {
|
||||
cy.cs('show-study-tracks-button').first().click()
|
||||
cy.contains('td', 'Matematiikka (MAT-MAT)').within(() => {
|
||||
cy.get('a').click()
|
||||
})
|
||||
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2022 - 2023')
|
||||
cy.contains('Studytrack MAT-MAT')
|
||||
cy.contains('Class size 26 students')
|
||||
cy.contains('Showing 3 out of 26 students')
|
||||
})
|
||||
})
|
||||
|
||||
it('Student progress data is shown correctly', () => {
|
||||
const years = getEmptyYears(true)
|
||||
const tableContents = [
|
||||
// [Year, All, < 30 credits, 30–60 credits, 60–90 credits, 90–120 credits, 120–150 credits, 150–180 credits, ≥ 180 credits, Graduated]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 8, 8, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2022 - 2023', 26, 9, 8, 4, 3, 0, 0, 0, 2],
|
||||
['2021 - 2022', 38, 8, 4, 9, 8, 4, 0, 0, 5],
|
||||
['2020 - 2021', 30, 2, 1, 2, 5, 0, 0, 1, 19],
|
||||
['2019 - 2020', 35, 1, 0, 2, 1, 0, 0, 4, 27],
|
||||
['2018 - 2019', 45, 0, 1, 1, 2, 0, 0, 2, 39],
|
||||
['2017 - 2018', 47, 0, 1, 3, 0, 0, 1, 0, 42],
|
||||
['Total', 229, 28, 15, 21, 19, 4, 1, 7, 134],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'study-programme-progress')
|
||||
})
|
||||
|
||||
it('Progress section', () => {
|
||||
cy.cs('programme-progress-bar-chart-section').scrollIntoView()
|
||||
|
||||
cy.cs('programme-progress-bar-chart-section')
|
||||
.should('contain', 'Less than 30 credits')
|
||||
.should('contain', '30–60 credits')
|
||||
.should('contain', 'At least 180 credits')
|
||||
.should('contain', '58.5%') // The percentage for total graduated, to check that the graph renders
|
||||
|
||||
cy.cs('programme-progress-bar-chart-section').contains('58.5%').trigger('mouseover', { force: true })
|
||||
cy.contains('Graduated: 134')
|
||||
})
|
||||
|
||||
it('Average graduation times section', () => {
|
||||
cy.cs('average-graduation-times-section').within(() => {
|
||||
cy.contains('2020 - 2021')
|
||||
cy.get('[aria-label="2020 - 2021, 16. On time."]').trigger('mouseover')
|
||||
cy.contains('On time: 16')
|
||||
cy.contains("Click a bar to view that year's study track level breakdown")
|
||||
cy.get('[aria-label="2019 - 2020, 22. On time."]').click()
|
||||
cy.contains('Year 2019 - 2020 by start year')
|
||||
cy.get('[aria-label="MAT-MAT, 13. On time."]').trigger('mouseover')
|
||||
cy.contains('Matematiikka')
|
||||
cy.contains('MAT-MAT')
|
||||
cy.contains('On time: 13')
|
||||
})
|
||||
|
||||
cy.cs('graduation-time-toggle').click()
|
||||
|
||||
cy.cs('average-graduation-times-section').within(() => {
|
||||
cy.contains('2020 - 2021')
|
||||
cy.contains('19 graduated').trigger('mouseover')
|
||||
cy.contains('From class of 2020 - 2021, 19/30 students have graduated')
|
||||
cy.contains('median study time: 6 semesters')
|
||||
cy.contains('16 graduated on time')
|
||||
cy.contains('2 graduated max year overtime')
|
||||
cy.contains('1 graduated over year late')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Study track can be changed', () => {
|
||||
beforeEach(() => {
|
||||
cy.wait('@stQuery')
|
||||
cy.contains('All students of the programme')
|
||||
cy.cs('study-track-select').click()
|
||||
cy.contains('Ekonometria')
|
||||
cy.contains('Tietojenkäsittelyteoria')
|
||||
cy.contains('Tilastotiede')
|
||||
cy.contains('Matematiikka').click()
|
||||
})
|
||||
|
||||
it('Students of the study track are shown correctly', () => {
|
||||
cy.cs('study-track-overview-section').contains('Students of the study track MAT-MAT by starting year')
|
||||
const tableContents = [
|
||||
// [Year, All, Started studying, Present, Absent, Passive, Graduated, Men, Women, Other/Unknown, Finland, Other]
|
||||
['2022 - 2023', 3, 3, 0, 0, 1, 2, 2, 1, 0, 3, 0],
|
||||
['2021 - 2022', 5, 1, 0, 0, 1, 4, 3, 2, 0, 5, 0],
|
||||
['2020 - 2021', 13, 10, 0, 0, 1, 12, 7, 6, 0, 12, 3],
|
||||
['2019 - 2020', 17, 14, 0, 0, 0, 17, 10, 7, 0, 17, 1],
|
||||
['2018 - 2019', 23, 21, 0, 0, 2, 21, 11, 12, 0, 23, 0],
|
||||
['2017 - 2018', 28, 24, 0, 0, 1, 27, 15, 13, 0, 28, 0],
|
||||
['Total', 89, 73, 0, 0, 6, 83, 48, 41, 0, 88, 4],
|
||||
]
|
||||
cy.checkTableStats(tableContents, 'study-tracks-and-class-statistics')
|
||||
})
|
||||
|
||||
it('Links to class statistics page with study track info included work', () => {
|
||||
cy.cs('2020-population-link-button').first().click()
|
||||
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
|
||||
cy.contains('Studytrack MAT-MAT')
|
||||
cy.contains('Class size 30 students')
|
||||
cy.contains('Showing 10 out of 30 students')
|
||||
})
|
||||
|
||||
it('Info message about missing progress stats is displayed', () => {
|
||||
cy.contains('Progress of students of the study track MAT-MAT by starting year')
|
||||
cy.contains('Progress data is currently only available for all students of the degree programme.')
|
||||
})
|
||||
|
||||
it.skip('Average graduation times are displayed correctly', () => {
|
||||
cy.get("[data-cy='Section-averageGraduationTimesStudyTracks']")
|
||||
cy.get("[data-cy='graduation-times-graph-breakdownBachelor']").within(() => {
|
||||
cy.contains('Start year')
|
||||
cy.contains('2020 - 2021')
|
||||
cy.get('[aria-label="2020 - 2021, 9. On time."]').trigger('mouseover')
|
||||
cy.contains('Graduated On time: 9 students')
|
||||
})
|
||||
|
||||
cy.cs('graduation-time-toggle').click()
|
||||
cy.cs('bachelor-median-time-bar-chart').within(() => {
|
||||
cy.contains('Start year')
|
||||
cy.contains('2020 - 2021')
|
||||
cy.contains('12 graduated').trigger('mouseover')
|
||||
cy.contains('From class of 2020 - 2021, 12/13 students have graduated')
|
||||
cy.contains('median study time: 34 months')
|
||||
cy.contains('9 graduated on time')
|
||||
cy.contains('2 graduated max year overtime')
|
||||
cy.contains('1 graduated over year late')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Programme courses tab works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click()
|
||||
cy.cs('ProgrammeCoursesTab').click()
|
||||
|
||||
cy.cs('by-credit-type-section')
|
||||
cy.cs('by-credit-type-tab')
|
||||
cy.cs('by-semester-tab')
|
||||
})
|
||||
|
||||
describe('By credit type tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.get('thead').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('th').eq(3).click()
|
||||
})
|
||||
})
|
||||
cy.get('tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(8)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('Course')
|
||||
cy.get('td').eq(1).contains('MAT11004')
|
||||
cy.get('td').eq(2).contains('Differentiaalilaskenta')
|
||||
cy.get('td').eq(3).contains('1170')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('time range selection works', () => {
|
||||
cy.cs('from-year-select').click()
|
||||
cy.cs('from-year-select-option-2021').click()
|
||||
cy.cs('to-year-select').click()
|
||||
cy.cs('to-year-select-option-2022').click()
|
||||
|
||||
cy.get('tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(9)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('Course')
|
||||
cy.get('td').eq(1).contains('MAT21009')
|
||||
cy.get('td').eq(2).contains('Kandidaatintutkielma')
|
||||
cy.get('td').eq(3).contains('414')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('year toggle works', () => {
|
||||
cy.cs('year-toggle').click()
|
||||
|
||||
cy.cs('from-year-select').click()
|
||||
cy.cs('from-year-select-option-2017').click()
|
||||
cy.cs('to-year-select').click()
|
||||
cy.cs('to-year-select-option-2023').click()
|
||||
|
||||
cy.get('tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(5)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('Course')
|
||||
cy.get('td').eq(1).contains('MAT11001')
|
||||
cy.get('td').eq(2).contains('Johdatus yliopistomatematiikkaan')
|
||||
cy.get('td').eq(3).contains('1220')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: Fix sorting / filters
|
||||
it.skip('different sorting options work', () => {
|
||||
cy.cs('CoursesSortableTable').within(() => {
|
||||
// Course code
|
||||
cy.get('th').eq(0).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('odgi-zef0')
|
||||
cy.get('td').eq(1).contains('Työ- ja organisaatiopsykologian perusopinnot')
|
||||
})
|
||||
cy.get('th').eq(0).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT-yht')
|
||||
cy.get('td').eq(1).contains('Muut opinnot')
|
||||
})
|
||||
|
||||
// Course name
|
||||
cy.get('th').eq(1).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT20003')
|
||||
cy.get('td').eq(1).contains('Äidinkielen opinnot')
|
||||
})
|
||||
cy.get('th').eq(1).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT11008')
|
||||
cy.get('td').eq(1).contains('Advanced calculus')
|
||||
})
|
||||
|
||||
// Total credits
|
||||
cy.get('th').eq(2).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT110')
|
||||
cy.get('td').eq(1).contains('Matematiikka, perusopinnot')
|
||||
})
|
||||
cy.get('th').eq(2).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT20001')
|
||||
cy.get('td').eq(1).contains('Kypsyysnäyte')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("'Show credits/Show students' toggle works", () => {
|
||||
cy.get('thead tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('th').should('have.length', 6)
|
||||
const headers = ['Type', 'Code', 'Name', 'Total credits', 'Credits produced by', 'Transferred credits']
|
||||
headers.forEach((header, index) => {
|
||||
cy.get('th').eq(index).contains(header)
|
||||
})
|
||||
})
|
||||
cy.get('thead tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('th').should('have.length', 7)
|
||||
const headers = [
|
||||
'Degree students',
|
||||
'Open university (hetu)',
|
||||
'Open university (no hetu)',
|
||||
'Exchange students',
|
||||
'Other universities',
|
||||
'Separate studies',
|
||||
'Other',
|
||||
]
|
||||
headers.forEach((header, index) => {
|
||||
cy.get('th').eq(index).contains(header)
|
||||
})
|
||||
})
|
||||
|
||||
cy.cs('show-credits-students-toggle').click()
|
||||
|
||||
cy.get('thead tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('th').should('have.length', 7)
|
||||
const headers = [
|
||||
'Type',
|
||||
'Code',
|
||||
'Name',
|
||||
'Total students',
|
||||
'Breakdown of total',
|
||||
'Breakdown of passed',
|
||||
'Not included in total or passed',
|
||||
]
|
||||
headers.forEach((header, index) => {
|
||||
cy.get('th').eq(index).contains(header)
|
||||
})
|
||||
})
|
||||
cy.get('thead tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('th').should('have.length', 10)
|
||||
const headers = [
|
||||
'Passed',
|
||||
'Not completed',
|
||||
'Degree students',
|
||||
'Open university students (with hetu)',
|
||||
'Open university students (without hetu)',
|
||||
'Exchange students',
|
||||
'Other university students',
|
||||
'Separate studies',
|
||||
'Other students',
|
||||
'Students with transferred credits',
|
||||
]
|
||||
headers.forEach((header, index) => {
|
||||
cy.get('th').eq(index).contains(header)
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(5)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('Course')
|
||||
cy.get('td').eq(1).contains('MAT11001')
|
||||
cy.get('td').eq(2).contains('Johdatus yliopistomatematiikkaan')
|
||||
cy.get('td').eq(3).contains('375')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('By semester tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.cs('by-semester-tab').click()
|
||||
})
|
||||
|
||||
// FIXME: Flaky
|
||||
it.skip('test', () => {
|
||||
// TODO: Implement some real tests for this tab
|
||||
cy.contains('From')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Degree courses tab works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click()
|
||||
cy.cs('DegreeCoursesTab').click()
|
||||
})
|
||||
|
||||
it('Curriculum section', () => {
|
||||
cy.cs('curriculum-section')
|
||||
cy.contains('Select curriculum to edit:')
|
||||
cy.cs('curriculum-picker').contains('2023–2026')
|
||||
// TODO: Test changing the curriculum
|
||||
})
|
||||
|
||||
describe('Credit criteria section', () => {
|
||||
it('info box', () => {
|
||||
cy.cs('credit-criteria-section')
|
||||
cy.cs('credit-criteria-info-box-button').click()
|
||||
cy.cs('credit-criteria-info-box-content').contains('Here you can change')
|
||||
})
|
||||
|
||||
it.skip('changing the credit criteria works', () => {
|
||||
// TODO: Test using the inputs
|
||||
// TODO: Button status
|
||||
// TODO: Test "previously set to _" text changing when saving
|
||||
})
|
||||
})
|
||||
|
||||
it('Degree course table', () => {
|
||||
cy.cs('degree-course-table')
|
||||
// TODO: Test clicking the arrow buttons
|
||||
// TODO: Test toggling visibility for MODULES and COURSES
|
||||
// TODO: Test changing criterion labels
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tags tab works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click({ force: true })
|
||||
cy.cs('TagsTab').click()
|
||||
})
|
||||
|
||||
it('info box', () => {
|
||||
cy.cs('create-new-tag-info-box-button').click()
|
||||
cy.cs('create-new-tag-info-box-content').contains('Here you can create')
|
||||
})
|
||||
|
||||
describe('Adding tags to populations and removing them works', () => {
|
||||
beforeEach(() => {
|
||||
cy.contains('This degree programme does not have any tags yet')
|
||||
cy.cs('tag-name-text-field').type(tagName)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
deleteTag(tagName)
|
||||
cy.contains('This degree programme does not have any tags yet')
|
||||
})
|
||||
|
||||
it('can create and delete tags without start year', () => {
|
||||
cy.cs('create-button').click()
|
||||
cy.contains(tagName)
|
||||
cy.contains('No associated start year')
|
||||
})
|
||||
|
||||
it.skip('can create and delete tags with start year', () => {
|
||||
// TODO: This test fails in the pipeline, but works locally. Investigate.
|
||||
selectYear('2022')
|
||||
cy.cs('create-button').click()
|
||||
cy.contains(tagName)
|
||||
cy.contains('Associated start year 2022')
|
||||
})
|
||||
|
||||
it.skip('population link works', () => {
|
||||
// TODO: This test fails in the pipeline, but works locally. Investigate.
|
||||
selectYear('2022')
|
||||
cy.cs('create-button').click()
|
||||
cy.contains(tagName)
|
||||
cy.cs('population-link-button').click()
|
||||
cy.contains(`Tagged with: ${tagName}`)
|
||||
cy.go('back')
|
||||
})
|
||||
|
||||
it('can create personal tags', () => {
|
||||
cy.cs('personal-tag-toggle').click()
|
||||
cy.cs('create-button').click()
|
||||
cy.contains(tagName)
|
||||
cy.cs(`${tagName}-visibility-icon`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Adding tags to students and removing them works', () => {
|
||||
const studentInput = '477806,478275; 478953 479239\n 480080'
|
||||
const studentNumbers = studentInput.match(/[^\s,;]+/g)
|
||||
|
||||
it.skip('can add tags to students', () => {
|
||||
// TODO: This test fails in the pipeline, but works locally. Investigate.
|
||||
cy.cs('tag-name-text-field').type(tagName)
|
||||
selectYear('2022')
|
||||
cy.cs('create-button').click()
|
||||
cy.contains(tagName)
|
||||
|
||||
cy.cs('add-students-button').click()
|
||||
cy.cs('add-students-text-field').type(studentInput)
|
||||
cy.cs('add-students-confirm-button').click()
|
||||
cy.contains('Students added to tag')
|
||||
|
||||
cy.cs('population-link-button').click()
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2022 - 2023')
|
||||
cy.contains(`Tagged with: ${tagName}`)
|
||||
cy.contains('Students (5)')
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains('Students (5)').click()
|
||||
})
|
||||
|
||||
for (const studentNumber of studentNumbers) {
|
||||
cy.contains(studentNumber)
|
||||
}
|
||||
|
||||
for (const studentNumber of studentNumbers) {
|
||||
cy.visit(`/students/${studentNumber}`)
|
||||
cy.contains('Tags')
|
||||
cy.contains(tagName)
|
||||
}
|
||||
|
||||
cy.get('a').contains('Matemaattisten tieteiden kandiohjelma').invoke('removeAttr', 'target').click()
|
||||
cy.url().should('include', '/study-programme/KH50_001?tab=4')
|
||||
|
||||
deleteTag(tagName)
|
||||
})
|
||||
|
||||
it('deleting a tag from tag view also removes it from students', () => {
|
||||
cy.contains(tagName).should('not.exist')
|
||||
for (const studentNumber of studentNumbers) {
|
||||
cy.visit(`/students/${studentNumber}`)
|
||||
cy.contains('Tags').should('not.exist')
|
||||
cy.contains(tagName).should('not.exist')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('IAM user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme', 'onlyiamrights')
|
||||
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click()
|
||||
})
|
||||
|
||||
it.skip('year selector', () => {
|
||||
// TODO: Implement this test
|
||||
// Things to test:
|
||||
// - Clicking the button works
|
||||
// - Selecting a study track works
|
||||
// - The button is disabled if only one year is selected
|
||||
// - The slider is not visible if the user does not have correct programme rights
|
||||
})
|
||||
|
||||
it('can access programme and correct tabs are visible', () => {
|
||||
cy.contains('Basic information')
|
||||
cy.contains('Study tracks and class statistics')
|
||||
|
||||
cy.contains('Update statistics').should('not.exist')
|
||||
cy.contains('Degree courses').should('not.exist')
|
||||
})
|
||||
|
||||
it('can access basic information', () => {
|
||||
cy.cs('BasicInformationTab').click()
|
||||
|
||||
cy.cs('students-of-the-study-programme-section')
|
||||
cy.cs('credits-produced-by-the-study-programme-section')
|
||||
cy.cs('graduated-and-thesis-writers-of-the-programme-section')
|
||||
cy.cs('average-graduation-times-section')
|
||||
cy.cs('programmes-before-or-after-section')
|
||||
})
|
||||
|
||||
it('can access study tracks', () => {
|
||||
cy.cs('StudyTracksAndClassStatisticsTab').click()
|
||||
|
||||
cy.cs('study-track-overview-section')
|
||||
cy.cs('progress-of-students-section')
|
||||
cy.cs('average-graduation-times-section')
|
||||
})
|
||||
|
||||
it("doesn't see other tabs", () => {
|
||||
cy.cs('DegreeCoursesTab').should('not.exist')
|
||||
cy.cs('TagsTab').should('not.exist')
|
||||
cy.cs('UpdateStatisticsTab').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,287 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const path = require('path')
|
||||
|
||||
const timestamp = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }) // YYYY-MM-DD, taking the local time zone into account
|
||||
const downloadsFolder = Cypress.config('downloadsFolder')
|
||||
|
||||
describe('Faculty statistics', () => {
|
||||
describe('Faculty list', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
})
|
||||
|
||||
it('contains faculty names and faculty codes', () => {
|
||||
cy.contains('Faculties')
|
||||
cy.contains('Teologinen tiedekunta')
|
||||
cy.contains('H10')
|
||||
cy.contains('Oikeustieteellinen tiedekunta')
|
||||
cy.contains('H20')
|
||||
cy.contains('Lääketieteellinen tiedekunta')
|
||||
cy.contains('H30')
|
||||
cy.contains('Humanistinen tiedekunta')
|
||||
cy.contains('H40')
|
||||
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
|
||||
cy.contains('H50')
|
||||
cy.contains('Farmasian tiedekunta')
|
||||
cy.contains('H55')
|
||||
cy.contains('Bio- ja ympäristötieteellinen tiedekunta')
|
||||
cy.contains('H57')
|
||||
cy.contains('Kasvatustieteellinen tiedekunta')
|
||||
cy.contains('H60')
|
||||
cy.contains('Valtiotieteellinen tiedekunta')
|
||||
cy.contains('H70')
|
||||
cy.contains('Svenska social- och kommunalhögskolan')
|
||||
cy.contains('H74')
|
||||
cy.contains('Maatalous-metsätieteellinen tiedekunta')
|
||||
cy.contains('H80')
|
||||
cy.contains('Eläinlääketieteellinen tiedekunta')
|
||||
cy.contains('H90')
|
||||
})
|
||||
|
||||
it('contains a working link to faculty page', () => {
|
||||
cy.contains('Faculties')
|
||||
cy.contains('a', 'Teologinen tiedekunta').click()
|
||||
cy.location('pathname').should('eq', '/faculties/hy-org-1000000580')
|
||||
cy.contains('Teologinen tiedekunta')
|
||||
cy.contains('H10')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Basic information tab', () => {
|
||||
describe('Admin user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.contains('a', 'Eläinlääketieteellinen tiedekunta').click()
|
||||
})
|
||||
|
||||
it('Correct tabs are shown', () => {
|
||||
cy.cs('faculty-tabs').should('contain', 'Basic information')
|
||||
cy.cs('faculty-tabs').should('contain', 'Students by starting year')
|
||||
cy.cs('faculty-tabs').should('contain', 'Graduation times')
|
||||
cy.cs('faculty-tabs').should('contain', 'Update statistics')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
|
||||
})
|
||||
|
||||
describe('Info boxes', () => {
|
||||
it('Students of the faculty', () => {
|
||||
cy.cs('students-of-the-faculty-info-box-button').click()
|
||||
cy.cs('students-of-the-faculty-info-box-content').contains('Taulukon luvut on')
|
||||
})
|
||||
|
||||
it('Graduated of the faculty', () => {
|
||||
cy.cs('graduated-of-the-faculty-info-box-button').click()
|
||||
cy.cs('graduated-of-the-faculty-info-box-content').contains('Sisältää kyseisenä')
|
||||
})
|
||||
|
||||
it('Thesis writers of the faculty', () => {
|
||||
cy.cs('thesis-writers-of-the-faculty-info-box-button').click()
|
||||
cy.cs('thesis-writers-of-the-faculty-info-box-content').contains('Sisältää kyseisenä')
|
||||
})
|
||||
|
||||
it('Credits produced by the faculty', () => {
|
||||
cy.cs('credits-produced-by-the-faculty-info-box-button').click()
|
||||
cy.cs('credits-produced-by-the-faculty-info-box-content').contains('Sisältää opintopisteet')
|
||||
})
|
||||
})
|
||||
|
||||
it('All graphs and tables are shown', () => {
|
||||
cy.cs('students-of-the-faculty-line-graph-section').should('be.visible')
|
||||
cy.cs('students-of-the-faculty-interactive-data-table').should('be.visible')
|
||||
cy.cs('graduated-of-the-faculty-line-graph-section').should('be.visible')
|
||||
cy.cs('graduated-of-the-faculty-interactive-data-table').should('be.visible')
|
||||
cy.cs('thesis-writers-of-the-faculty-line-graph-section').should('be.visible')
|
||||
cy.cs('thesis-writers-of-the-faculty-interactive-data-table').should('be.visible')
|
||||
cy.cs('credits-produced-by-the-faculty-stacked-bar-chart-section').should('be.visible')
|
||||
cy.cs('credits-produced-by-the-faculty-interactive-data-table').should('be.visible')
|
||||
})
|
||||
|
||||
it('Correct tabs are shown', () => {
|
||||
cy.cs('faculty-tabs').should('contain', 'Basic information')
|
||||
cy.cs('faculty-tabs').should('contain', 'Students by starting year')
|
||||
cy.cs('faculty-tabs').should('contain', 'Graduation times')
|
||||
cy.cs('faculty-tabs').should('not.contain', 'Update statistics')
|
||||
})
|
||||
|
||||
it('Toggle years works', () => {
|
||||
cy.cs('credits-produced-by-the-faculty-interactive-data-table').should('contain', '2022')
|
||||
cy.cs('thesis-writers-of-the-faculty-interactive-data-table').should('contain', '2022')
|
||||
cy.cs('students-of-the-faculty-interactive-data-table').should('contain', '2022')
|
||||
cy.cs('graduated-of-the-faculty-interactive-data-table').should('contain', '2022')
|
||||
cy.cs('year-toggle').click()
|
||||
cy.cs('credits-produced-by-the-faculty-interactive-data-table').should('contain', '2022 - 2023')
|
||||
cy.cs('thesis-writers-of-the-faculty-interactive-data-table').should('contain', '2022 - 2023')
|
||||
cy.cs('students-of-the-faculty-interactive-data-table').should('contain', '2022 - 2023')
|
||||
cy.cs('graduated-of-the-faculty-interactive-data-table').should('contain', '2022 - 2023')
|
||||
})
|
||||
|
||||
it('Toggle programmes works', () => {
|
||||
cy.cs('faculty-programmes-shown-info').should('not.exist')
|
||||
cy.cs('programme-toggle').click()
|
||||
cy.cs('faculty-programmes-shown-info').should('be.visible')
|
||||
})
|
||||
|
||||
it('Toggle study rights works', () => {
|
||||
cy.cs('faculty-exclude-specials-info').should('not.exist')
|
||||
cy.cs('study-right-toggle').click()
|
||||
cy.cs('faculty-exclude-specials-info').should('be.visible')
|
||||
})
|
||||
|
||||
it.skip('Data can be exported to Excel files', { retries: 2 }, () => {
|
||||
// TODO: Fix this test
|
||||
const sections = [
|
||||
'StudentsOfTheFaculty',
|
||||
'GraduatedOfTheFaculty',
|
||||
'ThesisWritersOfTheFaculty',
|
||||
'CreditsProducedByTheFaculty',
|
||||
]
|
||||
sections.forEach(section => {
|
||||
cy.cs(`${section}Graph`).within(() => {
|
||||
cy.get('[aria-label="View chart menu, Chart"]').click({ force: true })
|
||||
cy.contains('li.highcharts-menu-item', 'Download XLS').click()
|
||||
const downloadedFile = `oodikone_${section}_H50_${timestamp}.xls`
|
||||
cy.readFile(path.join(downloadsFolder, downloadedFile))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Students by starting year tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
|
||||
cy.contains('Students by starting year').click()
|
||||
})
|
||||
|
||||
it('Export button', { retries: 2 }, () => {
|
||||
cy.cs('faculty-student-table-export-button').should('be.visible').and('not.be.disabled').click()
|
||||
const downloadedFile = `oodikone_H50_programme_stats_${timestamp}.xlsx`
|
||||
cy.readFile(path.join(downloadsFolder, downloadedFile))
|
||||
})
|
||||
|
||||
it('Info box', () => {
|
||||
cy.cs('faculty-student-table-info-box-button').click()
|
||||
cy.cs('faculty-student-table-info-box-content').contains('Opiskelijat, joiden')
|
||||
})
|
||||
|
||||
it.skip('Study right toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Graduated toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Hoverable country list', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Expandable rows', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Population links', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it('Percentage toggle works', () => {
|
||||
cy.cs('faculty-student-stats-table').should('be.visible')
|
||||
cy.cs('faculty-student-stats-table').should('not.contain', '92.3 %')
|
||||
cy.cs('percentage-toggle').click()
|
||||
cy.cs('faculty-student-stats-table').should('contain', '92.3 %')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Progress tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
|
||||
cy.contains('Progress').click()
|
||||
})
|
||||
|
||||
it.skip('Export button', { retries: 2 }, () => {
|
||||
// TODO: Fix this test
|
||||
cy.cs('FacultyProgress-export-button').should('be.visible').and('not.be.disabled').click()
|
||||
const downloadedFile = `oodikone_H50_progress_tab_${timestamp}.xlsx`
|
||||
cy.readFile(path.join(downloadsFolder, downloadedFile))
|
||||
})
|
||||
|
||||
it.skip('Info boxes', () => {
|
||||
cy.cs('FacultyProgress-info-box-button').click()
|
||||
cy.cs('FacultyProgress-info-box-content').contains('Kuvaa tiedekuntaan')
|
||||
// TODO: Click open the accordion first
|
||||
cy.cs('BachelorMastersProgress-info-box-button').click()
|
||||
cy.cs('BachelorMastersProgress-info-box-content').contains('The starting year is the')
|
||||
})
|
||||
|
||||
it.skip('Progress bar charts exist', () => {
|
||||
// TODO: Click open the accordion first
|
||||
cy.cs('FacultyBachelorsProgressBarChart').should('be.visible')
|
||||
cy.cs('FacultyBachelorMastersProgressBarChart').should('be.visible')
|
||||
cy.cs('FacultyMastersProgressBarChart').should('be.visible')
|
||||
cy.cs('FacultyBachelorsProgressBarChart').should('be.visible')
|
||||
})
|
||||
|
||||
it.skip('Progress tables exist', () => {
|
||||
// TODO: Click open the accordion first
|
||||
cy.cs('FacultyBachelorsProgressTable').should('be.visible')
|
||||
cy.cs('FacultyBachelorMasterProgressTable').should('be.visible')
|
||||
cy.cs('FacultyMastersProgressTable').should('be.visible')
|
||||
cy.cs('FacultyBachelorsProgressTable').should('be.visible')
|
||||
})
|
||||
|
||||
it.skip('Graduated toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Study right toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
})
|
||||
|
||||
describe('Graduation times tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
|
||||
cy.contains('Graduation times').click()
|
||||
})
|
||||
|
||||
it.skip('Export button', { retries: 2 }, () => {
|
||||
// TODO: Fix this test
|
||||
cy.cs('average-graduation-times-export-button').should('be.visible').and('not.be.disabled').click()
|
||||
const downloadedFile = `oodikone_H50_graduation_times_${timestamp}.xlsx`
|
||||
cy.readFile(path.join(downloadsFolder, downloadedFile))
|
||||
})
|
||||
|
||||
it('Info boxes', () => {
|
||||
cy.cs('average-graduation-times-info-box-button').click()
|
||||
cy.cs('average-graduation-times-info-box-content').contains('Opiskelijoiden keskimääräiset')
|
||||
})
|
||||
|
||||
it.skip('Study times toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Year toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Degree programme toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it('Graphs are visible', () => {
|
||||
cy.cs('bachelor-graduation-times-section').should('be.visible')
|
||||
// ! No test data for bachelor + master
|
||||
cy.cs('master-graduation-times-section').should('be.visible')
|
||||
cy.cs('doctor-graduation-times-section').should('be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,468 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const setClockToMockedDate = () => {
|
||||
const MOCKED_DATE = new Date('2024-08-30')
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
}
|
||||
|
||||
const checkFilteredStudentCount = studentCount => {
|
||||
cy.contains(`Students (${studentCount})`)
|
||||
}
|
||||
|
||||
describe("Population statistics with a master's programme", { testIsolation: false }, () => {
|
||||
const pathToMathMSc2020 = '/populations?years=2020&programme=MH50_001&semesters=FALL&semesters=SPRING'
|
||||
const defaultAmountOfStudents = 26
|
||||
const expectedBaMaStudents = 24
|
||||
const expectedMasterOnlyStudents = 2
|
||||
|
||||
before(() => cy.init(pathToMathMSc2020))
|
||||
|
||||
it('Study track filter works', () => {
|
||||
cy.cs('studyTrackFilter-header').click()
|
||||
cy.selectFromDropdown('studyTrackFilter', 0, true)
|
||||
checkFilteredStudentCount(15)
|
||||
|
||||
cy.cs('studyTrackFilter-clear').click()
|
||||
cy.cs('studyTrackFilter-header').click()
|
||||
})
|
||||
|
||||
it('Study right type filter works', () => {
|
||||
cy.cs('studyRightTypeFilter-header').click()
|
||||
cy.get('input[name="studyRightTypeFilter"][value="0"]').should('be.checked')
|
||||
|
||||
// Bachelor + master
|
||||
cy.get('input[name="studyRightTypeFilter"][value="1"]').click()
|
||||
cy.get('input[name="studyRightTypeFilter"][value="1"]').should('be.checked')
|
||||
checkFilteredStudentCount(expectedBaMaStudents)
|
||||
// Master only
|
||||
cy.get('input[name="studyRightTypeFilter"][value="2"]').click()
|
||||
cy.get('input[name="studyRightTypeFilter"][value="2"]').should('be.checked')
|
||||
checkFilteredStudentCount(expectedMasterOnlyStudents)
|
||||
// All
|
||||
cy.get('input[name="studyRightTypeFilter"][value="0"]').click()
|
||||
cy.get('input[name="studyRightTypeFilter"][value="0"]').should('be.checked')
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
|
||||
cy.cs('studyRightTypeFilter-header').click()
|
||||
})
|
||||
})
|
||||
|
||||
describe.skip('Population Statistics', { testIsolation: false }, () => {
|
||||
const pathToMathBSc2020 = '/populations?years=2020&programme=KH50_001&semesters=FALL&semesters=SPRING'
|
||||
const defaultAmountOfStudents = 27
|
||||
const graduated = 16
|
||||
const transferred = 3
|
||||
|
||||
before(() => cy.init(pathToMathBSc2020))
|
||||
beforeEach(() => setClockToMockedDate())
|
||||
|
||||
it.skip('Study right type filter is not visible', () => {
|
||||
cy.contains('Study right type').should('not.exist')
|
||||
})
|
||||
|
||||
// TODO: FilterRadio is not working correcly
|
||||
it.skip('Transfer filter works and is set to "not transferred" by default', () => {
|
||||
cy.cs('transferredToProgrammeFilter-filter-card').should('have.attr', 'data-active', 'true')
|
||||
cy.get('input[name="transferredToProgrammeFilter"][value="Not transferred"]').should('be.checked')
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
|
||||
cy.cs('transferredToProgrammeFilter-filter-card').should('have.attr', 'data-active', 'true')
|
||||
cy.get('input[name="transferredToProgrammeFilter"][value="Not transferred"]').should('be.checked')
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
|
||||
cy.get('input[name="transferredToProgrammeFilter"][value="Transferred"]').click()
|
||||
cy.get('input[name="transferredToProgrammeFilter"][value="Transferred"]').should('be.checked')
|
||||
checkFilteredStudentCount(transferred)
|
||||
|
||||
cy.get('input[name="transferredToProgrammeFilter"][value="All"]').click()
|
||||
cy.get('input[name="transferredToProgrammeFilter"][value="All"]').should('be.checked')
|
||||
checkFilteredStudentCount(defaultAmountOfStudents + transferred)
|
||||
|
||||
cy.get('input[name="transferredToProgrammeFilter"][value="Not transferred"]').click()
|
||||
cy.get('input[name="transferredToProgrammeFilter"][value="Not transferred"]').should('be.checked')
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Graduation filter works', () => {
|
||||
cy.cs('graduatedFromProgrammeFilter-header').click()
|
||||
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="1"]').click()
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="1"]').should('be.checked')
|
||||
checkFilteredStudentCount(graduated)
|
||||
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="-1"]').click()
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="-1"]').should('be.checked')
|
||||
checkFilteredStudentCount(defaultAmountOfStudents - graduated)
|
||||
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="0"]').click()
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="0"]').should('be.checked')
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
|
||||
cy.cs('graduatedFromProgrammeFilter-header').click()
|
||||
})
|
||||
|
||||
it('Enrollment filter works', () => {
|
||||
cy.cs('enrollmentStatusFilter-header').click()
|
||||
cy.selectFromDropdown('enrollmentStatusFilter-status', 0)
|
||||
cy.selectFromDropdown('enrollmentStatusFilter-semester', 'Syksy 2023', true)
|
||||
checkFilteredStudentCount(25)
|
||||
|
||||
cy.cs('enrollmentStatusFilter-clear').click()
|
||||
cy.cs('enrollmentStatusFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Credit filter works', () => {
|
||||
cy.cs('creditsEarnedFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
cy.setRangeSelect('creditsEarnedFilter', 50, 150, 12)
|
||||
|
||||
cy.cs('creditsEarnedFilter-clear').click()
|
||||
cy.cs('creditsEarnedFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
cy.cs('ageFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
cy.setRangeSelect('ageFilter', 23, 30, 21)
|
||||
|
||||
cy.cs('ageFilter-clear').click()
|
||||
cy.cs('ageFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
cy.cs('genderFilter-header').click()
|
||||
cy.selectFromDropdown('genderFilter', 0)
|
||||
checkFilteredStudentCount(13)
|
||||
cy.selectFromDropdown('genderFilter', 1)
|
||||
checkFilteredStudentCount(14)
|
||||
cy.selectFromDropdown('genderFilter', 2)
|
||||
checkFilteredStudentCount(0)
|
||||
cy.selectFromDropdown('genderFilter', 3)
|
||||
checkFilteredStudentCount(0)
|
||||
|
||||
cy.cs('genderFilter-clear').click()
|
||||
cy.cs('genderFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Starting year filter works', () => {
|
||||
cy.cs('startYearAtUniFilter-header').click()
|
||||
cy.selectFromDropdown('startYearAtUniFilter', 0)
|
||||
checkFilteredStudentCount(1)
|
||||
cy.selectFromDropdown('startYearAtUniFilter', 7)
|
||||
checkFilteredStudentCount(21)
|
||||
|
||||
cy.cs('startYearAtUniFilter-clear').click()
|
||||
cy.cs('startYearAtUniFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Admission type filter works', () => {
|
||||
cy.cs('admissionTypeFilter-header').click()
|
||||
cy.selectFromDropdown('admissionTypeFilter', 0)
|
||||
checkFilteredStudentCount(18)
|
||||
cy.selectFromDropdown('admissionTypeFilter', 1)
|
||||
checkFilteredStudentCount(6)
|
||||
|
||||
cy.cs('admissionTypeFilter-clear').click()
|
||||
cy.cs('admissionTypeFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Courses filter works', () => {
|
||||
const courses = [
|
||||
{ code: 'MAT21012', name: 'Differentiaaliyhtälöt I' },
|
||||
{ code: 'MFK-M204', name: 'Matematiikkaa kaikkialla' },
|
||||
]
|
||||
|
||||
cy.cs('courseFilter-header').click()
|
||||
cy.selectFromAutocomplete('courseFilter', `${courses[0].code} - ${courses[0].name}`)
|
||||
checkFilteredStudentCount(9)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[0].code}`, 1)
|
||||
checkFilteredStudentCount(7)
|
||||
cy.selectFromAutocomplete('courseFilter', `${courses[1].code} - ${courses[1].name}`)
|
||||
checkFilteredStudentCount(5)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[1].code}`, 1)
|
||||
checkFilteredStudentCount(4)
|
||||
|
||||
cy.cs(`courseFilter-${courses[1].code}-clear`).click()
|
||||
checkFilteredStudentCount(7)
|
||||
|
||||
cy.cs('courseFilter-clear').click()
|
||||
cy.cs('courseFilter-header').click()
|
||||
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Filter combinations work', () => {
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
|
||||
cy.cs('graduatedFromProgrammeFilter-header').click()
|
||||
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="1"]').click()
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="1"]').should('be.checked')
|
||||
checkFilteredStudentCount(graduated)
|
||||
|
||||
cy.cs('ageFilter-header').click()
|
||||
cy.setRangeSelect('ageFilter', 23, 30, 12)
|
||||
|
||||
cy.cs('graduatedFromProgrammeFilter-clear').click()
|
||||
cy.cs('graduatedFromProgrammeFilter-header').click()
|
||||
cy.cs('ageFilter-clear').click()
|
||||
cy.cs('ageFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('"Reset All Filters" button works', () => {
|
||||
cy.cs('genderFilter-header').click()
|
||||
cy.selectFromDropdown('genderFilter', 0)
|
||||
checkFilteredStudentCount(13)
|
||||
cy.cs('startYearAtUniFilter-header').click()
|
||||
cy.selectFromDropdown('startYearAtUniFilter', 7)
|
||||
checkFilteredStudentCount(8)
|
||||
|
||||
cy.cs('reset-all-filters').click()
|
||||
cy.cs('genderFilter-header').click()
|
||||
cy.cs('startYearAtUniFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Population Statistics with Bachelor + Master', { testIsolation: false }, () => {
|
||||
const pathToMathBSc2017 =
|
||||
'/populations?years=2017&programme=KH50_001&showBachelorAndMaster=true&semesters=FALL&semesters=SPRING'
|
||||
const defaultAmountOfStudents = 47
|
||||
|
||||
before(() => cy.init(pathToMathBSc2017))
|
||||
|
||||
it('Graduation filter works', () => {
|
||||
cy.cs('graduatedFromProgrammeFilter-header').click()
|
||||
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="1"]').click()
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="1"]').should('be.checked')
|
||||
checkFilteredStudentCount(42)
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="2"]').click()
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="2"]').should('be.checked')
|
||||
checkFilteredStudentCount(18)
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="-1"]').click()
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="-1"]').should('be.checked')
|
||||
checkFilteredStudentCount(5)
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="-2"]').click()
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="-2"]').should('be.checked')
|
||||
checkFilteredStudentCount(29)
|
||||
|
||||
cy.cs('graduatedFromProgrammeFilter-clear').click()
|
||||
cy.cs('graduatedFromProgrammeFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Study right status filter works', () => {
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
cy.cs('studyRightStatusFilter-header').click()
|
||||
|
||||
// Bachelor
|
||||
cy.get('input[name="studyRightStatusFilter"][value="Active Bachelor study right"]').click()
|
||||
cy.get('input[name="studyRightStatusFilter"][value="Active Bachelor study right"]').should('be.checked')
|
||||
checkFilteredStudentCount(0)
|
||||
// Master (combined)
|
||||
cy.get('input[name="studyRightStatusFilter"][value="Active Master study right"]').click()
|
||||
cy.get('input[name="studyRightStatusFilter"][value="Active Master study right"]').should('be.checked')
|
||||
checkFilteredStudentCount(0)
|
||||
|
||||
// Non-Bachelor
|
||||
cy.get('input[name="studyRightStatusFilter"][value="Passive Bachelor study right"]').click()
|
||||
cy.get('input[name="studyRightStatusFilter"][value="Passive Bachelor study right"]').should('be.checked')
|
||||
checkFilteredStudentCount(5)
|
||||
// Non-Master (combined)
|
||||
cy.get('input[name="studyRightStatusFilter"][value="Passive Master study right"]').click()
|
||||
cy.get('input[name="studyRightStatusFilter"][value="Passive Master study right"]').should('be.checked')
|
||||
checkFilteredStudentCount(24)
|
||||
|
||||
cy.cs('studyRightStatusFilter-clear').click()
|
||||
cy.cs('studyRightStatusFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
})
|
||||
|
||||
// FIXME: Flaky
|
||||
describe.skip('Course Statistics', { testIsolation: false }, () => {
|
||||
const pathToLimits2021 =
|
||||
'/coursepopulation?coursecodes=%5B"MAT11003"%2C"57116"%2C"57016"%2C"AYMAT11003"%5D&from=72&separate=false&to=72&unifyCourses=unifyStats'
|
||||
const defaultAmountOfStudents = 48
|
||||
|
||||
before(() => cy.init(pathToLimits2021))
|
||||
beforeEach(() => setClockToMockedDate())
|
||||
|
||||
it('Grade filter works', () => {
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
cy.cs('gradeFilter-header').click()
|
||||
|
||||
cy.cs('gradeFilter-5').click()
|
||||
checkFilteredStudentCount(23)
|
||||
cy.cs('gradeFilter-3').click()
|
||||
checkFilteredStudentCount(29)
|
||||
cy.cs('gradeFilter-3').click()
|
||||
cy.cs('gradeFilter-5').click()
|
||||
|
||||
cy.cs('gradeFilter-clear').should('not.exist')
|
||||
|
||||
cy.cs('gradeFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Programme filter works and defaults to "Attainment"', () => {
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
cy.cs('programmeFilter-header').click()
|
||||
cy.cs('programmeFilter-mode-selector').children().eq(0).should('contain', 'Attainment')
|
||||
cy.selectFromDropdown('programmeFilter', 1, true)
|
||||
checkFilteredStudentCount(33)
|
||||
|
||||
cy.cs('programmeFilter-clear').click()
|
||||
cy.cs('programmeFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
cy.cs('ageFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
cy.setRangeSelect('ageFilter', 23, 30, 36)
|
||||
|
||||
cy.cs('ageFilter-clear').click()
|
||||
cy.cs('ageFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
cy.cs('genderFilter-header').click()
|
||||
cy.selectFromDropdown('genderFilter', 0)
|
||||
checkFilteredStudentCount(9)
|
||||
cy.selectFromDropdown('genderFilter', 1)
|
||||
checkFilteredStudentCount(39)
|
||||
cy.selectFromDropdown('genderFilter', 2)
|
||||
checkFilteredStudentCount(0)
|
||||
cy.selectFromDropdown('genderFilter', 3)
|
||||
checkFilteredStudentCount(0)
|
||||
|
||||
cy.cs('genderFilter-clear').click()
|
||||
cy.cs('genderFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Starting year filter works', () => {
|
||||
cy.cs('startYearAtUniFilter-header').click()
|
||||
cy.selectFromDropdown('startYearAtUniFilter', 0)
|
||||
checkFilteredStudentCount(1)
|
||||
cy.selectFromDropdown('startYearAtUniFilter', 10)
|
||||
checkFilteredStudentCount(24)
|
||||
|
||||
cy.cs('startYearAtUniFilter-clear').click()
|
||||
cy.cs('startYearAtUniFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Filter combinations work', () => {
|
||||
cy.cs('ageFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
cy.setRangeSelect('ageFilter', 23, 30, 36)
|
||||
|
||||
cy.cs('gradeFilter-header').click()
|
||||
cy.cs('gradeFilter-5').click()
|
||||
checkFilteredStudentCount(17)
|
||||
|
||||
cy.cs('gradeFilter-clear').click()
|
||||
cy.cs('gradeFilter-header').click()
|
||||
cy.cs('ageFilter-clear').click()
|
||||
cy.cs('ageFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom Population Statistics', { testIsolation: false }, () => {
|
||||
let defaultAmountOfStudents
|
||||
|
||||
before(() => {
|
||||
cy.init('/custompopulation')
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1, studentNumbersForCSStudentsSet2 }) => {
|
||||
const students = [...studentNumbersForCSStudentsSet1, ...studentNumbersForCSStudentsSet2]
|
||||
defaultAmountOfStudents = students.length
|
||||
cy.cs('student-number-input').click()
|
||||
cy.cs('student-number-input').type(students.join('{enter}'))
|
||||
cy.cs('search-button').click()
|
||||
})
|
||||
})
|
||||
beforeEach(() => setClockToMockedDate())
|
||||
|
||||
it('Age filter works', () => {
|
||||
cy.cs('ageFilter-header').click()
|
||||
cy.setRangeSelect('ageFilter', 24, 28, 5)
|
||||
|
||||
cy.cs('ageFilter-clear').click()
|
||||
cy.cs('ageFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Programme filter works and defaults to "Active Study Right"', () => {
|
||||
cy.cs('programmeFilter-header').click()
|
||||
cy.cs('programmeFilter-mode-selector').children().eq(0).should('contain', 'Active Study Right')
|
||||
cy.selectFromDropdown('programmeFilter', 1, true)
|
||||
checkFilteredStudentCount(3)
|
||||
|
||||
cy.cs('programmeFilter-clear').click()
|
||||
cy.cs('programmeFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
cy.cs('genderFilter-header').click()
|
||||
cy.selectFromDropdown('genderFilter', 0)
|
||||
checkFilteredStudentCount(5)
|
||||
cy.selectFromDropdown('genderFilter', 1)
|
||||
checkFilteredStudentCount(3)
|
||||
cy.selectFromDropdown('genderFilter', 2)
|
||||
checkFilteredStudentCount(0)
|
||||
cy.selectFromDropdown('genderFilter', 3)
|
||||
checkFilteredStudentCount(0)
|
||||
|
||||
cy.cs('genderFilter-clear').click()
|
||||
cy.cs('genderFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Starting year filter works', () => {
|
||||
cy.cs('startYearAtUniFilter-header').click()
|
||||
cy.selectFromDropdown('startYearAtUniFilter', 0)
|
||||
checkFilteredStudentCount(2)
|
||||
|
||||
cy.cs('startYearAtUniFilter-clear').click()
|
||||
cy.cs('startYearAtUniFilter-header').click()
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Courses filter works', () => {
|
||||
const courses = [
|
||||
{ code: 'MAT11005', name: 'Integraalilaskenta' },
|
||||
{ code: 'MAT21007', name: 'Mitta ja integraali' },
|
||||
]
|
||||
|
||||
cy.cs('courseFilter-header').click()
|
||||
cy.selectFromAutocomplete('courseFilter', `${courses[0].code} - ${courses[0].name}`)
|
||||
checkFilteredStudentCount(7)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[0].code}`, 1)
|
||||
checkFilteredStudentCount(6)
|
||||
cy.selectFromAutocomplete('courseFilter', `${courses[1].code} - ${courses[1].name}`)
|
||||
checkFilteredStudentCount(5)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[1].code}`, 3)
|
||||
checkFilteredStudentCount(2)
|
||||
|
||||
cy.cs(`courseFilter-${courses[1].code}-clear`).click()
|
||||
checkFilteredStudentCount(6)
|
||||
|
||||
cy.cs('courseFilter-clear').click()
|
||||
cy.cs('courseFilter-header').click()
|
||||
|
||||
checkFilteredStudentCount(defaultAmountOfStudents)
|
||||
})
|
||||
})
|
||||
@@ -1,89 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const chooseSemester = (semester, fromOrTo) => {
|
||||
cy.cs(`semester-${fromOrTo}`).click()
|
||||
cy.cs(`select-opt-${semester}`).click()
|
||||
cy.cs(`select-opt-${semester}`).should('not.have.class', 'visible')
|
||||
}
|
||||
|
||||
const checkNumbers = (numbers, numberOfColumns, mode) => {
|
||||
const offset = 1 // skip first col with codes / names
|
||||
cy.cs(`ooditable-${mode}`).find('table').find('tbody').as('tbody')
|
||||
cy.get('@tbody').find('tr').first().as('totalRow')
|
||||
cy.get('@totalRow').find('td').should('have.length', numberOfColumns)
|
||||
cy.get('@totalRow').find('td').eq(0).should('contain', 'All courses total')
|
||||
|
||||
numbers.forEach((number, index) => {
|
||||
cy.get('@totalRow')
|
||||
.find('td')
|
||||
.eq(index + offset)
|
||||
.should('contain', number)
|
||||
})
|
||||
}
|
||||
|
||||
describe('When language center is opened', () => {
|
||||
describe('as an admin user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/languagecenterview', 'admin')
|
||||
cy.cs('completions-button').click()
|
||||
})
|
||||
|
||||
describe('Faculties tab', () => {
|
||||
beforeEach(() => {
|
||||
chooseSemester('Syksy 2017', 'from')
|
||||
chooseSemester('Kevät 2024', 'to')
|
||||
})
|
||||
|
||||
it('Initial view is correct', () => {
|
||||
cy.contains('All courses total')
|
||||
cy.contains('Academic and Professional')
|
||||
cy.contains('AYKK-RUKIRJ')
|
||||
})
|
||||
|
||||
it('Faculties tab shows numbers', () => {
|
||||
cy.get('table > tbody > tr:first').within(() => {
|
||||
cy.get('td').should('have.length', 15)
|
||||
cy.get('td').eq(0).contains('All courses total')
|
||||
const numbers = [2076, 36, 9, 33, 756, 15, 40, 6, 48, 1, 24, 1059, 49, 0]
|
||||
numbers.forEach((number, index) => {
|
||||
cy.get('td')
|
||||
.eq(index + 1)
|
||||
.contains(number)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Faculties tab "exceeding" button works', () => {
|
||||
cy.cs('difference-button').click()
|
||||
checkNumbers([65, 2, 0, 3, 13, 0, 1, 1, 2, 0, 1, 30, 11, 1], 15, 'faculties')
|
||||
})
|
||||
|
||||
it('Faculties tab semester selector changes numbers', () => {
|
||||
chooseSemester('Syksy 2020', 'from')
|
||||
checkNumbers([1184, 28, 6, 27, 496, 4, 10, 2, 33, 0, 9, 535, 34, 0], 15, 'faculties')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Semester tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.contains('By semesters').click()
|
||||
chooseSemester('Syksy 2017', 'from')
|
||||
chooseSemester('Kevät 2024', 'to')
|
||||
})
|
||||
|
||||
it('Semester tab shows numbers', () => {
|
||||
checkNumbers([2076, 69, 26, 298, 58, 343, 98, 438, 138, 310, 90, 123, 74, 10, 1], 16, 'semesters')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a user with no rights', () => {
|
||||
it('"Access denied" is shown', () => {
|
||||
cy.init('/languagecenterview', 'norights')
|
||||
cy.contains('Access denied')
|
||||
cy.contains("You don't currently have permission to view this page.")
|
||||
cy.contains('Special populations').click()
|
||||
cy.contains('Language center view').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,126 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const deleteAllSearches = () => {
|
||||
cy.cs('open-uni-search-button').click()
|
||||
cy.contains('Saved populations').parent().parent().parent().parent().get('.search').as('search')
|
||||
|
||||
cy.get('@search').then($el => {
|
||||
const searchItems = $el.find('div[role=option] > span[class=text]')
|
||||
for (let i = searchItems.length - 1; 0 <= i; i--) {
|
||||
if (searchItems[i].textContent.includes('TEST-')) {
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type(`${searchItems[i].textContent}{enter}`)
|
||||
cy.get('button').contains('Delete').click()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Skipping the whole test suite as it is unclear whether the feature has any use or will be
|
||||
// removed alltogether.
|
||||
describe.skip('Open uni population tests', () => {
|
||||
describe('Open uni form can be used', () => {
|
||||
// When openUniUser is created, change it here
|
||||
beforeEach(() => {
|
||||
cy.init('/openunipopulation', 'admin')
|
||||
})
|
||||
after(() => {
|
||||
cy.init('/openunipopulation', 'admin')
|
||||
cy.contains('Open uni student population')
|
||||
deleteAllSearches()
|
||||
})
|
||||
|
||||
// TODO: Rewrite the whole component
|
||||
it.skip('Finds a proper population', () => {
|
||||
cy.fixture('openUniPopulation').then(({ courseCodesSet1 }) => {
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').as('textarea')
|
||||
cy.get('@textarea').type(courseCodesSet1.join('\n'))
|
||||
cy.cs('begin-of-search').click()
|
||||
cy.cs('begin-of-search').clear()
|
||||
cy.cs('begin-of-search').type('01.01.2015')
|
||||
cy.cs('end-of-search').click()
|
||||
cy.cs('end-of-search').clear()
|
||||
cy.cs('end-of-search').type('01.01.2020')
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.contains('Beginning of the search for all fields:')
|
||||
cy.contains('01.01.2015')
|
||||
cy.contains('End of the search for enrollments:')
|
||||
cy.contains('01.01.2020')
|
||||
cy.location('pathname').should('eq', '/openunipopulation')
|
||||
cy.location('search').then(search => {
|
||||
const params = new URLSearchParams(search)
|
||||
|
||||
const courseCodes = params.getAll('courseCode')
|
||||
expect(courseCodes).to.include.members(['TKT10001', 'TKT10002'])
|
||||
|
||||
expect(params.get('startdate')).to.eq('01-01-2015')
|
||||
expect(params.get('enddate')).to.eq('01-01-2020')
|
||||
})
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
|
||||
})
|
||||
})
|
||||
|
||||
it('Textarea can handle different kinds of input', () => {
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.contains('Insert course code(s)')
|
||||
.siblings()
|
||||
.get('textarea')
|
||||
.type('tkt10001 tkt10002,tkt10003\n TKT10004,,TKt10005\nTKT-1234')
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10001')
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10002')
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10003')
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10004')
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10005')
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('not.contain', 'TKT-1234')
|
||||
})
|
||||
|
||||
it('Saves a custom population search', () => {
|
||||
cy.fixture('openUniPopulation').then(({ courseCodesSet1 }) => {
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.get('[data-cy="search-name"]').type('TEST-Avoimen-populaatio')
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet1.join('\n'))
|
||||
cy.get('[data-cy="save-search"]').click()
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type('TEST-Avoimen-populaatio{enter}')
|
||||
cy.contains('TEST-Avoimen-populaatio')
|
||||
cy.get('button').contains('Search population').as('btn')
|
||||
cy.get('@btn').click()
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
|
||||
})
|
||||
})
|
||||
|
||||
it('Updates a custom population search', () => {
|
||||
cy.fixture('openUniPopulation').then(({ courseCodesSet1, courseCodesSet2 }) => {
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.get('[data-cy="search-name"]').type('TEST-Avoimen-populaatio')
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet1.join('\n'))
|
||||
cy.get('[data-cy="save-search"]').click()
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
|
||||
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type('TEST-Avoimen-populaatio{enter}')
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').should('contain', courseCodesSet1.join(', '))
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').type('\n')
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet2.join('\n'))
|
||||
cy.get('[data-cy="save-search"]').click()
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[0])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[1])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[2])
|
||||
})
|
||||
})
|
||||
|
||||
it('View does not crash with non-existing courses', () => {
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').type('cypress-fun')
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('not.contain', 'cypress-fun')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,443 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// Now "Class statistics" in UI
|
||||
|
||||
const selectStudyProgramme = programme => {
|
||||
cy.cs('population-programme-selector').within(() =>
|
||||
cy.get('input').should('have.attr', 'placeholder', 'Select degree programme')
|
||||
)
|
||||
cy.cs('population-programme-selector-parent').click()
|
||||
cy.cs('population-programme-selector-parent').within(() => cy.contains(programme).click())
|
||||
}
|
||||
|
||||
const selectStudyTrack = studyTrack => {
|
||||
cy.cs('population-studytrack-selector').within(() =>
|
||||
cy.get('input').should('have.attr', 'placeholder', 'Select study track')
|
||||
)
|
||||
cy.cs('population-studytrack-selector-parent').click()
|
||||
cy.cs('population-studytrack-selector-parent').within(() => cy.contains(studyTrack).click())
|
||||
}
|
||||
|
||||
describe('Population statistics tests', () => {
|
||||
const getPath = programme => {
|
||||
return `/populations?months=49&semesters=FALL&semesters=SPRING&programme=${programme}&years=2020`
|
||||
}
|
||||
const pathToMathBSc2020 = getPath('KH50_001')
|
||||
const pathToMathMSc2020 = getPath('MH50_001')
|
||||
|
||||
describe('When using basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/populations')
|
||||
cy.intercept('/api/v3/populationstatistics/studyprogrammes').as('studyprogrammes')
|
||||
cy.intercept('/api/v2/studyprogrammes/**/studytracks').as('studytracks')
|
||||
})
|
||||
|
||||
describe('Population search', () => {
|
||||
it('Info box works', () => {
|
||||
cy.cs('PopulationSearch-info-box-content').should('not.exist')
|
||||
cy.cs('PopulationSearch-info-box-button').trigger('mouseover')
|
||||
cy.cs('PopulationSearch-info-box-content').should('be.visible')
|
||||
cy.cs('PopulationSearch-info-box-content').contains('Tässä osiossa voi tarkastella')
|
||||
cy.cs('PopulationSearch-info-box-button').trigger('mouseout')
|
||||
cy.cs('PopulationSearch-info-box-content').should('not.exist')
|
||||
})
|
||||
|
||||
// TODO: Delete MUI
|
||||
it.skip('Form is usable', { retries: { runMode: 2 } }, () => {
|
||||
cy.contains('Search for class')
|
||||
cy.contains('See class').should('be.disabled')
|
||||
|
||||
cy.cs('population-year-selector').as('yearSelect')
|
||||
cy.cs('population-year-decrement').as('yearDecrement')
|
||||
cy.cs('population-year-increment').as('yearIncrement')
|
||||
|
||||
cy.get('@yearSelect').click()
|
||||
cy.contains('2018 - 2019').click()
|
||||
cy.get('@yearSelect').within(() => cy.contains('2018 - 2019'))
|
||||
cy.get('@yearDecrement').click()
|
||||
cy.get('@yearSelect').within(() => cy.contains('2017 - 2018'))
|
||||
cy.get('@yearIncrement').click()
|
||||
cy.get('@yearSelect').within(() => cy.contains('2018 - 2019'))
|
||||
|
||||
cy.cs('population-studytrack-selector').within(() => {
|
||||
cy.get('input').should('have.attr', 'placeholder', 'No study tracks available')
|
||||
})
|
||||
selectStudyProgramme('Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
cy.contains('See class').should('be.enabled')
|
||||
selectStudyTrack('Matematiikka ja soveltava matematiikka')
|
||||
cy.contains('See class').should('be.enabled')
|
||||
})
|
||||
|
||||
describe('Correct population is shown for programme', () => {
|
||||
// TODO: Delete MUI
|
||||
it.skip('without study tracks', () => {
|
||||
cy.wait('@studyprogrammes').its('response.statusCode').should('be.oneOf', [200, 304])
|
||||
selectStudyProgramme('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.wait('@studytracks').its('response.statusCode').should('be.oneOf', [200, 304])
|
||||
cy.contains('See class').click()
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2017 - 2018')
|
||||
cy.contains('Class size 47 students')
|
||||
})
|
||||
|
||||
// TODO: Delete MUI
|
||||
it.skip('with study tracks', () => {
|
||||
selectStudyProgramme('Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
cy.wait('@studytracks').its('response.statusCode').should('be.oneOf', [200, 304])
|
||||
selectStudyTrack('Matematiikka ja soveltava matematiikka')
|
||||
cy.contains('See class').click()
|
||||
cy.contains('Matematiikan ja tilastotieteen maisteriohjelma 2017 - 2018')
|
||||
cy.contains('studytrack MAST-MSM')
|
||||
cy.contains('Class size 1 students')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Population statistics is usable on general level', () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.cs('filtered-students')
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('Class size 30 students')
|
||||
cy.contains('Excludes exchange students')
|
||||
cy.contains('Excludes students with non-degree study right')
|
||||
cy.contains('Excludes students who have transferred out of this programme')
|
||||
cy.cs('filtered-students')
|
||||
})
|
||||
|
||||
// This test sometimes fails on headless mode. It seems that the click on the
|
||||
// 'Fetch class with new settings' button doesn't always trigger history.push()
|
||||
// so the page doesn't reload. This is why waiting also doesn't help.
|
||||
it('Advanced settings work', { retries: 2 }, () => {
|
||||
cy.visit(pathToMathMSc2020)
|
||||
cy.get('[data-cy=advanced-toggle]').click()
|
||||
// only spring
|
||||
cy.cs('toggle-fall').click()
|
||||
cy.contains('Fetch class').click()
|
||||
cy.contains('Credit accumulation (for 17 students)')
|
||||
cy.url().should('not.include', 'semesters=FALL')
|
||||
// only fall
|
||||
cy.get('[data-cy=advanced-toggle]').click()
|
||||
cy.cs('toggle-fall').click()
|
||||
cy.cs('toggle-spring').click()
|
||||
cy.contains('Fetch class').click()
|
||||
cy.contains('Credit accumulation (for 9 students)')
|
||||
cy.url().should('not.include', 'semesters=SPRING')
|
||||
// spring + fall
|
||||
cy.get('[data-cy=advanced-toggle]').click()
|
||||
cy.cs('toggle-spring').click()
|
||||
cy.contains('Fetch class').click()
|
||||
cy.contains('Credit accumulation (for 26 students)')
|
||||
cy.url().should('include', 'semesters=FALL&semesters=SPRING')
|
||||
})
|
||||
|
||||
describe('Credit statistics', () => {
|
||||
it("'Credits gained' tab shows correct statistics for all students of the class and also students grouped by admission type", () => {
|
||||
cy.clock(new Date('2024-08-30').getTime(), ['Date'])
|
||||
cy.visit(pathToMathBSc2020)
|
||||
const totalStudents = 27
|
||||
cy.contains('Credit statistics')
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains('Credit statistics').click()
|
||||
})
|
||||
cy.contains('Credits gained').click()
|
||||
const limits = [1, 45, 90, 135, 180, null]
|
||||
const ranges = limits.map((limit, i) => (i === 0 ? [null, 0] : [limits[i - 1], limit])).reverse()
|
||||
|
||||
const getTableData = (selector, numbersOfStudents) => {
|
||||
const studentsInCategory = numbersOfStudents.reduce((acc, val) => acc + val, 0)
|
||||
return ranges.map((range, index) => ({
|
||||
selector: `[data-cy='credits-gained-table-${selector}']`,
|
||||
start: range[0],
|
||||
end: range[1],
|
||||
students: numbersOfStudents[index],
|
||||
percentage: `${((numbersOfStudents[index] / studentsInCategory) * 100).toFixed(1)}%`,
|
||||
}))
|
||||
}
|
||||
|
||||
cy.cs('credits-gained-table-All students of the class').within(() => {
|
||||
cy.get('th').eq(1).contains('Credits gained between 01.08.2020 and 30.08.2024')
|
||||
cy.get('th').eq(1).contains('(49 months)')
|
||||
cy.get('th').eq(2).contains('Number of students')
|
||||
cy.get('th').eq(2).contains(`(n = ${totalStudents})`)
|
||||
cy.get('th').eq(3).contains('Percentage of population')
|
||||
})
|
||||
|
||||
for (const category of [
|
||||
getTableData('All students of the class', [9, 5, 7, 4, 2, 0]),
|
||||
getTableData('Avoin väylä', [0, 1, 3, 2, 0, 0]),
|
||||
getTableData('Muu', [0, 1, 1, 1, 0, 0]),
|
||||
getTableData('Todistusvalinta', [9, 3, 3, 1, 2, 0]),
|
||||
]) {
|
||||
category.forEach(({ selector, start, end, students, percentage }, index) => {
|
||||
let value
|
||||
if (start === null) {
|
||||
value = '0'
|
||||
} else if (end === null) {
|
||||
value = `${start} ≤ credits`
|
||||
} else {
|
||||
value = `${start} ≤ credits < ${end}`
|
||||
}
|
||||
cy.get(`${selector} [data-cy='credits-gained-table-body']`).within(() => {
|
||||
cy.get('tr')
|
||||
.eq(index)
|
||||
.within(() => {
|
||||
cy.get('td').eq(1).contains(value)
|
||||
cy.get('td').eq(2).contains(students)
|
||||
cy.get('td').eq(3).contains(percentage)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
cy.get(
|
||||
"[data-cy='credits-gained-table-All students of the class'] [data-cy='credits-gained-table-body'] td:nth-child(3)"
|
||||
).then(tds => {
|
||||
const sum = [...tds].reduce((acc, td) => acc + parseInt(td.innerText, 10), 0)
|
||||
expect(sum).to.equal(totalStudents)
|
||||
})
|
||||
})
|
||||
|
||||
it("'Statistics' tab shows correct statistics for all students of the class and also students grouped by admission type", () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.contains('Credit statistics')
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains('Credit statistics').click()
|
||||
})
|
||||
cy.cs('credit-statistics-tab').click()
|
||||
const rows = ['Total credits', 'Average', 'Median', 'Standard deviation', 'Minimum', 'Maximum']
|
||||
const categories = [
|
||||
{
|
||||
selector: 'All students of the population',
|
||||
data: ['6136.50', '227.28', '197.00', '117.94', '60', '533'],
|
||||
size: 27,
|
||||
},
|
||||
{
|
||||
selector: 'Muu',
|
||||
data: ['580.00', '193.33', '192.00', '36.75', '149', '239'],
|
||||
size: 3,
|
||||
},
|
||||
{
|
||||
selector: 'Todistusvalinta',
|
||||
data: ['3657.50', '203.19', '193.50', '98.35', '60', '460'],
|
||||
size: 18,
|
||||
},
|
||||
{
|
||||
selector: 'Avoin väylä',
|
||||
data: ['1899.00', '316.50', '257.00', '150.43', '150', '533'],
|
||||
size: 6,
|
||||
},
|
||||
]
|
||||
|
||||
for (const { selector, data, size } of categories) {
|
||||
cy.get(`[data-cy='statistics-table-${selector}']`).within(() => {
|
||||
cy.contains('h5', selector)
|
||||
cy.contains("[data-cy='credit-stats-population-size']", `n = ${size}`)
|
||||
cy.get('table tbody').within(() => {
|
||||
rows.forEach((text, index) => {
|
||||
cy.get('tr')
|
||||
.eq(index)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains(text)
|
||||
cy.get('td').eq(1).contains(data[index])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Courses of class', () => {
|
||||
it('Is displayed and link to individual course stats page works', () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.contains('Courses of class').click()
|
||||
cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
|
||||
cy.cs('toggle-group-module-MAT110').click()
|
||||
cy.contains('td', 'MAT11001').siblings().find('[data-testid="NorthEastIcon"]').click()
|
||||
cy.wait('@coursePage')
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('MAT11001 • Johdatus yliopistomatematiikkaan')
|
||||
cy.contains('AYMAT11001 • Avoin yo: Johdatus yliopistomatematiikkaan')
|
||||
cy.contains('57033 • Johdatus yliopistomatematiikkaan')
|
||||
cy.contains('A57033 • Avoin yo: Johdatus yliopistomatematiikkaan')
|
||||
})
|
||||
|
||||
it('Curriculum selection works', () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.contains('Courses of class').click()
|
||||
cy.get('[data-cy=curriculum-picker]').contains('2020–2023')
|
||||
cy.get('[data-cy=toggle-group-module-MAT-tyo]')
|
||||
cy.get('[data-cy=curriculum-picker]').click()
|
||||
cy.contains('2023–2026').click()
|
||||
cy.get('[data-cy=toggle-group-module-MAT-tyo]').should('not.exist')
|
||||
})
|
||||
|
||||
it('Courses data is changed when curriculum is changed', () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.contains('Courses of class').click()
|
||||
|
||||
cy.cs('curriculum-picker').scrollIntoView()
|
||||
cy.cs('curriculum-picker').should('be.visible')
|
||||
cy.cs('curriculum-picker').click()
|
||||
cy.contains('2020–2023').click({ force: true })
|
||||
cy.contains('Students (27)')
|
||||
cy.cs('toggle-group-module-DIGI-k').should('exist')
|
||||
cy.cs('toggle-group-module-DIGI-k').scrollIntoView()
|
||||
cy.cs('toggle-group-module-DIGI-k').should('be.visible')
|
||||
cy.cs('toggle-group-module-DIGI-k').click({ force: true })
|
||||
cy.contains('DIGI-100').should('exist')
|
||||
|
||||
cy.cs('curriculum-picker').scrollIntoView()
|
||||
cy.cs('toggle-group-module-DIGI-k').should('be.visible')
|
||||
cy.cs('curriculum-picker').click()
|
||||
cy.contains('2023–2026').click({ force: true })
|
||||
cy.cs('toggle-group-module-DIGI-k').should('exist')
|
||||
cy.contains('DIGI-100').should('not.exist')
|
||||
})
|
||||
|
||||
it('Courses data is changed when filtered students change', () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
|
||||
cy.cs('graduatedFromProgrammeFilter-filter-card').within(() => {
|
||||
cy.cs('graduatedFromProgrammeFilter-header').click()
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="1"]').click()
|
||||
cy.get('input[name="graduatedFromProgrammeFilter"][value="1"]').should('be.checked')
|
||||
})
|
||||
|
||||
cy.contains('Students (16)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Students', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.contains('Students (27)')
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains('Students (27)').click()
|
||||
})
|
||||
})
|
||||
|
||||
it("'General tab' is usable", () => {
|
||||
cy.cs('ooditable-general').within(() => {
|
||||
cy.contains('522142')
|
||||
cy.contains('Tilastotiede')
|
||||
cy.contains('Matematiikka')
|
||||
cy.contains('Taloustieteen maisteriohjelma')
|
||||
cy.contains('Todistusvalinta')
|
||||
cy.contains('Saksa')
|
||||
cy.contains('Female')
|
||||
})
|
||||
})
|
||||
|
||||
it("'Courses tab' is usable", () => {
|
||||
cy.cs('student-table-tabs').within(() => {
|
||||
cy.contains('Courses').click()
|
||||
})
|
||||
cy.cs('ooditable-courses').within(() => {
|
||||
cy.contains('MAT12001')
|
||||
cy.contains('MAT21001')
|
||||
})
|
||||
})
|
||||
|
||||
it("'Modules tab' Displays correct modules based on the selected programme", () => {
|
||||
cy.cs('student-table-tabs').contains('Modules').click()
|
||||
cy.cs('ooditable-modules').within(() => {
|
||||
cy.contains('MAT011')
|
||||
cy.contains('MAT110')
|
||||
})
|
||||
cy.contains('Courses of class').click()
|
||||
cy.cs('curriculum-picker').click()
|
||||
cy.contains('2023–2026').click()
|
||||
cy.contains('Courses of class').click()
|
||||
cy.cs('ooditable-modules').within(() => {
|
||||
cy.contains('MAT011').should('not.exist')
|
||||
cy.contains('MAT110')
|
||||
})
|
||||
})
|
||||
|
||||
it("Empty 'tags' tab has a link to the page where tags can be created", () => {
|
||||
cy.cs('student-table-tabs').within(() => {
|
||||
cy.contains('Tags').click()
|
||||
})
|
||||
cy.contains('No tags defined. You can define them here.').find('a').click()
|
||||
cy.url().should('include', '/study-programme/KH50_001?tab=4')
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('Create new tag')
|
||||
cy.cs('create-button').should('be.disabled')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('When using admin', () => {
|
||||
it('Student list checking works as intended', () => {
|
||||
cy.init(pathToMathBSc2020, 'admin')
|
||||
const existing = '433237'
|
||||
const nonExisting = '550004'
|
||||
cy.contains('Students (27)')
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains('Students (27)').click()
|
||||
})
|
||||
cy.contains(existing)
|
||||
cy.contains(nonExisting).should('not.exist')
|
||||
cy.contains('button', 'Check student numbers').click()
|
||||
cy.contains('Check for student numbers')
|
||||
cy.cs('check-student-numbers').type(`${existing}{enter}${nonExisting}`)
|
||||
cy.contains('button', 'Check students').click()
|
||||
cy.get('#checkstudentsresults').within(() => {
|
||||
cy.cs('found-title').click()
|
||||
cy.cs('found-data').contains(existing)
|
||||
cy.cs('not-found-title').click()
|
||||
cy.cs('not-found-data').contains(nonExisting)
|
||||
cy.cs('not-searched-title').click()
|
||||
cy.cs('not-searched-data').contains('457144')
|
||||
})
|
||||
cy.contains('button', 'Close').click()
|
||||
cy.contains('Student numbers in list and in Sisu').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('When using IAM user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init(pathToMathBSc2020, 'onlyiamrights')
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
|
||||
cy.contains('Class size 30 students')
|
||||
})
|
||||
|
||||
it('Population statistics is visible', () => {
|
||||
cy.cs('PopulationQueryCard').within(() => {
|
||||
cy.contains('Excludes exchange students')
|
||||
cy.contains('Excludes students with non-degree study right')
|
||||
cy.contains('Excludes students who have transferred out of this programme')
|
||||
})
|
||||
})
|
||||
|
||||
it('Only correct panels are visible', () => {
|
||||
cy.cs('panelview-parent').within(() => {
|
||||
cy.cs('Credit accumulation (for 27 students)')
|
||||
cy.cs('Credit statistics')
|
||||
cy.cs('Age distribution')
|
||||
cy.cs('Courses of class')
|
||||
})
|
||||
})
|
||||
|
||||
it('Ages cannot be ungrouped', () => {
|
||||
cy.contains('Age distribution')
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains('Age distribution').click()
|
||||
})
|
||||
cy.contains('ui.checked.toggle.checkbox', 'Group ages').should('not.exist')
|
||||
})
|
||||
|
||||
it('Age filter is not visible', () => {
|
||||
cy.get("[data-cy='filtered-students']")
|
||||
cy.get("[data-cy='Age-filter-card']").should('not.exist')
|
||||
})
|
||||
|
||||
it('Students tab is not available', () => {
|
||||
cy.contains('Students (27)').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,305 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const student = {
|
||||
firstNames: 'Varpu Roope',
|
||||
lastName: 'Mårtensson',
|
||||
studentNumber: '550003',
|
||||
sisPersonId: 'hy-hlo-115926826',
|
||||
email: 'sisutestidata134902@testisisudata.fi',
|
||||
}
|
||||
|
||||
const typeToSearch = text => {
|
||||
cy.cs('student-search').type(text)
|
||||
}
|
||||
|
||||
const typeStudentNumberAndClick = studentNumber => {
|
||||
typeToSearch(studentNumber)
|
||||
cy.contains('td', studentNumber).click()
|
||||
}
|
||||
|
||||
describe('Students tests', () => {
|
||||
describe('When using basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('')
|
||||
cy.contains('Students').click()
|
||||
cy.url().should('include', '/students')
|
||||
cy.contains('Show student names')
|
||||
})
|
||||
|
||||
// FIXME: Flaky
|
||||
it('Students search form is usable', () => {
|
||||
typeToSearch(student.lastName)
|
||||
cy.contains('Student number')
|
||||
cy.contains('Last name').should('not.exist')
|
||||
cy.contains('First names').should('not.exist')
|
||||
cy.contains('Started')
|
||||
cy.contains('Credits')
|
||||
cy.contains('Active study rights')
|
||||
cy.contains(student.firstNames).should('not.exist')
|
||||
cy.contains(student.lastName).should('not.exist')
|
||||
|
||||
cy.cs('toggleStudentNames').click()
|
||||
cy.contains(student.firstNames)
|
||||
cy.contains('Student number')
|
||||
cy.contains('Last name')
|
||||
cy.contains('First names')
|
||||
cy.contains('Started')
|
||||
cy.contains('Credits')
|
||||
cy.contains('Active study rights')
|
||||
cy.contains(student.firstNames)
|
||||
cy.contains(student.lastName)
|
||||
|
||||
cy.cs('toggleStudentNames').click()
|
||||
cy.contains('Student number')
|
||||
cy.contains('Last name').should('not.exist')
|
||||
cy.contains('First names').should('not.exist')
|
||||
cy.contains('Started')
|
||||
cy.contains('Credits')
|
||||
cy.contains('Active study rights')
|
||||
cy.contains(student.firstNames).should('not.exist')
|
||||
cy.contains(student.lastName).should('not.exist')
|
||||
})
|
||||
|
||||
it('Search term must be at least 4 characters long', () => {
|
||||
typeToSearch(student.lastName.slice(0, 3))
|
||||
cy.contains('Search term is not accurate enough')
|
||||
typeToSearch(student.lastName.slice(3))
|
||||
cy.get('table tbody tr').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('Can search with student number too', () => {
|
||||
typeToSearch(student.studentNumber)
|
||||
cy.contains(student.studentNumber)
|
||||
})
|
||||
|
||||
it('Can get student specific page by clicking student', () => {
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma (01.08.2020–31.07.2027)')
|
||||
cy.contains(student.lastName).should('not.exist')
|
||||
cy.contains(student.firstNames).should('not.exist')
|
||||
cy.contains(student.email).should('not.exist')
|
||||
|
||||
cy.cs('toggleStudentNames').click()
|
||||
cy.contains(student.lastName)
|
||||
cy.contains(student.firstNames)
|
||||
cy.contains(student.email)
|
||||
|
||||
cy.cs('toggleStudentNames').click()
|
||||
cy.contains(student.lastName).should('not.exist')
|
||||
cy.contains(student.firstNames).should('not.exist')
|
||||
cy.contains(student.email).should('not.exist')
|
||||
})
|
||||
|
||||
it("'Update student' button is not shown", () => {
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.cs('student-info-card').within(() => {
|
||||
cy.contains('button', 'Update student').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('Can get back to search menu', () => {
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.go('back')
|
||||
cy.contains('Student number').should('not.exist')
|
||||
cy.contains('Credits').should('not.exist')
|
||||
})
|
||||
|
||||
it('Can jump to course', () => {
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.contains('Tilastollinen päättely I (MAT12004)')
|
||||
.parent()
|
||||
.siblings()
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.get('a').click()
|
||||
})
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('MAT12004 • Tilastollinen päättely I')
|
||||
cy.contains('AYMAT12004 • Avoin yo: Tilastollinen päättely I')
|
||||
cy.contains('57046 • Johdatus tilastolliseen päättelyyn')
|
||||
})
|
||||
|
||||
it('Has correct Sisu link', () => {
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.cs('sisu-link')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', `https://sisu.helsinki.fi/tutor/role/staff/student/${student.sisPersonId}/basic/basic-info`)
|
||||
})
|
||||
|
||||
it('Semester enrollments can be toggled', () => {
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
const programmes = [
|
||||
'Oikeustieteen tohtoriohjelma',
|
||||
'Matemaattisten tieteiden kandiohjelma',
|
||||
'Oikeustieteen maisterin koulutusohjelmaOikeusnotaarin koulutusohjelma',
|
||||
]
|
||||
cy.cs('student-info-card').within(() => {
|
||||
cy.contains('Enrollments').click()
|
||||
cy.get('table tbody tr')
|
||||
.should('have.length', 3)
|
||||
.each(($tr, index) => {
|
||||
cy.wrap($tr).within(() => {
|
||||
cy.get('td').eq(0).should('have.text', programmes[index])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("Searching with bad name doesn't yield results", () => {
|
||||
typeToSearch('SWAG LITTINEN')
|
||||
cy.contains('Student number').should('not.exist')
|
||||
})
|
||||
|
||||
it("Searching with bad student number doesn't yield results", () => {
|
||||
typeToSearch('01114')
|
||||
cy.contains('Student number').should('not.exist')
|
||||
})
|
||||
|
||||
it('Can jump to population page', () => {
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.cs('study-rights-section').within(() => {
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
.parent()
|
||||
.within(() => cy.get('a').click())
|
||||
})
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
|
||||
cy.contains('Class size 30 students')
|
||||
})
|
||||
|
||||
it('Grade graph works in all three different modes', () => {
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.contains('button', 'Grade graph').click()
|
||||
cy.contains('button', 'Show group mean').click()
|
||||
cy.cs('group-size-input').within(() => {
|
||||
cy.contains('label', 'Group size')
|
||||
cy.get('input').should('have.value', '5')
|
||||
})
|
||||
cy.contains('.highcharts-container text', "Nov '22")
|
||||
cy.contains('.highcharts-container text', "Jul '22").should('not.exist')
|
||||
cy.cs('group-size-input').within(() => {
|
||||
cy.contains('label', 'Group size')
|
||||
cy.get('input').clear()
|
||||
cy.get('input').type('10')
|
||||
cy.get('input').should('have.value', '10')
|
||||
})
|
||||
cy.contains('.highcharts-container text', "Jul '22")
|
||||
cy.contains('.highcharts-container text', "Nov '22").should('not.exist')
|
||||
cy.contains('button', 'Show semester mean').click()
|
||||
cy.contains('.highcharts-container text', "Jan '22")
|
||||
})
|
||||
|
||||
describe('Bachelor Honours section', () => {
|
||||
it("Shows 'Qualified for Honours' tag and main modules info when the student is qualified", () => {
|
||||
cy.visit('/students/495976')
|
||||
cy.contains('h2', 'Bachelor Honours')
|
||||
cy.contains('[data-cy=honours-chip-qualified]', 'Qualified for Honours')
|
||||
cy.contains('Study modules').click()
|
||||
cy.get('[data-cy=main-modules] tbody tr')
|
||||
.should('have.length', 3)
|
||||
.each(($tr, index) => {
|
||||
const info = [
|
||||
['07.05.2020', 'Matemaattisten tieteiden kandiohjelma (KH50_001)', 'Hyv.'],
|
||||
['08.05.2018', 'Matematiikka, perusopinnot (MAT110)', '4'],
|
||||
['25.02.2020', 'Matematiikka, aineopinnot (MAT210)', '4'],
|
||||
]
|
||||
cy.wrap($tr).within(() => {
|
||||
info[index].forEach((text, i) => {
|
||||
cy.get('td').eq(i).contains(text)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("Shows 'Did not graduate in time' when the student has graduated but not in time", () => {
|
||||
cy.visit('/students/540355')
|
||||
cy.contains('h2', 'Bachelor Honours')
|
||||
cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours')
|
||||
cy.contains('[data-cy=honours-chip-error]', 'Did not graduate in time')
|
||||
})
|
||||
|
||||
it("Shows 'Module grades too low' when the student has graduated in time but has too low grades", () => {
|
||||
cy.visit('/students/547934')
|
||||
cy.contains('h2', 'Bachelor Honours')
|
||||
cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours')
|
||||
cy.contains('[data-cy=honours-chip-error]', 'Module grades too low')
|
||||
})
|
||||
|
||||
it("Shows 'Might need further inspection' when the student has graduated in time but has more than four main modules", () => {
|
||||
cy.visit('/students/478837')
|
||||
cy.contains('h2', 'Bachelor Honours')
|
||||
cy.contains('[data-cy=honours-chip-inspection]', 'Might need further inspection')
|
||||
})
|
||||
|
||||
it("Shows 'Has not graduated' when the student has not graduated", () => {
|
||||
cy.visit(`students/${student.studentNumber}`)
|
||||
cy.contains('h2', 'Bachelor Honours')
|
||||
cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours')
|
||||
cy.contains('[data-cy=honours-chip-error]', 'Has not graduated')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Use admin to see all students
|
||||
describe('When using admin user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/students', 'admin')
|
||||
})
|
||||
|
||||
it('Does not crash if student has no study rights or courses', () => {
|
||||
typeStudentNumberAndClick('450730')
|
||||
cy.contains('Credits: 0')
|
||||
})
|
||||
|
||||
it("'Update student' button is shown", () => {
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.cs('student-info-card').within(() => {
|
||||
cy.contains('button', 'Update student')
|
||||
})
|
||||
})
|
||||
|
||||
it('Bachelor Honours section is not shown for students outside of Faculty of Science', () => {
|
||||
cy.visit('/students/453146')
|
||||
cy.contains('h2', 'Bachelor Honours').should('not.exist')
|
||||
})
|
||||
|
||||
it('When a study plan is selected, courses included in the study plan are highlighted with a blue background', () => {
|
||||
cy.visit('/students/550789')
|
||||
cy.contains('table tbody tr', 'Kulttuurien tutkimuksen kandiohjelma (01.08.2020–30.06.2023)').within(() => {
|
||||
cy.get('td').eq(0).click()
|
||||
})
|
||||
cy.contains('table tbody tr', 'Kandidaatintutkielma (KUKA-LIS222)').should(
|
||||
'have.css',
|
||||
'background-color',
|
||||
'rgb(232, 244, 255)'
|
||||
)
|
||||
})
|
||||
|
||||
it('When a study plan is selected, the time frame of the credit graph is updated', () => {
|
||||
cy.clock(new Date('2024-08-30').getTime(), ['Date'])
|
||||
cy.visit('/students/550789')
|
||||
cy.contains('.highcharts-container text', '1 Aug 2020')
|
||||
cy.contains('.highcharts-container text', '30 Aug 2024')
|
||||
cy.contains('table tbody tr', 'Kulttuurien tutkimuksen kandiohjelma (01.08.2020–30.06.2023)').within(() => {
|
||||
cy.get('td').eq(0).click()
|
||||
})
|
||||
cy.contains('.highcharts-container text', '1 Aug 2020')
|
||||
cy.contains('.highcharts-container text', '19 Aug 2023')
|
||||
})
|
||||
|
||||
it("If there's a study plan corresponding to the degree programme, completed credits are displayed", () => {
|
||||
cy.visit('/students/550789')
|
||||
cy.contains('table tbody tr', 'Kulttuurien tutkimuksen kandiohjelma (01.08.2020–30.06.2023)').within(() => {
|
||||
cy.get('td')
|
||||
.eq(-1)
|
||||
.contains(/^185 cr$/)
|
||||
})
|
||||
})
|
||||
|
||||
it("If there's a study plan corresponding to the degree programme and the student hasn't graduated, percentage of completion is also displayed", () => {
|
||||
cy.visit('/students/458723')
|
||||
cy.contains('table tbody tr', 'Oikeusnotaarin koulutusohjelma (01.08.2020–31.07.2027)').within(() => {
|
||||
cy.get('td').eq(-1).contains('69% (125 cr)')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,164 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
describe('Study guidance group tests', () => {
|
||||
describe('Study guidance group overview page', () => {
|
||||
describe('without study guidance groups', () => {
|
||||
it('shows the correct notification', () => {
|
||||
cy.init('/studyguidancegroups', 'admin')
|
||||
cy.contains('Study guidance groups')
|
||||
cy.contains('You do not have access to any study guidance groups.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('without correct rights', () => {
|
||||
it('shows the correct notification', () => {
|
||||
cy.init('/studyguidancegroups')
|
||||
cy.contains('Access denied')
|
||||
cy.contains(
|
||||
"You don't currently have permission to view this page. If you believe this is a mistake, please contact oodikone@helsinki.fi."
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with study guidance groups', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/studyguidancegroups', 'onlystudyguidancegroups')
|
||||
cy.intercept('/api/studyguidancegroups', { fixture: 'studyGuidanceGroups.json' })
|
||||
})
|
||||
|
||||
it('shows the correct data', () => {
|
||||
cy.contains('Study guidance groups')
|
||||
|
||||
const studyGuidanceGroupsTableContent = [
|
||||
['MAT kandit 2020', 3, 'Matemaattisten tieteiden kandiohjelma', '2020 - 2021'],
|
||||
['Oma ohjausryhmä', 2, 'Add degree programme', 'Add starting year'],
|
||||
]
|
||||
|
||||
cy.checkTableStats(studyGuidanceGroupsTableContent, 'study-guidance-group-overview')
|
||||
})
|
||||
|
||||
it('has working links to single study guidance groups', () => {
|
||||
cy.cs('study-guidance-group-link-sgg-cypress-1').click()
|
||||
cy.url().should('eq', `${Cypress.config().baseUrl}/studyguidancegroups/sgg-cypress-1`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Page for single study guidance group', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/api/studyguidancegroups', { fixture: 'studyGuidanceGroups.json' })
|
||||
})
|
||||
|
||||
it('has a working back button', () => {
|
||||
cy.init('/studyguidancegroups/sgg-cypress-1', 'onlystudyguidancegroups')
|
||||
|
||||
cy.get('button').contains('Back to groups').click()
|
||||
cy.url().should('eq', `${Cypress.config().baseUrl}/studyguidancegroups`)
|
||||
})
|
||||
|
||||
describe('without associated degree programme and year', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/studyguidancegroups/sgg-cypress-2', 'onlystudyguidancegroups')
|
||||
})
|
||||
|
||||
it('shows the correct panes', () => {
|
||||
cy.contains('Oma ohjausryhmä')
|
||||
cy.cs('Credit accumulation (for 2 students)')
|
||||
cy.cs('Age distribution')
|
||||
cy.cs('Courses of population')
|
||||
cy.contains('Students (2)')
|
||||
cy.cs('Credit statistics').should('not.exist')
|
||||
})
|
||||
|
||||
it.skip('students table has the correct tabs', () => {
|
||||
cy.contains('Students (2)').click()
|
||||
cy.get('[data-cy="student-table-tabs"] .ui.attached.tabular.menu').then($menu => {
|
||||
const tabNames = Array.from($menu[0].children).map(elem => elem.innerText)
|
||||
expect(tabNames).to.deep.equal(['General'])
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('general tab of the students table has the correct columns', () => {
|
||||
const colHeaders = [
|
||||
'Student number',
|
||||
'All credits',
|
||||
'Credits since 1.1.1970',
|
||||
'Start year at uni',
|
||||
'Degree programmes',
|
||||
'Tags',
|
||||
]
|
||||
cy.contains('Students (2)').click()
|
||||
cy.get('[data-cy="student-table-tabs"] table thead tr').within(() => {
|
||||
colHeaders.every(heading => cy.contains(heading))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with associated degree programme and year', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/studyguidancegroups/sgg-cypress-1', 'onlystudyguidancegroups')
|
||||
})
|
||||
|
||||
it('shows the correct labels', () => {
|
||||
cy.get('h4').contains('MAT kandit 2020')
|
||||
cy.get('.MuiChip-root:first').contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.get('.MuiChip-root:last').contains('2020 - 2021')
|
||||
})
|
||||
|
||||
it('shows the correct panes', () => {
|
||||
cy.cs('Credit accumulation (for 3 students)')
|
||||
cy.cs('Credit statistics')
|
||||
cy.cs('Age distribution')
|
||||
cy.cs('Courses of population')
|
||||
cy.contains('Students (3)')
|
||||
})
|
||||
|
||||
it("clicking the 'Show credits starting from the associated academic year' filter", () => {
|
||||
cy.get('[data-cy="ParticipationDate-filter-card"][data-active="false"]')
|
||||
cy.cs('Credit accumulation (for 3 students)').click()
|
||||
cy.get('label').contains('Show credits starting from the associated academic year').click()
|
||||
cy.get('[data-cy="ParticipationDate-filter-card"][data-active="true"]')
|
||||
cy.get('.date-picker')
|
||||
.eq(0)
|
||||
.within(() => cy.get('input').should('have.value', '01.08.2020'))
|
||||
cy.contains('Reset All Filters')
|
||||
cy.get('label').contains('Show credits starting from the associated academic year').click()
|
||||
cy.get('[data-cy="ParticipationDate-filter-card"][data-active="false"]')
|
||||
})
|
||||
|
||||
it.skip('students table has the correct tabs', () => {
|
||||
cy.contains('Students (3)').click()
|
||||
cy.get('[data-cy="student-table-tabs"] .ui.attached.tabular.menu').then($menu => {
|
||||
const tabNames = Array.from($menu[0].children).map(elem => elem.innerText)
|
||||
expect(tabNames).to.deep.equal(['General', 'Courses', 'Modules', 'Progress'])
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('general tab of the students table has the correct columns', () => {
|
||||
const colHeaders = [
|
||||
'Student number',
|
||||
'All credits',
|
||||
'Credits in HOPS',
|
||||
'Credits since 1.8.2020',
|
||||
'Start of study right',
|
||||
'Started in programme',
|
||||
'Semesters present',
|
||||
'Graduation date',
|
||||
'Start year at uni',
|
||||
'Other programmes',
|
||||
'Transferred from',
|
||||
'Admission type',
|
||||
'Gender',
|
||||
'Citizenships',
|
||||
'Curriculum period',
|
||||
'Latest attainment date',
|
||||
'Tags',
|
||||
]
|
||||
cy.contains('Students (3)').click()
|
||||
cy.get('[data-cy="student-table-tabs"] table thead tr').within(() => {
|
||||
colHeaders.every(heading => cy.contains(heading))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,64 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
describe('Teachers page tests', () => {
|
||||
beforeEach(() => {
|
||||
// login as admin = has teacher rights
|
||||
cy.init('/teachers', 'admin')
|
||||
cy.url().should('include', '/teachers')
|
||||
})
|
||||
|
||||
const teacher1 = 'Luokkanen Liisa Viljami'
|
||||
const teacher2 = 'Perälä Juhani Susanna'
|
||||
|
||||
const statisticsHeaders = ['Name', 'Credits', 'Credits transferred', 'Passed']
|
||||
|
||||
it('Check Statistics', () => {
|
||||
cy.cs('semester-start').click()
|
||||
cy.contains('Syksy 2020').click()
|
||||
cy.cs('course-providers').click()
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma').click()
|
||||
cy.get('body').click(0, 0) // Click outside of the select
|
||||
cy.cs('search-statistics').click()
|
||||
|
||||
cy.contains('Teacher')
|
||||
cy.get('table thead tr th').should('have.length', 4)
|
||||
statisticsHeaders.forEach((header, index) => {
|
||||
cy.get('table thead tr th').eq(index).should('contain', header)
|
||||
})
|
||||
cy.contains('td', teacher1).siblings().eq(0).contains('235')
|
||||
cy.contains('td', teacher1).siblings().eq(2).contains('97.40%')
|
||||
cy.contains('td', teacher2).siblings().eq(0).contains('395')
|
||||
cy.contains('td', teacher2).siblings().eq(2).contains('98.78%')
|
||||
})
|
||||
|
||||
it('Teacher search works', () => {
|
||||
cy.cs('Search').click()
|
||||
cy.cs('teacher-search').type(teacher1.split(' ')[0])
|
||||
cy.get('table tbody tr').should('have.length', 4)
|
||||
cy.get('table tbody tr').eq(3).contains('td', teacher1)
|
||||
})
|
||||
|
||||
it('Can check teacher page', () => {
|
||||
cy.cs('Search').click()
|
||||
cy.cs('teacher-search').type(teacher2)
|
||||
|
||||
// Prevent opening in new tab
|
||||
cy.contains('a', teacher2).invoke('removeAttr', 'target').click()
|
||||
cy.url().should('include', '/teachers/hy-hlo-49026530')
|
||||
cy.contains(teacher2)
|
||||
cy.contains('Syksy 2023').click()
|
||||
cy.contains('Kevät 2019').click()
|
||||
cy.contains('tr', 'MAT12004').within(() => {
|
||||
const rowContent = ['MAT12004', 'Tilastollinen päättely I', '120', '0', '92.31%']
|
||||
rowContent.forEach((content, index) => {
|
||||
cy.get('td').eq(index).contains(content)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Check leaderboad works', () => {
|
||||
cy.cs('Leaderboard').click()
|
||||
cy.cs('academic-year').click()
|
||||
cy.contains('2020-2021').click()
|
||||
})
|
||||
})
|
||||
@@ -1,90 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const progressLevels = ['bachelors', 'bachelor-masters', 'masters', 'doctoral']
|
||||
|
||||
const checkProgressBarCharts = () => {
|
||||
progressLevels.forEach(level => cy.cs(`faculty-${level}-progress-bar-chart-section`))
|
||||
}
|
||||
|
||||
const checkProgressTables = () => {
|
||||
progressLevels.forEach(level => cy.cs(`${level}-faculty-progress-table`))
|
||||
}
|
||||
|
||||
const graduationTimesLevels = ['bachelor', 'master', 'doctor'] // ? Bachelor + master seems to be missing in test data
|
||||
|
||||
const checkAverageGraduationTimesBreakdownBarCharts = () => {
|
||||
graduationTimesLevels.forEach(level => {
|
||||
cy.cs(`${level}-breakdown-bar-chart`)
|
||||
})
|
||||
}
|
||||
|
||||
const checkAverageGraduationTimesMedianBarCharts = () => {
|
||||
graduationTimesLevels.forEach(level => {
|
||||
cy.cs(`${level}-median-bar-chart`)
|
||||
})
|
||||
}
|
||||
|
||||
describe('University view', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/university')
|
||||
cy.contains('University')
|
||||
})
|
||||
|
||||
describe('Faculty progress tab', () => {
|
||||
it('contains all the correct progress bar charts', () => {
|
||||
checkProgressBarCharts()
|
||||
})
|
||||
|
||||
it('contains all the correct progress tables', () => {
|
||||
checkProgressTables()
|
||||
})
|
||||
|
||||
it("'All study rights / Special study rights excluded' toggle works", () => {
|
||||
cy.cs('study-right-toggle').click()
|
||||
checkProgressBarCharts()
|
||||
checkProgressTables()
|
||||
})
|
||||
|
||||
// Nothing in this test is explained. Non of these values are verified.
|
||||
it.skip('years in the tables can be clicked to show faculty level breakdown', () => {
|
||||
cy.cs('study-right-toggle').click()
|
||||
cy.cs('bachelors-faculty-progress-table-show-button3').click()
|
||||
cy.contains('29.5%').trigger('mouseover', { force: true })
|
||||
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
|
||||
cy.contains('H50')
|
||||
cy.contains('0 Credits: 0')
|
||||
cy.contains('1 ≤ Credits < 45: 13')
|
||||
cy.contains('45 ≤ Credits < 90: 10')
|
||||
cy.contains('90 ≤ Credits < 135: 16')
|
||||
cy.contains('135 ≤ Credits < 180: 3')
|
||||
cy.contains('180 ≤ Credits: 2')
|
||||
})
|
||||
|
||||
it('info boxes contain correct information', () => {
|
||||
cy.cs('faculty-progress-info-box-button').click()
|
||||
cy.cs('faculty-progress-info-box-content').contains('Kuvaa tiedekuntaan kuuluvien')
|
||||
cy.cs('faculty-bachelor-masters-progress-info-box-button').click()
|
||||
cy.cs('faculty-bachelor-masters-progress-info-box-content').contains('The starting year is the')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty graduations tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.cs('faculty-graduations-tab').click()
|
||||
})
|
||||
|
||||
it('has all the correct median time bar charts', () => {
|
||||
checkAverageGraduationTimesBreakdownBarCharts()
|
||||
})
|
||||
|
||||
it("'Breakdown/Median times' toggle works", () => {
|
||||
cy.cs('graduation-time-toggle').click()
|
||||
checkAverageGraduationTimesMedianBarCharts()
|
||||
})
|
||||
|
||||
it('info boxes contain correct information', () => {
|
||||
cy.cs('average-graduation-times-info-box-button').click()
|
||||
cy.cs('average-graduation-times-info-box-content').contains('Opiskelijoiden keskimääräiset valmistumisajat')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,91 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const visibleLinks = {
|
||||
norights: ['University', 'Faculties', 'Special populations', 'Feedback'],
|
||||
onlycoursestatistics: ['University', 'Courses', 'Special populations', 'Feedback'],
|
||||
}
|
||||
|
||||
visibleLinks.basic = [...visibleLinks.onlycoursestatistics, 'Faculties', 'Programmes', 'Students']
|
||||
visibleLinks.admin = [...visibleLinks.basic, 'Teachers', 'Admin']
|
||||
|
||||
const containsLinks = links => {
|
||||
cy.cs('nav-bar').within(() => {
|
||||
for (const link of links) {
|
||||
cy.contains(link)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const userButtonWorks = (username, mocking = false) => {
|
||||
cy.cs('nav-bar-user-button').click()
|
||||
cy.contains(mocking ? `Mocking as ${username}` : `Logged in as ${username}`)
|
||||
cy.contains('Language')
|
||||
cy.contains('suomi')
|
||||
cy.contains('English')
|
||||
cy.contains('svenska')
|
||||
cy.contains(mocking ? 'Stop mocking' : 'Log out')
|
||||
}
|
||||
|
||||
describe('Users tests', () => {
|
||||
describe('Using as user with just grp-oodikone-user, no other rights', () => {
|
||||
it('shows correct tabs', () => {
|
||||
cy.init('', 'norights')
|
||||
containsLinks(visibleLinks.norights)
|
||||
userButtonWorks('norights')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Using as coursestatistics user', () => {
|
||||
it('shows correct tabs', () => {
|
||||
cy.init('', 'onlycoursestatistics')
|
||||
containsLinks(visibleLinks.onlycoursestatistics)
|
||||
userButtonWorks('onlycoursestatistics')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Using as basic user', () => {
|
||||
it('shows correct tabs', () => {
|
||||
cy.init('')
|
||||
containsLinks(visibleLinks.basic)
|
||||
userButtonWorks('basic')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Using as admin', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/users', 'admin')
|
||||
})
|
||||
|
||||
it('should see more stuff than others', () => {
|
||||
containsLinks(visibleLinks.admin)
|
||||
userButtonWorks('admin')
|
||||
})
|
||||
|
||||
describe('can mock as other users', { retries: 3 }, () => {
|
||||
beforeEach(() => {
|
||||
cy.cs('user-page-button-basic').click()
|
||||
cy.cs('mock-button').click()
|
||||
})
|
||||
|
||||
it('user button shows mocked user', () => {
|
||||
userButtonWorks('basic', true)
|
||||
})
|
||||
|
||||
it("only the mocked user's programmes are visible", () => {
|
||||
cy.intercept('/api/v3/populationstatistics/studyprogrammes').as('studyprogrammes')
|
||||
cy.visit('/populations')
|
||||
cy.wait('@studyprogrammes').its('response.statusCode').should('be.oneOf', [200, 304])
|
||||
|
||||
cy.contains('h6', 'Degree programme')
|
||||
cy.cs('population-programme-selector').within(() =>
|
||||
cy.get('input').should('have.attr', 'placeholder', 'Select degree programme')
|
||||
)
|
||||
cy.cs('population-programme-selector-parent').click()
|
||||
cy.cs('population-programme-selector-parent').within(() => cy.contains('Matemaattisten tieteiden kandiohjelma'))
|
||||
cy.cs('population-programme-selector-parent').within(() =>
|
||||
cy.contains('Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"studentSet1": ["433237", "457144", "458090", "474270", "479440"],
|
||||
"coursesSet1": ["TKT10001", "TKT10002", "TKT10003", "TKT10004"]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"studentNumbersForCSStudentsSet1": ["433237", "457144", "458090", "465136"],
|
||||
"studentNumbersForCSStudentsSet2": ["474270", "479440", "470391", "474789"]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"courseCodesSet1": ["TKT10001", "TKT10002"],
|
||||
"courseCodesSet2": ["TKT10003", "TKT10004", "TKT10005"]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "sgg-cypress-1",
|
||||
"name": {
|
||||
"fi": "MAT kandit 2020"
|
||||
},
|
||||
"members": [
|
||||
{ "personStudentNumber": "433237" },
|
||||
{ "personStudentNumber": "457144" },
|
||||
{ "personStudentNumber": "458090" }
|
||||
],
|
||||
"tags": {
|
||||
"studyProgramme": "KH50_001",
|
||||
"year": "2020"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sgg-cypress-2",
|
||||
"name": {
|
||||
"fi": "Oma ohjausryhmä"
|
||||
},
|
||||
"members": [{ "personStudentNumber": "474270" }, { "personStudentNumber": "479440" }],
|
||||
"tags": {
|
||||
"studyProgramme": null,
|
||||
"year": null
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
on('before:browser:launch', (browser = {}, launchOptions) => {
|
||||
// By manually setting the browser width and height in headless
|
||||
// mode, screenshots and videos will have a better quality
|
||||
// https://www.cypress.io/blog/generate-high-resolution-videos-and-screenshots
|
||||
const width = 2560
|
||||
const height = 1440
|
||||
|
||||
if (browser.name === 'chrome' && browser.isHeadless) {
|
||||
launchOptions.args.push(`--window-size=${width},${height}`)
|
||||
launchOptions.args.push('--force-device-scale-factor=1')
|
||||
}
|
||||
return launchOptions
|
||||
})
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
//
|
||||
// ! IMPORTANT: here we need to set keys to be all lowercase, since
|
||||
// ! we're replacing headers after they've left browser / frontend.
|
||||
// All of these users are available in anon user-db
|
||||
const adminUserHeaders = {
|
||||
uid: 'admin',
|
||||
displayname: 'Admin User',
|
||||
'shib-session-id': 'mock-cypress-session',
|
||||
hygroupcn: 'grp-oodikone-users;grp-oodikone-basic-users;grp-toska',
|
||||
mail: 'grp-toska+mockadminuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-6666666',
|
||||
}
|
||||
|
||||
const basicUserHeaders = {
|
||||
uid: 'basic',
|
||||
displayname: 'Basic User',
|
||||
'shib-session-id': 'mock-cypress-session',
|
||||
hygroupcn: 'grp-oodikone-users;grp-oodikone-basic-users',
|
||||
mail: 'grp-toska+mockbasicuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-1234567',
|
||||
}
|
||||
|
||||
const onlycoursestatisticsUserHeaders = {
|
||||
uid: 'onlycoursestatistics',
|
||||
displayname: 'Onlycoursestatistics User',
|
||||
'shib-session-id': 'mock-cypress-session',
|
||||
hygroupcn: 'grp-oodikone-basic-users',
|
||||
mail: 'grp-toska+mockonlycoursestatisticsuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-7654321',
|
||||
}
|
||||
|
||||
const norightsUserHeaders = {
|
||||
uid: 'norights',
|
||||
displayname: 'Norights User',
|
||||
'shib-session-id': 'mock-cypress-session',
|
||||
hygroupcn: 'grp-oodikone-users',
|
||||
mail: 'grp-toska+mocknorightuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-0000000',
|
||||
}
|
||||
|
||||
const onlyIamRightsUserHeaders = {
|
||||
uid: 'onlyiamrights',
|
||||
displayname: 'Only IAM rights user',
|
||||
'shib-session-id': 'mock-cypress-session',
|
||||
hygroupcn: 'hy-employees;hy-mltdk-mat-jory',
|
||||
mail: 'grp-toska+mockonlyiamrightsuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-1111111',
|
||||
}
|
||||
|
||||
// Also has element detail of "KH50_005" for the study guidance groups to work
|
||||
const onlyStudyGuidanceGroupsUser = {
|
||||
uid: 'onlystudyguidancegroups',
|
||||
displayname: 'Study Guidance Groups User',
|
||||
'shib-session-id': 'mock-cypress-session',
|
||||
hygroupcn: 'grp-oodikone-users',
|
||||
mail: 'grp-toska+mockonlystudyguidancegroupsuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-2222222',
|
||||
}
|
||||
|
||||
// TODO add user into anon-DB (check the need for changing hypersonid)
|
||||
// REMEMBER TO ADD ROLE openUniSearch
|
||||
// const openUniUser = {
|
||||
// uid: 'openuniuser',
|
||||
// displayname: 'Open Uni User',
|
||||
// 'shib-session-id': 'mock-cypress-session',
|
||||
// hygroupcn: 'grp-oodikone-basics-users;hy-ypa-opa-dojo',
|
||||
// mail: 'grp-toska+mockopenuniuser@helsinki.fi',
|
||||
// hypersonsisuid: 'hy-hlo-9999999',
|
||||
// }
|
||||
|
||||
const userHeaders = [
|
||||
adminUserHeaders,
|
||||
basicUserHeaders,
|
||||
onlycoursestatisticsUserHeaders,
|
||||
norightsUserHeaders,
|
||||
onlyIamRightsUserHeaders,
|
||||
onlyStudyGuidanceGroupsUser,
|
||||
]
|
||||
|
||||
/**
|
||||
* Set up headers to login, set up correct user (admin / basic / etc.) and open given path.
|
||||
*/
|
||||
Cypress.Commands.add('init', { prevSubject: false }, (path, userId = 'basic') => {
|
||||
const headersToUse = userHeaders.find(({ uid }) => uid === userId)
|
||||
if (!headersToUse) throw Error(`${userId} is not valid user id!`)
|
||||
|
||||
cy.intercept({ hostname: 'localhost', url: '**', method: '*' }, req => {
|
||||
req.headers = headersToUse
|
||||
})
|
||||
|
||||
// TODO: Add wait for visit url.
|
||||
// This is block by course population query.
|
||||
cy.visit({ method: 'GET', url: path, headers: headersToUse })
|
||||
})
|
||||
|
||||
/**
|
||||
* Shorthand for using "Cypress Selectors" (CS), i.e., `data-cy` attributes.
|
||||
*/
|
||||
Cypress.Commands.add('cs', { prevSubject: 'optional' }, (subject, name) => {
|
||||
const selector = `[data-cy="${name}"]`
|
||||
|
||||
if (subject === undefined) {
|
||||
return cy.get(selector)
|
||||
}
|
||||
return cy.wrap(subject).find(selector)
|
||||
})
|
||||
|
||||
/**
|
||||
* Select item from FilterSelect dropdown.
|
||||
* If the index is a number, select index-th item.
|
||||
* Otherwise select the item containing index text.
|
||||
*/
|
||||
Cypress.Commands.add('selectFromDropdown', { prevSubject: false }, (label, index, isMultiSelect = false) => {
|
||||
cy.cs(`${label}-selector`).click()
|
||||
|
||||
cy.get(`[role="listbox"][aria-labelledby="${label}"]`).within(() => {
|
||||
if (typeof index === 'number') cy.get('li').eq(index).click()
|
||||
else cy.get('li').contains(index).click()
|
||||
})
|
||||
|
||||
if (isMultiSelect) cy.get('#menu- > .MuiModal-backdrop').click()
|
||||
})
|
||||
|
||||
/**
|
||||
* Select item from FilterSearchableSelect dropdown.
|
||||
* If the index is a number, select index-th item.
|
||||
* Otherwise select the item containing index text.
|
||||
*/
|
||||
Cypress.Commands.add('selectFromAutocomplete', { prevSubject: false }, (label, index) => {
|
||||
cy.cs(`${label}-selector`).click()
|
||||
cy.get(`#${label}-popper.MuiAutocomplete-popper [role="listbox"]`).within(() => {
|
||||
if (typeof index === 'number') cy.get('li').eq(index).click()
|
||||
else cy.get('li').contains(index).click()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Set range selector values FilterRange.
|
||||
* If the reset flag is set, the function tries to reset the original values.
|
||||
*/
|
||||
Cypress.Commands.add('setRangeSelect', { prevSubject: false }, (filter, min, max, expected, reset) => {
|
||||
const minText = min.toString()
|
||||
const maxText = max.toString()
|
||||
const expectedText = expected.toString()
|
||||
|
||||
const parent = `${filter}-filter-card`
|
||||
|
||||
cy.cs(parent)
|
||||
.cs('FilterRangeStart')
|
||||
.find('input')
|
||||
.invoke('val')
|
||||
.then(initialMin => {
|
||||
cy.cs(parent)
|
||||
.cs('FilterRangeEnd')
|
||||
.find('input')
|
||||
.invoke('val')
|
||||
.then(initialMax => {
|
||||
cy.cs(parent).cs('FilterRangeStart').find('input').clear()
|
||||
cy.cs(parent).cs('FilterRangeStart').find('input').type(minText)
|
||||
cy.cs(parent).cs('FilterRangeEnd').find('input').clear()
|
||||
cy.cs(parent).cs('FilterRangeEnd').find('input').type(maxText)
|
||||
|
||||
// This should be linked to other checking functions.
|
||||
// Maybe one day.
|
||||
cy.contains(`Students (${expectedText})`)
|
||||
|
||||
if (reset) {
|
||||
cy.cs(parent).cs('FilterRangeStart').find('input').clear()
|
||||
cy.cs(parent).cs('FilterRangeStart').find('input').type(initialMin)
|
||||
cy.cs(parent).cs('FilterRangeEnd').find('input').clear()
|
||||
cy.cs(parent).cs('FilterRangeEnd').find('input')?.type(initialMax)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('checkTableStats', (correctStats, tableName) => {
|
||||
cy.get(`[data-cy=${tableName}-data-table] tbody`).within(() => {
|
||||
correctStats.forEach((values, trIndex) => {
|
||||
cy.get('tr')
|
||||
.filter(':visible')
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
cy.get('tr').filter(':visible').should('have.length', correctStats.length)
|
||||
})
|
||||
})
|
||||
@@ -1,21 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
require('./commands')
|
||||
|
||||
Cypress.on('uncaught:exception', error => {
|
||||
/* returning false here prevents Cypress from failing the test */
|
||||
return true
|
||||
})
|
||||
@@ -1,162 +0,0 @@
|
||||
x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
services:
|
||||
backend:
|
||||
<<: *defaults
|
||||
container_name: backend
|
||||
depends_on:
|
||||
- kone-db
|
||||
- redis
|
||||
- sis-db
|
||||
- user-db
|
||||
environment:
|
||||
CI: 'true'
|
||||
DB_URL_USER: postgres://postgres@user-db:5432/user-db
|
||||
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
|
||||
FRONT_URL: http://localhost:1337
|
||||
REDIS: redis
|
||||
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://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/importer'
|
||||
JAMI_URL: 'http://jami:3003/'
|
||||
CRYPT_KEY: 'e16730fce263d5270ba8dcf84d068a32556542455e71331880d6c1c5e4db9b4e'
|
||||
build:
|
||||
context: services
|
||||
dockerfile: backend/Dockerfile
|
||||
profiles:
|
||||
- oodikone
|
||||
|
||||
frontend:
|
||||
<<: *defaults
|
||||
container_name: frontend
|
||||
build:
|
||||
context: services
|
||||
dockerfile: frontend/Dockerfile
|
||||
profiles:
|
||||
- oodikone
|
||||
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
container_name: kone-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: kone-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/kone-db
|
||||
|
||||
redis:
|
||||
<<: *defaults
|
||||
command: >
|
||||
sh -c 'redis-server --appendonly yes && redis-cli flushall'
|
||||
image: redis:7.2.6
|
||||
container_name: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
sis-db:
|
||||
<<: *defaults
|
||||
container_name: sis-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
shm_size: 2gb
|
||||
|
||||
user-db:
|
||||
<<: *defaults
|
||||
container_name: user-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: user-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/user-db
|
||||
|
||||
jami:
|
||||
<<: *defaults
|
||||
image: quay.io/toska/jami:production
|
||||
environment:
|
||||
- npm_config_cache=/tmp
|
||||
- PORT=3003
|
||||
- NODE_ENV=dev
|
||||
- DATABASE_URL=postgres://postgres:postgres@jami-db:5432/postgres
|
||||
ports:
|
||||
- 3003:3003
|
||||
container_name: jami
|
||||
|
||||
jami-db:
|
||||
<<: *defaults
|
||||
image: postgres:15.10
|
||||
environment:
|
||||
- PGDATA=/data
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
container_name: jami-db
|
||||
ports:
|
||||
- 5433:5432
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
container_name: nginx
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
ports:
|
||||
- 1337:80
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
profiles:
|
||||
- oodikone
|
||||
|
||||
# Following services are used to run sis-updater-worker when anon sis-db needs to be updated. Could probably also use production version, but this certainly works.
|
||||
sis-importer-db:
|
||||
<<: *defaults
|
||||
container_name: sis-importer-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-importer-db
|
||||
profiles:
|
||||
- updater
|
||||
|
||||
sis-updater-worker:
|
||||
<<: *defaults
|
||||
build:
|
||||
context: updater/sis-updater-worker
|
||||
container_name: sis-updater-worker
|
||||
depends_on:
|
||||
- redis
|
||||
- sis-db
|
||||
- sis-importer-db
|
||||
environment:
|
||||
CI: 'true'
|
||||
DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
REDIS_HOST: redis
|
||||
SIS_IMPORTER_DATABASE: sis-importer-db
|
||||
SIS_IMPORTER_HOST: sis-importer-db
|
||||
SIS_IMPORTER_PASSWORD: postgres
|
||||
SIS_IMPORTER_USER: postgres
|
||||
SIS_PASSWORD: postgres
|
||||
profiles:
|
||||
- updater
|
||||
|
||||
sis-updater-scheduler:
|
||||
<<: *defaults
|
||||
build:
|
||||
context: updater/sis-updater-scheduler
|
||||
container_name: sis-updater-scheduler
|
||||
depends_on:
|
||||
- redis
|
||||
- sis-importer-db
|
||||
environment:
|
||||
CI: 'true'
|
||||
REDIS_HOST: redis
|
||||
SECRET_TOKEN: dev
|
||||
SIS_IMPORTER_DATABASE: sis-importer-db
|
||||
SIS_IMPORTER_HOST: sis-importer-db
|
||||
SIS_IMPORTER_PASSWORD: postgres
|
||||
SIS_IMPORTER_USER: postgres
|
||||
SCHEDULE_IMMEDIATE: '${SCHEDULE_IMMEDIATE}'
|
||||
EXIT_AFTER_IMMEDIATES: '${EXIT_AFTER_IMMEDIATES}'
|
||||
ports:
|
||||
- 8082:8082
|
||||
profiles:
|
||||
- updater
|
||||
@@ -1,22 +0,0 @@
|
||||
services:
|
||||
backend:
|
||||
environment:
|
||||
DB_URL_USER: postgres://postgres@user-db:5432/user-db-real
|
||||
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db-real
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db-real
|
||||
|
||||
redis:
|
||||
volumes:
|
||||
- redis-data-real:/data
|
||||
|
||||
sis-updater-scheduler:
|
||||
environment:
|
||||
SIS_IMPORTER_DATABASE: sis-importer-db-real
|
||||
|
||||
sis-updater-worker:
|
||||
environment:
|
||||
DB_URL: postgres://postgres@sis-db:5432/sis-db-real
|
||||
SIS_IMPORTER_DATABASE: sis-importer-db-real
|
||||
|
||||
volumes:
|
||||
redis-data-real:
|
||||
@@ -1,26 +0,0 @@
|
||||
services:
|
||||
api-tests:
|
||||
volumes:
|
||||
- backend-tests-nodemod:/opt/app-root/backend/node_modules
|
||||
- ./services/backend/:/opt/app-root/backend
|
||||
- ./services/shared/:/opt/app-root/shared
|
||||
environment:
|
||||
CI: 'true'
|
||||
NODE_ENV: 'test'
|
||||
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
|
||||
DB_URL_USER: postgres://postgres@user-db:5432/user-db
|
||||
FRONT_URL: http://localhost:3000
|
||||
REDIS: redis
|
||||
SECRET_TOKEN: test
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
|
||||
JAMI_URL: 'http://jami:3003/'
|
||||
IMPORTER_DB_API_URL: 'https://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/importer'
|
||||
CRYPT_KEY: 'e16730fce263d5270ba8dcf84d068a32556542455e71331880d6c1c5e4db9b4e'
|
||||
build:
|
||||
context: services/backend
|
||||
dockerfile: dev.Dockerfile
|
||||
command: npm run test
|
||||
|
||||
volumes:
|
||||
backend-tests-nodemod:
|
||||
@@ -1,200 +0,0 @@
|
||||
x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
x-build_defaults: &build_defaults
|
||||
dockerfile: dev.Dockerfile
|
||||
|
||||
services:
|
||||
adminer:
|
||||
<<: *defaults
|
||||
container_name: adminer
|
||||
environment:
|
||||
ADMINER_DESIGN: pepa-linha
|
||||
image: adminer:4.8.1
|
||||
ports:
|
||||
- 5050:8080
|
||||
volumes:
|
||||
- ./adminer-plugins:/var/www/html/plugins-enabled
|
||||
|
||||
backend:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
context: services/backend
|
||||
container_name: backend
|
||||
depends_on:
|
||||
- kone-db
|
||||
- redis
|
||||
- sis-db
|
||||
- user-db
|
||||
env_file:
|
||||
- .env
|
||||
tty: true
|
||||
environment:
|
||||
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
|
||||
DB_URL_USER: postgres://postgres@user-db:5432/user-db
|
||||
FRONT_URL: http://localhost:3000
|
||||
REDIS: redis
|
||||
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://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/importer'
|
||||
JAMI_URL: 'http://jami:3003/'
|
||||
ports:
|
||||
- 8080:8080
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- backend-nodemod:/opt/app-root/backend/node_modules
|
||||
- ./services/backend/:/opt/app-root/backend
|
||||
- ./services/shared/:/opt/app-root/shared
|
||||
|
||||
frontend:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
context: services/frontend
|
||||
container_name: frontend
|
||||
ports:
|
||||
- 3000:3000
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- frontend-nodemod:/opt/app-root/frontend/node_modules
|
||||
- ./services/frontend/:/opt/app-root/frontend
|
||||
- ./services/shared/:/opt/app-root/shared
|
||||
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
container_name: kone-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/kone-db
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- kone-db-data:/data
|
||||
|
||||
redis:
|
||||
<<: *defaults
|
||||
command: 'redis-server --appendonly yes'
|
||||
container_name: redis
|
||||
image: redis:7.2.6
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
sis-db:
|
||||
<<: *defaults
|
||||
container_name: sis-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-db
|
||||
volumes:
|
||||
- sis-db-data:/data
|
||||
shm_size: 2gb
|
||||
ports:
|
||||
- 12345:5432
|
||||
|
||||
sis-importer-db:
|
||||
<<: *defaults
|
||||
container_name: sis-importer-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-importer-db
|
||||
profiles:
|
||||
- updater
|
||||
volumes:
|
||||
- sis-importer-db-data:/data
|
||||
ports:
|
||||
- 12346:5432
|
||||
|
||||
sis-updater-scheduler:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
context: updater/sis-updater-scheduler
|
||||
container_name: sis-updater-scheduler
|
||||
depends_on:
|
||||
- redis
|
||||
- sis-importer-db
|
||||
environment:
|
||||
REDIS_HOST: redis
|
||||
SECRET_TOKEN: dev
|
||||
SIS_IMPORTER_DATABASE: sis-importer-db
|
||||
SIS_IMPORTER_HOST: sis-importer-db
|
||||
SIS_IMPORTER_PASSWORD: postgres
|
||||
SIS_IMPORTER_USER: postgres
|
||||
ports:
|
||||
- 8082:8082
|
||||
profiles:
|
||||
- updater
|
||||
volumes:
|
||||
- sis-updater-scheduler-nodemod:/opt/app-root/src/node_modules
|
||||
- ./updater/sis-updater-scheduler/:/opt/app-root/src
|
||||
|
||||
sis-updater-worker:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
context: updater/sis-updater-worker
|
||||
container_name: sis-updater-worker
|
||||
depends_on:
|
||||
- redis
|
||||
- sis-db
|
||||
- sis-importer-db
|
||||
environment:
|
||||
DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
REDIS_HOST: redis
|
||||
SIS_IMPORTER_DATABASE: sis-importer-db
|
||||
SIS_IMPORTER_HOST: sis-importer-db
|
||||
SIS_IMPORTER_PASSWORD: postgres
|
||||
SIS_IMPORTER_USER: postgres
|
||||
SIS_PASSWORD: postgres
|
||||
profiles:
|
||||
- updater
|
||||
volumes:
|
||||
- sis-updater-worker-nodemod:/opt/app-root/src/node_modules
|
||||
- ./updater/sis-updater-worker/:/opt/app-root/src
|
||||
|
||||
user-db:
|
||||
<<: *defaults
|
||||
container_name: user-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/user-db
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- user-db-data:/data
|
||||
|
||||
jami:
|
||||
<<: *defaults
|
||||
image: quay.io/toska/jami:production
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- npm_config_cache=/tmp
|
||||
- PORT=3003
|
||||
- NODE_ENV=dev
|
||||
- DATABASE_URL=postgres://postgres:postgres@jami-db:5432/jami-db-real
|
||||
ports:
|
||||
- 3003:3003
|
||||
container_name: jami
|
||||
|
||||
jami-db:
|
||||
<<: *defaults
|
||||
image: postgres:15.10
|
||||
environment:
|
||||
- PGDATA=/data
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
container_name: jami-db
|
||||
ports:
|
||||
- 5433:5432
|
||||
volumes:
|
||||
- jami-data:/data
|
||||
|
||||
volumes:
|
||||
backend-nodemod:
|
||||
frontend-nodemod:
|
||||
kone-db-data:
|
||||
redis-data:
|
||||
sis-db-data:
|
||||
sis-importer-db-data:
|
||||
sis-updater-scheduler-nodemod:
|
||||
sis-updater-worker-nodemod:
|
||||
user-db-data:
|
||||
jami-data:
|
||||
@@ -1,3 +0,0 @@
|
||||
# Documentation
|
||||
|
||||
Oodikone public documentation. For internal documentation, see Toska GitLab.
|
||||
@@ -1,72 +0,0 @@
|
||||
# Access rights in Oodikone
|
||||
|
||||
## Roles
|
||||
|
||||
Roles are defined [here](../services/backend/src/config/roles.ts). Criteria for each role can be found [here](../services/backend/src/services/userService.ts). Currently these roles exist:
|
||||
|
||||
- admin
|
||||
- courseStatistics
|
||||
- facultyStatistics
|
||||
- fullSisuAccess
|
||||
- openUniSearch
|
||||
- studyGuidanceGroups
|
||||
- teachers
|
||||
|
||||
The roles `facultyStatistics`, `openUniSearch`, and `studyGuidanceGroups` simply enable certain views (_Faculties_, _Open uni student population_, and _Guidance groups_ respectively).
|
||||
|
||||
### courseStatistics
|
||||
|
||||
- Can see statistics of every course but year statistics of ≤ 5 students are hidden
|
||||
- If the user has [full rights](#programmes) to **any** programme, they can also see exact statistics even if there are ≤ 5 students (the same behavior as with `admin` or `fullSisuAccess` roles)
|
||||
|
||||
### teachers
|
||||
|
||||
- By default, the role only allows seeing the statistics of course providers (degree programmes) they have full rights to (see [Programmes](#programmes))
|
||||
- This role always needs to be given and removed manually
|
||||
- If the user is a member of the `hy-dekaanit` or `hy-varadekaanit-opetus` IAM group (or is an admin), they can see leaderboards, information about individual teachers, and statistics of all course providers. They don't need to be given the `teachers` role manually.
|
||||
|
||||
### Other roles
|
||||
|
||||
| | Everybody\* | Admin | fullSisuAccess |
|
||||
| -------------------- | :---------: | :---: | :------------: |
|
||||
| Front page | ✅ | ✅ | ✅ |
|
||||
| Feedback | ✅ | ✅ | ✅ |
|
||||
| University | ✅ | ✅ | ✅ |
|
||||
| Language center view | | ✅ | |
|
||||
| Faculties | | ✅ | ✅ |
|
||||
| Updater | | ✅ | |
|
||||
| Users | | ✅ | |
|
||||
| Programmes | | ✅ | ✅ |
|
||||
| Students | | ✅ | ✅ |
|
||||
| Teachers | | ✅ | |
|
||||
| Guidance groups | | | |
|
||||
| Custom population | | ✅ | ✅ |
|
||||
| Completed courses | ✅ | ✅ | ✅ |
|
||||
| Close to graduation | | ✅ | ✅ |
|
||||
| Open uni population | | ✅ | |
|
||||
| Courses | | ✅ | ✅ |
|
||||
|
||||
\* = anyone who can log in to Oodikone, check the [IAM groups](#iam-groups) section for more information
|
||||
|
||||
## Programmes
|
||||
|
||||
There are two types of programme rights: limited and full. Limited rights mean that the user cannot see individual students of those programmes, while a user with full rights can.
|
||||
|
||||
Manually given programme rights are **always** full rights. Some of the programme rights are based on user's IAM groups: some only qualify for limited rights, some for full rights.
|
||||
|
||||
## IAM groups
|
||||
|
||||
A user must be a member of either the `grp-oodikone-basic-users` or `grp-oodikone-users` IAM group to be able to log in to Oodikone. The following IAM groups are used to give roles:
|
||||
|
||||
| IAM group | Role given based on the group |
|
||||
| ------------------------ | ----------------------------- |
|
||||
| grp-oodikone-basic-users | courseStatistics |
|
||||
| grp-oodikone-users | facultyStatistics |
|
||||
| grp-toska | admin |
|
||||
| hy-ypa-opa-dojo | openUniSearch |
|
||||
|
||||
`grp-oodikone-basic-users`: basically the whole staff (hy-\*-allstaff)
|
||||
|
||||
`grp-oodikone-users`: more limited (e.g. _koulutussuunnittelijat_, _ospa_, _opintoasiainpäälliköt_, _(vara)dekaanit_, _johtoryhmä_)
|
||||
|
||||
`grp-kielikeskus-esihenkilot`: gives access to the language center view
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 71 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB |
@@ -1,96 +0,0 @@
|
||||
# Combined programmes
|
||||
|
||||
At the moment only programmes KH90_001 and MH90_001 have a combined view.
|
||||
|
||||
The made changes do not have any effects on faculty or trends views. **Needs manual work** is added into those parts that needs visit, if this functionality is added to other programmes.
|
||||
|
||||
Basic rule of thumb is that for the master/licentiate programme I used names `combinedProgramme` and in some rare cases `secondProgramme` as whole or part of the variable names.
|
||||
|
||||
## Degree programme
|
||||
|
||||
**Needs manual work** in the function `getCombinedProgrammeId` in `services/frontend/src/common/index.js` if new programmes is added. For showing the both programme data correctly, the filtering mechanism is changed to add both programmes and it is not caring for which programme user has acual rights or IAMrights. However, the links for the class stats level are hidden in studytrack tables, if the user has no rights for the programme.
|
||||
|
||||
- Search by class
|
||||
- **Needs manual work** Combine programmes here
|
||||
- Overview
|
||||
- **Needs manual work**
|
||||
|
||||
**Redis nightly degree programme update**
|
||||
|
||||
- Updates automatically combined programme stats. **Needs manual work** here `services/backend/src/services/studyprogrammeHelpers.js` in the constant `combinedStudyprogrammes`. This might needs refactoring if bachelor programme can have many separate master programmes. Then remember to change function `refreshNewOverview` in `services/backend/src/events.js` accordingly to handle lists.
|
||||
|
||||
**NOTE:** `combinedStudyprogrammes` is also used to fetch course modules (mandatory courses). If the logic of this constant is changed in `studyProgrammeHelpers.js` -> the needed changes must be done also here `services/backend/src/services/programmeModules.js`. While writing this, the cleverer idea of doing this might have been to send `code` in form `KHxx_xxx+MHxx_xxx` and do the splitting into two codes in the backend as done in Study guidance groups logic. Changes were done here 'services/backend/src/routes/population.js', acual place is near `getFilteredAssociations` function.
|
||||
|
||||
**Basic information**
|
||||
|
||||
All things listed below should work automatically, if new combinations are done.
|
||||
|
||||
- Credits produced by the studyprogramme
|
||||
- credits are computed for both programmes separately
|
||||
- Average graduation times
|
||||
- For bachelor + master/licentiate studyright the time is the whole study time starting from bachelor
|
||||
- _Students of the programme_ and _Graduated and thesis writers of the programme_
|
||||
- separate fields for started, graduated and thesis writers
|
||||
|
||||
**Studytracks and class statistics**
|
||||
|
||||
- _Students of the studyprogramme by starting year_
|
||||
- Graduated field separated for both programmes
|
||||
- All = currently enrolled + absent + passive + graduated licentiate
|
||||
- _Progress of students of the studyprogramme by starting year_
|
||||
- computed for whole bachelor + master/licentiate studyright period
|
||||
- _Average graduation times_
|
||||
- bachelor + master/licentiate graph: start is bachelor studyright start
|
||||
|
||||
**Programme courses**
|
||||
|
||||
- Fetch all courses for both programmes and show them in one table.
|
||||
|
||||
**Degree courses**
|
||||
|
||||
- Possibility to add criteria for whole bachelor-master study time
|
||||
- For eläinlääkis this criteria is saved by bachelor programme code. Other cases may need other approaches.
|
||||
In eläinlääkis this done by adding the criteria for 6 years -> the whole study time criteria is saved with the bachelor programme code. That is, same criteria is found, if bachelor or combined programme view is shown. This approach might need extra thinking for different programmes.
|
||||
- Show own tables for both programmes
|
||||
- To fetch data **needs manual work** here `services/backend/src/services/studyprogrammeHelpers.js` in the constant `combinedStudyprogrammes`.
|
||||
|
||||
**Tags**
|
||||
|
||||
- Possibility to add tags for combined programme.
|
||||
|
||||
## Class statistics view
|
||||
|
||||
**Credit accumulation**
|
||||
|
||||
- Show graduations for both programmes
|
||||
|
||||
**Credit statistics**
|
||||
|
||||
- Bachelor and bachelor+master combo have own graphs.
|
||||
|
||||
**Courses of class**
|
||||
|
||||
- Show courses for both programmes
|
||||
|
||||
**Students**
|
||||
|
||||
- **General tab** - Additional fields for graduation and hops
|
||||
- **Courses tab** - show all courses for both programmes. The courses are fetched with same logic as for Degree programmes.
|
||||
- **Tags tab** - show tabs related to this combination.
|
||||
- **Progress tab** - possibility to have progress for whole bachelor-master study time. **See Degree courses approach** to add criteria. **May need future manual work**, depending on the programme id with which the data is saved on the database.
|
||||
|
||||
## Filters
|
||||
|
||||
- **Hops (study plan)** - possibility filter out credits separate for both programmes or for both at the same time
|
||||
|
||||
- **Graduated from programme** - possibility to filter out students based on graduation either in bachelor's or master's.
|
||||
|
||||
- **Studyright status** - active or passive based on the chosen degree
|
||||
|
||||
- **Studytrack filter** (needs programme code) is not touched at the moment, thus **may need future manual work**.
|
||||
|
||||
## Study guidance groups
|
||||
|
||||
- Backend: student data is fetched by bachelor degree programme code in `/v3/populationstatisticsbystudentnumbers`. Separation from code 'KHxx_xxx+MHxx_xxx' is done in backend.
|
||||
- Frontend: things are done in the same way as with class stats.
|
||||
- **Need manual work**: `./oodikone/services/frontend/src/redux/elementdetails.js` add combined programme codes (both bachelor and master) to `combinedProgrammeCodes` and modify `combinedOptions` list accordingly in `useFilterAndFormattedElementDetails()` function.
|
||||
@@ -1,78 +0,0 @@
|
||||
# Databases
|
||||
|
||||
Oodikone uses three main PostgreSQL databases, each for a different purpose. This document contains a simplified overview of each database and the relations between models.
|
||||
|
||||
Use Adminer or refer to the actual database for a detailed view of each model and their attributes.
|
||||
|
||||
## Storage
|
||||
|
||||
### sis-db
|
||||
|
||||
The `sis-db` database contains information originating from Sisu.
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: sis-db
|
||||
---
|
||||
erDiagram
|
||||
enrollment }o--|| course : ""
|
||||
enrollment }o--|| semesters : ""
|
||||
enrollment }o--|| student : ""
|
||||
credit_teachers }o--|| teacher : ""
|
||||
course_types
|
||||
curriculum_periods
|
||||
credit_teachers }o--|| credit : ""
|
||||
credit }o--|| course : ""
|
||||
credit }o--|| semesters : ""
|
||||
credit }o--|| credit_types : ""
|
||||
credit }o--|| student : ""
|
||||
studyplan }o--|| student : ""
|
||||
studyplan }o--|| sis_study_rights : ""
|
||||
sis_study_rights }o--|| studyright_extents : ""
|
||||
sis_study_rights }o--|| organization : ""
|
||||
sis_study_rights }o--|| student : ""
|
||||
sis_study_right_elements }o--|| sis_study_rights : ""
|
||||
course_providers }o--|| organization : ""
|
||||
course_providers }o--|| course : ""
|
||||
programme_module_children }o--|| programme_modules : "references parent"
|
||||
programme_module_children }o--|| programme_modules : "references child"
|
||||
programme_modules }o--o| organization : "belong to"
|
||||
```
|
||||
|
||||
### kone-db
|
||||
|
||||
The `kone-db` database contains information used in various features of Oodikone. This functionality is native to Oodikone and not necessarily directly related to Sisu.
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: kone-db
|
||||
---
|
||||
erDiagram
|
||||
custom_population_searches
|
||||
excluded_courses
|
||||
open_uni_population_searches
|
||||
progress_criteria
|
||||
study_guidance_group_tags
|
||||
degree_programme_pins
|
||||
tag ||--o{ tag_student : "belongs to"
|
||||
tag
|
||||
tag_student
|
||||
```
|
||||
|
||||
### user-db
|
||||
|
||||
The `user-db` database contains information about users of Oodikone.
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: user-db
|
||||
---
|
||||
erDiagram
|
||||
users
|
||||
```
|
||||
|
||||
## Caching
|
||||
|
||||
### Redis
|
||||
|
||||
Redis is used in the backend to cache calculation results.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 165 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 106 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 72 KiB |
@@ -1,127 +0,0 @@
|
||||
# How to update the database schema
|
||||
|
||||
These instructions explain how to update the schemas for the following databases used by Oodikone:
|
||||
|
||||
- `sis-db`
|
||||
- `user-db`
|
||||
- `kone-db`
|
||||
|
||||
The steps to **add, remove, or modify columns** are mostly the same for each database, but the file paths and container names differ. Always **double-check** that you have the correct function name, column name, data type, and other relevant details in your migration file.
|
||||
|
||||
> [!NOTE]
|
||||
> If you need to change the `sis-importer-db` schema, see the instructions in the [sis-importer repo](https://github.com/UniversityOfHelsinkiCS/sis-importer/blob/master/README.md#tricks--tips).
|
||||
|
||||
## General guidelines
|
||||
|
||||
### File names
|
||||
|
||||
- Name new migration files using the format `YYYYMMDD_XX_description.js`.
|
||||
- The `XX` part is a migration number (starting from `00` each day).
|
||||
- Example: `20250122_00_add_column_to_table.js`.
|
||||
|
||||
### Model vs. column names
|
||||
|
||||
- Use **camelCase** in your model definitions.
|
||||
- Use **snake_case** in the actual database columns.
|
||||
- If a model has `underscored: true`, Sequelize automatically converts camelCase to snake_case in all queries. Otherwise, specify the column name explicitly.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
|
||||
|
||||
### Verifying the migration locally
|
||||
|
||||
- Migrations are run automatically whenever the server restarts.
|
||||
- After creating a migration, ensure the relevant service is running:
|
||||
- For `sis-db`: `npm run both:real` or `npm run updater:real`
|
||||
- For `user-db`/`kone-db`: `npm run both:real` or `npm run oodikone:real`
|
||||
- **Connect to the database container** and check:
|
||||
|
||||
```bash
|
||||
docker exec -it <container-name> psql -U postgres <database-name>-real
|
||||
```
|
||||
|
||||
Replace `<container-name>` and `<database-name>` with the actual values (e.g., `sis-db`, `user-db`, or `kone-db`).
|
||||
|
||||
- In the psql prompt, verify your migration:
|
||||
|
||||
```sql
|
||||
SELECT * FROM migrations;
|
||||
```
|
||||
|
||||
Also confirm that the new (or updated) column exists in the appropriate table and has the correct type.
|
||||
|
||||
### Reverting a migration locally
|
||||
|
||||
If you need to **revert a migration locally**, follow these steps:
|
||||
|
||||
1. Locate the `migrator.up()` function in the code:
|
||||
- For `user-db` and `kone-db`: [`services/backend/src/database/connection.ts`](../services/backend/src/database/connection.ts)
|
||||
- For `sis-db`: [`updater/sis-updater-worker/src/db/connection.js`](../updater/sis-updater-worker/src/db/connection.js)
|
||||
1. Change `migrator.up()` to `migrator.down()` and save the file. The latest migration will be reverted automatically when the server reloads.
|
||||
1. **Important:** After the migration is reverted, change `migrator.down()` back to `migrator.up()` to ensure future migrations run correctly.
|
||||
1. For more instructions, see the [umzug documentation](https://github.com/sequelize/umzug?tab=readme-ov-file#usage).
|
||||
|
||||
### Deploying changes to production
|
||||
|
||||
- **Push changes** to the `master` branch and wait for the workflow to finish.
|
||||
- For **`sis-db`** migrations (in the updater):
|
||||
- You **do not** need to create a new release. Once tests pass, changes to the updater go directly to production.
|
||||
- For **`user-db`** and **`kone-db`** migrations (in the backend):
|
||||
- A new release must be made before the changes go to production.
|
||||
- After deployment, check logs (for example, in OpenShift) to confirm the migration ran successfully.
|
||||
- Optionally, use the `db-cli` tool to inspect the production database. See `how_to_db_cli.md` (in GitLab) for instructions.
|
||||
|
||||
## `sis-db`
|
||||
|
||||
> [!NOTE]
|
||||
> These instructions assume that needed data is already in the `sis-importer-db`. If not, you must add the data there first. See the [sis-importer repo](https://github.com/UniversityOfHelsinkiCS/sis-importer/blob/master/README.md#tricks--tips) for more details.
|
||||
|
||||
1. **Update the updater model:**
|
||||
|
||||
- Go to `updater/sis-updater-worker/src/db/models/` and choose the model you want to change.
|
||||
- Add or modify the column (with its **type** and any other necessary fields).
|
||||
- Check the correct data type in the importer.
|
||||
- Use camelCase in the model, snake_case in the database.
|
||||
|
||||
1. **Update the backend model:**
|
||||
|
||||
- Go to `services/backend/src/models/`.
|
||||
- The models are in different formats (JavaScript in the updater, TypeScript in the backend), but follow existing examples.
|
||||
|
||||
1. **Create a migration file:**
|
||||
|
||||
- Go to `updater/sis-updater-worker/src/db/migrations/`.
|
||||
- Name your file using the format described above (e.g. `20250122_00_add_column_to_table.js`).
|
||||
- **Remember**: The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
|
||||
|
||||
1. **Run and verify the migration:**
|
||||
|
||||
- Ensure that the updater is running (`npm run both:real` or `npm run updater:real`).
|
||||
- Use the steps in [General guidelines](#general-guidelines) to verify.
|
||||
|
||||
1. **Push to master and deploy:**
|
||||
- Your migration will go to production automatically after tests pass.
|
||||
- Check logs in production (OpenShift) or use `db-cli` to confirm success.
|
||||
|
||||
## `user-db` and `kone-db`
|
||||
|
||||
1. **Update the model:**
|
||||
|
||||
- Go to `services/backend/src/models/kone` (for `kone-db`) or `services/backend/src/models/user.ts` (the only model for user-db)
|
||||
- Add or modify the column, type, and any other needed fields. Follow existing models for structure.
|
||||
|
||||
1. **Create a migration file:**
|
||||
|
||||
- Go to `services/backend/src/database/`.
|
||||
- Use `migrations_kone` (for `kone-db`) or `migrations_user` (for `user-db`).
|
||||
- Name the file using the same `YYYYMMDD_XX_description.js` format.
|
||||
- **Remember**: The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
|
||||
|
||||
1. **Run and verify the migration:**
|
||||
|
||||
- Make sure Oodikone is running (`npm run both:real` or `npm run oodikone:real`).
|
||||
- Follow the same verification steps as in [General Guidelines](#general-guidelines).
|
||||
|
||||
1. **Push to master and release:**
|
||||
- Wait for the CI workflow to pass.
|
||||
- **Create a new release**; then check the production logs (in OpenShift) to confirm the migrationor use `db-cli`.
|
||||
@@ -1,84 +0,0 @@
|
||||
# Numeric types
|
||||
|
||||
Data used in Oodikone contains some numeric types (or _magic numbers_) that are not self-explanatory. The values of the numeric types are gathered here for convenience and quick reference.
|
||||
|
||||
## Extent code
|
||||
|
||||
Appears in model SISStudyRight (db: `sis_study_rights`) as _extentCode_ (db: `extent_code`). Values are defined in the database table `studyright_extents`.
|
||||
|
||||
| value | meaning |
|
||||
| ----: | :-------------------------------------------------- |
|
||||
| 1 | Bachelor's degree |
|
||||
| 2 | Master's degree |
|
||||
| 3 | Licentiate |
|
||||
| 4 | Doctor |
|
||||
| 5 | Bachelor's and Master's degree |
|
||||
| 6 | Continuing education |
|
||||
| 7 | Exchange studies |
|
||||
| 9 | Open University studies |
|
||||
| 13 | Non-degree pedagogical studies for teachers |
|
||||
| 14 | Contract training |
|
||||
| 16 | Studies for secondary school students |
|
||||
| 18 | Specialisation studies |
|
||||
| 22 | Non-degree programme for special education teachers |
|
||||
| 23 | Specialist training in medicine and dentistry |
|
||||
| 31 | Summer and winter school |
|
||||
| 34 | Exchange studies (postgraduate studies) |
|
||||
| 99 | Non-Degree studies |
|
||||
|
||||
## Phase
|
||||
|
||||
Appears in model SISStudyRightElement (db: `sis_study_right_elements`) as _phase_ (db: `phase`).
|
||||
|
||||
In Finland it is common for new students to be awarded the study right to both a bachelor's and a master's programme when they are admitted to the university. In Oodikone these two-phase study rights are marked with the [extent code](#extent-code) 5. In the study right elements of these study rights, phase 2 marks the second phase of the study right.
|
||||
|
||||
For example: Student is awarded the study right in the bachelor's programme in computer science with the extent code 5. The study right element for study right in the bachelor's programme is marked with phase 1. After graduating from the bachelor's programme, the student continues their studies in the master's programme in computer science. The new study right element for the study right in the master's programme is marked with phase 2.
|
||||
|
||||
All study right elements for other study rights (extent code other than 5) are marked with phase 1.
|
||||
|
||||
The level of the programme (bachelor's, master's etc.) of the study right can be directly checked from degreeProgrammeType (db: `degree_programme_type`) in model SISStudyRightElement (db: `sis_study_right_elements`).
|
||||
|
||||
| value | meaning |
|
||||
| ----: | :------- |
|
||||
| 1 | any |
|
||||
| 2 | master's |
|
||||
|
||||
## Enrollment type
|
||||
|
||||
This information is used in the model SISStudyRight (db: `sis_study_rights`), in the field _semesterEnrollments_ (db: `semester_enrollments`).
|
||||
|
||||
The `semester_enrollments` column is an array of JSON objects. Each object contains:
|
||||
|
||||
- `type` (the type of enrollment),
|
||||
- `semester` (the code for a semester),
|
||||
- and sometimes `statutoryAbsence` (this field is included only when the enrollment type is 2).
|
||||
|
||||
The `type` field represents one of the following enrollment types. The `semester` field refers to a semester code, defined as `semestercode` in the model Semester.
|
||||
|
||||
| value | meaning | notes |
|
||||
| ----: | :------ | :------------------------------- |
|
||||
| 1 | Present | |
|
||||
| 2 | Absent | |
|
||||
| 3 | Passive | neither 1 or 2 has been declared |
|
||||
|
||||
## Credit type code
|
||||
|
||||
Appears in model Credit (db: `credit`) as _credittypecode_ (db: `credittypecode`). Values are defined in the database table `credit_types`.
|
||||
|
||||
| value | meaning |
|
||||
| ----: | :--------------- |
|
||||
| 4 | Completed |
|
||||
| 7 | Improved (grade) |
|
||||
| 9 | Transferred |
|
||||
| 10 | Failed |
|
||||
|
||||
## Gender code
|
||||
|
||||
Appears in model Student (db: `student`) as _gender_code_ (db: `gender_code`).
|
||||
|
||||
| value | meaning |
|
||||
| ----: | :------ |
|
||||
| 0 | Unknown |
|
||||
| 1 | Male |
|
||||
| 2 | Female |
|
||||
| 3 | Other |
|
||||
@@ -1,51 +0,0 @@
|
||||
# Resources
|
||||
|
||||
A collection of useful tools, documentation and other resources.
|
||||
|
||||
## Code style and maintainability
|
||||
|
||||
- [ESLint documentation](https://eslint.org/docs/v8.x)
|
||||
- [Prettier documentation](https://prettier.io/docs/en)
|
||||
- [qlty.sh](https://qlty.sh/gh/UniversityOfHelsinkiCS/projects/oodikone)
|
||||
- Qlty.sh replaced Code climate which was introduced by Riku
|
||||
- The usefulness of qlty.sh is sometimes questionable
|
||||
|
||||
## Database
|
||||
|
||||
- [pgcli](https://www.pgcli.com)
|
||||
- A useful commandline tool to interact with the database
|
||||
- Can be installed from some package managers or optionally via `$ pip install pgcli`
|
||||
- [Postgres documentation](https://www.postgresql.org/docs/15/index.html)
|
||||
- [Sequelize documentation](https://sequelize.org/docs/v6/)
|
||||
|
||||
## GitHub
|
||||
|
||||
- [GitHub documentation](https://docs.github.com/en)
|
||||
- [GitHub Actions](https://docs.github.com/en/actions)
|
||||
- [Mermaid.js documentation](https://mermaid.js.org/intro/syntax-reference.html)
|
||||
|
||||
## TypeScript
|
||||
|
||||
- [TypeScript documentation](https://www.typescriptlang.org/docs)
|
||||
- [`tsconfig` reference](https://www.typescriptlang.org/tsconfig)
|
||||
- [TypeScript instructions for Sequelize](https://sequelize.org/docs/v6/other-topics/typescript)
|
||||
- [`sequelize-typescript` instructions](https://github.com/sequelize/sequelize-typescript?tab=readme-ov-file#readme)
|
||||
|
||||
## User interface
|
||||
|
||||
- [Material UI documentation](https://mui.com/material-ui/getting-started/)
|
||||
- [Material Icons](https://mui.com/material-ui/material-icons/)
|
||||
- [MUI X Date pickers](https://mui.com/x/react-date-pickers/)
|
||||
- [Material React Table documentation](https://www.material-react-table.com/docs)
|
||||
|
||||
<!--
|
||||
|
||||
## Visual Studio Code
|
||||
|
||||
_Todo: add relevant plugins_
|
||||
|
||||
-->
|
||||
|
||||
## Web technologies
|
||||
|
||||
- [MDN Web Docs](https://developer.mozilla.org/en-US/)
|
||||
@@ -1,3 +0,0 @@
|
||||
# Tietosuoja
|
||||
|
||||
Oodikoneessa noudatetaan Helsingin yliopiston [yleistä linjausta](https://www.helsinki.fi/fi/tutustu-meihin/tietosuoja#opiskelijoiden-henkil-tietojen-k-sittely--title) opiskelijoiden henkilötietojen käsittelystä.
|
||||
@@ -1,274 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
import globals from 'globals'
|
||||
import eslintPluginCypress from 'eslint-plugin-cypress'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import eslintPluginReact from 'eslint-plugin-react'
|
||||
import eslintPluginReactHooks from 'eslint-plugin-react-hooks'
|
||||
import eslintPluginImportX from 'eslint-plugin-import-x'
|
||||
import { createNodeResolver } from 'eslint-plugin-import-x'
|
||||
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
// Global ignores
|
||||
ignores: [
|
||||
'eslint.config.mjs',
|
||||
'**/build/**',
|
||||
'**/dist/**',
|
||||
'**/node_modules/**',
|
||||
'**/instrumented/**',
|
||||
'**/coverage/**',
|
||||
'nyc-config.cjs',
|
||||
'.lintstagedrc.mjs',
|
||||
'cypress.config.js',
|
||||
]
|
||||
},
|
||||
|
||||
// Global options (excl. e2e)
|
||||
{
|
||||
ignores: ['cypress/**/*.js'],
|
||||
extends: [
|
||||
eslint.configs.recommended,
|
||||
eslintPluginImportX.flatConfigs.recommended,
|
||||
eslintPluginImportX.flatConfigs.typescript,
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2021,
|
||||
sourceType: 'module',
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
projectService: {
|
||||
allowDefaultProject: ['*.{js,cjs}']
|
||||
}
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'import-x': eslintPluginImportX
|
||||
},
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: 'error',
|
||||
},
|
||||
rules: {
|
||||
'camelcase': 'off', // TODO: Enable. old db/backend logic is still using snake_case every now and then and needs a big refactor
|
||||
'class-methods-use-this': 'off', // Disabled in favor of @typescript-eslint
|
||||
'consistent-return': 'off', // Disabled in favor of @typescript-eslint
|
||||
'id-denylist': ['error', 'err'],
|
||||
'no-alert': 'error',
|
||||
'no-await-in-loop': 'error',
|
||||
'no-console': 'error',
|
||||
'no-implied-eval': 'off', // Disabled in favor of @typescript-eslint
|
||||
'no-param-reassign': 'error',
|
||||
'no-promise-executor-return': 'error',
|
||||
'no-return-assign': 'error',
|
||||
'no-unused-vars': 'off', // Disabled in favor of @typescript-eslint
|
||||
'object-shorthand': ['error', 'always'],
|
||||
'prefer-const': 'error',
|
||||
'prefer-destructuring': 'off', // Disabled in favor of @typescript-eslint
|
||||
'dot-notation': 'off', // Disabled in favor of @typescript-eslint
|
||||
'no-restricted-imports': ['error',
|
||||
{
|
||||
'patterns':
|
||||
[
|
||||
{ 'regex': '^@mui/[^/]+$', 'message': 'Import only the needed module. This has no effect in prod due to tree-shake, but speeds up development builds.' },
|
||||
{ 'regex': '^src/.*', 'message': 'Root imports will not resolve in production build (instead silently crash).' }
|
||||
]
|
||||
}],
|
||||
'quotes': ['error', 'single',
|
||||
{ avoidEscape: true, allowTemplateLiterals: false }
|
||||
],
|
||||
|
||||
'import-x/no-commonjs': 'error',
|
||||
'import-x/no-default-export': 'error',
|
||||
'import-x/no-extraneous-dependencies': ['error', { devDependencies: false }],
|
||||
'import-x/no-unused-modules': ['error',
|
||||
{
|
||||
missingExports: true,
|
||||
unusedExports: true,
|
||||
src: ['{services,updater}/**/*.{js,jsx,ts,tsx}']
|
||||
}],
|
||||
'import-x/order': ['error', {
|
||||
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||
groups: [['builtin', 'external'], ['internal'], ['parent'], ['sibling', 'index']],
|
||||
}],
|
||||
|
||||
// TypeScript
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', caughtErrors: 'none', ignoreRestSiblings: true, varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/array-type': 'off',
|
||||
'@typescript-eslint/class-methods-use-this': 'error',
|
||||
// '@typescript-eslint/consistent-indexed-object-style': 'off', // TODO: keep as error/record (default)
|
||||
'@typescript-eslint/consistent-return': 'error', // NOTE: This is weaker than tsconfig.json noImplicitReturns, and should be replaced once ts migration complete
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
'@typescript-eslint/dot-notation': 'error',
|
||||
'@typescript-eslint/prefer-destructuring': ['error',
|
||||
{ VariableDeclarator: { object: true } }
|
||||
],
|
||||
'@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }],
|
||||
'@typescript-eslint/no-explicit-any': 'off', // TODO: enable when feeling brave
|
||||
'@typescript-eslint/no-implied-eval': 'error',
|
||||
'@typescript-eslint/no-shadow': 'off', // TODO: enable?
|
||||
'@typescript-eslint/no-unsafe-argument': 'off', // TODO: enable
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off', // TODO: enable
|
||||
'@typescript-eslint/no-unsafe-call': 'off', // TODO: enable
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off', // TODO: enable
|
||||
'@typescript-eslint/no-unsafe-return': 'off', // TODO: enable
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
},
|
||||
settings: {
|
||||
'import-x/internal-regex': '^@oodikone/shared/',
|
||||
'import-x/resolver-next': [
|
||||
createTypeScriptImportResolver({
|
||||
project: [
|
||||
'services/frontend/tsconfig.json',
|
||||
'services/frontend/tsconfig.node.json',
|
||||
'services/backend/tsconfig.json',
|
||||
'services/shared/tsconfig.json'
|
||||
],
|
||||
}),
|
||||
createNodeResolver({
|
||||
extensions: ['.js', '.jsx', 'cjs', 'mjs', '.ts', '.tsx'],
|
||||
})
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ['services/backend/src/routes/*.{ts,js}'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-misused-promises': 'off', // Errors come mainly from Express route handlers that are handled correctly by express-async-errors
|
||||
'@typescript-eslint/consistent-return': 'off',
|
||||
}
|
||||
},
|
||||
|
||||
// Disable type-aware linting for javascript files
|
||||
{
|
||||
files: ['{updater,services}/**/*.{js,cjs}'],
|
||||
extends: [tseslint.configs.disableTypeChecked]
|
||||
},
|
||||
|
||||
// Frontend camelCase and react
|
||||
{
|
||||
files: ['services/frontend/**/*.{js,jsx,mjs,cjs,ts,tsx}'],
|
||||
plugins: {
|
||||
'react': eslintPluginReact,
|
||||
},
|
||||
extends: [
|
||||
eslintPluginReact.configs.flat.recommended,
|
||||
eslintPluginReact.configs.flat['jsx-runtime'],
|
||||
eslintPluginReactHooks.configs['recommended-latest'],
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'camelcase': 'warn',
|
||||
'react/prop-types': 'off', // Typescript but worse
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react/function-component-definition': ['error',
|
||||
{ namedComponents: 'arrow-function', unnamedComponents: 'arrow-function' },
|
||||
],
|
||||
'react/no-array-index-key': 'error',
|
||||
'react/no-unstable-nested-components': 'warn', // TODO: turn into error, reduces re-renders
|
||||
'react/no-this-in-sfc': 'error',
|
||||
'react/prefer-stateless-function': 'error',
|
||||
'react/jsx-boolean-value': ['error', 'never'],
|
||||
'react/jsx-filename-extension': ['error',
|
||||
{ allow: 'as-needed', extensions: ['.jsx', '.tsx'] }
|
||||
],
|
||||
'react/jsx-no-leaked-render': 'warn', // TODO: enable, tho not that useful after full ts migration
|
||||
'react/jsx-no-useless-fragment': 'error',
|
||||
'react/jsx-props-no-spreading': 'warn', // TODO: enable, improves readability
|
||||
'react/jsx-sort-props': ['error', { reservedFirst: false }],
|
||||
'react/jsx-tag-spacing': 'warn',
|
||||
},
|
||||
settings: {
|
||||
'react': {
|
||||
'version': 'detect'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Cypress should only use rules provided by the plugin below
|
||||
{
|
||||
files: ['cypress/**/*.js'],
|
||||
extends: [
|
||||
eslintPluginCypress.configs.recommended
|
||||
]
|
||||
},
|
||||
|
||||
// Override for backend api tests
|
||||
{
|
||||
files: [
|
||||
"services/backend/tests**/*.ts"
|
||||
],
|
||||
rules: {
|
||||
"import-x/no-extraneous-dependencies": "off",
|
||||
'no-console': 'off',
|
||||
}
|
||||
},
|
||||
|
||||
// Overrides for CommonJS
|
||||
{
|
||||
files: [
|
||||
'services/backend/**/*.js',
|
||||
'updater/sis-updater-worker/src/db/migrations/*.cjs',
|
||||
'updater/sis-updater-scheduler/**/*.js'
|
||||
],
|
||||
languageOptions: {
|
||||
sourceType: 'commonjs',
|
||||
},
|
||||
rules: {
|
||||
'import-x/no-commonjs': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Rapodiff
|
||||
{
|
||||
files: ['services/backend/src/rapodiff/**/*'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Migration files
|
||||
{
|
||||
files: [
|
||||
'updater/sis-updater-worker/src/db/migrations/*.cjs',
|
||||
'services/backend/src/database/**/*.js'
|
||||
],
|
||||
rules: {
|
||||
'import-x/no-unused-modules': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: See if these can be disabled and or fix
|
||||
{
|
||||
files: ['services/backend/**/*.{js,ts}', 'updater/**/*.js'],
|
||||
rules: {
|
||||
'import-x/no-default-export': 'off',
|
||||
'import-x/no-unused-modules': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
'no-promise-executor-return': 'off',
|
||||
},
|
||||
},
|
||||
eslintPluginPrettierRecommended, // This must be the last plugin so it can override other configs
|
||||
)
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
events { worker_connections 1024; }
|
||||
|
||||
http {
|
||||
|
||||
rewrite_log on;
|
||||
error_log /var/log/nginx/localhost.error_log notice;
|
||||
access_log off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
proxy_pass http://frontend:5000/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8080/;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+16152
-7188
File diff suppressed because it is too large
Load Diff
+88
-58
@@ -1,66 +1,96 @@
|
||||
{
|
||||
"name": "oodikone",
|
||||
"description": "An application for analyzing university data.",
|
||||
"engines": {
|
||||
"node": ">=24.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"cypress": "cypress",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run --browser chrome",
|
||||
"instrument": "./scripts/instrument.sh",
|
||||
"eslint": "eslint",
|
||||
"lint:all": "eslint services/ updater/",
|
||||
"stylelint": "stylelint",
|
||||
"prettier": "prettier",
|
||||
"cli": "./cli.sh",
|
||||
"oodikone": "./run.sh oodikone anon up --detach",
|
||||
"oodikone:real": "./run.sh oodikone real up --detach",
|
||||
"oodikone:ci:setup": "docker compose --file docker-compose.ci.yml --profile tests up --detach",
|
||||
"oodikone:ci:api": "docker compose --file docker-compose.test.yml up --exit-code-from api-tests",
|
||||
"updater": "./run.sh updater anon up --detach",
|
||||
"updater:real": "./run.sh updater real up --detach",
|
||||
"both": "./run.sh both anon up --detach",
|
||||
"both:real": "./run.sh both real up --detach",
|
||||
"docker:down": "./run.sh both anon down --remove-orphans; ./run.sh both real down --remove-orphans",
|
||||
"testupdater": "docker-compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker && SCHEDULE_IMMEDIATE=weekly EXIT_AFTER_IMMEDIATES=yes docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start",
|
||||
"flushredis": "docker exec -it redis redis-cli flushall",
|
||||
"prepare": "husky",
|
||||
"rapodiff": "docker exec -t oodikone-backend /opt/app-root/backend/node_modules/.bin/tsx /opt/app-root/backend/src/rapodiff/rapodiff.js",
|
||||
"install:local": "npm i && npm i --prefix services/shared && npm i --prefix services/frontend && npm i --prefix services/backend && npm i --prefix updater/sis-updater-scheduler && npm i --prefix updater/sis-updater-worker",
|
||||
"i:both": "npm run install:both",
|
||||
"ci:local": "npm ci && npm ci --prefix services/shared && npm ci --prefix services/frontend && npm ci --prefix services/backend && npm ci --prefix updater/sis-updater-scheduler && npm ci --prefix updater/sis-updater-worker",
|
||||
"install:docker": "docker exec frontend npm i && docker exec backend npm i && docker exec sis-updater-scheduler npm i && docker exec sis-updater-worker npm i",
|
||||
"ci:docker": "docker exec frontend npm ci && docker exec backend npm ci && docker exec sis-updater-scheduler npm ci && docker exec sis-updater-worker npm ci",
|
||||
"install:both": "npm run install:local && npm run install:docker",
|
||||
"ci:both": "npm run ci:local && npm run ci:docker"
|
||||
},
|
||||
"name": "oodikone2-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/UniversityOfHelsinkiCS/oodikone.git"
|
||||
"url": "git+https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend.git"
|
||||
},
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/UniversityOfHelsinkiCS/oodikone/issues"
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server --config webpack.config.local.js",
|
||||
"dist": "rimraf dist && webpack --config webpack.config.production.js",
|
||||
"lint": "concurrently 'npm run lint:js' 'npm run lint:css'",
|
||||
"lint:css": "stylelint 'src/**/*.css'",
|
||||
"lint:js": "eslint server ./ --ext .jsx,.js",
|
||||
"lintfix": "eslint server ./ --ext .jsx,.js --fix",
|
||||
"stats": "webpack --json --config webpack.config.production.js > stats.json",
|
||||
"prepush": "npm run lint",
|
||||
"pretest": "npm run lint",
|
||||
"test": "echo 'no tests'"
|
||||
},
|
||||
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
|
||||
"licence": "",
|
||||
"main": "src/app.jsx",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.28.0",
|
||||
"cypress": "^15.9.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-import-resolver-typescript": "^4.4.3",
|
||||
"eslint-plugin-cypress": "^5.1.1",
|
||||
"eslint-plugin-import-x": "^4.16.1",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"globals": "^16.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
"prettier": "3.5.3",
|
||||
"stylelint": "^16.20.0",
|
||||
"stylelint-config-standard": "^39.0.0",
|
||||
"typescript-eslint": "^8.34.0"
|
||||
"babel-core": "6.26.0",
|
||||
"babel-eslint": "8.2.1",
|
||||
"babel-loader": "7.1.4",
|
||||
"babel-plugin-inline-json-import": "^0.2.1",
|
||||
"babel-plugin-transform-async-to-generator": "^6.24.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-preset-latest": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-runtime": "6.26.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.1.1",
|
||||
"concurrently": "^3.5.1",
|
||||
"css-loader": "0.28.7",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-airbnb": "^16.1.0",
|
||||
"eslint-config-react-app": "^2.0.1",
|
||||
"eslint-loader": "1.9.0",
|
||||
"eslint-plugin-babel": "^4.1.2",
|
||||
"eslint-plugin-flowtype": "2.39.1",
|
||||
"eslint-plugin-import": "^2.10.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-prefer-object-spread": "^1.2.1",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"extract-text-webpack-plugin": "^v4.0.0-beta.0",
|
||||
"file-loader": "1.1.11",
|
||||
"glob": "^7.1.2",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"html-webpack-template": "^6.2.0",
|
||||
"husky": "^0.14.3",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"optimize-css-assets-webpack-plugin": "^3.2.0",
|
||||
"postcss-cssnext": "^3.0.2",
|
||||
"postcss-flexbugs-fixes": "3.2.0",
|
||||
"postcss-loader": "^2.1.2",
|
||||
"react-dev-utils": "^4.2.1",
|
||||
"style-loader": "^0.19.1",
|
||||
"stylelint": "^8.2.0",
|
||||
"stylelint-config-standard": "^18.2.0",
|
||||
"sw-precache-webpack-plugin": "0.11.5",
|
||||
"uglifyjs-webpack-plugin": "^1.2.4",
|
||||
"webpack": "^4.4.1",
|
||||
"webpack-cli": "^2.0.13",
|
||||
"webpack-dev-server": "3.1.1",
|
||||
"webpack-manifest-plugin": "^2.0.0-rc.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"dotenv": "4.0.0",
|
||||
"jStat": "^1.7.1",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"lodash": "^4.17.5",
|
||||
"moment": "^2.22.0",
|
||||
"prop-types": "^15.6.1",
|
||||
"react": "^16.3.1",
|
||||
"react-datetime": "^2.14.0",
|
||||
"react-dom": "^16.3.1",
|
||||
"react-hot-loader": "^3.1.3",
|
||||
"react-localize-redux": "^2.16.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"recharts": "^1.0.0-beta.10",
|
||||
"redux": "^3.7.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"reselect": "^3.0.1",
|
||||
"rimraf": "^2.6.2",
|
||||
"semantic-ui-css": "^2.3.1",
|
||||
"semantic-ui-react": "^0.77.2",
|
||||
"uuid": "^3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
const cssNextPlugin = require('postcss-cssnext') // eslint-disable-line import/no-extraneous-dependencies
|
||||
|
||||
const variables = require('./src/styles/variables')
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
cssNextPlugin({
|
||||
browsers: 'last 2 versions',
|
||||
features: {
|
||||
customProperties: { variables }
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "all non-major dependencies",
|
||||
"groupSlug": "all-minor-patch",
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"matchCurrentVersion": "!/^0/",
|
||||
"matchPackageNames": ["*"]
|
||||
},
|
||||
{
|
||||
"matchDatasources": ["docker"],
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"rebaseWhen": "never",
|
||||
"vulnerabilityAlerts": {
|
||||
"labels": ["security"]
|
||||
},
|
||||
"addLabels": ["dependencies"],
|
||||
"rangeStrategy": "bump"
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script is used to run oodikone with different setups and is mainly used by
|
||||
# scripts in package.json.
|
||||
# Base for script: https://betterdev.blog/minimal-safe-bash-script-template/
|
||||
|
||||
# === Config ===
|
||||
|
||||
# Fail immediately if script fails, unbound variables are referenced
|
||||
# or command inside pipe fails. -E ensures cleanup trap fires in rare ERR cases.
|
||||
set -euoE pipefail
|
||||
|
||||
# Set up constants
|
||||
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
||||
|
||||
# Set up messages and exiting
|
||||
source "$PROJECT_ROOT"/scripts/utils.sh
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "${BASH_SOURCE[0]}") option [version] command --flag
|
||||
|
||||
Parameters:
|
||||
* Option: oodikone/updater/both
|
||||
* Version: anon/real. Not necessary when running down/logs/pull.
|
||||
* Command: will be passed to docker-compose.
|
||||
EOF
|
||||
}
|
||||
|
||||
# Parse parameters. If arguments are not correct, print usage and exit with error.
|
||||
parse_params() {
|
||||
args=("$@")
|
||||
|
||||
[[ ${#args[@]} -eq 0 ]] && usage && die "Wrong number of arguments"
|
||||
option=${args[0]}
|
||||
|
||||
# parse arguments
|
||||
[[ ("$option" != "oodikone" && "$option" != "updater" && "$option" != "both" && "$option" != "tests") ]] && \
|
||||
usage && die "Wrong option: $option"
|
||||
|
||||
[[ ${#args[@]} -eq 1 ]] && usage && die "Wrong number of arguments"
|
||||
|
||||
# Some arguments can be passed without version. Otherwise parse version and then pass
|
||||
# rest to compose
|
||||
if [[ "${args[1]}" == "down" || "${args[1]}" == "logs" || "${args[1]}" == "pull" ]]; then
|
||||
version=""
|
||||
compose_command=${args[*]:1}
|
||||
else
|
||||
version=${args[1]}
|
||||
[[ "$version" != "anon" && "$version" != "real" && "$version" != "api" ]] && usage && \
|
||||
die "Wrong version $version"
|
||||
compose_command=${args[*]:2}
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Set which profile to use to launch correct services
|
||||
parse_profiles() {
|
||||
if [[ "$option" == "both" ]]; then
|
||||
profiles="--profile oodikone --profile updater"
|
||||
else
|
||||
profiles="--profile $option"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Set which docker-compose files to use based on version.
|
||||
parse_env() {
|
||||
env=""
|
||||
if [[ "$version" == "real" ]]; then
|
||||
env="--file docker-compose.yml --file docker-compose.real.yml"
|
||||
fi
|
||||
if [[ "$version" == "api" ]]; then
|
||||
env="--file docker-compose.ci.yml"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# === Run script ===
|
||||
|
||||
parse_params "$@"
|
||||
|
||||
# Create command that will be run. Empty command and "down" command will be handled
|
||||
# differently.
|
||||
parse_profiles
|
||||
parse_env
|
||||
if [[ "$compose_command" == "" ]]; then
|
||||
final_command="docker compose ${env}"
|
||||
else
|
||||
final_command="docker compose ${env} ${profiles} ${compose_command}"
|
||||
fi
|
||||
|
||||
msg "${BLUE}Running: ${final_command}${NOFORMAT}"
|
||||
eval "$final_command"
|
||||
@@ -1,10 +0,0 @@
|
||||
______ __ __ __
|
||||
/ \ | \| \| \
|
||||
| $$$$$$\ ______ ____| $$ \$$| $$ __ ______ _______ ______
|
||||
| $$ | $$ / \ / $$| \| $$ / \ / \ | \ / \
|
||||
| $$ | $$| $$$$$$\| $$$$$$$| $$| $$_/ $$| $$$$$$\| $$$$$$$\| $$$$$$\
|
||||
| $$ | $$| $$ | $$| $$ | $$| $$| $$ $$ | $$ | $$| $$ | $$| $$ $$
|
||||
| $$__/ $$| $$__/ $$| $$__| $$| $$| $$$$$$\ | $$__/ $$| $$ | $$| $$$$$$$$
|
||||
\$$ $$ \$$ $$ \$$ $$| $$| $$ \$$\ \$$ $$| $$ | $$ \$$ \
|
||||
\$$$$$$ \$$$$$$ \$$$$$$$ \$$ \$$ \$$ \$$$$$$ \$$ \$$ \$$$$$$$
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
::---..``` `ohmMMMMMNNNdy.
|
||||
/MMMMMMMMMMMMMMMMMMMMmyMMMMd
|
||||
`sddmmmNNNNNMMMMMMMMMMMMMMMMmdddddhhhhhhy+`
|
||||
``````mMMMMMMMMMMMMMMNNMMMMMMMMMMMN-
|
||||
.yMMMMMMMMMMMMo``````...----.
|
||||
:MMMMMMMMMMMMm-
|
||||
hMMNNMMMMMMMMMNy- `.--.`
|
||||
-MMMdMMMMMMMMMMMMNdo:.`` ``..://ossyhdmmNNNNNNmhys+:.
|
||||
` -mMMmdMMMMMMMMMMMMMMMNNNmmmmddmmmNNNNNMMMMMMMMMMMMMMMMMMMMMMNmh/.
|
||||
.mds+:-.` ` -+hNMMNsNNNNNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNy-
|
||||
.`.-/+syyy+:. -ddymNMMMMNmNNNNdNNNNNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNo`
|
||||
``-oydmmMMMMMMMMNNNNNNNNNNmNNmNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMm:
|
||||
/NMMMMMMMMMNNMMNNNNNNmNNmNNNNMMMMMMMMMMMMMNdmMMMMMMMMMMMMMMMMMMhmMMMMMMMMMMMMMMN/
|
||||
`oNMMMMMMMMMMMMMMMNNNNNNmdNNNMMNNMMMMMMMMMMMd`/MMMMMMMMMMMMMMMMMM`+MMMMMMMMMMMMMMMN-
|
||||
yMMMMMMMMMMMMMMMMMNNNNmNdmNMMNNNMMMMMMMMMMMMN .MMMMMMMMMMMMMMMMMM.:MMMMMMNMMMMMMMMMh
|
||||
oMMMMMMMMMMMMMdyyyhmNNNmmdMMMMNNMMMMMMMMMMMNmm` ddddhhhyyhdNMmyo+o..MMMd+:::/mMMMMMMM:
|
||||
-NMMMMMMMMMMMm+sdmdy/hNNNNNMMMMMMMMMMMMMMMMMy-:` -::. -///--o.-syo. NNo.+dmmosMMMMMMMm
|
||||
`/osyhmMMMMMMMMMMMM+mMMMMMN:MMMMMMNMMMMMMMMMMMMMMMNmN/ yNd`.hNMMMNs` oNMMs y:-hNdys+oMMMMMMMM`
|
||||
:mMMMhmMMMMMMMMMMMMMhmMMMMMhyMMMMMMNMMMMMMMMMMMMMMMMMMo sM: hMMMMMMMo .-::: . oh-:oyh+MMMMMMMM`
|
||||
NMMMMNMMMMMMMMMNNNMMMmmNmNmmMMMMMMMMMMMMMMMMMMMMMMMMMMy +M- mMMMMMMMy sddh+ os.`.NMMm:MMMMMMMM
|
||||
NMMMMMMMMMMMMMMMNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMd -Ms +NMMMMMN: sNMNy sMm/.oys:/MMMMMMMh
|
||||
+NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN `NN+`:ydmdy:.+-+ss+oNMMMddmNNMMMMMMMN-
|
||||
.+yhdmdMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM/.+ss++oyhhddNMNMMMMMMMMMMMMMMMMMMMMM+
|
||||
` hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmNNMMy
|
||||
.NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMmmMMMMMMm.
|
||||
/NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMmyNMMMMMMMMh
|
||||
+MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMdhMMMMMMMMMMM`
|
||||
/NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMmyMMMMMMMMMMMM.
|
||||
yMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMyMMMMMMMMMNMMM`
|
||||
hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNNhMMMMMMMMMMMMh
|
||||
yMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMhMMMMMMMMMMMMMMMMMMMMMMMNNmhys+/-...oMMMMMMMMMMMN:
|
||||
:MMMMMMMMMMMMMMMNddmNMMMMMMMMMMMMMhMMMMMMMMMNmmMMMMN++/++/- `dMMMMMMMMMMs`
|
||||
hMMMMMMMMMMMMMN: .:/+osyyhddmmmdMMMMMMMMMMMMMMMMy `omMMMMMMNmy-
|
||||
yMMMMMMMMMMMm- `+-`mMMMMMMMMMMMMMMMM. ````````.``.-osyo//`
|
||||
`sNMMMMMMMMo.`` ``` `````` ```+MMMMMMMMMMMMMMMy``` `````
|
||||
`yy.shmMMmh- ` ``. ``hMMMMMMMMMMMMN+-.``
|
||||
`.`...` `...``.```....`.-://mMMMMMMMMMMhy/
|
||||
`+hNMMMNmhs:-.
|
||||
@@ -1,270 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file includes functions that are used to setup Oodikone
|
||||
# File doesn't run anything by itself and should be sourced from other scripts
|
||||
|
||||
# === Config ===
|
||||
|
||||
# Set up constants
|
||||
## Folders
|
||||
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
||||
DUMP_DIR="$PROJECT_ROOT/.databasedumps"
|
||||
S3_CONFIG_FILE=~/.s3cfg
|
||||
S3_BUCKET="s3://psyduck"
|
||||
|
||||
## Following the naming convention in docker-compose, these are names for database
|
||||
## services. The real data databases inside service will have names suffixed by "-real".
|
||||
KONE_DB_NAME="kone-db"
|
||||
SIS_DB_NAME="sis-db"
|
||||
SIS_IMPORTER_DB_NAME="sis-importer-db"
|
||||
USER_DB_NAME="user-db"
|
||||
JAMI_DB_NAME="jami-db"
|
||||
DATABASES=("$KONE_DB_NAME" "$SIS_DB_NAME" "$SIS_IMPORTER_DB_NAME" "$USER_DB_NAME" "$JAMI_DB_NAME")
|
||||
|
||||
## Urls should be in same order as databases as both are iterated through by indexes.
|
||||
KONE_DB_S3_PATH="oodikone_kone"
|
||||
SIS_DB_S3_PATH="oodikone_updater"
|
||||
SIS_IMPORTER_DB_S3_PATH="sis_importer"
|
||||
USER_DB_S3_PATH="oodikone_user"
|
||||
JAMI_DB_S3_PATH="jami"
|
||||
S3_PATHS=("$KONE_DB_S3_PATH" "$SIS_DB_S3_PATH" "$SIS_IMPORTER_DB_S3_PATH" "$USER_DB_S3_PATH" "$JAMI_DB_S3_PATH")
|
||||
|
||||
# Source utility functions
|
||||
source "$PROJECT_ROOT"/scripts/utils.sh
|
||||
|
||||
# === Function ===
|
||||
|
||||
draw_mopo() {
|
||||
# Some hacks to print mopo as green, since colours from common config don't work when
|
||||
# catting images. Please feel free to fix this. And tell otahontas how you did it!
|
||||
local mopogreen
|
||||
mopogreen=$(tput setaf 34)
|
||||
local normal
|
||||
normal=$(tput sgr0)
|
||||
if [ "$(tput cols)" -gt "100" ]; then
|
||||
while IFS="" read -r p || [ -n "$p" ]; do
|
||||
printf '%40s\n' "${mopogreen}$p${normal}"
|
||||
done < "$PROJECT_ROOT"/scripts/assets/mopo.txt
|
||||
fi
|
||||
}
|
||||
|
||||
retry() {
|
||||
sleep 5
|
||||
for i in {1..60}; do
|
||||
"$@" && break || warningmsg "Retry attempt $i failed, waiting..." && sleep 5;
|
||||
done
|
||||
}
|
||||
|
||||
download_real_dump() {
|
||||
local database=$1
|
||||
local dump_destination="$DUMP_DIR/$database.sql.gz"
|
||||
local s3_path=$2
|
||||
local selection_flag=$3
|
||||
|
||||
rm -f "$dump_destination"
|
||||
|
||||
if [ "$selection_flag" = "true" ]; then
|
||||
local backup_files
|
||||
mapfile -t backup_files < <(s3cmd -c "$S3_CONFIG_FILE" ls "$S3_BUCKET/$s3_path/" | sort -rk1,2 | awk '{print $4}')
|
||||
|
||||
if [ ${#backup_files[@]} -eq 0 ]; then
|
||||
die "No backup files found in $S3_BUCKET bucket $s3_path path!"
|
||||
fi
|
||||
|
||||
infomsg "Available backups:"
|
||||
PS3="Select backup (or 0 to go back): "
|
||||
select chosen_backup in "${backup_files[@]}"; do
|
||||
if [ "$REPLY" = "0" ] || [ "$chosen_backup" = "Go back" ]; then
|
||||
infomsg "Going back to previous menu."
|
||||
break
|
||||
elif [ -n "$chosen_backup" ]; then
|
||||
infomsg "You selected: $chosen_backup"
|
||||
local FILE_NAME
|
||||
FILE_NAME=$(basename "$chosen_backup")
|
||||
infomsg "Fetching the selected dump: $FILE_NAME"
|
||||
s3cmd -c "$S3_CONFIG_FILE" get "$chosen_backup" "$dump_destination"
|
||||
if [ ! -f "$dump_destination" ]; then
|
||||
die "Download failed or file not found: $dump_destination"
|
||||
fi
|
||||
echo "$dump_destination"
|
||||
break
|
||||
else
|
||||
warningmsg "Invalid selection. Please select a valid backup number."
|
||||
fi
|
||||
done
|
||||
else
|
||||
local current_date
|
||||
current_date=$(date +"%Y%m%d")
|
||||
s3cmd -c "$S3_CONFIG_FILE" get "s3://psyduck/${s3_path}/${s3_path}_${current_date}.sql.gz" "$dump_destination"
|
||||
fi
|
||||
}
|
||||
|
||||
check_if_postgres_is_ready() {
|
||||
local container=$1
|
||||
local database=$2
|
||||
retry docker exec -u postgres "$container" pg_isready --dbname="$database"
|
||||
}
|
||||
|
||||
reset_databases() {
|
||||
local databases=("$@")
|
||||
local database_dump_dir=$DUMP_DIR
|
||||
|
||||
infomsg "Restoring PostgreSQL dumps from backups. This might take a while."
|
||||
|
||||
npm run docker:down
|
||||
docker-compose up -d "${databases[@]}"
|
||||
|
||||
for database in "${databases[@]}"; do
|
||||
local database_dump="$database_dump_dir/$database.sql.gz"
|
||||
local database_container="$database"
|
||||
local database_name="$database-real"
|
||||
|
||||
infomsg "Attempting to create database $database_name from dump $database_dump inside container $database_container"
|
||||
|
||||
infomsg "Trying to remove possibly existing previous version of database"
|
||||
check_if_postgres_is_ready "$database_container" "$database_name"
|
||||
docker exec -u postgres "$database_container" dropdb "$database_name" || warningmsg "This is okay, continuing"
|
||||
|
||||
infomsg "Trying to create new database"
|
||||
check_if_postgres_is_ready "$database_container" "$database_name"
|
||||
docker exec -u postgres "$database_container" createdb "$database_name" || warningmsg "This is okay, continuing"
|
||||
|
||||
infomsg "Restoring database from dump database"
|
||||
msg "Writing database..."
|
||||
docker exec -i "$database_container" /bin/bash -c "gunzip | psql -U postgres -d $database_name" < "$database_dump" 2> /dev/null
|
||||
msg ""
|
||||
done
|
||||
|
||||
successmsg "Database setup finished"
|
||||
}
|
||||
|
||||
reset_all_real_data() {
|
||||
infomsg "Downloading real data dumps"
|
||||
for i in ${!DATABASES[*]}; do
|
||||
local database="${DATABASES[$i]}"
|
||||
local url="${S3_PATHS[$i]}"
|
||||
download_real_dump "$database" "$url" false
|
||||
done
|
||||
reset_databases "${DATABASES[@]}"
|
||||
}
|
||||
|
||||
restore_data_from_dumps() {
|
||||
infomsg "Restoring earlier-downloaded dumps to databases"
|
||||
reset_databases "${DATABASES[@]}"
|
||||
}
|
||||
|
||||
reset_single_database() {
|
||||
# Define custom shell prompt for the nested interactive select loop
|
||||
PS3="Please enter which database to reset: "
|
||||
|
||||
options=(
|
||||
"kone-db"
|
||||
"sis-db"
|
||||
"sis-importer-db"
|
||||
"user-db"
|
||||
"jami-db"
|
||||
"Return to main menu."
|
||||
)
|
||||
|
||||
local database=""
|
||||
local url=""
|
||||
local selection_flag=true
|
||||
|
||||
while true; do
|
||||
select opt in "${options[@]}"; do
|
||||
case $opt in
|
||||
"kone-db")
|
||||
local database=$KONE_DB_NAME
|
||||
local url=$KONE_DB_S3_PATH
|
||||
break 2;;
|
||||
"sis-db")
|
||||
local database=$SIS_DB_NAME
|
||||
local url=$SIS_DB_S3_PATH
|
||||
break 2;;
|
||||
"sis-importer-db")
|
||||
local database=$SIS_IMPORTER_DB_NAME
|
||||
local url=$SIS_IMPORTER_DB_S3_PATH
|
||||
break 2;;
|
||||
"user-db")
|
||||
local database=$USER_DB_NAME
|
||||
local url=$USER_DB_S3_PATH
|
||||
break 2;;
|
||||
"jami-db")
|
||||
local database=$JAMI_DB_NAME
|
||||
local url=$JAMI_DB_S3_PATH
|
||||
break 2;;
|
||||
"Return to main menu.")
|
||||
break 2;;
|
||||
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
|
||||
";;
|
||||
esac
|
||||
break
|
||||
done
|
||||
done
|
||||
|
||||
if [ -n "$database" ]; then
|
||||
infomsg "Downloading $database dump"
|
||||
download_real_dump $database $url $selection_flag
|
||||
reset_databases $database
|
||||
fi
|
||||
}
|
||||
|
||||
set_up_oodikone_from_scratch() {
|
||||
draw_mopo
|
||||
folders_to_set_up=(
|
||||
"$PROJECT_ROOT"
|
||||
"$PROJECT_ROOT/services/frontend"
|
||||
"$PROJECT_ROOT/services/backend"
|
||||
"$PROJECT_ROOT/updater/sis-updater-scheduler"
|
||||
"$PROJECT_ROOT/updater/sis-updater-worker"
|
||||
)
|
||||
|
||||
infomsg "Cleaning up docker setup and node_modules from possible previous installations"
|
||||
"$PROJECT_ROOT"/run.sh both down --remove-orphans --volumes --rmi all || 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"
|
||||
done
|
||||
|
||||
infomsg "Installing npm packages locally to root and each subfolder to enable linting and formatting"
|
||||
|
||||
for folder in "${folders_to_set_up[@]}"; do
|
||||
cd "$folder" || die "Couldn't change directory to folder $folder"
|
||||
([[ -d node_modules ]] && die "$folder already has node_modules, exiting!") || npm ci
|
||||
done
|
||||
cd "$PROJECT_ROOT" || die "Couldn't change directory to project root"
|
||||
|
||||
infomsg "Setting up husky for pre-commit linting"
|
||||
npm run prepare
|
||||
|
||||
infomsg "Pulling images"
|
||||
"$PROJECT_ROOT"/run.sh both anon pull
|
||||
|
||||
infomsg "Building images"
|
||||
"$PROJECT_ROOT"/run.sh both anon build
|
||||
|
||||
successmsg "Setup ready, Oodikone can be started! See README for more info."
|
||||
}
|
||||
|
||||
docker_prune() {
|
||||
infomsg "Running: docker system prune -a && docker volume prune -a"
|
||||
docker system prune -a && docker volume prune -a
|
||||
}
|
||||
|
||||
init_dirs() {
|
||||
if [[ ! -d "$DUMP_DIR" ]]; then
|
||||
infomsg "Creating directory for dumps"
|
||||
mkdir "$DUMP_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# If username is not set, get username from data file.
|
||||
# Ask user to provide username, if username was not found from data file.
|
||||
get_s3_config() {
|
||||
if [ ! -f "$S3_CONFIG_FILE" ]; then
|
||||
warningmsg "No username provided, config file is required. The \".s3cfg\" config file can be found on toska/dokumentaatio repo."
|
||||
return 0
|
||||
fi
|
||||
|
||||
infomsg "Using your config $S3_CONFIG_FILE - for getting database dumps"
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file includes some utility functions that are used in Oodikone scripts
|
||||
# Functions are based on https://betterdev.blog/minimal-safe-bash-script-template/
|
||||
|
||||
# Print messages and logs that are not script output to stderr
|
||||
msg() {
|
||||
echo >&2 -e "${1-}"
|
||||
}
|
||||
|
||||
# Some special types of messages with colours
|
||||
successmsg() {
|
||||
msg "${GREEN}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
errormsg() {
|
||||
msg "${RED}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
infomsg() {
|
||||
msg "${BLUE}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
warningmsg() {
|
||||
msg "${ORANGE}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
|
||||
# Quit and exit with given message and error code
|
||||
# By default uses exit code 1, message must be given
|
||||
die() {
|
||||
local msg=$1
|
||||
local code=${2-1}
|
||||
msg "${RED}${msg}${NOFORMAT}"
|
||||
exit "$code"
|
||||
}
|
||||
|
||||
# Setup colors for messages if running interactive shell
|
||||
if [[ -t 2 ]] && [[ "${TERM-}" != "dumb" ]]; then
|
||||
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
|
||||
else
|
||||
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
|
||||
fi
|
||||
@@ -1,7 +0,0 @@
|
||||
*/.dockerignore
|
||||
*/.eslintrc.json
|
||||
*/.gitignore
|
||||
*/*Dockerfile
|
||||
*/build
|
||||
*/dist
|
||||
*/node_modules
|
||||
@@ -1,61 +0,0 @@
|
||||
FROM registry.access.redhat.com/ubi9/nodejs-24-minimal AS build
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NODE_OPTIONS=--max-old-space-size=4096
|
||||
|
||||
ARG BASE_PATH
|
||||
ENV PUBLIC_URL=$BASE_PATH
|
||||
|
||||
ARG STAGING
|
||||
ENV REACT_APP_STAGING=$STAGING
|
||||
|
||||
ARG SENTRY_ENVIRONMENT
|
||||
ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
|
||||
WORKDIR /opt/app-root/shared
|
||||
COPY ./shared .
|
||||
|
||||
WORKDIR /opt/app-root/backend
|
||||
COPY ./backend .
|
||||
|
||||
WORKDIR /opt/app-root
|
||||
|
||||
RUN npm ci --include=dev --prefix shared && npm ci --include=dev --prefix backend
|
||||
RUN npm run build --prefix shared && npm run build --prefix backend
|
||||
|
||||
|
||||
FROM registry.access.redhat.com/ubi9/nodejs-24-minimal
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NODE_OPTIONS=--max-old-space-size=4096
|
||||
|
||||
ARG BASE_PATH
|
||||
ENV PUBLIC_URL=$BASE_PATH
|
||||
|
||||
ARG STAGING
|
||||
ENV REACT_APP_STAGING=$STAGING
|
||||
|
||||
ARG SENTRY_ENVIRONMENT
|
||||
ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
|
||||
WORKDIR /opt/app-root/shared
|
||||
COPY --from=build /opt/app-root/shared/dist .
|
||||
|
||||
WORKDIR /opt/app-root/backend
|
||||
COPY --from=build /opt/app-root/backend/package*.json .
|
||||
COPY --from=build /opt/app-root/backend/dist ./dist
|
||||
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["node", "dist/index.js"]
|
||||
@@ -1,13 +0,0 @@
|
||||
FROM node:24-alpine
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
ENV NODE_ENV=development
|
||||
ENV NODE_OPTIONS=--max-old-space-size=4096
|
||||
|
||||
WORKDIR /opt/app-root/backend
|
||||
COPY . .
|
||||
RUN npm ci
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["node_modules/.bin/nodemon", "--exec", "node --import tsx index.ts", "--ext", "ts,js,json", "--watch", "src", "--watch", "index.ts", "--legacy-watch"]
|
||||
@@ -1,10 +0,0 @@
|
||||
import './src/util/sentry' // Sentry must be the first import
|
||||
import './src/app'
|
||||
import logger from './src/util/logger'
|
||||
|
||||
process.on('unhandledRejection', reason => {
|
||||
logger.error({ message: 'unhandledRejection: ', meta: reason })
|
||||
})
|
||||
|
||||
process.on('SIGTERM', () => process.exit())
|
||||
process.on('SIGINT', () => process.exit())
|
||||
Generated
-5613
File diff suppressed because it is too large
Load Diff
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc -p .",
|
||||
"test": "node --import tsx --test --test-reporter spec --test-force-exit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oodikone/shared": "file:../shared",
|
||||
"@sentry/node": "^10.43.0",
|
||||
"@sentry/profiling-node": "^10.43.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/pg": "^8.18.0",
|
||||
"bullmq": "^5.53.2",
|
||||
"compression": "^1.8.0",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.3.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^4.21.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lru-cache": "^11.1.0",
|
||||
"morgan": "^1.10.0",
|
||||
"papaparse": "^5.5.3",
|
||||
"pg": "^8.20.0",
|
||||
"redis": "^4.7.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sequelize": "^6.37.8",
|
||||
"sequelize-typescript": "^2.1.6",
|
||||
"umzug": "^3.8.2",
|
||||
"unfuck-utf8-headers-middleware": "^1.0.1",
|
||||
"winston": "^3.17.0",
|
||||
"winston-gelf-transporter": "^1.0.2",
|
||||
"winston-loki": "^6.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.8.1",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^4.17.23",
|
||||
"@types/morgan": "^1.9.10",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"nodemon": "^3.1.11",
|
||||
"supertest": "^7.2.2",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"overrides": {
|
||||
"winston-gelf-transporter": {
|
||||
"gelf-pro": "^1.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import 'express-async-errors'
|
||||
import express from 'express'
|
||||
|
||||
import { baseUrl, backendPort } from './config'
|
||||
import { initializeDatabaseConnection, dbConnections } from './database/connection'
|
||||
import { startCron } from './events'
|
||||
import routes from './routes'
|
||||
import logger from './util/logger'
|
||||
import './worker/worker'
|
||||
|
||||
initializeDatabaseConnection()
|
||||
.then(() => {
|
||||
void dbConnections.connect()
|
||||
|
||||
dbConnections.on('connect', () => {
|
||||
logger.info('Connected to sis db successfully')
|
||||
})
|
||||
|
||||
dbConnections.on('error', () => {
|
||||
logger.error('Failed to connect to sis db!')
|
||||
})
|
||||
startCron()
|
||||
|
||||
const app = express()
|
||||
routes(app, baseUrl)
|
||||
|
||||
const server = app.listen(backendPort, () => logger.info(`Backend listening on port ${backendPort}!`))
|
||||
process.on('SIGTERM', () => {
|
||||
server.close(() => {
|
||||
logger.info('Process terminated')
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
process.exitCode = 1
|
||||
logger.error(error)
|
||||
})
|
||||
@@ -1,55 +0,0 @@
|
||||
// Node env to use
|
||||
export const isDev = process.env.NODE_ENV === 'development'
|
||||
export const isTest = process.env.NODE_ENV === 'test'
|
||||
export const isStaging = process.env.REACT_APP_STAGING === 'true'
|
||||
export const isProduction = !isStaging && process.env.NODE_ENV === 'production'
|
||||
|
||||
export const runningInCI = process.env.CI === 'true'
|
||||
|
||||
// IAM group
|
||||
export const requiredGroup = isStaging
|
||||
? ['grp-oodikone-staging-users', 'grp-oodikone-basic-staging-users']
|
||||
: ['grp-oodikone-users', 'grp-oodikone-basic-users']
|
||||
|
||||
// Pate
|
||||
export const pateToken = process.env.PATE_API_TOKEN ?? ''
|
||||
|
||||
// Jami
|
||||
export const jamiUrl = process.env.JAMI_URL ?? ''
|
||||
|
||||
// Importer client
|
||||
export const importerUrl = process.env.IMPORTER_DB_API_URL ?? ''
|
||||
export const importerToken = process.env.IMPORTER_DB_API_TOKEN ?? ''
|
||||
export const importerDbApiUser = process.env.IMPORTER_DB_API_USER ?? ''
|
||||
export const importerDbApiPassword = process.env.IMPORTER_DB_API_PASSWORD ?? ''
|
||||
|
||||
// Sisu
|
||||
export const sisUrl = process.env.SIS_HOST
|
||||
export const sisGrapqlAppAccount = process.env.SIS_GRAPHQL_APPLICATION_ACCOUNT
|
||||
export const sisGrapqlAppKey = process.env.SIS_GRAPHQL_APPLICATION_KEY
|
||||
|
||||
// Networking: Urls & ports
|
||||
export const baseUrl = isDev ? '/api' : ''
|
||||
export const frontUrl = process.env.FRONT_URL
|
||||
export const backendPort = 8080
|
||||
|
||||
// Other stuff
|
||||
export const { DB_URL_KONE, DB_URL_USER, SECRET_TOKEN, SIS_DB_URL, SIS_UPDATER_URL, CRYPT_KEY } = process.env
|
||||
|
||||
export const rootOrgId = process.env.ROOT_ORG_ID ?? 'hy-university-root-id'
|
||||
|
||||
export const languageCenterViewEnabled =
|
||||
process.env.LANGUAGE_CENTER_VIEW_ENABLED == null ? true : process.env.LANGUAGE_CENTER_VIEW_ENABLED === 'true'
|
||||
|
||||
export const concurrentWorkers = parseInt(process.env.CONCURRENT_WORKERS!, 10) || 2
|
||||
|
||||
export const DB_MAX_CONNECTIONS = parseInt(process.env.DB_MAX_CONNECTIONS!, 10) || 5 // sequelize's default
|
||||
export const DB_MAX_CRON_CONNECTIONS = Math.max(1, DB_MAX_CONNECTIONS - 5)
|
||||
export const CRON_SCHEDULE = process.env.CRON_SCHEDULE ?? '0 23 * * *' // Default to 23:00 daily
|
||||
|
||||
export const redis = process.env.REDIS
|
||||
export const DB_SCHEMA_KONE = process.env.DB_SCHEMA_KONE ?? 'public'
|
||||
|
||||
export const KONE_PASSWORD = process.env.KONE_PASSWORD ?? 'postgres'
|
||||
export const USER_PASSWORD = process.env.USER_PASSWORD ?? 'postgres'
|
||||
export const SIS_PASSWORD = process.env.SIS_PASSWORD ?? 'postgres'
|
||||
@@ -1,7 +0,0 @@
|
||||
// Linter was not having any of this so needed to add resolveJsonModule:true to tsconfig
|
||||
|
||||
import organizationConfig from '../environment/organizationConfig.json'
|
||||
|
||||
export const { facultyCodes } = organizationConfig
|
||||
export const { ignoredFacultyCodes } = organizationConfig
|
||||
export const { magicFacultyCode } = organizationConfig
|
||||
@@ -1,138 +0,0 @@
|
||||
// Map programme codes to "letter indentifier"
|
||||
|
||||
export const programmeCodes = {
|
||||
KH10_001: 'TUK',
|
||||
MH10_001: 'TUM',
|
||||
KH20_001: 'ON',
|
||||
MH20_001: 'OTM',
|
||||
MH20_002: 'IBL',
|
||||
MH20_003: 'GGL',
|
||||
KH30_001: 'PSYKK',
|
||||
KH30_002: 'LOGOK',
|
||||
KH30_003: 'SPK',
|
||||
MH30_001: 'LL',
|
||||
MH30_002: 'TMED',
|
||||
MH30_003: 'HLL',
|
||||
MH30_004: 'PSYK',
|
||||
MH30_005: 'LOGO',
|
||||
MH30_006: 'TERV',
|
||||
KH40_001: 'FILK',
|
||||
KH40_002: 'TTK',
|
||||
KH40_003: 'KIK',
|
||||
KH40_004: 'KOK',
|
||||
KH40_005: 'KUKA',
|
||||
KH40_006: 'HISK',
|
||||
MH40_001: 'TTM',
|
||||
MH40_002: 'KIM',
|
||||
MH40_003: 'ENG',
|
||||
MH40_004: 'MAREEES',
|
||||
MH40_005: 'LingDig',
|
||||
MH40_006: 'TRA',
|
||||
MH40_007: 'SUKU',
|
||||
MH40_008: 'NOR',
|
||||
MH40_009: 'KIR',
|
||||
MH40_010: 'KUMA',
|
||||
MH40_011: 'ICE',
|
||||
MH40_012: 'ALKU',
|
||||
MH40_014: 'SPT',
|
||||
MH40_015: 'HISM',
|
||||
KH50_001: 'MAT',
|
||||
KH50_002: 'FYS',
|
||||
KH50_003: 'KEMK',
|
||||
KH50_004: 'MFKK',
|
||||
KH50_005: 'TKT',
|
||||
KH50_006: 'GEOK',
|
||||
KH50_007: 'MAA',
|
||||
KH50_008: 'BSC',
|
||||
MH50_001: 'MAST',
|
||||
MH50_002: 'LSI',
|
||||
MH50_003: 'TCM',
|
||||
MH50_004: 'PARAS',
|
||||
MH50_005: 'MATRES',
|
||||
MH50_006: 'ATM',
|
||||
MH50_007: 'KEM',
|
||||
MH50_008: 'MFKM',
|
||||
MH50_009: 'CSM',
|
||||
MH50_010: 'DATA',
|
||||
MH50_011: 'GEOM',
|
||||
MH50_012: 'GEOG',
|
||||
MH50_013: 'USP',
|
||||
MH50_014: 'EnCHiL',
|
||||
KH55_001: 'FARM',
|
||||
MH55_001: 'PROV',
|
||||
MH55_002: 'MPHARM',
|
||||
KH57_001: 'BIO',
|
||||
KH57_002: 'MOLE',
|
||||
KH57_003: 'ENV',
|
||||
MH57_001: 'EEB',
|
||||
MH57_002: 'IPS',
|
||||
MH57_003: 'GMB',
|
||||
MH57_004: 'NEURO',
|
||||
MH57_005: 'ECGS',
|
||||
KH60_001: 'EDUK',
|
||||
MH60_001: 'EDUM',
|
||||
MH60_002: 'CE',
|
||||
KH70_001: 'PVK',
|
||||
KH70_002: 'YK',
|
||||
KH70_003: 'SOSK',
|
||||
KH70_004: 'ECOK',
|
||||
MH70_001: 'FILM',
|
||||
MH70_002: 'PVM',
|
||||
MH70_003: 'GPC',
|
||||
MH70_004: 'YM',
|
||||
MH70_005: 'COS',
|
||||
MH70_006: 'ENS',
|
||||
MH70_007: 'MSV',
|
||||
MH70_008: 'SOSM',
|
||||
MH70_009: 'ECON',
|
||||
MH70_010: 'IMESS',
|
||||
MH70_011: 'SOTE',
|
||||
KH74_001: 'KSV',
|
||||
KH80_001: 'MAATK',
|
||||
KH80_002: 'METSAK',
|
||||
KH80_003: 'ETK',
|
||||
KH80_004: 'YET',
|
||||
MH80_001: 'AGRI',
|
||||
MH80_002: 'AGERE',
|
||||
MH80_003: 'FOR',
|
||||
MH80_004: 'FOOD',
|
||||
MH80_005: 'HNFB',
|
||||
MH80_006: 'EKM',
|
||||
MH80_007: 'MMB',
|
||||
MH80_008: 'emPlant',
|
||||
KH90_001: 'ELK',
|
||||
MH90_001: 'ELL',
|
||||
T920101: 'DPTHEOL',
|
||||
T920102: 'DPLAW',
|
||||
T920103: 'DPHISTCULT',
|
||||
T920104: 'DPLANG',
|
||||
T920105: 'SKY',
|
||||
T920106: 'DPSOCS',
|
||||
T920107: 'PYAM',
|
||||
T920108: 'DPE',
|
||||
T920109: 'SEDUCE',
|
||||
T920110: 'CLIC',
|
||||
T920111: 'PHILARTSOC',
|
||||
T921101: 'DPBM',
|
||||
T921102: 'KLTO',
|
||||
T921103: 'DocPop',
|
||||
T921104: 'FINDOS',
|
||||
T921105: 'DPDR',
|
||||
T921106: 'ILS',
|
||||
T921107: 'BM',
|
||||
T921108: 'CVM',
|
||||
T921109: 'DPHUB',
|
||||
T922101: 'LUOVA',
|
||||
T922102: 'DPPS',
|
||||
T922103: 'DENVI',
|
||||
T922104: 'AGFOREE',
|
||||
T922105: 'MBDP',
|
||||
T922106: 'FoodHealth',
|
||||
T923101: 'PAPU',
|
||||
T923102: 'GeoDoc',
|
||||
T923103: 'ATM-DP',
|
||||
T923104: 'CHEMS',
|
||||
T923105: 'DOMAST',
|
||||
T923106: 'MATRENA',
|
||||
T923107: 'DOCS',
|
||||
} as const
|
||||
@@ -1,11 +0,0 @@
|
||||
// This is locked behind admin check.
|
||||
// We probably want to keep it on the server side and not share it.
|
||||
export const roles = [
|
||||
'admin',
|
||||
'courseStatistics',
|
||||
'facultyStatistics',
|
||||
'fullSisuAccess',
|
||||
'openUniSearch',
|
||||
'studyGuidanceGroups',
|
||||
'teachers',
|
||||
] as const
|
||||
@@ -1,174 +0,0 @@
|
||||
import EventEmitter from 'events'
|
||||
import { Sequelize } from 'sequelize-typescript'
|
||||
import { SequelizeStorage, Umzug } from 'umzug'
|
||||
|
||||
import * as conf from '../config'
|
||||
import {
|
||||
CourseModel,
|
||||
CourseProviderModel,
|
||||
CourseTypeModel,
|
||||
CreditModel,
|
||||
CreditTeacherModel,
|
||||
CreditTypeModel,
|
||||
CurriculumPeriodModel,
|
||||
EnrollmentModel,
|
||||
OrganizationModel,
|
||||
ProgrammeModuleModel,
|
||||
ProgrammeModuleChildModel,
|
||||
SemesterModel,
|
||||
SISStudyRightModel,
|
||||
SISStudyRightElementModel,
|
||||
StudentModel,
|
||||
StudyplanModel,
|
||||
StudyrightExtentModel,
|
||||
TeacherModel,
|
||||
} from '../models'
|
||||
import {
|
||||
CustomPopulationSearchModel,
|
||||
ExcludedCourseModel,
|
||||
OpenUniPopulationSearchModel,
|
||||
ProgressCriteriaModel,
|
||||
StudyGuidanceGroupTagModel,
|
||||
StudyProgrammePinModel,
|
||||
TagModel,
|
||||
TagStudentModel,
|
||||
} from '../models/kone'
|
||||
import { UserModel } from '../models/user'
|
||||
|
||||
import logger from '../util/logger'
|
||||
|
||||
class DbConnection extends EventEmitter {
|
||||
sequelize: Sequelize
|
||||
|
||||
RETRY_ATTEMPTS: number
|
||||
|
||||
established: boolean
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.RETRY_ATTEMPTS = 15
|
||||
this.established = false
|
||||
|
||||
this.sequelize = new Sequelize(conf.SIS_DB_URL!, {
|
||||
dialect: 'postgres',
|
||||
pool: {
|
||||
max: 25,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 300000000,
|
||||
},
|
||||
logging: false,
|
||||
password: conf.SIS_PASSWORD,
|
||||
models: [
|
||||
CourseModel,
|
||||
CourseProviderModel,
|
||||
CourseTypeModel,
|
||||
CreditModel,
|
||||
CreditTeacherModel,
|
||||
CreditTypeModel,
|
||||
CurriculumPeriodModel,
|
||||
EnrollmentModel,
|
||||
OrganizationModel,
|
||||
ProgrammeModuleModel,
|
||||
ProgrammeModuleChildModel,
|
||||
SemesterModel,
|
||||
SISStudyRightModel,
|
||||
SISStudyRightElementModel,
|
||||
StudentModel,
|
||||
StudyplanModel,
|
||||
StudyrightExtentModel,
|
||||
TeacherModel,
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
async connect(attempt = 1) {
|
||||
try {
|
||||
await this.sequelize.authenticate()
|
||||
this.emit('connect')
|
||||
this.established = true
|
||||
} catch (error) {
|
||||
if (attempt > this.RETRY_ATTEMPTS) {
|
||||
this.emit('error', error)
|
||||
return
|
||||
}
|
||||
logger.error({
|
||||
message: `Sis database connection failed! Attempt ${attempt}/${this.RETRY_ATTEMPTS}`,
|
||||
meta: error,
|
||||
})
|
||||
setTimeout(() => void this.connect(attempt + 1), 1000 * attempt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Old-style kone + user db connections
|
||||
const sequelizeKone = new Sequelize(conf.DB_URL_KONE!, {
|
||||
schema: conf.DB_SCHEMA_KONE,
|
||||
logging: false,
|
||||
password: conf.KONE_PASSWORD,
|
||||
models: [
|
||||
CustomPopulationSearchModel,
|
||||
ExcludedCourseModel,
|
||||
OpenUniPopulationSearchModel,
|
||||
ProgressCriteriaModel,
|
||||
StudyGuidanceGroupTagModel,
|
||||
StudyProgrammePinModel,
|
||||
TagModel,
|
||||
TagStudentModel,
|
||||
],
|
||||
})
|
||||
|
||||
void sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
|
||||
|
||||
const sequelizeUser = new Sequelize(conf.DB_URL_USER!, {
|
||||
logging: false,
|
||||
password: conf.USER_PASSWORD,
|
||||
models: [UserModel],
|
||||
})
|
||||
|
||||
const initializeDatabaseConnection = async () => {
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const rounds = 60
|
||||
|
||||
for (const [seq, dbName] of [
|
||||
[sequelizeKone, 'kone-db'],
|
||||
[sequelizeUser, 'user-db'],
|
||||
] as const) {
|
||||
logger.info(`Connecting to ${dbName}...`)
|
||||
for (let round = 1; round <= rounds; round++) {
|
||||
try {
|
||||
await seq.authenticate()
|
||||
break
|
||||
} catch (error) {
|
||||
if (round === rounds) {
|
||||
logger.error(`${dbName} database connection failed!`)
|
||||
throw error
|
||||
}
|
||||
await sleep(1000)
|
||||
}
|
||||
}
|
||||
logger.info(`${dbName} database connection established`)
|
||||
|
||||
const schema = dbName === 'kone-db' ? conf.DB_SCHEMA_KONE : undefined
|
||||
const migrationsFolder = dbName === 'kone-db' ? 'migrations_kone' : 'migrations_user'
|
||||
const migrator = new Umzug({
|
||||
storage: new SequelizeStorage({ sequelize: seq, tableName: 'migrations', schema }),
|
||||
migrations: {
|
||||
glob: `${process.cwd()}${conf.isProduction || conf.isStaging ? '/dist' : ''}/src/database/${migrationsFolder}/*.js`,
|
||||
},
|
||||
context: seq.getQueryInterface(),
|
||||
logger: console,
|
||||
})
|
||||
try {
|
||||
await migrator.up()
|
||||
logger.info({ message: `${dbName} migrations up to date` })
|
||||
} catch (error) {
|
||||
logger.error({ message: `${dbName} migrations failed`, meta: error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dbConnections = new DbConnection()
|
||||
|
||||
export { dbConnections, sequelizeKone, sequelizeUser, initializeDatabaseConnection }
|
||||
@@ -1,134 +0,0 @@
|
||||
module.exports = {
|
||||
up: async ({ context: queryInterface }) => {
|
||||
await queryInterface.sequelize.query(`
|
||||
-- Adminer 4.6.2 PostgreSQL dump
|
||||
|
||||
CREATE TABLE "public"."course_duplicates" (
|
||||
"groupid" integer NOT NULL,
|
||||
"coursecode" character varying(255) NOT NULL,
|
||||
"createdAt" timestamptz,
|
||||
"updatedAt" timestamptz,
|
||||
CONSTRAINT "course_duplicates_pkey" PRIMARY KEY ("coursecode")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
CREATE TYPE enum_thesis_courses_thesisType AS ENUM ('MASTER', 'BACHELOR');
|
||||
|
||||
|
||||
CREATE TABLE "public"."thesis_courses" (
|
||||
"programmeCode" character varying(255) NOT NULL,
|
||||
"courseCode" character varying(255) NOT NULL,
|
||||
"thesisType" enum_thesis_courses_thesisType,
|
||||
"createdAt" timestamptz,
|
||||
"updatedAt" timestamptz,
|
||||
CONSTRAINT "thesis_courses_pkey" PRIMARY KEY ("programmeCode", "courseCode")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
-- 2019-07-01 15:28:04.971883+00
|
||||
|
||||
-- Adminer 4.6.2 PostgreSQL dump
|
||||
|
||||
CREATE SEQUENCE course_groups_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1;
|
||||
|
||||
CREATE TABLE "public"."course_groups" (
|
||||
"id" bigint DEFAULT nextval('course_groups_id_seq') NOT NULL,
|
||||
"name" character varying(255),
|
||||
"createdAt" timestamptz,
|
||||
"updatedAt" timestamptz,
|
||||
"programmeid" character varying(255),
|
||||
CONSTRAINT "course_groups_name_key" UNIQUE ("name"),
|
||||
CONSTRAINT "course_groups_pkey" PRIMARY KEY ("id")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
CREATE TABLE "public"."filters" (
|
||||
"id" character varying(255) NOT NULL,
|
||||
"name" character varying(255),
|
||||
"filters" jsonb,
|
||||
"population" jsonb,
|
||||
"createdAt" timestamptz NOT NULL,
|
||||
"updatedAt" timestamptz NOT NULL,
|
||||
"description" character varying(255) DEFAULT '',
|
||||
CONSTRAINT "filters_pkey" PRIMARY KEY ("id")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
CREATE SEQUENCE mandatory_course_labels_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1;
|
||||
|
||||
CREATE TABLE "public"."mandatory_course_labels" (
|
||||
"studyprogramme_id" character varying(255),
|
||||
"id" bigint DEFAULT nextval('mandatory_course_labels_id_seq') NOT NULL,
|
||||
"label" character varying(255),
|
||||
"orderNumber" integer,
|
||||
"createdAt" timestamptz,
|
||||
"updatedAt" timestamptz,
|
||||
CONSTRAINT "mandatory_course_labels_pkey" PRIMARY KEY ("id")
|
||||
) WITH (oids = false);
|
||||
|
||||
CREATE INDEX "mandatory_course_labels_studyprogramme_id" ON "public"."mandatory_course_labels" USING btree ("studyprogramme_id");
|
||||
|
||||
|
||||
CREATE SEQUENCE mandatory_courses_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1;
|
||||
|
||||
CREATE TABLE "public"."mandatory_courses" (
|
||||
"id" bigint DEFAULT nextval('mandatory_courses_id_seq') NOT NULL,
|
||||
"course_code" character varying(255),
|
||||
"studyprogramme_id" character varying(255),
|
||||
"createdAt" timestamptz,
|
||||
"updatedAt" timestamptz,
|
||||
"label" bigint,
|
||||
CONSTRAINT "mandatory_courses_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "mandatory_courses_label_fkey" FOREIGN KEY (label) REFERENCES mandatory_course_labels(id) ON UPDATE CASCADE ON DELETE SET NULL NOT DEFERRABLE
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
CREATE SEQUENCE tag_tag_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1;
|
||||
|
||||
CREATE TABLE "public"."tag" (
|
||||
"createdAt" timestamptz,
|
||||
"updatedAt" timestamptz,
|
||||
"tagname" character varying(255),
|
||||
"studytrack" character varying(255) NOT NULL,
|
||||
"tag_id" bigint DEFAULT nextval('tag_tag_id_seq') NOT NULL,
|
||||
CONSTRAINT "tag_tag_id_key" UNIQUE ("tag_id")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
CREATE SEQUENCE tag_student_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1;
|
||||
|
||||
CREATE TABLE "public"."tag_student" (
|
||||
"createdAt" timestamptz,
|
||||
"updatedAt" timestamptz,
|
||||
"studentnumber" character varying(255),
|
||||
"id" bigint DEFAULT nextval('tag_student_id_seq') NOT NULL,
|
||||
"tag_id" bigint NOT NULL,
|
||||
CONSTRAINT "tag_student_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "tag_student_tag_id_fkey" FOREIGN KEY (tag_id) REFERENCES tag(tag_id) NOT DEFERRABLE
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
CREATE SEQUENCE teacher_course_group_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1;
|
||||
|
||||
CREATE TABLE "public"."teacher_course_groups" (
|
||||
"id" bigint DEFAULT nextval('teacher_course_group_id_seq') NOT NULL,
|
||||
"teacher_id" character varying(255),
|
||||
"course_group_id" bigint,
|
||||
"createdAt" timestamptz,
|
||||
"updatedAt" timestamptz,
|
||||
CONSTRAINT "teacher_course_group_pkey" PRIMARY KEY ("id")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
CREATE TABLE "public"."unit_tag" (
|
||||
"tags_tagname" character varying(255) NOT NULL,
|
||||
"unit_id" bigint NOT NULL,
|
||||
CONSTRAINT "unit_tag_pkey" PRIMARY KEY ("tags_tagname", "unit_id")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
-- 2019-07-01 15:26:14.147541+00
|
||||
`)
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
module.exports = {
|
||||
up: async ({ context: queryInterface }) => {
|
||||
await queryInterface.sequelize.query(`
|
||||
-- Adminer 4.6.2 PostgreSQL dump
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."usage_statistics" (
|
||||
"id" character varying(255) NOT NULL,
|
||||
"username" character varying(255),
|
||||
"name" character varying(255),
|
||||
"time" integer,
|
||||
"admin" boolean,
|
||||
"method" character varying(255),
|
||||
"URL" character varying(255),
|
||||
"status" integer,
|
||||
"data" jsonb,
|
||||
CONSTRAINT "usage_statistics_pkey" PRIMARY KEY ("id")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
-- 2019-07-03 15:39:09.675736+00
|
||||
`)
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
module.exports = {
|
||||
up: async ({ context: queryInterface }) => {
|
||||
return queryInterface.sequelize.transaction(async transaction => {
|
||||
await queryInterface.sequelize.query(
|
||||
"SELECT setval('mandatory_courses_id_seq', (SELECT MAX(id) FROM mandatory_courses))",
|
||||
{ transaction }
|
||||
)
|
||||
await queryInterface.sequelize.query(
|
||||
"SELECT setval('course_groups_id_seq', (SELECT MAX(id) FROM course_groups))",
|
||||
{ transaction }
|
||||
)
|
||||
await queryInterface.sequelize.query(
|
||||
"SELECT setval('mandatory_course_labels_id_seq', (SELECT MAX(id) FROM mandatory_course_labels))",
|
||||
{ transaction }
|
||||
)
|
||||
await queryInterface.sequelize.query("SELECT setval('tag_tag_id_seq', (SELECT MAX(tag_id) FROM tag))", {
|
||||
transaction,
|
||||
})
|
||||
await queryInterface.sequelize.query("SELECT setval('tag_student_id_seq', (SELECT MAX(id) FROM tag_student))", {
|
||||
transaction,
|
||||
})
|
||||
await queryInterface.sequelize.query(
|
||||
"SELECT setval('teacher_course_group_id_seq', (SELECT MAX(id) FROM teacher_course_groups))",
|
||||
{ transaction }
|
||||
)
|
||||
})
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
module.exports = {
|
||||
up: async ({ context: queryInterface }) => {
|
||||
return queryInterface.sequelize.transaction(async transaction => {
|
||||
await queryInterface.removeConstraint('tag_student', 'tag_student_tag_id_fkey', { transaction })
|
||||
await queryInterface.sequelize.query('DELETE FROM tag_student WHERE tag_id IS NULL', { transaction })
|
||||
await queryInterface.removeConstraint('tag', 'tag_tag_id_key', { transaction })
|
||||
await queryInterface.addConstraint('tag', {
|
||||
type: 'primary key',
|
||||
fields: ['tag_id'],
|
||||
transaction,
|
||||
})
|
||||
await queryInterface.sequelize.query(
|
||||
'ALTER TABLE tag_student ADD CONSTRAINT "tag_student_tag_id_fkey" FOREIGN KEY (tag_id) REFERENCES tag(tag_id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE',
|
||||
{ transaction }
|
||||
)
|
||||
await queryInterface.removeColumn('tag_student', 'id', { transaction })
|
||||
await queryInterface.addConstraint('tag_student', {
|
||||
type: 'primary key',
|
||||
fields: ['studentnumber', 'tag_id'],
|
||||
transaction,
|
||||
})
|
||||
})
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
const { STRING } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async ({ context: queryInterface }) => {
|
||||
await queryInterface.bulkDelete('tag', [])
|
||||
await queryInterface.addColumn('tag', 'year', { type: STRING })
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
const { BIGINT } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async ({ context: queryInterface }) => {
|
||||
await queryInterface.addColumn('tag', 'personal_user_id', {
|
||||
type: BIGINT,
|
||||
})
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
const { BIGINT, STRING, DATE, ARRAY } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async ({ context: queryInterface }) => {
|
||||
await queryInterface.createTable('custom_population_searches', {
|
||||
id: {
|
||||
type: BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
userId: {
|
||||
type: BIGINT,
|
||||
},
|
||||
name: {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
students: {
|
||||
type: ARRAY(STRING),
|
||||
},
|
||||
createdAt: DATE,
|
||||
updatedAt: DATE,
|
||||
})
|
||||
},
|
||||
down: () => {},
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
const { STRING, DATE, JSONB } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async ({ context: queryInterface }) => {
|
||||
await queryInterface.createTable('programme_modules', {
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: STRING,
|
||||
},
|
||||
code: {
|
||||
type: STRING,
|
||||
},
|
||||
name: {
|
||||
type: JSONB,
|
||||
},
|
||||
type: {
|
||||
type: STRING,
|
||||
},
|
||||
created_at: {
|
||||
type: DATE,
|
||||
},
|
||||
updated_at: {
|
||||
type: DATE,
|
||||
},
|
||||
})
|
||||
await queryInterface.createTable('programme_module_children', {
|
||||
composite: {
|
||||
type: STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
parent_id: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'programme_modules',
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
child_id: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'programme_modules',
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
created_at: {
|
||||
type: DATE,
|
||||
},
|
||||
updated_at: {
|
||||
type: DATE,
|
||||
},
|
||||
})
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
const { INTEGER } = require('sequelize')
|
||||
|
||||
// Creates the exact same table as in last migration, but because staging data was messed,
|
||||
// this has to be done
|
||||
|
||||
module.exports = {
|
||||
up: async ({ context: queryInterface }) => {
|
||||
await queryInterface.addColumn('programme_modules', 'order', {
|
||||
type: INTEGER,
|
||||
})
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user