mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8c665ca5e | ||
|
|
5abac85e81 | ||
|
|
cf15dfab72 | ||
|
|
9ca60cacc5 | ||
|
|
8747161f0c | ||
|
|
03c13cedd2 | ||
|
|
086d1f89f4 | ||
|
|
3335891b5a | ||
|
|
1b36ac9dfe | ||
|
|
60d08ed9eb | ||
|
|
dc51480e27 | ||
|
|
aa30506df2 | ||
|
|
c929cd2325 | ||
|
|
0fef278dd8 | ||
|
|
1e9cab4b46 | ||
|
|
2beea33a4c | ||
|
|
6d0acb4091 | ||
|
|
6d8d41dbe1 | ||
|
|
e3f6c4a244 | ||
|
|
818b9e4e1c | ||
|
|
b8bbe7634e | ||
|
|
6e8e63b38d | ||
|
|
04deef0c2b | ||
|
|
a8e75754cd | ||
|
|
c350adac64 | ||
|
|
59b79d26d2 | ||
|
|
cd8f5b11b1 | ||
|
|
b8e110e39a | ||
|
|
95b60ed378 | ||
|
|
9dac180b95 | ||
|
|
908985410b | ||
|
|
9f686fafd5 | ||
|
|
59963e4961 | ||
|
|
1f1dc624e4 | ||
|
|
3e374f29d0 | ||
|
|
9c060874eb | ||
|
|
3a04397e5d | ||
|
|
6ae174a1c1 | ||
|
|
738ff03d2c | ||
|
|
b11d45da60 | ||
|
|
e2bc22fdab | ||
|
|
1433e949bc | ||
|
|
7040d78de8 | ||
|
|
79ff2887fb | ||
|
|
eac2789951 | ||
|
|
9688747773 | ||
|
|
27616be349 | ||
|
|
d330b84d1c | ||
|
|
9275058e31 | ||
|
|
de8a090898 | ||
|
|
ae393fa857 | ||
|
|
e46faa0904 | ||
|
|
ffa0120bd7 | ||
|
|
ed8f282cf5 | ||
|
|
f0e9a66a7e | ||
|
|
0e8c3696eb | ||
|
|
db986ee0fb | ||
|
|
a35f9cc6d3 | ||
|
|
d60232b954 | ||
|
|
5ef73f0094 | ||
|
|
433570f124 | ||
|
|
7bf8c54efd | ||
|
|
81da962991 | ||
|
|
d3d2c2aaf2 | ||
|
|
8fc2019358 | ||
|
|
7f253257b1 | ||
|
|
43c866fa74 | ||
|
|
a5c842222f | ||
|
|
2e948c802c | ||
|
|
da64f63d48 | ||
|
|
800012ef05 | ||
|
|
b29d478c86 | ||
|
|
d4506fe70f | ||
|
|
80159e46f0 | ||
|
|
3a834eb5f4 | ||
|
|
e56e553f25 | ||
|
|
d6e32d2b19 | ||
|
|
1413745a3a | ||
|
|
e174dc5403 | ||
|
|
45cd8d82d9 | ||
|
|
a10e3dbaf8 | ||
|
|
afa652db30 | ||
|
|
a1165c76dc | ||
|
|
affea21c5a | ||
|
|
3e68dcd494 | ||
|
|
eb6837a079 | ||
|
|
a29e01b6d9 | ||
|
|
6916824d26 | ||
|
|
21127a7d6c | ||
|
|
af9f7c06e6 | ||
|
|
f882a22263 | ||
|
|
89ae035cf3 | ||
|
|
1767790adb | ||
|
|
a46ff9b570 | ||
|
|
25b6de49c0 | ||
|
|
c6903ebe25 | ||
|
|
2d2c25b763 |
@@ -4,7 +4,7 @@ description: 'Runs ESLint and Stylelint for frontend and backend'
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
- name: 'Install dependencies'
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: 'Get current time'
|
||||
id: get_current_time
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Extract release version
|
||||
id: extract_version
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
- release_backend
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Extract release version
|
||||
id: extract_version
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
- release_backend
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Extract release version
|
||||
id: extract_version
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: 'Send notification to Slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||
with:
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
if: failure()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: 'Send notification to Slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||
with:
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
name: 'Lint packages'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Lint backend, frontend and shared
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
spec: cypress/e2e/University.js
|
||||
name: ${{ matrix.name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
|
||||
@@ -18,7 +18,7 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
uses: UniversityOfHelsinkiCS/oodikone/.github/workflows/test.yaml@master
|
||||
secrets:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Get current time
|
||||
id: get_current_time
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Build backend image
|
||||
id: build-image
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: 'Send notification to Slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||
with:
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
if: failure()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: 'Send notification to Slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||
with:
|
||||
|
||||
+15
-12
@@ -16,7 +16,7 @@ jobs:
|
||||
name: 'Lint packages'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Lint backend, frontend and shared
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
name: 'Build backend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: 'Build image and export it'
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
name: 'Build frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Get current time
|
||||
id: get_current_time
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
user_db_migrations: ${{ steps.check_migrations.outputs.user_db_migrations }}
|
||||
sis_db_migrations: ${{ steps.check_migrations.outputs.sis_db_migrations }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Check if new migrations have been added
|
||||
id: check_migrations
|
||||
uses: dorny/paths-filter@v3
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check_migration_files.outputs.sis_db_migrations == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -120,9 +120,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check_migration_files.outputs.kone_db_migrations == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: 'Download backend image'
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: backend-image
|
||||
- name: 'Load built backend image'
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check_migration_files.outputs.user_db_migrations == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: check_migrations
|
||||
with:
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
- 'services/backend/src/database/migrations_user/**'
|
||||
- '.github/workflows/test.yaml'
|
||||
- name: 'Download backend image'
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: backend-image
|
||||
- name: 'Load built backend image'
|
||||
@@ -176,7 +176,6 @@ jobs:
|
||||
|
||||
# === End-to-end tests ===
|
||||
test_end_to_end:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
@@ -215,9 +214,13 @@ jobs:
|
||||
spec: cypress/e2e/Study_guidance_groups.js
|
||||
- name: 'Test university'
|
||||
spec: cypress/e2e/University.js
|
||||
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
@@ -236,7 +239,7 @@ jobs:
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
|
||||
@@ -20,8 +20,8 @@ jobs:
|
||||
name: 'Lint scheduler and worker'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
- name: 'Install dependencies'
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
name: 'Build Updater worker'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
name: 'Build Updater scheduler'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
- build_worker
|
||||
- build_scheduler
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Download built images'
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: 'updater-{worker,scheduler}-image'
|
||||
merge-multiple: true
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
needs: test_updater
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Build worker image
|
||||
id: build-image
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
needs: test_updater
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Build scheduler image
|
||||
id: build-image
|
||||
|
||||
@@ -14,7 +14,6 @@ export default {
|
||||
Dockerfile: files =>
|
||||
`${dockerCmdBase} hadolint/hadolint:v2.12.0 hadolint --ignore DL3006 ${relativeFilePaths(files)}`,
|
||||
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck:v0.7.2 ${relativeFilePaths(files)} -x`,
|
||||
'.github/{workflows,actions}/*': files => `npm run actionlint ${relativeFilePaths(files)}`,
|
||||
'docker-compose*': files => {
|
||||
const composeFiles = file => {
|
||||
if (['docker-compose.ci.yml', 'docker-compose.test.yml'].some(f => file.includes(f))) {
|
||||
|
||||
@@ -60,7 +60,7 @@ set_custom_select_prompt() {
|
||||
# Run scripts and set the prompt
|
||||
show_welcome
|
||||
init_dirs
|
||||
get_username
|
||||
get_s3_config
|
||||
set_custom_select_prompt
|
||||
|
||||
while true; do
|
||||
|
||||
@@ -19,7 +19,7 @@ describe("Population statistics with a master's programme", { testIsolation: fal
|
||||
|
||||
it('Study track filter works', () => {
|
||||
cy.cs('studyTrackFilter-header').click()
|
||||
cy.selectFromDropdown('studyTrackFilter', 0)
|
||||
cy.selectFromDropdown('studyTrackFilter', 0, true)
|
||||
checkFilteredStudentCount(15)
|
||||
|
||||
cy.cs('studyTrackFilter-clear').click()
|
||||
@@ -179,11 +179,11 @@ describe.skip('Population Statistics', { testIsolation: false }, () => {
|
||||
]
|
||||
|
||||
cy.cs('courseFilter-header').click()
|
||||
cy.selectFromDropdown('courseFilter', `${courses[0].code} - ${courses[0].name}`)
|
||||
cy.selectFromAutocomplete('courseFilter', `${courses[0].code} - ${courses[0].name}`)
|
||||
checkFilteredStudentCount(9)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[0].code}`, 1)
|
||||
checkFilteredStudentCount(7)
|
||||
cy.selectFromDropdown('courseFilter', `${courses[1].code} - ${courses[1].name}`)
|
||||
cy.selectFromAutocomplete('courseFilter', `${courses[1].code} - ${courses[1].name}`)
|
||||
checkFilteredStudentCount(5)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[1].code}`, 1)
|
||||
checkFilteredStudentCount(4)
|
||||
@@ -449,11 +449,11 @@ describe('Custom Population Statistics', { testIsolation: false }, () => {
|
||||
]
|
||||
|
||||
cy.cs('courseFilter-header').click()
|
||||
cy.selectFromDropdown('courseFilter', `${courses[0].code} - ${courses[0].name}`)
|
||||
cy.selectFromAutocomplete('courseFilter', `${courses[0].code} - ${courses[0].name}`)
|
||||
checkFilteredStudentCount(7)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[0].code}`, 1)
|
||||
checkFilteredStudentCount(6)
|
||||
cy.selectFromDropdown('courseFilter', `${courses[1].code} - ${courses[1].name}`)
|
||||
cy.selectFromAutocomplete('courseFilter', `${courses[1].code} - ${courses[1].name}`)
|
||||
checkFilteredStudentCount(5)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[1].code}`, 3)
|
||||
checkFilteredStudentCount(2)
|
||||
|
||||
@@ -208,22 +208,22 @@ describe('Population statistics tests', () => {
|
||||
const categories = [
|
||||
{
|
||||
selector: 'All students of the population',
|
||||
data: ['3773.50', '139.76', '141.00', '60.07', '19', '268'],
|
||||
data: ['6136.50', '227.28', '197.00', '117.94', '60', '533'],
|
||||
size: 27,
|
||||
},
|
||||
{
|
||||
selector: 'Muu',
|
||||
data: ['359.00', '119.67', '111.00', '43.71', '71', '177'],
|
||||
data: ['580.00', '193.33', '192.00', '36.75', '149', '239'],
|
||||
size: 3,
|
||||
},
|
||||
{
|
||||
selector: 'Todistusvalinta',
|
||||
data: ['2742.50', '152.36', '178.00', '65.75', '19', '268'],
|
||||
data: ['3657.50', '203.19', '193.50', '98.35', '60', '460'],
|
||||
size: 18,
|
||||
},
|
||||
{
|
||||
selector: 'Avoin väylä',
|
||||
data: ['672.00', '112.00', '104.00', '29.40', '82', '169'],
|
||||
data: ['1899.00', '316.50', '257.00', '150.43', '150', '533'],
|
||||
size: 6,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -122,6 +122,19 @@ Cypress.Commands.add('selectFromDropdown', { prevSubject: false }, (label, index
|
||||
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.
|
||||
|
||||
@@ -120,6 +120,7 @@ services:
|
||||
- redis
|
||||
environment:
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
SECRET_TOKEN: dev
|
||||
SIS_IMPORTER_HOST: ${SIS_IMPORTER_DB_HOST:-host.docker.internal}
|
||||
SIS_IMPORTER_PORT: ${SIS_IMPORTER_DB_PORT:-15234}
|
||||
@@ -148,6 +149,7 @@ services:
|
||||
environment:
|
||||
DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
SIS_IMPORTER_HOST: ${SIS_IMPORTER_DB_HOST:-host.docker.internal}
|
||||
SIS_IMPORTER_PORT: ${SIS_IMPORTER_DB_PORT:-15234}
|
||||
SIS_IMPORTER_DATABASE: importer-db
|
||||
|
||||
Generated
+444
-664
File diff suppressed because it is too large
Load Diff
+6
-5
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"name": "oodikone",
|
||||
"description": "An application for analyzing university data.",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"cypress": "cypress",
|
||||
"cypress:open": "cypress open",
|
||||
@@ -9,7 +12,6 @@
|
||||
"eslint": "eslint",
|
||||
"lint:all": "eslint services/ updater/",
|
||||
"stylelint": "stylelint",
|
||||
"actionlint": "node-actionlint",
|
||||
"prettier": "prettier",
|
||||
"cli": "./cli.sh",
|
||||
"oodikone": "./run.sh oodikone anon up --detach",
|
||||
@@ -44,11 +46,11 @@
|
||||
"@cypress/code-coverage": "^3.14.4",
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"cypress": "^14.4.1",
|
||||
"cypress": "^15.0.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-import-resolver-typescript": "^4.4.3",
|
||||
"eslint-plugin-cypress": "^5.1.0",
|
||||
"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",
|
||||
@@ -56,11 +58,10 @@
|
||||
"globals": "^16.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.1.0",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"nyc": "^17.1.0",
|
||||
"prettier": "3.5.3",
|
||||
"stylelint": "^16.20.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"stylelint-config-standard": "^39.0.0",
|
||||
"typescript-eslint": "^8.34.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
## Folders
|
||||
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
||||
DUMP_DIR="$PROJECT_ROOT/.databasedumps"
|
||||
USER_DATA_FILE="$DUMP_DIR/hyuserdata"
|
||||
S3_CONFIG_FILE=~/.s3cfg
|
||||
S3_BUCKET="s3://psyduck"
|
||||
DOCKER_COMPOSE=$PROJECT_ROOT/docker-compose.yml
|
||||
|
||||
## Following the naming convention in docker-compose, these are names for database
|
||||
@@ -18,14 +19,16 @@ KONE_DB_NAME="kone-db"
|
||||
SIS_DB_NAME="sis-db"
|
||||
SIS_IMPORTER_DB_NAME="sis-importer-db"
|
||||
USER_DB_NAME="user-db"
|
||||
DATABASES=("$KONE_DB_NAME" "$SIS_DB_NAME" "$SIS_IMPORTER_DB_NAME" "$USER_DB_NAME")
|
||||
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_REAL_DUMP_URL="svm-116.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/oodikone-kone-db.sql.gz"
|
||||
SIS_DB_REAL_DUMP_URL="svm-116.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/oodikone-sis-db.sql.gz"
|
||||
SIS_IMPORTER_DB_REAL_DUMP_URL="svm-116.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/importer.sql.gz"
|
||||
USER_DB_REAL_DUMP_URL="svm-116.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/oodikone-user-db.sql.gz"
|
||||
REAL_DUMP_URLS=("$KONE_DB_REAL_DUMP_URL" "$SIS_DB_REAL_DUMP_URL" "$SIS_IMPORTER_DB_REAL_DUMP_URL" "$USER_DB_REAL_DUMP_URL")
|
||||
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
|
||||
@@ -55,9 +58,46 @@ retry() {
|
||||
|
||||
download_real_dump() {
|
||||
local database=$1
|
||||
local pannu_url=$2
|
||||
local dump_destination="$DUMP_DIR/$database.sql.gz"
|
||||
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkki.cs.helsinki.fi" "$username@$pannu_url" "$dump_destination"
|
||||
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() {
|
||||
@@ -101,11 +141,12 @@ reset_databases() {
|
||||
|
||||
reset_jami_data() {
|
||||
local database="jami-db"
|
||||
local pannu_url="svm-116.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/jami.sql.gz"
|
||||
local dump_destination="$DUMP_DIR/$database.sql.gz"
|
||||
local s3_path="jami"
|
||||
|
||||
infomsg "Removing old data"
|
||||
|
||||
|
||||
infomsg "Downloading Jami data"
|
||||
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkki.cs.helsinki.fi" "$username@$pannu_url" "$dump_destination"
|
||||
|
||||
infomsg "Removing database and related volume"
|
||||
docker volume rm oodikone_jami-data || warningmsg "This is okay, continuing"
|
||||
@@ -122,13 +163,12 @@ reset_jami_data() {
|
||||
}
|
||||
|
||||
reset_all_real_data() {
|
||||
infomsg "Downloading real data dumps, asking for pannu password when needed"
|
||||
infomsg "Downloading real data dumps"
|
||||
for i in ${!DATABASES[*]}; do
|
||||
local database="${DATABASES[$i]}"
|
||||
local url="${REAL_DUMP_URLS[$i]}"
|
||||
download_real_dump "$database" "$url"
|
||||
local url="${S3_PATHS[$i]}"
|
||||
download_real_dump "$database" "$url" false
|
||||
done
|
||||
reset_jami_data
|
||||
reset_databases "${DATABASES[@]}"
|
||||
}
|
||||
|
||||
@@ -152,28 +192,30 @@ reset_single_database() {
|
||||
|
||||
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_REAL_DUMP_URL
|
||||
local url=$KONE_DB_S3_PATH
|
||||
break 2;;
|
||||
"sis-db")
|
||||
local database=$SIS_DB_NAME
|
||||
local url=$SIS_DB_REAL_DUMP_URL
|
||||
local url=$SIS_DB_S3_PATH
|
||||
break 2;;
|
||||
"sis-importer-db")
|
||||
local database=$SIS_IMPORTER_DB_NAME
|
||||
local url=$SIS_IMPORTER_DB_REAL_DUMP_URL
|
||||
local url=$SIS_IMPORTER_DB_S3_PATH
|
||||
break 2;;
|
||||
"user-db")
|
||||
local database=$USER_DB_NAME
|
||||
local url=$USER_DB_REAL_DUMP_URL
|
||||
local url=$USER_DB_S3_PATH
|
||||
break 2;;
|
||||
"jami-db")
|
||||
reset_jami_data
|
||||
local database=$JAMI_DB_NAME
|
||||
local url=$JAMI_DB_S3_PATH
|
||||
break 2;;
|
||||
"Return to main menu.")
|
||||
break 2;;
|
||||
@@ -186,7 +228,7 @@ reset_single_database() {
|
||||
|
||||
if [ -n "$database" ]; then
|
||||
infomsg "Downloading $database dump"
|
||||
download_real_dump $database $url
|
||||
download_real_dump $database $url $selection_flag
|
||||
reset_databases $database
|
||||
fi
|
||||
}
|
||||
@@ -242,14 +284,11 @@ init_dirs() {
|
||||
|
||||
# If username is not set, get username from data file.
|
||||
# Ask user to provide username, if username was not found from data file.
|
||||
get_username() {
|
||||
if [ ! -f "$USER_DATA_FILE" ]; then
|
||||
warningmsg "University username is needed to get database dumps from toska servers, please enter it now:"
|
||||
read -r username
|
||||
echo "$username" > "$USER_DATA_FILE"
|
||||
successmsg "Succesfully saved username for later usage."
|
||||
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
|
||||
username=$(head -n 1 < "$USER_DATA_FILE")
|
||||
|
||||
infomsg "Using your university username - $username - for getting database dumps"
|
||||
infomsg "Using your config $S3_CONFIG_FILE - for getting database dumps"
|
||||
}
|
||||
|
||||
Generated
+1719
-1232
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@oodikone/shared": "file:../shared",
|
||||
"@sentry/node": "^9.28.1",
|
||||
"@sentry/profiling-node": "^9.28.1",
|
||||
"@sentry/node": "^10.5.0",
|
||||
"@sentry/profiling-node": "^10.5.0",
|
||||
"axios": "^1.9.0",
|
||||
"bullmq": "^5.53.2",
|
||||
"compression": "^1.8.0",
|
||||
@@ -40,7 +40,7 @@
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/validator": "^13.15.1",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "^5.8.3"
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"overrides": {
|
||||
"winston-gelf-transporter": {
|
||||
|
||||
@@ -11,16 +11,7 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import type {
|
||||
Credit,
|
||||
Enrollment,
|
||||
Organization,
|
||||
SISStudyRight,
|
||||
SISStudyRightElement,
|
||||
Student,
|
||||
Studyplan,
|
||||
} from '@oodikone/shared/models'
|
||||
import { ExtentCode, SemesterEnrollment } from '@oodikone/shared/types'
|
||||
import type { SISStudyRight } from '@oodikone/shared/models'
|
||||
|
||||
import { CreditModel } from './credit'
|
||||
import { EnrollmentModel } from './enrollment'
|
||||
@@ -34,69 +25,69 @@ import { StudyrightExtentModel } from './studyrightExtent'
|
||||
underscored: true,
|
||||
tableName: 'sis_study_rights',
|
||||
})
|
||||
export class SISStudyRightModel extends Model<SISStudyRight> implements SISStudyRight {
|
||||
export class SISStudyRightModel extends Model implements SISStudyRight {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
declare id: SISStudyRight['id']
|
||||
|
||||
@ForeignKey(() => OrganizationModel)
|
||||
@Column(DataType.STRING)
|
||||
facultyCode!: string
|
||||
declare facultyCode: SISStudyRight['facultyCode']
|
||||
|
||||
@BelongsTo(() => OrganizationModel, { foreignKey: 'facultyCode', targetKey: 'code' })
|
||||
organization!: Organization
|
||||
declare organization: SISStudyRight['organization']
|
||||
|
||||
@HasMany(() => StudyplanModel)
|
||||
studyPlans!: Studyplan[]
|
||||
declare studyPlans: SISStudyRight['studyPlans']
|
||||
|
||||
@HasMany(() => SISStudyRightElementModel, { foreignKey: 'studyRightId' })
|
||||
studyRightElements!: SISStudyRightElement[]
|
||||
declare studyRightElements: SISStudyRight['studyRightElements']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
startDate!: Date
|
||||
declare startDate: SISStudyRight['startDate']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
endDate!: Date
|
||||
declare endDate: SISStudyRight['endDate']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
studyStartDate!: Date
|
||||
declare studyStartDate: SISStudyRight['studyStartDate']
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
cancelled!: boolean
|
||||
declare cancelled: SISStudyRight['cancelled']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
studentNumber!: string
|
||||
declare studentNumber: SISStudyRight['studentNumber']
|
||||
|
||||
@BelongsTo(() => StudentModel, { foreignKey: 'studentNumber', targetKey: 'studentnumber' })
|
||||
student!: Student
|
||||
declare student: SISStudyRight['student']
|
||||
|
||||
@ForeignKey(() => StudyrightExtentModel)
|
||||
@Column(DataType.INTEGER)
|
||||
extentCode!: ExtentCode
|
||||
declare extentCode: SISStudyRight['extentCode']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
admissionType!: string
|
||||
declare admissionType: SISStudyRight['admissionType']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
semesterEnrollments!: SemesterEnrollment[] | null
|
||||
declare semesterEnrollments: SISStudyRight['semesterEnrollments']
|
||||
|
||||
@HasMany(() => CreditModel, { foreignKey: 'studyright_id', sourceKey: 'id' })
|
||||
credits!: Credit[]
|
||||
declare credits: SISStudyRight['credits']
|
||||
|
||||
@HasMany(() => EnrollmentModel, { foreignKey: 'studyright_id', sourceKey: 'id' })
|
||||
enrollments!: Enrollment[]
|
||||
declare enrollments: SISStudyRight['enrollments']
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
tvex!: boolean
|
||||
declare tvex: SISStudyRight['tvex']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
expirationRuleUrns!: string[]
|
||||
declare expirationRuleUrns: SISStudyRight['expirationRuleUrns']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: SISStudyRight['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: SISStudyRight['updatedAt']
|
||||
}
|
||||
|
||||
@@ -10,57 +10,54 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import type { SISStudyRight, SISStudyRightElement } from '@oodikone/shared/models'
|
||||
import type { Name, StudyTrack } from '@oodikone/shared/types'
|
||||
import { DegreeProgrammeType, Phase } from '@oodikone/shared/types'
|
||||
|
||||
import type { SISStudyRightElement } from '@oodikone/shared/models'
|
||||
import { SISStudyRightModel } from './SISStudyRight'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
tableName: 'sis_study_right_elements',
|
||||
})
|
||||
export class SISStudyRightElementModel extends Model<SISStudyRightElement> implements SISStudyRightElement {
|
||||
export class SISStudyRightElementModel extends Model implements SISStudyRightElement {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
declare id: SISStudyRightElement['id']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
startDate!: Date
|
||||
declare startDate: SISStudyRightElement['startDate']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
endDate!: Date
|
||||
declare endDate: SISStudyRightElement['endDate']
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
graduated!: boolean
|
||||
declare graduated: SISStudyRightElement['graduated']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
phase!: Phase
|
||||
declare phase: SISStudyRightElement['phase']
|
||||
|
||||
@ForeignKey(() => SISStudyRightModel)
|
||||
@Column(DataType.STRING)
|
||||
studyRightId!: string
|
||||
declare studyRightId: SISStudyRightElement['studyRightId']
|
||||
|
||||
@BelongsTo(() => SISStudyRightModel, { foreignKey: 'studyRightId' })
|
||||
studyRight!: SISStudyRight
|
||||
declare studyRight: SISStudyRightElement['studyRight']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
code!: string
|
||||
declare code: SISStudyRightElement['code']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Name
|
||||
declare name: SISStudyRightElement['name']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
studyTrack!: StudyTrack | null
|
||||
declare studyTrack: SISStudyRightElement['studyTrack']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
degreeProgrammeType!: DegreeProgrammeType
|
||||
declare degreeProgrammeType: SISStudyRightElement['degreeProgrammeType']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: SISStudyRightElement['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: SISStudyRightElement['updatedAt']
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import type { Course, CourseType, Credit, Enrollment, Organization } from '@oodikone/shared/models'
|
||||
import type { Name } from '@oodikone/shared/types'
|
||||
import type { Course } from '@oodikone/shared/models'
|
||||
|
||||
import { CourseProviderModel } from './courseProvider'
|
||||
import { CourseTypeModel } from './courseType'
|
||||
@@ -26,57 +25,57 @@ import { OrganizationModel } from './organization'
|
||||
modelName: 'course',
|
||||
tableName: 'course',
|
||||
})
|
||||
export class CourseModel extends Model<Course> implements Course {
|
||||
export class CourseModel extends Model implements Course {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
declare id: Course['id']
|
||||
|
||||
@ForeignKey(() => CourseModel)
|
||||
@Column(DataType.STRING)
|
||||
code!: string
|
||||
declare code: Course['code']
|
||||
|
||||
@ForeignKey(() => CourseTypeModel)
|
||||
@Column(DataType.STRING)
|
||||
coursetypecode!: string
|
||||
declare coursetypecode: Course['coursetypecode']
|
||||
|
||||
@BelongsTo(() => CourseTypeModel, { foreignKey: 'coursetypecode', targetKey: 'coursetypecode' })
|
||||
courseType!: CourseType
|
||||
declare courseType: Course['courseType']
|
||||
|
||||
@HasMany(() => CreditModel, { foreignKey: 'course_id', sourceKey: 'id' })
|
||||
credits!: Credit[]
|
||||
declare credits: Course['credits']
|
||||
|
||||
@HasMany(() => EnrollmentModel, { foreignKey: 'course_id' })
|
||||
enrollments!: Enrollment[]
|
||||
declare enrollments: Course['enrollments']
|
||||
|
||||
@BelongsToMany(() => OrganizationModel, () => CourseProviderModel, 'coursecode')
|
||||
organizations!: Organization[]
|
||||
declare organizations: Course['organizations']
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
is_study_module!: boolean
|
||||
declare is_study_module: Course['is_study_module']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Name
|
||||
declare name: Course['name']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
max_attainment_date!: Date
|
||||
declare max_attainment_date: Course['max_attainment_date']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
min_attainment_date!: Date
|
||||
declare min_attainment_date: Course['min_attainment_date']
|
||||
|
||||
@CreatedAt
|
||||
@Column({ field: 'created_at', type: DataType.DATE })
|
||||
createdAt!: Date
|
||||
declare createdAt: Course['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column({ field: 'updated_at', type: DataType.DATE })
|
||||
updatedAt!: Date
|
||||
declare updatedAt: Course['updatedAt']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
substitutions!: string[]
|
||||
declare substitutions: Course['substitutions']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
course_unit_type!: string
|
||||
declare course_unit_type: Course['course_unit_type']
|
||||
|
||||
@Column({ field: 'main_course_code', type: DataType.STRING })
|
||||
mainCourseCode!: string
|
||||
declare mainCourseCode: Course['mainCourseCode']
|
||||
}
|
||||
|
||||
@@ -10,23 +10,23 @@ import { OrganizationModel } from './organization'
|
||||
modelName: 'course_provider',
|
||||
tableName: 'course_providers',
|
||||
})
|
||||
export class CourseProviderModel extends Model<CourseProvider> implements CourseProvider {
|
||||
export class CourseProviderModel extends Model implements CourseProvider {
|
||||
@ForeignKey(() => CourseModel)
|
||||
@Column(DataType.STRING)
|
||||
coursecode!: string
|
||||
declare coursecode: CourseProvider['coursecode']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
shares!: Array<{ share: number; startDate?: string; endDate?: string }> | null
|
||||
declare shares: CourseProvider['shares']
|
||||
|
||||
@ForeignKey(() => OrganizationModel)
|
||||
@Column(DataType.STRING)
|
||||
organizationcode!: string
|
||||
declare organizationcode: CourseProvider['organizationcode']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: CourseProvider['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: CourseProvider['updatedAt']
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import type { Course, CourseType } from '@oodikone/shared/models'
|
||||
import type { CourseType } from '@oodikone/shared/models'
|
||||
|
||||
import { Name } from '@oodikone/shared/types'
|
||||
import { CourseModel } from './course'
|
||||
|
||||
@Table({
|
||||
@@ -10,22 +9,22 @@ import { CourseModel } from './course'
|
||||
modelName: 'course_type',
|
||||
tableName: 'course_types',
|
||||
})
|
||||
export class CourseTypeModel extends Model<CourseType> implements CourseType {
|
||||
export class CourseTypeModel extends Model implements CourseType {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
coursetypecode!: string
|
||||
declare coursetypecode: CourseType['coursetypecode']
|
||||
|
||||
@HasMany(() => CourseModel)
|
||||
courses!: Course[]
|
||||
declare courses: CourseType['courses']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Required<Name>
|
||||
declare name: CourseType['name']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: CourseType['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: CourseType['updatedAt']
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import type { Credit, Course, CreditType, Semester, SISStudyRight, Student, Teacher } from '@oodikone/shared/models'
|
||||
import type { Credit } from '@oodikone/shared/models'
|
||||
import { CreditTypeCode } from '@oodikone/shared/types'
|
||||
|
||||
import { CourseModel } from './course'
|
||||
@@ -27,85 +27,85 @@ import { TeacherModel } from './teacher'
|
||||
modelName: 'credit',
|
||||
tableName: 'credit',
|
||||
})
|
||||
export class CreditModel extends Model<Credit> implements Credit {
|
||||
export class CreditModel extends Model implements Credit {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
declare id: Credit['id']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
grade!: string
|
||||
declare grade: Credit['grade']
|
||||
|
||||
@ForeignKey(() => StudentModel)
|
||||
@Column(DataType.STRING)
|
||||
student_studentnumber!: string
|
||||
declare student_studentnumber: Credit['student_studentnumber']
|
||||
|
||||
@BelongsTo(() => StudentModel, { foreignKey: 'student_studentnumber', targetKey: 'studentnumber' })
|
||||
student!: Student
|
||||
declare student: Credit['student']
|
||||
|
||||
@BelongsTo(() => SemesterModel, { foreignKey: 'semester_composite' })
|
||||
semester!: Semester
|
||||
declare semester: Credit['semester']
|
||||
|
||||
@Column(DataType.DOUBLE)
|
||||
credits!: number
|
||||
declare credits: Credit['credits']
|
||||
|
||||
@ForeignKey(() => CreditTypeModel)
|
||||
@Column(DataType.INTEGER)
|
||||
credittypecode!: CreditTypeCode
|
||||
declare credittypecode: Credit['credittypecode']
|
||||
|
||||
@BelongsTo(() => CreditTypeModel, { foreignKey: 'credittypecode', targetKey: 'credittypecode' })
|
||||
creditType!: CreditType
|
||||
declare creditType: Credit['creditType']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
createdate!: Date
|
||||
declare createdate: Credit['createdate']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
attainment_date!: Date
|
||||
declare attainment_date: Credit['attainment_date']
|
||||
|
||||
@BelongsToMany(() => TeacherModel, () => CreditTeacherModel, 'credit_id')
|
||||
teachers!: Teacher[]
|
||||
declare teachers: Credit['teachers']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
course_code!: string
|
||||
declare course_code: Credit['course_code']
|
||||
|
||||
@BelongsTo(() => CourseModel, { foreignKey: 'course_id' })
|
||||
course!: Course
|
||||
declare course: Credit['course']
|
||||
|
||||
@ForeignKey(() => CourseModel)
|
||||
@Column(DataType.STRING)
|
||||
course_id!: string
|
||||
declare course_id: Credit['course_id']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
semester_composite!: string
|
||||
declare semester_composite: Credit['semester_composite']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
semestercode!: number
|
||||
declare semestercode: Credit['semestercode']
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
isStudyModule!: boolean
|
||||
declare isStudyModule: Credit['isStudyModule']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
org!: string
|
||||
declare org: Credit['org']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
language!: string
|
||||
declare language: Credit['language']
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
is_open!: boolean
|
||||
declare is_open: Credit['is_open']
|
||||
|
||||
@ForeignKey(() => SISStudyRightModel)
|
||||
@Column(DataType.STRING)
|
||||
studyright_id!: string
|
||||
declare studyright_id: Credit['studyright_id']
|
||||
|
||||
@BelongsTo(() => SISStudyRightModel)
|
||||
studyright!: SISStudyRight
|
||||
declare studyright: Credit['studyright']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: Credit['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: Credit['updatedAt']
|
||||
|
||||
static passed: ({ credittypecode }: { credittypecode: CreditTypeCode }) => boolean
|
||||
|
||||
|
||||
@@ -10,20 +10,20 @@ import { TeacherModel } from './teacher'
|
||||
modelName: 'credit_teacher',
|
||||
tableName: 'credit_teachers',
|
||||
})
|
||||
export class CreditTeacherModel extends Model<CreditTeacher> implements CreditTeacher {
|
||||
export class CreditTeacherModel extends Model implements CreditTeacher {
|
||||
@ForeignKey(() => CreditModel)
|
||||
@Column(DataType.STRING)
|
||||
credit_id!: string
|
||||
declare credit_id: CreditTeacher['credit_id']
|
||||
|
||||
@ForeignKey(() => TeacherModel)
|
||||
@Column(DataType.STRING)
|
||||
teacher_id!: string
|
||||
declare teacher_id: CreditTeacher['teacher_id']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: CreditTeacher['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: CreditTeacher['updatedAt']
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import type { CreditType } from '@oodikone/shared/models'
|
||||
import { CreditTypeCode, Name } from '@oodikone/shared/types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
@@ -11,16 +10,16 @@ import { CreditTypeCode, Name } from '@oodikone/shared/types'
|
||||
export class CreditTypeModel extends Model<CreditType> implements CreditType {
|
||||
@PrimaryKey
|
||||
@Column(DataType.INTEGER)
|
||||
credittypecode!: CreditTypeCode
|
||||
declare credittypecode: CreditType['credittypecode']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Required<Name>
|
||||
declare name: CreditType['name']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: CreditType['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: CreditType['updatedAt']
|
||||
}
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import { Column, CreatedAt, DataType, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import type { CurriculumPeriod } from '@oodikone/shared/models'
|
||||
import type { Name } from '@oodikone/shared/types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'curriculum_period',
|
||||
tableName: 'curriculum_periods',
|
||||
})
|
||||
export class CurriculumPeriodModel extends Model<CurriculumPeriod> implements CurriculumPeriod {
|
||||
export class CurriculumPeriodModel extends Model implements CurriculumPeriod {
|
||||
@Column({ type: DataType.STRING, primaryKey: true })
|
||||
id!: string
|
||||
declare id: CurriculumPeriod['id']
|
||||
|
||||
@Column({ type: DataType.JSONB, allowNull: false })
|
||||
name!: Name
|
||||
declare name: CurriculumPeriod['name']
|
||||
|
||||
@Column({ type: DataType.STRING, allowNull: false })
|
||||
universityOrgId!: string
|
||||
declare universityOrgId: CurriculumPeriod['universityOrgId']
|
||||
|
||||
@Column({ type: DataType.DATE, allowNull: false })
|
||||
startDate!: Date
|
||||
declare startDate: CurriculumPeriod['startDate']
|
||||
|
||||
@Column({ type: DataType.DATE, allowNull: false })
|
||||
endDate!: Date
|
||||
declare endDate: CurriculumPeriod['endDate']
|
||||
|
||||
@CreatedAt
|
||||
@Column({ type: DataType.DATE, allowNull: false })
|
||||
createdAt!: Date
|
||||
declare createdAt: CurriculumPeriod['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column({ type: DataType.DATE, allowNull: false })
|
||||
updatedAt!: Date
|
||||
declare updatedAt: CurriculumPeriod['updatedAt']
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import type { Enrollment, Semester, SISStudyRight, Student } from '@oodikone/shared/models'
|
||||
import { EnrollmentState } from '@oodikone/shared/types'
|
||||
import type { Enrollment } from '@oodikone/shared/models'
|
||||
|
||||
import { CourseModel } from './course'
|
||||
import { SemesterModel } from './semester'
|
||||
@@ -23,59 +22,59 @@ import { StudentModel } from './student'
|
||||
modelName: 'enrollment',
|
||||
tableName: 'enrollment',
|
||||
})
|
||||
export class EnrollmentModel extends Model<Enrollment> implements Enrollment {
|
||||
export class EnrollmentModel extends Model implements Enrollment {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
declare id: Enrollment['id']
|
||||
|
||||
@ForeignKey(() => StudentModel)
|
||||
@Column(DataType.STRING)
|
||||
studentnumber!: string
|
||||
declare studentnumber: Enrollment['studentnumber']
|
||||
|
||||
@BelongsTo(() => StudentModel, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
student!: Student
|
||||
declare student: Enrollment['student']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
course_code!: string
|
||||
declare course_code: Enrollment['course_code']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
state!: EnrollmentState
|
||||
declare state: Enrollment['state']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
enrollment_date_time!: Date
|
||||
declare enrollment_date_time: Enrollment['enrollment_date_time']
|
||||
|
||||
@ForeignKey(() => CourseModel)
|
||||
@Column(DataType.STRING)
|
||||
course_id!: string
|
||||
declare course_id: Enrollment['course_id']
|
||||
|
||||
@ForeignKey(() => SemesterModel)
|
||||
@Column(DataType.STRING)
|
||||
semester_composite!: string
|
||||
declare semester_composite: Enrollment['semester_composite']
|
||||
|
||||
@BelongsTo(() => SemesterModel, { foreignKey: 'semester_composite', targetKey: 'composite' })
|
||||
semester!: Semester
|
||||
declare semester: Enrollment['semester']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
semestercode!: number
|
||||
declare semestercode: Enrollment['semestercode']
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
is_open!: boolean
|
||||
declare is_open: Enrollment['is_open']
|
||||
|
||||
@ForeignKey(() => SISStudyRightModel)
|
||||
@Column({ type: DataType.STRING, allowNull: true })
|
||||
studyright_id!: string
|
||||
declare studyright_id: Enrollment['studyright_id']
|
||||
|
||||
@BelongsTo(() => SISStudyRightModel, { foreignKey: 'studyright_id', targetKey: 'id', constraints: false })
|
||||
studyright!: SISStudyRight
|
||||
declare studyright: Enrollment['studyright']
|
||||
|
||||
@BelongsTo(() => CourseModel, { foreignKey: 'course_id', targetKey: 'id' })
|
||||
course!: CourseModel
|
||||
declare course: Enrollment['course']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: Enrollment['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: Enrollment['updatedAt']
|
||||
}
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
import type { CustomPopulationSearch, CustomPopulationSearchCreation } from '@oodikone/shared/models/kone'
|
||||
import type { CustomPopulationSearch } from '@oodikone/shared/models/kone'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'custom_population_search',
|
||||
tableName: 'custom_population_searches',
|
||||
})
|
||||
export class CustomPopulationSearchModel
|
||||
extends Model<CustomPopulationSearch, CustomPopulationSearchCreation>
|
||||
implements CustomPopulationSearch
|
||||
{
|
||||
export class CustomPopulationSearchModel extends Model implements CustomPopulationSearch {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.BIGINT)
|
||||
id!: CreationOptional<string>
|
||||
declare id: CustomPopulationSearch['id']
|
||||
|
||||
@Column(DataType.BIGINT)
|
||||
userId!: string
|
||||
declare userId: CustomPopulationSearch['userId']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
name!: string
|
||||
declare name: CustomPopulationSearch['name']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
students!: string[]
|
||||
declare students: CustomPopulationSearch['students']
|
||||
}
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { Column, DataType, Model, PrimaryKey, Table, AutoIncrement, Default } from 'sequelize-typescript'
|
||||
|
||||
import type { ExcludedCourse, ExcludedCourseCreation } from '@oodikone/shared/models/kone'
|
||||
import type { ExcludedCourse } from '@oodikone/shared/models/kone'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'excluded_course',
|
||||
tableName: 'excluded_courses',
|
||||
})
|
||||
export class ExcludedCourseModel extends Model<ExcludedCourse, ExcludedCourseCreation> implements ExcludedCourse {
|
||||
export class ExcludedCourseModel extends Model implements ExcludedCourse {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.INTEGER)
|
||||
id!: CreationOptional<number>
|
||||
declare id: ExcludedCourse['id']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
programme_code!: string
|
||||
declare programme_code: ExcludedCourse['programme_code']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
course_code!: string
|
||||
declare course_code: ExcludedCourse['course_code']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
curriculum_version!: string
|
||||
declare curriculum_version: ExcludedCourse['curriculum_version']
|
||||
|
||||
@Default(DataType.NOW)
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: CreationOptional<Date>
|
||||
declare createdAt: ExcludedCourse['createdAt']
|
||||
|
||||
@Default(DataType.NOW)
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: CreationOptional<Date>
|
||||
declare updatedAt: ExcludedCourse['updatedAt']
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
import type { OpenUniPopulationSearch, OpenUniPopulationSearchCreation } from '@oodikone/shared/models/kone'
|
||||
import type { OpenUniPopulationSearch } from '@oodikone/shared/models/kone'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
@@ -9,21 +8,18 @@ import type { OpenUniPopulationSearch, OpenUniPopulationSearchCreation } from '@
|
||||
modelName: 'open_uni_population_search',
|
||||
tableName: 'open_uni_population_searches',
|
||||
})
|
||||
export class OpenUniPopulationSearchModel
|
||||
extends Model<OpenUniPopulationSearch, OpenUniPopulationSearchCreation>
|
||||
implements OpenUniPopulationSearch
|
||||
{
|
||||
export class OpenUniPopulationSearchModel extends Model implements OpenUniPopulationSearch {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.BIGINT)
|
||||
id!: CreationOptional<string>
|
||||
declare id: OpenUniPopulationSearch['id']
|
||||
|
||||
@Column(DataType.BIGINT)
|
||||
userId!: string
|
||||
declare userId: OpenUniPopulationSearch['userId']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
name!: string
|
||||
declare name: OpenUniPopulationSearch['name']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
courseCodes!: string[]
|
||||
declare courseCodes: OpenUniPopulationSearch['courseCodes']
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
import type { ProgressCriteria, ProgressCriteriaCreation } from '@oodikone/shared/models/kone'
|
||||
import type { ProgressCriteria } from '@oodikone/shared/models/kone'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
@@ -9,50 +8,47 @@ import type { ProgressCriteria, ProgressCriteriaCreation } from '@oodikone/share
|
||||
modelName: 'progress_criteria',
|
||||
tableName: 'progress_criteria',
|
||||
})
|
||||
export class ProgressCriteriaModel
|
||||
extends Model<ProgressCriteria, ProgressCriteriaCreation>
|
||||
implements ProgressCriteria
|
||||
{
|
||||
export class ProgressCriteriaModel extends Model implements ProgressCriteria {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
code!: string
|
||||
declare code: ProgressCriteria['code']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
curriculumVersion!: CreationOptional<string>
|
||||
declare curriculumVersion: ProgressCriteria['curriculumVersion']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearOne!: string[]
|
||||
declare coursesYearOne: ProgressCriteria['coursesYearOne']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearTwo!: string[]
|
||||
declare coursesYearTwo: ProgressCriteria['coursesYearTwo']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearThree!: string[]
|
||||
declare coursesYearThree: ProgressCriteria['coursesYearThree']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearFour!: string[]
|
||||
declare coursesYearFour: ProgressCriteria['coursesYearFour']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearFive!: string[]
|
||||
declare coursesYearFive: ProgressCriteria['coursesYearFive']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearSix!: string[]
|
||||
declare coursesYearSix: ProgressCriteria['coursesYearSix']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearOne!: number
|
||||
declare creditsYearOne: ProgressCriteria['creditsYearOne']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearTwo!: number
|
||||
declare creditsYearTwo: ProgressCriteria['creditsYearTwo']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearThree!: number
|
||||
declare creditsYearThree: ProgressCriteria['creditsYearThree']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearFour!: number
|
||||
declare creditsYearFour: ProgressCriteria['creditsYearFour']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearFive!: number
|
||||
declare creditsYearFive: ProgressCriteria['creditsYearFive']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearSix!: number
|
||||
declare creditsYearSix: ProgressCriteria['creditsYearSix']
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table, Unique } from 'sequelize-typescript'
|
||||
|
||||
import type { StudyGuidanceGroupTag, StudyGuidanceGroupTagCreation } from '@oodikone/shared/models/kone'
|
||||
import type { StudyGuidanceGroupTag } from '@oodikone/shared/models/kone'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
@@ -9,22 +8,19 @@ import type { StudyGuidanceGroupTag, StudyGuidanceGroupTagCreation } from '@oodi
|
||||
modelName: 'study_guidance_group_tag',
|
||||
tableName: 'study_guidance_group_tags',
|
||||
})
|
||||
export class StudyGuidanceGroupTagModel
|
||||
extends Model<StudyGuidanceGroupTag, StudyGuidanceGroupTagCreation>
|
||||
implements StudyGuidanceGroupTag
|
||||
{
|
||||
export class StudyGuidanceGroupTagModel extends Model implements StudyGuidanceGroupTag {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.BIGINT)
|
||||
id?: CreationOptional<string>
|
||||
declare id: StudyGuidanceGroupTag['id']
|
||||
|
||||
@Unique
|
||||
@Column(DataType.STRING)
|
||||
studyGuidanceGroupId!: string
|
||||
declare studyGuidanceGroupId: StudyGuidanceGroupTag['studyGuidanceGroupId']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
studyProgramme!: string | null
|
||||
declare studyProgramme: StudyGuidanceGroupTag['studyProgramme']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
year!: string | null
|
||||
declare year: StudyGuidanceGroupTag['year']
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import type { StudyProgrammePin } from '@oodikone/shared/models/kone'
|
||||
modelName: 'study_programme_pin',
|
||||
tableName: 'study_programme_pins',
|
||||
})
|
||||
export class StudyProgrammePinModel extends Model<StudyProgrammePin> implements StudyProgrammePin {
|
||||
export class StudyProgrammePinModel extends Model implements StudyProgrammePin {
|
||||
@PrimaryKey
|
||||
@Column(DataType.INTEGER)
|
||||
userId!: number
|
||||
declare userId: StudyProgrammePin['userId']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
studyProgrammes!: string[]
|
||||
declare studyProgrammes: StudyProgrammePin['studyProgrammes']
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
import type { Tag, TagCreation, TagStudent } from '@oodikone/shared/models/kone'
|
||||
import type { Tag } from '@oodikone/shared/models/kone'
|
||||
|
||||
import { TagStudentModel } from './tagStudent'
|
||||
|
||||
@@ -9,25 +8,25 @@ import { TagStudentModel } from './tagStudent'
|
||||
modelName: 'tag',
|
||||
tableName: 'tag',
|
||||
})
|
||||
export class TagModel extends Model<Tag, TagCreation> implements Tag {
|
||||
export class TagModel extends Model implements Tag {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@ForeignKey(() => TagStudentModel)
|
||||
@Column(DataType.BIGINT)
|
||||
tag_id!: CreationOptional<string>
|
||||
declare tag_id: Tag['tag_id']
|
||||
|
||||
@BelongsTo(() => TagStudentModel)
|
||||
tagStudent!: CreationOptional<TagStudent>
|
||||
declare tagStudent: Tag['tagStudent']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
tagname!: string
|
||||
declare tagname: Tag['tagname']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
studytrack!: string
|
||||
declare studytrack: Tag['studytrack']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
year!: string | null
|
||||
declare year: Tag['year']
|
||||
|
||||
@Column(DataType.BIGINT)
|
||||
personal_user_id!: string | null
|
||||
declare personal_user_id: Tag['personal_user_id']
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
import type { Tag, TagStudent, TagStudentCreation } from '@oodikone/shared/models/kone'
|
||||
import type { TagStudent } from '@oodikone/shared/models/kone'
|
||||
|
||||
import { StudentModel } from '../student'
|
||||
import { TagModel } from './tag'
|
||||
@@ -10,18 +9,18 @@ import { TagModel } from './tag'
|
||||
modelName: 'tag_student',
|
||||
tableName: 'tag_student',
|
||||
})
|
||||
export class TagStudentModel extends Model<TagStudent, TagStudentCreation> implements TagStudent {
|
||||
export class TagStudentModel extends Model implements TagStudent {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@ForeignKey(() => TagModel)
|
||||
@Column(DataType.BIGINT)
|
||||
tag_id!: string
|
||||
declare tag_id: TagStudent['tag_id']
|
||||
|
||||
@PrimaryKey
|
||||
@ForeignKey(() => StudentModel)
|
||||
@Column(DataType.STRING)
|
||||
studentnumber!: string
|
||||
declare studentnumber: TagStudent['studentnumber']
|
||||
|
||||
@BelongsTo(() => TagModel)
|
||||
tag!: CreationOptional<Tag>
|
||||
declare tag: TagStudent['tag']
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import type { Organization, ProgrammeModule, SISStudyRight } from '@oodikone/shared/models'
|
||||
import type { Name } from '@oodikone/shared/types'
|
||||
import type { Organization } from '@oodikone/shared/models'
|
||||
|
||||
import { CourseModel } from './course'
|
||||
import { CourseProviderModel } from './courseProvider'
|
||||
@@ -23,37 +22,37 @@ import { SISStudyRightModel } from './SISStudyRight'
|
||||
modelName: 'organization',
|
||||
tableName: 'organization',
|
||||
})
|
||||
export class OrganizationModel extends Model<Organization> implements Organization {
|
||||
export class OrganizationModel extends Model implements Organization {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
declare id: Organization['id']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
code!: string
|
||||
declare code: Organization['code']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Name
|
||||
declare name: Organization['name']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
parent_id!: string
|
||||
declare parent_id: Organization['parent_id']
|
||||
|
||||
@HasMany(() => OrganizationModel, { foreignKey: 'parent_id', as: 'children' })
|
||||
children!: Organization[]
|
||||
declare children: Organization['children']
|
||||
|
||||
@HasMany(() => ProgrammeModuleModel, { foreignKey: 'organization_id' })
|
||||
programmeModules!: ProgrammeModule[]
|
||||
declare programmeModules: Organization['programmeModules']
|
||||
|
||||
@HasMany(() => SISStudyRightModel, { foreignKey: 'facultyCode', sourceKey: 'code' })
|
||||
SISStudyRights!: SISStudyRight[]
|
||||
declare SISStudyRights: Organization['SISStudyRights']
|
||||
|
||||
@BelongsToMany(() => CourseModel, () => CourseProviderModel, 'organizationcode')
|
||||
courses!: CourseModel[]
|
||||
declare courses: Organization['courses']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: Organization['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: Organization['updatedAt']
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import type { Organization, ProgrammeModule } from '@oodikone/shared/models'
|
||||
import { DegreeProgrammeType, Name } from '@oodikone/shared/types'
|
||||
|
||||
import { OrganizationModel } from './organization'
|
||||
import { ProgrammeModuleChildModel } from './programmeModuleChild'
|
||||
@@ -22,62 +21,62 @@ import { ProgrammeModuleChildModel } from './programmeModuleChild'
|
||||
modelName: 'programme_module',
|
||||
tableName: 'programme_modules',
|
||||
})
|
||||
export class ProgrammeModuleModel extends Model<ProgrammeModule> implements ProgrammeModule {
|
||||
export class ProgrammeModuleModel extends Model implements ProgrammeModule {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
declare id: ProgrammeModule['id']
|
||||
|
||||
@BelongsToMany(() => ProgrammeModuleModel, () => ProgrammeModuleChildModel, 'child_id')
|
||||
parents!: ProgrammeModule[]
|
||||
declare parents: ProgrammeModule[]
|
||||
|
||||
@BelongsToMany(() => ProgrammeModuleModel, () => ProgrammeModuleChildModel, 'parent_id')
|
||||
children!: ProgrammeModule[]
|
||||
declare children: ProgrammeModule[]
|
||||
|
||||
@BelongsTo(() => OrganizationModel, { foreignKey: 'organization_id' })
|
||||
declare organization: Organization
|
||||
|
||||
@Column(DataType.STRING)
|
||||
group_id!: string
|
||||
declare group_id: ProgrammeModule['group_id']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
code!: string
|
||||
declare code: ProgrammeModule['code']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Name
|
||||
declare name: ProgrammeModule['name']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
type!: 'course' | 'module'
|
||||
declare type: ProgrammeModule['type']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
order!: number
|
||||
declare order: ProgrammeModule['order']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
studyLevel!: string
|
||||
declare studyLevel: ProgrammeModule['studyLevel']
|
||||
|
||||
@ForeignKey(() => OrganizationModel)
|
||||
@Column(DataType.STRING)
|
||||
organization_id!: string
|
||||
|
||||
@BelongsTo(() => OrganizationModel, { foreignKey: 'organization_id' })
|
||||
organization!: Organization
|
||||
declare organization_id: ProgrammeModule['organization_id']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
valid_from!: Date
|
||||
declare valid_from: ProgrammeModule['valid_from']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
valid_to!: Date
|
||||
declare valid_to: ProgrammeModule['valid_to']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
curriculum_period_ids!: string[]
|
||||
declare curriculum_period_ids: ProgrammeModule['curriculum_period_ids']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
degreeProgrammeType!: DegreeProgrammeType | null
|
||||
declare degreeProgrammeType: ProgrammeModule['degreeProgrammeType']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
minimumCredits!: number | null
|
||||
declare minimumCredits: ProgrammeModule['minimumCredits']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: ProgrammeModule['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: ProgrammeModule['updatedAt']
|
||||
}
|
||||
|
||||
@@ -9,23 +9,23 @@ import { ProgrammeModuleModel } from './programmeModule'
|
||||
modelName: 'programme_module_child',
|
||||
tableName: 'programme_module_children',
|
||||
})
|
||||
export class ProgrammeModuleChildModel extends Model<ProgrammeModuleChild> implements ProgrammeModuleChild {
|
||||
export class ProgrammeModuleChildModel extends Model implements ProgrammeModuleChild {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
composite!: string
|
||||
declare composite: ProgrammeModuleChild['composite']
|
||||
|
||||
@ForeignKey(() => ProgrammeModuleModel)
|
||||
@Column(DataType.STRING)
|
||||
parentId!: string
|
||||
declare parentId: ProgrammeModuleChild['parentId']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
childId!: string
|
||||
declare childId: ProgrammeModuleChild['childId']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: ProgrammeModuleChild['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: ProgrammeModuleChild['updatedAt']
|
||||
}
|
||||
|
||||
@@ -1,50 +1,49 @@
|
||||
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import type { Semester } from '@oodikone/shared/models'
|
||||
import type { Name } from '@oodikone/shared/types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'semester',
|
||||
tableName: 'semesters',
|
||||
})
|
||||
export class SemesterModel extends Model<Semester> implements Semester {
|
||||
export class SemesterModel extends Model implements Semester {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
composite!: string
|
||||
declare composite: Semester['composite']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
semestercode!: number
|
||||
declare semestercode: Semester['semestercode']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Name
|
||||
declare name: Semester['name']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
startYear!: number
|
||||
declare startYear: Semester['startYear']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
startdate!: Date
|
||||
declare startdate: Semester['startdate']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
enddate!: Date
|
||||
declare enddate: Semester['enddate']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
yearcode!: number
|
||||
declare yearcode: Semester['yearcode']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
yearname!: string
|
||||
declare yearname: Semester['yearname']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
org!: string
|
||||
declare org: Semester['org']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
termIndex!: number
|
||||
declare termIndex: Semester['termIndex']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: Semester['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: Semester['updatedAt']
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import type { Student, Credit, Enrollment, SISStudyRight, Studyplan } from '@oodikone/shared/models'
|
||||
import type { Name } from '@oodikone/shared/types'
|
||||
import { GenderCode } from '@oodikone/shared/types'
|
||||
import type { Student } from '@oodikone/shared/models'
|
||||
|
||||
import { CreditModel } from './credit'
|
||||
import { EnrollmentModel } from './enrollment'
|
||||
@@ -14,73 +12,73 @@ import { StudyplanModel } from './studyplan'
|
||||
modelName: 'student',
|
||||
tableName: 'student',
|
||||
})
|
||||
export class StudentModel extends Model<Student> implements Student {
|
||||
export class StudentModel extends Model implements Student {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
studentnumber!: string
|
||||
declare studentnumber: Student['studentnumber']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
lastname!: string
|
||||
declare lastname: Student['lastname']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
firstnames!: string
|
||||
declare firstnames: Student['firstnames']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
abbreviatedname!: string
|
||||
declare abbreviatedname: Student['abbreviatedname']
|
||||
|
||||
@HasMany(() => EnrollmentModel, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
enrollments!: Enrollment[]
|
||||
declare enrollments: Student['enrollments']
|
||||
|
||||
@HasMany(() => SISStudyRightModel, { foreignKey: 'studentNumber', sourceKey: 'studentnumber' })
|
||||
studyRights!: SISStudyRight[]
|
||||
declare studyRights: Student['studyRights']
|
||||
|
||||
@HasMany(() => StudyplanModel, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
studyplans!: Studyplan[]
|
||||
declare studyplans: Student['studyplans']
|
||||
|
||||
@HasMany(() => CreditModel, { foreignKey: 'student_studentnumber', sourceKey: 'studentnumber' })
|
||||
credits!: Credit[]
|
||||
declare credits: Student['credits']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
birthdate!: Date
|
||||
declare birthdate: Student['birthdate']
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditcount!: number
|
||||
declare creditcount: Student['creditcount']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
dateofuniversityenrollment!: Date
|
||||
declare dateofuniversityenrollment: Student['dateofuniversityenrollment']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
email!: string
|
||||
declare email: Student['email']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
secondary_email!: string
|
||||
declare secondary_email: Student['secondary_email']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
national_student_number!: string
|
||||
declare national_student_number: Student['national_student_number']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
phone_number!: string
|
||||
declare phone_number: Student['phone_number']
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
citizenships!: Name[]
|
||||
declare citizenships: Student['citizenships']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
gender_code!: GenderCode
|
||||
declare gender_code: Student['gender_code']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
sis_person_id!: string
|
||||
declare sis_person_id: Student['sis_person_id']
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
hasPersonalIdentityCode!: boolean
|
||||
declare hasPersonalIdentityCode: Student['hasPersonalIdentityCode']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
preferredLanguage!: string
|
||||
declare preferredLanguage: Student['preferredLanguage']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: Student['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: Student['updatedAt']
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import type { SISStudyRight, Student, Studyplan } from '@oodikone/shared/models'
|
||||
import type { Studyplan } from '@oodikone/shared/models'
|
||||
|
||||
import { SISStudyRightModel } from './SISStudyRight'
|
||||
import { StudentModel } from './student'
|
||||
@@ -20,48 +20,48 @@ import { StudentModel } from './student'
|
||||
modelName: 'studyplan',
|
||||
tableName: 'studyplan',
|
||||
})
|
||||
export class StudyplanModel extends Model<Studyplan> implements Studyplan {
|
||||
export class StudyplanModel extends Model implements Studyplan {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
declare id: Studyplan['id']
|
||||
|
||||
@ForeignKey(() => StudentModel)
|
||||
@Column(DataType.STRING)
|
||||
studentnumber!: string
|
||||
declare studentnumber: Studyplan['studentnumber']
|
||||
|
||||
@BelongsTo(() => StudentModel, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
student!: Student
|
||||
declare student: Studyplan['student']
|
||||
|
||||
@ForeignKey(() => SISStudyRightModel)
|
||||
@Column(DataType.STRING)
|
||||
sis_study_right_id!: string
|
||||
declare sis_study_right_id: Studyplan['sis_study_right_id']
|
||||
|
||||
@BelongsTo(() => SISStudyRightModel, { foreignKey: 'sis_study_right_id', targetKey: 'id', as: 'studyRight' })
|
||||
studyRight!: SISStudyRight
|
||||
declare studyRight: Studyplan['studyRight']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
programme_code!: string
|
||||
declare programme_code: Studyplan['programme_code']
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
included_courses!: string[]
|
||||
declare included_courses: Studyplan['included_courses']
|
||||
|
||||
@Column({ type: DataType.ARRAY(DataType.TEXT), field: 'included_modules' })
|
||||
includedModules!: string[]
|
||||
declare includedModules: Studyplan['includedModules']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
sisu_id!: string
|
||||
declare sisu_id: Studyplan['sisu_id']
|
||||
|
||||
@Column(DataType.DOUBLE)
|
||||
completed_credits!: number
|
||||
declare completed_credits: Studyplan['completed_credits']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
curriculum_period_id!: string
|
||||
declare curriculum_period_id: Studyplan['curriculum_period_id']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: Studyplan['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: Studyplan['updatedAt']
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import type { StudyrightExtent } from '@oodikone/shared/models'
|
||||
import { ExtentCode } from '@oodikone/shared/types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'studyright_extent',
|
||||
tableName: 'studyright_extents',
|
||||
})
|
||||
export class StudyrightExtentModel extends Model<StudyrightExtent> implements StudyrightExtent {
|
||||
export class StudyrightExtentModel extends Model implements StudyrightExtent {
|
||||
@PrimaryKey
|
||||
@Column(DataType.INTEGER)
|
||||
extentcode!: ExtentCode
|
||||
declare extentcode: StudyrightExtent['extentcode']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
name!: string
|
||||
declare name: StudyrightExtent['name']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: StudyrightExtent['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: StudyrightExtent['updatedAt']
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BelongsToMany, Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import type { Teacher, Credit } from '@oodikone/shared/models'
|
||||
import type { Teacher } from '@oodikone/shared/models'
|
||||
|
||||
import { CreditModel } from './credit'
|
||||
import { CreditTeacherModel } from './creditTeacher'
|
||||
@@ -10,22 +10,22 @@ import { CreditTeacherModel } from './creditTeacher'
|
||||
modelName: 'teacher',
|
||||
tableName: 'teacher',
|
||||
})
|
||||
export class TeacherModel extends Model<Teacher> implements Teacher {
|
||||
export class TeacherModel extends Model implements Teacher {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
declare id: Teacher['id']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
name!: string
|
||||
declare name: Teacher['name']
|
||||
|
||||
@BelongsToMany(() => CreditModel, () => CreditTeacherModel)
|
||||
credits!: Credit[]
|
||||
declare credits: Teacher['credits']
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
declare createdAt: Teacher['createdAt']
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
declare updatedAt: Teacher['updatedAt']
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Default, Model, PrimaryKey, Table, Unique } from 'sequelize-typescript'
|
||||
|
||||
import type { Language } from '@oodikone/shared/language'
|
||||
import type { User, UserCreation } from '@oodikone/shared/models/user'
|
||||
|
||||
import type { Role } from '@oodikone/shared/types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
timestamps: false,
|
||||
@@ -16,33 +12,33 @@ export class UserModel extends Model<User, UserCreation> implements User {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.BIGINT)
|
||||
id!: CreationOptional<string>
|
||||
declare id: User['id']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
fullName!: string
|
||||
declare fullName: User['fullName']
|
||||
|
||||
@Unique
|
||||
@Column(DataType.STRING)
|
||||
username!: string
|
||||
declare username: User['username']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
email!: string
|
||||
declare email: User['email']
|
||||
|
||||
@Default('fi')
|
||||
@Column(DataType.STRING)
|
||||
language!: CreationOptional<Language>
|
||||
declare language: User['language']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
sisuPersonId!: string
|
||||
declare sisuPersonId: User['sisuPersonId']
|
||||
|
||||
@Column(DataType.DATE)
|
||||
lastLogin!: Date
|
||||
declare lastLogin: User['lastLogin']
|
||||
|
||||
@Default([])
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
roles!: CreationOptional<Role[]>
|
||||
declare roles: User['roles']
|
||||
|
||||
@Default([])
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
programmeRights!: CreationOptional<string[]>
|
||||
declare programmeRights: User['programmeRights']
|
||||
}
|
||||
|
||||
@@ -207,8 +207,7 @@ router.get(
|
||||
const updatedStats = await combineFacultyStudents(code, newProgrammes, specialGroups, graduated)
|
||||
|
||||
if (updatedStats) {
|
||||
const redisResponse = await setFacultyStudentStats(updatedStats, specialGroups, graduated)
|
||||
if (!redisResponse) return res.json(updatedStats)
|
||||
void setFacultyStudentStats(updatedStats, specialGroups, graduated)
|
||||
}
|
||||
return res.json(updatedStats)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ const getEnrollments = async (courses: Courses, fullCourseCodes: string[], stude
|
||||
return formattedEnrollments
|
||||
}
|
||||
|
||||
const getStudents = async (studentNumbers: string[]) => {
|
||||
const getStudents = async (studentNumbers: string[]): Promise<StudentWithCourses[]> => {
|
||||
const students = await StudentModel.findAll({
|
||||
attributes: ['studentnumber', 'firstnames', 'lastname', 'email', 'sis_person_id', 'secondary_email'],
|
||||
where: {
|
||||
@@ -147,9 +147,9 @@ const getStudents = async (studentNumbers: string[]) => {
|
||||
})
|
||||
|
||||
return students.map(student => {
|
||||
const { studyplans, ...rest } = student.toJSON() as StudentWithStudyplanNested
|
||||
const { studyplans, ...rest }: StudentWithStudyplanNested = student.toJSON()
|
||||
const coursesInStudyPlan = studyplans.flatMap(studyplan => studyplan.included_courses)
|
||||
return { ...rest, coursesInStudyPlan } as StudentWithCourses
|
||||
return { ...rest, coursesInStudyPlan }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,18 @@ import type { ProgrammesOfOrganization } from './faculty'
|
||||
type StudyTrackStats = Awaited<ReturnType<typeof getStudyTrackStatsForStudyProgramme>>
|
||||
|
||||
const calculateCombinedStats = (programmeCodes: string[], stats: StudyTrackStats[]) => {
|
||||
const facultyTableStats: Record<string, Array<string | number>> = {}
|
||||
const facultyTableStats: Record<string, (string | number)[]> = {}
|
||||
const facultyTableStatsExtra: Record<string, Record<string, Record<string, number>>> = {}
|
||||
const programmeTableStats: Record<string, Record<string, Array<string | number>>> = {}
|
||||
const programmeTableStats: Record<string, Record<string, (string | number)[]>> = {}
|
||||
|
||||
for (const programmeCode of programmeCodes) {
|
||||
const statsForProgramme = stats.find(programmeStats => programmeStats.id === programmeCode)?.mainStatsByTrack?.[
|
||||
programmeCode
|
||||
]
|
||||
|
||||
const programmeOtherCountriesCount = stats.find(programmeStats => programmeStats.id === programmeCode)
|
||||
?.otherCountriesCount?.[programmeCode]
|
||||
|
||||
for (const prog of programmeCodes) {
|
||||
const statsForProgramme = stats.find(programmeStats => programmeStats.id === prog)?.mainStatsByTrack?.[prog]
|
||||
const programmeOtherCountriesCount = stats.find(programmeStats => programmeStats.id === prog)
|
||||
?.otherCountriesCount?.[prog]
|
||||
if (!statsForProgramme) continue
|
||||
|
||||
for (const yearStats of statsForProgramme) {
|
||||
@@ -34,11 +38,11 @@ const calculateCombinedStats = (programmeCodes: string[], stats: StudyTrackStats
|
||||
}
|
||||
}
|
||||
|
||||
if (!(prog in programmeTableStats)) {
|
||||
programmeTableStats[prog] = {}
|
||||
if (!(programmeCode in programmeTableStats)) {
|
||||
programmeTableStats[programmeCode] = {}
|
||||
}
|
||||
if (!(yearStats[0] in programmeTableStats[prog])) {
|
||||
programmeTableStats[prog][yearStats[0]] = yearStats.slice(1)
|
||||
if (!(yearStats[0] in programmeTableStats[programmeCode])) {
|
||||
programmeTableStats[programmeCode][yearStats[0]] = yearStats.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +51,7 @@ const calculateCombinedStats = (programmeCodes: string[], stats: StudyTrackStats
|
||||
if (!(year in facultyTableStatsExtra)) {
|
||||
facultyTableStatsExtra[year] = {}
|
||||
}
|
||||
facultyTableStatsExtra[year][prog] = countryStats
|
||||
facultyTableStatsExtra[year][programmeCode] = countryStats
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { SISStudyRight } from '@oodikone/shared/models'
|
||||
import { FormattedStudent, CriteriaYear, Name, ProgressCriteria } from '@oodikone/shared/types'
|
||||
import { StudentData, StudentTags } from '@oodikone/shared/types/studentData'
|
||||
import { dateYearsFromNow, dateDaysFromNow, dayInMilliseconds } from '@oodikone/shared/util/datetime'
|
||||
import { CreditModel } from '../../models'
|
||||
import { hasTransferredFromOrToProgramme } from '../studyProgramme/studyProgrammeHelpers'
|
||||
import type { StudentStudyPlan, StudentStudyRight, TaggetStudentData } from './getStudentData'
|
||||
import type { StudentStudyPlan, StudentStudyRight } from './getStudentData'
|
||||
import { getCurriculumVersion } from './shared'
|
||||
import type { AnonymousCredit, AnonymousEnrollment } from './statisticsOf'
|
||||
|
||||
@@ -131,9 +132,12 @@ const getProgressCriteria = (
|
||||
})
|
||||
|
||||
yearMap.forEach(([yearToAdd, criteriaYear]) => {
|
||||
criteriaChecked[yearToAdd].totalSatisfied +=
|
||||
Object.values(criteriaChecked[yearToAdd].coursesSatisfied).filter(course => !!course).length ?? 0
|
||||
// UPDATE CREDIT CRITERIA
|
||||
if (!!criteria.credits[criteriaYear] && criteria.credits[criteriaYear] < academicYears[yearToAdd]) {
|
||||
criteriaChecked[yearToAdd].credits = true
|
||||
criteriaChecked[yearToAdd].totalSatisfied += 1
|
||||
}
|
||||
})
|
||||
|
||||
@@ -143,7 +147,8 @@ const getProgressCriteria = (
|
||||
export const formatStudentForAPI = (
|
||||
code: string,
|
||||
startDate: string,
|
||||
student: TaggetStudentData,
|
||||
student: StudentData,
|
||||
tags: StudentTags[],
|
||||
credits: AnonymousCredit[],
|
||||
enrollments: AnonymousEnrollment[],
|
||||
optionData: Name | undefined,
|
||||
@@ -187,15 +192,15 @@ export const formatStudentForAPI = (
|
||||
secondaryEmail: student.secondary_email,
|
||||
phoneNumber: student.phone_number,
|
||||
updatedAt: student.updatedAt,
|
||||
tags: student.tags,
|
||||
studyrightStart: startDate,
|
||||
option: optionData ? { name: optionData } : null,
|
||||
option: optionData ?? null,
|
||||
birthdate: student.birthdate,
|
||||
sis_person_id: student.sis_person_id,
|
||||
citizenships: student.citizenships,
|
||||
criteriaProgress: getProgressCriteria(startDate, criteria, credits, hops),
|
||||
curriculumVersion: getCurriculumVersion(hops?.curriculum_period_id),
|
||||
|
||||
tags,
|
||||
transferredStudyright,
|
||||
transferSource,
|
||||
studyRights,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Op } from 'sequelize'
|
||||
import { Credit, Enrollment, SISStudyRight, SISStudyRightElement, Student, Studyplan } from '@oodikone/shared/models'
|
||||
import { Tag, TagStudent } from '@oodikone/shared/models/kone'
|
||||
import { EnrollmentState } from '@oodikone/shared/types'
|
||||
import { serviceProvider } from '../../config'
|
||||
import {
|
||||
CreditModel,
|
||||
EnrollmentModel,
|
||||
@@ -18,10 +17,6 @@ export type StudentTags = TagStudent & {
|
||||
tag: Pick<Tag, 'tag_id' | 'tagname' | 'personal_user_id'>
|
||||
}
|
||||
|
||||
export type TaggetStudentData = StudentData & {
|
||||
tags: StudentTags[]
|
||||
}
|
||||
|
||||
export const getStudentTags = async (studyRights: string[], studentNumbers: string[], userId: string) => {
|
||||
const studentTags = await TagStudentModel.findAll({
|
||||
attributes: ['tag_id', 'studentnumber'],
|
||||
@@ -190,28 +185,8 @@ export type StudentCredit = Pick<
|
||||
| 'studyright_id'
|
||||
>
|
||||
|
||||
export const getCredits = async (
|
||||
studentNumbers: string[],
|
||||
studyRights: string[],
|
||||
attainmentDateFrom: string
|
||||
): Promise<Array<StudentCredit>> => {
|
||||
const includedCoursesInStudyPlans: Array<Pick<Studyplan, 'included_courses'>> = await StudyplanModel.findAll({
|
||||
attributes: ['included_courses'],
|
||||
where: {
|
||||
studentnumber: { [Op.in]: studentNumbers },
|
||||
},
|
||||
raw: true,
|
||||
})
|
||||
|
||||
const uniqueCourses = new Set(includedCoursesInStudyPlans.flatMap(plan => plan.included_courses))
|
||||
|
||||
// takes into account possible progress tests taken earlier than the start date
|
||||
const courseCodes =
|
||||
['320001', 'MH30_001'].includes(studyRights[0]) && serviceProvider !== 'fd'
|
||||
? [...uniqueCourses, '375063', '339101']
|
||||
: [...uniqueCourses]
|
||||
|
||||
return CreditModel.findAll({
|
||||
export const getCredits = async (studentNumbers: string[]): Promise<Array<StudentCredit>> =>
|
||||
CreditModel.findAll({
|
||||
attributes: [
|
||||
'grade',
|
||||
'credits',
|
||||
@@ -225,8 +200,6 @@ export const getCredits = async (
|
||||
],
|
||||
where: {
|
||||
student_studentnumber: { [Op.in]: studentNumbers },
|
||||
[Op.or]: [{ attainment_date: { [Op.gte]: attainmentDateFrom } }, { course_code: courseCodes }],
|
||||
},
|
||||
raw: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ export const getOptionsForStudents = async (
|
||||
)
|
||||
}
|
||||
|
||||
export const getCourses = (courses: string[]): Promise<Array<Pick<CourseModel, 'code' | 'name'>>> =>
|
||||
export const getCourses = (courses: string[]): Promise<Array<Pick<CourseModel, 'code' | 'name' | 'substitutions'>>> =>
|
||||
CourseModel.findAll({
|
||||
attributes: ['code', 'name'],
|
||||
attributes: ['code', 'name', 'substitutions'],
|
||||
where: {
|
||||
code: { [Op.in]: courses },
|
||||
},
|
||||
@@ -267,13 +267,14 @@ export const parseCourseData = async (
|
||||
|
||||
const courses = await getCourses(Array.from(coursestats.keys()))
|
||||
const courseMap = new Map(courses.map(({ code, name }) => [code, name]))
|
||||
const substitutionMap = new Map(courses.map(({ code, substitutions }) => [code, substitutions]))
|
||||
|
||||
return Array.from(coursestats.entries()).map(([code, { attempts, enrollments, grades, students, stats }]) => {
|
||||
return {
|
||||
course: {
|
||||
code,
|
||||
name: courseMap.get(code) ?? { en: code },
|
||||
substitutions: [],
|
||||
substitutions: substitutionMap.get(code) ?? [],
|
||||
},
|
||||
attempts,
|
||||
enrollments: {
|
||||
|
||||
@@ -5,10 +5,10 @@ import { formatStudentForAPI } from './formatStatisticsForApi'
|
||||
import {
|
||||
type StudentEnrollment,
|
||||
type StudentCredit,
|
||||
type StudentTags,
|
||||
getStudents,
|
||||
getEnrollments,
|
||||
getCredits,
|
||||
StudentTags,
|
||||
} from './getStudentData'
|
||||
import { getOptionsForStudents, parseCourseData } from './shared'
|
||||
|
||||
@@ -37,7 +37,7 @@ export const statisticsOf = async (
|
||||
|
||||
const [enrollments, credits, students] = await Promise.all([
|
||||
getEnrollments(studentNumbers, mockedStartDate),
|
||||
getCredits(studentNumbers, studyRights, mockedStartDate),
|
||||
getCredits(studentNumbers),
|
||||
getStudents(studentNumbers),
|
||||
])
|
||||
|
||||
@@ -74,10 +74,11 @@ export const statisticsOf = async (
|
||||
formatStudentForAPI(
|
||||
code,
|
||||
mockedStartDate,
|
||||
{ ...student, tags: tagList[student.studentnumber] },
|
||||
creditsByStudent.get(student.studentnumber) ?? [],
|
||||
enrollmentsByStudent.get(student.studentnumber) ?? [],
|
||||
optionData?.[student.studentnumber],
|
||||
student,
|
||||
tagList[student.studentnumber],
|
||||
creditsByStudent.get(student.studentnumber)!,
|
||||
enrollmentsByStudent.get(student.studentnumber)!,
|
||||
optionData[student.studentnumber],
|
||||
criteria
|
||||
)
|
||||
),
|
||||
|
||||
@@ -116,12 +116,9 @@ const combineStats = (
|
||||
for (const year of Object.keys(yearlyStats)) {
|
||||
for (const programmeOrStudyTrack of Object.keys(yearlyStats[year])) {
|
||||
const yearStats = yearlyStats[year][programmeOrStudyTrack]
|
||||
if (!mainStatsByYear[year]) {
|
||||
mainStatsByYear[year] = []
|
||||
}
|
||||
if (!mainStatsByTrack[programmeOrStudyTrack]) {
|
||||
mainStatsByTrack[programmeOrStudyTrack] = []
|
||||
}
|
||||
|
||||
mainStatsByYear[year] ??= []
|
||||
mainStatsByTrack[programmeOrStudyTrack] ??= []
|
||||
|
||||
const yearArray = [
|
||||
yearStats.all,
|
||||
@@ -197,13 +194,11 @@ const getMainStatsByTrackAndYear = async (
|
||||
startedInProgramme: Date,
|
||||
studyRightElement: SISStudyRightElement
|
||||
) => {
|
||||
if (!yearlyStats[year]) {
|
||||
yearlyStats[year] = {}
|
||||
}
|
||||
if (!yearlyStats[year][programmeOrStudyTrack]) {
|
||||
yearlyStats[year][programmeOrStudyTrack] = getEmptyYear()
|
||||
}
|
||||
yearlyStats[year] ??= {}
|
||||
yearlyStats[year][programmeOrStudyTrack] ??= getEmptyYear()
|
||||
yearlyStats[year][programmeOrStudyTrack].all += 1
|
||||
|
||||
/* Started studying col */
|
||||
if (!hasTransferredToProgramme) {
|
||||
const firstPresentAt = getDateOfFirstSemesterPresent(
|
||||
studyRight.semesterEnrollments,
|
||||
@@ -218,6 +213,8 @@ const getMainStatsByTrackAndYear = async (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Gender cols */
|
||||
if (studyRight.student.gender_code === GenderCode.MALE) {
|
||||
yearlyStats[year][programmeOrStudyTrack].male += 1
|
||||
} else if (studyRight.student.gender_code === GenderCode.FEMALE) {
|
||||
@@ -226,6 +223,7 @@ const getMainStatsByTrackAndYear = async (
|
||||
yearlyStats[year][programmeOrStudyTrack].otherUnknown += 1
|
||||
}
|
||||
|
||||
/* Current status cols */
|
||||
const hasGraduated = studyRightElement.graduated
|
||||
const hasGraduatedFromCombinedProgramme = combinedProgramme
|
||||
? (studyRight.studyRightElements.find(element => element.code === combinedProgramme)?.graduated ?? false)
|
||||
@@ -234,6 +232,7 @@ const getMainStatsByTrackAndYear = async (
|
||||
if (hasGraduated) {
|
||||
yearlyStats[year][programmeOrStudyTrack].graduated += 1
|
||||
}
|
||||
|
||||
if (hasGraduatedFromCombinedProgramme) {
|
||||
yearlyStats[year][programmeOrStudyTrack].graduatedCombinedProgramme += 1
|
||||
} else if (combinedProgramme || !hasGraduated) {
|
||||
@@ -249,6 +248,7 @@ const getMainStatsByTrackAndYear = async (
|
||||
}
|
||||
}
|
||||
|
||||
/* Citizenship cols */
|
||||
const studentCitizenships = studyRight.student.citizenships
|
||||
|
||||
for (const citizenship of studentCitizenships) {
|
||||
@@ -265,6 +265,7 @@ const getMainStatsByTrackAndYear = async (
|
||||
}
|
||||
}
|
||||
|
||||
/* Graduation statistics */
|
||||
if (!hasGraduated) return
|
||||
const countAsBachelorMaster = doCombo && studyRight.extentCode === ExtentCode.BACHELOR_AND_MASTER
|
||||
const [firstStudyRightElementWithSamePhase] = getStudyRightElementsWithPhase(studyRight, studyRightElement.phase)
|
||||
@@ -364,7 +365,7 @@ const getMainStatsByTrackAndYear = async (
|
||||
studyRightElement
|
||||
)
|
||||
|
||||
const studyTrack = studyRightElement.studyTrack?.code ?? null
|
||||
const studyTrack = studyRightElement.studyTrack?.code
|
||||
if (studyTrack) {
|
||||
updateCounts(
|
||||
startYear,
|
||||
@@ -375,6 +376,7 @@ const getMainStatsByTrackAndYear = async (
|
||||
studyRightElement
|
||||
)
|
||||
}
|
||||
|
||||
const countAsBachelorMaster = doCombo && studyRight.extentCode === ExtentCode.BACHELOR_AND_MASTER
|
||||
if (countAsBachelorMaster) {
|
||||
const startedInBachelor = getStudyRightElementsWithPhase(studyRight, 1)[0]?.startDate
|
||||
@@ -391,12 +393,8 @@ const getMainStatsByTrackAndYear = async (
|
||||
for (const year of Object.keys(yearlyStats)) {
|
||||
if (year === 'Total') continue
|
||||
for (const track of Object.keys(yearlyStats[year])) {
|
||||
if (!yearlyStats.Total) {
|
||||
yearlyStats.Total = {}
|
||||
}
|
||||
if (!yearlyStats.Total[track]) {
|
||||
yearlyStats.Total[track] = getEmptyYear()
|
||||
}
|
||||
yearlyStats.Total ??= {}
|
||||
yearlyStats.Total[track] ??= getEmptyYear()
|
||||
|
||||
for (const field of keysOf(yearlyStats[year][track])) {
|
||||
if (field !== 'otherCountriesCounts') {
|
||||
@@ -404,9 +402,7 @@ const getMainStatsByTrackAndYear = async (
|
||||
continue
|
||||
}
|
||||
for (const country of Object.keys(yearlyStats[year][track].otherCountriesCounts)) {
|
||||
if (!yearlyStats.Total[track].otherCountriesCounts[country]) {
|
||||
yearlyStats.Total[track].otherCountriesCounts[country] = 0
|
||||
}
|
||||
yearlyStats.Total[track].otherCountriesCounts[country] ??= 0
|
||||
yearlyStats.Total[track].otherCountriesCounts[country] +=
|
||||
yearlyStats[year][track].otherCountriesCounts[country]
|
||||
}
|
||||
@@ -458,7 +454,7 @@ export const getStudyTrackStatsForStudyProgramme = async ({
|
||||
studyProgramme: string
|
||||
combinedProgramme?: string
|
||||
settings: { graduated: boolean; specialGroups: boolean }
|
||||
studyRightsOfProgramme: Array<SISStudyRight>
|
||||
studyRightsOfProgramme: SISStudyRight[]
|
||||
}) => {
|
||||
const isAcademicYear = true
|
||||
const includeYearsCombined = true
|
||||
|
||||
@@ -155,7 +155,7 @@ const updateAccessGroups = async (
|
||||
if (!userFromDb) {
|
||||
throw new Error(`User ${username} not found`)
|
||||
}
|
||||
const currentAccessGroups = userFromDb.roles
|
||||
const currentAccessGroups = userFromDb?.roles ?? []
|
||||
|
||||
const newAccessGroups = [
|
||||
...(currentAccessGroups.includes('studyGuidanceGroups') || (await checkStudyGuidanceGroupsAccess(sisId))
|
||||
|
||||
Generated
+1152
-598
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@
|
||||
"@mui/x-date-pickers": "^7.29.4",
|
||||
"@oodikone/shared": "file:../shared",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@sentry/browser": "^9.28.1",
|
||||
"@sentry/browser": "^10.5.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"axios": "^1.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
@@ -36,9 +36,9 @@
|
||||
"scheduler": "^0.26.0",
|
||||
"semantic-ui-css": "^2.5.0",
|
||||
"semantic-ui-react": "^3.0.0-beta.2",
|
||||
"unfuck-spa-shibboleth-session": "^4.1.0",
|
||||
"unfuck-spa-shibboleth-session": "^5.0.0",
|
||||
"use-context-selector": "^2.0.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.17.17",
|
||||
@@ -46,8 +46,8 @@
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/react-highcharts": "^16.0.7",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"typescript": "^5.8.3",
|
||||
"@vitejs/plugin-react-swc": "^4.0.0",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "sisUrl": "https://sis-qa.funidata.fi" }
|
||||
@@ -10,7 +10,7 @@ export const filterToolTips = {
|
||||
startingYear:
|
||||
'Valitse opiskelijoita sen mukaan, minä vuonna heidän ensimmäinen opinto-oikeutensa yliopistolla alkoi (ei siis ainoastaan tarkasteltavan tutkinnon oikeus). Avoimia opinto-oikeuksia ei oteta huomioon.',
|
||||
studentNumber:
|
||||
'Rajaa yksittäisiä opiskelijoita pois tilastoista (blocklist) tai näytä vain määritellyt opiskelijat (allowlist). Vain täsmälliset opiskelijanumerot toimivat.',
|
||||
'Rajaa yksittäisiä opiskelijoita pois tilastoista (excluded student numbers) tai näytä **vain** määritellyt opiskelijat (allowed student numbers). Vain täsmälliset tarkasteltavan populaation joukosta löytyvät opiskelijanumerot toimivat. Suodatin tukee useamman opiskelijanumeron syöttämistä kerrallaan.',
|
||||
studyRightStatus: `
|
||||
Opiskeluoikeus on **aktiivinen** (active), jos opiskelija on ilmoittautunut läsnä- tai poissaolevaksi käynnissä olevalle lukukaudelle.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs, { extend as dayjsExtend } from 'dayjs'
|
||||
import isBetween from 'dayjs/plugin/isBetween'
|
||||
import { maxBy, orderBy, range } from 'lodash'
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '@oodikone/shared/types'
|
||||
import { StudentStudyRight } from '@oodikone/shared/types/studentData'
|
||||
|
||||
dayjs.extend(isBetween)
|
||||
dayjsExtend(isBetween)
|
||||
|
||||
export const textAndDescriptionSearch = (dropDownOptions: any[], param: string) =>
|
||||
dropDownOptions.filter(option =>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs/AdapterDayjs'
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider/LocalizationProvider'
|
||||
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs, { extend as dayjsExtend } from 'dayjs'
|
||||
import updateLocale from 'dayjs/plugin/updateLocale'
|
||||
import HighCharts from 'highcharts' // eslint-disable-line import-x/default
|
||||
import { useEffect } from 'react'
|
||||
@@ -37,7 +37,7 @@ HighCharts.setOptions({
|
||||
},
|
||||
})
|
||||
|
||||
dayjs.extend(updateLocale)
|
||||
dayjsExtend(updateLocale)
|
||||
dayjs.updateLocale('en', {
|
||||
week: {
|
||||
dow: 1, // First day of week is Monday
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs, { extend as dayjsExtend } from 'dayjs'
|
||||
import isBetween from 'dayjs/plugin/isBetween'
|
||||
import { Table, Progress } from 'semantic-ui-react'
|
||||
|
||||
import { languageAbbreviations } from '@/common'
|
||||
|
||||
dayjs.extend(isBetween)
|
||||
dayjsExtend(isBetween)
|
||||
|
||||
export const CoursePopulationLanguageDist = ({ samples, codes, from, to }) => {
|
||||
let total = 0
|
||||
|
||||
@@ -9,7 +9,8 @@ export const useColumns = (): [string[], string[]] => {
|
||||
|
||||
const adminColumns = isAdmin ? ['extent', 'updatedAt'] : []
|
||||
|
||||
const excelOnlyColumns = ['email', 'phoneNumber']
|
||||
/* REQUIRED TO ALWAYS BE VISIBLE */
|
||||
const excelRequired = ['studentNumber', 'lastName', 'firstNames', 'email', 'phoneNumber']
|
||||
|
||||
return [
|
||||
[
|
||||
@@ -26,6 +27,6 @@ export const useColumns = (): [string[], string[]] => {
|
||||
...nameColumns,
|
||||
...adminColumns,
|
||||
],
|
||||
excelOnlyColumns,
|
||||
excelRequired,
|
||||
]
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs, { extend as dayjsExtend } from 'dayjs'
|
||||
import isBetween from 'dayjs/plugin/isBetween'
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
|
||||
@@ -12,9 +12,9 @@ import { useLanguage } from '@/components/LanguagePicker/useLanguage'
|
||||
import { useGetSemestersQuery } from '@/redux/semesters'
|
||||
import { SearchResultTable } from './SearchResultTable'
|
||||
|
||||
dayjs.extend(isBetween)
|
||||
dayjs.extend(isSameOrBefore)
|
||||
dayjs.extend(isSameOrAfter)
|
||||
dayjsExtend(isBetween)
|
||||
dayjsExtend(isSameOrBefore)
|
||||
dayjsExtend(isSameOrAfter)
|
||||
|
||||
export const findCorrectProgramme = (student, coursecodes, semesters, startDate, endDate, currentSemester) => {
|
||||
let programme
|
||||
|
||||
@@ -13,7 +13,8 @@ export const useColumns = ({ programme }): [string[], string[]] => {
|
||||
|
||||
const adminColumns = isAdmin ? ['extent', 'updatedAt'] : []
|
||||
|
||||
const excelOnlyColumns = ['email', 'phoneNumber']
|
||||
/* REQUIRED TO ALWAYS BE VISIBLE */
|
||||
const excelRequired = ['studentNumber', 'lastName', 'firstNames', 'email', 'phoneNumber']
|
||||
|
||||
return [
|
||||
[
|
||||
@@ -38,6 +39,6 @@ export const useColumns = ({ programme }): [string[], string[]] => {
|
||||
...populationWithProgrammeColumns,
|
||||
...adminColumns,
|
||||
],
|
||||
excelOnlyColumns,
|
||||
excelRequired,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker/DatePicker'
|
||||
|
||||
@@ -30,56 +31,58 @@ export const DateSelector = ({ value, onChange, before, after, showSemesters })
|
||||
}
|
||||
}
|
||||
|
||||
const createDateButton = (date, label) => (
|
||||
<Button
|
||||
key={label}
|
||||
onClick={() => triggerOnChange(date)}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'primary.light',
|
||||
color: value?.isSame(date, 'day') ? 'primary.light' : 'text.primary',
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
const DateButton = ({ date, label }) => (
|
||||
<span>
|
||||
<Button
|
||||
key={label}
|
||||
// HACK: Use onMouseDown as it fires faster than onClick, bypassing the onBlur of whatever would be focused (the calendar) and prevents having to double click
|
||||
onMouseDown={() => triggerOnChange(date)}
|
||||
size="small"
|
||||
sx={{
|
||||
background: 'primary.light',
|
||||
color: value?.isSame(date, 'day') ? 'primary.light' : 'text.primary',
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</span>
|
||||
)
|
||||
|
||||
const CustomPicker = _ => {
|
||||
return (
|
||||
<Box sx={{ padding: '0 0.5em' }}>
|
||||
<Typography sx={{ paddingLeft: '0.3em', fontWeight: 'bold' }}>Semesters:</Typography>
|
||||
<List
|
||||
sx={{
|
||||
overflowY: 'auto',
|
||||
margin: 0,
|
||||
padding: '0.5em 0',
|
||||
maxHeight: '10em',
|
||||
borderTop: '1px solid #f9f9f9',
|
||||
}}
|
||||
>
|
||||
{Object.values(allSemesters)
|
||||
.filter(({ startdate, enddate }) => {
|
||||
const start = dayjs(startdate)
|
||||
const end = dayjs(enddate)
|
||||
const CustomPicker = _ => (
|
||||
<Box sx={{ padding: '0 0.5em' }}>
|
||||
<Typography sx={{ paddingLeft: '0.3em', fontWeight: 'bold' }}>Semesters:</Typography>
|
||||
<List
|
||||
sx={{
|
||||
overflowY: 'auto',
|
||||
padding: '0.5em 0',
|
||||
maxHeight: '10em',
|
||||
borderTop: '1px solid #f9f9f9',
|
||||
}}
|
||||
>
|
||||
{Object.values(allSemesters)
|
||||
.filter(({ startdate, enddate }) => {
|
||||
const start = dayjs(startdate)
|
||||
const end = dayjs(enddate)
|
||||
|
||||
return start.isBefore() && (!before || start.isBefore(before)) && (!after || end.isAfter(after))
|
||||
})
|
||||
.sort(({ startdate: a }, { startdate: b }) => new Date(b).getTime() - new Date(a).getTime())
|
||||
.map(({ name, startdate, enddate }) => (
|
||||
<ListItem
|
||||
key={`${getTextIn(name)}-${Math.random()}`}
|
||||
sx={{ padding: '0.2em 0.3em 0.2em', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Typography sx={{ flexGrow: 1 }}>{getTextIn(name)}</Typography>
|
||||
<Typography sx={{ margin: '0 0.25em' }}>{createDateButton(dayjs(startdate), 'Start')}</Typography>
|
||||
<Typography>{createDateButton(dayjs(enddate).subtract(1, 'days'), 'End')}</Typography>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return start.isBefore() && (!before || start.isBefore(before)) && (!after || end.isAfter(after))
|
||||
})
|
||||
.sort(({ startdate: a }, { startdate: b }) => new Date(b).getTime() - new Date(a).getTime())
|
||||
.map(({ name, startdate, enddate }) => (
|
||||
<ListItem
|
||||
key={`${getTextIn(name)}-${Math.random()}`}
|
||||
sx={{ padding: '0.2em 0.3em', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<Typography sx={{ flexGrow: 1 }}>{getTextIn(name)}</Typography>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<DateButton date={dayjs(startdate)} label="Start" />
|
||||
<DateButton date={dayjs(enddate).subtract(1, 'days')} label="End" />
|
||||
</Stack>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
@@ -90,10 +93,10 @@ export const DateSelector = ({ value, onChange, before, after, showSemesters })
|
||||
maxDate={dayjs(before ?? today)}
|
||||
minDate={after ? dayjs(after) : undefined}
|
||||
onChange={onChange}
|
||||
reduceAnimations
|
||||
showDaysOutsideCurrentMonth
|
||||
slotProps={{ popper: { disablePortal: true } }}
|
||||
// @ts-expect-error HACK: there should be a custom component that eats the picker
|
||||
slots={{ tabs: showSemesters && CustomPicker }}
|
||||
slots={{ tabs: showSemesters && CustomPicker, desktopTrapFocus: undefined }}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { ADMISSION_TYPES } from '@/common'
|
||||
import { FormattedStudent } from '@oodikone/shared/types/studentData'
|
||||
import { FilterTrayProps } from '../FilterTray'
|
||||
import { FilterSelect } from './common/FilterSelect'
|
||||
import { createFilter } from './createFilter'
|
||||
|
||||
const findAllStudyRightsForProgramme = (student, programme) =>
|
||||
const findAllStudyRightsForProgramme = (student: FormattedStudent, programme: string) =>
|
||||
student.studyRights.filter(studyRight => studyRight.studyRightElements.some(el => el.code === programme))
|
||||
|
||||
export const filter = code => value => student => {
|
||||
const programmeStudyRights = findAllStudyRightsForProgramme(student, code)
|
||||
export const filter = (programme: string, value: string | null) => (student: FormattedStudent) => {
|
||||
const programmeStudyRights = findAllStudyRightsForProgramme(student, programme)
|
||||
const fixedValue = value !== 'Valintakoe' ? value : 'Koepisteet'
|
||||
|
||||
return programmeStudyRights.some(
|
||||
@@ -22,7 +23,7 @@ export const filter = code => value => student => {
|
||||
const AdmissionTypeFilterCard = ({ args, options, onOptionsChange, students }: FilterTrayProps) => {
|
||||
const code = args.programme
|
||||
const { selected } = options
|
||||
const count = (admissionType: string | null): number => students.filter(filter(code)(admissionType)).length
|
||||
const count = (admissionType: string | null): number => students.filter(filter(code, admissionType)).length
|
||||
|
||||
const selectOptions = Object.entries(ADMISSION_TYPES)
|
||||
.filter(([_, admissionType]) => !!admissionType)
|
||||
@@ -57,15 +58,15 @@ export const admissionTypeFilter = createFilter({
|
||||
title: 'Admission type',
|
||||
|
||||
defaultOptions: {
|
||||
selected: null,
|
||||
selected: '',
|
||||
},
|
||||
|
||||
isActive: ({ selected }) => !!selected,
|
||||
isActive: ({ selected }) => !!selected.length,
|
||||
|
||||
filter(student, { args, options }) {
|
||||
const { selected } = options
|
||||
|
||||
return filter(args.programme)(selected)(student)
|
||||
return filter(args.programme, selected)(student)
|
||||
},
|
||||
|
||||
render: AdmissionTypeFilterCard,
|
||||
|
||||
@@ -1,42 +1,64 @@
|
||||
import ClearIcon from '@mui/icons-material/Clear'
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'
|
||||
import Box from '@mui/material/Box'
|
||||
import Collapse from '@mui/material/Collapse'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { FC, useState } from 'react'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { InfoBox } from '@/components/material/InfoBox'
|
||||
|
||||
import type { Filter } from '../createFilter'
|
||||
|
||||
export const FilterCard: FC<{
|
||||
const ConditionalInfoBox = ({ info }) => {
|
||||
return info ? <InfoBox content={info} mini /> : null
|
||||
}
|
||||
|
||||
export const FilterCard = ({
|
||||
active,
|
||||
filter,
|
||||
children,
|
||||
onClear,
|
||||
}: {
|
||||
active: boolean
|
||||
filter: Filter
|
||||
children: React.ReactNode
|
||||
onClear: () => void
|
||||
}> = ({ active, filter, children, onClear }) => {
|
||||
}) => {
|
||||
const [opened, setOpened] = useState<boolean>(active)
|
||||
useEffect(() => setOpened(active), [active])
|
||||
|
||||
const { info, key, title } = filter
|
||||
|
||||
const handleOnClick = () => setOpened(state => !state)
|
||||
|
||||
const iconStyle = {
|
||||
transition: 'transform 300ms',
|
||||
...(opened ? { transform: 'rotate(90deg)' } : {}),
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack data-active={active} data-cy={`${key}-filter-card`} spacing={1.2} sx={{ width: '100%' }}>
|
||||
<Stack
|
||||
data-active={active}
|
||||
data-cy={`${key}-filter-card`}
|
||||
padding={0.5}
|
||||
spacing={Number(opened) * 1.2}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
<Box
|
||||
data-cy={`${key}-header`}
|
||||
sx={{ alignItems: 'center', justifyContent: 'center', cursor: 'pointer', display: 'inline-flex' }}
|
||||
>
|
||||
<IconButton onClick={handleOnClick} size="small">
|
||||
{opened ? <KeyboardArrowDownIcon /> : <KeyboardArrowRightIcon />}
|
||||
<IconButton onClick={handleOnClick} size="small" sx={iconStyle}>
|
||||
<KeyboardArrowRightIcon />
|
||||
</IconButton>
|
||||
<Typography
|
||||
component="span"
|
||||
fontWeight={500}
|
||||
fontWeight="medium"
|
||||
onClick={handleOnClick}
|
||||
sx={{ mr: 1, flex: 1, whiteSpace: 'wrap' }}
|
||||
sx={{ flex: 1, whiteSpace: 'wrap' }}
|
||||
variant="subtitle1"
|
||||
>
|
||||
{title}
|
||||
@@ -47,17 +69,19 @@ export const FilterCard: FC<{
|
||||
onClick={() => onClear()}
|
||||
sx={{
|
||||
color: theme => theme.palette.error.dark,
|
||||
mr: info ? 0.5 : 0,
|
||||
m: '0 0.5',
|
||||
'&:hover': {
|
||||
color: theme => theme.palette.error.light,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{info ? <InfoBox content={info} mini /> : null}
|
||||
<ConditionalInfoBox info={info} />
|
||||
</Box>
|
||||
<Collapse in={opened}>
|
||||
<Box sx={{ px: 1, mb: 1.5 }}>{children}</Box>
|
||||
<Collapse in={opened} sx={{ transition: '300ms' }}>
|
||||
<Paper sx={{ p: 1.5 }} variant="outlined">
|
||||
{children}
|
||||
</Paper>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
@@ -3,40 +3,46 @@ import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
import Radio from '@mui/material/Radio'
|
||||
import RadioGroup, { type RadioGroupProps, useRadioGroup } from '@mui/material/RadioGroup'
|
||||
|
||||
type RadioOption = {
|
||||
type RadioOption<T> = {
|
||||
key: string | undefined
|
||||
text: string
|
||||
value: string
|
||||
value: T
|
||||
}
|
||||
|
||||
type FilterRadioProps = {
|
||||
defaultOption: RadioOption
|
||||
options: RadioOption[]
|
||||
type FilterRadioProps<T> = {
|
||||
defaultValue: RadioOption<T>['value']
|
||||
controlledValue?: RadioOption<T>['value']
|
||||
options: RadioOption<T>[]
|
||||
filterKey: string
|
||||
onChange: RadioGroupProps['onChange']
|
||||
}
|
||||
|
||||
export const FilterRadio = ({ defaultOption, options, onChange, filterKey }: FilterRadioProps) => {
|
||||
const RadioButton = ({ option }: { option: RadioOption }) => {
|
||||
const radioGroup = useRadioGroup()
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
checked={radioGroup?.value === String(option.value)}
|
||||
control={<Radio />}
|
||||
data-cy={`${filterKey}-radio-${option.key}`}
|
||||
label={option.text}
|
||||
value={option.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const RadioButton = <T,>({ filterKey, option }: { filterKey: string; option: RadioOption<T> }) => {
|
||||
const radioGroup = useRadioGroup()
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<RadioGroup defaultValue={defaultOption.value} name={filterKey} onChange={onChange}>
|
||||
<RadioButton option={defaultOption} />
|
||||
<FormControlLabel
|
||||
checked={radioGroup?.value === String(option.value)}
|
||||
control={<Radio />}
|
||||
data-cy={`${filterKey}-radio-${option.key}`}
|
||||
label={option.text}
|
||||
value={option.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const FilterRadio = <T,>({
|
||||
defaultValue,
|
||||
controlledValue,
|
||||
options,
|
||||
onChange,
|
||||
filterKey,
|
||||
}: FilterRadioProps<T>) => {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'left', px: 1 }}>
|
||||
<RadioGroup defaultValue={defaultValue} name={filterKey} onChange={onChange} value={controlledValue}>
|
||||
{options.map(option => (
|
||||
<RadioButton key={option.key} option={option} />
|
||||
<RadioButton filterKey={filterKey} key={option.key} option={option} />
|
||||
))}
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import Autocomplete from '@mui/material/Autocomplete'
|
||||
import Box from '@mui/material/Box'
|
||||
import Popper from '@mui/material/Popper'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
|
||||
type ValidValueType = string
|
||||
|
||||
type SelectOption<T extends ValidValueType> = {
|
||||
key?: string
|
||||
text: string
|
||||
value: T
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type FilterSearchableSelectProps<T extends ValidValueType> = {
|
||||
label: string
|
||||
options: SelectOption<T>[]
|
||||
value: SelectOption<T> | SelectOption<T>[] | null
|
||||
onChange: (target: any) => void // target varies per implementation
|
||||
filterKey: string
|
||||
multiple?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches arbitrary user input against values in options.
|
||||
* Multiple values can be entered, accepted separators: [',', ';', '\n' ' ']
|
||||
*/
|
||||
const matchUserInput = (options: SelectOption<string>[], target: any[]) => {
|
||||
const parsedInput = target.flatMap(item => item.split(/[\s,;]+/).filter(Boolean))
|
||||
return options.filter(opt => parsedInput.includes(opt.value)).map(opt => opt.value)
|
||||
}
|
||||
|
||||
export const FilterSearchableSelect = <T extends ValidValueType = string>({
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
filterKey,
|
||||
multiple,
|
||||
}: FilterSearchableSelectProps<T>) => {
|
||||
const disabled = !options.length
|
||||
|
||||
const CustomWidthPopper = props => (
|
||||
<Popper
|
||||
{...props}
|
||||
id={`${filterKey}-popper`}
|
||||
placement="bottom-start"
|
||||
style={{ minWidth: '14em', maxWidth: '40em' }}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Tooltip title={disabled ? 'No valid values found' : ''}>
|
||||
<Autocomplete
|
||||
autoHighlight
|
||||
blurOnSelect
|
||||
clearOnEscape
|
||||
data-cy={`${filterKey}-selector`}
|
||||
disabled={disabled}
|
||||
filterSelectedOptions
|
||||
freeSolo={multiple}
|
||||
fullWidth
|
||||
getOptionLabel={opt => (typeof opt === 'string' ? opt : opt.text)}
|
||||
limitTags={5}
|
||||
multiple={multiple}
|
||||
onChange={(_, target, reason) => {
|
||||
// Target is always an array if multiple is enabled
|
||||
if (multiple && Array.isArray(target)) {
|
||||
if (reason === 'createOption') {
|
||||
onChange(matchUserInput(options, target))
|
||||
} else {
|
||||
onChange(target.map(item => (typeof item === 'string' ? item : item.value)))
|
||||
}
|
||||
} else if (target !== null && typeof target !== 'string' && !Array.isArray(target)) {
|
||||
// When not in multiple mode (=freesolo is off) target is always SelectOption<T>
|
||||
onChange(target)
|
||||
}
|
||||
}}
|
||||
options={options}
|
||||
renderInput={params => <TextField {...params} id={`${filterKey}-label`} label={label} size="small" />}
|
||||
slots={{
|
||||
popper: CustomWidthPopper,
|
||||
}}
|
||||
value={value}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import FormControl from '@mui/material/FormControl'
|
||||
import InputLabel from '@mui/material/InputLabel'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
|
||||
// HACK: This is ripped off of MUI MenuItem error.
|
||||
// TODO: Find an actual type to replace this.
|
||||
@@ -41,24 +42,31 @@ export const FilterSelect = <T extends ValidValueType = string>({
|
||||
multiple,
|
||||
InputItem,
|
||||
MenuItem,
|
||||
}: FilterSelectProps<T>) => (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<FormControl sx={{ width: '95%' }} variant="outlined">
|
||||
<InputLabel id={`${filterKey}-label`} size="small">
|
||||
{label}
|
||||
</InputLabel>
|
||||
<Select
|
||||
data-cy={`${filterKey}-selector`}
|
||||
label={label}
|
||||
labelId={filterKey}
|
||||
multiple={multiple}
|
||||
onChange={onChange}
|
||||
renderValue={InputItem}
|
||||
size="small"
|
||||
value={value}
|
||||
>
|
||||
{options.map(MenuItem ?? DefaultMenuItem)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
)
|
||||
}: FilterSelectProps<T>) => {
|
||||
const disabled = !options.length
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<InputLabel id={`${filterKey}-label`} size="small" sx={{ textOverflow: 'ellipsis', pr: 2.5 }}>
|
||||
{label}
|
||||
</InputLabel>
|
||||
<Tooltip title={disabled ? 'No valid values found' : null}>
|
||||
<Select
|
||||
data-cy={`${filterKey}-selector`}
|
||||
disabled={disabled}
|
||||
label={label}
|
||||
labelId={filterKey}
|
||||
multiple={multiple}
|
||||
onChange={onChange}
|
||||
renderValue={InputItem}
|
||||
size="small"
|
||||
value={value}
|
||||
>
|
||||
{options.map(MenuItem ?? DefaultMenuItem)}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</FormControl>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,26 +11,24 @@ type SwitchOption = {
|
||||
|
||||
type FilterSwitchProps = {
|
||||
options: SwitchOption[]
|
||||
filterKey?: string
|
||||
filterKey: string
|
||||
}
|
||||
|
||||
export const FilterSwitch = ({ options, filterKey }: FilterSwitchProps) => {
|
||||
const SwitchButton = ({ option }: { option: SwitchOption }) => {
|
||||
return (
|
||||
<FormControlLabel
|
||||
checked={option.checked}
|
||||
control={<Switch />}
|
||||
data-cy={`${filterKey}-radio-${option.key}`}
|
||||
label={option.text}
|
||||
onClick={option.onClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const SwitchButton = ({ filterKey, option }: { filterKey: string; option: SwitchOption }) => (
|
||||
<FormControlLabel
|
||||
checked={!!option.checked}
|
||||
control={<Switch />}
|
||||
data-cy={`${filterKey}-radio-${option.key}`}
|
||||
label={option.text}
|
||||
onClick={option.onClick}
|
||||
/>
|
||||
)
|
||||
|
||||
export const FilterSwitch = ({ options, filterKey }: FilterSwitchProps) => {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{options.map(option => (
|
||||
<SwitchButton key={option.key} option={option} />
|
||||
<SwitchButton filterKey={filterKey} key={option.key} option={option} />
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import ClearIcon from '@mui/icons-material/Clear'
|
||||
import Box from '@mui/material/Box'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
import { useLanguage } from '@/components/LanguagePicker/useLanguage'
|
||||
@@ -25,7 +26,12 @@ const filterTexts = {
|
||||
label: 'Enrolled, No Grade',
|
||||
},
|
||||
}
|
||||
|
||||
const getSubstitutionTooltip = (substitutions: string[]) => (
|
||||
<Typography fontSize="0.9rem" whiteSpace="pre-line">
|
||||
Included course substitutions:
|
||||
{substitutions.map(code => `\n${code}`)}
|
||||
</Typography>
|
||||
)
|
||||
export const CourseCard = ({
|
||||
course,
|
||||
filterType,
|
||||
@@ -56,7 +62,18 @@ export const CourseCard = ({
|
||||
})}
|
||||
>
|
||||
<Stack direction="row" sx={{ justifyContent: 'space-between' }}>
|
||||
<Typography sx={{ my: 2 }}>{getTextIn(course.course?.name)}</Typography>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography>{getTextIn(course.course?.name)}</Typography>
|
||||
{course.course?.substitutions?.length ? (
|
||||
<Tooltip title={getSubstitutionTooltip(course.course.substitutions)}>
|
||||
<Typography sx={{ color: 'text.secondary' }}>
|
||||
{course.course?.code}... +{course.course?.substitutions?.length}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Typography sx={{ color: 'text.secondary' }}>{course.course?.code}</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<ClearIcon
|
||||
data-cy={`courseFilter-${course.course?.code}-clear`}
|
||||
onClick={() => onChange(null)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { keyBy } from 'lodash'
|
||||
|
||||
import { useLanguage } from '@/components/LanguagePicker/useLanguage'
|
||||
import type { FilterTrayProps } from '../../FilterTray'
|
||||
import { FilterSelect } from '../common/FilterSelect'
|
||||
import { FilterSearchableSelect } from '../common/FilterSearchableSelect'
|
||||
import { createFilter } from '../createFilter'
|
||||
import { CourseCard } from './CourseCard'
|
||||
import { FilterType } from './filterType'
|
||||
@@ -10,7 +10,7 @@ import { FilterType } from './filterType'
|
||||
type CourseStats = Record<string, any>
|
||||
|
||||
const CourseFilterCard = ({ precomputed, options, onOptionsChange }: FilterTrayProps) => {
|
||||
const courseStats: CourseStats = precomputed
|
||||
const courseStats: CourseStats = precomputed.courses
|
||||
|
||||
const courseFilters: Record<string, keyof typeof FilterType> = options?.courseFilters
|
||||
const { getTextIn } = useLanguage()
|
||||
@@ -34,12 +34,12 @@ const CourseFilterCard = ({ precomputed, options, onOptionsChange }: FilterTrayP
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterSelect
|
||||
<FilterSearchableSelect
|
||||
filterKey="courseFilter"
|
||||
label="Select course"
|
||||
onChange={({ target }) => setCourseFilter(target.value, FilterType.ALL)}
|
||||
label="Select a course"
|
||||
onChange={target => setCourseFilter(target.value, FilterType.ALL)}
|
||||
options={dropdownOptions}
|
||||
value={''}
|
||||
value={null}
|
||||
/>
|
||||
{Object.entries(courseFilters).map(([code, type]) => (
|
||||
<CourseCard
|
||||
@@ -62,7 +62,25 @@ export const courseFilter = createFilter({
|
||||
courseFilters: {},
|
||||
},
|
||||
|
||||
precompute: ({ args }) => keyBy(args.courses, 'course.code'),
|
||||
precompute: ({ args }) => {
|
||||
const substitutedBy = args.courses.reduce(
|
||||
(acc, { course }) => {
|
||||
const { code, substitutions } = course
|
||||
for (const original of substitutions) {
|
||||
acc[original] ??= []
|
||||
acc[original].push(code)
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, string[]>
|
||||
)
|
||||
|
||||
return {
|
||||
courses: keyBy(args.courses, 'course.code'),
|
||||
substitutedBy,
|
||||
}
|
||||
},
|
||||
|
||||
isActive: ({ courseFilters }) => Object.keys(courseFilters).length > 0,
|
||||
|
||||
@@ -75,10 +93,14 @@ export const courseFilter = createFilter({
|
||||
}
|
||||
|
||||
for (const [code, filterType] of Object.entries(options.courseFilters)) {
|
||||
const { students = {} } = precomputed[code]
|
||||
const key = filterKeys[filterType as string]
|
||||
const found = [code, ...(precomputed.substitutedBy[code] ?? [])].some(course => {
|
||||
const students = precomputed.courses?.[course]?.students ?? {}
|
||||
const key = filterKeys[filterType as string]
|
||||
|
||||
if (!students?.[key]?.includes(studentNumber)) return false
|
||||
return students?.[key]?.includes(studentNumber)
|
||||
})
|
||||
|
||||
if (!found) return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -14,7 +14,7 @@ const CreditDateFilterCard = ({ options, onOptionsChange }: FilterTrayProps) =>
|
||||
const { startDate, endDate } = options
|
||||
|
||||
return (
|
||||
<div className="card-content" style={{ marginTop: '0.5rem' }}>
|
||||
<div className="card-content">
|
||||
<DateRangeSelector
|
||||
onChange={([startDate, endDate]) =>
|
||||
onOptionsChange({
|
||||
|
||||
@@ -24,6 +24,7 @@ const GraduatedFromProgrammeFilterCard = ({ args, onOptionsChange }: FilterTrayP
|
||||
const typeOfCombined = isLicentiate ? 'Licentiate' : 'Master'
|
||||
const modeOptions = isCombinedExtent
|
||||
? [
|
||||
{ key: 'All', text: 'All', value: DEFAULT_STATE },
|
||||
{ key: 'graduated-bachelor', text: "Graduated with Bachelor's", value: GRADUATION_PHASE.GRADUATED_BACHELOR },
|
||||
{
|
||||
key: 'graduated-master',
|
||||
@@ -42,13 +43,14 @@ const GraduatedFromProgrammeFilterCard = ({ args, onOptionsChange }: FilterTrayP
|
||||
},
|
||||
]
|
||||
: [
|
||||
{ key: 'All', text: 'All', value: DEFAULT_STATE },
|
||||
{ key: 'graduated-true', text: 'Graduated', value: GRADUATION_PHASE.GRADUATED },
|
||||
{ key: 'graduated-false', text: 'Not graduated', value: GRADUATION_PHASE.NOT_GRADUATED },
|
||||
]
|
||||
|
||||
return (
|
||||
<FilterRadio
|
||||
defaultOption={{ key: undefined, text: 'All', value: DEFAULT_STATE }}
|
||||
defaultValue={DEFAULT_STATE}
|
||||
filterKey={graduatedFromProgrammeFilter.key}
|
||||
onChange={({ target }) => onOptionsChange({ mode: target.value })}
|
||||
options={modeOptions}
|
||||
|
||||
@@ -44,11 +44,11 @@ const CombinedProgramme = ({ options, onOptionsChange, combinedProgramme }) => {
|
||||
value: key,
|
||||
}))
|
||||
|
||||
const defaultOption = modeOptions.shift()!
|
||||
const defaultOption = modeOptions.at(0)?.value ?? ''
|
||||
|
||||
return (
|
||||
<FilterRadio
|
||||
defaultOption={defaultOption}
|
||||
defaultValue={defaultOption}
|
||||
filterKey="hopsFilter"
|
||||
onChange={({ target }) => modeObject[target.value]()}
|
||||
options={modeOptions}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs, { extend as dayjsExtend } from 'dayjs'
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
|
||||
|
||||
@@ -10,11 +10,26 @@ import { FilterTrayProps } from '../FilterTray'
|
||||
import { FilterSelect } from './common/FilterSelect'
|
||||
import { createFilter } from './createFilter'
|
||||
|
||||
dayjs.extend(isSameOrBefore)
|
||||
dayjs.extend(isSameOrAfter)
|
||||
dayjsExtend(isSameOrBefore)
|
||||
dayjsExtend(isSameOrAfter)
|
||||
|
||||
const NO_PROGRAMME = { code: '00000', name: { en: 'No programme', fi: 'Ei ohjelmaa' } }
|
||||
|
||||
const SelectInputItem = ({ text }) => (
|
||||
<Typography fontWeight={500} px={0.5}>
|
||||
{text}
|
||||
</Typography>
|
||||
)
|
||||
|
||||
const CourseMenuStack = ({ disabled, text, value, description }) => (
|
||||
<MenuItem disabled={disabled} value={value}>
|
||||
<Stack>
|
||||
<Typography fontWeight={500}>{text}</Typography>
|
||||
<Typography sx={{ color: '#5b5b5b', fontSize: '0.9em' }}>{description}</Typography>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
)
|
||||
|
||||
const ProgrammeFilterCard = ({
|
||||
args,
|
||||
onOptionsChange,
|
||||
@@ -94,20 +109,15 @@ const ProgrammeFilterCard = ({
|
||||
value={selectedProgrammes}
|
||||
/>
|
||||
<FilterSelect
|
||||
InputItem={value => (
|
||||
<Typography fontWeight={500} px={0.5}>
|
||||
{modeOptions.find(mode => value === mode.value)!.text}
|
||||
</Typography>
|
||||
)}
|
||||
InputItem={value => <SelectInputItem text={modeOptions.find(mode => value === mode.value)!.text} />}
|
||||
MenuItem={option => (
|
||||
<MenuItem disabled={option.disabled} key={option.key} value={option.value}>
|
||||
<Stack>
|
||||
<Typography fontWeight={500}>{option.text}</Typography>
|
||||
<Typography sx={{ color: '#5b5b5b', fontSize: '0.9em' }}>
|
||||
{modes.find(mode => option.key === mode.key)?.description}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
<CourseMenuStack
|
||||
description={modes.find(mode => option.key === mode.key)?.description}
|
||||
disabled={option.disabled}
|
||||
key={option.key}
|
||||
text={option.text}
|
||||
value={option.value}
|
||||
/>
|
||||
)}
|
||||
filterKey="programmeFilter-mode"
|
||||
label="Select Mode"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { includes, isEqual, union } from 'lodash'
|
||||
import { filterToolTips } from '@/common/InfoToolTips'
|
||||
import { formatToArray } from '@oodikone/shared/util'
|
||||
import { FilterTrayProps } from '../FilterTray'
|
||||
import { FilterSelect } from './common/FilterSelect'
|
||||
import { FilterSearchableSelect } from './common/FilterSearchableSelect'
|
||||
import { createFilter } from './createFilter'
|
||||
|
||||
const StudentNumberFilterCard = ({ options, onOptionsChange, students }: FilterTrayProps) => {
|
||||
@@ -19,10 +19,11 @@ const StudentNumberFilterCard = ({ options, onOptionsChange, students }: FilterT
|
||||
|
||||
return (
|
||||
<Stack gap={1}>
|
||||
<FilterSelect
|
||||
<FilterSearchableSelect
|
||||
filterKey="studentNumberFilter-allowlist"
|
||||
label="Allowed student numbers"
|
||||
multiple
|
||||
onChange={({ target }) => onOptionsChange({ ...options, allowlist: target.value })}
|
||||
onChange={studentNumbers => onOptionsChange({ ...options, allowlist: studentNumbers })}
|
||||
options={dropdownOptions}
|
||||
value={options.allowlist}
|
||||
/>
|
||||
@@ -41,10 +42,11 @@ const StudentNumberFilterCard = ({ options, onOptionsChange, students }: FilterT
|
||||
<Typography>Swap lists</Typography>
|
||||
</Stack>
|
||||
</Button>
|
||||
<FilterSelect
|
||||
<FilterSearchableSelect
|
||||
filterKey="studentNumberFilter-blocklist"
|
||||
label="Excluded student numbers"
|
||||
multiple
|
||||
onChange={({ target }) => onOptionsChange({ ...options, blocklist: target.value })}
|
||||
onChange={studentNumbers => onOptionsChange({ ...options, blocklist: studentNumbers })}
|
||||
options={dropdownOptions}
|
||||
value={options.blocklist}
|
||||
/>
|
||||
|
||||
@@ -41,11 +41,11 @@ const StudyRightStatusFilterCard = ({ args, options, onOptionsChange }: FilterTr
|
||||
value: key,
|
||||
}))
|
||||
|
||||
const defaultOption = modeOptions.shift()!
|
||||
const defaultOption = modeOptions.at(0)?.value ?? ''
|
||||
|
||||
return (
|
||||
<FilterRadio
|
||||
defaultOption={defaultOption}
|
||||
defaultValue={defaultOption}
|
||||
filterKey="studyRightStatusFilter"
|
||||
onChange={({ target }) => modeObject[target.value]()}
|
||||
options={modeOptions}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createFilter } from './createFilter'
|
||||
const DEFAULT_STATE = '0' as const
|
||||
|
||||
const STUDYRIGHT_TYPES = [
|
||||
{ text: 'All', value: DEFAULT_STATE },
|
||||
{ text: 'Bachelor + master', value: '1' },
|
||||
{ text: 'Master only', value: '2' },
|
||||
]
|
||||
@@ -19,7 +20,7 @@ const StudyRightTypeFilterCard = ({ onOptionsChange }: FilterTrayProps) => {
|
||||
|
||||
return (
|
||||
<FilterRadio
|
||||
defaultOption={{ key: undefined, text: 'All', value: DEFAULT_STATE }}
|
||||
defaultValue={DEFAULT_STATE}
|
||||
filterKey={studyRightTypeFilter.key}
|
||||
onChange={({ target }) => onOptionsChange({ mode: target.value })}
|
||||
options={modeOptions}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Alert from '@mui/material/Alert'
|
||||
import { useLanguage } from '@/components/LanguagePicker/useLanguage'
|
||||
import type { StudyTrack } from '@oodikone/shared/types'
|
||||
import { FilterTrayProps } from '../FilterTray'
|
||||
@@ -15,6 +16,9 @@ const StudyTrackFilterCard = ({ args, onOptionsChange, options, students }: Filt
|
||||
.filter(element => element.code === code && element.studyTrack !== null)
|
||||
.map(element => element.studyTrack) as StudyTrack[]
|
||||
|
||||
if (!Object.keys(validStudyTracks).length)
|
||||
return <Alert severity="warning">No study tracks have been defined for the selected students.</Alert>
|
||||
|
||||
const dropdownOptions = [...new Map(validStudyTracks.map(({ code, name }) => [code, name]))].map(([code, name]) => ({
|
||||
key: code,
|
||||
value: code,
|
||||
@@ -30,6 +34,7 @@ const StudyTrackFilterCard = ({ args, onOptionsChange, options, students }: Filt
|
||||
<FilterSelect
|
||||
filterKey={studyTrackFilter.key}
|
||||
label="Choose study track"
|
||||
multiple
|
||||
onChange={({ target }) => onOptionsChange({ selected: target.value })}
|
||||
options={dropdownOptions}
|
||||
value={selected}
|
||||
|
||||
@@ -20,7 +20,7 @@ const TagsFilterCard = ({ options, onOptionsChange, students }: FilterTrayProps)
|
||||
}
|
||||
|
||||
if (!Object.keys(tagCounts).length)
|
||||
return <Alert severity="warning">No tags have been defined for any of the selected students.</Alert>
|
||||
return <Alert severity="warning">No tags have been defined for the selected students.</Alert>
|
||||
|
||||
const dropdownOptions = Object.entries(tagCounts).map(([tagId, { count, name }]) => ({
|
||||
key: `tag-${tagId}`,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { filterToolTips } from '@/common/InfoToolTips'
|
||||
import { FilterTrayProps } from '../FilterTray'
|
||||
import { FilterRadio } from './common/FilterRadio'
|
||||
@@ -8,28 +10,24 @@ const TransferredToProgrammeFilterCard = ({ options, onOptionsChange }: FilterTr
|
||||
|
||||
const toggle = buttonValue =>
|
||||
onOptionsChange({
|
||||
transferred: transferred === buttonValue ? null : buttonValue,
|
||||
transferred: String(transferred) === buttonValue ? null : buttonValue,
|
||||
})
|
||||
|
||||
const modeObject = {
|
||||
All: () => toggle(''),
|
||||
Transferred: () => toggle(true),
|
||||
'Not transferred': () => toggle(false),
|
||||
All: '',
|
||||
Transferred: 'true',
|
||||
'Not transferred': 'false',
|
||||
}
|
||||
|
||||
const modeOptions = Object.keys(modeObject).map(key => ({
|
||||
key,
|
||||
text: key,
|
||||
value: key,
|
||||
}))
|
||||
|
||||
const defaultOption = modeOptions.shift()!
|
||||
const defaultOption = useMemo(() => transferred ?? modeOptions.at(0)?.value ?? '', [])
|
||||
const modeOptions = Object.entries(modeObject).map(([key, value]) => ({ key, text: key, value }))
|
||||
|
||||
return (
|
||||
<FilterRadio
|
||||
defaultOption={defaultOption}
|
||||
controlledValue={transferred}
|
||||
defaultValue={defaultOption}
|
||||
filterKey={transferredToProgrammeFilter.key}
|
||||
onChange={({ target }) => modeObject[target.value]()}
|
||||
onChange={({ target }) => toggle(target.value)}
|
||||
options={modeOptions}
|
||||
/>
|
||||
)
|
||||
@@ -51,25 +49,13 @@ export const transferredToProgrammeFilter = createFilter({
|
||||
filter: (student, { options }) => {
|
||||
const { transferred } = options
|
||||
|
||||
return student.transferredStudyright === transferred
|
||||
return String(student.transferredStudyright) === transferred
|
||||
},
|
||||
|
||||
actions: {
|
||||
set: (options, value) => {
|
||||
options.transferred = value
|
||||
|
||||
return options
|
||||
},
|
||||
set: (options, value) => Object.assign(options, { transferred: String(value) }),
|
||||
// Toggle between 'null' -> 'All' and 'false' -> 'Not transferred'
|
||||
toggle: options => {
|
||||
if (options.transferred !== false) {
|
||||
options.transferred = false
|
||||
} else {
|
||||
options.transferred = ''
|
||||
}
|
||||
|
||||
return options
|
||||
},
|
||||
toggle: options => Object.assign(options, { transferred: options.transferred !== 'false' ? 'false' : '' }),
|
||||
},
|
||||
|
||||
selectors: {
|
||||
|
||||
@@ -5,11 +5,14 @@ import TableContainer from '@mui/material/TableContainer'
|
||||
import TableHead from '@mui/material/TableHead'
|
||||
|
||||
import { type Table as TableType } from '@tanstack/react-table'
|
||||
import { OodiTableAggregationRow } from './components/AggregationRow'
|
||||
import { OodiTableDataRow } from './components/Cell'
|
||||
import { OodiTableHeaderGroup } from './components/Header'
|
||||
import { OodiTablePagination } from './components/Pagination'
|
||||
|
||||
export const OodiTableContainer = <OTData,>({ table }: { table: TableType<OTData> }) => {
|
||||
const verticalHeaders = table.getState().useVerticalHeaders ?? []
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2, borderRadius: 0 }} variant="outlined">
|
||||
<TableContainer
|
||||
@@ -35,7 +38,7 @@ export const OodiTableContainer = <OTData,>({ table }: { table: TableType<OTData
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{table.getHeaderGroups().map(OodiTableHeaderGroup)}
|
||||
{table.getHeaderGroups().map(group => OodiTableHeaderGroup(group, verticalHeaders))}
|
||||
</TableHead>
|
||||
<TableBody
|
||||
sx={{
|
||||
@@ -47,6 +50,7 @@ export const OodiTableContainer = <OTData,>({ table }: { table: TableType<OTData
|
||||
},
|
||||
}}
|
||||
>
|
||||
{table.getAggregationRowModel().rows.map(OodiTableAggregationRow)}
|
||||
{table.getRowModel().rows.map(OodiTableDataRow)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import TableCell, { type TableCellProps } from '@mui/material/TableCell'
|
||||
import TableRow from '@mui/material/TableRow'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { flexRender } from '@tanstack/react-table'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import { AggregationRow } from '../features/aggregationRows'
|
||||
import { getCommonPinningStyles } from '../styles'
|
||||
|
||||
const OodiTableAggregationCell: FC<TableCellProps> = ({ children, ...props }) => {
|
||||
return (
|
||||
<TableCell
|
||||
{...props}
|
||||
sx={{
|
||||
borderWidth: '0 1px 1px 0',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'grey.300',
|
||||
padding: '0 1em',
|
||||
height: '3em',
|
||||
...props?.sx,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
)
|
||||
}
|
||||
|
||||
export const OodiTableAggregationRow = <OTData,>(row: AggregationRow<OTData>) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map(cell => {
|
||||
const { maxSize } = cell.column.columnDef
|
||||
return (
|
||||
<OodiTableAggregationCell
|
||||
key={cell.id}
|
||||
sx={{
|
||||
...getCommonPinningStyles(cell.column),
|
||||
maxWidth: maxSize !== Number.MAX_SAFE_INTEGER ? maxSize : '20em',
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.aggregatedCell, cell.getContext())}
|
||||
</OodiTableAggregationCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
)
|
||||
@@ -3,11 +3,11 @@ import TableCell, { type TableCellProps } from '@mui/material/TableCell'
|
||||
import TableRow from '@mui/material/TableRow'
|
||||
import { flexRender } from '@tanstack/react-table'
|
||||
import type { Row } from '@tanstack/react-table'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { FC } from 'react'
|
||||
|
||||
import { getCommonPinningStyles } from '../styles'
|
||||
|
||||
const OodiTableCell: FC<TableCellProps & { children?: ReactNode }> = ({ children, ...props }) => {
|
||||
const OodiTableCell: FC<TableCellProps> = ({ children, ...props }) => {
|
||||
return (
|
||||
<TableCell
|
||||
{...props}
|
||||
@@ -15,19 +15,17 @@ const OodiTableCell: FC<TableCellProps & { children?: ReactNode }> = ({ children
|
||||
borderWidth: '0 1px 1px 0',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'grey.300',
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
padding: '0 1em',
|
||||
height: '3em',
|
||||
minHeight: '3em',
|
||||
...props?.sx,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
maxWidth: '20em',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
@@ -39,10 +37,19 @@ const OodiTableCell: FC<TableCellProps & { children?: ReactNode }> = ({ children
|
||||
|
||||
export const OodiTableDataRow = <OTData,>(row: Row<OTData>) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<OodiTableCell key={cell.id} sx={{ ...getCommonPinningStyles(cell.column) }}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</OodiTableCell>
|
||||
))}
|
||||
{row.getVisibleCells().map(cell => {
|
||||
const { maxSize } = cell.column.columnDef
|
||||
return (
|
||||
<OodiTableCell
|
||||
key={cell.id}
|
||||
sx={{
|
||||
...getCommonPinningStyles(cell.column),
|
||||
maxWidth: maxSize !== Number.MAX_SAFE_INTEGER ? maxSize : '20em',
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</OodiTableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import Box from '@mui/material/Box'
|
||||
import { SxProps, Theme } from '@mui/material/styles'
|
||||
import TableCell, { type TableCellProps } from '@mui/material/TableCell'
|
||||
import TableRow from '@mui/material/TableRow'
|
||||
import { flexRender } from '@tanstack/react-table'
|
||||
import type { HeaderGroup } from '@tanstack/react-table'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
|
||||
import { getCommonPinningStyles } from '../styles'
|
||||
import { getCommonPinningStyles, verticalStyles } from '../styles'
|
||||
import { OodiTableSortIcons } from './SortIcons'
|
||||
|
||||
const OodiTableHeader: FC<TableCellProps & { children?: ReactNode }> = ({ children, ...props }) => {
|
||||
@@ -27,7 +29,7 @@ const OodiTableHeader: FC<TableCellProps & { children?: ReactNode }> = ({ childr
|
||||
)
|
||||
}
|
||||
|
||||
export const OodiTableHeaderGroup = <OTData,>(headerGroup: HeaderGroup<OTData>) => (
|
||||
export const OodiTableHeaderGroup = <OTData,>(headerGroup: HeaderGroup<OTData>, verticalHeaders: string[]) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => {
|
||||
if (header.depth - header.column.depth > 1) return null
|
||||
@@ -38,18 +40,48 @@ export const OodiTableHeaderGroup = <OTData,>(headerGroup: HeaderGroup<OTData>)
|
||||
rowSpan = leafs[leafs.length - 1].depth - header.depth
|
||||
}
|
||||
|
||||
const isVertical = verticalHeaders.includes(header.id)
|
||||
|
||||
const sx = {
|
||||
cursor: header.column.getCanSort() ? 'pointer' : 'inherit',
|
||||
...(isVertical && verticalStyles),
|
||||
...getCommonPinningStyles(header.column),
|
||||
}
|
||||
|
||||
return (
|
||||
<OodiTableHeader
|
||||
colSpan={header.colSpan}
|
||||
key={header.id}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
rowSpan={rowSpan}
|
||||
sx={{
|
||||
...getCommonPinningStyles(header.column),
|
||||
cursor: header.column.getCanSort() ? 'pointer' : 'inherit',
|
||||
}}
|
||||
sx={sx as SxProps<Theme>}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{isVertical ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
writingMode: 'vertical-rl',
|
||||
textOrientation: 'mixed',
|
||||
whiteSpace: 'normal',
|
||||
overflow: 'visible',
|
||||
height: '100%',
|
||||
maxWidth: '15em',
|
||||
minHeight: '160px',
|
||||
maxHeight: '240px',
|
||||
margin: '0 auto',
|
||||
}}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box>{flexRender(header.column.columnDef.header, header.getContext())}</Box>
|
||||
)}
|
||||
<OodiTableSortIcons canSort={header.column.getCanSort()} isSorted={header.column.getIsSorted()} />
|
||||
</OodiTableHeader>
|
||||
)
|
||||
|
||||
@@ -136,11 +136,14 @@ export const OodiTableExcelExport = <TData extends object>({
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
|
||||
const handleExport = () => {
|
||||
const selectedColumns = Object.keys(rowSelection).map(key => Number(key))
|
||||
const selectedRows = Object.entries(rowSelection)
|
||||
.filter(([_, selected]) => selected)
|
||||
.map(([key, _]) => Number(key))
|
||||
|
||||
const selectedCols = pivotedData.filter((_, index) => selectedRows.includes(index)).map(({ header }) => header)
|
||||
|
||||
const worksheet = utils.json_to_sheet(
|
||||
exportData.map(row =>
|
||||
Object.fromEntries(Object.entries(row).filter((_, index) => selectedColumns.includes(index)))
|
||||
)
|
||||
exportData.map(row => Object.fromEntries(Object.entries(row).filter(([key, _]) => selectedCols.includes(key))))
|
||||
)
|
||||
const workbook = utils.book_new()
|
||||
utils.book_append_sheet(workbook, worksheet)
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { Cell, CellContext, Column, Row, Table, TableFeature } from '@tanstack/react-table'
|
||||
|
||||
/**
|
||||
* This doesn't implement all the features of Row, so be aware of that
|
||||
*/
|
||||
export interface AggregationRow<TData> extends Row<TData> {
|
||||
getIsAggregationRow: () => true
|
||||
}
|
||||
|
||||
type AggregationRowValueFn<TData, TValue = unknown> = (ctx: {
|
||||
table: Table<TData>
|
||||
rowId: string
|
||||
column: Column<TData, TValue>
|
||||
}) => TValue
|
||||
|
||||
interface AggregationRowCellDef<TData, TValue = unknown> {
|
||||
id: string
|
||||
value: TValue | AggregationRowValueFn<TData, TValue>
|
||||
}
|
||||
|
||||
export type AggregationRowsInput<TData, TValue = unknown> =
|
||||
| AggregationRowCellDef<TData, TValue>[]
|
||||
| ((ctx: { table: Table<TData>; column: Column<TData> }) => AggregationRowCellDef<TData, TValue>[])
|
||||
|
||||
export interface AggregationRowModel<TData> {
|
||||
rows: AggregationRow<TData>[]
|
||||
flatRows: AggregationRow<TData>[]
|
||||
rowsById: Record<string, AggregationRow<TData>>
|
||||
}
|
||||
|
||||
const resolveAggregationRowsArray = <TData, TValue>(
|
||||
table: Table<TData>,
|
||||
column: Column<TData, TValue>
|
||||
): AggregationRowCellDef<TData, TValue>[] => {
|
||||
const input = column.columnDef.aggregationRows
|
||||
if (!input) return []
|
||||
return typeof input === 'function' ? (input({ table, column }) ?? []) : input
|
||||
}
|
||||
|
||||
const collectAggregationRowIds = <TData>(table: Table<TData>): string[] => {
|
||||
const seen: string[] = []
|
||||
const set = new Set<string>()
|
||||
|
||||
table.getAllLeafColumns().forEach(col => {
|
||||
const defs = resolveAggregationRowsArray(table, col)
|
||||
if (!defs) return
|
||||
for (const def of defs) {
|
||||
if (!set.has(def.id)) {
|
||||
set.add(def.id)
|
||||
seen.push(def.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
return seen
|
||||
}
|
||||
|
||||
const resolveAggregationCellValue = <TData, TValue>(
|
||||
table: Table<TData>,
|
||||
column: Column<TData, unknown>,
|
||||
aggRowId: string
|
||||
): TValue | undefined => {
|
||||
const defs = resolveAggregationRowsArray(table, column)
|
||||
const def = defs.find(def => def.id === aggRowId)
|
||||
if (!def) return undefined
|
||||
const { value } = def
|
||||
if (typeof value === 'function') {
|
||||
return (value as AggregationRowValueFn<TData>)({
|
||||
table,
|
||||
rowId: aggRowId,
|
||||
column,
|
||||
}) as TValue
|
||||
}
|
||||
return value as TValue
|
||||
}
|
||||
|
||||
const buildAggregationRowModel = <TData>(table: Table<TData>): AggregationRowModel<TData> => {
|
||||
const aggRowIds = collectAggregationRowIds(table)
|
||||
|
||||
if (!aggRowIds.length) {
|
||||
return {
|
||||
rows: [],
|
||||
flatRows: [],
|
||||
rowsById: {},
|
||||
} as unknown as AggregationRowModel<TData>
|
||||
}
|
||||
|
||||
const visibleLeafColumns =
|
||||
table.getVisibleLeafColumns?.() ?? table.getAllLeafColumns().filter((col: any) => col.getIsVisible?.() ?? true)
|
||||
|
||||
const rowsById: Record<string, AggregationRow<TData>> = {}
|
||||
const rows: AggregationRow<TData>[] = []
|
||||
const flatRows: AggregationRow<TData>[] = []
|
||||
|
||||
aggRowIds.forEach((aggRowId, index) => {
|
||||
const row: AggregationRow<TData> = {
|
||||
id: aggRowId,
|
||||
index,
|
||||
depth: 0,
|
||||
original: undefined as any,
|
||||
parentId: undefined,
|
||||
_valuesCache: {},
|
||||
_uniqueValuesCache: {},
|
||||
_groupingValuesCache: {},
|
||||
getValue: <TValue>(columnId: string): TValue => {
|
||||
const column = table.getColumn(columnId)
|
||||
return column
|
||||
? (resolveAggregationCellValue<TData, TValue>(table, column, aggRowId) as TValue)
|
||||
: (undefined as TValue)
|
||||
},
|
||||
|
||||
getVisibleCells: () => {
|
||||
if (!(row as any)._cells) {
|
||||
;(row as any)._cells = visibleLeafColumns.map(column => {
|
||||
const cell: Cell<TData, unknown> = {
|
||||
id: `${column.id}_${row.id}`,
|
||||
row,
|
||||
column,
|
||||
getValue: () => row.getValue(column.id),
|
||||
getContext: () =>
|
||||
({
|
||||
table,
|
||||
column,
|
||||
row,
|
||||
cell,
|
||||
getValue: cell.getValue,
|
||||
renderValue: cell.getValue(),
|
||||
}) as CellContext<TData, unknown>,
|
||||
renderValue: () => cell.getValue(),
|
||||
} as Cell<TData, unknown>
|
||||
return cell
|
||||
})
|
||||
}
|
||||
return (row as any)._cells as Cell<TData, unknown>[]
|
||||
},
|
||||
|
||||
/** Unimplemented and redundant, use getVisibleCells instead */
|
||||
getAllCells: () => undefined,
|
||||
getLeafRows: () => [row],
|
||||
getParentRow: () => undefined,
|
||||
subRows: [],
|
||||
getIsAggregationRow: () => true,
|
||||
} as unknown as AggregationRow<TData>
|
||||
|
||||
rows.push(row)
|
||||
flatRows.push(row)
|
||||
rowsById[row.id] = row
|
||||
})
|
||||
|
||||
return { rows, flatRows, rowsById }
|
||||
}
|
||||
|
||||
export const AggregationRowFeature: TableFeature<any> = {
|
||||
createTable: <TData>(table: Table<TData>) => {
|
||||
table.getAllAggregationRowIds = () => collectAggregationRowIds(table)
|
||||
|
||||
table.hasAggregationRows = () => collectAggregationRowIds(table).length > 0
|
||||
|
||||
table.getAggregationRowModel = () => buildAggregationRowModel(table)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/* eslint-disable import-x/no-unused-modules */
|
||||
export {}
|
||||
|
||||
import '@tanstack/react-table'
|
||||
|
||||
import { AggregationRowModel, AggregationRowsInput } from './aggregationRows'
|
||||
import { VerticalHeaders } from './verticalHeader'
|
||||
|
||||
declare module '@tanstack/react-table' {
|
||||
interface TableState extends VerticalHeaders {}
|
||||
|
||||
interface ColumnDefBase<TData, TValue = unknown> {
|
||||
/**
|
||||
* Accepts two kinds of inputs:
|
||||
*
|
||||
* 1. An array directly `{ id: String, value: T }[]`
|
||||
* 2. A function `({ table, column }) => {id: String, value:T}[]`
|
||||
*
|
||||
* `id` is used for grouping
|
||||
*
|
||||
* Use same `id` on multiple rows to supply data to the same aggregation row,
|
||||
* or different `id` to create multiple rows.
|
||||
*
|
||||
* Order matters and is based on the first occurence of id.
|
||||
*/
|
||||
aggregationRows?: AggregationRowsInput<TData, TValue>
|
||||
}
|
||||
|
||||
interface Table<TData> {
|
||||
getAggregationRowModel: () => AggregationRowModel<TData>
|
||||
getAllAggregationRowIds: () => string[]
|
||||
hasAggregationRows: () => boolean
|
||||
}
|
||||
|
||||
interface Row {
|
||||
getIsAggregationRow?: () => boolean
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { VerticalHeaderFeature } from './verticalHeader'
|
||||
export { AggregationRowFeature } from './aggregationRows'
|
||||
@@ -0,0 +1,14 @@
|
||||
import { TableFeature } from '@tanstack/react-table'
|
||||
|
||||
export interface VerticalHeaders {
|
||||
useVerticalHeaders: string[]
|
||||
}
|
||||
|
||||
export const VerticalHeaderFeature: TableFeature<unknown> = {
|
||||
getInitialState: (state): VerticalHeaders => {
|
||||
return {
|
||||
useVerticalHeaders: [],
|
||||
...state,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
// 03-07-2025 summer hackathon never forget
|
||||
|
||||
import type { ColumnDef, TableOptions } from '@tanstack/react-table'
|
||||
import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel } from '@tanstack/react-table'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { AggregationRowFeature, VerticalHeaderFeature } from './features'
|
||||
import { OodiTableContainer } from './OodiTable'
|
||||
|
||||
/**
|
||||
@@ -37,6 +36,7 @@ export const OodiTable = <TData,>({
|
||||
}
|
||||
|
||||
const table = useReactTable<TData>({
|
||||
_features: [VerticalHeaderFeature, AggregationRowFeature],
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SxProps } from '@mui/material/styles'
|
||||
import type { SxProps, Theme } from '@mui/material/styles'
|
||||
import type { Column } from '@tanstack/react-table'
|
||||
|
||||
export const getCommonPinningStyles = <TData>(column: Column<TData>): SxProps => {
|
||||
export const getCommonPinningStyles = <TData>(column: Column<TData>): SxProps<Theme> => {
|
||||
const isPinned = column.getIsPinned()
|
||||
return isPinned
|
||||
? {
|
||||
@@ -19,3 +19,11 @@ export const getCommonPinningStyles = <TData>(column: Column<TData>): SxProps =>
|
||||
}
|
||||
: {}
|
||||
}
|
||||
|
||||
export const verticalStyles: SxProps<Theme> = {
|
||||
overflow: 'visible',
|
||||
borderWidth: '1px 1px 1px 0',
|
||||
borderStyle: 'solid',
|
||||
borderColor: 'grey.300',
|
||||
padding: '0.5em',
|
||||
}
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ export const CreditsGainedTab = ({ filteredStudents, programmeGoalTime, programm
|
||||
if (!filteredStudents?.length || !programme) return null
|
||||
|
||||
const admissionTypesAvailable =
|
||||
filteredStudents.length !== filteredStudents.filter(admissionTypeFilter(programme)(null)).length
|
||||
filteredStudents.length !== filteredStudents.filter(admissionTypeFilter(programme, null)).length
|
||||
|
||||
return (
|
||||
<Stack direction="column" sx={{ p: 1 }}>
|
||||
@@ -34,7 +34,7 @@ export const CreditsGainedTab = ({ filteredStudents, programmeGoalTime, programm
|
||||
<Box>
|
||||
<DividerWithText text="By admission type" />
|
||||
{admissionTypes.map(type => {
|
||||
const studentsByAdmissionType = filteredStudents.filter(admissionTypeFilter(programme)(type))
|
||||
const studentsByAdmissionType = filteredStudents.filter(admissionTypeFilter(programme, type))
|
||||
return studentsByAdmissionType.length ? (
|
||||
<CreditsGainedTable
|
||||
filteredStudents={studentsByAdmissionType}
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ import TableHead from '@mui/material/TableHead'
|
||||
import TableRow from '@mui/material/TableRow'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
import dayjs, { type Dayjs } from 'dayjs'
|
||||
import dayjs, { type Dayjs, extend as dayjsExtend } from 'dayjs'
|
||||
import isBetween from 'dayjs/plugin/isBetween'
|
||||
|
||||
import { getStudentTotalCredits } from '@/common'
|
||||
@@ -19,7 +19,7 @@ import { formatDate } from '@/util/timeAndDate'
|
||||
import { FormattedStudent } from '@oodikone/shared/types'
|
||||
import { CollapsibleCreditRow } from './CollapsibleCreditRow'
|
||||
|
||||
dayjs.extend(isBetween)
|
||||
dayjsExtend(isBetween)
|
||||
|
||||
type CreditsGainedTableProps = {
|
||||
filteredStudents: FormattedStudent[]
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ type StatisticsTabProps = {
|
||||
|
||||
export const StatisticsTab = ({ filteredStudents, programme }: StatisticsTabProps) => {
|
||||
const admissionTypesAvailable =
|
||||
filteredStudents.length !== filteredStudents.filter(admissionTypeFilter(programme)(null)).length
|
||||
filteredStudents.length !== filteredStudents.filter(admissionTypeFilter(programme, null)).length
|
||||
|
||||
return (
|
||||
<Stack sx={{ alignItems: 'center', p: '2em' }}>
|
||||
@@ -28,7 +28,7 @@ export const StatisticsTab = ({ filteredStudents, programme }: StatisticsTabProp
|
||||
<DividerWithText text="By admission type" />
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '2em', justifyContent: 'center', width: '100%' }}>
|
||||
{admissionTypes.map(type => {
|
||||
const studentsByAdmissionType = filteredStudents.filter(admissionTypeFilter(programme)(type))
|
||||
const studentsByAdmissionType = filteredStudents.filter(admissionTypeFilter(programme, type))
|
||||
return studentsByAdmissionType.length ? (
|
||||
<StatisticsTable
|
||||
filteredStudents={studentsByAdmissionType}
|
||||
|
||||
@@ -25,7 +25,6 @@ import { useFormat as formatGeneralTab } from '@/components/PopulationStudents/S
|
||||
import { useDebouncedState } from '@/hooks/debouncedState'
|
||||
import { useCurriculumState } from '@/hooks/useCurriculums'
|
||||
import { useGetAuthorizedUserQuery } from '@/redux/auth'
|
||||
import { useGetProgressCriteriaQuery } from '@/redux/progressCriteria'
|
||||
import { PopulationQuery } from '@/types/populationSearch'
|
||||
import { getFullStudyProgrammeRights } from '@/util/access'
|
||||
|
||||
@@ -70,7 +69,6 @@ export const PopulationDetails = ({
|
||||
|
||||
const { isFetching: authLoading, programmeRights, fullAccessToStudentData } = useGetAuthorizedUserQuery()
|
||||
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
|
||||
const { data: criteria } = useGetProgressCriteriaQuery({ programmeCode: programme }, { skip: !programme })
|
||||
|
||||
useEffect(() => setStudentAmountLimit(Math.floor(filteredStudents.length * 0.3)), [filteredStudents.length])
|
||||
|
||||
@@ -146,7 +144,6 @@ export const PopulationDetails = ({
|
||||
{/* @ts-expect-error leave for later :) TODO */}
|
||||
<PopulationStudents
|
||||
combinedProgramme={combinedProgramme}
|
||||
criteria={criteria}
|
||||
curriculum={curriculum}
|
||||
filteredCourses={filteredCourses}
|
||||
filteredStudents={filteredStudents}
|
||||
@@ -221,7 +218,7 @@ export const PopulationDetails = ({
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={transferredSelected !== false}
|
||||
checked={transferredSelected !== 'false'}
|
||||
onChange={() => filterDispatch(transferredToProgrammeFilter.actions.toggle(undefined))}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ export const useColumns = ({ showCombinedProgrammeColumns }): [string[], string[
|
||||
|
||||
const adminColumns = isAdmin ? ['extent', 'updatedAt'] : []
|
||||
|
||||
const excelOnlyColumns = ['email', 'phoneNumber']
|
||||
/* REQUIRED TO ALWAYS BE VISIBLE */
|
||||
const excelRequired = ['studentNumber', 'lastName', 'firstNames', 'email', 'phoneNumber']
|
||||
|
||||
return [
|
||||
[
|
||||
@@ -42,6 +43,6 @@ export const useColumns = ({ showCombinedProgrammeColumns }): [string[], string[
|
||||
...combinedProgrammeColumns,
|
||||
...adminColumns,
|
||||
],
|
||||
excelOnlyColumns,
|
||||
excelRequired,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type EnrollmentDateSelectorProps = {
|
||||
export const EnrollmentDateSelector = ({ year, setYear }: EnrollmentDateSelectorProps) => {
|
||||
const currentYear = new Date().getFullYear()
|
||||
const lowestYear = 2017
|
||||
const options = [...Array(currentYear + 1 - lowestYear).keys()].map(value => lowestYear + value)
|
||||
const options = [...Array(currentYear + 1 - lowestYear).keys()].map(value => currentYear - value)
|
||||
return (
|
||||
<Stack direction="row" spacing={1} sx={{ mt: 0 }}>
|
||||
<IconButton
|
||||
|
||||
@@ -172,7 +172,7 @@ export const PopulationStatistics = () => {
|
||||
|
||||
const initialOptions = {
|
||||
[transferredToProgrammeFilter.key]: {
|
||||
transferred: false,
|
||||
transferred: 'false',
|
||||
},
|
||||
[hopsFilter.key]: {
|
||||
studyStart: students.some(student => !!student?.studyrightStart),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user