mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7531a9def4 | ||
|
|
f5b81d73ac | ||
|
|
4544d1cdec | ||
|
|
072360763f | ||
|
|
639858f874 | ||
|
|
92f3e67293 | ||
|
|
983390040e | ||
|
|
277b97327e | ||
|
|
3dab357396 | ||
|
|
a39648f380 | ||
|
|
ccb1865853 | ||
|
|
81da089798 | ||
|
|
785b7d5d74 | ||
|
|
aca70d5442 | ||
|
|
41d93c9b43 | ||
|
|
835c9e580b | ||
|
|
9c9f317ecb | ||
|
|
3ee6758c72 | ||
|
|
88764f83de | ||
|
|
e8908d28fc | ||
|
|
a6d7da315f | ||
|
|
e72e0bb5ad | ||
|
|
15b0286eda | ||
|
|
1104483cac | ||
|
|
604bcf4998 | ||
|
|
cc839a9e14 | ||
|
|
2a076ef6e8 | ||
|
|
b05e1c22e5 | ||
|
|
b9800c857f | ||
|
|
6d535bf4bb | ||
|
|
714f85d5c0 | ||
|
|
01af4cfd2c | ||
|
|
fe55b770e9 | ||
|
|
43e4e605d6 | ||
|
|
15a54f1e26 | ||
|
|
631b5be00a | ||
|
|
b8d9648b8c | ||
|
|
d52562330b | ||
|
|
f4c9137a05 | ||
|
|
9a1802b6b4 | ||
|
|
fd2d95a832 | ||
|
|
dfb557d4e1 | ||
|
|
22e2ccf562 | ||
|
|
68480a7211 | ||
|
|
bb4edef070 | ||
|
|
6877716468 | ||
|
|
a21e91fbaa | ||
|
|
62e7de5fef | ||
|
|
785b84b14c | ||
|
|
e488327fd4 | ||
|
|
2642e15479 | ||
|
|
547d1c7d35 | ||
|
|
d57ecb2aa9 | ||
|
|
ef76b0f350 | ||
|
|
3ba2bd8d5b | ||
|
|
12976a961b | ||
|
|
01c6159ffa | ||
|
|
a6c1e3891d | ||
|
|
6c4b42c98d | ||
|
|
10941259a2 | ||
|
|
b7563cd4ec | ||
|
|
8d261aae51 | ||
|
|
ccf70ed420 | ||
|
|
20f4bd32f8 | ||
|
|
d62140e97d | ||
|
|
0e510f039f | ||
|
|
893d3847e2 | ||
|
|
0c46fcdd09 | ||
|
|
4397a542c2 | ||
|
|
25a4975265 | ||
|
|
b89967fb17 | ||
|
|
36ce4f1405 | ||
|
|
71d5849360 | ||
|
|
6f335a3555 | ||
|
|
e7eab4741e | ||
|
|
64a67151d6 | ||
|
|
229d155b7c | ||
|
|
42ef2b0245 | ||
|
|
79bfb690b7 | ||
|
|
1b8b71075a | ||
|
|
a77bba0881 | ||
|
|
eda8dcac97 | ||
|
|
b0e0bae727 | ||
|
|
47d5faec81 | ||
|
|
8f90824d8b | ||
|
|
e16f7ddf85 | ||
|
|
0c02d25257 | ||
|
|
68484728d2 | ||
|
|
1d3fbdf869 | ||
|
|
c22b415b85 | ||
|
|
920c1aac5f | ||
|
|
31787e6df0 | ||
|
|
82a4ea9465 | ||
|
|
bb7f965299 | ||
|
|
ff5fac8f0d | ||
|
|
cede895135 | ||
|
|
0ebff9dd39 | ||
|
|
ce94943d54 | ||
|
|
98f388a8a3 | ||
|
|
9b366e2974 | ||
|
|
452fb93b2c | ||
|
|
b9cd6aab7f | ||
|
|
42e76e25e6 | ||
|
|
5ae4f80fab | ||
|
|
d441f3a6b3 | ||
|
|
d22e746e02 | ||
|
|
5b5292d53a | ||
|
|
12f90192c6 | ||
|
|
94d38fecc1 | ||
|
|
6a649b5f00 | ||
|
|
ab391472a6 | ||
|
|
267067e83a | ||
|
|
17f376a681 | ||
|
|
9d83a06377 | ||
|
|
0f5c2e3160 | ||
|
|
364ba0bca3 | ||
|
|
9d5f2a51e3 | ||
|
|
d9260c17f8 | ||
|
|
612cb08689 | ||
|
|
ae90dfa006 | ||
|
|
a4635d8d5b | ||
|
|
2a48dc5a77 | ||
|
|
778807ecd0 | ||
|
|
eca6b643bd | ||
|
|
cde59177ed | ||
|
|
c229e4f030 | ||
|
|
cff0cbef04 | ||
|
|
c0a4e9d8e5 | ||
|
|
3cd2bcefea | ||
|
|
4b40011f6b | ||
|
|
b8c147ceec | ||
|
|
5d169b3321 | ||
|
|
1b86aada0e | ||
|
|
f3b027a609 | ||
|
|
3797fed1a9 | ||
|
|
ec3f36a9e4 | ||
|
|
ed7677d126 | ||
|
|
bcbeeaceb5 | ||
|
|
f80200d481 | ||
|
|
e64e0d04fd | ||
|
|
85432092fa | ||
|
|
b6a39c829e | ||
|
|
0a6edf6638 | ||
|
|
2d44506c55 | ||
|
|
d5dc511b39 | ||
|
|
1e9c2857c5 | ||
|
|
58190f8757 | ||
|
|
caa715677b | ||
|
|
94c34a10eb | ||
|
|
f86c90ff8c | ||
|
|
617379e63f | ||
|
|
cb1b44d4a3 | ||
|
|
2ae9d662dd | ||
|
|
cd73900749 | ||
|
|
60a03b8643 | ||
|
|
35ffc21b91 | ||
|
|
e2886c9e35 | ||
|
|
c50d365dcb | ||
|
|
6843b587cd | ||
|
|
d1d4e3105e | ||
|
|
0fe018bb96 | ||
|
|
d2685b13d7 | ||
|
|
ec2bdcc8ea | ||
|
|
37518f554a | ||
|
|
5b33803d9f | ||
|
|
5dd534e4d9 | ||
|
|
589bcb6128 | ||
|
|
a5af6ebd47 | ||
|
|
08b3d29b91 | ||
|
|
8ee1cac66f | ||
|
|
32a9cdbea6 | ||
|
|
372dbd4769 | ||
|
|
49089975db | ||
|
|
e4ff103cb1 | ||
|
|
ef683f8fe1 | ||
|
|
6e3da7e1af | ||
|
|
c9fe222dd3 | ||
|
|
084be9df1b | ||
|
|
0b86f82eb7 | ||
|
|
993d5e842e | ||
|
|
6ab7b9bbc4 | ||
|
|
0ccb1e79c8 | ||
|
|
24ade5b222 | ||
|
|
ae9fbf182c | ||
|
|
4be53e7707 | ||
|
|
c980560f53 | ||
|
|
b1928a7843 | ||
|
|
2e60df5804 | ||
|
|
2ccd526805 | ||
|
|
92b6b3cb60 | ||
|
|
fbc842cac5 | ||
|
|
cb334e2865 | ||
|
|
24bd750763 | ||
|
|
eb07173f36 | ||
|
|
815d85d8e2 | ||
|
|
a4a3c0f2ca | ||
|
|
20c3632972 | ||
|
|
7af6374e7b | ||
|
|
9accbc299a | ||
|
|
7a03455c8e | ||
|
|
a72c5eeaea | ||
|
|
1f2d381039 | ||
|
|
a31ef9af07 | ||
|
|
fcd855a3e2 | ||
|
|
6a31bcf4d7 | ||
|
|
d3de30bdec | ||
|
|
1ee4fb8bcf | ||
|
|
3b58c13f4a | ||
|
|
9ca46021aa | ||
|
|
87ade36e4b | ||
|
|
32a670a8d1 | ||
|
|
f2478fbe24 | ||
|
|
f81a5ee1a4 | ||
|
|
bf780d09d7 | ||
|
|
3bbee5c0b3 | ||
|
|
880f5997c1 | ||
|
|
44e2bad801 | ||
|
|
18a5eadbcc | ||
|
|
e871b77192 | ||
|
|
4772ca99c2 | ||
|
|
6f6b059d83 | ||
|
|
2cc016e439 | ||
|
|
7670dbd50f | ||
|
|
86a3081a1c |
@@ -9,10 +9,7 @@ runs:
|
||||
node-version: 20
|
||||
- name: 'Install dependencies'
|
||||
shell: bash
|
||||
run: npm ci && cd services/frontend && npm ci && cd ../backend && npm ci
|
||||
- name: 'Create symlink for shared'
|
||||
shell: bash
|
||||
run: ln -s $PWD/services/shared $PWD/services/backend/src/shared && ln -s $PWD/services/shared $PWD/services/frontend/src/shared
|
||||
run: npm ci && npm ci --prefix services/shared && npm ci --prefix services/frontend && npm ci --prefix services/backend
|
||||
- name: 'Run Stylelint for CSS files'
|
||||
shell: bash
|
||||
run: npm run stylelint "services/frontend/src/**/*.css"
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
run: docker create --name frontend quay.io/toska/oodikone-frontend:production
|
||||
|
||||
- name: Copy assets from the frontend container
|
||||
run: docker cp frontend:/opt/app-root/src/build ./build
|
||||
run: docker cp frontend:/opt/app-root/frontend/build ./build
|
||||
|
||||
- name: Create Sentry release
|
||||
uses: getsentry/action-release@v3
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
run: docker create --name backend quay.io/toska/oodikone-backend:production
|
||||
|
||||
- name: Copy assets from the backend container
|
||||
run: docker cp backend:/opt/app-root/src/dist ./dist
|
||||
run: docker cp backend:/opt/app-root/backend/dist ./dist
|
||||
|
||||
- name: Create Sentry release
|
||||
uses: getsentry/action-release@v3
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
environment: production
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './dist'
|
||||
url_prefix: '~/opt/app-root/src/dist/'
|
||||
url_prefix: '~/opt/app-root/backend/dist/'
|
||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
report_success:
|
||||
|
||||
@@ -13,18 +13,18 @@ on:
|
||||
- 'package*.json'
|
||||
|
||||
jobs:
|
||||
lint_backend_and_frontend:
|
||||
name: 'Lint backend and frontend'
|
||||
lint_packages:
|
||||
name: 'Lint packages'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Lint backend and frontend
|
||||
- name: Lint backend, frontend and shared
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
test_end_to_end:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- lint_backend_and_frontend
|
||||
- lint_packages
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -12,12 +12,12 @@ on:
|
||||
|
||||
jobs:
|
||||
# === Static tests / linting ===
|
||||
lint_backend_and_frontend:
|
||||
name: 'Lint backend and frontend'
|
||||
lint_packages:
|
||||
name: 'Lint packages'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Lint backend and frontend
|
||||
- name: Lint backend, frontend and shared
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
# === Build database images ===
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
spec: cypress/e2e/Students.js
|
||||
- name: 'Test completed courses'
|
||||
spec: cypress/e2e/Completed_courses.js
|
||||
- name: 'Test study_programme overview'
|
||||
- name: 'Test study programme overview'
|
||||
spec: cypress/e2e/Study_programme_overview.js
|
||||
- name: 'Test teachers and users'
|
||||
spec: |
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
node-version: 18
|
||||
- name: 'Install dependencies'
|
||||
run: npm ci && cd updater/sis-updater-scheduler && npm ci && cd ../sis-updater-worker && npm ci
|
||||
run: npm ci && npm ci --prefix updater/sis-updater-scheduler && npm ci --prefix updater/sis-updater-worker
|
||||
- name: 'Run linter'
|
||||
run: npm run eslint -- updater
|
||||
|
||||
|
||||
@@ -70,12 +70,9 @@ graph TB
|
||||
end
|
||||
server -->|Read| sisdb[(sis-db)]
|
||||
subgraph Updater
|
||||
updaterscheduler[Scheduler] --> |Push jobs| nats
|
||||
nats(NATS server)
|
||||
updaterworker --> |Fetch jobs| nats
|
||||
updaterscheduler[Scheduler] --> |Push BullMQ jobs| redis(Redis)
|
||||
updaterworker[Worker] -->|Write| sisdb
|
||||
updaterscheduler --> redis(Redis)
|
||||
updaterworker --> redis
|
||||
updaterworker --> |Fetch BullMQ jobs| redis
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -351,8 +351,8 @@ describe('Course Statistics tests', () => {
|
||||
cy.cs('course-population-for-2019-2020').click()
|
||||
cy.contains('Population of course Tietorakenteet ja algoritmit 2019-2020 (open and normal)')
|
||||
cy.contains('Students (33)').click()
|
||||
cy.contains('td', '394776').siblings().eq(2).contains('3')
|
||||
cy.contains('td', '497388').siblings().eq(2).contains('2')
|
||||
cy.contains('td', '394776').siblings().eq(1).contains('3')
|
||||
cy.contains('td', '497388').siblings().eq(1).contains('2')
|
||||
})
|
||||
|
||||
it("In 'Course population' view, student numbers of students that the user isn't allowed to see are hidden", () => {
|
||||
@@ -365,7 +365,7 @@ describe('Course Statistics tests', () => {
|
||||
cy.cs('course-population-for-2019-2020').click()
|
||||
cy.contains('Population of course Tietorakenteet ja algoritmit 2019-2020 (open and normal)')
|
||||
cy.contains('Students (33)').click()
|
||||
cy.get('table tbody td').filter(':contains("hidden")').should('have.length', 9)
|
||||
cy.get('table tbody td').filter(':contains("Hidden")').should('have.length', 9)
|
||||
})
|
||||
|
||||
it('Language distribution is correct', () => {
|
||||
|
||||
@@ -194,7 +194,7 @@ describe('Population Statistics', () => {
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(1)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(6)
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(7)
|
||||
checkFilteringResult(21)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
})
|
||||
@@ -249,7 +249,7 @@ describe('Population Statistics', () => {
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(13)
|
||||
cy.cs('StartYearAtUni-header').click()
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(5)
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(7)
|
||||
checkFilteringResult(8)
|
||||
cy.cs('reset-all-filters').click()
|
||||
checkFilteringResult(defaultAmountOfStudents)
|
||||
|
||||
+10
-4
@@ -43,10 +43,16 @@ describe('Open uni population tests', () => {
|
||||
cy.contains('01.01.2015')
|
||||
cy.contains('End of the search for enrollments:')
|
||||
cy.contains('01.01.2020')
|
||||
cy.url().should(
|
||||
'include',
|
||||
'/openunipopulation?courseCode=TKT10001&courseCode=TKT10002&enddate=01-01-2020&startdate=01-01-2015'
|
||||
)
|
||||
cy.location('pathname').should('eq', '/openunipopulation')
|
||||
cy.location('search').then(search => {
|
||||
const params = new URLSearchParams(search)
|
||||
|
||||
const courseCodes = params.getAll('courseCode')
|
||||
expect(courseCodes).to.include.members(['TKT10001', 'TKT10002'])
|
||||
|
||||
expect(params.get('startdate')).to.eq('01-01-2015')
|
||||
expect(params.get('enddate')).to.eq('01-01-2020')
|
||||
})
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
|
||||
})
|
||||
|
||||
@@ -264,7 +264,7 @@ describe('Population statistics tests', () => {
|
||||
it('New fetch of courses data is done when curriculum is changed', () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.contains('Courses of class').click()
|
||||
cy.intercept('/api/v2/populationstatistics/courses').as('courseData')
|
||||
cy.intercept('/api/v4/populationstatistics/courses').as('courseData')
|
||||
cy.get('[data-cy=curriculum-picker]').click()
|
||||
cy.contains('2020–2023').click({ force: true })
|
||||
|
||||
@@ -287,7 +287,7 @@ describe('Population statistics tests', () => {
|
||||
it('New fetch of courses data is done when filtered students change', () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.contains('Courses of class').click()
|
||||
cy.intercept('/api/v2/populationstatistics/courses').as('courseData')
|
||||
cy.intercept('/api/v4/populationstatistics/courses').as('courseData')
|
||||
cy.wait('@courseData').then(({ response }) => {
|
||||
expect(response.body).to.have.property('allStudents')
|
||||
expect(response.body).to.have.property('coursestatistics')
|
||||
@@ -320,8 +320,10 @@ describe('Population statistics tests', () => {
|
||||
cy.contains('522142')
|
||||
cy.contains('Tilastotiede')
|
||||
cy.contains('Matematiikka')
|
||||
cy.contains('Teoreettisten ja laskennallisten menetelmien maisteriohjelma')
|
||||
cy.contains('Taloustieteen maisteriohjelma')
|
||||
cy.contains('Todistusvalinta')
|
||||
cy.contains('Saksa')
|
||||
cy.contains('Female')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -79,14 +79,17 @@ describe('Study guidance group tests', () => {
|
||||
})
|
||||
|
||||
it('general tab of the students table has the correct columns', () => {
|
||||
const firstRowHeadings = ['Student number', 'Credits', 'Start year at uni', 'Study programmes', 'Tags']
|
||||
const secondRowHeadings = ['All', 'Since 1.1.1970']
|
||||
const colHeaders = [
|
||||
'Student number',
|
||||
'All credits',
|
||||
'Credits since 1.1.1970',
|
||||
'Start year at uni',
|
||||
'Study programmes',
|
||||
'Tags',
|
||||
]
|
||||
cy.cs('Students (2)').click()
|
||||
cy.get('[data-cy="student-table-tabs"] table thead tr').then($tr => {
|
||||
const firstRowTexts = Array.from($tr[0].children).map(elem => elem.innerText.replace('\n', ' '))
|
||||
expect(firstRowTexts).to.deep.equal(firstRowHeadings)
|
||||
const secondRowTexts = Array.from($tr[1].children).map(elem => elem.innerText)
|
||||
expect(secondRowTexts).to.deep.equal(secondRowHeadings)
|
||||
cy.get('[data-cy="student-table-tabs"] table thead tr').within(() => {
|
||||
colHeaders.every(heading => cy.contains(heading))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -129,9 +132,11 @@ describe('Study guidance group tests', () => {
|
||||
})
|
||||
|
||||
it('general tab of the students table has the correct columns', () => {
|
||||
const firstRowHeadings = [
|
||||
const colHeaders = [
|
||||
'Student number',
|
||||
'Credits',
|
||||
'All credits',
|
||||
'Credits in HOPS',
|
||||
'Credits since 1.8.2020',
|
||||
'Start of study right',
|
||||
'Started in programme',
|
||||
'Semesters present',
|
||||
@@ -146,15 +151,9 @@ describe('Study guidance group tests', () => {
|
||||
'Latest attainment date',
|
||||
'Tags',
|
||||
]
|
||||
const secondRowHeadings = ['All', 'HOPS', 'Since 1.8.2020']
|
||||
cy.cs('Students (3)').click()
|
||||
cy.get('[data-cy="student-table-tabs"] table thead tr').then($tr => {
|
||||
const firstRowTexts = Array.from($tr[0].children).map(elem => elem.innerText.replaceAll('\n', ' '))
|
||||
expect(firstRowTexts).to.deep.equal(firstRowHeadings)
|
||||
const secondRowTexts = Array.from($tr[1].children)
|
||||
.filter(elem => elem.style.display !== 'none')
|
||||
.map(elem => elem.innerText)
|
||||
expect(secondRowTexts).to.deep.equal(secondRowHeadings)
|
||||
cy.get('[data-cy="student-table-tabs"] table thead tr').within(() => {
|
||||
colHeaders.every(heading => cy.contains(heading))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -277,11 +277,28 @@ describe('Study programme overview', () => {
|
||||
cy.cs('study-tracks-and-class-statistics-tab').click()
|
||||
})
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Study tracks and class statistics', () => {
|
||||
cy.cs('Section-studyTrackOverview')
|
||||
cy.cs('Section-studyTrackProgress')
|
||||
cy.cs('Section-averageGraduationTimesStudyTracks')
|
||||
describe('Info boxes', () => {
|
||||
it('Study track overview section', () => {
|
||||
cy.cs('study-track-overview-info-box-button').click()
|
||||
cy.cs('study-track-overview-info-box-content').contains('Opiskelijat, joiden')
|
||||
})
|
||||
|
||||
it('Progress of students section', () => {
|
||||
cy.cs('progress-of-students-info-box-button').click()
|
||||
cy.cs('progress-of-students-info-box-content').contains('Kuvaa koulutusohjelmassa')
|
||||
})
|
||||
|
||||
it('Average graduation times section', () => {
|
||||
cy.cs('average-graduation-times-info-box-button').click()
|
||||
cy.cs('average-graduation-times-info-box-content').contains('Yksittäinen palkki')
|
||||
})
|
||||
})
|
||||
|
||||
it('All sections are visible', () => {
|
||||
cy.cs('study-track-selector-section')
|
||||
cy.cs('study-track-overview-section')
|
||||
cy.cs('progress-of-students-section')
|
||||
cy.cs('average-graduation-times-section')
|
||||
})
|
||||
|
||||
it('Students of the study programme are shown correctly', () => {
|
||||
@@ -300,7 +317,8 @@ describe('Study programme overview', () => {
|
||||
cy.checkTableStats(tableContents, 'study-tracks-and-class-statistics')
|
||||
})
|
||||
|
||||
it('Years in the students table can be expanded and study track data will be shown', () => {
|
||||
it.skip('Years in the students table can be expanded and study track data will be shown', () => {
|
||||
// TODO: Fix this test
|
||||
cy.cs('study-tracks-and-class-statistics-data-table').within(() => {
|
||||
cy.get('tbody tr.header-row')
|
||||
.eq(3)
|
||||
@@ -335,45 +353,30 @@ describe('Study programme overview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Links to class statistics page work', () => {
|
||||
cy.cs('study-tracks-and-class-statistics-data-table').within(() => {
|
||||
cy.get('tbody tr.header-row')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td i.level.up.alternate.icon').click()
|
||||
})
|
||||
describe('Population link button works for', () => {
|
||||
it('a single year', () => {
|
||||
cy.cs('2023-population-link-button').first().click()
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2023 - 2024')
|
||||
cy.contains('class size 8 students')
|
||||
})
|
||||
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2022 - 2023')
|
||||
cy.contains('class size 26 students')
|
||||
})
|
||||
|
||||
it('Links to class statistics page with all years combined work', { retries: 2 }, () => {
|
||||
cy.cs('study-tracks-and-class-statistics-data-table').within(() => {
|
||||
cy.get('td.total-row-cell a').click()
|
||||
it('total', () => {
|
||||
cy.cs('total-population-link-button').click()
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('class size 227 students')
|
||||
})
|
||||
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('class size 227 students')
|
||||
})
|
||||
it('Links to class statistics page with study track info included work', () => {
|
||||
cy.cs('show-study-tracks-button').first().click()
|
||||
cy.contains('td', 'Matematiikka (MAT-MAT)').within(() => {
|
||||
cy.get('a').click()
|
||||
})
|
||||
|
||||
it('Links to class statistics page with study track info included work', () => {
|
||||
cy.cs('study-tracks-and-class-statistics-data-table').within(() => {
|
||||
cy.get('tbody tr.header-row')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
cy.get('td i.angle.right.icon').click()
|
||||
})
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2022 - 2023')
|
||||
cy.contains('studytrack MAT-MAT')
|
||||
cy.contains('class size 26 students')
|
||||
cy.contains('3 students out of 26 shown')
|
||||
})
|
||||
|
||||
cy.contains('td', 'Matematiikka, MAT-MAT').within(() => {
|
||||
cy.get('a').click()
|
||||
})
|
||||
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
|
||||
cy.contains('div.sub.header', 'studytrack MAT-MAT')
|
||||
cy.contains('class size 30 students')
|
||||
cy.contains('10 students out of 30 shown')
|
||||
})
|
||||
|
||||
it('Student progress data is shown correctly', () => {
|
||||
@@ -391,27 +394,27 @@ describe('Study programme overview', () => {
|
||||
['Total', 229, 28, 18, 24, 21, 10, 14, 114],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'study-track-progress-basic')
|
||||
cy.checkTableStats(tableContents, 'study-programme-progress')
|
||||
})
|
||||
|
||||
it('Study track overview graphs render', () => {
|
||||
cy.cs('study-track-progress-bar-chart')
|
||||
it('Progress section', () => {
|
||||
cy.cs('programme-progress-bar-chart-section')
|
||||
.should('contain', 'Less than 30 credits')
|
||||
.should('contain', '30–60 credits')
|
||||
.should('contain', 'At least 180 credits')
|
||||
.should('contain', '49.8%') // The percentage for total, at least 180 credits, to check that the graph renders
|
||||
|
||||
cy.cs('study-track-progress-bar-chart').contains('49.8%').trigger('mouseover', { force: true })
|
||||
cy.cs('programme-progress-bar-chart-section').contains('49.8%').trigger('mouseover', { force: true })
|
||||
cy.contains('At least 180 credits: 114')
|
||||
})
|
||||
|
||||
cy.cs('KH50_001-section').within(() => {
|
||||
cy.contains('Start year')
|
||||
it('Average graduation times section', () => {
|
||||
cy.cs('average-graduation-times-section').within(() => {
|
||||
cy.contains('2020 - 2021')
|
||||
cy.get('[aria-label="2020 - 2021, 15. On time."]').trigger('mouseover')
|
||||
cy.contains('On time: 15')
|
||||
cy.contains("Click a bar to view that year's study track level breakdown")
|
||||
cy.get('[aria-label="2019 - 2020, 22. On time."]').click()
|
||||
cy.contains("Click a bar to view that year's study track level breakdown").should('not.exist')
|
||||
cy.contains('Year 2019 - 2020 by start year')
|
||||
cy.get('[aria-label="MAT-MAT, 14. On time."]').trigger('mouseover')
|
||||
cy.contains('Matematiikka')
|
||||
@@ -421,8 +424,7 @@ describe('Study programme overview', () => {
|
||||
|
||||
cy.cs('graduation-time-toggle').click()
|
||||
|
||||
cy.cs('KH50_001-section').within(() => {
|
||||
cy.contains('Start year')
|
||||
cy.cs('average-graduation-times-section').within(() => {
|
||||
cy.contains('2020 - 2021')
|
||||
cy.contains('19 graduated').trigger('mouseover')
|
||||
cy.contains('From class of 2020 - 2021, 19/30 students have graduated')
|
||||
@@ -435,12 +437,16 @@ describe('Study programme overview', () => {
|
||||
|
||||
describe('Study track can be changed', () => {
|
||||
beforeEach(() => {
|
||||
cy.get('.studytrack-selector').contains('All students of the programme, KH50_001').click()
|
||||
cy.get('.studytrack-selector .visible.menu').contains('Matematiikka, MAT-MAT').click()
|
||||
cy.cs('study-track-select').click()
|
||||
cy.contains('All students of the programme')
|
||||
cy.contains('Ekonometria')
|
||||
cy.contains('Tietojenkäsittelyteoria')
|
||||
cy.contains('Tilastotiede')
|
||||
cy.contains('Matematiikka').click()
|
||||
})
|
||||
|
||||
it('Students of the study track are shown correctly', () => {
|
||||
cy.cs('Section-studyTrackOverview').contains('Students of the study track MAT-MAT by starting year')
|
||||
cy.cs('study-track-overview-section').contains('Students of the study track MAT-MAT by starting year')
|
||||
const tableContents = [
|
||||
// [Year, All, Started studying, Present, Absent, Inactive, Graduated, Men, Women, Other/Unknown, Finland, Other]
|
||||
['2022 - 2023', 3, 3, 0, 0, 1, 2, 2, 1, 0, 3, 0],
|
||||
@@ -455,26 +461,17 @@ describe('Study programme overview', () => {
|
||||
})
|
||||
|
||||
it('Links to class statistics page with study track info included work', () => {
|
||||
cy.cs('study-tracks-and-class-statistics-data-table').within(() => {
|
||||
cy.get('tbody tr.regular-row')
|
||||
.eq(2)
|
||||
.within(() => {
|
||||
cy.get('a').click()
|
||||
})
|
||||
})
|
||||
cy.cs('2020-population-link-button').first().click()
|
||||
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
|
||||
cy.contains('div.sub.header', 'studytrack MAT-MAT')
|
||||
cy.contains('studytrack MAT-MAT')
|
||||
cy.contains('class size 30 students')
|
||||
cy.contains('10 students out of 30 shown')
|
||||
})
|
||||
|
||||
it('Info message about missing progress stats is displayed', () => {
|
||||
cy.contains('.divider', 'Progress of students of the study track MAT-MAT by starting year')
|
||||
cy.contains(
|
||||
'.message',
|
||||
'Currently progress data is only available for all students of the study programme. Please select ”All students of the programme” to view the progress data.'
|
||||
)
|
||||
cy.contains('Progress of students of the study track MAT-MAT by starting year')
|
||||
cy.contains('Progress data is currently only available for all students of the study programme.')
|
||||
})
|
||||
|
||||
it.skip('Average graduation times are displayed correctly', () => {
|
||||
@@ -506,184 +503,170 @@ describe('Study programme overview', () => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click()
|
||||
cy.cs('programme-courses-tab').click()
|
||||
|
||||
cy.cs('by-credit-type-section')
|
||||
cy.cs('by-credit-type-tab')
|
||||
cy.cs('by-semester-tab')
|
||||
})
|
||||
|
||||
it('content loads', () => {
|
||||
cy.cs('CoursesYearFilter')
|
||||
cy.cs('CourseTabs')
|
||||
})
|
||||
|
||||
it('time range selection works', () => {
|
||||
cy.get('[data-cy=CoursesSortableTable] tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT11008')
|
||||
cy.get('td').eq(1).contains('Advanced calculus')
|
||||
cy.get('td').eq(2).contains('75')
|
||||
})
|
||||
})
|
||||
cy.cs('fromYear').click().contains('2018').click()
|
||||
cy.cs('toYear').click().contains('2019').click()
|
||||
|
||||
cy.get('[data-cy=CoursesSortableTable] tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT11008')
|
||||
cy.get('td').eq(1).contains('Advanced calculus')
|
||||
cy.get('td').eq(2).contains('15')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("'Calendar year/Academic year' toggle works", () => {
|
||||
cy.cs('calendarAcademicYearToggle').click()
|
||||
cy.cs('fromYear').contains('2017-2018')
|
||||
cy.cs('toYear').contains('2023-2024')
|
||||
cy.get('[data-cy=CoursesSortableTable] tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT11008')
|
||||
cy.get('td').eq(1).contains('Advanced calculus')
|
||||
cy.get('td').eq(2).contains('75')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('contains correct courses in alphabetical order', () => {
|
||||
cy.cs('CoursesSortableTable').within(() => {
|
||||
cy.get('tr').eq(1).contains('Advanced calculus')
|
||||
cy.get('tr').eq(-1).contains('Äidinkielen opinnot')
|
||||
})
|
||||
})
|
||||
|
||||
it('different sorting options work', () => {
|
||||
cy.cs('CoursesSortableTable').within(() => {
|
||||
// Course code
|
||||
cy.get('th').eq(0).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('odgi-zef0')
|
||||
cy.get('td').eq(1).contains('Työ- ja organisaatiopsykologian perusopinnot')
|
||||
})
|
||||
cy.get('th').eq(0).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT-yht')
|
||||
cy.get('td').eq(1).contains('Muut opinnot')
|
||||
})
|
||||
|
||||
// Course name
|
||||
cy.get('th').eq(1).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT20003')
|
||||
cy.get('td').eq(1).contains('Äidinkielen opinnot')
|
||||
})
|
||||
cy.get('th').eq(1).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT11008')
|
||||
cy.get('td').eq(1).contains('Advanced calculus')
|
||||
})
|
||||
|
||||
// Total credits
|
||||
cy.get('th').eq(2).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT110')
|
||||
cy.get('td').eq(1).contains('Matematiikka, perusopinnot')
|
||||
})
|
||||
cy.get('th').eq(2).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT20001')
|
||||
cy.get('td').eq(1).contains('Kypsyysnäyte')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("'Show credits/Show students' toggle works", () => {
|
||||
cy.get('[data-cy=CoursesSortableTable] thead tr').within(() => {
|
||||
cy.get('th').should('have.length', 8)
|
||||
const headers = [
|
||||
'Code',
|
||||
'Name',
|
||||
'Total credits',
|
||||
'Major credits',
|
||||
'Non-major credits',
|
||||
'Non-degree credits',
|
||||
'Transferred credits',
|
||||
'Type',
|
||||
]
|
||||
headers.forEach((header, index) => {
|
||||
cy.get('th').eq(index).contains(header)
|
||||
describe('By credit type tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.get('tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('Course')
|
||||
cy.get('td').eq(1).contains('MAT21001')
|
||||
cy.get('td').eq(2).contains('Lineaarialgebra ja matriisilaskenta II')
|
||||
cy.get('td').eq(3).contains('341')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-cy=CoursesSortableTable] tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT11008')
|
||||
cy.get('td').eq(1).contains('Advanced calculus')
|
||||
cy.get('td').eq(2).contains('75')
|
||||
})
|
||||
it('time range selection works', () => {
|
||||
cy.cs('from-year-select').click()
|
||||
cy.cs('from-year-select-option-2021').click()
|
||||
cy.cs('to-year-select').click()
|
||||
cy.cs('to-year-select-option-2022').click()
|
||||
|
||||
cy.get('tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('Course')
|
||||
cy.get('td').eq(1).contains('MAT21001')
|
||||
cy.get('td').eq(2).contains('Lineaarialgebra ja matriisilaskenta II')
|
||||
cy.get('td').eq(3).contains('121')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
cy.cs('creditsStudentsToggle').click()
|
||||
it('year toggle works', () => {
|
||||
cy.cs('year-toggle').click()
|
||||
|
||||
cy.get('[data-cy=CoursesSortableTable] thead tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('th').should('have.length', 7)
|
||||
const headers = [
|
||||
'Code',
|
||||
'Name',
|
||||
'Total',
|
||||
'Breakdown of total',
|
||||
'Breakdown of passed students',
|
||||
'Not included in total nor passed',
|
||||
'Type',
|
||||
]
|
||||
cy.cs('from-year-select').click()
|
||||
cy.cs('from-year-select-option-2017').click()
|
||||
cy.cs('to-year-select').click()
|
||||
cy.cs('to-year-select-option-2023').click()
|
||||
|
||||
cy.get('tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('Course')
|
||||
cy.get('td').eq(1).contains('MAT21001')
|
||||
cy.get('td').eq(2).contains('Lineaarialgebra ja matriisilaskenta II')
|
||||
cy.get('td').eq(3).contains('311')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: Fix sorting / filters
|
||||
it.skip('different sorting options work', () => {
|
||||
cy.cs('CoursesSortableTable').within(() => {
|
||||
// Course code
|
||||
cy.get('th').eq(0).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('odgi-zef0')
|
||||
cy.get('td').eq(1).contains('Työ- ja organisaatiopsykologian perusopinnot')
|
||||
})
|
||||
cy.get('th').eq(0).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT-yht')
|
||||
cy.get('td').eq(1).contains('Muut opinnot')
|
||||
})
|
||||
|
||||
// Course name
|
||||
cy.get('th').eq(1).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT20003')
|
||||
cy.get('td').eq(1).contains('Äidinkielen opinnot')
|
||||
})
|
||||
cy.get('th').eq(1).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT11008')
|
||||
cy.get('td').eq(1).contains('Advanced calculus')
|
||||
})
|
||||
|
||||
// Total credits
|
||||
cy.get('th').eq(2).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT110')
|
||||
cy.get('td').eq(1).contains('Matematiikka, perusopinnot')
|
||||
})
|
||||
cy.get('th').eq(2).click()
|
||||
cy.get('tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT20001')
|
||||
cy.get('td').eq(1).contains('Kypsyysnäyte')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("'Show credits/Show students' toggle works", () => {
|
||||
cy.get('thead tr').within(() => {
|
||||
cy.get('th').should('have.length', 8)
|
||||
const headers = ['Type', 'Code', 'Name', 'Total', 'Major', 'Non-major', 'Non-degree', 'Transferred']
|
||||
headers.forEach((header, index) => {
|
||||
cy.get('th').eq(index).contains(header)
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-cy=CoursesSortableTable] thead tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('th').should('have.length', 6)
|
||||
const headers = [
|
||||
'Passed',
|
||||
'Not completed',
|
||||
'Major students',
|
||||
'Non-major students',
|
||||
'Non-degree students',
|
||||
'Transferred students',
|
||||
]
|
||||
headers.forEach((header, index) => {
|
||||
cy.get('th').eq(index).contains(header)
|
||||
})
|
||||
})
|
||||
cy.cs('show-credits-students-toggle').click()
|
||||
|
||||
cy.get('[data-cy=CoursesSortableTable] tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(0)
|
||||
cy.get('thead tr')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('MAT11008')
|
||||
cy.get('td').eq(1).contains('Advanced calculus')
|
||||
cy.get('td').eq(2).contains('23')
|
||||
cy.get('th').should('have.length', 10)
|
||||
const headers = [
|
||||
'Type',
|
||||
'Code',
|
||||
'Name',
|
||||
'Total',
|
||||
'Passed',
|
||||
'Not',
|
||||
'Major',
|
||||
'Non-major',
|
||||
'Non-degree',
|
||||
'Transferred',
|
||||
]
|
||||
headers.forEach((header, index) => {
|
||||
cy.get('th').eq(index).contains(header)
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('tbody').within(() => {
|
||||
cy.get('tr')
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get('td').eq(0).contains('Course')
|
||||
cy.get('td').eq(1).contains('MAT21001')
|
||||
cy.get('td').eq(2).contains('Lineaarialgebra ja matriisilaskenta II')
|
||||
cy.get('td').eq(3).contains('341')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('By semester tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.cs('by-semester-tab').click()
|
||||
})
|
||||
|
||||
it('test', () => {
|
||||
// TODO: Implement some real tests for this tab
|
||||
cy.contains('From')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -695,17 +678,32 @@ describe('Study programme overview', () => {
|
||||
cy.cs('degree-courses-tab').click()
|
||||
})
|
||||
|
||||
it('content loads', () => {
|
||||
cy.contains('h3', 'Select curriculum to edit:')
|
||||
it('Curriculum section', () => {
|
||||
cy.cs('curriculum-section')
|
||||
cy.contains('Select curriculum to edit:')
|
||||
cy.cs('curriculum-picker').contains('2023–2026')
|
||||
cy.contains('Change visibility of degree courses and select criteria for academic years')
|
||||
cy.contains('form', 'First year (12 months) last set: 0')
|
||||
cy.get('table').within(() => {
|
||||
cy.contains('Muut opinnot')
|
||||
cy.contains('Matematiikka, perusopinnot')
|
||||
cy.contains('div.green.label', 'visible')
|
||||
cy.contains('button', 'Set hidden')
|
||||
// TODO: Test changing the curriculum
|
||||
})
|
||||
|
||||
describe('Credit criteria section', () => {
|
||||
it('info box', () => {
|
||||
cy.cs('credit-criteria-section')
|
||||
cy.cs('credit-criteria-info-box-button').click()
|
||||
cy.cs('credit-criteria-info-box-content').contains('Here you can change')
|
||||
})
|
||||
|
||||
it.skip('changing the credit criteria works', () => {
|
||||
// TODO: Test using the inputs
|
||||
// TODO: Button status
|
||||
// TODO: Test "previously set to _" text changing when saving
|
||||
})
|
||||
})
|
||||
|
||||
it('Degree course table', () => {
|
||||
cy.cs('degree-course-table')
|
||||
// TODO: Test clicking the arrow buttons
|
||||
// TODO: Test toggling visibility for MODULES and COURSES
|
||||
// TODO: Test changing criterion labels
|
||||
})
|
||||
})
|
||||
|
||||
@@ -822,6 +820,15 @@ describe('Study programme overview', () => {
|
||||
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click()
|
||||
})
|
||||
|
||||
it.skip('year selector', () => {
|
||||
// TODO: Implement this test
|
||||
// Things to test:
|
||||
// - Clicking the button works
|
||||
// - Selecting a study track works
|
||||
// - The button is disabled if only one year is selected
|
||||
// - The slider is not visible if the user does not have correct programme rights
|
||||
})
|
||||
|
||||
it('can access programme and correct tabs are visible', () => {
|
||||
cy.contains('Basic information')
|
||||
cy.contains('Study tracks and class statistics')
|
||||
@@ -843,9 +850,9 @@ describe('Study programme overview', () => {
|
||||
it('can access study tracks', () => {
|
||||
cy.cs('study-tracks-and-class-statistics-tab').click()
|
||||
|
||||
cy.cs('Section-studyTrackOverview')
|
||||
cy.cs('Section-studyTrackProgress')
|
||||
cy.cs('Section-averageGraduationTimesStudyTracks')
|
||||
cy.cs('study-track-overview-section')
|
||||
cy.cs('progress-of-students-section')
|
||||
cy.cs('average-graduation-times-section')
|
||||
})
|
||||
|
||||
it("doesn't see other tabs", () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ const checkProgressBarCharts = () => {
|
||||
}
|
||||
|
||||
const checkProgressTables = () => {
|
||||
progressLevels.forEach(level => cy.cs(`faculty-${level}-progress-table`))
|
||||
progressLevels.forEach(level => cy.cs(`${level}-faculty-progress-table`))
|
||||
}
|
||||
|
||||
const graduationTimesLevels = ['bachelor', 'master', 'doctor'] // ? Bachelor + master seems to be missing in test data
|
||||
@@ -53,7 +53,7 @@ describe('University view', () => {
|
||||
|
||||
it('years in the tables can be clicked to show faculty level breakdown', () => {
|
||||
cy.cs('study-right-toggle').click()
|
||||
cy.cs('faculty-bachelors-progress-table-show-button3').click()
|
||||
cy.cs('bachelors-faculty-progress-table-show-button3').click()
|
||||
cy.contains('29.5%').trigger('mouseover', { force: true })
|
||||
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
|
||||
cy.contains('H50')
|
||||
|
||||
@@ -49,9 +49,10 @@ services:
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./environments/ha:/opt/app-root/src/src/environment
|
||||
- ./services/backend/src:/opt/app-root/src/src
|
||||
- ./services/shared/:/opt/app-root/src/src/shared
|
||||
- backend-nodemod:/opt/app-root/backend/node_modules
|
||||
- ./environments/testu:/opt/app-root/backend/src/environment
|
||||
- ./services/backend/:/opt/app-root/backend
|
||||
- ./services/shared/:/opt/app-root/shared
|
||||
|
||||
frontend:
|
||||
<<: *defaults
|
||||
@@ -66,8 +67,9 @@ services:
|
||||
REACT_APP_LANGUAGE_CENTER_VIEW_ENABLED: 'false'
|
||||
REACT_APP_SIS_URL: 'https://sis-qa.funidata.fi'
|
||||
volumes:
|
||||
- ./services/frontend/src/:/opt/app-root/src/src
|
||||
- ./services/shared/:/opt/app-root/src/src/shared
|
||||
- frontend-nodemod:/opt/app-root/frontend/node_modules
|
||||
- ./services/frontend/:/opt/app-root/frontend
|
||||
- ./services/shared/:/opt/app-root/shared
|
||||
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
@@ -171,6 +173,8 @@ services:
|
||||
- 12347:5432
|
||||
|
||||
volumes:
|
||||
backend-nodemod:
|
||||
frontend-nodemod:
|
||||
kone-db-data:
|
||||
redis-data:
|
||||
sis-db-data:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
services:
|
||||
frontend:
|
||||
volumes:
|
||||
- frontend-nodemod:/opt/app-root/src/node_modules
|
||||
- ./services/frontend/instrumented:/opt/app-root/src/src
|
||||
- frontend-nodemod:/opt/app-root/frontend/node_modules
|
||||
- ./services/frontend/instrumented:/opt/app-root/frontend/src
|
||||
|
||||
+6
-6
@@ -44,9 +44,9 @@ services:
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- backend-nodemod:/opt/app-root/src/node_modules
|
||||
- ./services/backend/:/opt/app-root/src
|
||||
- ./services/shared/:/opt/app-root/src/src/shared
|
||||
- backend-nodemod:/opt/app-root/backend/node_modules
|
||||
- ./services/backend/:/opt/app-root/backend
|
||||
- ./services/shared/:/opt/app-root/shared
|
||||
|
||||
frontend:
|
||||
<<: *defaults
|
||||
@@ -59,9 +59,9 @@ services:
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- frontend-nodemod:/opt/app-root/src/node_modules
|
||||
- ./services/frontend/:/opt/app-root/src
|
||||
- ./services/shared/:/opt/app-root/src/src/shared
|
||||
- frontend-nodemod:/opt/app-root/frontend/node_modules
|
||||
- ./services/frontend/:/opt/app-root/frontend
|
||||
- ./services/shared/:/opt/app-root/shared
|
||||
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
|
||||
+2
-1
@@ -83,6 +83,7 @@ export default [
|
||||
'object-shorthand': ['error', 'always'],
|
||||
'prefer-const': 'error',
|
||||
'prefer-destructuring': ['error', { VariableDeclarator: { object: true } }],
|
||||
"no-restricted-imports": ['error', { 'patterns': [{ 'regex': '^@mui/[^/]+$' }] }],
|
||||
quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: false }],
|
||||
'react/display-name': 'off', // TODO: Delete this override
|
||||
'react/function-component-definition': [
|
||||
@@ -163,7 +164,7 @@ export default [
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
'import/internal-regex': '^@/shared/',
|
||||
'import/internal-regex': '^@oodikone/shared/',
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: [
|
||||
|
||||
Generated
+817
-454
File diff suppressed because it is too large
Load Diff
+20
-17
@@ -23,10 +23,13 @@
|
||||
"testupdater": "docker-compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker && SCHEDULE_IMMEDIATE=weekly EXIT_AFTER_IMMEDIATES=yes docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start",
|
||||
"flushredis": "docker exec -it redis redis-cli flushall",
|
||||
"prepare": "husky",
|
||||
"rapodiff": "docker exec -t oodikone_backend_1 /opt/app-root/src/node_modules/.bin/tsx /opt/app-root/src/src/rapodiff/rapodiff.js",
|
||||
"install:local": "npm i && cd services/frontend && npm i && cd ../backend && npm i && cd ../../updater/sis-updater-scheduler && npm i && cd ../sis-updater-worker && npm i && cd ../..",
|
||||
"rapodiff": "docker exec -t oodikone_backend_1 /opt/app-root/backend/node_modules/.bin/tsx /opt/app-root/backend/src/rapodiff/rapodiff.js",
|
||||
"install:local": "npm i && npm i --prefix services/shared && npm i --prefix services/frontend && npm i --prefix services/backend && npm i --prefix updater/sis-updater-scheduler && npm i --prefix updater/sis-updater-worker",
|
||||
"ci:local": "npm ci && npm ci --prefix services/shared && npm ci --prefix services/frontend && npm ci --prefix services/backend && npm ci --prefix updater/sis-updater-scheduler && npm ci --prefix updater/sis-updater-worker",
|
||||
"install:docker": "docker exec frontend npm i && docker exec oodikone_backend_1 npm i && docker exec sis-updater-scheduler npm i && docker exec oodikone_sis-updater-worker_1 npm i",
|
||||
"install:both": "npm run install:local && npm run install:docker"
|
||||
"ci:docker": "docker exec frontend npm ci && docker exec oodikone_backend_1 npm ci && docker exec sis-updater-scheduler npm ci && docker exec oodikone_sis-updater-worker_1 npm ci",
|
||||
"install:both": "npm run install:local && npm run install:docker",
|
||||
"ci:both": "npm run ci:local && npm run ci:docker"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -38,27 +41,27 @@
|
||||
},
|
||||
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
|
||||
"devDependencies": {
|
||||
"@cypress/code-coverage": "^3.13.12",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@cypress/code-coverage": "^3.14.4",
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"cypress": "^14.1.0",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-config-prettier": "^10.0.2",
|
||||
"eslint-import-resolver-typescript": "^3.8.3",
|
||||
"eslint-plugin-cypress": "^4.1.0",
|
||||
"cypress": "^14.4.1",
|
||||
"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-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-prettier": "^5.4.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"globals": "^16.0.0",
|
||||
"globals": "^16.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.4.3",
|
||||
"lint-staged": "^16.1.0",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"nyc": "^17.1.0",
|
||||
"prettier": "3.5.3",
|
||||
"stylelint": "^16.15.0",
|
||||
"stylelint-config-standard": "^37.0.0",
|
||||
"typescript-eslint": "^8.26.0"
|
||||
"stylelint": "^16.20.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"typescript-eslint": "^8.34.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -225,21 +225,6 @@ set_up_oodikone_from_scratch() {
|
||||
infomsg "Building images"
|
||||
"$PROJECT_ROOT"/run.sh both anon build
|
||||
|
||||
TARGET_PATH="$PROJECT_ROOT/services/shared"
|
||||
LINK_PATHS=(
|
||||
"$PROJECT_ROOT/services/backend/src/shared"
|
||||
"$PROJECT_ROOT/services/frontend/src/shared"
|
||||
)
|
||||
|
||||
for SYMLINK_PATH in "${LINK_PATHS[@]}"; do
|
||||
infomsg "Linking $TARGET_PATH to $SYMLINK_PATH"
|
||||
if [ ! -L "$SYMLINK_PATH" ]; then
|
||||
ln -s "$TARGET_PATH" "$SYMLINK_PATH"
|
||||
else
|
||||
infomsg "Symbolic link $SYMLINK_PATH already exists. Skipping creation."
|
||||
fi
|
||||
done
|
||||
|
||||
successmsg "Setup ready, Oodikone can be started! See README for more info."
|
||||
}
|
||||
|
||||
|
||||
+15
-13
@@ -2,8 +2,6 @@ FROM registry.access.redhat.com/ubi9/nodejs-22-minimal AS build
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NODE_OPTIONS=--max-old-space-size=4096
|
||||
|
||||
@@ -19,23 +17,22 @@ ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
|
||||
COPY ./backend/package*.json ./
|
||||
RUN npm ci --include=dev
|
||||
COPY ./shared ./src/shared
|
||||
WORKDIR /opt/app-root/shared
|
||||
COPY ./shared .
|
||||
|
||||
WORKDIR /opt/app-root/backend
|
||||
COPY ./backend .
|
||||
|
||||
RUN npm run build
|
||||
WORKDIR /opt/app-root
|
||||
|
||||
RUN npm ci --include=dev --prefix shared && npm ci --include=dev --prefix backend
|
||||
RUN npm run build --prefix shared && npm run build --prefix backend
|
||||
|
||||
|
||||
FROM registry.access.redhat.com/ubi9/nodejs-22-minimal
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NODE_OPTIONS=--max-old-space-size=4096
|
||||
|
||||
@@ -51,9 +48,14 @@ ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
|
||||
COPY --from=build /opt/app-root/src/package*.json ./
|
||||
COPY --from=build /opt/app-root/src/dist ./dist
|
||||
WORKDIR /opt/app-root/shared
|
||||
COPY --from=build /opt/app-root/shared/dist .
|
||||
|
||||
RUN npm ci
|
||||
WORKDIR /opt/app-root/backend
|
||||
COPY --from=build /opt/app-root/backend/package*.json .
|
||||
COPY --from=build /opt/app-root/backend/dist ./dist
|
||||
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["node", "dist/index.js"]
|
||||
|
||||
@@ -2,15 +2,12 @@ FROM node:22-alpine
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENV NODE_ENV=development
|
||||
ENV NODE_OPTIONS=--max-old-space-size=4096
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci
|
||||
WORKDIR /opt/app-root/backend
|
||||
COPY . .
|
||||
RUN npm ci
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["node_modules/.bin/tsx", "watch", "--clear-screen=false", "index.js"]
|
||||
|
||||
Generated
+740
-428
File diff suppressed because it is too large
Load Diff
@@ -5,40 +5,42 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "^8.55.0",
|
||||
"@sentry/profiling-node": "^8.55.0",
|
||||
"axios": "^1.8.1",
|
||||
"bullmq": "^5.41.7",
|
||||
"@oodikone/shared": "file:../shared",
|
||||
"@sentry/node": "^9.28.1",
|
||||
"@sentry/profiling-node": "^9.28.1",
|
||||
"axios": "^1.9.0",
|
||||
"bullmq": "^5.53.2",
|
||||
"compression": "^1.8.0",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.1.0",
|
||||
"cron": "^4.3.1",
|
||||
"express": "^4.21.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^11.0.2",
|
||||
"lru-cache": "^11.1.0",
|
||||
"moment": "^2.30.1",
|
||||
"morgan": "^1.10.0",
|
||||
"papaparse": "^5.5.2",
|
||||
"pg": "^8.13.3",
|
||||
"redis": "^4.7.0",
|
||||
"papaparse": "^5.5.3",
|
||||
"pg": "^8.16.0",
|
||||
"redis": "^4.7.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sequelize": "^6.37.6",
|
||||
"sequelize": "^6.37.7",
|
||||
"sequelize-typescript": "^2.1.6",
|
||||
"umzug": "^3.8.2",
|
||||
"unfuck-utf8-headers-middleware": "^1.0.1",
|
||||
"winston": "^3.17.0",
|
||||
"winston-gelf-transporter": "^1.0.2"
|
||||
"winston-gelf-transporter": "^1.0.2",
|
||||
"winston-loki": "^6.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/node": "^22.13.9",
|
||||
"@types/validator": "^13.12.2",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2"
|
||||
"@types/compression": "^1.8.1",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^4.17.23",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"@types/morgan": "^1.9.10",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/validator": "^13.15.1",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"overrides": {
|
||||
"winston-gelf-transporter": {
|
||||
|
||||
@@ -4,36 +4,36 @@ import { SequelizeStorage, Umzug } from 'umzug'
|
||||
|
||||
import conf from '../config'
|
||||
import {
|
||||
Course,
|
||||
CourseProvider,
|
||||
CourseType,
|
||||
Credit,
|
||||
CreditTeacher,
|
||||
CreditType,
|
||||
CurriculumPeriod,
|
||||
Enrollment,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
ProgrammeModuleChild,
|
||||
Semester,
|
||||
SISStudyRight,
|
||||
SISStudyRightElement,
|
||||
Student,
|
||||
Studyplan,
|
||||
StudyrightExtent,
|
||||
Teacher,
|
||||
CourseModel,
|
||||
CourseProviderModel,
|
||||
CourseTypeModel,
|
||||
CreditModel,
|
||||
CreditTeacherModel,
|
||||
CreditTypeModel,
|
||||
CurriculumPeriodModel,
|
||||
EnrollmentModel,
|
||||
OrganizationModel,
|
||||
ProgrammeModuleModel,
|
||||
ProgrammeModuleChildModel,
|
||||
SemesterModel,
|
||||
SISStudyRightModel,
|
||||
SISStudyRightElementModel,
|
||||
StudentModel,
|
||||
StudyplanModel,
|
||||
StudyrightExtentModel,
|
||||
TeacherModel,
|
||||
} from '../models'
|
||||
import {
|
||||
CustomPopulationSearch,
|
||||
ExcludedCourse,
|
||||
OpenUniPopulationSearch,
|
||||
ProgressCriteria,
|
||||
StudyGuidanceGroupTag,
|
||||
StudyProgrammePin,
|
||||
Tag,
|
||||
TagStudent,
|
||||
CustomPopulationSearchModel,
|
||||
ExcludedCourseModel,
|
||||
OpenUniPopulationSearchModel,
|
||||
ProgressCriteriaModel,
|
||||
StudyGuidanceGroupTagModel,
|
||||
StudyProgrammePinModel,
|
||||
TagModel,
|
||||
TagStudentModel,
|
||||
} from '../models/kone'
|
||||
import { User } from '../models/user'
|
||||
import { UserModel } from '../models/user'
|
||||
|
||||
import logger from '../util/logger'
|
||||
|
||||
@@ -60,24 +60,24 @@ class DbConnection extends EventEmitter {
|
||||
logging: false,
|
||||
password: conf.SIS_PASSWORD,
|
||||
models: [
|
||||
Course,
|
||||
CourseProvider,
|
||||
CourseType,
|
||||
Credit,
|
||||
CreditTeacher,
|
||||
CreditType,
|
||||
CurriculumPeriod,
|
||||
Enrollment,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
ProgrammeModuleChild,
|
||||
Semester,
|
||||
SISStudyRight,
|
||||
SISStudyRightElement,
|
||||
Student,
|
||||
Studyplan,
|
||||
StudyrightExtent,
|
||||
Teacher,
|
||||
CourseModel,
|
||||
CourseProviderModel,
|
||||
CourseTypeModel,
|
||||
CreditModel,
|
||||
CreditTeacherModel,
|
||||
CreditTypeModel,
|
||||
CurriculumPeriodModel,
|
||||
EnrollmentModel,
|
||||
OrganizationModel,
|
||||
ProgrammeModuleModel,
|
||||
ProgrammeModuleChildModel,
|
||||
SemesterModel,
|
||||
SISStudyRightModel,
|
||||
SISStudyRightElementModel,
|
||||
StudentModel,
|
||||
StudyplanModel,
|
||||
StudyrightExtentModel,
|
||||
TeacherModel,
|
||||
],
|
||||
})
|
||||
}
|
||||
@@ -107,14 +107,14 @@ const sequelizeKone = new Sequelize(conf.DB_URL_KONE!, {
|
||||
logging: false,
|
||||
password: conf.KONE_PASSWORD,
|
||||
models: [
|
||||
CustomPopulationSearch,
|
||||
ExcludedCourse,
|
||||
OpenUniPopulationSearch,
|
||||
ProgressCriteria,
|
||||
StudyGuidanceGroupTag,
|
||||
StudyProgrammePin,
|
||||
Tag,
|
||||
TagStudent,
|
||||
CustomPopulationSearchModel,
|
||||
ExcludedCourseModel,
|
||||
OpenUniPopulationSearchModel,
|
||||
ProgressCriteriaModel,
|
||||
StudyGuidanceGroupTagModel,
|
||||
StudyProgrammePinModel,
|
||||
TagModel,
|
||||
TagStudentModel,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -123,7 +123,7 @@ void sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
|
||||
const sequelizeUser = new Sequelize(conf.DB_URL_USER!, {
|
||||
logging: false,
|
||||
password: conf.USER_PASSWORD,
|
||||
models: [User],
|
||||
models: [UserModel],
|
||||
})
|
||||
|
||||
const initializeDatabaseConnection = async () => {
|
||||
|
||||
@@ -65,7 +65,7 @@ const refreshProgrammesAndFaculties = async () => {
|
||||
export const refreshProgramme = async (code: string) => {
|
||||
await updateBasicView(code, '')
|
||||
await updateStudyTrackView(code, '')
|
||||
const combinedProgramme = combinedStudyProgrammes[code] || ''
|
||||
const combinedProgramme = combinedStudyProgrammes[code] ?? ''
|
||||
await updateBasicView(code, combinedProgramme)
|
||||
await updateStudyTrackView(code, combinedProgramme)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
|
||||
import { Role } from '../shared/types'
|
||||
import { Role } from '@oodikone/shared/types'
|
||||
|
||||
export const roles =
|
||||
(requiredRoles: Role[], requiredIamGroups?: string[]) => (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
@@ -12,29 +11,45 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { ExtentCode, SemesterEnrollment } from '../types'
|
||||
import { Credit, Enrollment, Organization, SISStudyRightElement, Student, Studyplan, StudyrightExtent } from '.'
|
||||
import type {
|
||||
Credit,
|
||||
Enrollment,
|
||||
Organization,
|
||||
SISStudyRight,
|
||||
SISStudyRightElement,
|
||||
Student,
|
||||
Studyplan,
|
||||
} from '@oodikone/shared/models'
|
||||
import { ExtentCode, SemesterEnrollment } from '@oodikone/shared/types'
|
||||
|
||||
import { CreditModel } from './credit'
|
||||
import { EnrollmentModel } from './enrollment'
|
||||
import { OrganizationModel } from './organization'
|
||||
import { SISStudyRightElementModel } from './SISStudyRightElement'
|
||||
import { StudentModel } from './student'
|
||||
import { StudyplanModel } from './studyplan'
|
||||
import { StudyrightExtentModel } from './studyrightExtent'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
tableName: 'sis_study_rights',
|
||||
})
|
||||
export class SISStudyRight extends Model<InferAttributes<SISStudyRight>> {
|
||||
export class SISStudyRightModel extends Model<SISStudyRight> implements SISStudyRight {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
|
||||
@ForeignKey(() => Organization)
|
||||
@ForeignKey(() => OrganizationModel)
|
||||
@Column(DataType.STRING)
|
||||
facultyCode!: string
|
||||
|
||||
@BelongsTo(() => Organization, { foreignKey: 'facultyCode', targetKey: 'code' })
|
||||
@BelongsTo(() => OrganizationModel, { foreignKey: 'facultyCode', targetKey: 'code' })
|
||||
organization!: Organization
|
||||
|
||||
@HasMany(() => Studyplan)
|
||||
@HasMany(() => StudyplanModel)
|
||||
studyPlans!: Studyplan[]
|
||||
|
||||
@HasMany(() => SISStudyRightElement, { foreignKey: 'studyRightId' })
|
||||
@HasMany(() => SISStudyRightElementModel, { foreignKey: 'studyRightId' })
|
||||
studyRightElements!: SISStudyRightElement[]
|
||||
|
||||
@Column(DataType.DATE)
|
||||
@@ -52,10 +67,10 @@ export class SISStudyRight extends Model<InferAttributes<SISStudyRight>> {
|
||||
@Column(DataType.STRING)
|
||||
studentNumber!: string
|
||||
|
||||
@BelongsTo(() => Student, { foreignKey: 'studentNumber', targetKey: 'studentnumber' })
|
||||
@BelongsTo(() => StudentModel, { foreignKey: 'studentNumber', targetKey: 'studentnumber' })
|
||||
student!: Student
|
||||
|
||||
@ForeignKey(() => StudyrightExtent)
|
||||
@ForeignKey(() => StudyrightExtentModel)
|
||||
@Column(DataType.INTEGER)
|
||||
extentCode!: ExtentCode
|
||||
|
||||
@@ -65,12 +80,18 @@ export class SISStudyRight extends Model<InferAttributes<SISStudyRight>> {
|
||||
@Column(DataType.JSONB)
|
||||
semesterEnrollments!: SemesterEnrollment[] | null
|
||||
|
||||
@HasMany(() => Credit, { foreignKey: 'studyright_id', sourceKey: 'id' })
|
||||
@HasMany(() => CreditModel, { foreignKey: 'studyright_id', sourceKey: 'id' })
|
||||
credits!: Credit[]
|
||||
|
||||
@HasMany(() => Enrollment, { foreignKey: 'studyright_id', sourceKey: 'id' })
|
||||
@HasMany(() => EnrollmentModel, { foreignKey: 'studyright_id', sourceKey: 'id' })
|
||||
enrollments!: Enrollment[]
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
tvex!: boolean
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
expirationRuleUrns!: string[]
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
@@ -11,15 +10,17 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../shared/types'
|
||||
import { DegreeProgrammeType, Phase, StudyTrack } from '../types'
|
||||
import { SISStudyRight } from './SISStudyRight'
|
||||
import type { SISStudyRight, SISStudyRightElement } from '@oodikone/shared/models'
|
||||
import type { Name, StudyTrack } from '@oodikone/shared/types'
|
||||
import { DegreeProgrammeType, Phase } from '@oodikone/shared/types'
|
||||
|
||||
import { SISStudyRightModel } from './SISStudyRight'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
tableName: 'sis_study_right_elements',
|
||||
})
|
||||
export class SISStudyRightElement extends Model<InferAttributes<SISStudyRightElement>> {
|
||||
export class SISStudyRightElementModel extends Model<SISStudyRightElement> implements SISStudyRightElement {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
@@ -36,11 +37,11 @@ export class SISStudyRightElement extends Model<InferAttributes<SISStudyRightEle
|
||||
@Column(DataType.INTEGER)
|
||||
phase!: Phase
|
||||
|
||||
@ForeignKey(() => SISStudyRight)
|
||||
@ForeignKey(() => SISStudyRightModel)
|
||||
@Column(DataType.STRING)
|
||||
studyRightId!: string
|
||||
|
||||
@BelongsTo(() => SISStudyRight, { foreignKey: 'studyRightId' })
|
||||
@BelongsTo(() => SISStudyRightModel, { foreignKey: 'studyRightId' })
|
||||
studyRight!: SISStudyRight
|
||||
|
||||
@Column(DataType.STRING)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
BelongsToMany,
|
||||
@@ -13,41 +12,43 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../shared/types'
|
||||
import { CourseProvider } from './courseProvider'
|
||||
import { CourseType } from './courseType'
|
||||
import { Credit } from './credit'
|
||||
import { Enrollment } from './enrollment'
|
||||
import { Organization } from './organization'
|
||||
import type { Course, CourseType, Credit, Enrollment, Organization } from '@oodikone/shared/models'
|
||||
import type { Name } from '@oodikone/shared/types'
|
||||
|
||||
import { CourseProviderModel } from './courseProvider'
|
||||
import { CourseTypeModel } from './courseType'
|
||||
import { CreditModel } from './credit'
|
||||
import { EnrollmentModel } from './enrollment'
|
||||
import { OrganizationModel } from './organization'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'course',
|
||||
tableName: 'course',
|
||||
})
|
||||
export class Course extends Model<InferAttributes<Course>> {
|
||||
export class CourseModel extends Model<Course> implements Course {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
|
||||
@ForeignKey(() => Course)
|
||||
@ForeignKey(() => CourseModel)
|
||||
@Column(DataType.STRING)
|
||||
code!: string
|
||||
|
||||
@ForeignKey(() => CourseType)
|
||||
@ForeignKey(() => CourseTypeModel)
|
||||
@Column(DataType.STRING)
|
||||
coursetypecode!: string
|
||||
|
||||
@BelongsTo(() => CourseType, { foreignKey: 'coursetypecode', targetKey: 'coursetypecode' })
|
||||
@BelongsTo(() => CourseTypeModel, { foreignKey: 'coursetypecode', targetKey: 'coursetypecode' })
|
||||
courseType!: CourseType
|
||||
|
||||
@HasMany(() => Credit, { foreignKey: 'course_id', sourceKey: 'id' })
|
||||
@HasMany(() => CreditModel, { foreignKey: 'course_id', sourceKey: 'id' })
|
||||
credits!: Credit[]
|
||||
|
||||
@HasMany(() => Enrollment, { foreignKey: 'course_id' })
|
||||
@HasMany(() => EnrollmentModel, { foreignKey: 'course_id' })
|
||||
enrollments!: Enrollment[]
|
||||
|
||||
@BelongsToMany(() => Organization, () => CourseProvider, 'coursecode')
|
||||
@BelongsToMany(() => OrganizationModel, () => CourseProviderModel, 'coursecode')
|
||||
organizations!: Organization[]
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
@@ -63,17 +64,11 @@ export class Course extends Model<InferAttributes<Course>> {
|
||||
min_attainment_date!: Date
|
||||
|
||||
@CreatedAt
|
||||
@Column({
|
||||
field: 'created_at',
|
||||
type: DataType.DATE,
|
||||
})
|
||||
@Column({ field: 'created_at', type: DataType.DATE })
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column({
|
||||
field: 'updated_at',
|
||||
type: DataType.DATE,
|
||||
})
|
||||
@Column({ field: 'updated_at', type: DataType.DATE })
|
||||
updatedAt!: Date
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
@@ -82,9 +77,6 @@ export class Course extends Model<InferAttributes<Course>> {
|
||||
@Column(DataType.STRING)
|
||||
course_unit_type!: string
|
||||
|
||||
@Column({
|
||||
field: 'main_course_code',
|
||||
type: DataType.STRING,
|
||||
})
|
||||
@Column({ field: 'main_course_code', type: DataType.STRING })
|
||||
mainCourseCode!: string
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Course } from './course'
|
||||
import { Organization } from './organization'
|
||||
import type { CourseProvider } from '@oodikone/shared/models'
|
||||
|
||||
import { CourseModel } from './course'
|
||||
import { OrganizationModel } from './organization'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'course_provider',
|
||||
tableName: 'course_providers',
|
||||
})
|
||||
export class CourseProvider extends Model<InferAttributes<CourseProvider>> {
|
||||
@ForeignKey(() => Course)
|
||||
export class CourseProviderModel extends Model<CourseProvider> implements CourseProvider {
|
||||
@ForeignKey(() => CourseModel)
|
||||
@Column(DataType.STRING)
|
||||
coursecode!: string
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
shares!: Array<{ share: number; startDate?: string; endDate?: string }> | null
|
||||
|
||||
@ForeignKey(() => Organization)
|
||||
@ForeignKey(() => OrganizationModel)
|
||||
@Column(DataType.STRING)
|
||||
organizationcode!: string
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../shared/types'
|
||||
import { Course } from './course'
|
||||
import type { Course, CourseType } from '@oodikone/shared/models'
|
||||
|
||||
import { Name } from '@oodikone/shared/types'
|
||||
import { CourseModel } from './course'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'course_type',
|
||||
tableName: 'course_types',
|
||||
})
|
||||
export class CourseType extends Model<InferAttributes<CourseType>> {
|
||||
export class CourseTypeModel extends Model<CourseType> implements CourseType {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
coursetypecode!: string
|
||||
|
||||
@HasMany(() => Course)
|
||||
@HasMany(() => CourseModel)
|
||||
courses!: Course[]
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
BelongsToMany,
|
||||
@@ -12,15 +11,23 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { CreditTypeCode } from '../types'
|
||||
import { Course, CreditTeacher, CreditType, Semester, SISStudyRight, Student, Teacher } from '.'
|
||||
import type { Credit, Course, CreditType, Semester, SISStudyRight, Student, Teacher } from '@oodikone/shared/models'
|
||||
import { CreditTypeCode } from '@oodikone/shared/types'
|
||||
|
||||
import { CourseModel } from './course'
|
||||
import { CreditTeacherModel } from './creditTeacher'
|
||||
import { CreditTypeModel } from './creditType'
|
||||
import { SemesterModel } from './semester'
|
||||
import { SISStudyRightModel } from './SISStudyRight'
|
||||
import { StudentModel } from './student'
|
||||
import { TeacherModel } from './teacher'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'credit',
|
||||
tableName: 'credit',
|
||||
})
|
||||
export class Credit extends Model<InferAttributes<Credit>> {
|
||||
export class CreditModel extends Model<Credit> implements Credit {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
@@ -28,24 +35,24 @@ export class Credit extends Model<InferAttributes<Credit>> {
|
||||
@Column(DataType.STRING)
|
||||
grade!: string
|
||||
|
||||
@ForeignKey(() => Student)
|
||||
@ForeignKey(() => StudentModel)
|
||||
@Column(DataType.STRING)
|
||||
student_studentnumber!: string
|
||||
|
||||
@BelongsTo(() => Student, { foreignKey: 'student_studentnumber', targetKey: 'studentnumber' })
|
||||
@BelongsTo(() => StudentModel, { foreignKey: 'student_studentnumber', targetKey: 'studentnumber' })
|
||||
student!: Student
|
||||
|
||||
@BelongsTo(() => Semester, { foreignKey: 'semester_composite' })
|
||||
@BelongsTo(() => SemesterModel, { foreignKey: 'semester_composite' })
|
||||
semester!: Semester
|
||||
|
||||
@Column(DataType.DOUBLE)
|
||||
credits!: number
|
||||
|
||||
@ForeignKey(() => CreditType)
|
||||
@ForeignKey(() => CreditTypeModel)
|
||||
@Column(DataType.INTEGER)
|
||||
credittypecode!: CreditTypeCode
|
||||
|
||||
@BelongsTo(() => CreditType, { foreignKey: 'credittypecode', targetKey: 'credittypecode' })
|
||||
@BelongsTo(() => CreditTypeModel, { foreignKey: 'credittypecode', targetKey: 'credittypecode' })
|
||||
creditType!: CreditType
|
||||
|
||||
@Column(DataType.DATE)
|
||||
@@ -54,16 +61,16 @@ export class Credit extends Model<InferAttributes<Credit>> {
|
||||
@Column(DataType.DATE)
|
||||
attainment_date!: Date
|
||||
|
||||
@BelongsToMany(() => Teacher, () => CreditTeacher, 'credit_id')
|
||||
@BelongsToMany(() => TeacherModel, () => CreditTeacherModel, 'credit_id')
|
||||
teachers!: Teacher[]
|
||||
|
||||
@Column(DataType.STRING)
|
||||
course_code!: string
|
||||
|
||||
@BelongsTo(() => Course, { foreignKey: 'course_id' })
|
||||
@BelongsTo(() => CourseModel, { foreignKey: 'course_id' })
|
||||
course!: Course
|
||||
|
||||
@ForeignKey(() => Course)
|
||||
@ForeignKey(() => CourseModel)
|
||||
@Column(DataType.STRING)
|
||||
course_id!: string
|
||||
|
||||
@@ -85,11 +92,11 @@ export class Credit extends Model<InferAttributes<Credit>> {
|
||||
@Column(DataType.BOOLEAN)
|
||||
is_open!: boolean
|
||||
|
||||
@ForeignKey(() => SISStudyRight)
|
||||
@ForeignKey(() => SISStudyRightModel)
|
||||
@Column(DataType.STRING)
|
||||
studyright_id!: string
|
||||
|
||||
@BelongsTo(() => SISStudyRight)
|
||||
@BelongsTo(() => SISStudyRightModel)
|
||||
studyright!: SISStudyRight
|
||||
|
||||
@CreatedAt
|
||||
@@ -108,3 +115,8 @@ export class Credit extends Model<InferAttributes<Credit>> {
|
||||
|
||||
static notUnnecessary: ({ credits }: { credits: number }) => boolean
|
||||
}
|
||||
|
||||
CreditModel.passed = ({ credittypecode }) => [CreditTypeCode.PASSED, CreditTypeCode.APPROVED].includes(credittypecode)
|
||||
CreditModel.failed = ({ credittypecode }) => credittypecode === CreditTypeCode.FAILED
|
||||
CreditModel.improved = ({ credittypecode }) => credittypecode === CreditTypeCode.IMPROVED
|
||||
CreditModel.notUnnecessary = ({ credits }) => credits > 0 && credits <= 12
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Credit } from './credit'
|
||||
import { Teacher } from './teacher'
|
||||
import type { CreditTeacher } from '@oodikone/shared/models'
|
||||
|
||||
import { CreditModel } from './credit'
|
||||
import { TeacherModel } from './teacher'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'credit_teacher',
|
||||
tableName: 'credit_teachers',
|
||||
})
|
||||
export class CreditTeacher extends Model<InferAttributes<CreditTeacher>> {
|
||||
@ForeignKey(() => Credit)
|
||||
export class CreditTeacherModel extends Model<CreditTeacher> implements CreditTeacher {
|
||||
@ForeignKey(() => CreditModel)
|
||||
@Column(DataType.STRING)
|
||||
credit_id!: string
|
||||
|
||||
@ForeignKey(() => Teacher)
|
||||
@ForeignKey(() => TeacherModel)
|
||||
@Column(DataType.STRING)
|
||||
teacher_id!: string
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../shared/types'
|
||||
import { CreditTypeCode } from '../types'
|
||||
import type { CreditType } from '@oodikone/shared/models'
|
||||
import { CreditTypeCode, Name } from '@oodikone/shared/types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'credit_type',
|
||||
tableName: 'credit_types',
|
||||
})
|
||||
export class CreditType extends Model<InferAttributes<CreditType>> {
|
||||
export class CreditTypeModel extends Model<CreditType> implements CreditType {
|
||||
@PrimaryKey
|
||||
@Column(DataType.INTEGER)
|
||||
credittypecode!: CreditTypeCode
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../shared/types'
|
||||
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 CurriculumPeriod extends Model<InferAttributes<CurriculumPeriod>> {
|
||||
export class CurriculumPeriodModel extends Model<CurriculumPeriod> implements CurriculumPeriod {
|
||||
@Column({ type: DataType.STRING, primaryKey: true })
|
||||
id!: string
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
@@ -11,24 +10,29 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { EnrollmentState } from '../types'
|
||||
import { Course, Semester, SISStudyRight, Student } from '.'
|
||||
import type { Enrollment, Semester, SISStudyRight, Student } from '@oodikone/shared/models'
|
||||
import { EnrollmentState } from '@oodikone/shared/types'
|
||||
|
||||
import { CourseModel } from './course'
|
||||
import { SemesterModel } from './semester'
|
||||
import { SISStudyRightModel } from './SISStudyRight'
|
||||
import { StudentModel } from './student'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'enrollment',
|
||||
tableName: 'enrollment',
|
||||
})
|
||||
export class Enrollment extends Model<InferAttributes<Enrollment>> {
|
||||
export class EnrollmentModel extends Model<Enrollment> implements Enrollment {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
|
||||
@ForeignKey(() => Student)
|
||||
@ForeignKey(() => StudentModel)
|
||||
@Column(DataType.STRING)
|
||||
studentnumber!: string
|
||||
|
||||
@BelongsTo(() => Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
@BelongsTo(() => StudentModel, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
student!: Student
|
||||
|
||||
@Column(DataType.STRING)
|
||||
@@ -40,15 +44,15 @@ export class Enrollment extends Model<InferAttributes<Enrollment>> {
|
||||
@Column(DataType.DATE)
|
||||
enrollment_date_time!: Date
|
||||
|
||||
@ForeignKey(() => Course)
|
||||
@ForeignKey(() => CourseModel)
|
||||
@Column(DataType.STRING)
|
||||
course_id!: string
|
||||
|
||||
@ForeignKey(() => Semester)
|
||||
@ForeignKey(() => SemesterModel)
|
||||
@Column(DataType.STRING)
|
||||
semester_composite!: string
|
||||
|
||||
@BelongsTo(() => Semester, { foreignKey: 'semester_composite', targetKey: 'composite' })
|
||||
@BelongsTo(() => SemesterModel, { foreignKey: 'semester_composite', targetKey: 'composite' })
|
||||
semester!: Semester
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
@@ -57,15 +61,15 @@ export class Enrollment extends Model<InferAttributes<Enrollment>> {
|
||||
@Column(DataType.BOOLEAN)
|
||||
is_open!: boolean
|
||||
|
||||
@ForeignKey(() => SISStudyRight)
|
||||
@ForeignKey(() => SISStudyRightModel)
|
||||
@Column({ type: DataType.STRING, allowNull: true })
|
||||
studyright_id!: string
|
||||
|
||||
@BelongsTo(() => SISStudyRight, { foreignKey: 'studyright_id', targetKey: 'id', constraints: false })
|
||||
@BelongsTo(() => SISStudyRightModel, { foreignKey: 'studyright_id', targetKey: 'id', constraints: false })
|
||||
studyright!: SISStudyRight
|
||||
|
||||
@BelongsTo(() => Course, { foreignKey: 'course_id', targetKey: 'id' })
|
||||
course!: Course
|
||||
@BelongsTo(() => CourseModel, { foreignKey: 'course_id', targetKey: 'id' })
|
||||
course!: CourseModel
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
|
||||
@@ -1,45 +1,18 @@
|
||||
import { CreditTypeCode } from '../types'
|
||||
import { Course } from './course'
|
||||
import { CourseProvider } from './courseProvider'
|
||||
import { CourseType } from './courseType'
|
||||
import { Credit } from './credit'
|
||||
import { CreditTeacher } from './creditTeacher'
|
||||
import { CreditType } from './creditType'
|
||||
import { CurriculumPeriod } from './curriculumPeriod'
|
||||
import { Enrollment } from './enrollment'
|
||||
import { Organization } from './organization'
|
||||
import { ProgrammeModule } from './programmeModule'
|
||||
import { ProgrammeModuleChild } from './programmeModuleChild'
|
||||
import { Semester } from './semester'
|
||||
import { SISStudyRight } from './SISStudyRight'
|
||||
import { SISStudyRightElement } from './SISStudyRightElement'
|
||||
import { Student } from './student'
|
||||
import { Studyplan } from './studyplan'
|
||||
import { StudyrightExtent } from './studyrightExtent'
|
||||
import { Teacher } from './teacher'
|
||||
|
||||
Credit.passed = ({ credittypecode }) => [CreditTypeCode.PASSED, CreditTypeCode.APPROVED].includes(credittypecode)
|
||||
Credit.failed = ({ credittypecode }) => credittypecode === CreditTypeCode.FAILED
|
||||
Credit.improved = ({ credittypecode }) => credittypecode === CreditTypeCode.IMPROVED
|
||||
Credit.notUnnecessary = ({ credits }) => credits > 0 && credits <= 12
|
||||
|
||||
export {
|
||||
Course,
|
||||
CourseProvider,
|
||||
CourseType,
|
||||
Credit,
|
||||
CreditTeacher,
|
||||
CreditType,
|
||||
CurriculumPeriod,
|
||||
Enrollment,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
ProgrammeModuleChild,
|
||||
Semester,
|
||||
SISStudyRight,
|
||||
SISStudyRightElement,
|
||||
Student,
|
||||
Studyplan,
|
||||
StudyrightExtent,
|
||||
Teacher,
|
||||
}
|
||||
export { CourseModel } from './course'
|
||||
export { CourseProviderModel } from './courseProvider'
|
||||
export { CourseTypeModel } from './courseType'
|
||||
export { CreditModel } from './credit'
|
||||
export { CreditTeacherModel } from './creditTeacher'
|
||||
export { CreditTypeModel } from './creditType'
|
||||
export { CurriculumPeriodModel } from './curriculumPeriod'
|
||||
export { EnrollmentModel } from './enrollment'
|
||||
export { OrganizationModel } from './organization'
|
||||
export { ProgrammeModuleModel } from './programmeModule'
|
||||
export { ProgrammeModuleChildModel } from './programmeModuleChild'
|
||||
export { SemesterModel } from './semester'
|
||||
export { SISStudyRightModel } from './SISStudyRight'
|
||||
export { SISStudyRightElementModel } from './SISStudyRightElement'
|
||||
export { StudentModel } from './student'
|
||||
export { StudyplanModel } from './studyplan'
|
||||
export { StudyrightExtentModel } from './studyrightExtent'
|
||||
export { TeacherModel } from './teacher'
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
import type { CustomPopulationSearch, CustomPopulationSearchCreation } from '@oodikone/shared/models/kone'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'custom_population_search',
|
||||
tableName: 'custom_population_searches',
|
||||
})
|
||||
export class CustomPopulationSearch extends Model<
|
||||
InferAttributes<CustomPopulationSearch>,
|
||||
InferCreationAttributes<CustomPopulationSearch>
|
||||
> {
|
||||
export class CustomPopulationSearchModel
|
||||
extends Model<CustomPopulationSearch, CustomPopulationSearchCreation>
|
||||
implements CustomPopulationSearch
|
||||
{
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.BIGINT)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { Column, DataType, Model, PrimaryKey, Table, AutoIncrement, Default } from 'sequelize-typescript'
|
||||
|
||||
import type { ExcludedCourse, ExcludedCourseCreation } from '@oodikone/shared/models/kone'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'excluded_course',
|
||||
tableName: 'excluded_courses',
|
||||
})
|
||||
export class ExcludedCourse extends Model<InferAttributes<ExcludedCourse>, InferCreationAttributes<ExcludedCourse>> {
|
||||
export class ExcludedCourseModel extends Model<ExcludedCourse, ExcludedCourseCreation> implements ExcludedCourse {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.INTEGER)
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import { CustomPopulationSearch } from './customPopulationSearch'
|
||||
import { ExcludedCourse } from './excludedCourse'
|
||||
import { OpenUniPopulationSearch } from './openUniPopulationSearch'
|
||||
import { ProgressCriteria } from './progressCriteria'
|
||||
import { StudyGuidanceGroupTag } from './studyGuidanceGroupTag'
|
||||
import { StudyProgrammePin } from './studyProgrammePin'
|
||||
import { Tag } from './tag'
|
||||
import { TagStudent } from './tagStudent'
|
||||
|
||||
export {
|
||||
CustomPopulationSearch,
|
||||
ExcludedCourse,
|
||||
OpenUniPopulationSearch,
|
||||
ProgressCriteria,
|
||||
StudyGuidanceGroupTag,
|
||||
StudyProgrammePin,
|
||||
Tag,
|
||||
TagStudent,
|
||||
}
|
||||
export { CustomPopulationSearchModel } from './customPopulationSearch'
|
||||
export { ExcludedCourseModel } from './excludedCourse'
|
||||
export { OpenUniPopulationSearchModel } from './openUniPopulationSearch'
|
||||
export { ProgressCriteriaModel } from './progressCriteria'
|
||||
export { StudyGuidanceGroupTagModel } from './studyGuidanceGroupTag'
|
||||
export { StudyProgrammePinModel } from './studyProgrammePin'
|
||||
export { TagModel } from './tag'
|
||||
export { TagStudentModel } from './tagStudent'
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
import type { OpenUniPopulationSearch, OpenUniPopulationSearchCreation } from '@oodikone/shared/models/kone'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
modelName: 'open_uni_population_search',
|
||||
tableName: 'open_uni_population_searches',
|
||||
})
|
||||
export class OpenUniPopulationSearch extends Model<
|
||||
InferAttributes<OpenUniPopulationSearch>,
|
||||
InferCreationAttributes<OpenUniPopulationSearch>
|
||||
> {
|
||||
export class OpenUniPopulationSearchModel
|
||||
extends Model<OpenUniPopulationSearch, OpenUniPopulationSearchCreation>
|
||||
implements OpenUniPopulationSearch
|
||||
{
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.BIGINT)
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
import type { ProgressCriteria, ProgressCriteriaCreation } from '@oodikone/shared/models/kone'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
modelName: 'progress_criteria',
|
||||
tableName: 'progress_criteria',
|
||||
})
|
||||
export class ProgressCriteria extends Model<
|
||||
InferAttributes<ProgressCriteria>,
|
||||
InferCreationAttributes<ProgressCriteria>
|
||||
> {
|
||||
export class ProgressCriteriaModel
|
||||
extends Model<ProgressCriteria, ProgressCriteriaCreation>
|
||||
implements ProgressCriteria
|
||||
{
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
code!: string
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { CreationOptional, InferAttributes } from 'sequelize'
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table, Unique } from 'sequelize-typescript'
|
||||
|
||||
import type { StudyGuidanceGroupTag, StudyGuidanceGroupTagCreation } from '@oodikone/shared/models/kone'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
modelName: 'study_guidance_group_tag',
|
||||
tableName: 'study_guidance_group_tags',
|
||||
})
|
||||
export class StudyGuidanceGroupTag extends Model<InferAttributes<StudyGuidanceGroupTag>> {
|
||||
export class StudyGuidanceGroupTagModel
|
||||
extends Model<StudyGuidanceGroupTag, StudyGuidanceGroupTagCreation>
|
||||
implements StudyGuidanceGroupTag
|
||||
{
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.BIGINT)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
import type { StudyProgrammePin } from '@oodikone/shared/models/kone'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
timestamps: false,
|
||||
modelName: 'study_programme_pin',
|
||||
tableName: 'study_programme_pins',
|
||||
})
|
||||
export class StudyProgrammePin extends Model<InferAttributes<StudyProgrammePin>> {
|
||||
export class StudyProgrammePinModel extends Model<StudyProgrammePin> implements StudyProgrammePin {
|
||||
@PrimaryKey
|
||||
@Column(DataType.INTEGER)
|
||||
userId!: number
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
import { TagStudent } from './tagStudent'
|
||||
import type { Tag, TagCreation, TagStudent } from '@oodikone/shared/models/kone'
|
||||
|
||||
import { TagStudentModel } from './tagStudent'
|
||||
|
||||
@Table({
|
||||
modelName: 'tag',
|
||||
tableName: 'tag',
|
||||
})
|
||||
export class Tag extends Model<InferAttributes<Tag>, InferCreationAttributes<Tag>> {
|
||||
export class TagModel extends Model<Tag, TagCreation> implements Tag {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@ForeignKey(() => TagStudent)
|
||||
@ForeignKey(() => TagStudentModel)
|
||||
@Column(DataType.BIGINT)
|
||||
tag_id!: CreationOptional<string>
|
||||
|
||||
@BelongsTo(() => TagStudent)
|
||||
@BelongsTo(() => TagStudentModel)
|
||||
tagStudent!: CreationOptional<TagStudent>
|
||||
|
||||
@Column(DataType.STRING)
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
import { Student } from '../student'
|
||||
import { Tag } from './tag'
|
||||
import type { Tag, TagStudent, TagStudentCreation } from '@oodikone/shared/models/kone'
|
||||
|
||||
import { StudentModel } from '../student'
|
||||
import { TagModel } from './tag'
|
||||
|
||||
@Table({
|
||||
modelName: 'tag_student',
|
||||
tableName: 'tag_student',
|
||||
})
|
||||
export class TagStudent extends Model<InferAttributes<TagStudent>, InferCreationAttributes<TagStudent>> {
|
||||
export class TagStudentModel extends Model<TagStudent, TagStudentCreation> implements TagStudent {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@ForeignKey(() => Tag)
|
||||
@ForeignKey(() => TagModel)
|
||||
@Column(DataType.BIGINT)
|
||||
tag_id!: string
|
||||
|
||||
@PrimaryKey
|
||||
@ForeignKey(() => Student)
|
||||
@ForeignKey(() => StudentModel)
|
||||
@Column(DataType.STRING)
|
||||
studentnumber!: string
|
||||
|
||||
@BelongsTo(() => Tag)
|
||||
@BelongsTo(() => TagModel)
|
||||
tag!: CreationOptional<Tag>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsToMany,
|
||||
Column,
|
||||
@@ -11,15 +10,20 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../shared/types'
|
||||
import { Course, CourseProvider, ProgrammeModule, SISStudyRight } from '.'
|
||||
import type { Organization, ProgrammeModule, SISStudyRight } from '@oodikone/shared/models'
|
||||
import type { Name } from '@oodikone/shared/types'
|
||||
|
||||
import { CourseModel } from './course'
|
||||
import { CourseProviderModel } from './courseProvider'
|
||||
import { ProgrammeModuleModel } from './programmeModule'
|
||||
import { SISStudyRightModel } from './SISStudyRight'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'organization',
|
||||
tableName: 'organization',
|
||||
})
|
||||
export class Organization extends Model<InferAttributes<Organization>> {
|
||||
export class OrganizationModel extends Model<Organization> implements Organization {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
@@ -33,17 +37,17 @@ export class Organization extends Model<InferAttributes<Organization>> {
|
||||
@Column(DataType.STRING)
|
||||
parent_id!: string
|
||||
|
||||
@HasMany(() => Organization, { foreignKey: 'parent_id', as: 'children' })
|
||||
@HasMany(() => OrganizationModel, { foreignKey: 'parent_id', as: 'children' })
|
||||
children!: Organization[]
|
||||
|
||||
@HasMany(() => ProgrammeModule, { foreignKey: 'organization_id' })
|
||||
@HasMany(() => ProgrammeModuleModel, { foreignKey: 'organization_id' })
|
||||
programmeModules!: ProgrammeModule[]
|
||||
|
||||
@HasMany(() => SISStudyRight, { foreignKey: 'facultyCode', sourceKey: 'code' })
|
||||
@HasMany(() => SISStudyRightModel, { foreignKey: 'facultyCode', sourceKey: 'code' })
|
||||
SISStudyRights!: SISStudyRight[]
|
||||
|
||||
@BelongsToMany(() => Course, () => CourseProvider, 'organizationcode')
|
||||
courses!: Course[]
|
||||
@BelongsToMany(() => CourseModel, () => CourseProviderModel, 'organizationcode')
|
||||
courses!: CourseModel[]
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
BelongsToMany,
|
||||
@@ -12,25 +11,26 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../shared/types'
|
||||
import { DegreeProgrammeType } from '../types'
|
||||
import { Organization } from './organization'
|
||||
import { ProgrammeModuleChild } from './programmeModuleChild'
|
||||
import type { Organization, ProgrammeModule } from '@oodikone/shared/models'
|
||||
import { DegreeProgrammeType, Name } from '@oodikone/shared/types'
|
||||
|
||||
import { OrganizationModel } from './organization'
|
||||
import { ProgrammeModuleChildModel } from './programmeModuleChild'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'programme_module',
|
||||
tableName: 'programme_modules',
|
||||
})
|
||||
export class ProgrammeModule extends Model<InferAttributes<ProgrammeModule>> {
|
||||
export class ProgrammeModuleModel extends Model<ProgrammeModule> implements ProgrammeModule {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
|
||||
@BelongsToMany(() => ProgrammeModule, () => ProgrammeModuleChild, 'child_id')
|
||||
@BelongsToMany(() => ProgrammeModuleModel, () => ProgrammeModuleChildModel, 'child_id')
|
||||
parents!: ProgrammeModule[]
|
||||
|
||||
@BelongsToMany(() => ProgrammeModule, () => ProgrammeModuleChild, 'parent_id')
|
||||
@BelongsToMany(() => ProgrammeModuleModel, () => ProgrammeModuleChildModel, 'parent_id')
|
||||
children!: ProgrammeModule[]
|
||||
|
||||
@Column(DataType.STRING)
|
||||
@@ -51,11 +51,11 @@ export class ProgrammeModule extends Model<InferAttributes<ProgrammeModule>> {
|
||||
@Column(DataType.STRING)
|
||||
studyLevel!: string
|
||||
|
||||
@ForeignKey(() => Organization)
|
||||
@ForeignKey(() => OrganizationModel)
|
||||
@Column(DataType.STRING)
|
||||
organization_id!: string
|
||||
|
||||
@BelongsTo(() => Organization, { foreignKey: 'organization_id' })
|
||||
@BelongsTo(() => OrganizationModel, { foreignKey: 'organization_id' })
|
||||
organization!: Organization
|
||||
|
||||
@Column(DataType.DATE)
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, ForeignKey, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { ProgrammeModule } from './programmeModule'
|
||||
import type { ProgrammeModuleChild } from '@oodikone/shared/models'
|
||||
|
||||
import { ProgrammeModuleModel } from './programmeModule'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'programme_module_child',
|
||||
tableName: 'programme_module_children',
|
||||
})
|
||||
export class ProgrammeModuleChild extends Model<InferAttributes<ProgrammeModuleChild>> {
|
||||
export class ProgrammeModuleChildModel extends Model<ProgrammeModuleChild> implements ProgrammeModuleChild {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
composite!: string
|
||||
|
||||
@ForeignKey(() => ProgrammeModule)
|
||||
@ForeignKey(() => ProgrammeModuleModel)
|
||||
@Column(DataType.STRING)
|
||||
parentId!: string
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../shared/types'
|
||||
import type { Semester } from '@oodikone/shared/models'
|
||||
import type { Name } from '@oodikone/shared/types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'semester',
|
||||
tableName: 'semesters',
|
||||
})
|
||||
export class Semester extends Model<InferAttributes<Semester>> {
|
||||
export class SemesterModel extends Model<Semester> implements Semester {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
composite!: string
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../shared/types'
|
||||
import { GenderCode } from '../types'
|
||||
import { Credit, Enrollment, SISStudyRight, Studyplan } from '.'
|
||||
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 { CreditModel } from './credit'
|
||||
import { EnrollmentModel } from './enrollment'
|
||||
import { SISStudyRightModel } from './SISStudyRight'
|
||||
import { StudyplanModel } from './studyplan'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'student',
|
||||
tableName: 'student',
|
||||
})
|
||||
export class Student extends Model<InferAttributes<Student>> {
|
||||
export class StudentModel extends Model<Student> implements Student {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
studentnumber!: string
|
||||
@@ -24,16 +28,16 @@ export class Student extends Model<InferAttributes<Student>> {
|
||||
@Column(DataType.STRING)
|
||||
abbreviatedname!: string
|
||||
|
||||
@HasMany(() => Enrollment, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
@HasMany(() => EnrollmentModel, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
enrollments!: Enrollment[]
|
||||
|
||||
@HasMany(() => SISStudyRight, { foreignKey: 'studentNumber', sourceKey: 'studentnumber' })
|
||||
@HasMany(() => SISStudyRightModel, { foreignKey: 'studentNumber', sourceKey: 'studentnumber' })
|
||||
studyRights!: SISStudyRight[]
|
||||
|
||||
@HasMany(() => Studyplan, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
@HasMany(() => StudyplanModel, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
studyplans!: Studyplan[]
|
||||
|
||||
@HasMany(() => Credit, { foreignKey: 'student_studentnumber', sourceKey: 'studentnumber' })
|
||||
@HasMany(() => CreditModel, { foreignKey: 'student_studentnumber', sourceKey: 'studentnumber' })
|
||||
credits!: Credit[]
|
||||
|
||||
@Column(DataType.DATE)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
@@ -11,30 +10,33 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { Student, SISStudyRight } from '.'
|
||||
import type { SISStudyRight, Student, Studyplan } from '@oodikone/shared/models'
|
||||
|
||||
import { SISStudyRightModel } from './SISStudyRight'
|
||||
import { StudentModel } from './student'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'studyplan',
|
||||
tableName: 'studyplan',
|
||||
})
|
||||
export class Studyplan extends Model<InferAttributes<Studyplan>> {
|
||||
export class StudyplanModel extends Model<Studyplan> implements Studyplan {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
|
||||
@ForeignKey(() => Student)
|
||||
@ForeignKey(() => StudentModel)
|
||||
@Column(DataType.STRING)
|
||||
studentnumber!: string
|
||||
|
||||
@BelongsTo(() => Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
@BelongsTo(() => StudentModel, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
student!: Student
|
||||
|
||||
@ForeignKey(() => SISStudyRight)
|
||||
@ForeignKey(() => SISStudyRightModel)
|
||||
@Column(DataType.STRING)
|
||||
sis_study_right_id!: string
|
||||
|
||||
@BelongsTo(() => SISStudyRight, { foreignKey: 'sis_study_right_id', targetKey: 'id', as: 'studyRight' })
|
||||
@BelongsTo(() => SISStudyRightModel, { foreignKey: 'sis_study_right_id', targetKey: 'id', as: 'studyRight' })
|
||||
studyRight!: SISStudyRight
|
||||
|
||||
@Column(DataType.STRING)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { ExtentCode } from '../types'
|
||||
import type { StudyrightExtent } from '@oodikone/shared/models'
|
||||
import { ExtentCode } from '@oodikone/shared/types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'studyright_extent',
|
||||
tableName: 'studyright_extents',
|
||||
})
|
||||
export class StudyrightExtent extends Model<InferAttributes<StudyrightExtent>> {
|
||||
export class StudyrightExtentModel extends Model<StudyrightExtent> implements StudyrightExtent {
|
||||
@PrimaryKey
|
||||
@Column(DataType.INTEGER)
|
||||
extentcode!: ExtentCode
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { BelongsToMany, Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Credit } from './credit'
|
||||
import { CreditTeacher } from './creditTeacher'
|
||||
import type { Teacher, Credit } from '@oodikone/shared/models'
|
||||
|
||||
import { CreditModel } from './credit'
|
||||
import { CreditTeacherModel } from './creditTeacher'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'teacher',
|
||||
tableName: 'teacher',
|
||||
})
|
||||
export class Teacher extends Model<InferAttributes<Teacher>> {
|
||||
export class TeacherModel extends Model<Teacher> implements Teacher {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
@@ -17,7 +18,7 @@ export class Teacher extends Model<InferAttributes<Teacher>> {
|
||||
@Column(DataType.STRING)
|
||||
name!: string
|
||||
|
||||
@BelongsToMany(() => Credit, () => CreditTeacher)
|
||||
@BelongsToMany(() => CreditModel, () => CreditTeacherModel)
|
||||
credits!: Credit[]
|
||||
|
||||
@CreatedAt
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { CreationOptional } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Default, Model, PrimaryKey, Table, Unique } from 'sequelize-typescript'
|
||||
|
||||
import { Language } from '../shared/language'
|
||||
import { Role } from '../shared/types'
|
||||
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,
|
||||
@@ -10,7 +12,7 @@ import { Role } from '../shared/types'
|
||||
modelName: 'users',
|
||||
tableName: 'users',
|
||||
})
|
||||
export class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
|
||||
export class UserModel extends Model<User, UserCreation> implements User {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.BIGINT)
|
||||
|
||||
@@ -53,9 +53,9 @@ const routes = (app: Express, url: string) => {
|
||||
app.use(`${url}/semesters/codes`, semesters)
|
||||
app.use(url, tags)
|
||||
app.use(url, programmeModules)
|
||||
app.use(`${url}/v3/course_exclusions`, courseExclusions)
|
||||
app.use(`${url}/v3/course-exclusions`, courseExclusions)
|
||||
app.use(`${url}/v2/studyprogrammes`, studyProgramme)
|
||||
app.use(`${url}/programmecriteria`, studyProgrammeCriteria)
|
||||
app.use(`${url}/programme-criteria`, studyProgrammeCriteria)
|
||||
app.use(`${url}/openunisearch`, auth.roles(['openUniSearch']), customOpenUniSearch)
|
||||
app.use(`${url}/changelog`, changelog)
|
||||
app.use(`${url}/completedcoursessearch`, completedCoursesSearch)
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import axios from 'axios'
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import { Release } from '@oodikone/shared/types'
|
||||
import { isDev } from '../config'
|
||||
import { Release } from '../shared/types'
|
||||
|
||||
const router = Router()
|
||||
|
||||
const changelog: { data?: Release[] } = {}
|
||||
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
export type ChangelogResBody = Release[]
|
||||
|
||||
router.get<never, ChangelogResBody>('/', async (_req: Request, res: Response) => {
|
||||
if (changelog.data) {
|
||||
return res.status(200).send(changelog.data)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { Router } from 'express'
|
||||
import { difference, intersection } from 'lodash'
|
||||
|
||||
import { getCompletedCourses } from '../services/completedCoursesSearch'
|
||||
import { CanError } from '@oodikone/shared/routes'
|
||||
import { tryCatch } from '@oodikone/shared/util'
|
||||
import { Courses, FormattedStudent, getCompletedCourses } from '../services/completedCoursesSearch'
|
||||
import {
|
||||
getOpenUniSearches,
|
||||
createNewSearch,
|
||||
deleteSearch,
|
||||
updateSearch,
|
||||
FoundSearch,
|
||||
} from '../services/openUni/openUniManageSearches'
|
||||
import { hasFullAccessToStudentData } from '../util'
|
||||
import { hasFullAccessToStudentData, safeJSONParse } from '../util'
|
||||
import { getImporterClient } from '../util/importerClient'
|
||||
import logger from '../util/logger'
|
||||
|
||||
@@ -16,17 +19,22 @@ const router = Router()
|
||||
|
||||
const importerClient = getImporterClient()
|
||||
|
||||
interface GetSearchRequest extends Request {
|
||||
query: {
|
||||
studentlist: string
|
||||
courselist: string
|
||||
}
|
||||
export type SearchResBody = CanError<
|
||||
{ discardedStudentNumbers: string[] } & { students: Omit<FormattedStudent, 'allEnrollments'>[]; courses: Courses }
|
||||
>
|
||||
export type SearchReqBody = never
|
||||
export type SearchQuery = {
|
||||
studentlist: string
|
||||
courselist: string
|
||||
}
|
||||
|
||||
router.get('/', async (req: GetSearchRequest, res: Response) => {
|
||||
const studentNumbers = JSON.parse(req.query?.studentlist) || []
|
||||
const courseCodes = JSON.parse(req.query?.courselist) || []
|
||||
router.get<never, SearchResBody, SearchReqBody, SearchQuery>('/', async (req, res) => {
|
||||
const { studentlist, courselist } = req.query
|
||||
const studentNumbers: string[] = (await safeJSONParse(studentlist)) ?? []
|
||||
const courseCodes: string[] = (await safeJSONParse(courselist)) ?? []
|
||||
|
||||
const { roles, studentsUserCanAccess, sisPersonId: teacherId } = req.user
|
||||
|
||||
if (!Array.isArray(studentNumbers)) {
|
||||
return res.status(400).json({ error: 'Student numbers must be of type array' })
|
||||
}
|
||||
@@ -34,48 +42,60 @@ router.get('/', async (req: GetSearchRequest, res: Response) => {
|
||||
return res.status(400).json({ error: 'Courses must be of type array' })
|
||||
}
|
||||
|
||||
const answerTimeout = new Promise(resolve => setTimeout(resolve, 6000))
|
||||
const answerTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), 6000))
|
||||
|
||||
try {
|
||||
// Teachers can also get rights to students via Importer if
|
||||
// the students have enrolled to their courses in last 8 months
|
||||
// (acual logic a bit different, see Importer)
|
||||
const teacherRightsToStudents = (await Promise.race([
|
||||
importerClient!.post('/teacher-rights/', { teacherId, studentNumbers }),
|
||||
answerTimeout,
|
||||
])) as { data: string[] }
|
||||
if (teacherRightsToStudents && Array.isArray(teacherRightsToStudents.data)) {
|
||||
studentsUserCanAccess.push(...teacherRightsToStudents.data)
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Teachers can also get rights to students via Importer if
|
||||
// the students have enrolled to their courses in last 8 months
|
||||
// (acual logic a bit different, see Importer)
|
||||
const { data: teacherRightsToStudents, error } = importerClient
|
||||
? await tryCatch<{ data: string[] }>(
|
||||
Promise.race([
|
||||
importerClient.post('/teacher-rights/', { teacherId, studentNumbers }),
|
||||
answerTimeout as Promise<never>, // This will always reject with an Error
|
||||
])
|
||||
)
|
||||
: { data: null, error: null }
|
||||
|
||||
if (error) {
|
||||
logger.error(`Importer teacher-rights request failed with message: ${error?.message}`)
|
||||
} else if (teacherRightsToStudents && Array.isArray(teacherRightsToStudents.data)) {
|
||||
studentsUserCanAccess.push(...teacherRightsToStudents.data)
|
||||
}
|
||||
|
||||
const filteredStudentNumbers = hasFullAccessToStudentData(roles)
|
||||
? studentNumbers
|
||||
: intersection(studentNumbers, studentsUserCanAccess)
|
||||
|
||||
const completedCourses = await getCompletedCourses(filteredStudentNumbers, courseCodes)
|
||||
const discardedStudentNumbers = difference(
|
||||
studentNumbers,
|
||||
completedCourses.students.map(student => student.studentNumber)
|
||||
)
|
||||
|
||||
return res.json({ discardedStudentNumbers, ...completedCourses })
|
||||
})
|
||||
|
||||
router.get('/searches', async (req: Request, res: Response) => {
|
||||
export type GetSearchResBody = FoundSearch[]
|
||||
|
||||
router.get<never, GetSearchResBody>('/searches', async (req, res) => {
|
||||
const userId = req.user.id
|
||||
const foundSearches = await getOpenUniSearches(userId)
|
||||
return res.json(foundSearches)
|
||||
})
|
||||
|
||||
interface CreateSearchRequest extends Request {
|
||||
body: {
|
||||
courselist: string[]
|
||||
name: string
|
||||
}
|
||||
export type CreateSearchResBody = CanError<{
|
||||
id: string
|
||||
userId: string
|
||||
name: string
|
||||
courseList: string[]
|
||||
updatedAt: string
|
||||
}>
|
||||
export type CreateSearchReqBody = {
|
||||
courselist: string[]
|
||||
name: string
|
||||
}
|
||||
|
||||
router.post('/searches', async (req: CreateSearchRequest, res: Response) => {
|
||||
router.post<never, CreateSearchResBody, CreateSearchReqBody>('/searches', async (req, res) => {
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const name = req.body?.name
|
||||
const userId = req.user.id
|
||||
@@ -98,26 +118,30 @@ router.post('/searches', async (req: CreateSearchRequest, res: Response) => {
|
||||
})
|
||||
})
|
||||
|
||||
interface UpdateSearchRequest extends Request {
|
||||
body: {
|
||||
courselist: string[]
|
||||
}
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
export type UpdateSearchParams = { id: string }
|
||||
export type UpdateSearchResBody = CanError<{
|
||||
id: string
|
||||
userId: string
|
||||
name: string
|
||||
courseList: string[]
|
||||
updatedAt: string
|
||||
} | void>
|
||||
export type UpdateSearchReqBody = { courselist: string[] }
|
||||
|
||||
router.put<UpdateSearchParams, UpdateSearchResBody, UpdateSearchReqBody>('/searches/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const { courselist: courseCodes = [] } = req.body
|
||||
const { id: userId } = req.user
|
||||
|
||||
router.put('/searches/:id', async (req: UpdateSearchRequest, res: Response) => {
|
||||
const id = req.params?.id
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const userId = req.user.id
|
||||
if (!id || !userId) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
const updatedSearch = await updateSearch(userId, id, courseCodes)
|
||||
if (!updatedSearch) {
|
||||
return res.status(404).json({ error: 'Courselist could not be found' })
|
||||
}
|
||||
|
||||
return res.json({
|
||||
id: updatedSearch.id,
|
||||
userId: updatedSearch.userId,
|
||||
@@ -127,22 +151,22 @@ router.put('/searches/:id', async (req: UpdateSearchRequest, res: Response) => {
|
||||
})
|
||||
})
|
||||
|
||||
interface DeleteSearchRequest extends Request {
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
export type DeleteSearchParams = { id: string }
|
||||
export type DeleteSearchResBody = CanError<string | void>
|
||||
|
||||
router.delete<DeleteSearchParams, DeleteSearchResBody>('/searches/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const { id: userId } = req.user
|
||||
|
||||
router.delete('/searches/:id', async (req: DeleteSearchRequest, res: Response) => {
|
||||
const id = req.params?.id
|
||||
const userId = req.user.id
|
||||
if (!id || !userId) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
const deletedSearch = await deleteSearch(userId, id)
|
||||
if (!deletedSearch) {
|
||||
return res.status(404).json({ error: 'Courselist could not be found' })
|
||||
}
|
||||
|
||||
return res.json(id)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,39 +1,36 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { Router } from 'express'
|
||||
|
||||
import { addExcludedCourses, removeExcludedCourses } from '../services/excludedCourses'
|
||||
|
||||
const router = Router()
|
||||
|
||||
interface ExcludedCoursesRequest extends Request {
|
||||
body: {
|
||||
courseCodes: string[]
|
||||
curriculumVersion: string
|
||||
}
|
||||
params: {
|
||||
code: string
|
||||
}
|
||||
export type ExcludedCoursesParams = { code: string }
|
||||
export type ExcludedCoursesResBody = void
|
||||
export type ExcludedCoursesReqBody = {
|
||||
courseCodes: string[]
|
||||
curriculumVersion: string
|
||||
}
|
||||
|
||||
router.post('/:code', async (req: ExcludedCoursesRequest, res: Response) => {
|
||||
router.post<ExcludedCoursesParams, ExcludedCoursesResBody, ExcludedCoursesReqBody>('/:code', async (req, res) => {
|
||||
const { code: programmeCode } = req.params
|
||||
const { courseCodes, curriculumVersion } = req.body
|
||||
const result = await addExcludedCourses(programmeCode, courseCodes, curriculumVersion)
|
||||
const result = await addExcludedCourses(courseCodes, curriculumVersion, programmeCode)
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
res.json(result)
|
||||
res.status(201).end()
|
||||
})
|
||||
|
||||
router.delete('/:code', async (req: ExcludedCoursesRequest, res: Response) => {
|
||||
router.delete<ExcludedCoursesParams, ExcludedCoursesResBody, ExcludedCoursesReqBody>('/:code', async (req, res) => {
|
||||
const { code: programmeCode } = req.params
|
||||
const { courseCodes, curriculumVersion } = req.body
|
||||
const result = await removeExcludedCourses(programmeCode, courseCodes, curriculumVersion)
|
||||
const result = await removeExcludedCourses(courseCodes, curriculumVersion, programmeCode)
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
res.json(result)
|
||||
res.status(204).end()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import crypto from 'crypto'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import moment from 'moment'
|
||||
|
||||
import { getCourseYearlyStats } from '../services/courses'
|
||||
import { getCoursesByNameAndOrCode, getCoursesByCodes } from '../services/courses/courseFinders'
|
||||
@@ -51,10 +50,10 @@ router.get('/v2/coursesmulti', async (req: GetCoursesRequest, res: Response) =>
|
||||
}
|
||||
} else {
|
||||
const mergedCourse = mergedCourses[groupId]
|
||||
if (moment(mergedCourse.max_attainment_date).isBefore(course.max_attainment_date)) {
|
||||
if (mergedCourse.max_attainment_date < course.max_attainment_date) {
|
||||
mergedCourse.max_attainment_date = course.max_attainment_date
|
||||
}
|
||||
if (moment(mergedCourse.min_attainment_date).isAfter(course.min_attainment_date)) {
|
||||
if (mergedCourse.min_attainment_date > course.min_attainment_date) {
|
||||
mergedCourse.min_attainment_date = course.min_attainment_date
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import { CurriculumPeriod } from '../models'
|
||||
import { CurriculumPeriodModel } from '../models'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
const result = await CurriculumPeriod.findAll()
|
||||
const result = await CurriculumPeriodModel.findAll()
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { Router } from 'express'
|
||||
|
||||
import { CanError } from '@oodikone/shared/routes'
|
||||
import {
|
||||
getOpenUniSearches,
|
||||
createNewSearch,
|
||||
@@ -7,59 +8,74 @@ import {
|
||||
updateSearch,
|
||||
} from '../services/openUni/openUniManageSearches'
|
||||
import { getCustomOpenUniCourses } from '../services/openUni/openUniStats'
|
||||
import { safeJSONParse } from '../util'
|
||||
|
||||
const router = Router()
|
||||
|
||||
interface GetSearchRequest extends Request {
|
||||
query: {
|
||||
courselist: string
|
||||
startdate: string
|
||||
enddate: string
|
||||
}
|
||||
type SearchResBody = CanError<Awaited<ReturnType<typeof getCustomOpenUniCourses>>>
|
||||
type SearchReqBody = never
|
||||
type SearchQuery = {
|
||||
courselist: string
|
||||
startdate: string
|
||||
enddate: string
|
||||
}
|
||||
|
||||
router.get('/', async (req: GetSearchRequest, res: Response) => {
|
||||
const courseCodes = JSON.parse(req.query?.courselist) || []
|
||||
const startDate = req.query?.startdate !== 'null' ? new Date(req.query.startdate) : new Date(2017, 7, 1, 0, 0, 0)
|
||||
const endDate = req.query?.enddate !== 'null' ? new Date(req.query.enddate) : new Date()
|
||||
if (req.query?.enddate === 'null') {
|
||||
endDate.setHours(23, 59, 59, 999)
|
||||
}
|
||||
router.get<never, SearchResBody, SearchReqBody, SearchQuery>('/', async (req, res) => {
|
||||
const { courselist, startdate, enddate } = req.query
|
||||
|
||||
const courseCodes = (await safeJSONParse(courselist)) ?? []
|
||||
if (!Array.isArray(courseCodes)) {
|
||||
return res.status(400).json({ error: 'Courses must be of type array' })
|
||||
}
|
||||
|
||||
const startDate = startdate !== 'null' ? new Date(startdate) : new Date(2017, 7, 1, 0, 0, 0)
|
||||
const endDate = enddate !== 'null' ? new Date(enddate) : new Date()
|
||||
if (enddate === 'null') {
|
||||
endDate.setHours(23, 59, 59, 999)
|
||||
}
|
||||
|
||||
const customOpenUniSearches = await getCustomOpenUniCourses(courseCodes, startDate, endDate)
|
||||
return res.json(customOpenUniSearches)
|
||||
})
|
||||
|
||||
router.get('/searches', async (req: Request, res: Response) => {
|
||||
const userId = req.user.id
|
||||
type GetSearchResBody = Awaited<ReturnType<typeof getOpenUniSearches>>
|
||||
router.get<never, GetSearchResBody>('/searches', async (req, res) => {
|
||||
const { id: userId } = req.user
|
||||
const foundSearches = await getOpenUniSearches(userId)
|
||||
return res.json(foundSearches)
|
||||
})
|
||||
|
||||
interface CreateSearchRequest extends Request {
|
||||
body: {
|
||||
courselist: string[]
|
||||
name: string
|
||||
}
|
||||
type CreateSearchResBody = CanError<{
|
||||
id: string
|
||||
userId: string
|
||||
name: string
|
||||
courseList: string[]
|
||||
updatedAt: string
|
||||
}>
|
||||
type CreateSearchReqBody = {
|
||||
courselist: string[]
|
||||
name: string
|
||||
}
|
||||
|
||||
router.post('/searches', async (req: CreateSearchRequest, res: Response) => {
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const name = req.body?.name
|
||||
const userId = req.user.id
|
||||
router.post<never, CreateSearchResBody, CreateSearchReqBody>('/searches', async (req, res) => {
|
||||
const { courselist, name } = req.body
|
||||
const { id: userId } = req.user
|
||||
|
||||
const courseCodes = courselist || []
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'Name missing' })
|
||||
}
|
||||
|
||||
if (courseCodes && !Array.isArray(courseCodes)) {
|
||||
return res.status(400).json({ error: 'Course codes must be type of array' })
|
||||
}
|
||||
|
||||
const createdSearch = await createNewSearch(userId, name, courseCodes)
|
||||
if (!createdSearch) {
|
||||
return res.status(400).json({ error: 'Failed to create search' })
|
||||
}
|
||||
|
||||
return res.status(201).json({
|
||||
id: createdSearch.id,
|
||||
userId: createdSearch.userId,
|
||||
@@ -69,26 +85,36 @@ router.post('/searches', async (req: CreateSearchRequest, res: Response) => {
|
||||
})
|
||||
})
|
||||
|
||||
interface UpdateSearchRequest extends Request {
|
||||
body: {
|
||||
courselist: string[]
|
||||
}
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
type UpdateSearchParams = {
|
||||
id: string
|
||||
}
|
||||
type UpdateSearchResBody = CanError<{
|
||||
id: string
|
||||
userId: string
|
||||
name: string
|
||||
courseList: string[]
|
||||
updatedAt: string
|
||||
}>
|
||||
type UpdateSearchReqBody = {
|
||||
courselist: string[]
|
||||
}
|
||||
|
||||
router.put('/searches/:id', async (req: UpdateSearchRequest, res: Response) => {
|
||||
const id = req.params?.id
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const userId = req.user.id
|
||||
router.put<UpdateSearchParams, UpdateSearchResBody, UpdateSearchReqBody>('/searches/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const { courselist } = req.body
|
||||
const { id: userId } = req.user
|
||||
|
||||
const courseCodes = courselist || []
|
||||
|
||||
if (!id || !userId) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
const updatedSearch = await updateSearch(userId, id, courseCodes)
|
||||
if (!updatedSearch) {
|
||||
return res.status(404).json({ error: 'Open uni search could not be found' })
|
||||
}
|
||||
|
||||
return res.json({
|
||||
id: updatedSearch.id,
|
||||
userId: updatedSearch.userId,
|
||||
@@ -98,22 +124,24 @@ router.put('/searches/:id', async (req: UpdateSearchRequest, res: Response) => {
|
||||
})
|
||||
})
|
||||
|
||||
interface DeleteSearchRequest extends Request {
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
type DeleteSearchParams = {
|
||||
id: string
|
||||
}
|
||||
type DeleteSearchResBody = CanError<string>
|
||||
|
||||
router.delete<DeleteSearchParams, DeleteSearchResBody>('/searches/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const { id: userId } = req.user
|
||||
|
||||
router.delete('/searches/:id', async (req: DeleteSearchRequest, res: Response) => {
|
||||
const id = req.params?.id
|
||||
const userId = req.user.id
|
||||
if (!id || !userId) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
const deletedSearch = await deleteSearch(userId, id)
|
||||
if (!deletedSearch) {
|
||||
return res.status(404).json({ error: 'Open uni search could not be found' })
|
||||
}
|
||||
|
||||
return res.json(id)
|
||||
})
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ router.post('/', async (req: Request, res: Response) => {
|
||||
return res.status(400).json({ error: 'Students must be of type array' })
|
||||
}
|
||||
|
||||
const customPopulationSearch = await createCustomPopulationSearch(name, id, students || [])
|
||||
const customPopulationSearch = await createCustomPopulationSearch(name, id, students ?? [])
|
||||
res.json(customPopulationSearch)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import { Graduated, ProgrammeFilter, SpecialGroups, StatsType, YearType } from '@oodikone/shared/types'
|
||||
import * as auth from '../middleware/auth'
|
||||
import { getDegreeProgrammesOfFaculty, getFacultyCodeById } from '../services/faculty/faculty'
|
||||
import { combineFacultyBasics } from '../services/faculty/facultyBasics'
|
||||
@@ -22,7 +23,6 @@ import { combineFacultyStudentProgress } from '../services/faculty/facultyStuden
|
||||
import { combineFacultyStudents } from '../services/faculty/facultyStudents'
|
||||
import { combineFacultyThesisWriters } from '../services/faculty/facultyThesisWriters'
|
||||
import { updateFacultyOverview, updateFacultyProgressOverview } from '../services/faculty/facultyUpdates'
|
||||
import { Graduated, ProgrammeFilter, SpecialGroups, StatsType, YearType } from '../shared/types'
|
||||
import logger from '../util/logger'
|
||||
|
||||
// Faculty uses a lot of tools designed for Study programme.
|
||||
|
||||
@@ -3,297 +3,183 @@ import crypto from 'crypto'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { difference, intersection, uniq } from 'lodash'
|
||||
|
||||
import { CanError } from '@oodikone/shared/routes'
|
||||
import { GenderCode } from '@oodikone/shared/types'
|
||||
import { mapToProviders } from '@oodikone/shared/util'
|
||||
import { rootOrgId } from '../config'
|
||||
import { SISStudyRight } from '../models'
|
||||
import { SISStudyRightModel } from '../models'
|
||||
import { maxYearsToCreatePopulationFrom, getCourseProvidersForCourses } from '../services/courses'
|
||||
import { encrypt, decrypt, type EncrypterData } from '../services/encrypt'
|
||||
import { encrypt, type EncrypterData } from '../services/encrypt'
|
||||
import { getDegreeProgrammesOfOrganization, ProgrammesOfOrganization } from '../services/faculty/faculty'
|
||||
import { bottlenecksOf } from '../services/populations/bottlenecksOf'
|
||||
import { Bottlenecks, bottlenecksOf } from '../services/populations/bottlenecksOf'
|
||||
import { optimizedStatisticsOf } from '../services/populations/optimizedStatisticsOf'
|
||||
import { populationStudentsMerger, populationCourseStatsMerger } from '../services/statMerger'
|
||||
import { findByTag, findByCourseAndSemesters } from '../services/students'
|
||||
import { mapToProviders } from '../shared/util'
|
||||
import { GenderCode, ParsedCourse, Unarray, Unification, UnifyStatus } from '../types'
|
||||
import { getFullStudyProgrammeRights, hasFullAccessToStudentData } from '../util'
|
||||
import { ApplicationError } from '../util/customErrors'
|
||||
import { findByCourseAndSemesters } from '../services/students'
|
||||
import { ParsedCourse, Unarray, Unification, UnifyStatus } from '../types'
|
||||
import { getFullStudyProgrammeRights, hasFullAccessToStudentData, safeJSONParse } from '../util'
|
||||
|
||||
const router = Router()
|
||||
|
||||
const filterPersonalTags = (population: Record<string, any>, userId: string) => {
|
||||
return {
|
||||
...population,
|
||||
students: population.students.map(student => {
|
||||
student.tags = student.tags.filter(({ tag }) => !tag.personal_user_id || tag.personal_user_id === userId)
|
||||
return student
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type EncryptedStudent = EncrypterData
|
||||
const isEncryptedStudent = (student?: string | EncryptedStudent) => {
|
||||
return (student as EncryptedStudent)?.encryptedData !== undefined
|
||||
}
|
||||
|
||||
interface PostPopulationStatisticsRequest extends Request {
|
||||
body: {
|
||||
year: string
|
||||
semesters: string[]
|
||||
studyRights: string | string[]
|
||||
months: number
|
||||
selectedStudents: string[] | EncryptedStudent[]
|
||||
selectedStudentsByYear?: { [year: string]: string[] }
|
||||
studentStatuses?: string[]
|
||||
years?: string[]
|
||||
}
|
||||
export type PopulationstatisticsCoursesResBody = CanError<Bottlenecks>
|
||||
export type PopulationstatisticsCoursesReqBody = {
|
||||
// NOTE: Encrypted students have their iv in selectedStudents
|
||||
selectedStudents: string[] | EncryptedStudent[]
|
||||
selectedStudentsByYear?: { [year: string]: string[] }
|
||||
courses?: string[]
|
||||
}
|
||||
|
||||
// POST instead of GET because of too long params and "sensitive" data
|
||||
router.post('/v2/populationstatistics/courses', async (req: PostPopulationStatisticsRequest, res: Response) => {
|
||||
if (!req.body.year || !req.body.semesters || !req.body.studyRights) {
|
||||
Sentry.captureException(new Error('The body should have a year, semester and study rights defined'))
|
||||
return res.status(400).json({ error: 'The body should have a year, semester and study rights defined' })
|
||||
}
|
||||
// NOTE: POST instead of GET because of too long params and "sensitive" data
|
||||
router.post<never, PopulationstatisticsCoursesResBody, PopulationstatisticsCoursesReqBody>(
|
||||
'/v4/populationstatistics/courses',
|
||||
async (req, res) => {
|
||||
const { roles, studentsUserCanAccess } = req.user
|
||||
const { selectedStudents, courses: selectedCourses = [], selectedStudentsByYear = {} } = req.body
|
||||
|
||||
if (!Array.isArray(req.body.studyRights)) {
|
||||
req.body.studyRights = [req.body.studyRights]
|
||||
}
|
||||
const hasFullAccess = hasFullAccessToStudentData(roles)
|
||||
|
||||
if (req.body.months == null) {
|
||||
req.body.months = 12
|
||||
}
|
||||
const isEncrypted = selectedStudents.some(isEncryptedStudent)
|
||||
const hasAccessToStudents =
|
||||
isEncrypted || (selectedStudents as string[]).every(student => studentsUserCanAccess.includes(student))
|
||||
|
||||
const encrypted = isEncryptedStudent(req.body.selectedStudents[0])
|
||||
|
||||
if (
|
||||
!encrypted &&
|
||||
!hasFullAccessToStudentData(req.user.roles) &&
|
||||
req.body.selectedStudents.some(student => !req.user.studentsUserCanAccess.includes(student))
|
||||
) {
|
||||
return res.status(403).json({ error: 'Trying to request unauthorized students data' })
|
||||
}
|
||||
|
||||
if (!req.body.semesters.every(semester => semester === 'FALL' || semester === 'SPRING')) {
|
||||
return res.status(400).json({ error: 'Semester should be either SPRING OR FALL' })
|
||||
}
|
||||
|
||||
if (
|
||||
req.body.studentStatuses &&
|
||||
!req.body.studentStatuses.every(
|
||||
status => status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
|
||||
)
|
||||
) {
|
||||
return res.status(400).json({ error: 'Student status should be either EXCHANGE or NONDEGREE or TRANSFERRED' })
|
||||
}
|
||||
|
||||
if (req.body.years) {
|
||||
const upperYearBound = new Date().getFullYear() + 1
|
||||
const multiCourseStatPromises = Promise.all(
|
||||
req.body.years.map(year => {
|
||||
if (req.body.selectedStudentsByYear) {
|
||||
req.body.selectedStudents = encrypted
|
||||
? (req.body.selectedStudents as EncryptedStudent[]).filter(isEncryptedStudent).map(decrypt)
|
||||
: req.body.selectedStudentsByYear[year]
|
||||
}
|
||||
const newMonths = (upperYearBound - Number(year)) * 12
|
||||
const query = { ...req.body, year, months: newMonths, selectedStudents: req.body.selectedStudents as string[] }
|
||||
const coursestatistics = bottlenecksOf(query, null, encrypted)
|
||||
return coursestatistics
|
||||
})
|
||||
)
|
||||
const multiCourseStats = await multiCourseStatPromises
|
||||
const result = populationCourseStatsMerger(multiCourseStats)
|
||||
if (result.error) {
|
||||
return res.status(400).json(result)
|
||||
if (!hasAccessToStudents && !hasFullAccess) {
|
||||
return res.status(403).json({ error: 'Trying to request unauthorized students data' })
|
||||
}
|
||||
|
||||
if (isEncrypted && !selectedStudents.every(isEncryptedStudent)) {
|
||||
Sentry.captureException(new Error('Trying to request unencrypted student data as encrypted'))
|
||||
return res.status(403).json({ error: 'Trying to request unauthorized student data' })
|
||||
}
|
||||
|
||||
const requiredFields = [selectedStudents, selectedCourses]
|
||||
if (requiredFields.some(field => !field)) {
|
||||
Sentry.captureException(new Error('The request body should countain: selected students and courses'))
|
||||
return res.status(400).json({ error: 'The request body should countain: selected students and courses' })
|
||||
}
|
||||
|
||||
const result = await bottlenecksOf(
|
||||
selectedStudents as string[],
|
||||
selectedStudentsByYear,
|
||||
selectedCourses,
|
||||
isEncrypted
|
||||
)
|
||||
|
||||
return res.json(result)
|
||||
}
|
||||
|
||||
if (encrypted) {
|
||||
req.body.selectedStudents = (req.body.selectedStudents as EncryptedStudent[]).map(decrypt)
|
||||
}
|
||||
|
||||
const query = { ...req.body, selectedStudents: req.body.selectedStudents as string[] }
|
||||
try {
|
||||
const result = await bottlenecksOf(query, null, encrypted)
|
||||
return res.json(result)
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
Sentry.captureException(error)
|
||||
}
|
||||
return res.status(400).end()
|
||||
}
|
||||
})
|
||||
|
||||
interface PostPopulationStatisticsByTagRequest extends Request {
|
||||
body: {
|
||||
tag: string
|
||||
}
|
||||
}
|
||||
|
||||
router.post(
|
||||
'/v2/populationstatistics/coursesbytag',
|
||||
async (req: PostPopulationStatisticsByTagRequest, res: Response) => {
|
||||
const { roles, studentsUserCanAccess } = req.user
|
||||
const { tag } = req.body
|
||||
if (!tag) {
|
||||
return res.status(400).json({ error: 'The body should have a tag defined' })
|
||||
}
|
||||
|
||||
const studentNumbers = await findByTag(tag)
|
||||
const studentNumberList = hasFullAccessToStudentData(roles)
|
||||
? studentNumbers
|
||||
: intersection(studentNumbers, studentsUserCanAccess)
|
||||
|
||||
try {
|
||||
const result = await bottlenecksOf(
|
||||
{
|
||||
year: '1900',
|
||||
studyRights: [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
tag,
|
||||
},
|
||||
studentNumberList
|
||||
)
|
||||
return res.json(result)
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
Sentry.captureException(error)
|
||||
}
|
||||
return res.status(400).end()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
interface PostPopulationStatisticsByStudentNumberListRequest extends Request {
|
||||
body: {
|
||||
studentnumberlist: string[]
|
||||
year: string
|
||||
studyRights: string[]
|
||||
semesters: string[]
|
||||
months: number
|
||||
}
|
||||
export type PopulationstatisticsResBody = CanError<{ students: any }>
|
||||
export type PopulationstatisticsReqBody = never
|
||||
export type PopulationstatisticsQuery = {
|
||||
semesters: string[]
|
||||
studentStatuses?: string[]
|
||||
// NOTE: This param is a JSON -object
|
||||
studyRights: string
|
||||
year: string
|
||||
months?: string
|
||||
years?: string[]
|
||||
}
|
||||
|
||||
router.post(
|
||||
'/v2/populationstatistics/coursesbystudentnumberlist',
|
||||
async (req: PostPopulationStatisticsByStudentNumberListRequest, res: Response) => {
|
||||
const { roles, studentsUserCanAccess } = req.user
|
||||
const {
|
||||
body: { studentnumberlist, ...query },
|
||||
} = req
|
||||
router.get<never, PopulationstatisticsResBody, PopulationstatisticsReqBody, PopulationstatisticsQuery>(
|
||||
'/v3/populationstatistics',
|
||||
async (req, res) => {
|
||||
const { id: userId, roles: userRoles, programmeRights: userProgrammeRights } = req.user
|
||||
const { year, semesters, studyRights: requestedStudyRightsJSON, studentStatuses } = req.query
|
||||
|
||||
if (!studentnumberlist) {
|
||||
throw new ApplicationError('The body should have a studentnumberlist defined', 400)
|
||||
const months = req.query.months ?? '12'
|
||||
|
||||
// NOTE: `year` isn't needed anymore if `years` is defined
|
||||
const requiredFields = [year, semesters, requestedStudyRightsJSON]
|
||||
if (requiredFields.some(field => !field)) {
|
||||
return res.status(400).json({ error: 'The query should have a year, semester and studyRights defined' })
|
||||
}
|
||||
|
||||
const studentNumbers = hasFullAccessToStudentData(roles)
|
||||
? studentnumberlist
|
||||
: intersection(studentnumberlist, studentsUserCanAccess)
|
||||
|
||||
try {
|
||||
const result = await bottlenecksOf(
|
||||
{
|
||||
year: query?.year ?? 1900,
|
||||
studyRights: query?.studyRights ?? [],
|
||||
semesters: query?.semesters ?? ['FALL', 'SPRING'],
|
||||
months: query?.months ?? 10000,
|
||||
},
|
||||
studentNumbers
|
||||
)
|
||||
return res.json(result)
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
Sentry.captureException(error)
|
||||
}
|
||||
return res.status(400).end()
|
||||
if (!semesters.every(semester => semester === 'FALL' || semester === 'SPRING')) {
|
||||
return res.status(400).json({ error: 'Semester should be either SPRING OR FALL' })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
interface GetPopulationStatisticsRequest extends Request {
|
||||
query: {
|
||||
year: string
|
||||
semesters: string[]
|
||||
studyRights: string
|
||||
months: string
|
||||
years?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/v3/populationstatistics', async (req: GetPopulationStatisticsRequest, res: Response) => {
|
||||
const { year, semesters, studyRights: studyRightsJSON } = req.query
|
||||
const { roles, programmeRights, id: userId } = req.user
|
||||
if (!year || !semesters || !studyRightsJSON) {
|
||||
res.status(400).json({ error: 'The query should have a year, semester and studyRights defined' })
|
||||
return
|
||||
}
|
||||
|
||||
const studyRights: { programme: string; combinedProgramme: string } = JSON.parse(studyRightsJSON)
|
||||
|
||||
const fullProgrammeRights = getFullStudyProgrammeRights(programmeRights)
|
||||
const programmeRightsCodes = programmeRights.map(({ code }) => code)
|
||||
|
||||
try {
|
||||
if (
|
||||
!hasFullAccessToStudentData(roles) &&
|
||||
!programmeRightsCodes.includes(studyRights.programme) &&
|
||||
!programmeRightsCodes.includes(studyRights.combinedProgramme)
|
||||
) {
|
||||
return res.status(403).json([])
|
||||
const hasCorrectStatus = (studentStatuses: string[]) =>
|
||||
studentStatuses.every(status => ['EXCHANGE', 'NONDEGREE', 'TRANSFERRED'].includes(status))
|
||||
if (studentStatuses && !hasCorrectStatus(studentStatuses)) {
|
||||
return res.status(400).json({ error: 'Student status should be either EXCHANGE or NONDEGREE or TRANSFERRED' })
|
||||
}
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: 'The query had invalid studyRights' })
|
||||
}
|
||||
|
||||
if (req.query.months === null) {
|
||||
req.query.months = '12'
|
||||
}
|
||||
const requestedStudyRights: { programme: string; combinedProgramme: string } | null =
|
||||
await safeJSONParse(requestedStudyRightsJSON)
|
||||
if (requestedStudyRights === null) {
|
||||
return res.status(400).json({ error: 'Invalid studyrights value!' })
|
||||
}
|
||||
|
||||
if (req.query.years) {
|
||||
const upperYearBound = new Date().getFullYear() + 1
|
||||
const multiPopulationStudentPromises = Promise.all(
|
||||
req.query.years.map(year => {
|
||||
const newMonths = (upperYearBound - Number(year)) * 12
|
||||
const populationStudents = optimizedStatisticsOf({
|
||||
...req.query,
|
||||
year: Number(year),
|
||||
studyRights: { programme: studyRights.programme },
|
||||
months: newMonths,
|
||||
const userFullProgrammeRights = getFullStudyProgrammeRights(userProgrammeRights)
|
||||
const userProgrammeRightsCodes = userProgrammeRights.map(({ code }) => code)
|
||||
|
||||
const hasFullAccessToStudents = hasFullAccessToStudentData(userRoles)
|
||||
const hasAccessToProgramme = userProgrammeRightsCodes.includes(requestedStudyRights.programme)
|
||||
const hasAccessToCombinedProgramme = userProgrammeRightsCodes.includes(requestedStudyRights.combinedProgramme)
|
||||
|
||||
if (!hasFullAccessToStudents && !hasAccessToProgramme && !hasAccessToCombinedProgramme) {
|
||||
return res.status(403).json({ error: 'Trying to request unauthorized students data' })
|
||||
}
|
||||
|
||||
let result
|
||||
if (req.query.years) {
|
||||
const upperYearBound = new Date().getFullYear() + 1
|
||||
const multiYearStudents = Promise.all(
|
||||
req.query.years.map(year => {
|
||||
const yearAsNumber = +year
|
||||
const monthsForCurrentYear = String((upperYearBound - yearAsNumber) * 12)
|
||||
return optimizedStatisticsOf({
|
||||
...req.query,
|
||||
userId,
|
||||
studyRights: requestedStudyRights.programme,
|
||||
year,
|
||||
months: monthsForCurrentYear,
|
||||
})
|
||||
})
|
||||
return populationStudents
|
||||
)
|
||||
|
||||
const populationStudentsMerger = (multiYearStudents: any) => {
|
||||
const samples = { students: [], courses: [] as any[] }
|
||||
const uniqueCourseCodes = new Set<string>()
|
||||
|
||||
for (const year of multiYearStudents) {
|
||||
samples.students = samples.students.concat(year.students)
|
||||
for (const course of year.courses) {
|
||||
if (!uniqueCourseCodes.has(course.code)) {
|
||||
uniqueCourseCodes.add(course.code)
|
||||
samples.courses.push(course)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return samples
|
||||
}
|
||||
|
||||
result = populationStudentsMerger(await multiYearStudents)
|
||||
} else {
|
||||
result = await optimizedStatisticsOf({
|
||||
...req.query,
|
||||
userId,
|
||||
studyRights: requestedStudyRights.programme,
|
||||
year,
|
||||
months,
|
||||
})
|
||||
)
|
||||
const multiPopulationStudents = await multiPopulationStudentPromises
|
||||
|
||||
const result = populationStudentsMerger(multiPopulationStudents)
|
||||
res.json(filterPersonalTags(result, userId))
|
||||
} else {
|
||||
const result: any = await optimizedStatisticsOf({
|
||||
...req.query,
|
||||
year: Number(req.query.year),
|
||||
studyRights: { programme: studyRights.programme },
|
||||
months: Number(req.query.months),
|
||||
})
|
||||
|
||||
if ('error' in result && result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
return res.status(400).end()
|
||||
}
|
||||
|
||||
// Obfuscate if user has only limited study programme rights
|
||||
// Obfuscate if user has only limited study programme rights and there are any students
|
||||
if (
|
||||
!hasFullAccessToStudentData(roles) &&
|
||||
!fullProgrammeRights.includes(studyRights.programme) &&
|
||||
!fullProgrammeRights.includes(studyRights.combinedProgramme)
|
||||
'students' in result &&
|
||||
!hasFullAccessToStudentData(userRoles) &&
|
||||
!userFullProgrammeRights.includes(requestedStudyRights.programme) &&
|
||||
!userFullProgrammeRights.includes(requestedStudyRights.combinedProgramme)
|
||||
) {
|
||||
if (!('students' in result)) {
|
||||
return
|
||||
}
|
||||
result.students = result.students.map(student => {
|
||||
const { iv, encryptedData: studentNumber } = encrypt(student.studentNumber)
|
||||
const obfuscatedBirthDate = new Date(Date.UTC(new Date(student.birthdate).getUTCFullYear(), 0, 1)) // correct year for age distribution calculation but the date is always January 1st
|
||||
// correct year for age distribution calculation but the date is always January 1st
|
||||
const obfuscatedBirthDate = new Date(Date.UTC(new Date(student.birthdate).getUTCFullYear(), 0))
|
||||
return {
|
||||
...student,
|
||||
firstnames: '',
|
||||
@@ -305,7 +191,6 @@ router.get('/v3/populationstatistics', async (req: GetPopulationStatisticsReques
|
||||
email: '',
|
||||
secondaryEmail: '',
|
||||
sis_person_id: '',
|
||||
enrollments: student.enrollments?.map(enrollment => ({ ...enrollment, studentnumber: studentNumber })),
|
||||
tags: [],
|
||||
birthdate: obfuscatedBirthDate,
|
||||
obfuscated: true,
|
||||
@@ -313,9 +198,9 @@ router.get('/v3/populationstatistics', async (req: GetPopulationStatisticsReques
|
||||
})
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, userId))
|
||||
return res.json(result)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
interface GetPopulationStatisticsByCourseRequest extends Request {
|
||||
query: {
|
||||
@@ -329,7 +214,7 @@ interface GetPopulationStatisticsByCourseRequest extends Request {
|
||||
|
||||
router.get('/v3/populationstatisticsbycourse', async (req: GetPopulationStatisticsByCourseRequest, res: Response) => {
|
||||
const { coursecodes, from, to, separate: separateString, unifyCourses } = req.query
|
||||
const { id, roles, studentsUserCanAccess: allStudentsUserCanAccess, programmeRights } = req.user
|
||||
const { id: userId, roles, studentsUserCanAccess: allStudentsUserCanAccess, programmeRights } = req.user
|
||||
|
||||
if (!coursecodes || !from || !to) {
|
||||
return res.status(400).json({ error: 'The body should have a yearcode and coursecode defined' })
|
||||
@@ -354,10 +239,10 @@ router.get('/v3/populationstatisticsbycourse', async (req: GetPopulationStatisti
|
||||
{
|
||||
// Useless, because studentnumbers are already filtered above by from & to.
|
||||
// We should probably refactor this to avoid more confusement.
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
userId,
|
||||
year: '1900',
|
||||
studyRights: undefined,
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
},
|
||||
studentNumbers
|
||||
)
|
||||
@@ -381,7 +266,7 @@ router.get('/v3/populationstatisticsbycourse', async (req: GetPopulationStatisti
|
||||
courses,
|
||||
gender_code,
|
||||
}: {
|
||||
studyRights: SISStudyRight[]
|
||||
studyRights: SISStudyRightModel[]
|
||||
studentNumber: string
|
||||
courses: ParsedCourse[]
|
||||
gender_code: GenderCode
|
||||
@@ -401,18 +286,13 @@ router.get('/v3/populationstatisticsbycourse', async (req: GetPopulationStatisti
|
||||
obfuscated: true,
|
||||
})
|
||||
|
||||
if ('error' in result && result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
return res.status(400).end()
|
||||
}
|
||||
|
||||
if ('students' in result) {
|
||||
result.students = result.students.map(student =>
|
||||
studentsUserCanAccess.has(student.studentNumber) ? student : obfuscateStudent(student)
|
||||
)
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, id))
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
interface PostByStudentNumbersRequest extends Request {
|
||||
@@ -427,32 +307,27 @@ interface PostByStudentNumbersRequest extends Request {
|
||||
|
||||
router.post('/v3/populationstatisticsbystudentnumbers', async (req: PostByStudentNumbersRequest, res: Response) => {
|
||||
const { studentnumberlist, tags } = req.body
|
||||
const { roles, id, studentsUserCanAccess } = req.user
|
||||
const { id: userId, roles, studentsUserCanAccess } = req.user
|
||||
const filteredStudentNumbers = hasFullAccessToStudentData(roles)
|
||||
? studentnumberlist
|
||||
: intersection(studentnumberlist, studentsUserCanAccess)
|
||||
const studyProgrammeCode = tags?.studyProgramme?.includes('+')
|
||||
? tags?.studyProgramme.split('+')[0]
|
||||
: tags?.studyProgramme
|
||||
|
||||
const studyProgrammeCode = tags?.studyProgramme?.split('+')[0]
|
||||
|
||||
const result = await optimizedStatisticsOf(
|
||||
{
|
||||
year: tags?.year ? Number(tags.year) : 1900,
|
||||
studyRights: studyProgrammeCode ? [studyProgrammeCode] : [],
|
||||
userId,
|
||||
year: tags?.year ?? '1900',
|
||||
studyRights: studyProgrammeCode,
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
},
|
||||
filteredStudentNumbers
|
||||
)
|
||||
if ('error' in result && result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
return res.status(500).json({ error: result.error })
|
||||
}
|
||||
|
||||
const resultWithStudyProgramme = { ...result, studyProgramme: tags?.studyProgramme }
|
||||
const discardedStudentNumbers = difference(studentnumberlist, filteredStudentNumbers)
|
||||
|
||||
res.status(200).json({ ...filterPersonalTags(resultWithStudyProgramme, id), discardedStudentNumbers })
|
||||
res.status(200).json({ ...resultWithStudyProgramme, discardedStudentNumbers })
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatistics/studyprogrammes', async (req: Request, res: Response) => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import { getCoursesAndModules, getCurriculumVersions } from '../services/programmeModules'
|
||||
import { getCoursesAndModules, getCurriculumOptions } from '../services/programmeModules'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/v3/curriculum-options/:code', async (req: Request, res: Response) => {
|
||||
const { code } = req.params
|
||||
const result = await getCurriculumVersions(code)
|
||||
const result = await getCurriculumOptions(code)
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import { Graduated, SpecialGroups, YearType } from '@oodikone/shared/types'
|
||||
import {
|
||||
getBasicStats,
|
||||
setBasicStats,
|
||||
@@ -18,7 +19,6 @@ import { getGraduationStatsForStudyTrack } from '../services/studyProgramme/stud
|
||||
import { updateBasicView, updateStudyTrackView } from '../services/studyProgramme/studyProgrammeUpdates'
|
||||
import { getStudyRightsInProgramme, getStudyTracksForProgramme } from '../services/studyProgramme/studyRightFinders'
|
||||
import { getStudyTrackStatsForStudyProgramme } from '../services/studyProgramme/studyTrackStats'
|
||||
import { Graduated, SpecialGroups, YearType } from '../shared/types'
|
||||
import logger from '../util/logger'
|
||||
import { logInfoForGrafana } from '../util/logInfoForGrafana'
|
||||
|
||||
@@ -27,16 +27,16 @@ const router = Router()
|
||||
interface GetCreditStatsRequest extends Request {
|
||||
query: {
|
||||
codes: string
|
||||
isAcademicYear: string
|
||||
includeSpecials: string
|
||||
specialGroups: SpecialGroups
|
||||
yearType: YearType
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/creditstats', async (req: GetCreditStatsRequest, res: Response) => {
|
||||
const { codes, isAcademicYear, includeSpecials } = req.query
|
||||
const { codes, specialGroups, yearType } = req.query
|
||||
const stats = {}
|
||||
for (const code of JSON.parse(codes)) {
|
||||
stats[code] = await getCreditsProduced(code, isAcademicYear !== 'false', includeSpecials !== 'false')
|
||||
stats[code] = await getCreditsProduced(code, yearType === 'ACADEMIC_YEAR', specialGroups === 'SPECIAL_INCLUDED')
|
||||
}
|
||||
return res.json({ stats })
|
||||
})
|
||||
@@ -105,20 +105,19 @@ router.get('/:id/graduationstats', async (req: GetStatsRequest, res: Response) =
|
||||
|
||||
interface GetCourseStatsRequest extends Request {
|
||||
query: {
|
||||
academicyear: string
|
||||
combined_programme: string
|
||||
combinedProgramme: string
|
||||
yearType: YearType
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/:id/coursestats', async (req: GetCourseStatsRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
const showByYear = req.query.academicyear
|
||||
const combinedProgramme = req.query?.combined_programme
|
||||
const { combinedProgramme, yearType } = req.query
|
||||
const date = new Date()
|
||||
date.setHours(23, 59, 59, 999)
|
||||
void logInfoForGrafana(code, combinedProgramme)
|
||||
try {
|
||||
const data = await getStudyProgrammeCoursesForStudyTrack(date.getTime(), code, showByYear, combinedProgramme)
|
||||
const data = await getStudyProgrammeCoursesForStudyTrack(date.getTime(), code, yearType, combinedProgramme)
|
||||
return res.json(data)
|
||||
} catch (error) {
|
||||
logger.error({ message: `Failed to get code ${code} programme courses stats`, meta: `${error}` })
|
||||
|
||||
@@ -10,12 +10,12 @@ const router = Router()
|
||||
|
||||
interface StudyProgrammeCriteriaRequest extends Request {
|
||||
query: {
|
||||
programmecode: string
|
||||
programmeCode: string
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/', async (req: StudyProgrammeCriteriaRequest, res: Response) => {
|
||||
const studyProgramme = req.query.programmecode
|
||||
const studyProgramme = req.query.programmeCode
|
||||
if (studyProgramme !== '' && !studyProgramme) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
@@ -37,7 +37,10 @@ router.post('/courses', async (req: CriteriaCoursesRequest, res: Response) => {
|
||||
return res.status(400).end()
|
||||
}
|
||||
const studyProgrammeCriteria = await saveYearlyCourseCriteria(code, courses, year)
|
||||
return res.json(studyProgrammeCriteria)
|
||||
if (!studyProgrammeCriteria) {
|
||||
return res.status(404).end()
|
||||
}
|
||||
return res.status(201).end()
|
||||
})
|
||||
|
||||
interface CriteriaCreditsRequest extends Request {
|
||||
@@ -53,7 +56,10 @@ router.post('/credits', async (req: CriteriaCreditsRequest, res: Response) => {
|
||||
return res.status(400).end()
|
||||
}
|
||||
const studyProgrammeCriteria = await saveYearlyCreditCriteria(code, credits)
|
||||
return res.json(studyProgrammeCriteria)
|
||||
if (!studyProgrammeCriteria) {
|
||||
return res.status(404).end()
|
||||
}
|
||||
return res.status(201).end()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { Router } from 'express'
|
||||
import { difference } from 'lodash'
|
||||
|
||||
import { CanError } from '@oodikone/shared/routes'
|
||||
import { NewTag, Role, StudentTag, Tag } from '@oodikone/shared/types'
|
||||
import { filterStudentNumbersByAccessRights } from '../services/students'
|
||||
import {
|
||||
findTagsByStudyTrack,
|
||||
@@ -11,7 +13,6 @@ import {
|
||||
findTagsFromStudyTrackById,
|
||||
deleteMultipleStudentTags,
|
||||
} from '../services/tags'
|
||||
import { NewTag, Role, StudentTag, Tag } from '../shared/types'
|
||||
import { getFullStudyProgrammeRights, hasFullAccessToStudentData } from '../util'
|
||||
|
||||
const router = Router()
|
||||
@@ -36,13 +37,10 @@ const getStudyTrackCode = (studyTrack: string, combinedProgramme: string) => {
|
||||
return combinedProgramme ? `${studyTrack}-${combinedProgramme}` : studyTrack
|
||||
}
|
||||
|
||||
interface GetTagsByStudyTrackRequest extends Request {
|
||||
params: {
|
||||
studyTrack: string
|
||||
}
|
||||
}
|
||||
export type GetTagsByStudyTrackParams = { studyTrack: string }
|
||||
export type GetTagsByStudyTrackResBody = Tag[] | null
|
||||
|
||||
router.get('/tags/:studyTrack', async (req: GetTagsByStudyTrackRequest, res: Response) => {
|
||||
router.get<GetTagsByStudyTrackParams, GetTagsByStudyTrackResBody>('/tags/:studyTrack', async (req, res) => {
|
||||
const { studyTrack } = req.params
|
||||
const { roles, id, programmeRights } = req.user
|
||||
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
|
||||
@@ -58,11 +56,10 @@ router.get('/tags/:studyTrack', async (req: GetTagsByStudyTrackRequest, res: Res
|
||||
res.status(200).json(filterRelevantTags(tags, id))
|
||||
})
|
||||
|
||||
interface PostTagRequest extends Request {
|
||||
body: { tag: NewTag }
|
||||
}
|
||||
export type PostTagResBody = CanError<Tag[] | void>
|
||||
export type PostTagReqBody = { tag: NewTag }
|
||||
|
||||
router.post('/tags', async (req: PostTagRequest, res: Response) => {
|
||||
router.post<never, PostTagResBody, PostTagReqBody>('/tags', async (req, res) => {
|
||||
const {
|
||||
tag: { name, personalUserId, studyTrack, year },
|
||||
} = req.body
|
||||
@@ -83,11 +80,10 @@ router.post('/tags', async (req: PostTagRequest, res: Response) => {
|
||||
res.status(200).json(filterRelevantTags(tags, id))
|
||||
})
|
||||
|
||||
interface DeleteTagRequest extends Request {
|
||||
body: { tag: Tag }
|
||||
}
|
||||
export type DeleteTagResBody = Tag[] | void
|
||||
export type DeleteTagReqBody = { tag: Tag }
|
||||
|
||||
router.delete('/tags', async (req: DeleteTagRequest, res: Response) => {
|
||||
router.delete<never, DeleteTagResBody, DeleteTagReqBody>('/tags', async (req, res) => {
|
||||
const {
|
||||
tag: { id, personalUserId, studyTrack },
|
||||
} = req.body
|
||||
@@ -105,37 +101,36 @@ router.delete('/tags', async (req: DeleteTagRequest, res: Response) => {
|
||||
res.status(200).json(filterRelevantTags(tags, id))
|
||||
})
|
||||
|
||||
interface GetStudentTagsByStudyTrackRequest extends Request {
|
||||
params: {
|
||||
studyTrack: string
|
||||
export type GetStudentTagsByStudyTrackParams = { studyTrack: string }
|
||||
export type GetStudentTagsByStudyTrackResBody = StudentTag[] | null
|
||||
|
||||
router.get<GetStudentTagsByStudyTrackParams, GetStudentTagsByStudyTrackResBody>(
|
||||
'/studenttags/:studyTrack',
|
||||
async (req, res) => {
|
||||
const { studyTrack } = req.params
|
||||
const { roles, id, programmeRights } = req.user
|
||||
|
||||
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
|
||||
const programmeCodes = getProgrammeCodes(studyTrack)
|
||||
const lacksProgrammeAccess = programmeCodes.every(code => !fullStudyProgrammeRights.includes(code))
|
||||
if (lacksProgrammeAccess && !roles?.includes('admin')) {
|
||||
return res.json(null)
|
||||
}
|
||||
|
||||
const tags = await getStudentTagsByStudyTrack(studyTrack)
|
||||
const relevantTags = filterRelevantStudentTags(tags, id)
|
||||
res.status(200).json(relevantTags)
|
||||
}
|
||||
)
|
||||
|
||||
export type PostStudentTagsResBody = CanError<void>
|
||||
export type PostStudentTagsReqBody = {
|
||||
combinedProgramme: string
|
||||
studentTags: StudentTag[]
|
||||
studyTrack: string
|
||||
}
|
||||
|
||||
router.get('/studenttags/:studyTrack', async (req: GetStudentTagsByStudyTrackRequest, res: Response) => {
|
||||
const { studyTrack } = req.params
|
||||
const { roles, id, programmeRights } = req.user
|
||||
|
||||
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
|
||||
const programmeCodes = getProgrammeCodes(studyTrack)
|
||||
const lacksProgrammeAccess = programmeCodes.every(code => !fullStudyProgrammeRights.includes(code))
|
||||
if (lacksProgrammeAccess && !roles?.includes('admin')) {
|
||||
return res.json(null)
|
||||
}
|
||||
|
||||
const tags = await getStudentTagsByStudyTrack(studyTrack)
|
||||
const relevantTags = filterRelevantStudentTags(tags, id)
|
||||
res.status(200).json(relevantTags)
|
||||
})
|
||||
|
||||
interface PostStudentTagsRequest extends Request {
|
||||
body: {
|
||||
combinedProgramme: string
|
||||
studentTags: StudentTag[]
|
||||
studyTrack: string
|
||||
}
|
||||
}
|
||||
|
||||
router.post('/studenttags', async (req: PostStudentTagsRequest, res: Response) => {
|
||||
router.post<never, PostStudentTagsResBody, PostStudentTagsReqBody>('/studenttags', async (req, res) => {
|
||||
const { combinedProgramme, studentTags, studyTrack } = req.body
|
||||
const { roles, programmeRights } = req.user
|
||||
|
||||
@@ -170,16 +165,15 @@ router.post('/studenttags', async (req: PostStudentTagsRequest, res: Response) =
|
||||
res.status(204).end()
|
||||
})
|
||||
|
||||
interface DeleteStudentTagsRequest extends Request {
|
||||
body: {
|
||||
combinedProgramme: string
|
||||
tagId: string
|
||||
studentNumbers: string[]
|
||||
studyTrack: string
|
||||
}
|
||||
export type DeleteStudentTagsResBody = CanError<void>
|
||||
export type DeleteStudentTagsReqBody = {
|
||||
combinedProgramme: string
|
||||
tagId: string
|
||||
studentNumbers: string[]
|
||||
studyTrack: string
|
||||
}
|
||||
|
||||
router.delete('/studenttags', async (req: DeleteStudentTagsRequest, res: Response) => {
|
||||
router.delete<never, DeleteStudentTagsResBody, DeleteStudentTagsReqBody>('/studenttags', async (req, res) => {
|
||||
const { combinedProgramme, tagId, studentNumbers, studyTrack } = req.body
|
||||
const { roles, programmeRights } = req.user
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import { mapToProviders } from '@oodikone/shared/util'
|
||||
import { serviceProvider } from '../config'
|
||||
import * as auth from '../middleware/auth'
|
||||
import { getProvidersOfFaculty, isFaculty } from '../services/organizations'
|
||||
import { getTeachersBySearchTerm, getTeacherStatistics, getYearlyStatistics } from '../services/teachers'
|
||||
import { CategoryID, getTeacherStats, findAndSaveTeachers, getCategoriesAndYears } from '../services/teachers/top'
|
||||
import { mapToProviders } from '../shared/util'
|
||||
import { getFullStudyProgrammeRights, splitByEmptySpace } from '../util'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
import { Graduated, NameWithCode } from '@oodikone/shared/types'
|
||||
import { serviceProvider } from '../config'
|
||||
import { magicFacultyCode } from '../config/organizationConstants'
|
||||
import { Organization } from '../models'
|
||||
import { OrganizationModel } from '../models'
|
||||
import { getDegreeProgrammesOfFaculty } from '../services/faculty/faculty'
|
||||
import { countGraduationTimes, LevelGraduationStats } from '../services/faculty/facultyGraduationTimes'
|
||||
import { getSortedFaculties } from '../services/faculty/facultyHelpers'
|
||||
@@ -15,16 +16,15 @@ import {
|
||||
} from '../services/faculty/facultyService'
|
||||
import { combineFacultyStudentProgress, FacultyProgressData } from '../services/faculty/facultyStudentProgress'
|
||||
import { getMedian } from '../services/studyProgramme/studyProgrammeHelpers'
|
||||
import { Graduated, NameWithCode } from '../shared/types'
|
||||
import logger from '../util/logger'
|
||||
|
||||
const router = Router()
|
||||
|
||||
const degreeNames = ['bachelor', 'bachelorMaster', 'master', 'doctor'] as const
|
||||
|
||||
const getProgrammeNames = (faculties: Organization[]) => {
|
||||
const getProgrammeNames = (faculties: OrganizationModel[]) => {
|
||||
return faculties.reduce<Record<string, NameWithCode>>((obj, faculty) => {
|
||||
const { name, code } = faculty.dataValues
|
||||
const { name, code } = faculty.toJSON()
|
||||
obj[faculty.code] = { code, ...name }
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import { LANGUAGE_CODES, Language } from '@oodikone/shared/language'
|
||||
import { Role } from '@oodikone/shared/types'
|
||||
import { tryCatch } from '@oodikone/shared/util'
|
||||
import { roles } from '../config/roles'
|
||||
import * as auth from '../middleware/auth'
|
||||
import { sendNotificationAboutAccessToUser, previewNotificationAboutAccessToUser } from '../services/mailService'
|
||||
import * as userService from '../services/userService'
|
||||
import { LANGUAGE_CODES, Language } from '../shared/language'
|
||||
import { Role } from '../shared/types'
|
||||
import logger from '../util/logger'
|
||||
|
||||
const router = Router()
|
||||
@@ -26,13 +27,11 @@ interface UidRequest extends Request {
|
||||
}
|
||||
|
||||
router.get('/:uid', auth.roles(['admin']), async (req: UidRequest, res: Response) => {
|
||||
try {
|
||||
const { uid } = req.params
|
||||
const user = await userService.findOne(uid)
|
||||
return res.json(user)
|
||||
} catch (error: any) {
|
||||
return res.status(400).json({ error: error.message })
|
||||
}
|
||||
const { uid } = req.params
|
||||
const { data: user, error } = await tryCatch(userService.findOne(uid))
|
||||
if (error) return res.status(400).json({ error: error.message })
|
||||
|
||||
return res.json(user)
|
||||
})
|
||||
|
||||
interface ModifyRolesRequest extends Request {
|
||||
@@ -44,12 +43,13 @@ interface ModifyRolesRequest extends Request {
|
||||
|
||||
router.post('/modify-roles', auth.roles(['admin']), async (req: ModifyRolesRequest, res: Response) => {
|
||||
const { username, roles } = req.body
|
||||
try {
|
||||
await userService.modifyAccess(username, roles)
|
||||
res.status(204).end()
|
||||
} catch (error) {
|
||||
res.status(400).json(error)
|
||||
const { error } = await tryCatch(userService.modifyAccess(username, roles))
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json(error)
|
||||
}
|
||||
|
||||
return res.status(204).end()
|
||||
})
|
||||
|
||||
router.get('/email/preview', auth.roles(['admin']), (_req: Request, res: Response) => {
|
||||
@@ -64,18 +64,14 @@ interface EmailRequest extends Request {
|
||||
}
|
||||
|
||||
router.post('/email', auth.roles(['admin']), async (req: EmailRequest, res: Response) => {
|
||||
const userEmail = req.body.email
|
||||
if (!userEmail) {
|
||||
return res.status(400).json({ error: 'Email address is missing' })
|
||||
}
|
||||
const { email } = req.body
|
||||
if (!email) return res.status(400).json({ error: 'Email address is missing' })
|
||||
|
||||
const { error } = await tryCatch(sendNotificationAboutAccessToUser(email))
|
||||
if (error) return res.status(500).json({ error: error.message })
|
||||
|
||||
try {
|
||||
await sendNotificationAboutAccessToUser(userEmail)
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ error: error.message })
|
||||
}
|
||||
logger.info('Succesfully sent message about Oodikone access to user')
|
||||
res.status(200).end()
|
||||
return res.status(200).end()
|
||||
})
|
||||
|
||||
interface ElementsRequest extends Request {
|
||||
@@ -108,18 +104,16 @@ interface ChangeLanguageRequest extends Request {
|
||||
}
|
||||
|
||||
router.post('/language', async (req: ChangeLanguageRequest, res: Response) => {
|
||||
const {
|
||||
body: { language },
|
||||
} = req
|
||||
const { language } = req.body
|
||||
|
||||
if (!LANGUAGE_CODES.includes(language)) {
|
||||
return res.status(400).json('Invalid language')
|
||||
}
|
||||
try {
|
||||
await userService.updateUser(req.user.username, { language })
|
||||
return res.status(204).end()
|
||||
} catch (error: any) {
|
||||
return res.status(500).json({ error: error.message })
|
||||
}
|
||||
|
||||
const { error } = await tryCatch(userService.updateUser(req.user.username, { language }))
|
||||
if (error) return res.status(500).json({ error: error.message })
|
||||
|
||||
return res.status(204).end()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import moment from 'moment'
|
||||
|
||||
import { Graduated, SpecialGroups, YearType } from '@oodikone/shared/types'
|
||||
import { facultyCodes, ignoredFacultyCodes } from '../config/organizationConstants'
|
||||
import { Graduated, SpecialGroups, YearType } from '../shared/types'
|
||||
import { redisClient } from './redis'
|
||||
import { isRelevantProgramme } from './studyProgramme/studyProgrammeHelpers'
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { col, fn, Op, where } from 'sequelize'
|
||||
|
||||
import { Course, Credit, Enrollment, Student, Studyplan } from '../models/index'
|
||||
import { CreditTypeCode, EnrollmentState } from '../types'
|
||||
import { CreditTypeCode, EnrollmentState } from '@oodikone/shared/types'
|
||||
import { omitKeys } from '@oodikone/shared/util'
|
||||
import { CourseModel, CreditModel, EnrollmentModel, StudentModel, StudyplanModel } from '../models'
|
||||
|
||||
type Courses = Array<Pick<Course, 'code' | 'name' | 'substitutions'>>
|
||||
export type Courses = Array<Pick<CourseModel, 'code' | 'name' | 'substitutions'>>
|
||||
|
||||
interface StudentCredit {
|
||||
date: Date
|
||||
@@ -37,13 +38,16 @@ type StudentCredits = Record<
|
||||
}
|
||||
>
|
||||
|
||||
interface FormattedStudent extends StudentInfo {
|
||||
export interface FormattedStudent extends StudentInfo {
|
||||
studentNumber: string
|
||||
allEnrollments: StudentEnrollment[]
|
||||
}
|
||||
|
||||
interface StudentWithStudyplanNested
|
||||
extends Pick<Student, 'studentnumber' | 'firstnames' | 'lastname' | 'email' | 'sis_person_id' | 'secondary_email'> {
|
||||
extends Pick<
|
||||
StudentModel,
|
||||
'studentnumber' | 'firstnames' | 'lastname' | 'email' | 'sis_person_id' | 'secondary_email'
|
||||
> {
|
||||
studyplans: { included_courses: string[] }[]
|
||||
}
|
||||
|
||||
@@ -53,7 +57,7 @@ interface StudentWithCourses extends Omit<StudentWithStudyplanNested, 'studyplan
|
||||
|
||||
const getCourses = async (courseCodes: string[]) => {
|
||||
const courses: Courses = (
|
||||
await Course.findAll({
|
||||
await CourseModel.findAll({
|
||||
attributes: ['code', 'name', 'substitutions'],
|
||||
where: where(fn('LOWER', col('code')), {
|
||||
[Op.in]: courseCodes.map(code => code.toLowerCase()),
|
||||
@@ -69,18 +73,19 @@ const getCredits = async (
|
||||
courseCodes: string[],
|
||||
studentNumbers: string[]
|
||||
) => {
|
||||
const credits: Array<Pick<Credit, 'course_code' | 'student_studentnumber' | 'credittypecode' | 'attainment_date'>> =
|
||||
await Credit.findAll({
|
||||
attributes: ['course_code', 'student_studentnumber', 'credittypecode', 'attainment_date'],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: fullCourseCodes,
|
||||
},
|
||||
student_studentnumber: {
|
||||
[Op.in]: studentNumbers,
|
||||
},
|
||||
const credits: Array<
|
||||
Pick<CreditModel, 'course_code' | 'student_studentnumber' | 'credittypecode' | 'attainment_date'>
|
||||
> = await CreditModel.findAll({
|
||||
attributes: ['course_code', 'student_studentnumber', 'credittypecode', 'attainment_date'],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: fullCourseCodes,
|
||||
},
|
||||
})
|
||||
student_studentnumber: {
|
||||
[Op.in]: studentNumbers,
|
||||
},
|
||||
},
|
||||
})
|
||||
const formattedCredits = credits.map(credit => {
|
||||
const originalCode = courseCodes.includes(credit.course_code)
|
||||
? null
|
||||
@@ -97,8 +102,8 @@ const getCredits = async (
|
||||
}
|
||||
|
||||
const getEnrollments = async (courses: Courses, fullCourseCodes: string[], studentNumbers: string[]) => {
|
||||
const enrollments: Array<Pick<Enrollment, 'course_code' | 'enrollment_date_time' | 'studentnumber'>> =
|
||||
await Enrollment.findAll({
|
||||
const enrollments: Array<Pick<EnrollmentModel, 'course_code' | 'enrollment_date_time' | 'studentnumber'>> =
|
||||
await EnrollmentModel.findAll({
|
||||
attributes: ['course_code', 'enrollment_date_time', 'studentnumber'],
|
||||
where: {
|
||||
course_code: {
|
||||
@@ -125,7 +130,7 @@ const getEnrollments = async (courses: Courses, fullCourseCodes: string[], stude
|
||||
}
|
||||
|
||||
const getStudents = async (studentNumbers: string[]) => {
|
||||
const students = await Student.findAll({
|
||||
const students = await StudentModel.findAll({
|
||||
attributes: ['studentnumber', 'firstnames', 'lastname', 'email', 'sis_person_id', 'secondary_email'],
|
||||
where: {
|
||||
studentnumber: {
|
||||
@@ -134,7 +139,7 @@ const getStudents = async (studentNumbers: string[]) => {
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Studyplan,
|
||||
model: StudyplanModel,
|
||||
as: 'studyplans',
|
||||
attributes: ['included_courses'],
|
||||
},
|
||||
@@ -148,7 +153,10 @@ const getStudents = async (studentNumbers: string[]) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const getCompletedCourses = async (studentNumbers: string[], courseCodes: string[]) => {
|
||||
export const getCompletedCourses = async (
|
||||
studentNumbers: string[],
|
||||
courseCodes: string[]
|
||||
): Promise<{ students: Omit<FormattedStudent, 'allEnrollments'>[]; courses: Courses }> => {
|
||||
const courses = await getCourses(courseCodes)
|
||||
const courseCodesSet = new Set(courseCodes)
|
||||
courses.forEach(course => {
|
||||
@@ -244,7 +252,7 @@ export const getCompletedCourses = async (studentNumbers: string[], courseCodes:
|
||||
// Omit allEnrollments, we're only supposed to show the recent, relevant enrollment,
|
||||
// the user does not have rights to see all enrollments.
|
||||
return {
|
||||
students: students.map(({ allEnrollments, ...rest }) => rest),
|
||||
students: students.map(student => omitKeys(student, ['allEnrollments'])),
|
||||
courses,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { orderBy } from 'lodash'
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
import { Course } from '../../models'
|
||||
import { CourseModel } from '../../models'
|
||||
import { CourseWithSubsId } from '../../types'
|
||||
import { getSortRank } from '../../util/sortRank'
|
||||
|
||||
@@ -41,7 +41,7 @@ const codeLikeTerm = (code: string) => {
|
||||
}
|
||||
|
||||
const getRawCourses = async (name: string, code: string) => {
|
||||
return await Course.findAll({
|
||||
return await CourseModel.findAll({
|
||||
where: {
|
||||
...nameLikeTerm(name),
|
||||
...codeLikeTerm(code),
|
||||
@@ -102,7 +102,7 @@ export const getCoursesByNameAndOrCode = async (name: string, code: string) => {
|
||||
}
|
||||
|
||||
export const getCoursesByCodes = (codes: string[]) => {
|
||||
return Course.findAll({
|
||||
return CourseModel.findAll({
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: codes,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Name } from '../../shared/types'
|
||||
import { EnrollmentState } from '../../types'
|
||||
import { lengthOf, percentageOf } from '../../util'
|
||||
import { Name, EnrollmentState } from '@oodikone/shared/types'
|
||||
|
||||
const lengthOf = (obj: object) => Object.keys(obj).length
|
||||
const percentageOf = (num: number, denom: number) => {
|
||||
if (denom === 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.round(((100 * num) / denom) * 100) / 100
|
||||
}
|
||||
|
||||
const fall: string[] = []
|
||||
const spring: string[] = []
|
||||
@@ -95,15 +101,13 @@ export type CourseStatistics = {
|
||||
}
|
||||
|
||||
export class CourseStatsCounter {
|
||||
private studentsInTotal: number
|
||||
private course: Course
|
||||
private students: Students
|
||||
private stats: Stats
|
||||
private enrollments: DynamicEnrollments
|
||||
private grades: Grades
|
||||
|
||||
constructor(code: string, name: Name, studentsInTotal: number) {
|
||||
this.studentsInTotal = studentsInTotal
|
||||
constructor(code: string, name: Name) {
|
||||
this.course = {
|
||||
code,
|
||||
name,
|
||||
@@ -176,16 +180,10 @@ export class CourseStatsCounter {
|
||||
}
|
||||
|
||||
public markEnrollment(studentNumber: string, state: EnrollmentState, semester: string) {
|
||||
if (!this.enrollments[state]) {
|
||||
this.enrollments[state] = new Set()
|
||||
}
|
||||
this.enrollments[state] ??= new Set()
|
||||
this.enrollments[state].add(studentNumber)
|
||||
if (!this.enrollments.semesters[semester]) {
|
||||
this.enrollments.semesters[semester] = {}
|
||||
}
|
||||
if (!this.enrollments.semesters[semester][state]) {
|
||||
this.enrollments.semesters[semester][state] = new Set()
|
||||
}
|
||||
this.enrollments.semesters[semester] ??= {}
|
||||
this.enrollments.semesters[semester][state] ??= new Set()
|
||||
this.enrollments.semesters[semester][state].add(studentNumber)
|
||||
}
|
||||
|
||||
@@ -233,10 +231,9 @@ export class CourseStatsCounter {
|
||||
|
||||
private getPassingSemestersCumulative() {
|
||||
const { passingSemesters } = this.stats
|
||||
const attemptStats: Record<string, number> = {
|
||||
BEFORE: passingSemesters.BEFORE,
|
||||
}
|
||||
const attemptStats: Record<string, number> = {}
|
||||
|
||||
attemptStats.BEFORE = passingSemesters.BEFORE
|
||||
attemptStats['0-FALL'] = passingSemesters.BEFORE + passingSemesters['0-FALL']
|
||||
attemptStats['0-SPRING'] = attemptStats['0-FALL'] + passingSemesters['0-SPRING']
|
||||
|
||||
@@ -250,8 +247,8 @@ export class CourseStatsCounter {
|
||||
return attemptStats
|
||||
}
|
||||
|
||||
public getFinalStats() {
|
||||
const { stats, students } = this
|
||||
public getFinalStats(populationCount: number): CourseStatistics {
|
||||
const { stats, students, course, grades } = this
|
||||
const allStudents = Object.keys(students.all)
|
||||
const filteredEnrolledNoGrade = getFilteredEnrolledNoGrade(this.enrollments, allStudents)
|
||||
|
||||
@@ -262,14 +259,14 @@ export class CourseStatsCounter {
|
||||
stats.failed = lengthOf(students.failed)
|
||||
stats.improvedPassedGrade = lengthOf(students.improvedPassedGrade)
|
||||
stats.percentage = percentageOf(stats.passed, stats.students)
|
||||
stats.passedOfPopulation = percentageOf(stats.passed, this.studentsInTotal)
|
||||
stats.triedOfPopulation = percentageOf(stats.students, this.studentsInTotal)
|
||||
stats.passedOfPopulation = percentageOf(stats.passed, populationCount)
|
||||
stats.triedOfPopulation = percentageOf(stats.students, populationCount)
|
||||
stats.perStudent = percentageOf(stats.attempts, stats.passed + stats.failed) / 100
|
||||
stats.passingSemestersCumulative = this.getPassingSemestersCumulative()
|
||||
stats.totalStudents = stats.students
|
||||
stats.totalEnrolledNoGrade = lengthOf(filteredEnrolledNoGrade)
|
||||
stats.percentageWithEnrollments = percentageOf(stats.passed, stats.totalStudents)
|
||||
|
||||
return { stats, students, course: this.course, grades: this.grades } as CourseStatistics
|
||||
return { stats, students, course, grades }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { max, min } from 'lodash'
|
||||
|
||||
import { Name } from '../../shared/types'
|
||||
import { Name } from '@oodikone/shared/types'
|
||||
import { getSemesterNameByCode } from '../semesters'
|
||||
import { FormattedProgramme, OrganizationDetails } from './helpers'
|
||||
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
import { Credit, Student, Semester, Organization, Enrollment, SISStudyRight, SISStudyRightElement } from '../../models'
|
||||
import { EnrollmentState, Unification } from '../../types'
|
||||
import { EnrollmentState } from '@oodikone/shared/types'
|
||||
import {
|
||||
CreditModel,
|
||||
StudentModel,
|
||||
SemesterModel,
|
||||
OrganizationModel,
|
||||
EnrollmentModel,
|
||||
SISStudyRightModel,
|
||||
SISStudyRightElementModel,
|
||||
} from '../../models'
|
||||
import { Unification } from '../../types'
|
||||
import { getIsOpen } from './helpers'
|
||||
|
||||
export const getCreditsForCourses = async (codes: string[], unification: Unification) => {
|
||||
return await Credit.findAll({
|
||||
return await CreditModel.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
model: StudentModel,
|
||||
attributes: ['studentnumber'],
|
||||
},
|
||||
{
|
||||
model: Semester,
|
||||
model: SemesterModel,
|
||||
attributes: ['semestercode', 'name', 'yearcode', 'yearname'],
|
||||
where: {
|
||||
startdate: {
|
||||
@@ -32,7 +41,7 @@ export const getCreditsForCourses = async (codes: string[], unification: Unifica
|
||||
}
|
||||
|
||||
export const getStudentNumberToSrElementsMap = async (studentNumbers: string[]) => {
|
||||
const studyRights = await SISStudyRight.findAll({
|
||||
const studyRights = await SISStudyRightModel.findAll({
|
||||
attributes: ['facultyCode', 'id', 'studentNumber'],
|
||||
where: {
|
||||
studentNumber: {
|
||||
@@ -40,7 +49,7 @@ export const getStudentNumberToSrElementsMap = async (studentNumbers: string[])
|
||||
},
|
||||
},
|
||||
include: {
|
||||
model: Organization,
|
||||
model: OrganizationModel,
|
||||
attributes: ['name', 'code'],
|
||||
},
|
||||
})
|
||||
@@ -52,7 +61,7 @@ export const getStudentNumberToSrElementsMap = async (studentNumbers: string[])
|
||||
|
||||
const studyRightIds = Object.keys(studyRightMap)
|
||||
|
||||
const studyRightElements = await SISStudyRightElement.findAll({
|
||||
const studyRightElements = await SISStudyRightElementModel.findAll({
|
||||
attributes: ['code', 'name', 'startDate', 'endDate', 'studyRightId'],
|
||||
where: {
|
||||
studyRightId: {
|
||||
@@ -64,9 +73,7 @@ export const getStudentNumberToSrElementsMap = async (studentNumbers: string[])
|
||||
const studentNumberToSrElementsMap = studyRightElements.reduce((obj, cur) => {
|
||||
const studyRight = studyRightMap[cur.studyRightId]
|
||||
const { studentNumber } = studyRight
|
||||
if (!obj[studentNumber]) {
|
||||
obj[studentNumber] = []
|
||||
}
|
||||
obj[studentNumber] ??= []
|
||||
obj[studentNumber].push({ ...cur.toJSON(), studyRight })
|
||||
return obj
|
||||
}, {})
|
||||
@@ -74,14 +81,14 @@ export const getStudentNumberToSrElementsMap = async (studentNumbers: string[])
|
||||
}
|
||||
|
||||
export const getEnrollmentsForCourses = async (codes: string[], unification: Unification) => {
|
||||
return await Enrollment.findAll({
|
||||
return await EnrollmentModel.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
model: StudentModel,
|
||||
attributes: ['studentnumber'],
|
||||
},
|
||||
{
|
||||
model: Semester,
|
||||
model: SemesterModel,
|
||||
attributes: ['semestercode', 'name', 'yearcode', 'yearname'],
|
||||
where: {
|
||||
startdate: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
import { Name } from '../../shared/types'
|
||||
import { Name } from '@oodikone/shared/types'
|
||||
import { Unification } from '../../types'
|
||||
|
||||
export type FormattedProgramme = {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import crypto from 'crypto'
|
||||
import moment from 'moment'
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
import { Course, Credit, Enrollment, Organization, SISStudyRightElement } from '../../models'
|
||||
import { Name } from '../../shared/types'
|
||||
import { EnrollmentState, Unification } from '../../types'
|
||||
import { Name, EnrollmentState } from '@oodikone/shared/types'
|
||||
import { CourseModel, CreditModel, EnrollmentModel, OrganizationModel, SISStudyRightElementModel } from '../../models'
|
||||
import { Unification } from '../../types'
|
||||
import { isOpenUniCourseCode } from '../../util'
|
||||
import { dateIsBetween } from '../../util/datetime'
|
||||
import { getSortRank } from '../../util/sortRank'
|
||||
import { CourseYearlyStatsCounter } from './courseYearlyStatsCounter'
|
||||
import {
|
||||
@@ -22,7 +22,7 @@ const sortMainCode = (codes: string[]) => {
|
||||
return codes.sort((a, b) => getSortRank(b) - getSortRank(a))
|
||||
}
|
||||
|
||||
const formatStudyRightElement = (studyRightElement: SISStudyRightElement): FormattedProgramme => ({
|
||||
const formatStudyRightElement = (studyRightElement: SISStudyRightElementModel): FormattedProgramme => ({
|
||||
code: studyRightElement.code,
|
||||
name: studyRightElement.name,
|
||||
startDate: studyRightElement.startDate,
|
||||
@@ -55,9 +55,9 @@ type FormattedCredit = {
|
||||
}
|
||||
|
||||
const parseCredit = (
|
||||
credit: Credit,
|
||||
credit: CreditModel,
|
||||
anonymizationSalt: string | null,
|
||||
studentNumberToSrElementsMap: Record<string, Array<SISStudyRightElement>>
|
||||
studentNumberToSrElementsMap: Record<string, Array<SISStudyRightElementModel>>
|
||||
) => {
|
||||
const {
|
||||
semester,
|
||||
@@ -71,15 +71,13 @@ const parseCredit = (
|
||||
const { yearcode: yearCode, yearname: yearName, semestercode: semesterCode, name: semesterName } = semester
|
||||
|
||||
const studyRightElements = studentNumberToSrElementsMap[studentNumber] || []
|
||||
let programmeOfCredit: SISStudyRightElement | undefined
|
||||
programmeOfCredit = studyRightElements.find(studyRightElement => studyRightElement.studyRightId === studyRightId)
|
||||
if (!programmeOfCredit) {
|
||||
programmeOfCredit = studyRightElements
|
||||
const programmeOfCredit: SISStudyRightElementModel | undefined =
|
||||
studyRightElements.find(studyRightElement => studyRightElement.studyRightId === studyRightId) ??
|
||||
studyRightElements
|
||||
.filter(studyRightElement =>
|
||||
moment(attainmentDate).isBetween(moment(studyRightElement.startDate), moment(studyRightElement.endDate))
|
||||
dateIsBetween(attainmentDate, studyRightElement.startDate, studyRightElement.endDate)
|
||||
)
|
||||
.sort((a, b) => b.startDate.getTime() - a.startDate.getTime())[0] // The newest studyRightElement
|
||||
}
|
||||
|
||||
const programmes = programmeOfCredit ? [programmeOfCredit].map(formatStudyRightElement) : []
|
||||
|
||||
@@ -91,7 +89,7 @@ const parseCredit = (
|
||||
attainmentDate,
|
||||
courseCode,
|
||||
grade,
|
||||
passed: !Credit.failed(credit) || Credit.passed(credit) || Credit.improved(credit),
|
||||
passed: !CreditModel.failed(credit) || CreditModel.passed(credit) || CreditModel.improved(credit),
|
||||
studentNumber,
|
||||
programmes,
|
||||
credits,
|
||||
@@ -118,9 +116,9 @@ type FormattedEnrollment = {
|
||||
}
|
||||
|
||||
const parseEnrollment = (
|
||||
enrollment: Enrollment,
|
||||
enrollment: EnrollmentModel,
|
||||
anonymizationSalt: string | null,
|
||||
studentNumberToSrElementsMap: Record<string, SISStudyRightElement[]>
|
||||
studentNumberToSrElementsMap: Record<string, SISStudyRightElementModel[]>
|
||||
) => {
|
||||
const {
|
||||
studentnumber: studentNumber,
|
||||
@@ -157,9 +155,9 @@ const getYearlyStatsOfNew = async (
|
||||
unification: Unification,
|
||||
anonymizationSalt: string | null,
|
||||
combineSubstitutions: boolean,
|
||||
studentNumberToSrElementsMap: Record<string, SISStudyRightElement[]>
|
||||
studentNumberToSrElementsMap: Record<string, SISStudyRightElementModel[]>
|
||||
) => {
|
||||
const courseForSubs = await Course.findOne({
|
||||
const courseForSubs = await CourseModel.findOne({
|
||||
where: { code: courseCode },
|
||||
})
|
||||
|
||||
@@ -175,7 +173,7 @@ const getYearlyStatsOfNew = async (
|
||||
const [credits, enrollments, course] = await Promise.all([
|
||||
getCreditsForCourses(codes, unification),
|
||||
getEnrollmentsForCourses(codes, unification),
|
||||
Course.findOne({
|
||||
CourseModel.findOne({
|
||||
where: {
|
||||
code: courseCode,
|
||||
},
|
||||
@@ -242,9 +240,9 @@ const getYearlyStatsOfNew = async (
|
||||
|
||||
const statistics = await counter.getFinalStatistics(anonymizationSalt)
|
||||
|
||||
let substitutionCourses: Course[] | undefined
|
||||
let substitutionCourses: CourseModel[] | undefined
|
||||
if (combineSubstitutions && courseForSubs?.substitutions && courseForSubs.substitutions.length > 0) {
|
||||
substitutionCourses = await Course.findAll({
|
||||
substitutionCourses = await CourseModel.findAll({
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: codes,
|
||||
@@ -266,7 +264,7 @@ export const maxYearsToCreatePopulationFrom = async (courseCodes: string[], unif
|
||||
const maxAttainmentDate = new Date(
|
||||
Math.max(
|
||||
...(
|
||||
await Course.findAll({
|
||||
await CourseModel.findAll({
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: courseCodes,
|
||||
@@ -281,7 +279,7 @@ export const maxYearsToCreatePopulationFrom = async (courseCodes: string[], unif
|
||||
const attainmentThreshold = new Date(maxAttainmentDate.getFullYear(), 0, 1)
|
||||
attainmentThreshold.setFullYear(attainmentThreshold.getFullYear() - 6)
|
||||
|
||||
const credits = await Credit.findAll({
|
||||
const credits = await CreditModel.findAll({
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: courseCodes,
|
||||
@@ -324,11 +322,11 @@ export const getCourseYearlyStats = async (
|
||||
anonymizationSalt: string | null,
|
||||
combineSubstitutions: boolean
|
||||
) => {
|
||||
const credits = await Credit.findAll({
|
||||
const credits = await CreditModel.findAll({
|
||||
attributes: ['student_studentnumber'],
|
||||
where: { course_code: { [Op.in]: courseCodes } },
|
||||
})
|
||||
const enrollments = await Enrollment.findAll({
|
||||
const enrollments = await EnrollmentModel.findAll({
|
||||
attributes: ['studentnumber'],
|
||||
where: {
|
||||
course_code: {
|
||||
@@ -385,10 +383,10 @@ export const getCourseYearlyStats = async (
|
||||
|
||||
export const getCourseProvidersForCourses = async (codes: string[]) => {
|
||||
return (
|
||||
await Organization.findAll({
|
||||
await OrganizationModel.findAll({
|
||||
attributes: ['code'],
|
||||
include: {
|
||||
model: Course,
|
||||
model: CourseModel,
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: codes,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { CurriculumPeriod } from '../models'
|
||||
import { CurriculumPeriodModel } from '../models'
|
||||
|
||||
export type CurriculumPeriods = Awaited<ReturnType<typeof getCurriculumPeriods>>
|
||||
|
||||
export const getCurriculumPeriods = async () => {
|
||||
const curriculumPeriods = await CurriculumPeriod.findAll()
|
||||
const curriculumPeriods = await CurriculumPeriodModel.findAll()
|
||||
const result = curriculumPeriods.reduce<
|
||||
Record<string, Pick<CurriculumPeriod, 'id' | 'name' | 'universityOrgId' | 'startDate' | 'endDate'>>
|
||||
Record<string, Pick<CurriculumPeriodModel, 'id' | 'name' | 'universityOrgId' | 'startDate' | 'endDate'>>
|
||||
>((acc, curriculumPeriod) => {
|
||||
const { id, name, universityOrgId, startDate, endDate } = curriculumPeriod
|
||||
acc[id] = { id, name, universityOrgId, startDate, endDate }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CustomPopulationSearch } from '../models/kone'
|
||||
import { CustomPopulationSearchModel } from '../models/kone'
|
||||
|
||||
export const getCustomPopulationSearchesByUser = async (userId: string) => {
|
||||
return CustomPopulationSearch.findAll({
|
||||
return CustomPopulationSearchModel.findAll({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
@@ -9,7 +9,7 @@ export const getCustomPopulationSearchesByUser = async (userId: string) => {
|
||||
}
|
||||
|
||||
export const createCustomPopulationSearch = async (name: string, userId: string, students: string[]) => {
|
||||
return CustomPopulationSearch.create({
|
||||
return CustomPopulationSearchModel.create({
|
||||
name,
|
||||
userId,
|
||||
students,
|
||||
@@ -17,7 +17,7 @@ export const createCustomPopulationSearch = async (name: string, userId: string,
|
||||
}
|
||||
|
||||
export const updateCustomPopulationSearch = async (userId: string, id: string, students: string[]) => {
|
||||
const targetCustomPopulationSearch = await CustomPopulationSearch.findOne({
|
||||
const targetCustomPopulationSearch = await CustomPopulationSearchModel.findOne({
|
||||
where: {
|
||||
id,
|
||||
userId,
|
||||
@@ -34,7 +34,7 @@ export const updateCustomPopulationSearch = async (userId: string, id: string, s
|
||||
}
|
||||
|
||||
export const deleteCustomPopulationSearch = async (userId: string, id: string) => {
|
||||
return CustomPopulationSearch.destroy({
|
||||
return CustomPopulationSearchModel.destroy({
|
||||
where: {
|
||||
id,
|
||||
userId,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
import { ExcludedCourse } from '../models/kone'
|
||||
import { ExcludedCourseModel } from '../models/kone'
|
||||
|
||||
export const addExcludedCourses = async (programmeCode: string, courseCodes: string[], curriculumVersion: string) => {
|
||||
return ExcludedCourse.bulkCreate(
|
||||
export const addExcludedCourses = async (courseCodes: string[], curriculumVersion: string, programmeCode: string) => {
|
||||
return ExcludedCourseModel.bulkCreate(
|
||||
courseCodes.map(courseCode => ({
|
||||
programme_code: programmeCode,
|
||||
curriculum_version: curriculumVersion,
|
||||
@@ -13,11 +13,11 @@ export const addExcludedCourses = async (programmeCode: string, courseCodes: str
|
||||
}
|
||||
|
||||
export const removeExcludedCourses = async (
|
||||
programmeCode: string,
|
||||
courseCodes: string[],
|
||||
curriculumVersion: string
|
||||
curriculumVersion: string,
|
||||
programmeCode: string
|
||||
) => {
|
||||
return ExcludedCourse.destroy({
|
||||
return ExcludedCourseModel.destroy({
|
||||
where: {
|
||||
programme_code: programmeCode,
|
||||
curriculum_version: curriculumVersion,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { groupBy, orderBy } from 'lodash'
|
||||
import moment from 'moment'
|
||||
import { InferAttributes, QueryTypes } from 'sequelize'
|
||||
import { QueryTypes } from 'sequelize'
|
||||
|
||||
import { ProgrammeModule } from '@oodikone/shared/models'
|
||||
import { serviceProvider } from '../../config'
|
||||
import { programmeCodes } from '../../config/programmeCodes'
|
||||
import { dbConnections } from '../../database/connection'
|
||||
import { Organization, ProgrammeModule } from '../../models'
|
||||
import { OrganizationModel } from '../../models'
|
||||
import { CurriculumPeriods, getCurriculumPeriods } from '../curriculumPeriods'
|
||||
|
||||
const { sequelize } = dbConnections
|
||||
@@ -20,10 +21,10 @@ const mapCurriculumPeriodIdToYear = (curriculumPeriodId: string, curriculumPerio
|
||||
return { startDate: new Date('1800-08-01'), endDate: new Date('1801-08-01') }
|
||||
}
|
||||
|
||||
type ProgrammeModuleWithRelevantAttributes = Pick<
|
||||
InferAttributes<ProgrammeModule>,
|
||||
'code' | 'name' | 'degreeProgrammeType'
|
||||
> & { curriculumPeriodIds: string[]; progId: string }
|
||||
type ProgrammeModuleWithRelevantAttributes = Pick<ProgrammeModule, 'code' | 'name' | 'degreeProgrammeType'> & {
|
||||
curriculumPeriodIds: string[]
|
||||
progId: string
|
||||
}
|
||||
|
||||
// Some programme modules are not directly associated to a faculty (organization).
|
||||
// Some have intermediate organizations, such as department, so the connection must be digged up
|
||||
@@ -103,7 +104,7 @@ export const getDegreeProgrammesOfOrganization = async (organizationId: string,
|
||||
export type ProgrammesOfOrganization = Awaited<ReturnType<typeof getDegreeProgrammesOfOrganization>>
|
||||
|
||||
export const getDegreeProgrammesOfFaculty = async (facultyCode: string, onlyCurrentProgrammes: boolean) => {
|
||||
const organization = await Organization.findOne({
|
||||
const organization = await OrganizationModel.findOne({
|
||||
attributes: ['id'],
|
||||
where: {
|
||||
code: facultyCode,
|
||||
@@ -116,7 +117,7 @@ export const getDegreeProgrammesOfFaculty = async (facultyCode: string, onlyCurr
|
||||
}
|
||||
|
||||
export const getFacultyCodeById = async (facultyId: string) => {
|
||||
const organization = await Organization.findOne({
|
||||
const organization = await OrganizationModel.findOne({
|
||||
attributes: ['code'],
|
||||
where: {
|
||||
id: facultyId,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
import { NameWithCode } from '../../shared/types'
|
||||
import { DegreeProgrammeType } from '../../types'
|
||||
import { NameWithCode, DegreeProgrammeType } from '@oodikone/shared/types'
|
||||
import { getBasicStats, setBasicStats } from '../analyticsService'
|
||||
import { getBasicStatsForStudytrack } from '../studyProgramme/studyProgrammeBasics'
|
||||
import type { ProgrammesOfOrganization } from './faculty'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Name } from '../../shared/types'
|
||||
import { Name } from '@oodikone/shared/types'
|
||||
import { getCreditsProduced } from '../providerCredits'
|
||||
import { isRelevantProgramme } from '../studyProgramme/studyProgrammeHelpers'
|
||||
import { getDegreeProgrammesOfFaculty } from './faculty'
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { cloneDeep, omit } from 'lodash'
|
||||
|
||||
import { Name } from '../../shared/types'
|
||||
import { DegreeProgrammeType } from '../../types'
|
||||
import { Name, DegreeProgrammeType } from '@oodikone/shared/types'
|
||||
import { getGraduationStats, getStudyTrackStats, setGraduationStats, setStudyTrackStats } from '../analyticsService'
|
||||
import { getGraduationStatsForStudyTrack, GraduationTimes } from '../studyProgramme/studyProgrammeGraduations'
|
||||
import { getMedian } from '../studyProgramme/studyProgrammeHelpers'
|
||||
import { getStudyRightsInProgramme } from '../studyProgramme/studyRightFinders'
|
||||
import {
|
||||
getStudyTrackStatsForStudyProgramme,
|
||||
ProgrammeOrStudyTrackGraduationStats,
|
||||
} from '../studyProgramme/studyTrackStats'
|
||||
import { getStudyTrackStatsForStudyProgramme } from '../studyProgramme/studyTrackStats'
|
||||
import type { ProgrammesOfOrganization } from './faculty'
|
||||
|
||||
export type LevelGraduationStats = Omit<GraduationTimes['medians'][number], 'y'> & { median: number }
|
||||
@@ -161,7 +157,7 @@ const getStatsByStartYear = async (facultyProgrammes: ProgrammesOfOrganization)
|
||||
const { degreeProgrammeType, progId, code } = facultyProgrammes.find(({ code }) => code === programmeCode)!
|
||||
const level = programmeTypes[degreeProgrammeType as keyof typeof programmeTypes]
|
||||
if (!(programmeCode in graduationTimes)) continue
|
||||
const statsForProgramme = graduationTimes[programmeCode] as ProgrammeOrStudyTrackGraduationStats
|
||||
const statsForProgramme = graduationTimes[programmeCode]
|
||||
const basicStats = statsForProgramme.medians.basic
|
||||
const comboStats = statsForProgramme.medians.combo
|
||||
const classSizesByYear = basicStats.reduce<Record<string, number>>(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import moment from 'moment'
|
||||
|
||||
import { Graduated, Name, ProgrammeFilter, SpecialGroups, YearType } from '../../shared/types'
|
||||
import { DegreeProgrammeType } from '../../types'
|
||||
import { Graduated, Name, ProgrammeFilter, SpecialGroups, YearType, DegreeProgrammeType } from '@oodikone/shared/types'
|
||||
import { redisClient } from '../redis'
|
||||
import { FacultyProgressData } from './facultyStudentProgress'
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { cloneDeep } from 'lodash'
|
||||
import moment from 'moment'
|
||||
|
||||
import { Graduated, SpecialGroups } from '@oodikone/shared/types'
|
||||
import { rootOrgId } from '../../config'
|
||||
import { Graduated, SpecialGroups } from '../../shared/types'
|
||||
import { Unarray } from '../../types'
|
||||
import { getStudyTrackStats, setStudyTrackStats } from '../analyticsService'
|
||||
import { getYearsArray } from '../studyProgramme/studyProgrammeHelpers'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
import { DegreeProgrammeType } from '../../types'
|
||||
import { DegreeProgrammeType } from '@oodikone/shared/types'
|
||||
import { getGraduationStats, setGraduationStats } from '../analyticsService'
|
||||
import { getGraduationStatsForStudyTrack } from '../studyProgramme/studyProgrammeGraduations'
|
||||
import { getDegreeProgrammesOfFaculty, ProgrammesOfOrganization } from './faculty'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { StatsType } from '../../shared/types'
|
||||
import { StatsType } from '@oodikone/shared/types'
|
||||
import { setCreditStats } from '../analyticsService'
|
||||
import { computeCreditsProduced } from '../providerCredits'
|
||||
import { getDegreeProgrammesOfFaculty } from './faculty'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { orderBy } = require('lodash')
|
||||
const { Op } = require('sequelize')
|
||||
|
||||
const { Course, Credit, Enrollment, SISStudyRight } = require('../models')
|
||||
const { CourseModel, CreditModel, EnrollmentModel, SISStudyRightModel } = require('../models')
|
||||
const { redisClient } = require('./redis')
|
||||
|
||||
const LANGUAGE_CENTER_REDIS_KEY = 'LANGUAGE_CENTER_DATA'
|
||||
@@ -23,7 +23,7 @@ const getDifference = stats => {
|
||||
}
|
||||
|
||||
const getLanguageCenterCourses = async () => {
|
||||
const courses = await Course.findAll({
|
||||
const courses = await CourseModel.findAll({
|
||||
attributes: ['code', 'name'],
|
||||
where: {
|
||||
[Op.or]: [{ code: { [Op.like]: 'KK%' } }, { code: { [Op.like]: 'AYKK%' } }],
|
||||
@@ -92,7 +92,7 @@ const computeLanguageCenterData = async () => {
|
||||
const courses = await getLanguageCenterCourses()
|
||||
const autumnSemester2017 = 135
|
||||
|
||||
const credits = await Credit.findAll({
|
||||
const credits = await CreditModel.findAll({
|
||||
attributes: ['course_code', 'student_studentnumber', 'semestercode', 'attainment_date', 'studyright_id'],
|
||||
where: {
|
||||
[Op.or]: [{ course_code: { [Op.like]: 'KK%' } }, { course_code: { [Op.like]: 'AYKK%' } }],
|
||||
@@ -102,7 +102,7 @@ const computeLanguageCenterData = async () => {
|
||||
raw: true,
|
||||
})
|
||||
|
||||
const enrollments = await Enrollment.findAll({
|
||||
const enrollments = await EnrollmentModel.findAll({
|
||||
attributes: ['studentnumber', 'semestercode', 'course_code', 'enrollment_date_time', 'studyright_id', 'state'],
|
||||
where: {
|
||||
[Op.or]: [{ course_code: { [Op.like]: 'KK%' } }, { course_code: { [Op.like]: 'AYKK%' } }],
|
||||
@@ -112,7 +112,7 @@ const computeLanguageCenterData = async () => {
|
||||
raw: true,
|
||||
})
|
||||
|
||||
const studyRights = await SISStudyRight.findAll({ raw: true })
|
||||
const studyRights = await SISStudyRightModel.findAll({ raw: true })
|
||||
|
||||
const attemptStudyRightToFacultyMap = studyRights.reduce((obj, cur) => {
|
||||
if (!cur.id) return obj
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Course, Credit, Enrollment, Student } from '../../models'
|
||||
import { CourseModel, CreditModel, EnrollmentModel, StudentModel } from '../../models'
|
||||
|
||||
export const formatCourseInfo = (course: Course) => {
|
||||
export const formatCourseInfo = (course: CourseModel) => {
|
||||
return {
|
||||
label: course.code,
|
||||
name: course.name,
|
||||
}
|
||||
}
|
||||
|
||||
export const formatOpenCredits = (credit: Credit) => {
|
||||
export const formatOpenCredits = (credit: CreditModel) => {
|
||||
return {
|
||||
attainmentCourseCode: credit.course_code,
|
||||
attainmentDate: credit.attainment_date,
|
||||
@@ -16,7 +16,7 @@ export const formatOpenCredits = (credit: Credit) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const formatOpenEnrollments = (enrollment: Enrollment) => {
|
||||
export const formatOpenEnrollments = (enrollment: EnrollmentModel) => {
|
||||
return {
|
||||
enrollmentCourseCode: enrollment.course_code,
|
||||
enrollmentDateTime: enrollment.enrollment_date_time,
|
||||
@@ -24,7 +24,7 @@ export const formatOpenEnrollments = (enrollment: Enrollment) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const formatStudentInfo = (student: Student) => {
|
||||
export const formatStudentInfo = (student: StudentModel) => {
|
||||
return {
|
||||
studentNumber: student.studentnumber,
|
||||
email: student.email,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
deleteOpenUniSearch,
|
||||
} from './openUniSearches'
|
||||
|
||||
type FoundSearch = {
|
||||
export type FoundSearch = {
|
||||
id: string
|
||||
userId: string
|
||||
name: string
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { InferAttributes, Op, WhereOptions } from 'sequelize'
|
||||
import { Op, WhereOptions } from 'sequelize'
|
||||
|
||||
import { Course, Credit, Enrollment, Student, SISStudyRight } from '../../models'
|
||||
import { OpenUniPopulationSearch } from '../../models/kone'
|
||||
import { EnrollmentState, ExtentCode } from '../../types'
|
||||
import { SISStudyRight } from '@oodikone/shared/models'
|
||||
import { EnrollmentState, ExtentCode } from '@oodikone/shared/types'
|
||||
import { CourseModel, CreditModel, EnrollmentModel, StudentModel, SISStudyRightModel } from '../../models'
|
||||
import { OpenUniPopulationSearchModel } from '../../models/kone'
|
||||
import { formatCourseInfo, formatOpenCredits, formatOpenEnrollments, formatStudentInfo } from './format'
|
||||
|
||||
export const getCredits = async (courseCodes: string[], startdate: Date) =>
|
||||
(
|
||||
await Credit.findAll({
|
||||
await CreditModel.findAll({
|
||||
attributes: ['attainment_date', 'course_code', 'grade', 'student_studentnumber'],
|
||||
where: {
|
||||
course_code: {
|
||||
@@ -22,7 +23,7 @@ export const getCredits = async (courseCodes: string[], startdate: Date) =>
|
||||
|
||||
export const getStudentInfo = async (studentNumbers: string[]) =>
|
||||
(
|
||||
await Student.findAll({
|
||||
await StudentModel.findAll({
|
||||
attributes: ['studentnumber', 'email', 'secondary_email'],
|
||||
where: {
|
||||
studentnumber: {
|
||||
@@ -34,7 +35,7 @@ export const getStudentInfo = async (studentNumbers: string[]) =>
|
||||
|
||||
export const getEnrollments = async (courseCodes: string[], startDate: Date, endDate: Date) =>
|
||||
(
|
||||
await Enrollment.findAll({
|
||||
await EnrollmentModel.findAll({
|
||||
attributes: ['course_code', 'enrollment_date_time', 'studentnumber'],
|
||||
where: {
|
||||
course_code: {
|
||||
@@ -54,7 +55,7 @@ export const getEnrollments = async (courseCodes: string[], startDate: Date, end
|
||||
|
||||
export const getCourseNames = async (courseCodes: string[]) =>
|
||||
(
|
||||
await Course.findAll({
|
||||
await CourseModel.findAll({
|
||||
attributes: ['code', 'name'],
|
||||
where: {
|
||||
code: {
|
||||
@@ -65,7 +66,7 @@ export const getCourseNames = async (courseCodes: string[]) =>
|
||||
).map(formatCourseInfo)
|
||||
|
||||
export const getStudyRights = async (studentNumbers: string[]) => {
|
||||
const where: WhereOptions<InferAttributes<SISStudyRight>> = {
|
||||
const where: WhereOptions<SISStudyRight> = {
|
||||
extentCode: {
|
||||
[Op.in]: [
|
||||
ExtentCode.BACHELOR,
|
||||
@@ -83,14 +84,14 @@ export const getStudyRights = async (studentNumbers: string[]) => {
|
||||
}
|
||||
}
|
||||
|
||||
return await SISStudyRight.findAll({
|
||||
return await SISStudyRightModel.findAll({
|
||||
attributes: ['startDate', 'endDate', 'studentNumber'],
|
||||
where,
|
||||
})
|
||||
}
|
||||
|
||||
export const getOpenUniSearchesByUser = async (userId: string) => {
|
||||
return await OpenUniPopulationSearch.findAll({
|
||||
return await OpenUniPopulationSearchModel.findAll({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
@@ -98,7 +99,7 @@ export const getOpenUniSearchesByUser = async (userId: string) => {
|
||||
}
|
||||
|
||||
export const createOpenUniPopulationSearch = async (userId: string, name: string, courseCodes: string[]) => {
|
||||
return await OpenUniPopulationSearch.create({
|
||||
return await OpenUniPopulationSearchModel.create({
|
||||
userId,
|
||||
name,
|
||||
courseCodes,
|
||||
@@ -106,7 +107,7 @@ export const createOpenUniPopulationSearch = async (userId: string, name: string
|
||||
}
|
||||
|
||||
export const updateOpenUniPopulationSearch = async (userId: string, id: string, courseCodes: string[]) => {
|
||||
const searchToUpdate = await OpenUniPopulationSearch.findOne({
|
||||
const searchToUpdate = await OpenUniPopulationSearchModel.findOne({
|
||||
where: {
|
||||
userId,
|
||||
id,
|
||||
@@ -121,7 +122,7 @@ export const updateOpenUniPopulationSearch = async (userId: string, id: string,
|
||||
}
|
||||
|
||||
export const deleteOpenUniSearch = async (userId: string, id: string) => {
|
||||
return await OpenUniPopulationSearch.destroy({
|
||||
return await OpenUniPopulationSearchModel.destroy({
|
||||
where: {
|
||||
userId,
|
||||
id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import moment from 'moment'
|
||||
|
||||
import { SISStudyRight } from '../../models'
|
||||
import { SISStudyRightModel } from '../../models'
|
||||
import { getCourseNames, getCredits, getEnrollments, getStudentInfo, getStudyRights } from './openUniSearches'
|
||||
|
||||
type CourseInfo = {
|
||||
@@ -46,15 +46,15 @@ const getAllCourseCodes = (courseCodes: string[]) => {
|
||||
return courseCodes.concat(ayCourseCodes)
|
||||
}
|
||||
|
||||
const isStartDateOutsideInterval = (studyRight: SISStudyRight, startDate: Date) => {
|
||||
const isStartDateOutsideInterval = (studyRight: SISStudyRightModel, startDate: Date) => {
|
||||
return moment(studyRight.startDate).isBetween(startDate, moment())
|
||||
}
|
||||
|
||||
const isStartDateInsideAndEndDateOutside = (studyRight: SISStudyRight, startDate: Date) => {
|
||||
const isStartDateInsideAndEndDateOutside = (studyRight: SISStudyRightModel, startDate: Date) => {
|
||||
return moment(studyRight.startDate).isSameOrBefore(startDate) && moment(studyRight.endDate).isSameOrAfter(moment())
|
||||
}
|
||||
|
||||
const isEndDateBeforeNow = (studyRight: SISStudyRight) => moment(studyRight.endDate).isSameOrBefore(moment())
|
||||
const isEndDateBeforeNow = (studyRight: SISStudyRightModel) => moment(studyRight.endDate).isSameOrBefore(moment())
|
||||
|
||||
const getEmptyCourseInfo = (): CourseInfo => ({
|
||||
status: {
|
||||
|
||||
@@ -2,10 +2,10 @@ import { Op, QueryTypes } from 'sequelize'
|
||||
|
||||
import { facultyCodes } from '../config/organizationConstants'
|
||||
import { dbConnections } from '../database/connection'
|
||||
import { Organization } from '../models'
|
||||
import { OrganizationModel } from '../models'
|
||||
|
||||
export const getOrganizations = () => {
|
||||
return Organization.findAll({
|
||||
return OrganizationModel.findAll({
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: facultyCodes,
|
||||
|
||||
@@ -1,65 +1,19 @@
|
||||
import { keyBy } from 'lodash'
|
||||
|
||||
import { getPassingSemester } from '../../util/semester'
|
||||
import { CourseStatistics, CourseStatsCounter } from '../courses/courseStatsCounter'
|
||||
import { encrypt } from '../encrypt'
|
||||
import {
|
||||
CoursesQueryResult,
|
||||
dateMonthsFromNow,
|
||||
EnrollmentsQueryResult,
|
||||
findCourses,
|
||||
findCourseEnrollments,
|
||||
Params,
|
||||
parseCreditInfo,
|
||||
parseQueryParams,
|
||||
Query,
|
||||
} from './shared'
|
||||
import { getStudentNumbersWithAllStudyRightElements } from './studentNumbersWithAllElements'
|
||||
import { type CourseStatistics, CourseStatsCounter } from '../courses/courseStatsCounter'
|
||||
import { encrypt, decrypt, EncrypterData } from '../encrypt'
|
||||
import { findCourses, parseCreditInfo } from './shared'
|
||||
|
||||
const getStudentsAndCourses = async (
|
||||
params: Params,
|
||||
selectedStudents: string[] | undefined,
|
||||
studentNumbers: string[] | null,
|
||||
courseCodes: string[] | undefined
|
||||
): Promise<[number, CoursesQueryResult, EnrollmentsQueryResult]> => {
|
||||
const { months, studyRights, startDate, endDate, exchangeStudents, nondegreeStudents, transferredStudents } = params
|
||||
|
||||
const studentnumbers =
|
||||
studentNumbers ??
|
||||
selectedStudents ??
|
||||
(await getStudentNumbersWithAllStudyRightElements({
|
||||
studyRights,
|
||||
startDate,
|
||||
endDate,
|
||||
exchangeStudents,
|
||||
nondegreeStudents,
|
||||
transferredOutStudents: transferredStudents,
|
||||
}))
|
||||
|
||||
const allStudents = studentnumbers.length
|
||||
|
||||
const beforeDate = months && startDate ? dateMonthsFromNow(startDate, months) : new Date()
|
||||
const courses = !studentNumbers
|
||||
? await findCourses(studentnumbers, dateMonthsFromNow(startDate, months), courseCodes)
|
||||
: await findCourses(studentnumbers, beforeDate, courseCodes)
|
||||
|
||||
const foundCourseCodes = [...new Set(courses.map(({ code }) => code))].toSorted()
|
||||
const filteredCourseCodes = courseCodes?.filter(code => !foundCourseCodes.includes(code))
|
||||
const courseEnrollments = filteredCourseCodes?.length
|
||||
? !studentNumbers
|
||||
? await findCourseEnrollments(studentnumbers, dateMonthsFromNow(startDate, months), filteredCourseCodes)
|
||||
: await findCourseEnrollments(studentnumbers, beforeDate, filteredCourseCodes)
|
||||
: []
|
||||
|
||||
return [allStudents, courses, courseEnrollments]
|
||||
}
|
||||
|
||||
type Bottlenecks = {
|
||||
coursestatistics: CourseStatistics[]
|
||||
export type Bottlenecks = {
|
||||
allStudents: number
|
||||
coursestatistics: CourseStatistics[]
|
||||
}
|
||||
|
||||
export const bottlenecksOf = async (query: Query, studentNumbers: string[] | null, encryptData = false) => {
|
||||
export const bottlenecksOf = async (
|
||||
selectedStudents: string[],
|
||||
selectedStudentsByYear: { [year: string]: string[] },
|
||||
selectedCourses: string[],
|
||||
isEncrypted = false
|
||||
) => {
|
||||
const encryptStudentNumbers = (bottlenecks: Bottlenecks) => {
|
||||
for (const course of Object.keys(bottlenecks.coursestatistics)) {
|
||||
const encryptedStudentStats = {}
|
||||
@@ -75,91 +29,58 @@ export const bottlenecksOf = async (query: Query, studentNumbers: string[] | nul
|
||||
}
|
||||
}
|
||||
|
||||
const params = parseQueryParams(query)
|
||||
const allStudentsByYears = query?.selectedStudentsByYear
|
||||
? Object.keys(query.selectedStudentsByYear).reduce(
|
||||
(res, year) => [...res, ...query.selectedStudentsByYear![year]],
|
||||
[] as string[]
|
||||
)
|
||||
: []
|
||||
// HACK: Encrypted students can't be decrypted if passed as strings
|
||||
if (isEncrypted) selectedStudents = (selectedStudents as unknown as EncrypterData[]).map(decrypt)
|
||||
|
||||
// To fix failed and enrolled, no grade filter options some not so clean and nice solutions were added
|
||||
// Get the data with actual 1. courses and filtered students. 2. all students by year, if provided.
|
||||
const [[allStudents, courses, courseEnrollements], [, allCourses]] = await Promise.all([
|
||||
getStudentsAndCourses(params, query.selectedStudents, studentNumbers, query.courses),
|
||||
getStudentsAndCourses(params, allStudentsByYears, null, query.courses),
|
||||
])
|
||||
const courses = await findCourses(selectedStudents, selectedCourses)
|
||||
|
||||
// Get the substitution codes for the fetch data by selected students
|
||||
const substitutionCodes = Object.entries(courses).reduce(
|
||||
(codes, [, course]) => [...codes, ...(course?.substitutions || [])],
|
||||
[] as string[]
|
||||
)
|
||||
const codes = Object.keys(keyBy(courses, 'code')).map(code => code)
|
||||
// Filter substitution courses for fetched courses -> by this we avoid the situation in which there
|
||||
// are only courses with old course codes. Frontend NEEDS in most cases the current course.
|
||||
const substitutionCourses = allCourses.filter(
|
||||
course => substitutionCodes.includes(course.code) && !codes.includes(course.code)
|
||||
)
|
||||
const bottlenecks: Bottlenecks = {
|
||||
coursestatistics: [],
|
||||
allStudents: 0,
|
||||
}
|
||||
|
||||
const stats = {} as Record<string, CourseStatsCounter>
|
||||
const startYear = parseInt(query.year, 10)
|
||||
let coursesToLoop: Array<EnrollmentsQueryResult[number] | CoursesQueryResult[number]> =
|
||||
courses.concat(substitutionCourses)
|
||||
const courseCodes = coursesToLoop.map(course => course.code)
|
||||
|
||||
// This fixes a problem when "Enrolled, no grade" is chosen. The SQL query for fetching
|
||||
// credits does not fetch enrollments if no credits are found for selected students.
|
||||
// This and other SQL query ensures that enrollments are added.
|
||||
const coursesOnlyWithEnrollments = courseEnrollements.filter(course => !courseCodes.includes(course.code))
|
||||
coursesToLoop = coursesToLoop.concat(coursesOnlyWithEnrollments)
|
||||
|
||||
const coursesByCode = keyBy(coursesToLoop, 'code')
|
||||
for (const course of coursesToLoop) {
|
||||
let mainCourse = course
|
||||
|
||||
if (course.main_course_code && course.main_course_code !== course.code) {
|
||||
const newMainCourse = coursesByCode[course.main_course_code]
|
||||
if (newMainCourse) {
|
||||
mainCourse = newMainCourse
|
||||
// HACK: If year isn't present or is invalid, return MAGIC_NUMBER.
|
||||
// The new degree programs start on 2017.
|
||||
const getYear = (studentnumber: string): number => {
|
||||
const MAGIC_NUMBER = 2017
|
||||
for (const year in selectedStudentsByYear) {
|
||||
if (selectedStudentsByYear[year].includes(studentnumber)) {
|
||||
return parseInt(year, 10) || MAGIC_NUMBER
|
||||
}
|
||||
}
|
||||
|
||||
if (!stats[mainCourse.code]) {
|
||||
stats[mainCourse.code] = new CourseStatsCounter(mainCourse.code, mainCourse.name, allStudents)
|
||||
}
|
||||
|
||||
const coursestats = stats[mainCourse.code]
|
||||
coursestats.addCourseSubstitutions(course.substitutions)
|
||||
if (course.enrollments) {
|
||||
course.enrollments.forEach(({ studentnumber, state, enrollment_date_time }) => {
|
||||
if (query?.selectedStudents?.includes(studentnumber) || !query?.selectedStudents) {
|
||||
const semester = getPassingSemester(startYear, enrollment_date_time)
|
||||
coursestats.markEnrollment(studentnumber, state, semester)
|
||||
}
|
||||
})
|
||||
}
|
||||
if ('credits' in course) {
|
||||
course.credits?.forEach(credit => {
|
||||
const { studentnumber, passingGrade, improvedGrade, failingGrade, grade, date } = parseCreditInfo(credit)
|
||||
if (query?.selectedStudents?.includes(studentnumber) || !query?.selectedStudents) {
|
||||
const semester = getPassingSemester(startYear, date)
|
||||
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stats[mainCourse.code] = coursestats
|
||||
return MAGIC_NUMBER
|
||||
}
|
||||
|
||||
bottlenecks.coursestatistics = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
|
||||
bottlenecks.allStudents = allStudents
|
||||
const selectedStudentCount = selectedStudents.length
|
||||
const stats = {} as Record<string, CourseStatsCounter>
|
||||
|
||||
if (encryptData) {
|
||||
for (const course of courses) {
|
||||
const { code: mainCode, name: mainName } = courses.find(({ code }) => code == course?.main_course_code) ?? course
|
||||
const coursestats = stats[mainCode] ?? new CourseStatsCounter(mainCode, mainName)
|
||||
|
||||
coursestats.addCourseSubstitutions(course.substitutions)
|
||||
course.enrollments?.forEach(({ studentnumber, state, enrollment_date_time }) => {
|
||||
// HACK: date should already be Date object
|
||||
const initialDate = new Date(enrollment_date_time)
|
||||
const semester = getPassingSemester(getYear(studentnumber), initialDate)
|
||||
coursestats.markEnrollment(studentnumber, state, semester)
|
||||
})
|
||||
course.credits
|
||||
?.map(parseCreditInfo)
|
||||
.forEach(({ studentnumber, passingGrade, improvedGrade, failingGrade, grade, date }) => {
|
||||
// HACK: date should already be Date object
|
||||
const initialDate = new Date(date)
|
||||
const semester = getPassingSemester(getYear(studentnumber), initialDate)
|
||||
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
|
||||
})
|
||||
|
||||
stats[mainCode] = coursestats
|
||||
}
|
||||
|
||||
const bottlenecks: Bottlenecks = {
|
||||
allStudents: selectedStudentCount,
|
||||
coursestatistics: Object.values(stats).map(coursestatistics =>
|
||||
coursestatistics.getFinalStats(selectedStudentCount)
|
||||
),
|
||||
}
|
||||
|
||||
if (isEncrypted) {
|
||||
encryptStudentNumbers(bottlenecks)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import { col, InferAttributes, Op, where } from 'sequelize'
|
||||
import { col, Op, where } from 'sequelize'
|
||||
|
||||
import { Credit, Student, Studyplan } from '@oodikone/shared/models'
|
||||
import {
|
||||
Course,
|
||||
Credit,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
Student,
|
||||
Studyplan,
|
||||
SISStudyRight,
|
||||
SISStudyRightElement,
|
||||
Name,
|
||||
CreditTypeCode,
|
||||
DegreeProgrammeType,
|
||||
EnrollmentType,
|
||||
ExtentCode,
|
||||
SemesterEnrollment,
|
||||
} from '@oodikone/shared/types'
|
||||
import {
|
||||
CourseModel,
|
||||
CreditModel,
|
||||
OrganizationModel,
|
||||
ProgrammeModuleModel,
|
||||
StudentModel,
|
||||
StudyplanModel,
|
||||
SISStudyRightModel,
|
||||
SISStudyRightElementModel,
|
||||
} from '../../models'
|
||||
import { Name } from '../../shared/types'
|
||||
import { CreditTypeCode, DegreeProgrammeType, EnrollmentType, ExtentCode, SemesterEnrollment } from '../../types'
|
||||
import { redisClient } from '../redis'
|
||||
import { getCurrentSemester } from '../semesters'
|
||||
import { getCurriculumVersion } from './shared'
|
||||
@@ -82,13 +89,9 @@ const findThesisAndLatestAndEarliestAttainments = (
|
||||
) {
|
||||
thesisData = attainment
|
||||
}
|
||||
if (!attainmentDates.latestTotal) {
|
||||
attainmentDates.latestTotal = attainment.attainment_date
|
||||
}
|
||||
attainmentDates.latestTotal ??= attainment.attainment_date
|
||||
if (studyPlan.included_courses.includes(attainment.course?.code)) {
|
||||
if (!attainmentDates.latestHops) {
|
||||
attainmentDates.latestHops = attainment.attainment_date
|
||||
}
|
||||
attainmentDates.latestHops ??= attainment.attainment_date
|
||||
if (
|
||||
!attainmentDates.earliestHops ||
|
||||
attainment.attainment_date.getTime() < attainmentDates.earliestHops.getTime()
|
||||
@@ -101,11 +104,7 @@ const findThesisAndLatestAndEarliestAttainments = (
|
||||
return { attainmentDates, thesisData }
|
||||
}
|
||||
|
||||
const formatStudent = (
|
||||
student: InferAttributes<Student>,
|
||||
facultyMap: Record<string, Name>,
|
||||
currentSemesterCode: number
|
||||
) => {
|
||||
const formatStudent = (student: Student, facultyMap: Record<string, Name>, currentSemesterCode: number) => {
|
||||
const {
|
||||
studentnumber: studentNumber,
|
||||
abbreviatedname: name,
|
||||
@@ -191,7 +190,7 @@ const formatStudent = (
|
||||
|
||||
export const findStudentsCloseToGraduation = async (studentNumbers?: string[]) => {
|
||||
const students = (
|
||||
await Student.findAll({
|
||||
await StudentModel.findAll({
|
||||
attributes: [
|
||||
'abbreviatedname',
|
||||
'creditcount',
|
||||
@@ -211,18 +210,18 @@ export const findStudentsCloseToGraduation = async (studentNumbers?: string[]) =
|
||||
: {},
|
||||
include: [
|
||||
{
|
||||
model: Studyplan,
|
||||
model: StudyplanModel,
|
||||
attributes: ['completed_credits', 'included_courses', 'programme_code', 'curriculum_period_id'],
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
model: SISStudyRight,
|
||||
model: SISStudyRightModel,
|
||||
as: 'studyRight',
|
||||
attributes: ['id', 'semesterEnrollments', 'startDate', 'extentCode'],
|
||||
where: { cancelled: false },
|
||||
include: [
|
||||
{
|
||||
model: SISStudyRightElement,
|
||||
model: SISStudyRightElementModel,
|
||||
as: 'studyRightElements',
|
||||
where: {
|
||||
graduated: false,
|
||||
@@ -279,23 +278,23 @@ export const findStudentsCloseToGraduation = async (studentNumbers?: string[]) =
|
||||
],
|
||||
},
|
||||
{
|
||||
model: Credit,
|
||||
model: CreditModel,
|
||||
attributes: ['attainment_date', 'grade', 'studyright_id'],
|
||||
where: { credittypecode: CreditTypeCode.PASSED },
|
||||
include: [
|
||||
{
|
||||
model: Course,
|
||||
model: CourseModel,
|
||||
attributes: ['code', 'course_unit_type'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
order: [[{ model: Credit, as: 'credits' }, 'attainment_date', 'DESC']],
|
||||
order: [[{ model: CreditModel, as: 'credits' }, 'attainment_date', 'DESC']],
|
||||
})
|
||||
).map(student => student.toJSON())
|
||||
|
||||
const programmeCodes = [...new Set(students.flatMap(student => student.studyplans.map(sp => sp.programme_code)))]
|
||||
const programmesWithFaculties = await ProgrammeModule.findAll({
|
||||
const programmesWithFaculties = await ProgrammeModuleModel.findAll({
|
||||
attributes: ['code'],
|
||||
where: {
|
||||
code: {
|
||||
@@ -303,7 +302,7 @@ export const findStudentsCloseToGraduation = async (studentNumbers?: string[]) =
|
||||
},
|
||||
},
|
||||
include: {
|
||||
model: Organization,
|
||||
model: OrganizationModel,
|
||||
attributes: ['name'],
|
||||
},
|
||||
raw: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user