mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b192d2b104 | ||
|
|
c912438265 | ||
|
|
713e996ba9 | ||
|
|
03bb0b37dd | ||
|
|
efce3e52c8 | ||
|
|
d5d728e1f7 | ||
|
|
0ec0451971 | ||
|
|
3256757dc1 | ||
|
|
d3c03e8451 | ||
|
|
5f3e715452 | ||
|
|
66d4c80bef | ||
|
|
ad2133ef85 | ||
|
|
1eca9d1bb4 | ||
|
|
401269571a | ||
|
|
7f4050c5dc | ||
|
|
581950bd53 | ||
|
|
c712189d0e | ||
|
|
252c09f656 | ||
|
|
e7494bb269 | ||
|
|
f859886429 | ||
|
|
1b0f009a1e | ||
|
|
b216e69609 | ||
|
|
37eb61603a | ||
|
|
75681e2a2d | ||
|
|
6617c2d1bc | ||
|
|
955f50271a | ||
|
|
426eab64b8 | ||
|
|
b67d1b8950 | ||
|
|
390b9dfd50 | ||
|
|
653f558265 | ||
|
|
b8092c24f0 | ||
|
|
9bf13dc720 | ||
|
|
2111b39cfd | ||
|
|
307e2d7cc3 | ||
|
|
199e8b74a0 |
+33
-166
@@ -154,13 +154,42 @@ jobs:
|
||||
./anonyymioodi/commit_and_push_to_toska_hub.sh user-db
|
||||
|
||||
# === End-to-end tests ===
|
||||
test_course_statistics:
|
||||
name: 'Test course statistics'
|
||||
test_end_to_end:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: 'Test course statistics'
|
||||
spec: cypress/e2e/Course_statistics.js
|
||||
- name: 'Test custom population'
|
||||
spec: cypress/e2e/Custom_population.js
|
||||
- name: 'Test population statistics'
|
||||
spec: cypress/e2e/Population_statistics.js
|
||||
- name: 'Test filters'
|
||||
spec: cypress/e2e/Filters.js
|
||||
- name: 'Test students'
|
||||
spec: cypress/e2e/Students.js
|
||||
- name: 'Test completed courses'
|
||||
spec: cypress/e2e/Completed_courses.js
|
||||
- name: 'Test studyprogramme overview'
|
||||
spec: cypress/e2e/Studyprogramme_overview.js
|
||||
- name: 'Test teachers and users'
|
||||
spec: |
|
||||
cypress/e2e/Teachers.js
|
||||
cypress/e2e/Users.js
|
||||
- name: 'Test faculty statistics'
|
||||
spec: cypress/e2e/Faculty_statistics.js
|
||||
- name: 'Test open uni search'
|
||||
spec: cypress/e2e/Openuni.js
|
||||
- name: 'Test language center view'
|
||||
spec: cypress/e2e/Language_center.js
|
||||
- name: 'Test study guidance groups'
|
||||
spec: cypress/e2e/Study_guidance_groups.js
|
||||
name: ${{ matrix.name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run tests'
|
||||
@@ -168,169 +197,7 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Course_statistics.js
|
||||
|
||||
test_custom_population:
|
||||
name: 'Test custom population'
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Custom_population.js
|
||||
|
||||
test_population_statistics:
|
||||
name: 'Test population statistics'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Population_statistics.js
|
||||
|
||||
test_filters:
|
||||
name: 'Test filters'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Filters.js
|
||||
|
||||
test_student_statistics:
|
||||
name: 'Test student statistics'
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Student_statistics.js
|
||||
|
||||
test_completed_courses:
|
||||
name: 'Test completed courses'
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Completed_courses.js
|
||||
|
||||
test_studyprogramme_overview:
|
||||
name: 'Test studyprogramme overview'
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Studyprogramme_overview.js
|
||||
|
||||
test_teachers_and_users:
|
||||
name: 'Test teachers and users'
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: |
|
||||
cypress/e2e/Teachers.js
|
||||
cypress/e2e/Users.js
|
||||
|
||||
test_faculty_statistics:
|
||||
name: 'Test faculty statistics'
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Faculty_statistics.js
|
||||
|
||||
test_open_uni_search:
|
||||
name: 'Test open uni search'
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Openuni.js
|
||||
|
||||
test_language_center:
|
||||
name: 'Test language center view'
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Language_center.js
|
||||
spec: ${{ matrix.spec }}
|
||||
|
||||
# === Other tests ===
|
||||
test_service_exit_statuses:
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Searching single course having substitution mappings shows course statistics', () => {
|
||||
it('Searching single course having substitution mappings shows course statistics', { retries: 3 }, () => {
|
||||
cy.contains('Search for courses')
|
||||
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
|
||||
// Click uni course, not avoin
|
||||
@@ -89,10 +89,11 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('TKT20001')
|
||||
cy.contains('58131') // old mapped code
|
||||
|
||||
cy.contains('.tabular.menu a', 'Table').click()
|
||||
cy.contains('.tabular.menu a', 'Students').click()
|
||||
cy.contains('All')
|
||||
|
||||
cy.contains('.tabular.menu a', 'Attempts').click()
|
||||
cy.contains('All')
|
||||
cy.cs('viewMode-Attempts').click()
|
||||
cy.cs('viewMode-Students').click()
|
||||
|
||||
cy.contains('.tabular.menu a', 'Pass rate chart').click()
|
||||
cy.contains('svg', 'Pass rate chart')
|
||||
@@ -137,7 +138,7 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('Show population')
|
||||
cy.contains('TKT20003')
|
||||
|
||||
cy.contains('Course statistics').click()
|
||||
cy.contains('Courses').click()
|
||||
cy.contains('Search for courses')
|
||||
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
|
||||
cy.contains('td', 'TKT20001').click()
|
||||
@@ -310,7 +311,6 @@ describe('Course Statistics tests', () => {
|
||||
cy.get("div[role='option']").should('have.length', 11)
|
||||
})
|
||||
|
||||
cy.contains('#CourseStatPanes a.item', 'Table').click()
|
||||
cy.contains('#CourseStatPanes a.item', 'Attempts').click()
|
||||
cy.get('#CourseStatPanes table>tbody').within(() => {
|
||||
attemptsTableContents.forEach((values, trIndex) => {
|
||||
@@ -358,16 +358,6 @@ describe('Course Statistics tests', () => {
|
||||
['Total', null, 511, 486, 25],
|
||||
]
|
||||
|
||||
const attemptsTableContentsOpen = [
|
||||
// [time, total, passed, failed]
|
||||
['Total', null, 199, 188, 11],
|
||||
]
|
||||
|
||||
const attemptsTableContentsRegular = [
|
||||
// [time, total, passed, failed]
|
||||
['Total', null, 312, 298, 14],
|
||||
]
|
||||
|
||||
const gradesTableContents = [
|
||||
// [time, attempts, 0, 1, 2, 3, 4, 5, other passed]
|
||||
['Total', null, 511, 25, 11, 17, 21, 59, 365, 13],
|
||||
@@ -395,7 +385,6 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
cy.contains('Show population').should('not.be.enabled')
|
||||
|
||||
cy.contains('#CourseStatPanes a.item', 'Table').click()
|
||||
cy.contains('#CourseStatPanes a.item', 'Attempts').click()
|
||||
cy.get('#CourseStatPanes table>tbody').within(() => {
|
||||
attemptsTableContents.forEach((values, trIndex) => {
|
||||
@@ -426,44 +415,6 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Toggling course provider changes stats correctly', () => {
|
||||
cy.get('[data-cy=unify_radio_regular]').click()
|
||||
cy.get("div[name='toYear']").within(() => {
|
||||
cy.get("div[role='option']").first().should('have.text', '2020-2021')
|
||||
cy.contains("div[role='option']", yearRange.to).should('have.class', 'selected')
|
||||
cy.get("div[role='option']").last().should('have.text', yearRange.from)
|
||||
cy.get("div[role='option']").should('have.length', 21)
|
||||
})
|
||||
cy.contains('#CourseStatPanes a.item', 'Table').click()
|
||||
cy.contains('#CourseStatPanes a.item', 'Attempts').click()
|
||||
cy.get('#CourseStatPanes table>tbody').within(() => {
|
||||
attemptsTableContentsRegular.forEach((values, trIndex) => {
|
||||
cy.get('tr')
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
if (value === null) return
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
cy.get('[data-cy=unify_radio_open]').click()
|
||||
cy.get('#CourseStatPanes table>tbody').within(() => {
|
||||
attemptsTableContentsOpen.forEach((values, trIndex) => {
|
||||
cy.get('tr')
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
if (value === null) return
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
cy.get('[data-cy=unify_radio_unify]').click()
|
||||
})
|
||||
|
||||
it('After changing time range shows same stats', () => {
|
||||
const newYearRange = { from: '2016-2017', to: '2019-2020' }
|
||||
cy.get("div[name='fromYear']")
|
||||
@@ -510,7 +461,7 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ! This test does not pass locally but it does pass on GitHub Actions (see issue #4317)
|
||||
// TODO: Why does this test not show "5 or less students" but 5 instead?
|
||||
it('Some features of Course Statistics are hidden for courseStatistics-users without other rights', () => {
|
||||
cy.init('/coursestatistics', 'onlycoursestatistics')
|
||||
cy.get('[data-cy=navbar-courseStatistics]').click()
|
||||
@@ -519,13 +470,12 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('Filter statistics by study programmes').should('not.exist')
|
||||
cy.contains('Faculty statistics').should('not.exist')
|
||||
cy.contains('Show population').should('not.exist')
|
||||
|
||||
cy.cs('viewMode-Attempts').click()
|
||||
cy.contains('.tabular.menu a', 'Attempts').click()
|
||||
|
||||
const attemptsTableContents = [
|
||||
// [time, passed, failed, passrate, total enrollments, enrolled, rejected, aborted]
|
||||
['Total', null, 288, 213, 75, '73.96 %', 0, 0, 0, 0],
|
||||
['2020-2021', null, '5 or less students', 'NA', 'NA', 'NA', 'NA', 'NA', 'NA', 'NA'],
|
||||
['Total', null, 295, 216, 79, '73.22 %', 0, 0, 0, 0],
|
||||
['2020-2021', null, 5, 2, 3, '40.00 %'],
|
||||
['2019-2020', null, 164, 121, 43, '73.78 %'],
|
||||
['2018-2019', null, 85, 60, 25, '70.59 %'],
|
||||
['2017-2018', null, 39, 32, 7, '82.05 %'],
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Graduation filter works', () => {
|
||||
it('Graduation filter works', { retries: 2 }, () => {
|
||||
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
|
||||
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
describe('Student Statistics tests', () => {
|
||||
describe('Students tests', () => {
|
||||
beforeEach(() => {
|
||||
cy.init()
|
||||
cy.contains('Student statistics').click()
|
||||
cy.contains('Students').click()
|
||||
cy.contains('Show student names')
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('Student Statistics tests', () => {
|
||||
sis_person_id: 'hy-hlo-84830887',
|
||||
}
|
||||
|
||||
it('Student statistics search form is usable', () => {
|
||||
it('Students search form is usable', () => {
|
||||
cy.contains('Show student names')
|
||||
cy.url().should('include', '/students')
|
||||
cy.get('.prompt').type(student.lastname)
|
||||
@@ -0,0 +1,165 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
describe('Study guidance group tests', () => {
|
||||
describe('Study guidance group overview page', () => {
|
||||
describe('without study guidance groups', () => {
|
||||
it('shows the correct notification', () => {
|
||||
cy.init('/studyguidancegroups', 'admin')
|
||||
cy.contains('Study guidance groups')
|
||||
cy.contains('You do not have access to any study guidance groups.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('without correct rights', () => {
|
||||
it('shows the correct notification', () => {
|
||||
cy.init('/studyguidancegroups')
|
||||
cy.contains('Access denied')
|
||||
cy.contains(
|
||||
"You're currently not allowed to see this page. Please contant oodikone@helsinki.fi, if this is a mistake."
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with study guidance groups', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/studyguidancegroups', 'onlystudyguidancegroups')
|
||||
cy.intercept('/api/studyguidancegroups', { fixture: 'studyGuidanceGroups.json' })
|
||||
})
|
||||
|
||||
it('shows the correct data', () => {
|
||||
cy.contains('Study guidance groups')
|
||||
|
||||
const studyGuidanceGroupsTableContent = [
|
||||
['TKT kandit 2018', 3, 'Tietojenkäsittelytieteen kandiohjelma', '2018 - 2019'],
|
||||
['Oma ohjausryhmä', 2, 'Add study programme', 'Add year'],
|
||||
]
|
||||
|
||||
cy.checkTableStats(studyGuidanceGroupsTableContent, 'study-guidance-group-overview')
|
||||
})
|
||||
|
||||
it('has working links to single study guidance groups', () => {
|
||||
cy.cs('study-guidance-group-link-sgg-cypress-1').click()
|
||||
cy.url().should('eq', `${Cypress.config().baseUrl}/studyguidancegroups/sgg-cypress-1`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Page for single study guidance group', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/api/studyguidancegroups', { fixture: 'studyGuidanceGroups.json' })
|
||||
})
|
||||
|
||||
it('has a working back button', () => {
|
||||
cy.init('/studyguidancegroups/sgg-cypress-1', 'onlystudyguidancegroups')
|
||||
|
||||
cy.get('button.ui.button').contains('Back').click()
|
||||
cy.url().should('eq', `${Cypress.config().baseUrl}/studyguidancegroups`)
|
||||
})
|
||||
|
||||
describe('without associated study programme and year', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/studyguidancegroups/sgg-cypress-2', 'onlystudyguidancegroups')
|
||||
})
|
||||
|
||||
it('shows the correct panes', () => {
|
||||
cy.contains('Oma ohjausryhmä')
|
||||
cy.cs('Credit accumulation (for 2 students)')
|
||||
cy.cs('Age distribution')
|
||||
cy.cs('Courses of population')
|
||||
cy.cs('Students (2)')
|
||||
cy.cs('Credit statistics').should('not.exist')
|
||||
})
|
||||
|
||||
it('students table has the correct tabs', () => {
|
||||
cy.cs('Students (2)').click()
|
||||
cy.get('[data-cy="student-table-tabs"] .ui.attached.tabular.menu').then($menu => {
|
||||
const tabNames = Array.from($menu[0].children).map(elem => elem.innerText)
|
||||
expect(tabNames).to.deep.equal(['General'])
|
||||
})
|
||||
})
|
||||
|
||||
it('general tab of the students table has the correct columns', () => {
|
||||
const firstRowHeadings = [
|
||||
'Phone number',
|
||||
'Student number',
|
||||
'Credits',
|
||||
'Start year at uni',
|
||||
'Study programmes',
|
||||
'Tags',
|
||||
'Last Updated At',
|
||||
]
|
||||
const secondRowHeadings = ['All', 'Since 1.1.1970']
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with associated study programme and year', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/studyguidancegroups/sgg-cypress-1', 'onlystudyguidancegroups')
|
||||
})
|
||||
|
||||
it('shows the correct labels', () => {
|
||||
cy.get('.ui.medium.header').contains('TKT kandit 2018')
|
||||
cy.get('.ui.blue.tag.label:first').contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.get('.ui.blue.tag.label:last').contains('2018 - 2019')
|
||||
})
|
||||
|
||||
it('shows the correct panes', () => {
|
||||
cy.cs('Credit accumulation (for 3 students)')
|
||||
cy.cs('Credit statistics')
|
||||
cy.cs('Age distribution')
|
||||
cy.cs('Courses of population')
|
||||
cy.cs('Students (3)')
|
||||
})
|
||||
|
||||
it("clicking the 'Show starting from associated year' button activates the 'Date of Course Credits' filter", () => {
|
||||
cy.get('[data-cy="CreditDate-filter-card"][data-open="false"]')
|
||||
cy.get('.ui.primary.button').contains('Show starting from associated year').click()
|
||||
cy.get('[data-cy="CreditDate-filter-card"][data-open="true"]')
|
||||
cy.get('.ui.mini.icon.button.credit-date-filter-input').contains('01.08.2018')
|
||||
cy.contains('Reset All Filters')
|
||||
cy.get('.ui.primary.button').contains('Show all credits').click()
|
||||
cy.get('[data-cy="CreditDate-filter-card"][data-open="false"]')
|
||||
})
|
||||
|
||||
it('students table has the correct tabs', () => {
|
||||
cy.cs('Students (3)').click()
|
||||
cy.get('[data-cy="student-table-tabs"] .ui.attached.tabular.menu').then($menu => {
|
||||
const tabNames = Array.from($menu[0].children).map(elem => elem.innerText)
|
||||
expect(tabNames).to.deep.equal(['General', 'Courses', 'Progress'])
|
||||
})
|
||||
})
|
||||
|
||||
it('general tab of the students table has the correct columns', () => {
|
||||
const firstRowHeadings = [
|
||||
'Phone number',
|
||||
'Student number',
|
||||
'Credits',
|
||||
'Start of studyright',
|
||||
'Started in programme',
|
||||
'Semesters present',
|
||||
'Graduation date',
|
||||
'Start year at uni',
|
||||
'Other programmes',
|
||||
'Semesters present amount',
|
||||
'Tags',
|
||||
'Last Updated At',
|
||||
]
|
||||
const secondRowHeadings = ['All', 'HOPS', 'Since 1.8.2018']
|
||||
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.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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -401,7 +401,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('textarea').type('010113437')
|
||||
cy.get('.positive').click()
|
||||
|
||||
cy.contains('Student statistics').click()
|
||||
cy.contains('Students').click()
|
||||
cy.get('.prompt').type(student)
|
||||
cy.contains('a', student).click()
|
||||
cy.contains(name)
|
||||
@@ -410,7 +410,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.go('back')
|
||||
deleteTag(name)
|
||||
|
||||
cy.contains('Student statistics').click()
|
||||
cy.contains('Students').click()
|
||||
cy.get('.prompt').type(student)
|
||||
cy.contains('a', student).click()
|
||||
cy.contains(name).should('not.exist')
|
||||
|
||||
@@ -8,8 +8,8 @@ describe('Users tests', () => {
|
||||
cy.get('[data-cy="navBar"]').contains('Give feedback').should('exist')
|
||||
|
||||
cy.get('[data-cy="navBar"]').contains('Study programme').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Student statistics').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Course statistics').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Students').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Courses').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Teachers').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Users').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Faculty').should('not.exist')
|
||||
@@ -30,8 +30,8 @@ describe('Users tests', () => {
|
||||
|
||||
it('should see more stuff than others', () => {
|
||||
cy.get('[data-cy="navBar"]').contains('Study programme').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Student statistics').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Course statistics').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Students').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Courses').should('exist')
|
||||
// TODO: admin should probably see this as well, since can access this page: cy.get('[data-cy="navBar"]').contains('Teachers').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Users').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Trends').should('exist')
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"id": "sgg-cypress-1",
|
||||
"name": {
|
||||
"fi": "TKT kandit 2018"
|
||||
},
|
||||
"members": [
|
||||
{ "personStudentNumber": "010450938" },
|
||||
{ "personStudentNumber": "010589388" },
|
||||
{ "personStudentNumber": "010262566" }
|
||||
],
|
||||
"tags": {
|
||||
"studyProgramme": "KH50_005",
|
||||
"year": "2018"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sgg-cypress-2",
|
||||
"name": {
|
||||
"fi": "Oma ohjausryhmä"
|
||||
},
|
||||
"members": [{ "personStudentNumber": "010614509" }, { "personStudentNumber": "010307146" }],
|
||||
"tags": {
|
||||
"studyProgramme": null,
|
||||
"year": null
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -53,6 +53,16 @@ const onlyIamRightsUserHeaders = {
|
||||
hypersonsisuid: 'hy-hlo-1111111',
|
||||
}
|
||||
|
||||
// Also has element detail of "KH50_005" for the study guidance groups to work
|
||||
const onlyStudyGuidanceGroupsUser = {
|
||||
uid: 'onlystudyguidancegroups',
|
||||
displayname: 'Study Guidance Groups User',
|
||||
'shib-session-id': 'mock-cypress-session',
|
||||
hygroupcn: 'grp-oodikone-users',
|
||||
mail: 'grp-toska+mockonlystudyguidancegroupsuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-2222222',
|
||||
}
|
||||
|
||||
// TODO add user into anon-DB (check the need for changing hypersonid)
|
||||
// REMEMBER TO ADD ROLE openUniSearch
|
||||
// const openUniUser = {
|
||||
@@ -71,6 +81,7 @@ const userHeaders = [
|
||||
onlycoursestatisticsUserHeaders,
|
||||
norightsUserHeaders,
|
||||
onlyIamRightsUserHeaders,
|
||||
onlyStudyGuidanceGroupsUser,
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -134,7 +145,7 @@ Cypress.Commands.add('selectStudyProgramme', name => {
|
||||
})
|
||||
|
||||
Cypress.Commands.add('checkTableStats', (correctStats, tableName) => {
|
||||
cy.get(`[data-cy=Table-${tableName}]>tbody`).within(() => {
|
||||
cy.get(`[data-cy=Table-${tableName}] tbody`).within(() => {
|
||||
correctStats.forEach((values, trIndex) => {
|
||||
cy.get('tr')
|
||||
.eq(trIndex)
|
||||
|
||||
@@ -83,8 +83,6 @@ parse_profiles
|
||||
parse_env
|
||||
if [[ "$compose_command" == "" ]]; then
|
||||
final_command="docker-compose ${env}"
|
||||
elif [[ "$compose_command" == *"down"* ]]; then
|
||||
final_command="docker-compose ${env} ${compose_command}"
|
||||
else
|
||||
final_command="docker-compose ${env} ${profiles} ${compose_command}"
|
||||
fi
|
||||
|
||||
Generated
+6
@@ -20,6 +20,7 @@
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.37",
|
||||
"morgan": "^1.10.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"pg": "^8.8.0",
|
||||
"redis": "^3.1.2",
|
||||
"sequelize": "^6.24.0",
|
||||
@@ -2913,6 +2914,11 @@
|
||||
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
|
||||
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
|
||||
},
|
||||
"node_modules/papaparse": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
|
||||
"integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.37",
|
||||
"morgan": "^1.10.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"pg": "^8.8.0",
|
||||
"redis": "^3.1.2",
|
||||
"sequelize": "^6.24.0",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
*.csv
|
||||
*.csv#
|
||||
@@ -0,0 +1,74 @@
|
||||
/* eslint-disable no-console */
|
||||
const { getCreditStats } = require('../services/analyticsService')
|
||||
const { parseCsv } = require('./helpers')
|
||||
|
||||
const diffs = []
|
||||
|
||||
const diff = (rapoData, okData, code, field) => {
|
||||
for (const year of Object.keys(okData)) {
|
||||
const rapo = rapoData[year]
|
||||
const ok = okData[year]
|
||||
if (!rapo) {
|
||||
if (!ok) console.log(`Year ${year} not in rapo for code ${code}, skipping`)
|
||||
continue
|
||||
}
|
||||
const diff = Math.abs(rapo[field] - ok[field])
|
||||
const bigger = Math.max(rapo[field], ok[field])
|
||||
if ((diff > bigger * 0.1 && bigger > 400) || diff > 100) {
|
||||
console.log(`${code} \t ${year}: ${field} diff: ${diff} \tOk: ${ok[field]} \tRapo: ${rapo[field]}`)
|
||||
diffs.push({ code, year, diff })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parseOkData = data => {
|
||||
return data.tableStats.reduce((obj, cur) => {
|
||||
const [year, total, major, nonmajor, nondegree, transferred] = cur
|
||||
obj[year] = { total, basic: major, nonmajor, nondegree, transferred }
|
||||
return obj
|
||||
}, {})
|
||||
}
|
||||
|
||||
// Acual logic in this function
|
||||
const process = async (data, field = 'total') => {
|
||||
const rapoProgrammeData = formatData(data.slice(1))
|
||||
const allProgrammeCodes = [...new Set(Object.keys(rapoProgrammeData))]
|
||||
for (const programmeCode of allProgrammeCodes) {
|
||||
const okProgrammeData = await getCreditStats(programmeCode, '', 'CALENDAR_YEAR', 'SPECIAL_INCLUDED')
|
||||
diff(rapoProgrammeData[programmeCode], parseOkData(okProgrammeData), programmeCode, field)
|
||||
}
|
||||
console.log(`${diffs.length} diffs found.`)
|
||||
}
|
||||
|
||||
const programmeCreditsDiff = async fileName => {
|
||||
console.log('Running studyprogramme credits diff')
|
||||
await parseCsv(fileName, process)
|
||||
console.log('Diff completed.')
|
||||
}
|
||||
|
||||
// Change weird rapo programmecodes to oodikone format, example: 300-M003 => MH30_003
|
||||
const transformProgrammeCode = oldCode => `${oldCode[4]}H${oldCode.slice(0, 2)}_${oldCode.slice(5, 8)}`
|
||||
|
||||
const formatData = data =>
|
||||
data.reduce((obj, cur) => {
|
||||
const parseNum = str => (str === '' ? 0 : parseInt(str, 10))
|
||||
const [year, facultyCode, programmeInfo, basic, exchange, otherUni, openUni, special, total, abroad, other] = cur
|
||||
const programmeCode = transformProgrammeCode(programmeInfo.split(' ')[0])
|
||||
if (!obj[programmeCode]) obj[programmeCode] = {}
|
||||
obj[programmeCode][year] = {
|
||||
year,
|
||||
facultyCode,
|
||||
programmeInfo,
|
||||
basic: parseNum(basic),
|
||||
exchange: parseNum(exchange),
|
||||
otherUni: parseNum(otherUni),
|
||||
openUni: parseNum(openUni),
|
||||
special: parseNum(special),
|
||||
total: parseNum(total),
|
||||
abroad: parseNum(abroad),
|
||||
other: parseNum(other),
|
||||
}
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
module.exports = { programmeCreditsDiff }
|
||||
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable no-console */
|
||||
const Papa = require('papaparse')
|
||||
const fs = require('fs/promises')
|
||||
|
||||
const parseCsv = async (fileName, fun) => {
|
||||
const file = await fs.readFile(`${__dirname}/${fileName}`, 'utf8')
|
||||
|
||||
Papa.parsePromise = file => {
|
||||
return new Promise((complete, error) => {
|
||||
Papa.parse(file, { complete, error })
|
||||
})
|
||||
}
|
||||
|
||||
const data = await Papa.parsePromise(file)
|
||||
await fun(data?.data)
|
||||
}
|
||||
|
||||
module.exports = { parseCsv }
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
const { bscProgrammesOfFaculties } = require('./conf')
|
||||
const { facultyDiff } = require('./studyrightDiff')
|
||||
const { programmeCreditsDiff } = require('./creditsDiff')
|
||||
|
||||
const runFacultyStudyrightDiffs = async () => {
|
||||
console.log('Running faculty diffs.')
|
||||
@@ -19,18 +20,17 @@ const runCreditDiffs = async () => {
|
||||
}
|
||||
|
||||
// ** To add new mode, just type key and function here **
|
||||
const modes = {} // { studyrights: runFacultyStudyrightDiffs, credits: runCreditDiffs }
|
||||
const modes = { credits: () => programmeCreditsDiff('data.csv') } // { studyrights: runFacultyStudyrightDiffs, credits: runCreditDiffs }
|
||||
|
||||
const main = async () => {
|
||||
console.log('**RAPODIFF**\n')
|
||||
const mode = process.argv[2]
|
||||
const modeFunction = modes[mode]
|
||||
if (!modeFunction) {
|
||||
/*console.log(
|
||||
console.log(
|
||||
'Unknown or missing mode. Supply the mode as an argument. Supported modes:\n',
|
||||
Object.keys(modes).join('\n ')
|
||||
) */
|
||||
console.log('Rapodiff disabled: an importer route in api-gateway must be first opened & possibly other fixes.')
|
||||
)
|
||||
return
|
||||
}
|
||||
await modeFunction()
|
||||
|
||||
@@ -6,7 +6,7 @@ const _ = require('lodash')
|
||||
|
||||
const { optimizedStatisticsOf } = require('../services/populations')
|
||||
const { Student, Transfer, StudyrightElement, Studyright } = require('../models')
|
||||
const { rapoToken, tokenImporterApi, importerDbApiUrl, nodeproxyUrl } = require('./rapodiff.js')
|
||||
const { rapoToken, tokenImporterApi, importerDbApiUrl, nodeproxyUrl } = require('./conf.js')
|
||||
|
||||
const getFromRapo = async (urlStart, urlEnd) => {
|
||||
console.log({ urlStart, urlEnd })
|
||||
|
||||
@@ -17,6 +17,7 @@ const elementdetails = require('./routes/elementdetails')
|
||||
const teachers = require('./routes/teachers')
|
||||
const providers = require('./routes/providers')
|
||||
const faculties = require('./routes/faculties')
|
||||
const university = require('./routes/university')
|
||||
const courseExclusions = require('./routes/courseExclusions')
|
||||
const semesters = require('./routes/semesters')
|
||||
const feedback = require('./routes/feedback')
|
||||
@@ -62,6 +63,7 @@ module.exports = (app, url) => {
|
||||
app.use(`${url}/completedcoursessearch`, completedCoursesSearch)
|
||||
app.use(`${url}/languagecenterdata`, languageCenterData)
|
||||
app.use(`${url}/faculties`, auth.roles(['facultyStatistics', 'katselmusViewer']), faculties)
|
||||
app.use(`${url}/university`, university)
|
||||
app.use(`${url}/updater`, auth.roles(['admin']), updater)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
app.use(`${url}/users`, auth.roles(['admin']), users)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const router = require('express').Router()
|
||||
const { faculties } = require('../services/organisations')
|
||||
const { combineFacultyBasics } = require('../services/faculty/facultyBasics')
|
||||
const { combineFacultyCredits } = require('../services/faculty/facultyCredits')
|
||||
const { combineFacultyThesisWriters } = require('../services/faculty/facultyThesisWriters')
|
||||
@@ -23,20 +22,11 @@ const {
|
||||
setFacultyProgressStats,
|
||||
} = require('../services/faculty/facultyService')
|
||||
const logger = require('../util/logger')
|
||||
const { getMedian } = require('../services/studyprogramme/studyprogrammeHelpers')
|
||||
const { getFacultyList } = require('../services/faculty/facultyHelpers')
|
||||
|
||||
// Faculty uses a lot of tools designed for Study programme.
|
||||
// Some of them have been copied here and slightly edited for faculty purpose.
|
||||
|
||||
const degreeNames = ['bachelor', 'bachelorMaster', 'master', 'licentiate', 'doctor']
|
||||
|
||||
const getFacultyList = async () => {
|
||||
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
|
||||
const facultyList = (await faculties()).filter(f => !ignore.includes(f.code))
|
||||
facultyList.sort((a, b) => (a.name.fi > b.name.fi ? 1 : -1))
|
||||
return facultyList
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const facultyList = await getFacultyList()
|
||||
res.json(facultyList)
|
||||
@@ -152,190 +142,6 @@ router.get('/:id/progressstats', async (req, res) => {
|
||||
return res.json(updateStats)
|
||||
})
|
||||
|
||||
router.get('/allprogressstats', async (req, res) => {
|
||||
const specialGroups = 'SPECIAL_EXCLUDED'
|
||||
const graduated = req.query?.graduated
|
||||
const allFaculties = await getFacultyList()
|
||||
const facultyCodes = allFaculties.map(f => f.code)
|
||||
const codeToData = {}
|
||||
|
||||
for (const facultyCode of facultyCodes) {
|
||||
const data = await getFacultyProgressStats(facultyCode, specialGroups, graduated)
|
||||
if (!data) {
|
||||
return res.status(500).end()
|
||||
}
|
||||
codeToData[facultyCode] = data
|
||||
}
|
||||
|
||||
const universityData = {
|
||||
years: codeToData.H50.years,
|
||||
yearlyBachelorTitles: codeToData.H50.yearlyBachelorTitles,
|
||||
programmeNames: allFaculties.reduce((obj, fac) => {
|
||||
const { name, ...rest } = fac.dataValues
|
||||
obj[fac.code] = { ...rest, ...name }
|
||||
return obj
|
||||
}, {}),
|
||||
bachelorsProgStats: {},
|
||||
creditCounts: {
|
||||
bachelor: {},
|
||||
},
|
||||
}
|
||||
|
||||
const unifyProgressStats = progStats => {
|
||||
return progStats.reduce(
|
||||
(all, prog) => {
|
||||
prog.forEach((yearStats, yearIndex) =>
|
||||
yearStats.forEach((category, categoryIndex) => {
|
||||
all[yearIndex][categoryIndex] += prog[yearIndex][categoryIndex]
|
||||
})
|
||||
)
|
||||
return all
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
progStats[0].map(year => year.map(_num => 0))
|
||||
)
|
||||
}
|
||||
|
||||
for (const facultyCode of facultyCodes) {
|
||||
const facultyData = codeToData[facultyCode]
|
||||
for (const year of universityData.years.slice(1).reverse()) {
|
||||
for (const fieldName of degreeNames) {
|
||||
if (!facultyData.creditCounts[fieldName] || Object.keys(facultyData.creditCounts[fieldName]).length === 0)
|
||||
continue
|
||||
if (!universityData.creditCounts[fieldName]) universityData.creditCounts[fieldName] = {}
|
||||
if (!universityData.creditCounts[fieldName][year]) universityData.creditCounts[fieldName][year] = []
|
||||
universityData.creditCounts[fieldName][year].push(...facultyData.creditCounts[fieldName][year])
|
||||
}
|
||||
for (const fieldName of [
|
||||
'bachelorsProgStats',
|
||||
'bcMsProgStats',
|
||||
'licentiateProgStats',
|
||||
'mastersProgStats',
|
||||
'doctoralProgStats',
|
||||
]) {
|
||||
if (Object.keys(facultyData[fieldName]).length === 0) continue
|
||||
if (!universityData[fieldName]) universityData[fieldName] = {}
|
||||
universityData[fieldName][facultyCode] = unifyProgressStats(Object.values(facultyData[fieldName]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove ELL bachelor+master progresstats for now, because it is problematic due to different credit limits
|
||||
// when expanding the table rows to show 'programme'(faculty) specific progress bars
|
||||
delete universityData.bcMsProgStats['H90']
|
||||
|
||||
return res.status(200).json(universityData)
|
||||
})
|
||||
|
||||
router.get('/allgraduationstats', async (req, res) => {
|
||||
const degreeNames = ['bachelor', 'bcMsCombo', 'master', 'licentiate', 'doctor']
|
||||
const allFaculties = await getFacultyList()
|
||||
const facultyCodes = allFaculties.map(f => f.code)
|
||||
const facultyData = {}
|
||||
const timesArrays = [] // keep book of these to null them in the end, large lists not used in frontend
|
||||
for (const facultyCode of facultyCodes) {
|
||||
const data = await getGraduationStats(facultyCode, 'NEW_STUDY_PROGRAMMES', true)
|
||||
if (!data) res.status(500).json({ message: `Did not find data for ${facultyCode}` })
|
||||
facultyData[facultyCode] = data
|
||||
}
|
||||
|
||||
const unifyTotals = (facultyData, universityData, isLast) => {
|
||||
for (const degree of degreeNames) {
|
||||
if (!universityData[degree]) universityData[degree] = []
|
||||
if (!facultyData[degree]) continue
|
||||
for (const yearStats of facultyData[degree]) {
|
||||
const universityStats = universityData[degree]
|
||||
const universityYearStats = universityData[degree].find(stats => stats.name === yearStats.name)
|
||||
if (!universityYearStats) {
|
||||
universityStats.push(yearStats)
|
||||
} else {
|
||||
universityYearStats.times.push(...yearStats.times)
|
||||
timesArrays.push(universityYearStats.times)
|
||||
universityYearStats.amount += yearStats.amount
|
||||
universityYearStats.statistics.onTime += yearStats.statistics.onTime
|
||||
universityYearStats.statistics.yearOver += yearStats.statistics.yearOver
|
||||
universityYearStats.statistics.wayOver += yearStats.statistics.wayOver
|
||||
if (isLast) {
|
||||
universityYearStats.y = getMedian(universityYearStats.times)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unifyProgrammeStats = (universityData, facultyData, facultyCode) => {
|
||||
for (const degree of degreeNames) {
|
||||
if (!facultyData[degree]) continue
|
||||
if (!universityData[degree]) universityData[degree] = {}
|
||||
for (const yearData of facultyData[degree]) {
|
||||
if (!universityData[degree][yearData.name]) {
|
||||
universityData[degree][yearData.name] = { programmes: [], data: [] }
|
||||
}
|
||||
const uniYearStats = universityData[degree][yearData.name]
|
||||
if (!uniYearStats.programmes.find(prog => prog === facultyCode)) {
|
||||
uniYearStats.programmes.push(facultyCode)
|
||||
}
|
||||
const uniYearFacultyStats = uniYearStats.data.find(item => item.code === facultyCode)
|
||||
const yearDataClone = { ...yearData, times: null, statistics: { ...yearData.statistics } }
|
||||
if (!uniYearFacultyStats) {
|
||||
uniYearStats.data.push({ ...yearDataClone, name: facultyCode, code: facultyCode })
|
||||
} else {
|
||||
uniYearFacultyStats.y = yearData.y
|
||||
uniYearFacultyStats.amount += yearData.amount
|
||||
uniYearFacultyStats.statistics.onTime += yearData.statistics.onTime
|
||||
uniYearFacultyStats.statistics.yearOver += yearData.statistics.yearOver
|
||||
uniYearFacultyStats.statistics.wayOver += yearData.statistics.wayOver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return universityData
|
||||
}
|
||||
|
||||
const universityData = {
|
||||
goals: {
|
||||
bachelor: 36,
|
||||
bcMsCombo: 60,
|
||||
master: 24,
|
||||
doctor: 48,
|
||||
licentiate: 78,
|
||||
},
|
||||
programmeNames: allFaculties.reduce((obj, fac) => {
|
||||
const { name, ...rest } = fac.dataValues
|
||||
obj[fac.code] = { ...rest, ...name }
|
||||
return obj
|
||||
}, {}),
|
||||
byGradYear: { medians: {}, programmes: { medians: {} } },
|
||||
classSizes: { programmes: {} },
|
||||
}
|
||||
|
||||
for (let i = 0; i < facultyCodes.length; i++) {
|
||||
const facultyCode = facultyCodes[i]
|
||||
const data = facultyData[facultyCode]
|
||||
unifyTotals(data.byGradYear.medians, universityData.byGradYear.medians, i === facultyCodes.length - 1)
|
||||
unifyProgrammeStats(universityData.byGradYear.programmes.medians, data.byGradYear.medians, facultyCode)
|
||||
for (const degree of degreeNames) {
|
||||
if (!universityData.classSizes[degree]) {
|
||||
universityData.classSizes[degree] = data.classSizes[degree]
|
||||
} else {
|
||||
Object.entries(data.classSizes[degree]).forEach(([key, value]) => {
|
||||
universityData.classSizes[degree][key] += value
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { programmes, ...rest } = data.classSizes
|
||||
universityData.classSizes.programmes[facultyCode] = rest
|
||||
}
|
||||
|
||||
// Empty "times" arrays because that's not needed anymore.
|
||||
timesArrays.forEach(arr => {
|
||||
arr.length = 0
|
||||
})
|
||||
|
||||
return res.status(200).json(universityData)
|
||||
})
|
||||
|
||||
router.get('/:id/studentstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const specialGroups = req.query?.special_groups
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
const router = require('express').Router()
|
||||
const { getFacultyList } = require('../services/faculty/facultyHelpers')
|
||||
const { getGraduationStats, getFacultyProgressStats } = require('../services/faculty/facultyService')
|
||||
|
||||
const { getMedian } = require('../services/studyprogramme/studyprogrammeHelpers')
|
||||
const degreeNames = ['bachelor', 'bachelorMaster', 'master', 'licentiate', 'doctor']
|
||||
|
||||
router.get('/allprogressstats', async (req, res) => {
|
||||
const specialGroups = 'SPECIAL_EXCLUDED'
|
||||
const graduated = req.query?.graduated
|
||||
const allFaculties = await getFacultyList()
|
||||
const facultyCodes = allFaculties.map(f => f.code)
|
||||
const codeToData = {}
|
||||
|
||||
for (const facultyCode of facultyCodes) {
|
||||
const data = await getFacultyProgressStats(facultyCode, specialGroups, graduated)
|
||||
if (!data) {
|
||||
return res.status(500).end()
|
||||
}
|
||||
codeToData[facultyCode] = data
|
||||
}
|
||||
|
||||
const universityData = {
|
||||
years: codeToData.H50.years,
|
||||
yearlyBachelorTitles: codeToData.H50.yearlyBachelorTitles,
|
||||
programmeNames: allFaculties.reduce((obj, fac) => {
|
||||
const { name, ...rest } = fac.dataValues
|
||||
obj[fac.code] = { ...rest, ...name }
|
||||
return obj
|
||||
}, {}),
|
||||
bachelorsProgStats: {},
|
||||
creditCounts: {
|
||||
bachelor: {},
|
||||
},
|
||||
}
|
||||
|
||||
const unifyProgressStats = progStats => {
|
||||
return progStats.reduce(
|
||||
(all, prog) => {
|
||||
prog.forEach((yearStats, yearIndex) =>
|
||||
yearStats.forEach((category, categoryIndex) => {
|
||||
all[yearIndex][categoryIndex] += prog[yearIndex][categoryIndex]
|
||||
})
|
||||
)
|
||||
return all
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
progStats[0].map(year => year.map(_num => 0))
|
||||
)
|
||||
}
|
||||
|
||||
for (const facultyCode of facultyCodes) {
|
||||
const facultyData = codeToData[facultyCode]
|
||||
for (const year of universityData.years.slice(1).reverse()) {
|
||||
for (const fieldName of degreeNames) {
|
||||
if (!facultyData.creditCounts[fieldName] || Object.keys(facultyData.creditCounts[fieldName]).length === 0)
|
||||
continue
|
||||
if (!universityData.creditCounts[fieldName]) universityData.creditCounts[fieldName] = {}
|
||||
if (!universityData.creditCounts[fieldName][year]) universityData.creditCounts[fieldName][year] = []
|
||||
universityData.creditCounts[fieldName][year].push(...facultyData.creditCounts[fieldName][year])
|
||||
}
|
||||
for (const fieldName of [
|
||||
'bachelorsProgStats',
|
||||
'bcMsProgStats',
|
||||
'licentiateProgStats',
|
||||
'mastersProgStats',
|
||||
'doctoralProgStats',
|
||||
]) {
|
||||
if (Object.keys(facultyData[fieldName]).length === 0) continue
|
||||
if (!universityData[fieldName]) universityData[fieldName] = {}
|
||||
universityData[fieldName][facultyCode] = unifyProgressStats(Object.values(facultyData[fieldName]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove ELL bachelor+master progresstats for now, because it is problematic due to different credit limits
|
||||
// when expanding the table rows to show 'programme'(faculty) specific progress bars
|
||||
delete universityData.bcMsProgStats['H90']
|
||||
|
||||
return res.status(200).json(universityData)
|
||||
})
|
||||
|
||||
router.get('/allgraduationstats', async (req, res) => {
|
||||
const degreeNames = ['bachelor', 'bcMsCombo', 'master', 'licentiate', 'doctor']
|
||||
const allFaculties = await getFacultyList()
|
||||
const facultyCodes = allFaculties.map(f => f.code)
|
||||
const facultyData = {}
|
||||
const timesArrays = [] // keep book of these to null them in the end, large lists not used in frontend
|
||||
for (const facultyCode of facultyCodes) {
|
||||
const data = await getGraduationStats(facultyCode, 'NEW_STUDY_PROGRAMMES', true)
|
||||
if (!data) res.status(500).json({ message: `Did not find data for ${facultyCode}` })
|
||||
facultyData[facultyCode] = data
|
||||
}
|
||||
|
||||
const unifyTotals = (facultyData, universityData, isLast) => {
|
||||
for (const degree of degreeNames) {
|
||||
if (!universityData[degree]) universityData[degree] = []
|
||||
if (!facultyData[degree]) continue
|
||||
for (const yearStats of facultyData[degree]) {
|
||||
const universityStats = universityData[degree]
|
||||
const universityYearStats = universityData[degree].find(stats => stats.name === yearStats.name)
|
||||
if (!universityYearStats) {
|
||||
universityStats.push(yearStats)
|
||||
} else {
|
||||
universityYearStats.times.push(...yearStats.times)
|
||||
timesArrays.push(universityYearStats.times)
|
||||
universityYearStats.amount += yearStats.amount
|
||||
universityYearStats.statistics.onTime += yearStats.statistics.onTime
|
||||
universityYearStats.statistics.yearOver += yearStats.statistics.yearOver
|
||||
universityYearStats.statistics.wayOver += yearStats.statistics.wayOver
|
||||
if (isLast) {
|
||||
universityYearStats.y = getMedian(universityYearStats.times)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unifyProgrammeStats = (universityData, facultyData, facultyCode) => {
|
||||
for (const degree of degreeNames) {
|
||||
if (!facultyData[degree]) continue
|
||||
if (!universityData[degree]) universityData[degree] = {}
|
||||
for (const yearData of facultyData[degree]) {
|
||||
if (!universityData[degree][yearData.name]) {
|
||||
universityData[degree][yearData.name] = { programmes: [], data: [] }
|
||||
}
|
||||
const uniYearStats = universityData[degree][yearData.name]
|
||||
if (!uniYearStats.programmes.find(prog => prog === facultyCode)) {
|
||||
uniYearStats.programmes.push(facultyCode)
|
||||
}
|
||||
const uniYearFacultyStats = uniYearStats.data.find(item => item.code === facultyCode)
|
||||
const yearDataClone = { ...yearData, times: null, statistics: { ...yearData.statistics } }
|
||||
if (!uniYearFacultyStats) {
|
||||
uniYearStats.data.push({ ...yearDataClone, name: facultyCode, code: facultyCode })
|
||||
} else {
|
||||
uniYearFacultyStats.y = yearData.y
|
||||
uniYearFacultyStats.amount += yearData.amount
|
||||
uniYearFacultyStats.statistics.onTime += yearData.statistics.onTime
|
||||
uniYearFacultyStats.statistics.yearOver += yearData.statistics.yearOver
|
||||
uniYearFacultyStats.statistics.wayOver += yearData.statistics.wayOver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return universityData
|
||||
}
|
||||
|
||||
const universityData = {
|
||||
goals: {
|
||||
bachelor: 36,
|
||||
bcMsCombo: 60,
|
||||
master: 24,
|
||||
doctor: 48,
|
||||
licentiate: 78,
|
||||
},
|
||||
programmeNames: allFaculties.reduce((obj, fac) => {
|
||||
const { name, ...rest } = fac.dataValues
|
||||
obj[fac.code] = { ...rest, ...name }
|
||||
return obj
|
||||
}, {}),
|
||||
byGradYear: { medians: {}, programmes: { medians: {} } },
|
||||
classSizes: { programmes: {} },
|
||||
}
|
||||
|
||||
for (let i = 0; i < facultyCodes.length; i++) {
|
||||
const facultyCode = facultyCodes[i]
|
||||
const data = facultyData[facultyCode]
|
||||
unifyTotals(data.byGradYear.medians, universityData.byGradYear.medians, i === facultyCodes.length - 1)
|
||||
unifyProgrammeStats(universityData.byGradYear.programmes.medians, data.byGradYear.medians, facultyCode)
|
||||
for (const degree of degreeNames) {
|
||||
if (!universityData.classSizes[degree]) {
|
||||
universityData.classSizes[degree] = data.classSizes[degree]
|
||||
} else {
|
||||
Object.entries(data.classSizes[degree]).forEach(([key, value]) => {
|
||||
universityData.classSizes[degree][key] += value
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { programmes, ...rest } = data.classSizes
|
||||
universityData.classSizes.programmes[facultyCode] = rest
|
||||
}
|
||||
|
||||
// Empty "times" arrays because that's not needed anymore.
|
||||
timesArrays.forEach(arr => {
|
||||
arr.length = 0
|
||||
})
|
||||
|
||||
return res.status(200).json(universityData)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -289,9 +289,9 @@ const count = async (
|
||||
y: median,
|
||||
amount: graduationAmounts[level][year],
|
||||
/* Include invidivual graduation times for the sake of university-level evaluation overview (UniversityView)
|
||||
which has to count medians by itself - but this is not needed for startYear as that is not enabled there
|
||||
which has to count medians by itself
|
||||
*/
|
||||
times: mode === 'startYear' ? graduationTimes[level][year] : null,
|
||||
times: graduationTimes[level][year],
|
||||
statistics,
|
||||
name: year,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
const { codes } = require('../../../config/programmeCodes')
|
||||
const { faculties } = require('../organisations')
|
||||
|
||||
const getFacultyList = async () => {
|
||||
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
|
||||
const facultyList = (await faculties()).filter(f => !ignore.includes(f.code))
|
||||
facultyList.sort((a, b) => (a.name.fi > b.name.fi ? 1 : -1))
|
||||
return facultyList
|
||||
}
|
||||
|
||||
const findRightProgramme = (studyrightElements, code) => {
|
||||
let programme = ''
|
||||
@@ -140,6 +148,7 @@ const mapCodesToIds = data => {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getFacultyList,
|
||||
findRightProgramme,
|
||||
facultyFormatStudyright,
|
||||
facultyFormatProgramme,
|
||||
|
||||
@@ -161,6 +161,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
'admission_type',
|
||||
'cancelled',
|
||||
'is_ba_ma',
|
||||
'semester_enrollments',
|
||||
],
|
||||
separate: true,
|
||||
include: [
|
||||
|
||||
@@ -8,7 +8,10 @@ const { combine, timestamp, printf, splat } = winston.format
|
||||
|
||||
let transports = []
|
||||
|
||||
const formatDate = timestamp => new Date(timestamp).toLocaleTimeString('fi-FI').replace(' klo', '')
|
||||
const formatDate = timestamp => {
|
||||
if (!isProduction) return new Date(timestamp).toLocaleTimeString('fi-FI').replace(' klo', '')
|
||||
return new Date(timestamp).toLocaleString('fi-FI').replace(' klo', '')
|
||||
}
|
||||
|
||||
const consoleFormat = printf(
|
||||
({ level, message, timestamp }) => `${formatDate(timestamp).split(' ')} ${level}: ${message}`
|
||||
|
||||
@@ -13,7 +13,7 @@ export const courseStatisticsToolTips = {
|
||||
Arvosanajakaumaa katsoessasi voit muuttaa luvut prosenteiksi, klikkaamalla painiketta "Relative".
|
||||
Mahdollinen sarake "Other passed" sisältää sekä arvosanan "Hyv." saaneet että kurssin hyväksilukeneet.
|
||||
`,
|
||||
STUDENT: `
|
||||
STUDENTS: `
|
||||
**Table - Students**
|
||||
|
||||
Taulukko näyttää yksittäiset opiskelijat, jotka ovat kurssia suorittaneet.
|
||||
@@ -35,7 +35,7 @@ export const courseStatisticsToolTips = {
|
||||
Hylätyt suoritukset sisältävät arvosanat Hyl. ja 0, sekä suoritukset, joille opettaja on antanut merkinnän
|
||||
"Luop" (Luopunut suorituksesta) tai "Eisa" (Ei saapunut tenttiin).
|
||||
`,
|
||||
STUDENT: `
|
||||
STUDENTS: `
|
||||
**Pass rate chart - Students**
|
||||
|
||||
Graafi näyttää yksittäiset opiskelijat, jotka ovat kurssia suorittaneet.
|
||||
|
||||
@@ -281,6 +281,11 @@ export const isMastersProgramme = programmeCode => programmeCode.startsWith('MH'
|
||||
|
||||
export const getMonthsForDegree = code => getTargetCreditsForProgramme(code) / (60 / 12)
|
||||
|
||||
export const calculatePercentage = (numerator, denominator) =>
|
||||
new Intl.NumberFormat(undefined, { style: 'percent', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(
|
||||
numerator / denominator
|
||||
)
|
||||
|
||||
/**
|
||||
* Get start and end dates for study right element. For bachelor a 3 year target is used
|
||||
* and for masters a 2 year target. Any absences within the study right element extends the end date.
|
||||
|
||||
@@ -9,10 +9,9 @@ export const HelpButton = ({ tab, viewMode }) => {
|
||||
|
||||
if (viewMode) {
|
||||
content = content[viewMode]
|
||||
content = content.replace(/\n +/g, '\n')
|
||||
}
|
||||
|
||||
content = content.replace(/\n +/g, '\n')
|
||||
|
||||
return (
|
||||
<Popup
|
||||
on="click"
|
||||
|
||||
+3
-3
@@ -189,7 +189,7 @@ const getColumns = (stats, showDetails, showGrades, userHasAccessToAllStats, alt
|
||||
})
|
||||
}
|
||||
|
||||
export const StudentTable = ({
|
||||
export const StudentsTable = ({
|
||||
data: { name, stats },
|
||||
settings: { showDetails, separate, showGrades },
|
||||
alternatives,
|
||||
@@ -231,13 +231,13 @@ export const StudentTable = ({
|
||||
)
|
||||
}
|
||||
|
||||
StudentTable.propTypes = {
|
||||
StudentsTable.propTypes = {
|
||||
data: object.isRequired,
|
||||
alternatives: arrayOf(string).isRequired,
|
||||
separate: bool,
|
||||
userHasAccessToAllStats: bool.isRequired,
|
||||
}
|
||||
|
||||
StudentTable.defaultProps = {
|
||||
StudentsTable.defaultProps = {
|
||||
separate: false,
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Menu, Radio } from 'semantic-ui-react'
|
||||
import { getCourseAlternatives } from '../../../../selectors/courseStats'
|
||||
import { HelpButton } from '../HelpButton'
|
||||
import { UnifyRadioButtons } from '../UnifyRadioButtons'
|
||||
import { AttemptsTable } from './Tables/attempts'
|
||||
|
||||
export const AttemptsTableSettings = ({ value, onChange, availableStats, onSeparateChange }) => {
|
||||
const { showGrades, separate } = value
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu style={{ flexWrap: 'wrap' }} secondary>
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Show grades"
|
||||
data-cy="gradeToggle"
|
||||
checked={showGrades}
|
||||
onChange={() => onChange({ ...value, showGrades: !showGrades })}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Separate by semesters"
|
||||
data-cy="separateToggle"
|
||||
checked={separate}
|
||||
onChange={() => onSeparateChange(!separate)}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<HelpButton tab="Tables" viewMode="ATTEMPTS" />
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
<UnifyRadioButtons availableStats={availableStats} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const AttemptsTableContent = ({ settings, ...otherProps }) => {
|
||||
const alternatives = useSelector(getCourseAlternatives)
|
||||
const openOrRegular = useSelector(state => state.courseSearch.openOrRegular)
|
||||
return <AttemptsTable settings={settings} {...otherProps} alternatives={alternatives} unifyCourses={openOrRegular} />
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import React from 'react'
|
||||
import { Menu, Radio } from 'semantic-ui-react'
|
||||
import ReactHighcharts from 'react-highcharts'
|
||||
|
||||
import { HelpButton } from '../HelpButton'
|
||||
import { Menu, Radio } from 'semantic-ui-react'
|
||||
import { gradeGraphOptions } from '../../../../constants'
|
||||
import { HelpButton } from '../HelpButton'
|
||||
import {
|
||||
absoluteToRelative,
|
||||
getDataObject,
|
||||
getMaxValueOfSeries,
|
||||
getGradeSpread,
|
||||
getMaxValueOfSeries,
|
||||
getThesisGradeSpread,
|
||||
isThesisSeries,
|
||||
absoluteToRelative,
|
||||
} from './util'
|
||||
|
||||
const getGradeSeries = series => {
|
||||
@@ -98,9 +97,9 @@ export const Distribution = ({ data, settings: { isRelative }, userHasAccessToAl
|
||||
|
||||
const gradeGraphSeries = getGradeSeries(grades)
|
||||
|
||||
const maxGradeValue = isRelative ? 1 : getMaxValueOfSeries(gradeGraphSeries.absolute)
|
||||
const maxGradeValue = isRelative ? 100 : getMaxValueOfSeries(gradeGraphSeries.absolute)
|
||||
|
||||
const primaryDistributionOptions = gradeGraphOptions(statYears, maxGradeValue, 'Grades')
|
||||
const primaryDistributionOptions = gradeGraphOptions(isRelative, statYears, maxGradeValue, 'Grades')
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -99,15 +99,13 @@ export const PassRate = ({ data, settings: { viewMode, isRelative }, userHasAcce
|
||||
|
||||
const stats = data.stats.filter(stat => stat.name !== 'Total')
|
||||
const statYears = stats.map(year => year.name)
|
||||
// const comparisonStats = comparison ? comparison.stats : []
|
||||
const passGraphSerieFn = isAttemptsMode ? getPassRateAttemptSeriesFromStats : getPassRateStudSeriesFromStats
|
||||
|
||||
const passGraphSerie = passGraphSerieFn(stats)
|
||||
// const comparisonGraphSerie = passGraphSerieFn(comparisonStats)
|
||||
|
||||
const maxPassRateVal = isRelative ? 1 : getMaxValueOfSeries(passGraphSerie.absolute)
|
||||
const maxPassRateVal = isRelative ? 100 : getMaxValueOfSeries(passGraphSerie.absolute)
|
||||
const graphOptionsFn = isAttemptsMode ? passRateAttemptGraphOptions : passRateStudGraphOptions
|
||||
const primaryGraphOptions = graphOptionsFn(statYears, maxPassRateVal, 'Pass rate chart', isRelative)
|
||||
const primaryGraphOptions = graphOptionsFn(isRelative, statYears, maxPassRateVal, 'Pass rate chart', isRelative)
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
+8
-29
@@ -1,39 +1,22 @@
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Menu, Radio } from 'semantic-ui-react'
|
||||
|
||||
import { viewModeNames } from './util'
|
||||
import { HelpButton } from '../HelpButton'
|
||||
import { StudentTable } from './Tables/student'
|
||||
import { AttemptsTable } from './Tables/attempts'
|
||||
import { getCourseAlternatives } from '../../../../selectors/courseStats'
|
||||
import { HelpButton } from '../HelpButton'
|
||||
import { UnifyRadioButtons } from '../UnifyRadioButtons'
|
||||
import { StudentsTable } from './Tables/students'
|
||||
|
||||
export const TablesSettings = ({ value, onChange, availableStats, onSeparateChange }) => {
|
||||
const { viewMode, showDetails, showGrades, separate } = value
|
||||
export const StudentsTableSettings = ({ value, onChange, availableStats, onSeparateChange }) => {
|
||||
const { showDetails, showGrades, separate } = value
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu style={{ flexWrap: 'wrap' }} secondary>
|
||||
{Object.entries(viewModeNames).map(([key, name]) => (
|
||||
<Menu.Item
|
||||
key={key}
|
||||
active={viewMode === key}
|
||||
data-cy={`viewMode-${name}`}
|
||||
name={name}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
...value,
|
||||
viewMode: key,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<Menu.Item>
|
||||
<Radio
|
||||
toggle
|
||||
label="Show details"
|
||||
disabled={viewMode !== 'STUDENT'}
|
||||
data-cy="detailToggle"
|
||||
checked={showDetails}
|
||||
onChange={() => onChange({ ...value, showDetails: !showDetails })}
|
||||
/>
|
||||
@@ -56,9 +39,8 @@ export const TablesSettings = ({ value, onChange, availableStats, onSeparateChan
|
||||
onChange={() => onSeparateChange(!separate)}
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item>
|
||||
<HelpButton tab="Tables" viewMode={viewMode} />
|
||||
<HelpButton tab="Tables" viewMode="STUDENTS" />
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
<UnifyRadioButtons availableStats={availableStats} />
|
||||
@@ -66,11 +48,8 @@ export const TablesSettings = ({ value, onChange, availableStats, onSeparateChan
|
||||
)
|
||||
}
|
||||
|
||||
export const Tables = ({ settings, ...otherProps }) => {
|
||||
export const StudentsTableContent = ({ settings, ...otherProps }) => {
|
||||
const alternatives = useSelector(getCourseAlternatives)
|
||||
const viewModes = { ATTEMPTS: AttemptsTable, STUDENT: StudentTable }
|
||||
const Content = viewModes[settings.viewMode]
|
||||
const openOrRegular = useSelector(state => state.courseSearch.openOrRegular)
|
||||
|
||||
return <Content settings={settings} {...otherProps} alternatives={alternatives} unifyCourses={openOrRegular} />
|
||||
return <StudentsTable settings={settings} {...otherProps} alternatives={alternatives} unifyCourses={openOrRegular} />
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { flatten } from 'lodash'
|
||||
import { calculatePercentage } from 'common'
|
||||
|
||||
const gradesMap = {
|
||||
0: 0,
|
||||
@@ -19,7 +20,7 @@ const gradesMap = {
|
||||
}
|
||||
|
||||
export const viewModeNames = {
|
||||
STUDENT: 'Students',
|
||||
STUDENTS: 'Students',
|
||||
ATTEMPTS: 'Attempts',
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ export const isThesisGrades = grades => Object.keys(grades).some(k => THESIS_GRA
|
||||
|
||||
export const isThesisSeries = series => series && series.some(s => isThesisGrades(s))
|
||||
|
||||
export const absoluteToRelative = all => (p, i) => p / all[i] || 0
|
||||
export const absoluteToRelative = all => (p, i) => parseFloat(calculatePercentage(p, all[i]).slice(0, -1))
|
||||
|
||||
export const resolveGrades = stats => {
|
||||
const failedGrades = ['eisa', 'hyl.', 'hyl', '0', 'luop']
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Tab, Segment, Menu, Icon } from 'semantic-ui-react'
|
||||
import { useHistory, useLocation } from 'react-router-dom'
|
||||
import qs from 'query-string'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { getCourseStats } from 'redux/coursestats'
|
||||
import { PassRate, PassRateSettings } from './Panes/passRate'
|
||||
import { Distribution, DistributionSettings } from './Panes/distribution'
|
||||
import { Tables, TablesSettings } from './Panes/tables'
|
||||
import { useProgress, useTabs } from '../../../common/hooks'
|
||||
import { AttemptsTableContent, AttemptsTableSettings } from './Panes/attempts'
|
||||
import { Distribution, DistributionSettings } from './Panes/distribution'
|
||||
import { PassRate, PassRateSettings } from './Panes/passRate'
|
||||
import { StudentsTableContent, StudentsTableSettings } from './Panes/students'
|
||||
|
||||
import './resultTabs.css'
|
||||
|
||||
@@ -97,23 +97,30 @@ export const ResultTabs = ({ primary, comparison, separate, availableStats }) =>
|
||||
|
||||
const paneTypes = [
|
||||
{
|
||||
label: 'Tables',
|
||||
icon: 'table',
|
||||
initialSettings: { showDetails: false, viewMode: 'STUDENT', separate },
|
||||
settings: TablesSettings,
|
||||
component: Tables,
|
||||
label: 'Students',
|
||||
icon: 'user',
|
||||
initialSettings: { showDetails: false, separate },
|
||||
settings: StudentsTableSettings,
|
||||
component: StudentsTableContent,
|
||||
},
|
||||
{
|
||||
label: 'Attempts',
|
||||
icon: 'redo',
|
||||
initialSettings: { separate },
|
||||
settings: AttemptsTableSettings,
|
||||
component: AttemptsTableContent,
|
||||
},
|
||||
{
|
||||
label: 'Pass rate chart',
|
||||
icon: 'balance',
|
||||
initialSettings: { viewMode: 'STUDENT', separate },
|
||||
initialSettings: { viewMode: 'STUDENTS', separate },
|
||||
settings: PassRateSettings,
|
||||
component: PassRate,
|
||||
},
|
||||
{
|
||||
label: 'Grade distribution chart',
|
||||
icon: 'chart bar',
|
||||
initialSettings: { isRelative: false, viewMode: 'STUDENT' },
|
||||
initialSettings: { isRelative: false, viewMode: 'STUDENTS' },
|
||||
settings: DistributionSettings,
|
||||
component: Distribution,
|
||||
},
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Divider, Header, Loader, Message } from 'semantic-ui-react'
|
||||
|
||||
import {
|
||||
useGetFacultiesQuery,
|
||||
useGetAllFacultiesProgressStatsQuery,
|
||||
useGetAllFacultiesGraduationStatsQuery,
|
||||
} from 'redux/facultyStats'
|
||||
import { useGetAllFacultiesProgressStatsQuery, useGetAllFacultiesGraduationStatsQuery } from 'redux/facultyStats'
|
||||
import { facultyToolTips } from 'common/InfoToolTips'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useGetAuthorizedUserQuery } from 'redux/auth'
|
||||
import { useLanguage } from 'components/LanguagePicker/useLanguage'
|
||||
import { orderBy } from 'lodash'
|
||||
import { FacultyProgress } from './FacultyProgress'
|
||||
import { Toggle } from '../StudyProgramme/Toggle'
|
||||
import { InfoBox } from '../Info/InfoBox'
|
||||
@@ -16,14 +15,14 @@ import { FacultyGraduations } from './FacultyGraduations'
|
||||
export const UniversityView = ({ faculty }) => {
|
||||
const [graduatedGroup, setGraduatedGroup] = useState(false)
|
||||
const [medianMode, setMedianMode] = useState(false)
|
||||
const { isAdmin, roles } = useGetAuthorizedUserQuery()
|
||||
const userHasFacultyRights = isAdmin || roles.includes('facultyStatistics') || roles.includes('katselmusViewer')
|
||||
const graduated = graduatedGroup ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
|
||||
const allFaculties = useGetFacultiesQuery()
|
||||
const progressStats = useGetAllFacultiesProgressStatsQuery({
|
||||
graduated,
|
||||
})
|
||||
|
||||
const { getTextIn } = useLanguage()
|
||||
const graduationStats = useGetAllFacultiesGraduationStatsQuery()
|
||||
|
||||
const getDivider = (title, toolTipText, content, cypress = undefined) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
@@ -35,20 +34,17 @@ export const UniversityView = ({ faculty }) => {
|
||||
</>
|
||||
)
|
||||
|
||||
if (allFaculties.isLoading || allFaculties.isFetching) {
|
||||
if (graduationStats.isLoading || graduationStats.isFetching || progressStats.isFetching || progressStats.isLoading) {
|
||||
return <Loader active style={{ marginTop: '10em' }} />
|
||||
}
|
||||
|
||||
const isFetchingOrLoading =
|
||||
progressStats.isLoading || progressStats.isFetching || graduationStats.isLoading || graduationStats.isFetching
|
||||
|
||||
const isError =
|
||||
progressStats.isError ||
|
||||
(progressStats.isSuccess && !progressStats.data) ||
|
||||
graduationStats.isError ||
|
||||
!graduationStats.data
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
const allFaculties = Object.values(progressStats.data.programmeNames)
|
||||
return (
|
||||
<>
|
||||
<div style={{ padding: '30px', textAlign: 'center' }}>
|
||||
@@ -68,34 +64,45 @@ export const UniversityView = ({ faculty }) => {
|
||||
a target time of 6 years, whereas the other degrees in that category have a target of 5 years.{' '}
|
||||
</p>
|
||||
</Message>
|
||||
<div className="faculty-overview">
|
||||
{isFetchingOrLoading ? (
|
||||
<Loader active style={{ marginTop: '15em' }} />
|
||||
) : (
|
||||
<div className="programmes-overview">
|
||||
{progressStats.isSuccess && progressStats.data && (
|
||||
<>
|
||||
{getDivider(
|
||||
'Progress of students of the faculty ',
|
||||
'BachelorStudentsOfTheFacultyByStartingYear',
|
||||
facultyToolTips.StudentProgress,
|
||||
'InfoFacultyProgress'
|
||||
)}
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="GraduatedToggle"
|
||||
toolTips={facultyToolTips.GraduatedToggle}
|
||||
firstLabel="Graduated included"
|
||||
secondLabel="Graduated excluded"
|
||||
value={graduatedGroup}
|
||||
setValue={setGraduatedGroup}
|
||||
/>
|
||||
</div>
|
||||
<FacultyProgress faculty="ALL" progressStats={progressStats} getDivider={getDivider} />
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
{userHasFacultyRights && (
|
||||
<div>
|
||||
<p>
|
||||
<b>Click here to open the corresponding view for an individual faculty</b>
|
||||
</p>
|
||||
<div className="facultyLinkBox">
|
||||
{orderBy(allFaculties, 'code').map(faculty => (
|
||||
<span key={faculty.code}>
|
||||
<Link style={{ marginTop: '5px' }} to={`/evaluationoverview/faculty/${faculty.code}`}>{`${
|
||||
faculty.code
|
||||
} ${getTextIn(faculty)}`}</Link>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="faculty-overview">
|
||||
<div className="programmes-overview">
|
||||
{getDivider(
|
||||
'Progress of students of the faculty ',
|
||||
'BachelorStudentsOfTheFacultyByStartingYear',
|
||||
facultyToolTips.StudentProgress,
|
||||
'InfoFacultyProgress'
|
||||
)}
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="GraduatedToggle"
|
||||
toolTips={facultyToolTips.GraduatedToggle}
|
||||
firstLabel="Graduated included"
|
||||
secondLabel="Graduated excluded"
|
||||
value={graduatedGroup}
|
||||
setValue={setGraduatedGroup}
|
||||
/>
|
||||
</div>
|
||||
<FacultyProgress faculty="ALL" progressStats={progressStats} getDivider={getDivider} />
|
||||
</div>
|
||||
|
||||
{getDivider('Average graduation times', 'AverageGraduationTimes', facultyToolTips.AverageGraduationTimes)}
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
|
||||
@@ -150,3 +150,12 @@
|
||||
justify-content: space-between;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
.facultyLinkBox {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(4, 30%);
|
||||
grid-template-columns: repeat(3, 30%);
|
||||
row-gap: 0;
|
||||
column-gap: 8px;
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ const allNavigationItems = {
|
||||
{ path: '/study-programme', key: 'overview', label: 'Overview' },
|
||||
],
|
||||
},
|
||||
courseStatistics: { path: '/coursestatistics', key: 'courseStatistics', label: 'Course statistics' },
|
||||
students: { path: '/students', key: 'students', label: 'Student statistics' },
|
||||
courseStatistics: { path: '/coursestatistics', key: 'courseStatistics', label: 'Courses' },
|
||||
students: { path: '/students', key: 'students', label: 'Students' },
|
||||
teachers: { path: '/teachers', key: 'teachers', label: 'Teachers', reqRights: ['teachers'] },
|
||||
users: { path: '/users', key: 'users', label: 'Users', reqRights: ['admin'] },
|
||||
studyGuidanceGroups: {
|
||||
|
||||
+25
-13
@@ -52,12 +52,29 @@ export const GeneralTab = ({
|
||||
|
||||
if (!populationStatistics || !populationStatistics.elementdetails) return null
|
||||
|
||||
const createSemesterEnrollmentsMap = student =>
|
||||
student.semesterenrollments?.reduce((enrollments, enrollment) => {
|
||||
const newEnrollmentsObject = { ...enrollments }
|
||||
newEnrollmentsObject[enrollment.semestercode] = enrollment.enrollmenttype
|
||||
return newEnrollmentsObject
|
||||
const studyGuidanceGroupProgrammes =
|
||||
group?.tags?.studyProgramme && group?.tags?.studyProgramme.includes('+')
|
||||
? group?.tags?.studyProgramme.split('+')
|
||||
: [group?.tags?.studyProgramme]
|
||||
|
||||
const programmeCode = query?.studyRights?.programme || studyGuidanceGroupProgrammes[0] || customPopulationProgramme
|
||||
|
||||
const createSemesterEnrollmentsMap = student => {
|
||||
let semesterEnrollments
|
||||
|
||||
if (programmeCode) {
|
||||
semesterEnrollments = student.studyrights.find(sr =>
|
||||
sr.studyright_elements.some(e => e.code === programmeCode)
|
||||
)?.semester_enrollments
|
||||
}
|
||||
|
||||
semesterEnrollments = semesterEnrollments ?? student.semesterenrollments
|
||||
|
||||
return semesterEnrollments.reduce((enrollments, enrollment) => {
|
||||
enrollments[enrollment.semestercode] = enrollment.enrollmenttype
|
||||
return enrollments
|
||||
}, {})
|
||||
}
|
||||
|
||||
const selectedStudents = filteredStudents.map(stu => stu.studentNumber)
|
||||
const students = Object.fromEntries(
|
||||
@@ -71,21 +88,16 @@ export const GeneralTab = ({
|
||||
.map(stu => [stu.studentNumber, stu])
|
||||
)
|
||||
|
||||
const getCombinedProgrammeCode = (query, studyGuidangeGroupProgrammes) => {
|
||||
const getCombinedProgrammeCode = (query, studyGuidanceGroupProgrammes) => {
|
||||
if (query && query?.studyRights?.combinedProgramme) return query.studyRights.combinedProgramme
|
||||
if (studyGuidangeGroupProgrammes.length > 1) return studyGuidangeGroupProgrammes[1]
|
||||
if (studyGuidanceGroupProgrammes.length > 1) return studyGuidanceGroupProgrammes[1]
|
||||
return ''
|
||||
}
|
||||
|
||||
const queryStudyrights = query ? Object.values(query.studyRights) : []
|
||||
const cleanedQueryStudyrights = queryStudyrights.filter(sr => !!sr)
|
||||
const studyGuidangeGroupProgrammes =
|
||||
group?.tags?.studyProgramme && group?.tags?.studyProgramme.includes('+')
|
||||
? group?.tags?.studyProgramme.split('+')
|
||||
: [group?.tags?.studyProgramme]
|
||||
|
||||
const programmeCode = query?.studyRights?.programme || studyGuidangeGroupProgrammes[0] || customPopulationProgramme
|
||||
const combinedProgrammeCode = getCombinedProgrammeCode(query, studyGuidangeGroupProgrammes)
|
||||
const combinedProgrammeCode = getCombinedProgrammeCode(query, studyGuidanceGroupProgrammes)
|
||||
|
||||
const {
|
||||
studentToStudyrightStartMap,
|
||||
|
||||
+6
-2
@@ -64,6 +64,7 @@ export const getSemestersPresentFunctions = ({
|
||||
}
|
||||
|
||||
const graduatedOnSemester = (student, sem, programmeCode) => {
|
||||
if (!programmeCode) return 0
|
||||
const firstGraduation = studentToStudyrightEndMap[student.studentNumber]
|
||||
const secondGraduation = studentToSecondStudyrightEndMap[student.studentNumber]
|
||||
if (
|
||||
@@ -81,7 +82,7 @@ export const getSemestersPresentFunctions = ({
|
||||
return 0
|
||||
}
|
||||
|
||||
const getSemesterEnrollmentsContent = student => {
|
||||
const getSemesterEnrollmentsContent = (student, studyright) => {
|
||||
if (allSemesters.length === 0) return ''
|
||||
if (!student.semesterenrollments) return ''
|
||||
const semesterIcons = []
|
||||
@@ -136,10 +137,13 @@ export const getSemestersPresentFunctions = ({
|
||||
}
|
||||
|
||||
for (let sem = firstSemester; sem <= lastSemester; sem++) {
|
||||
const enrollment = student.semesterEnrollmentsMap
|
||||
? student.semesterEnrollmentsMap[sem]
|
||||
: studyright[0].semesterEnrollments.find(e => e.semestercode === sem)?.enrollmenttype
|
||||
semesterIcons.push(
|
||||
getSemesterJSX(
|
||||
sem,
|
||||
student.semesterEnrollmentsMap[sem],
|
||||
enrollment,
|
||||
graduatedOnSemester(student, sem, programmeCode),
|
||||
`${student.studentNumber}-${sem}`
|
||||
)
|
||||
|
||||
@@ -37,6 +37,9 @@ export const ProtectedRoute = ({ requiredRoles = [], requireUserHasRights = fals
|
||||
return iamGroups.includes('grp-kielikeskus-esihenkilot')
|
||||
}
|
||||
if (rest.path.includes('evaluationoverview')) {
|
||||
if (rest.location.pathname.includes('university')) {
|
||||
return true
|
||||
}
|
||||
return roles?.length > 0 || rights?.length > 0
|
||||
}
|
||||
|
||||
|
||||
+101
-89
@@ -1,107 +1,119 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Icon, Accordion, Table, Popup } from 'semantic-ui-react'
|
||||
import { array } from 'prop-types'
|
||||
import _ from 'lodash'
|
||||
|
||||
import './studentInfoCard.css'
|
||||
import { useGetSemestersQuery } from 'redux/semesters'
|
||||
import { useLanguage } from 'components/LanguagePicker/useLanguage'
|
||||
import { getSemestersPresentFunctions } from 'components/PopulationStudents/StudentTable/GeneralTab/columnHelpers/semestersPresent'
|
||||
|
||||
const enrolmentTypes = {
|
||||
1: { text: 'Present', className: 'label-present' },
|
||||
2: { text: 'Absent', className: 'label-absent' },
|
||||
3: { text: 'Inactive', className: 'label-passive' },
|
||||
STATUTORY: { text: 'Absent, statutory', className: 'label-absent-statutory' },
|
||||
const calculateSemesterEnrollmentsByStudyright = (semestersAndYears, studyrights) => {
|
||||
const { semesters, years } = semestersAndYears
|
||||
const programmeNames = {}
|
||||
let firstYear
|
||||
|
||||
const enrollmentsByStudyright = Object.values(studyrights).reduce((enrollments, studyright) => {
|
||||
// Let's choose the first studyright as they are all linked to the same actual (Sisu) studyright
|
||||
studyright[0].semesterEnrollments.forEach(enrollment => {
|
||||
const year = years[semesters[enrollment.semestercode].yearcode]
|
||||
const enrollmentYear = new Date(year.startdate).getFullYear()
|
||||
|
||||
if (!firstYear || enrollmentYear < firstYear) {
|
||||
firstYear = enrollmentYear
|
||||
}
|
||||
|
||||
const studyrightId = studyright[0].actual_studyrightid
|
||||
if (!enrollments[studyrightId]) {
|
||||
enrollments[studyrightId] = {}
|
||||
}
|
||||
enrollments[studyrightId][enrollment.semestercode] = enrollment.enrollmenttype
|
||||
})
|
||||
|
||||
studyright.forEach(element => {
|
||||
const studyrightId = element.actual_studyrightid
|
||||
if (!programmeNames[studyrightId]) {
|
||||
programmeNames[studyrightId] = []
|
||||
}
|
||||
element.studyright_elements
|
||||
.filter(elem => elem.element_detail.type === 20)
|
||||
.forEach(elem => {
|
||||
programmeNames[studyrightId].push(elem.element_detail.name)
|
||||
})
|
||||
})
|
||||
return enrollments
|
||||
}, {})
|
||||
|
||||
return { enrollmentsByStudyright, programmeNames, firstYear }
|
||||
}
|
||||
|
||||
const helpTexts = {
|
||||
1: '',
|
||||
2: 'The registration has been done and the student is not attending to the study term.',
|
||||
3: 'The registration has not been done and the student is counted as inactive.',
|
||||
STATUTORY: 'This absence is statutory e.g. parental leave or military service',
|
||||
}
|
||||
const curDate = new Date()
|
||||
|
||||
const renderSemester = ({ enrollmenttype, statutoryAbsence }) => {
|
||||
const { text, className } = statutoryAbsence ? enrolmentTypes.STATUTORY : enrolmentTypes[enrollmenttype]
|
||||
const helpText = helpTexts[enrollmenttype] + (statutoryAbsence ? ` ${helpTexts.STATUTORY}` : '')
|
||||
return (
|
||||
<div className="enrollment-container">
|
||||
{text}
|
||||
<div className={`enrollment-label ${className}`}> </div>
|
||||
{enrollmenttype > 1 ? (
|
||||
<Popup content={helpText} trigger={<Icon className="help-icon" name="question circle outline" />} />
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getEnrollmentStatus = (semester, season) => {
|
||||
if (semester.name.en.includes('Spring') && season === 'SPRING') {
|
||||
return renderSemester(semester)
|
||||
}
|
||||
if (semester.name.en.includes('Autumn') && season === 'FALL') {
|
||||
return renderSemester(semester)
|
||||
}
|
||||
return 'Absent'
|
||||
}
|
||||
|
||||
export const EnrollmentAccordion = ({ semesterEnrollments }) => {
|
||||
export const EnrollmentAccordion = ({ student }) => {
|
||||
const { data: semestersAndYears } = useGetSemestersQuery()
|
||||
const [active, setActive] = useState(false)
|
||||
const { getTextIn } = useLanguage()
|
||||
|
||||
const handleAccordionClick = () => {
|
||||
setActive(!active)
|
||||
}
|
||||
if (!semestersAndYears || !student) return null
|
||||
|
||||
const groupedEnrollments = _.groupBy(semesterEnrollments, 'yearname')
|
||||
const { studyrights } = student
|
||||
|
||||
const sortedKeys = Object.keys(groupedEnrollments).sort((a, b) => {
|
||||
return groupedEnrollments[b][0].semestercode - groupedEnrollments[a][0].semestercode
|
||||
const studyrightsGroupedByStudyright = _.groupBy(
|
||||
studyrights?.filter(sr => sr.studyright_elements.length > 0),
|
||||
'actual_studyrightid'
|
||||
)
|
||||
|
||||
const { enrollmentsByStudyright, programmeNames, firstYear } = calculateSemesterEnrollmentsByStudyright(
|
||||
semestersAndYears,
|
||||
studyrightsGroupedByStudyright
|
||||
)
|
||||
|
||||
const { getSemesterEnrollmentsContent } = getSemestersPresentFunctions({
|
||||
allSemesters: Object.values(semestersAndYears.semesters),
|
||||
allSemestersMap: semestersAndYears.semesters,
|
||||
year: `${Math.max(new Date().getFullYear() - 7, firstYear)}`,
|
||||
filteredStudents: [student],
|
||||
getTextIn,
|
||||
})
|
||||
|
||||
if (curDate < new Date(curDate.getFullYear(), 7, 1)) {
|
||||
sortedKeys.shift()
|
||||
}
|
||||
const semesterEnrollments = Object.values(studyrightsGroupedByStudyright).reduce((acc, studyright) => {
|
||||
acc[studyright[0].actual_studyrightid] = getSemesterEnrollmentsContent(student, studyright)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<div className={active ? 'enrollmentAccordion' : ''}>
|
||||
<Accordion>
|
||||
<Accordion.Title active={active} onClick={handleAccordionClick}>
|
||||
<Icon name="dropdown" />
|
||||
Enrollments
|
||||
</Accordion.Title>
|
||||
<Accordion.Content active={active}>
|
||||
<Table celled>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Semester</Table.HeaderCell>
|
||||
<Table.HeaderCell>Autumn</Table.HeaderCell>
|
||||
<Table.HeaderCell>Spring</Table.HeaderCell>
|
||||
<Accordion style={{ marginBottom: active ? '0.5em' : 0 }}>
|
||||
<Accordion.Title active={active} onClick={() => setActive(!active)}>
|
||||
<Icon name="dropdown" />
|
||||
Enrollments
|
||||
</Accordion.Title>
|
||||
<Accordion.Content active={active}>
|
||||
<Table celled>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Programme(s)</Table.HeaderCell>
|
||||
<Table.HeaderCell>
|
||||
Semesters
|
||||
<Popup
|
||||
position="top center"
|
||||
trigger={<Icon name="question circle outline" style={{ opacity: 0.5, marginLeft: '0.25em' }} />}
|
||||
content="Displays enrollment data for the current and up to six previous academic years."
|
||||
/>
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{Object.entries(enrollmentsByStudyright).map(([studyrightId]) => (
|
||||
<Table.Row key={studyrightId}>
|
||||
<Table.Cell>
|
||||
{_.flatten(programmeNames[studyrightId])
|
||||
.map(getTextIn)
|
||||
.map(elem => (
|
||||
<p key={elem}>{elem}</p>
|
||||
))}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{semesterEnrollments[studyrightId]}</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{sortedKeys.map(key => {
|
||||
const semester = groupedEnrollments[key]
|
||||
return semester[0].startYear <= curDate.getFullYear() ? (
|
||||
<Table.Row key={key}>
|
||||
<Table.Cell>{key}</Table.Cell>
|
||||
|
||||
<Table.Cell>{getEnrollmentStatus(semester[0], 'FALL')}</Table.Cell>
|
||||
|
||||
<Table.Cell>
|
||||
{semester.length > 1
|
||||
? getEnrollmentStatus(semester[1], 'SPRING')
|
||||
: getEnrollmentStatus(semester[0], 'SPRING')}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : null
|
||||
})}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Accordion.Content>
|
||||
</Accordion>
|
||||
</div>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Accordion.Content>
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
EnrollmentAccordion.propTypes = {
|
||||
semesterEnrollments: array.isRequired,
|
||||
}
|
||||
|
||||
@@ -3,15 +3,16 @@ import { Card, Icon, Button } from 'semantic-ui-react'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import { useGetAuthorizedUserQuery } from 'redux/auth'
|
||||
import { SisuLinkItem } from 'components/common/SisuLinkItem'
|
||||
import { reformatDate } from 'common'
|
||||
import { callApi } from 'apiConnection'
|
||||
import { useGetAuthorizedUserQuery } from 'redux/auth'
|
||||
import { removeStudentSelection, resetStudent } from 'redux/students'
|
||||
import { DISPLAY_DATE_FORMAT, DISPLAY_DATE_FORMAT_DEV } from '../../../constants'
|
||||
import './studentInfoCard.css'
|
||||
import { removeStudentSelection, resetStudent } from '../../../redux/students'
|
||||
import { callApi } from '../../../apiConnection'
|
||||
import { EnrollmentAccordion } from './EnrollmentAccordion'
|
||||
|
||||
import './studentInfoCard.css'
|
||||
|
||||
export const StudentInfoCard = ({ student }) => {
|
||||
const dispatch = useDispatch()
|
||||
const history = useHistory()
|
||||
@@ -48,7 +49,7 @@ export const StudentInfoCard = ({ student }) => {
|
||||
</Card.Meta>
|
||||
<Card.Description>{`Credits: ${student.credits || 0}`}</Card.Description>
|
||||
<div style={{ paddingTop: '4px' }}>
|
||||
<EnrollmentAccordion semesterEnrollments={student.semesterenrollments} />
|
||||
<EnrollmentAccordion student={student} />
|
||||
<p style={{ fontSize: 14 }}>{`Updated at ${formattedTimestamp}`}</p>
|
||||
<Button disabled={updating} compact size="medium" labelPosition="left" onClick={updateStudent}>
|
||||
<Icon loading={updating} name="refresh" />
|
||||
|
||||
-31
@@ -3,41 +3,10 @@
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
|
||||
.startDate {
|
||||
margin-top: 3px !important;
|
||||
}
|
||||
|
||||
.controlIcon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.enrollmentAccordion {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
opacity: 0.5 !important;
|
||||
margin-left: 0.75rem !important;
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
.enrollment-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.enrollment-label {
|
||||
border-radius: 0.28rem;
|
||||
box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5;
|
||||
display: inline-block;
|
||||
margin-left: 0.75rem;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
min-width: 15px;
|
||||
min-height: 15px;
|
||||
}
|
||||
|
||||
.enrollment-label-no-margin {
|
||||
border-radius: 0.28rem;
|
||||
box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5;
|
||||
|
||||
@@ -27,6 +27,7 @@ const LinkToGroup = ({ group }) => {
|
||||
padding: '.78571429em .78571429em',
|
||||
}}
|
||||
to={dest}
|
||||
data-cy={`study-guidance-group-link-${group.id}`}
|
||||
>
|
||||
{getTextIn(group.name)}
|
||||
<Icon color="blue" name="level up alternate" onClick={() => history.push(dest)} />
|
||||
@@ -242,7 +243,9 @@ export const StudyGuidanceGroupOverview = ({ groups }) => {
|
||||
lisäominaisuuksia.{' '}
|
||||
</p>
|
||||
</StyledMessage>
|
||||
<SortableTable hideHeaderBar columns={headers} data={groups} singleLine={false} />
|
||||
<div data-cy="Table-study-guidance-group-overview">
|
||||
<SortableTable hideHeaderBar columns={headers} data={groups} singleLine={false} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { Link, useHistory } from 'react-router-dom'
|
||||
import { Segment, Icon, Item } from 'semantic-ui-react'
|
||||
|
||||
import { calculatePercentage } from 'common'
|
||||
import { SortableTable } from 'components/SortableTable'
|
||||
|
||||
const calculatePassrate = (pass, fail) =>
|
||||
new Intl.NumberFormat(undefined, { style: 'percent', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(
|
||||
pass / (pass + fail)
|
||||
)
|
||||
|
||||
const createColumnWithTitle = title => ({
|
||||
key: title,
|
||||
title: title[0].toUpperCase() + title.slice(1),
|
||||
@@ -95,7 +90,7 @@ export const TeacherStatisticsTable = ({ statistics, variant }) => {
|
||||
() =>
|
||||
statistics.map(stat => ({
|
||||
...stat,
|
||||
passrate: calculatePassrate(stat.passed, stat.failed),
|
||||
passrate: calculatePercentage(stat.passed, stat.passed + stat.failed),
|
||||
})),
|
||||
[statistics]
|
||||
)
|
||||
|
||||
@@ -7,102 +7,104 @@ export const DISPLAY_DATE_FORMAT = 'DD.MM.YYYY'
|
||||
export const DISPLAY_DATE_FORMAT_DEV = 'DD.MM.YYYY HH:mm:ss'
|
||||
export const API_DATE_FORMAT = 'YYYY.MM.DD'
|
||||
|
||||
export const passRateAttemptGraphOptions = (categories, max, title, skipFirstColor) => ({
|
||||
export const passRateAttemptGraphOptions = (isRelative, categories, max, title, skipFirstColor) => ({
|
||||
chart: {
|
||||
type: 'column',
|
||||
},
|
||||
colors: skipFirstColor ? [green, red] : [chartblue, green, red],
|
||||
|
||||
title: {
|
||||
text: title,
|
||||
},
|
||||
|
||||
xAxis: {
|
||||
categories,
|
||||
},
|
||||
|
||||
yAxis: {
|
||||
allowDecimals: false,
|
||||
title: {
|
||||
text: 'Number of Students',
|
||||
text: isRelative ? 'Share of Students' : 'Number of Students',
|
||||
},
|
||||
max,
|
||||
floor: -max,
|
||||
},
|
||||
|
||||
plotOptions: {
|
||||
column: {
|
||||
stacking: 'normal',
|
||||
borderRadius: 3,
|
||||
},
|
||||
series: {
|
||||
tooltip: {
|
||||
valueSuffix: isRelative ? '%' : '',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const passRateStudGraphOptions = (categories, max, title, skipFirstColor) => ({
|
||||
export const passRateStudGraphOptions = (isRelative, categories, max, title, skipFirstColor) => ({
|
||||
chart: {
|
||||
type: 'column',
|
||||
},
|
||||
colors: skipFirstColor
|
||||
? [chartlgreen, chartdarkg, chartlred, chartdarkred]
|
||||
: [chartblue, chartlgreen, chartdarkg, chartlred, chartdarkred],
|
||||
|
||||
title: {
|
||||
text: title,
|
||||
},
|
||||
|
||||
xAxis: {
|
||||
categories,
|
||||
},
|
||||
|
||||
yAxis: {
|
||||
allowDecimals: false,
|
||||
title: {
|
||||
text: 'Number of Students',
|
||||
text: isRelative ? 'Share of Students' : 'Number of Students',
|
||||
},
|
||||
max,
|
||||
floor: -max,
|
||||
},
|
||||
|
||||
plotOptions: {
|
||||
column: {
|
||||
stacking: 'normal',
|
||||
borderRadius: 1,
|
||||
},
|
||||
series: {
|
||||
tooltip: {
|
||||
valueSuffix: isRelative ? '%' : '',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const gradeGraphOptions = (categories, max, title) => ({
|
||||
export const gradeGraphOptions = (isRelative, categories, max, title) => ({
|
||||
chart: {
|
||||
type: 'column',
|
||||
},
|
||||
colors: [red, chartblue, chartblue, chartblue, chartblue, chartblue, green, green],
|
||||
|
||||
title: {
|
||||
text: title,
|
||||
},
|
||||
|
||||
legend: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
xAxis: {
|
||||
categories,
|
||||
},
|
||||
|
||||
yAxis: {
|
||||
allowDecimals: false,
|
||||
title: {
|
||||
text: 'Number of Students',
|
||||
text: isRelative ? 'Share of Students' : 'Number of Students',
|
||||
},
|
||||
max,
|
||||
floor: -max,
|
||||
},
|
||||
|
||||
plotOptions: {
|
||||
column: {
|
||||
stacking: 'normal',
|
||||
borderRadius: 2,
|
||||
},
|
||||
series: {
|
||||
tooltip: {
|
||||
valueSuffix: isRelative ? '%' : '',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ const facultystatsApi = RTKApi.injectEndpoints({
|
||||
`/faculties/${id}/progressstats?special_groups=${specialGroups}&graduated=${graduated}`,
|
||||
}),
|
||||
getAllFacultiesProgressStats: builder.query({
|
||||
query: ({ graduated }) => `/faculties/allprogressstats?graduated=${graduated}`,
|
||||
query: ({ graduated }) => `/university/allprogressstats?graduated=${graduated}`,
|
||||
}),
|
||||
getAllFacultiesGraduationStats: builder.query({
|
||||
query: () => '/faculties/allgraduationstats',
|
||||
query: () => '/university/allgraduationstats',
|
||||
}),
|
||||
getFacultyStudentStats: builder.query({
|
||||
query: ({ id, specialGroups, graduated }) =>
|
||||
|
||||
@@ -6,10 +6,10 @@ const usersApi = RTKApi.injectEndpoints({
|
||||
query: () => '/users/email/preview',
|
||||
}),
|
||||
sendUserAccessEmail: builder.mutation({
|
||||
query: ({ recipientAddress }) => ({
|
||||
query: recipientAddress => ({
|
||||
url: '/users/email',
|
||||
method: 'POST',
|
||||
body: { recipientAddress },
|
||||
body: { email: recipientAddress },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user