mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b86ed252f | ||
|
|
f535a5dbdb | ||
|
|
ffe6425999 | ||
|
|
256997633a | ||
|
|
1d3fef6c8d | ||
|
|
428e46084c | ||
|
|
09f4814f80 | ||
|
|
a220050a53 | ||
|
|
115674636f | ||
|
|
f901fec6a3 | ||
|
|
a77ba0e1e1 | ||
|
|
434183bac0 | ||
|
|
7993c2b45a | ||
|
|
8551a76fc9 | ||
|
|
b25f9ba55f | ||
|
|
f28276e086 | ||
|
|
6327ca7e2c | ||
|
|
625f2962eb | ||
|
|
f85e9f0a5d | ||
|
|
d4d67b239e | ||
|
|
83d17a3026 | ||
|
|
e3f8fdb4ae | ||
|
|
b7051ccd41 | ||
|
|
b0a0be7969 | ||
|
|
896cb46ad1 | ||
|
|
17f7289b1c | ||
|
|
a019dd9672 | ||
|
|
881aaf93b4 | ||
|
|
5c973d4f8c | ||
|
|
f9e8bf6d31 | ||
|
|
a2726d4894 | ||
|
|
11377da7bd | ||
|
|
8c4515b158 | ||
|
|
db3cd52d69 | ||
|
|
b4c8bf0f4a | ||
|
|
384afb1e5f | ||
|
|
090d327405 | ||
|
|
30117e60b3 | ||
|
|
9a4fec0974 | ||
|
|
37114c5570 | ||
|
|
63b7c5876d | ||
|
|
9790c829c6 | ||
|
|
082aaa4022 | ||
|
|
3906efbcca | ||
|
|
9d575d6c88 | ||
|
|
4d4a288719 | ||
|
|
a39f522501 | ||
|
|
32293eb050 | ||
|
|
ed56d32cce | ||
|
|
f4b04d3283 | ||
|
|
3a6e5132f3 | ||
|
|
694e537baa |
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable cypress/no-unnecessary-waiting */
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
// const baseUrl = Cypress.config().baseUrl
|
||||
@@ -49,8 +50,6 @@ const createRunTestStepWithPreAndPostPartsFunction = amountWithoutFiltering => {
|
||||
const card = cy.cs(`${filterName}-filter-card`)
|
||||
|
||||
card.invoke('attr', 'data-open').then(open => {
|
||||
console.log('open', open)
|
||||
|
||||
const getHeader = () => cy.cs(`${filterName}-header`)
|
||||
|
||||
if (open === 'false') {
|
||||
@@ -325,6 +324,7 @@ describe('Custom Population Statistics', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 20, 30, 1)
|
||||
})
|
||||
cy.cs('reset-all-filters').click()
|
||||
})
|
||||
|
||||
it('Programme defaults to "Active Study Right" mode', () => {
|
||||
@@ -340,6 +340,7 @@ describe('Custom Population Statistics', () => {
|
||||
checkFilteringResult(5)
|
||||
programmeDropdown.get('i.delete').click()
|
||||
})
|
||||
cy.cs('reset-all-filters').click()
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
|
||||
@@ -369,7 +369,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('[data-cy=creditsStudentsToggle]').first().click()
|
||||
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
cy.get('tr').eq(1).contains('267')
|
||||
cy.get('tr').eq(2).contains('267')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -144,7 +144,7 @@ const OpenUniPopulationSearch = sequelizeKone.define(
|
||||
{ underscored: true, timestamps: true }
|
||||
)
|
||||
|
||||
const StudyProgrammeCriteria = sequelizeKone.define(
|
||||
const ProgressCriteria = sequelizeKone.define(
|
||||
'progress_criteria',
|
||||
{
|
||||
code: {
|
||||
@@ -218,5 +218,5 @@ module.exports = {
|
||||
sequelizeKone,
|
||||
ExcludedCourse,
|
||||
StudyGuidanceGroupTag,
|
||||
StudyProgrammeCriteria,
|
||||
ProgressCriteria,
|
||||
}
|
||||
|
||||
@@ -84,11 +84,10 @@ router.get('/:id/basicstats', async (req, res) => {
|
||||
router.get('/:id/creditstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getCreditStats(code, yearType, programmeFilter, specialGroups)
|
||||
const data = await getCreditStats(code, yearType, programmeFilter)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
@@ -97,9 +96,9 @@ router.get('/:id/creditstats', async (req, res) => {
|
||||
// list of all programmes is also needed in classification of credits
|
||||
const allProgrammes = programmeFilter === 'ALL_PROGRAMMES' ? programmes : await getProgrammes(code, 'ALL_PROGRAMMES')
|
||||
|
||||
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType, specialGroups)
|
||||
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter)
|
||||
}
|
||||
|
||||
return res.json(updatedStats)
|
||||
|
||||
@@ -82,20 +82,6 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
//These cause problems in front (where they were originally calculated)
|
||||
// Do them here for the moment
|
||||
for (const course of result.coursestatistics) {
|
||||
// course.students.notParticipated = {}
|
||||
course.students.didNotPass = {}
|
||||
|
||||
for (const student of Object.keys(course.students.all)) {
|
||||
if (
|
||||
!Object.keys(course.students.passed).includes(student) &&
|
||||
!Object.keys(course.students.retryPassed).includes(student)
|
||||
)
|
||||
course.students.didNotPass[student] = true
|
||||
}
|
||||
}
|
||||
res.json(result)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ const {
|
||||
setStudytrackStats,
|
||||
} = require('../services/analyticsService')
|
||||
const { updateBasicView, updateStudytrackView } = require('../services/studyprogrammeUpdates')
|
||||
const { getProgrammeName } = require('../services/studyprogramme')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/basicstats', async (req, res) => {
|
||||
@@ -143,4 +144,73 @@ router.get('/v2/studyprogrammes/:id/update_studytrackview', async (req, res) =>
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/evaluationstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const graduated = req.query?.graduated
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
// Statistics for Tilannekuvalomake view
|
||||
|
||||
let gradData = await getGraduationStats(code, yearType, specialGroups)
|
||||
if (!gradData) {
|
||||
const updatedStats = await getGraduationStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
if (updatedStats) {
|
||||
await setGraduationStats(updatedStats, yearType, specialGroups)
|
||||
gradData = updatedStats
|
||||
}
|
||||
}
|
||||
|
||||
let progressData = await getStudytrackStats(code, graduated, specialGroups)
|
||||
if (!progressData) {
|
||||
const updated = await getStudytrackStatsForStudyprogramme({
|
||||
studyprogramme: code,
|
||||
settings: {
|
||||
graduated: graduated === 'GRADUATED_INCLUDED',
|
||||
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
if (updated) {
|
||||
await setStudytrackStats(updated, graduated, specialGroups)
|
||||
progressData = updated
|
||||
}
|
||||
}
|
||||
|
||||
const programmeName = await getProgrammeName(code)
|
||||
|
||||
delete gradData.tableStats
|
||||
delete gradData.graphStats
|
||||
delete gradData.titles
|
||||
|
||||
const data = {
|
||||
id: code,
|
||||
programmeName: programmeName?.name,
|
||||
status: gradData?.status,
|
||||
lastUpdated: gradData.lastUpdated,
|
||||
graduations: gradData,
|
||||
progress: {
|
||||
creditTableStats: {},
|
||||
creditTableTitles: progressData?.creditTableTitles,
|
||||
graphData: {
|
||||
creditGraphStats: {},
|
||||
id: progressData?.id,
|
||||
years: progressData?.years,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data.progress.creditTableStats[code] = progressData?.creditTableStats[code]
|
||||
data.progress.graphData.creditGraphStats[code] = progressData?.creditGraphStats[code]
|
||||
|
||||
return res.json(data)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -26,7 +26,9 @@ class CourseStatsCounter {
|
||||
failedMany: {},
|
||||
improvedPassedGrade: {},
|
||||
markedToSemester: {},
|
||||
enrolledNoGrade: {},
|
||||
}
|
||||
|
||||
this.stats = {
|
||||
students: 0,
|
||||
passed: 0,
|
||||
@@ -38,10 +40,10 @@ class CourseStatsCounter {
|
||||
percentage: undefined,
|
||||
passedOfPopulation: undefined,
|
||||
triedOfPopulation: undefined,
|
||||
perStudent: undefined,
|
||||
passingSemesters: this.initializePassingSemesters(),
|
||||
}
|
||||
this.enrollments = {
|
||||
enrolledNoGrade: new Set(),
|
||||
semesters: this.initializePassingSemesters({}),
|
||||
}
|
||||
this.grades = {}
|
||||
@@ -184,11 +186,20 @@ class CourseStatsCounter {
|
||||
getFinalStats() {
|
||||
const stats = this.stats
|
||||
const students = this.students
|
||||
const allStudents = Object.keys(students.all)
|
||||
const totalEnrolledNoGrade = [...(this.enrollments.ENROLLED || [])].filter(
|
||||
student => !allStudents.includes(student)
|
||||
).length
|
||||
const studs = this.enrollments
|
||||
? Object.keys(this.enrollments)
|
||||
.filter(key => key !== 'ENROLLED' && key !== 'semesters')
|
||||
.reduce((acc, key) => [...acc, ...[...this.enrollments[key]].map(studentnumber => studentnumber)], [])
|
||||
: []
|
||||
const allStudents = Object.keys(students.all).map(student => student)
|
||||
const fileredEnrolledNoGrade = this.enrollments['ENROLLED']
|
||||
? [...this.enrollments['ENROLLED']]
|
||||
.filter(student => !studs.includes(student) && !allStudents.includes(student))
|
||||
.reduce((acc, student) => ({ ...acc, [student]: true }), {})
|
||||
: {}
|
||||
|
||||
students.all = { ...students.all, ...fileredEnrolledNoGrade }
|
||||
students.enrolledNoGrade = fileredEnrolledNoGrade
|
||||
stats.students = lengthOf(students.all)
|
||||
stats.passed = lengthOf(students.passed)
|
||||
stats.failed = lengthOf(students.failed)
|
||||
@@ -200,8 +211,8 @@ class CourseStatsCounter {
|
||||
stats.triedOfPopulation = percentageOf(stats.students, this.studentsInTotal)
|
||||
stats.perStudent = stats.attempts / (stats.passed + stats.failed)
|
||||
stats.passingSemestersCumulative = this.getPassingSemestersCumulative()
|
||||
stats.totalStudents = stats.students + totalEnrolledNoGrade
|
||||
stats.totalEnrolledNoGrade = totalEnrolledNoGrade
|
||||
stats.totalStudents = stats.students
|
||||
stats.totalEnrolledNoGrade = lengthOf(fileredEnrolledNoGrade)
|
||||
stats.percentageWithEnrollments = percentageOf(stats.passed, stats.totalStudents)
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,14 +7,8 @@ const {
|
||||
isNonMajorCredit,
|
||||
getYearsArray,
|
||||
getStartDate,
|
||||
isSpecialGroupCredit,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const {
|
||||
getStudyRights,
|
||||
getCreditsForStudyProgramme,
|
||||
allTransfers,
|
||||
getCourseCodesForStudyProgramme,
|
||||
} = require('../studyprogramme')
|
||||
const { getStudyRights, getCreditsForStudyProgramme, getCourseCodesForStudyProgramme } = require('../studyprogramme')
|
||||
|
||||
const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes) => {
|
||||
let right = ''
|
||||
@@ -42,25 +36,14 @@ const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes
|
||||
// Fetches all the credits for the studyprogramme and divides them into major-students, non-major faculty students,
|
||||
// non-major other faculty students adn non-degree student credits
|
||||
// Division is done on the basis that whether the student had a primary studyright to the programme on the attainment_date
|
||||
const getFacultyRegularCreditStats = async ({
|
||||
studyprogramme,
|
||||
since,
|
||||
years,
|
||||
isAcademicYear,
|
||||
includeAllSpecials,
|
||||
facultyProgrammes,
|
||||
}) => {
|
||||
const getFacultyRegularCreditStats = async ({ studyprogramme, since, years, isAcademicYear, facultyProgrammes }) => {
|
||||
const providercode = mapToProviders([studyprogramme])[0]
|
||||
const courses = await getCourseCodesForStudyProgramme(providercode)
|
||||
// console.log(JSON.stringify(courses))
|
||||
const credits = await getCreditsForStudyProgramme(courses, since)
|
||||
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
|
||||
|
||||
let studyrights = await getStudyRights(students)
|
||||
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid).filter(s => s !== 'undefined')
|
||||
if (!includeAllSpecials) {
|
||||
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
|
||||
}
|
||||
|
||||
let majors = getStatsBasis(years)
|
||||
let allNonMajors = getStatsBasis(years)
|
||||
let facultyNonMajors = getStatsBasis(years)
|
||||
@@ -71,10 +54,6 @@ const getFacultyRegularCreditStats = async ({
|
||||
const studentStudyrights = studyrights.filter(studyright => studyright.studentnumber === student_studentnumber)
|
||||
const attainmentYear = defineYear(attainment_date, isAcademicYear)
|
||||
|
||||
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainment_date, transfers)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isMajorStudentCredit(studentStudyrights, attainment_date, studyprogramme)) {
|
||||
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
majors.tableStats[attainmentYear] += credits || 0
|
||||
@@ -116,12 +95,11 @@ const getFacultyRegularCreditStats = async ({
|
||||
// Fetches all credits for the studytrack and combines them into statistics
|
||||
// These are for the faculty view which differs from study programme overview
|
||||
// (no transfers, non majors split to in- and outside-faculty)
|
||||
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, settings }) => {
|
||||
const { isAcademicYear, includeAllSpecials } = settings
|
||||
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, isAcademicYear }) => {
|
||||
const since = getStartDate(studyprogramme, isAcademicYear)
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
|
||||
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials, facultyProgrammes }
|
||||
const queryParameters = { studyprogramme, since, years, isAcademicYear, facultyProgrammes }
|
||||
const { majors, facultyNonMajors, otherNonMajors, nonDegree } = await getFacultyRegularCreditStats(queryParameters)
|
||||
|
||||
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
|
||||
@@ -129,47 +107,36 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
|
||||
|
||||
if (!dataFound) return null
|
||||
|
||||
const tableStats = reversedYears.map(year =>
|
||||
includeAllSpecials
|
||||
? [
|
||||
year,
|
||||
majors.tableStats[year] +
|
||||
facultyNonMajors.tableStats[year] +
|
||||
otherNonMajors.tableStats[year] +
|
||||
nonDegree.tableStats[year],
|
||||
majors.tableStats[year],
|
||||
facultyNonMajors.tableStats[year],
|
||||
otherNonMajors.tableStats[year],
|
||||
nonDegree.tableStats[year],
|
||||
]
|
||||
: [year, majors.tableStats[year], majors.tableStats[year]]
|
||||
)
|
||||
const tableStats = reversedYears.map(year => [
|
||||
year,
|
||||
majors.tableStats[year] +
|
||||
facultyNonMajors.tableStats[year] +
|
||||
otherNonMajors.tableStats[year] +
|
||||
nonDegree.tableStats[year],
|
||||
majors.tableStats[year],
|
||||
facultyNonMajors.tableStats[year],
|
||||
otherNonMajors.tableStats[year],
|
||||
nonDegree.tableStats[year],
|
||||
])
|
||||
|
||||
const graphStats = includeAllSpecials
|
||||
? [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major faculty students credits',
|
||||
data: facultyNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major other faculty students credits 2',
|
||||
data: otherNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-degree credits',
|
||||
data: nonDegree.graphStats,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
]
|
||||
const graphStats = [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major faculty students credits',
|
||||
data: facultyNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major other faculty students credits 2',
|
||||
data: otherNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-degree credits',
|
||||
data: nonDegree.graphStats,
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
id: studyprogramme,
|
||||
@@ -179,33 +146,27 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
|
||||
}
|
||||
}
|
||||
|
||||
const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgrammes) => {
|
||||
const getProgrammeCredits = async (code, yearType, facultyProgrammes) => {
|
||||
const updatedStats = await getFacultyCreditStatsForStudytrack({
|
||||
studyprogramme: code,
|
||||
facultyProgrammes,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
})
|
||||
|
||||
return updatedStats
|
||||
}
|
||||
|
||||
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType, specialGroups) => {
|
||||
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType) => {
|
||||
let counts = {}
|
||||
let years = []
|
||||
const titles =
|
||||
specialGroups === 'SPECIAL_INCLUDED'
|
||||
? [
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non-major faculty students credits',
|
||||
'Non-major other faculty students credits',
|
||||
'Non-degree student credits',
|
||||
]
|
||||
: ['', 'Total', 'Major students credits']
|
||||
const titles = [
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non-major faculty students credits',
|
||||
'Non-major other faculty students credits',
|
||||
'Non-degree student credits',
|
||||
]
|
||||
|
||||
let allCredits = {
|
||||
id: faculty,
|
||||
@@ -220,7 +181,7 @@ const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearTyp
|
||||
const progCodes = allProgrammes.map(p => p.code)
|
||||
for (const prog of programmes) {
|
||||
if (prog.code === 'MH70_008_2') continue
|
||||
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
|
||||
const data = await getProgrammeCredits(prog.code, yearType, progCodes)
|
||||
if (data) {
|
||||
allCredits.programmeNames[prog.progId] = { code: prog.code, ...prog.name }
|
||||
allCredits.programmeTableStats[prog.progId] = data.tableStats
|
||||
@@ -260,16 +221,12 @@ const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearTyp
|
||||
nonDegree.push(counts[year][4])
|
||||
})
|
||||
|
||||
allCredits.graphStats =
|
||||
specialGroups === 'SPECIAL_INCLUDED'
|
||||
? [
|
||||
{ name: 'Major students credits', data: majors },
|
||||
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
|
||||
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
|
||||
{ name: 'Non-degree credits', data: nonDegree },
|
||||
]
|
||||
: [{ name: 'Major students credits', data: majors }]
|
||||
|
||||
allCredits.graphStats = [
|
||||
{ name: 'Major students credits', data: majors },
|
||||
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
|
||||
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
|
||||
{ name: 'Non-degree credits', data: nonDegree },
|
||||
]
|
||||
return allCredits
|
||||
}
|
||||
|
||||
|
||||
@@ -54,9 +54,9 @@ const getBasicStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setCreditStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const setCreditStats = async (data, yearType, programmeFilter) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
|
||||
@@ -100,13 +100,12 @@ const updateFacultyOverview = async (faculty, statsType) => {
|
||||
)
|
||||
await setBasicStats(updatedStudentInfo, option.yearType, option.programmeFilter, option.specialGroups)
|
||||
}
|
||||
if (statsType === 'ALL' || statsType === 'CREDITS') {
|
||||
if ((statsType === 'ALL' || statsType === 'CREDITS') && specialGroups !== 'SPECIAL_EXCLUDED') {
|
||||
const updatedCredits = await combineFacultyCredits(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
allProgrammes.data,
|
||||
yearType,
|
||||
specialGroups
|
||||
yearType
|
||||
)
|
||||
await setCreditStats(updatedCredits, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ const { CourseStatsCounter } = require('./course_stats_counter')
|
||||
const { getPassingSemester, semesterEnd, semesterStart } = require('../util/semester')
|
||||
const { getAllProgrammes } = require('./studyrights')
|
||||
const { encrypt } = require('../services/encrypt')
|
||||
const { getCriteria } = require('./studyProgrammeCriteria')
|
||||
|
||||
const enrolmentDates = () => {
|
||||
const query = 'SELECT DISTINCT s.dateOfUniversityEnrollment as date FROM Student s'
|
||||
@@ -48,6 +49,7 @@ const formatStudentForPopulationStatistics = (
|
||||
creditcount,
|
||||
abbreviatedname,
|
||||
email,
|
||||
secondary_email,
|
||||
phone_number,
|
||||
studyrights,
|
||||
studyplans,
|
||||
@@ -68,7 +70,8 @@ const formatStudentForPopulationStatistics = (
|
||||
credits,
|
||||
startDate,
|
||||
startDateMoment,
|
||||
endDateMoment
|
||||
endDateMoment,
|
||||
criteria
|
||||
) => {
|
||||
const toCourse = ({ grade, attainment_date, credits, course_code, credittypecode, isStudyModule, language }) => {
|
||||
const attainment_date_normailized =
|
||||
@@ -87,8 +90,74 @@ const formatStudentForPopulationStatistics = (
|
||||
}
|
||||
}
|
||||
|
||||
studyrights = studyrights || []
|
||||
const toProgressCriteria = () => {
|
||||
const criteriaFullfilled = {
|
||||
year1: {
|
||||
credits: false,
|
||||
totalSatisfied: 0,
|
||||
},
|
||||
year2: {
|
||||
credits: false,
|
||||
totalSatisfied: 0,
|
||||
},
|
||||
year3: {
|
||||
credits: false,
|
||||
totalSatisfied: 0,
|
||||
},
|
||||
}
|
||||
let firstAcademic = 0
|
||||
let secondAcademic = 0
|
||||
let thirdAcademic = 0
|
||||
if (criteria !== {}) {
|
||||
const courses = credits[studentnumber] ? credits[studentnumber].map(toCourse) : []
|
||||
courses.forEach(course => {
|
||||
if (course.passed) {
|
||||
if (
|
||||
criteria?.courses?.yearOne.includes(course.course_code) ||
|
||||
criteria?.courses?.yearOne.some(c => criteria?.allCourses?.yearOne[c].includes(course.course_code))
|
||||
) {
|
||||
criteriaFullfilled.year1.totalSatisfied += 1
|
||||
}
|
||||
if (
|
||||
criteria?.courses?.yearTwo.includes(course.course_code) ||
|
||||
criteria?.courses?.yearTwo.some(c => criteria?.allCourses?.yearTwo[c].includes(course.course_code))
|
||||
) {
|
||||
criteriaFullfilled.year2.totalSatisfied += 1
|
||||
}
|
||||
if (
|
||||
criteria?.courses?.yearThree.includes(course.course_code) ||
|
||||
criteria?.courses?.yearThree.some(c => criteria?.allCourses?.yearThree[c].includes(course.course_code))
|
||||
) {
|
||||
criteriaFullfilled.year3.totalSatisfied += 1
|
||||
}
|
||||
firstAcademic += moment(course.date).isBetween(moment(startDate), moment(startDate).add(1, 'year'))
|
||||
? course.credits
|
||||
: 0
|
||||
secondAcademic += moment(course.date).isBetween(moment(startDate), moment(startDate).add(2, 'year'))
|
||||
? course.credits
|
||||
: 0
|
||||
thirdAcademic += moment(course.date).isBetween(moment(startDate), moment(startDate).add(3, 'year'))
|
||||
? course.credits
|
||||
: 0
|
||||
}
|
||||
})
|
||||
}
|
||||
if (firstAcademic >= criteria?.credits?.yearOne) {
|
||||
criteriaFullfilled.year1.credits = true
|
||||
criteriaFullfilled.year1.totalSatisfied += 1
|
||||
}
|
||||
if (secondAcademic >= criteria?.credits?.yearTwo) {
|
||||
criteriaFullfilled.year2.credits = true
|
||||
criteriaFullfilled.year2.totalSatisfied += 1
|
||||
}
|
||||
if (thirdAcademic >= criteria?.credits?.yearThree) {
|
||||
criteriaFullfilled.year3.credits = true
|
||||
criteriaFullfilled.year3.totalSatisfied += 1
|
||||
}
|
||||
return criteriaFullfilled
|
||||
}
|
||||
|
||||
studyrights = studyrights || []
|
||||
const started = dateofuniversityenrollment
|
||||
return {
|
||||
firstnames,
|
||||
@@ -104,6 +173,7 @@ const formatStudentForPopulationStatistics = (
|
||||
gender_code,
|
||||
gender: { gender_en, gender_fi, gender_sv },
|
||||
email,
|
||||
secondaryEmail: secondary_email,
|
||||
phoneNumber: phone_number,
|
||||
semesterenrollments: semester_enrollments || [],
|
||||
updatedAt: updatedAt || createdAt,
|
||||
@@ -114,6 +184,7 @@ const formatStudentForPopulationStatistics = (
|
||||
birthdate,
|
||||
studyplans,
|
||||
sis_person_id,
|
||||
criteriaProgress: toProgressCriteria(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +304,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
'creditcount',
|
||||
'abbreviatedname',
|
||||
'email',
|
||||
'secondary_email',
|
||||
'phone_number',
|
||||
'updatedAt',
|
||||
'gender_code',
|
||||
@@ -749,7 +821,8 @@ const formatStudentsForApi = async (
|
||||
startDate,
|
||||
endDate,
|
||||
{ studyRights },
|
||||
optionData
|
||||
optionData,
|
||||
criteria
|
||||
) => {
|
||||
const startDateMoment = moment(startDate)
|
||||
const endDateMoment = moment(endDate)
|
||||
@@ -783,8 +856,17 @@ const formatStudentsForApi = async (
|
||||
})
|
||||
if (student.studentnumber in optionData) student.option = optionData[student.studentnumber]
|
||||
else student.option = null
|
||||
|
||||
stats.students.push(
|
||||
formatStudentForPopulationStatistics(student, enrollments, credits, startDate, startDateMoment, endDateMoment)
|
||||
formatStudentForPopulationStatistics(
|
||||
student,
|
||||
enrollments,
|
||||
credits,
|
||||
startDate,
|
||||
startDateMoment,
|
||||
endDateMoment,
|
||||
criteria
|
||||
)
|
||||
)
|
||||
return stats
|
||||
},
|
||||
@@ -868,16 +950,15 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
|
||||
nondegreeStudents,
|
||||
transferredStudents
|
||||
)
|
||||
// wtf
|
||||
// plz
|
||||
const code = studyRights[0] || ''
|
||||
let optionData = {}
|
||||
let criteria = {}
|
||||
if (code.includes('MH')) {
|
||||
optionData = await getOptionsForStudents(studentnumbers, code, 'MSC')
|
||||
} else if (code.includes('KH')) {
|
||||
optionData = await getOptionsForStudents(studentnumbers, code, 'BSC')
|
||||
criteria = await getCriteria(code)
|
||||
}
|
||||
|
||||
const { students, enrollments, credits, extents, semesters, elementdetails, courses } =
|
||||
await getStudentsIncludeCoursesBetween(
|
||||
studentnumbers,
|
||||
@@ -892,7 +973,8 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
|
||||
startDate,
|
||||
endDate,
|
||||
formattedQueryParams,
|
||||
optionData
|
||||
optionData,
|
||||
criteria
|
||||
)
|
||||
|
||||
return formattedStudents
|
||||
@@ -902,10 +984,52 @@ const getSubstitutions = async codes => {
|
||||
const courses = await Course.findAll({ where: { code: codes }, attributes: ['code', 'substitutions'], raw: true })
|
||||
return [...new Set(courses.map(({ code, substitutions }) => [code, ...substitutions]).flat())]
|
||||
}
|
||||
|
||||
// This duplicate code is added here to ensure that we get the enrollments in cases no credits found for the selected students.
|
||||
const findCourseEnrollments = async (studentnumbers, beforeDate, courses = [], studentCountLimit = 0) => {
|
||||
const courseCodes = courses.length > 0 ? await getSubstitutions(courses) : ['DUMMY']
|
||||
const res = await sequelize.query(
|
||||
`
|
||||
SELECT DISTINCT ON (course.id)
|
||||
course.code,
|
||||
course.name,
|
||||
course.coursetypecode,
|
||||
course.substitutions,
|
||||
course.main_course_code,
|
||||
course_types.name AS course_type,
|
||||
enrollment.data AS enrollments
|
||||
FROM course
|
||||
INNER JOIN course_types ON course_types.coursetypecode = course.coursetypecode
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
course_id,
|
||||
ARRAY_AGG(JSON_BUILD_OBJECT(
|
||||
'studentnumber', studentnumber,
|
||||
'state', state,
|
||||
'enrollment_date_time', enrollment_date_time
|
||||
)) AS data
|
||||
FROM enrollment
|
||||
WHERE enrollment.studentnumber IN (:studentnumbers) AND enrollment.enrollment_date_time < :beforeDate
|
||||
GROUP BY enrollment.course_id
|
||||
HAVING COUNT(DISTINCT enrollment.studentnumber) >= :studentCountLimit
|
||||
) enrollment ON enrollment.course_id = course.id
|
||||
WHERE :skipCourseCodeFilter OR course.code IN (:courseCodes)
|
||||
-- GROUP BY 1, 2, 3, 4, 5, 6
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
studentnumbers: studentnumbers.length > 0 ? studentnumbers : ['DUMMY'],
|
||||
beforeDate,
|
||||
studentCountLimit,
|
||||
courseCodes,
|
||||
skipCourseCodeFilter: courses.length === 0,
|
||||
},
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
)
|
||||
return res
|
||||
}
|
||||
const findCourses = async (studentnumbers, beforeDate, courses = [], studentCountLimit = 0) => {
|
||||
const courseCodes = courses.length > 0 ? await getSubstitutions(courses) : ['DUMMY']
|
||||
|
||||
const res = await sequelize.query(
|
||||
`
|
||||
SELECT DISTINCT ON (course.id)
|
||||
@@ -1030,14 +1154,22 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
|
||||
numbers[num] = true
|
||||
return numbers
|
||||
}, {})
|
||||
|
||||
const courses = await findCourses(
|
||||
studentnumbers,
|
||||
dateMonthsFromNow(startDate, months),
|
||||
courseCodes,
|
||||
query.studentCountLimit
|
||||
)
|
||||
return [allstudents, courses]
|
||||
const foundCourseCodes = Object.keys(keyBy(courses, 'code'))
|
||||
const filteredCourseCodes = courseCodes?.filter(code => !foundCourseCodes.includes(code))
|
||||
|
||||
const courseEnrollements = await findCourseEnrollments(
|
||||
studentnumbers,
|
||||
dateMonthsFromNow(startDate, months),
|
||||
filteredCourseCodes,
|
||||
query.studentCountLimit
|
||||
)
|
||||
return [allstudents, courses, courseEnrollements]
|
||||
} else {
|
||||
const { months, startDate } = params
|
||||
const beforeDate = months && startDate ? dateMonthsFromNow(startDate, months) : new Date()
|
||||
@@ -1047,7 +1179,16 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
|
||||
return numbers
|
||||
}, {})
|
||||
const courses = await findCourses(studentnumberlist, beforeDate, courseCodes, query.studentCountLimit)
|
||||
return [allstudents, courses]
|
||||
const foundCourseCodes = Object.keys(keyBy(courses, 'code'))
|
||||
const filteredCourseCodes = courseCodes?.filter(code => !foundCourseCodes.includes(code))
|
||||
|
||||
const courseEnrollements = await findCourseEnrollments(
|
||||
studentnumberlist,
|
||||
dateMonthsFromNow(startDate, months),
|
||||
filteredCourseCodes,
|
||||
query.studentCountLimit
|
||||
)
|
||||
return [allstudents, courses, courseEnrollements]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1067,24 +1208,52 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
|
||||
}
|
||||
|
||||
const params = parseQueryParams(query)
|
||||
const [[allstudents, courses], error] = await Promise.all([
|
||||
const allStudentsByYears = query?.selectedStudentsByYear
|
||||
? Object.keys(query.selectedStudentsByYear).reduce(
|
||||
(res, year) => [...res, ...query.selectedStudentsByYear[year]],
|
||||
[]
|
||||
)
|
||||
: []
|
||||
// To fix failed and enrolled, no grade filter options some not so clean and nice solutions were added
|
||||
// Get the data with actual 1. courses and filtered students. 2. all students by year, if provided.
|
||||
const [[allstudents, courses, courseEnrollements], [, allCourses], error] = await Promise.all([
|
||||
getStudentsAndCourses(query.selectedStudents, studentnumberlist, query.courses),
|
||||
getStudentsAndCourses(allStudentsByYears, null, query.courses),
|
||||
isValidRequest(query, params),
|
||||
])
|
||||
if (error) return error
|
||||
|
||||
if (error) return error
|
||||
// Get the substitution codes for the fetch data by selscted students
|
||||
const substitutionCodes = Object.entries(courses).reduce(
|
||||
(res, [, obj]) => [...res, ...(obj?.substitutions || [])],
|
||||
[]
|
||||
)
|
||||
const codes = Object.keys(keyBy(courses, 'code')).map(code => code)
|
||||
// Filter substitution courses for fetched courses -> this avoid the situation in which there is only
|
||||
// courses with old course codes. Frontend NEEDS in most cases the current course.
|
||||
const substitutionCourses = allCourses.filter(
|
||||
obj => substitutionCodes.includes(obj.code) && !codes.includes(obj.code)
|
||||
)
|
||||
const bottlenecks = {
|
||||
disciplines: {},
|
||||
coursetypes: {},
|
||||
}
|
||||
|
||||
const stats = {}
|
||||
|
||||
const startYear = parseInt(query.year, 10)
|
||||
const allstudentslength = Object.keys(allstudents).length
|
||||
const coursesByCode = keyBy(courses, 'code')
|
||||
let coursesToLoop = courses.concat(substitutionCourses)
|
||||
const codesList = coursesToLoop.map(course => course.code)
|
||||
// This fix problem when Enrolled no grade is chosen. The sql query for fetching
|
||||
// credits do not fetch enrollments if no credits found for selected students.
|
||||
// This and other sql query ensures that enrollments are added.
|
||||
const coursesOnlyWithEnrollments = courseEnrollements.filter(
|
||||
course => !codesList.includes(course.code) && course.enrollments
|
||||
)
|
||||
coursesToLoop = coursesToLoop.concat(coursesOnlyWithEnrollments)
|
||||
|
||||
for (const course of courses) {
|
||||
const coursesByCode = keyBy(coursesToLoop, 'code')
|
||||
for (const course of coursesToLoop) {
|
||||
let { course_type } = course
|
||||
let maincourse = course
|
||||
|
||||
@@ -1101,28 +1270,32 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
|
||||
}
|
||||
|
||||
const coursestats = stats[maincourse.code]
|
||||
|
||||
coursestats.addCourseType(course_type.coursetypecode, course_type.name)
|
||||
coursestats.addCourseSubstitutions(course.substitutions)
|
||||
bottlenecks.coursetypes[course_type.coursetypecode] = course_type.name
|
||||
|
||||
course.credits.forEach(credit => {
|
||||
const { studentnumber, passingGrade, improvedGrade, failingGrade, grade, date } = parseCreditInfo(credit)
|
||||
const semester = getPassingSemester(startYear, date)
|
||||
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
|
||||
})
|
||||
|
||||
if (course.enrollments) {
|
||||
course.enrollments.forEach(({ studentnumber, state, enrollment_date_time }) => {
|
||||
const semester = getPassingSemester(startYear, enrollment_date_time)
|
||||
coursestats.markEnrollment(studentnumber, state, semester)
|
||||
if ((query?.selectedStudents && query?.selectedStudents.includes(studentnumber)) || !query?.selectedStudents) {
|
||||
const semester = getPassingSemester(startYear, enrollment_date_time)
|
||||
coursestats.markEnrollment(studentnumber, state, semester, enrollment_date_time)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (course.credits) {
|
||||
course.credits.forEach(credit => {
|
||||
const { studentnumber, passingGrade, improvedGrade, failingGrade, grade, date } = parseCreditInfo(credit)
|
||||
if ((query?.selectedStudents && query?.selectedStudents.includes(studentnumber)) || !query?.selectedStudents) {
|
||||
const semester = getPassingSemester(startYear, date)
|
||||
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stats[maincourse.code] = coursestats
|
||||
}
|
||||
|
||||
bottlenecks.coursestatistics = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
|
||||
const allStats = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
|
||||
bottlenecks.coursestatistics = allStats.filter(course => course.stats.students > 0)
|
||||
bottlenecks.allStudents = allstudentslength
|
||||
|
||||
if (encryptdata) encryptStudentnumbers(bottlenecks)
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
const { StudyProgrammeCriteria } = require('../models/models_kone')
|
||||
const { ProgressCriteria } = require('../models/models_kone')
|
||||
const { Course } = require('../models')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const getCriteriaByStudyProgramme = code =>
|
||||
StudyProgrammeCriteria.findOne({
|
||||
ProgressCriteria.findOne({
|
||||
where: {
|
||||
code: code,
|
||||
},
|
||||
})
|
||||
|
||||
const formattedData = updatedData => {
|
||||
const getSubstitutions = async codes => {
|
||||
if (codes === []) return []
|
||||
const courses = await Course.findAll({ where: { code: codes }, attributes: ['code', 'substitutions'], raw: true })
|
||||
return courses.reduce((acc, { code, substitutions }) => ({ ...acc, [code]: substitutions }), {})
|
||||
}
|
||||
|
||||
const formattedData = async updatedData => {
|
||||
const allCoursesOne = await getSubstitutions(updatedData.coursesYearOne)
|
||||
const allCoursesTwo = await getSubstitutions(updatedData.coursesYearTwo)
|
||||
const allCoursesThree = await getSubstitutions(updatedData.coursesYearThree)
|
||||
const criteria = {
|
||||
courses: {
|
||||
yearOne: updatedData.coursesYearOne,
|
||||
yearTwo: updatedData.coursesYearTwo,
|
||||
yearThree: updatedData.coursesYearThree,
|
||||
},
|
||||
allCourses: {
|
||||
yearOne: allCoursesOne,
|
||||
yearTwo: allCoursesTwo,
|
||||
yearThree: allCoursesThree,
|
||||
},
|
||||
credits: {
|
||||
yearOne: updatedData.creditsYearOne,
|
||||
yearTwo: updatedData.creditsYearTwo,
|
||||
@@ -35,9 +50,8 @@ const createCriteria = async (studyProgramme, coYear1, coYear2, coYear3, crYear1
|
||||
creditsYearThree: crYear3,
|
||||
}
|
||||
try {
|
||||
const createdData = await StudyProgrammeCriteria.create(newProgrammeCriteria)
|
||||
const criteria = formattedData(createdData)
|
||||
return criteria
|
||||
const createdData = await ProgressCriteria.create(newProgrammeCriteria)
|
||||
return await formattedData(createdData)
|
||||
} catch (error) {
|
||||
logger.error(`Create criteria failed: ${error}`)
|
||||
}
|
||||
@@ -64,8 +78,7 @@ const saveYearlyCreditCriteria = async (studyProgramme, credits) => {
|
||||
|
||||
try {
|
||||
const updatedData = await studyProgrammeToUpdate.update({ ...yearlyCredits })
|
||||
const criteria = formattedData(updatedData)
|
||||
return criteria
|
||||
return await formattedData(updatedData)
|
||||
} catch (error) {
|
||||
logger.error(`Update yearly credit criteria failed: ${error}`)
|
||||
}
|
||||
@@ -100,11 +113,28 @@ const saveYearlyCourseCriteria = async (studyProgramme, courses, year) => {
|
||||
|
||||
const getCriteria = async studyProgramme => {
|
||||
const studyProgrammeCriteria = await getCriteriaByStudyProgramme(studyProgramme)
|
||||
let allCoursesOne = []
|
||||
let allCoursesTwo = []
|
||||
let allCoursesThree = []
|
||||
const coursesOne = studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearOne : []
|
||||
const coursesTwo = studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearTwo : []
|
||||
const coursesThree = studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearThree : []
|
||||
if (studyProgrammeCriteria) {
|
||||
allCoursesOne = await getSubstitutions(coursesOne)
|
||||
allCoursesTwo = await getSubstitutions(coursesTwo)
|
||||
allCoursesThree = await getSubstitutions(coursesThree)
|
||||
}
|
||||
|
||||
const criteria = {
|
||||
courses: {
|
||||
yearOne: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearOne : [],
|
||||
yearTwo: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearTwo : [],
|
||||
yearThree: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearThree : [],
|
||||
yearOne: coursesOne,
|
||||
yearTwo: coursesTwo,
|
||||
yearThree: coursesThree,
|
||||
},
|
||||
allCourses: {
|
||||
yearOne: allCoursesOne,
|
||||
yearTwo: allCoursesTwo,
|
||||
yearThree: allCoursesThree,
|
||||
},
|
||||
credits: {
|
||||
yearOne: studyProgrammeCriteria ? studyProgrammeCriteria.creditsYearOne : 0,
|
||||
|
||||
@@ -12,13 +12,16 @@ const {
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
ElementDetail,
|
||||
ProgrammeModule,
|
||||
Transfer,
|
||||
Student,
|
||||
SemesterEnrollment,
|
||||
Semester,
|
||||
Enrollment,
|
||||
} = require('../models')
|
||||
const { formatStudyright, formatStudent, formatTransfer } = require('./studyprogrammeHelpers')
|
||||
const { getCurrentSemester } = require('./semesters')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const whereStudents = studentnumbers => {
|
||||
return studentnumbers ? studentnumbers : { [Op.not]: null }
|
||||
@@ -379,7 +382,7 @@ const getProgrammesStudyrights = async studyprogramme =>
|
||||
const getStudyRights = async students =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
|
||||
attributes: ['studyrightid', 'startdate', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
|
||||
where: {
|
||||
studentStudentnumber: students,
|
||||
},
|
||||
@@ -415,9 +418,7 @@ const getCreditsForStudyProgramme = async (codes, since) => {
|
||||
},
|
||||
},
|
||||
where: {
|
||||
credittypecode: {
|
||||
[Op.notIn]: [10, 9, 7],
|
||||
},
|
||||
credittypecode: 4,
|
||||
isStudyModule: {
|
||||
[Op.not]: true,
|
||||
},
|
||||
@@ -498,16 +499,110 @@ const getStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
|
||||
code: course.code,
|
||||
name: course.course_name,
|
||||
year: course.year,
|
||||
totalAllStudents: parseInt(course.total_students),
|
||||
totalPassed: parseInt(course.total_students),
|
||||
totalAllcredits: parseInt(course.total_credits),
|
||||
type: 'total',
|
||||
type: 'passed',
|
||||
}))
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e)
|
||||
logger.log(e)
|
||||
}
|
||||
}
|
||||
const getNotCompletedForProgrammeCourses = async (from, to, programmeCourses) => {
|
||||
try {
|
||||
const enrollmentsCourses = await Enrollment.findAll({
|
||||
attributes: ['studentnumber', 'course_code'],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: programmeCourses,
|
||||
},
|
||||
studentnumber: {
|
||||
[Op.ne]: null,
|
||||
},
|
||||
enrollment_date_time: {
|
||||
[Op.between]: [from, to],
|
||||
},
|
||||
state: 'ENROLLED',
|
||||
},
|
||||
})
|
||||
const allEnrollments = {}
|
||||
for (const { studentnumber, course_code } of enrollmentsCourses) {
|
||||
if (!(course_code in allEnrollments)) {
|
||||
allEnrollments[course_code] = []
|
||||
}
|
||||
allEnrollments[course_code].push(studentnumber)
|
||||
}
|
||||
|
||||
const credits = await Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'student_studentnumber', 'credittypecode', 'attainment_date'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['code', 'name'],
|
||||
required: true,
|
||||
where: {
|
||||
is_study_module: false,
|
||||
code: programmeCourses,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
credittypecode: [4, 7, 10],
|
||||
isStudyModule: {
|
||||
[Op.not]: true,
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.between]: [from, to],
|
||||
},
|
||||
},
|
||||
})
|
||||
const creditsObj = credits.map(credit => {
|
||||
return {
|
||||
code: credit.course_code,
|
||||
studentnumber: credit.student_studentnumber,
|
||||
credittype: credit.credittypecode,
|
||||
courseName: credit.course.name,
|
||||
year: moment(credit.attainment_date).year(),
|
||||
}
|
||||
})
|
||||
|
||||
const passedByCourseCodes = {}
|
||||
const notCompletedByCourseCodes = {}
|
||||
const courses = {}
|
||||
for (const course of creditsObj) {
|
||||
if (!(course.code in courses)) {
|
||||
courses[course.code] = {
|
||||
code: course.code,
|
||||
name: course.courseName,
|
||||
year: course.year,
|
||||
}
|
||||
|
||||
passedByCourseCodes[course.code] = []
|
||||
notCompletedByCourseCodes[course.code] = []
|
||||
}
|
||||
if ([4, 7].includes(course.credittype)) {
|
||||
passedByCourseCodes[course.code].push(course.studentnumber)
|
||||
}
|
||||
if (
|
||||
(allEnrollments[course.code]?.includes(course.studentnumber) || course.credittype === 10) &&
|
||||
!passedByCourseCodes[course.code].includes(course.studentnumber)
|
||||
) {
|
||||
notCompletedByCourseCodes[course.code].push(course.studentnumber)
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(courses)
|
||||
.reduce((acc, val) => [...acc, { ...courses[val] }], [])
|
||||
.map(course => ({
|
||||
code: course.code,
|
||||
name: course.name,
|
||||
year: course.year,
|
||||
totalNotCompleted: [...new Set(notCompletedByCourseCodes[course.code])].length,
|
||||
type: 'notCompleted',
|
||||
}))
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
logger.log(e)
|
||||
}
|
||||
}
|
||||
const getOwnStudentsForProgrammeCourses = async (from, to, programmeCourses, studyprogramme) => {
|
||||
const res = await sequelize.query(
|
||||
`
|
||||
@@ -534,7 +629,6 @@ const getOwnStudentsForProgrammeCourses = async (from, to, programmeCourses, stu
|
||||
}
|
||||
)
|
||||
return res.map(course => {
|
||||
// console.log('course: ', course)
|
||||
const res = {
|
||||
code: course.code,
|
||||
name: course.course_name,
|
||||
@@ -732,6 +826,15 @@ const getCurrentStudyYearStartDate = _.memoize(
|
||||
)
|
||||
)
|
||||
|
||||
const getProgrammeName = async code => {
|
||||
return ProgrammeModule.findOne({
|
||||
attributes: ['name'],
|
||||
where: {
|
||||
code,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
studytrackStudents,
|
||||
enrolledStudents,
|
||||
@@ -758,4 +861,6 @@ module.exports = {
|
||||
getStudyRights,
|
||||
getCourseCodesForStudyProgramme,
|
||||
getTransferStudentsForProgrammeCourses,
|
||||
getProgrammeName,
|
||||
getNotCompletedForProgrammeCourses,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const {
|
||||
getStudentsForProgrammeCourses,
|
||||
getCurrentStudyYearStartDate,
|
||||
getOwnStudentsForProgrammeCourses,
|
||||
getNotCompletedForProgrammeCourses,
|
||||
getStudentsWithoutStudyrightForProgrammeCourses,
|
||||
getTransferStudentsForProgrammeCourses,
|
||||
getOtherStudentsForProgrammeCourses,
|
||||
@@ -39,9 +40,12 @@ const makeYearlyPromises = (years, academicYear, type, programmeCourses, studypr
|
||||
let result = null
|
||||
|
||||
switch (type) {
|
||||
case 'allStudents':
|
||||
case 'passed':
|
||||
result = await getStudentsForProgrammeCourses(from, to, programmeCourses)
|
||||
break
|
||||
case 'notCompleted':
|
||||
result = await getNotCompletedForProgrammeCourses(from, to, programmeCourses)
|
||||
break
|
||||
case 'ownStudents':
|
||||
result = await getOwnStudentsForProgrammeCourses(from, to, programmeCourses, studyprogramme)
|
||||
break
|
||||
@@ -74,7 +78,13 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
const yearRange = _.range(2017, startYear + 1)
|
||||
const programmeCourses = await getAllStudyprogrammeCourses(studyprogramme)
|
||||
|
||||
const yearlyStudentByCoursePromises = makeYearlyPromises(yearRange, academicYear, 'allStudents', programmeCourses)
|
||||
const yearlyPassedStudentByCoursePromises = makeYearlyPromises(yearRange, academicYear, 'passed', programmeCourses)
|
||||
const yearlyNotCompletedStudentByCoursePromises = makeYearlyPromises(
|
||||
yearRange,
|
||||
academicYear,
|
||||
'notCompleted',
|
||||
programmeCourses
|
||||
)
|
||||
const yearlyProgrammeStudentsPromises = makeYearlyPromises(
|
||||
yearRange,
|
||||
academicYear,
|
||||
@@ -105,22 +115,24 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
)
|
||||
|
||||
const [
|
||||
yearlyStudentByCourse,
|
||||
yearlyPassedStudentByCourse,
|
||||
yearlyNotCompletedStudentByCourse,
|
||||
yearlyProgrammeStudents,
|
||||
yearlyStudentsWithoutStudyright,
|
||||
yearlyOtherProgrammeStudents,
|
||||
yearlyTransferStudents,
|
||||
] = await Promise.all([
|
||||
Promise.all(yearlyStudentByCoursePromises),
|
||||
Promise.all(yearlyPassedStudentByCoursePromises),
|
||||
Promise.all(yearlyNotCompletedStudentByCoursePromises),
|
||||
Promise.all(yearlyProgrammeStudentsPromises),
|
||||
Promise.all(yearlyStudentsWithoutStudyrightPromises),
|
||||
Promise.all(yearlyOtherProgrammeStudentsPromises),
|
||||
Promise.all(yearlyTransferStudentsPromises),
|
||||
])
|
||||
|
||||
let maxYear = 0
|
||||
const allCourses = [
|
||||
...yearlyStudentByCourse.flat(),
|
||||
...yearlyPassedStudentByCourse.flat(),
|
||||
...yearlyNotCompletedStudentByCourse.flat(),
|
||||
...yearlyProgrammeStudents.flat(),
|
||||
...yearlyStudentsWithoutStudyright.flat(),
|
||||
...yearlyOtherProgrammeStudents.flat(),
|
||||
@@ -134,9 +146,12 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
years: {},
|
||||
}
|
||||
}
|
||||
|
||||
if (!acc[curr.code]['years'][curr.year]) {
|
||||
acc[curr.code]['years'][curr.year] = {
|
||||
totalAllStudents: 0,
|
||||
totalPassed: 0,
|
||||
totalNotCompleted: 0,
|
||||
totalAllCredits: 0,
|
||||
totalProgrammeStudents: 0,
|
||||
totalProgrammeCredits: 0,
|
||||
@@ -149,10 +164,16 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
}
|
||||
}
|
||||
switch (curr.type) {
|
||||
case 'total':
|
||||
acc[curr.code]['years'][curr.year]['totalAllStudents'] += curr.totalAllStudents
|
||||
case 'passed':
|
||||
acc[curr.code]['years'][curr.year]['totalPassed'] += curr.totalPassed
|
||||
acc[curr.code]['years'][curr.year]['totalAllStudents'] += acc[curr.code]['years'][curr.year]['totalPassed']
|
||||
acc[curr.code]['years'][curr.year]['totalAllCredits'] += curr.totalAllcredits
|
||||
break
|
||||
case 'notCompleted':
|
||||
acc[curr.code]['years'][curr.year]['totalNotCompleted'] += curr.totalNotCompleted
|
||||
acc[curr.code]['years'][curr.year]['totalAllStudents'] +=
|
||||
acc[curr.code]['years'][curr.year]['totalNotCompleted']
|
||||
break
|
||||
case 'ownProgramme':
|
||||
acc[curr.code]['years'][curr.year]['totalProgrammeStudents'] += curr.totalProgrammeStudents
|
||||
acc[curr.code]['years'][curr.year]['totalProgrammeCredits'] += curr.totalProgrammeCredits
|
||||
@@ -175,6 +196,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
const ayCourses = Object.keys(allCourses).filter(c => c.startsWith('AY'))
|
||||
const properties = [
|
||||
'totalAllStudents',
|
||||
'totalPassed',
|
||||
'totalNotCompleted',
|
||||
'totalAllCredits',
|
||||
'totalProgrammeStudents',
|
||||
'totalProgrammeCredits',
|
||||
@@ -200,6 +223,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
if (!allCourses[normCode]['years'][year]) {
|
||||
mergedCourse['years'][year] = {
|
||||
totalAllStudents: 0,
|
||||
totalPassed: 0,
|
||||
totalNotCompleted: 0,
|
||||
totalAllCredits: 0,
|
||||
totalProgrammeStudents: 0,
|
||||
totalProgrammeCredits: 0,
|
||||
|
||||
@@ -152,16 +152,15 @@ const isNonMajorCredit = (studyrights, attainment_date) =>
|
||||
)
|
||||
})
|
||||
|
||||
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) => {
|
||||
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) =>
|
||||
studyrights.some(studyright => {
|
||||
if (!studyright) return true // If there is no studyright matching the credit, is not a major student credit
|
||||
if (studyright.studystartdate > attainment_date) return true // Credits before the studyright started are not major student credits
|
||||
if (studyright.enddate && attainment_date > studyright.enddate) return true // Credits after studyright are not major student credits
|
||||
if (studyright.studystartdate && new Date(studyright.studystartdate) > new Date(attainment_date)) return true // Credits before the studyright started are not major student credits
|
||||
if (studyright.enddate && new Date(attainment_date) > new Date(studyright.enddate)) return true // Credits after studyright are not major student credits
|
||||
if ([7, 9, 16, 34, 22, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
|
||||
if (transfers.includes(studyright.studyrightid)) return true // Excludes both transfers in and out of the programme
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
const getMedian = values => {
|
||||
if (values.length === 0) return 0
|
||||
|
||||
@@ -174,7 +174,8 @@ const updateAccessGroups = async (username, iamGroups = [], specialGroup = {}, s
|
||||
|
||||
// Modify accessgroups based on current groups and iamGroups
|
||||
let newAccessGroups = []
|
||||
if (sisId && (await checkStudyGuidanceGroupsAccess(sisId))) newAccessGroups.push('studyGuidanceGroups')
|
||||
if (currentAccessGroups.includes('studyGuidanceGroups') || (sisId && (await checkStudyGuidanceGroupsAccess(sisId))))
|
||||
newAccessGroups.push('studyGuidanceGroups')
|
||||
if (iamGroups.includes(courseStatisticsGroup)) newAccessGroups.push('courseStatistics')
|
||||
if (jory || iamGroups.includes(facultyStatisticsGroup)) newAccessGroups.push('facultyStatistics')
|
||||
if (hyOne || currentAccessGroups.includes('teachers')) newAccessGroups.push('teachers')
|
||||
|
||||
@@ -129,6 +129,16 @@ export default {
|
||||
**Tags**
|
||||
|
||||
Opiskelijoille voi luoda tag:eja ryhmittelyä varten kohdassa "Study Program" > "Overview" > haluttu koulutusohjelma.
|
||||
|
||||
**Progress**
|
||||
|
||||
Tämä näkymä on tällä hetkellä käytössä vain kandivaiheen opinto-ohjelmissa.
|
||||
Tällä välilehdellä näkyy opintojen kehitys annettujen kriteerien mukaan akateemista vuotta kohti. Kurssi merkataan tehdyksi, jos sillä on suoritusmerkintä tai hyväksiluku
|
||||
minä tahansa lukuvuonna. Opintopistekriteerin täyttämiseen vaaditaan, että opintopisteet on suoritettu kyseisen akateemisen vuoden aikana.
|
||||
|
||||
|
||||
**Huomaa**, että klikkaamalla taulukon yläreunassa olevaa kolme pistettä sisältävää kuvaketta, voit ladata itsellesi Excel tiedoston. Tiedosto sisältää lisäksi muita sarakkeita, kuten läsnäolon lukukausittain sekä yhteystiedot.
|
||||
Yhteystietojen käyttö on kuitenkin sallittua vain opintoneuvontaan liittyvissä asiossa.
|
||||
|
||||
`,
|
||||
StudentsCustom: `**Students**
|
||||
|
||||
@@ -105,4 +105,7 @@ export default {
|
||||
TransferredCredits: `
|
||||
Sisältää opintopisteet, jotka ovat suoritettu kyseisenä vuonna, ja hyväksiluettu tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
|
||||
`,
|
||||
NotCompleted: `
|
||||
Sisältää opiskelijat, jotka ovat ilmoittautuneet kurssille, mutta eivät ole suorittaneet sitä ja opiskelijat, jotka ovat saaneet hylätyn arvosanan.
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -628,7 +628,7 @@ const ProtoC = ({
|
||||
</Menu.Item>
|
||||
{sorterNames.map(sorterName => (
|
||||
<Menu.Item
|
||||
basic={sorter !== sorterName}
|
||||
basic={(sorter !== sorterName).toString()}
|
||||
color={sorter === sorterName ? 'blue' : 'black'}
|
||||
key={sorterName}
|
||||
active={sorter === sorterName}
|
||||
@@ -727,7 +727,7 @@ const ProtoC = ({
|
||||
</Segment>
|
||||
<RenderBelowGraph />
|
||||
<Message>
|
||||
<ReactMarkdown children={CoolDataScience.protoC} escapeHtml={false} />
|
||||
<ReactMarkdown children={CoolDataScience.protoC} />
|
||||
</Message>
|
||||
</Segment>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
import React, { useState, useCallback, useMemo } from 'react'
|
||||
import Highcharts from 'highcharts'
|
||||
import ReactHighcharts from 'react-highcharts'
|
||||
@@ -449,7 +450,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
|
||||
</Menu.Item>
|
||||
{sorterNames.map(sorterName => (
|
||||
<Menu.Item
|
||||
basic={sorter !== sorterName}
|
||||
basic={(sorter !== sorterName).toString()}
|
||||
color={sorter === sorterName ? 'blue' : 'black'}
|
||||
key={sorterName}
|
||||
active={sorter === sorterName}
|
||||
@@ -485,7 +486,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
|
||||
<Message>
|
||||
{
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={CoolDataScience.protoC2} escapeHtml={false} />
|
||||
<ReactMarkdown children={CoolDataScience.protoC2} />
|
||||
}
|
||||
</Message>
|
||||
</Segment>
|
||||
@@ -541,7 +542,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
|
||||
</Menu.Item>
|
||||
{sorterNames.map(sorterName => (
|
||||
<Menu.Item
|
||||
basic={sorter !== sorterName}
|
||||
basic={(sorter !== sorterName).toString()}
|
||||
color={sorter === sorterName ? 'blue' : 'black'}
|
||||
key={sorterName}
|
||||
active={sorter === sorterName}
|
||||
@@ -594,7 +595,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
|
||||
<Message>
|
||||
{
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={CoolDataScience.protoC2} escapeHtml={false} />
|
||||
<ReactMarkdown children={CoolDataScience.protoC2} />
|
||||
}
|
||||
</Message>
|
||||
</Segment>
|
||||
|
||||
@@ -100,7 +100,7 @@ const Chart = React.memo(({ tickDates, snapshots }) => (
|
||||
return `${d.getDate()}.${d.getMonth()}.${d
|
||||
.getFullYear()
|
||||
.toString()
|
||||
.substr(-2)}`
|
||||
.substring(-2)}`
|
||||
}
|
||||
},
|
||||
tickPositions: tickDates.map(date => date.getTime()),
|
||||
@@ -292,7 +292,7 @@ const ProtoG = ({ uberdata, years, isLoading, loadingYears, getUberDispatch, get
|
||||
)}
|
||||
<Message>
|
||||
{//eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={CoolDataScience.protoC} escapeHtml={false} />
|
||||
<ReactMarkdown children={CoolDataScience.protoC} />
|
||||
}
|
||||
</Message>
|
||||
</Segment>
|
||||
|
||||
@@ -185,7 +185,7 @@ export default ({ value, changeSetting, settings, generalHelp }) => {
|
||||
<div style={{ padding: '1em' }}>
|
||||
{
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={unindent(generalHelp)} escapeHtml={false} />
|
||||
<ReactMarkdown children={unindent(generalHelp)} />
|
||||
}
|
||||
</div>
|
||||
{settings.map(({ key, label, long }) => (
|
||||
@@ -196,7 +196,7 @@ export default ({ value, changeSetting, settings, generalHelp }) => {
|
||||
Valinta "<i>{label}</i>"
|
||||
</b>
|
||||
<div style={{ margin: '0.5em', fontSize: '0.9em' }}>
|
||||
<ReactMarkdown escapeHtml={false}>{unindent(long ?? '')}</ReactMarkdown>
|
||||
<ReactMarkdown>{unindent(long ?? '')}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,8 +39,8 @@ const CustomOpenUniPopulation = () => {
|
||||
<p>
|
||||
Here you can create custom population using a list of courses. Clicking the blue custom population button will
|
||||
open a modal where you can enter a list of courses. <br />
|
||||
<Icon fitted name="check" color="green" />: Student passed the course. <br />
|
||||
<Icon fitted name="times" color="red" />: Student failed the course. <br />
|
||||
<Icon fitted name="check" color="green" />: Student has passed the course. <br />
|
||||
<Icon fitted name="times" color="red" />: Student has failed the course. <br />
|
||||
<Icon fitted name="minus" color="grey" />: Student has enrolled, but has not received any grade from the
|
||||
course. <br />
|
||||
<b>Empty cell</b>: Student has no enrollments for the course. <br />
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react'
|
||||
import GraduationTimes from 'components/FacultyStatistics/TimesAndPaths/GraduationTimes'
|
||||
|
||||
const FacultyGraduations = ({ faculty, graduationStats, groupByStartYear, showMedian }) => {
|
||||
const groupBy = groupByStartYear ? 'byStartYear' : 'byGradYear'
|
||||
const label = groupByStartYear ? 'Start year' : 'Graduation year'
|
||||
const data = graduationStats?.data?.[groupBy].medians
|
||||
const goals = graduationStats?.data?.goals
|
||||
const goalExceptions = { ...goals?.exceptions, needed: faculty === 'H30' }
|
||||
const programmeData = graduationStats?.data?.[groupBy].programmes.medians
|
||||
const programmeNames = graduationStats?.data?.programmeNames
|
||||
const classSizes = graduationStats?.data?.classSizes
|
||||
const commonProps = { label, programmeNames, showMedian, classSizes, goalExceptions }
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<GraduationTimes
|
||||
level="bachelor"
|
||||
title="Bachelor"
|
||||
data={data?.bachelor}
|
||||
goal={goals?.bachelor}
|
||||
levelProgrammeData={programmeData?.bachelor}
|
||||
{...commonProps}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="bcMsCombo"
|
||||
title="Bachelor + Master"
|
||||
data={data?.bcMsCombo}
|
||||
goal={goals?.bcMsCombo}
|
||||
levelProgrammeData={programmeData?.bcMsCombo}
|
||||
groupBy={groupBy}
|
||||
{...commonProps}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="master"
|
||||
title="Master"
|
||||
data={data?.master}
|
||||
goal={goals?.master}
|
||||
levelProgrammeData={programmeData?.master}
|
||||
{...commonProps}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="doctor"
|
||||
title="Doctor"
|
||||
data={data?.doctor}
|
||||
goal={goals?.doctor}
|
||||
levelProgrammeData={programmeData?.doctor}
|
||||
{...commonProps}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="licentiate"
|
||||
title="Licentiate"
|
||||
data={data?.licentiate}
|
||||
goal={goals?.licentiate}
|
||||
levelProgrammeData={programmeData?.licentiate}
|
||||
{...commonProps}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FacultyGraduations
|
||||
@@ -0,0 +1,153 @@
|
||||
import React from 'react'
|
||||
import { Message } from 'semantic-ui-react'
|
||||
|
||||
import FacultyProgressTable from '../FacultyStatistics/FacultyProgrammeOverview/FacultyProgressTable'
|
||||
import FacultyBarChart from '../FacultyStatistics/FacultyProgrammeOverview/FacultyBarChart'
|
||||
import sortProgrammeKeys from '../FacultyStatistics/facultyHelpers'
|
||||
|
||||
const FacultyProgress = ({ faculty, progressStats, language, getDivider }) => {
|
||||
return (
|
||||
<>
|
||||
{getDivider('Bachelor', 'BachelorStudentsOfTheFacultyByStartingYear', 'no-infobox')}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyBachelorsProgress"
|
||||
data={{
|
||||
id: faculty,
|
||||
stats: progressStats?.data.bachelorsGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.bachelorsTableStats}
|
||||
programmeStats={progressStats?.data.bachelorsProgStats}
|
||||
titles={progressStats?.data.bachelorTitles}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.bachelorsProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyBachelorsProgressTable"
|
||||
progressTitles={progressStats?.data.yearlyBachelorTitles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{getDivider('Bachelor + Master', 'ProgressOfBachelorMaster', 'no-infobox')}
|
||||
<Message data-cy="FacultyProgrammesShownInfo">
|
||||
The starting year is the studyright start in the bachelor programme. The credits are comouted by the start date
|
||||
of the bachelor programme and at the moment, they do not include any transferred credits. Thus, in these
|
||||
statistics some students have less credits than in reality.
|
||||
</Message>
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyBachelorMastersProgress"
|
||||
data={{
|
||||
id: faculty,
|
||||
stats: progressStats?.data.bcMsGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.bcMsTableStats}
|
||||
programmeStats={progressStats?.data.bcMsProgStats}
|
||||
titles={progressStats?.data.bcMsTitles}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.bcMsProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyBachelorMasterProgressTable"
|
||||
progressTitles={progressStats?.data.yearlyBcMsTitles}
|
||||
needsExtra={faculty === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!(faculty === 'H90') && (
|
||||
<>
|
||||
{getDivider('Master', 'MasterStudentsOfTheFacultyByStartingYear', 'no-infobox')}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyMastersProgress"
|
||||
data={{
|
||||
id: faculty,
|
||||
stats: progressStats?.data.mastersGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.mastersTableStats}
|
||||
programmeStats={progressStats?.data.mastersProgStats}
|
||||
titles={progressStats?.data.mastersTitles}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.mastersProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyMastersProgressTable"
|
||||
progressTitles={progressStats?.data.yearlyMasterTitles}
|
||||
needsExtra={faculty === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{getDivider('Doctor', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyDoctoralProgress"
|
||||
data={{
|
||||
id: faculty,
|
||||
stats: progressStats?.data.doctoralGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.doctoralTableStats}
|
||||
programmeStats={progressStats?.data.doctoralProgStats}
|
||||
titles={progressStats?.data.doctoralTitles}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.doctoralProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyDoctoralProgressTable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FacultyProgress
|
||||
@@ -0,0 +1,145 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Divider, Header, Loader, Message } from 'semantic-ui-react'
|
||||
import {
|
||||
useGetFacultiesQuery,
|
||||
useGetFacultyProgressStatsQuery,
|
||||
useGetFacultyGraduationTimesQuery,
|
||||
} from 'redux/facultyStats'
|
||||
import { getTextIn } from 'common'
|
||||
|
||||
import FacultyGraduations from './FacultyGraduations'
|
||||
import FacultyProgress from './FacultyProgress'
|
||||
|
||||
import Toggle from '../StudyProgramme/Toggle'
|
||||
import InfoBox from '../Info/InfoBox'
|
||||
import InfotoolTips from '../../common/InfoToolTips'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import '../FacultyStatistics/faculty.css'
|
||||
|
||||
const FacultyView = ({ faculty }) => {
|
||||
const [graduatedGroup, setGraduatedGroup] = useState(false)
|
||||
const [showMedian, setShowMedian] = useState(false)
|
||||
const toolTips = InfotoolTips.Faculty
|
||||
const studyProgrammeFilter = 'NEW_STUDY_PROGRAMMES'
|
||||
const specials = 'SPECIAL_EXCLUDED'
|
||||
const graduated = graduatedGroup ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
|
||||
const { language } = useLanguage()
|
||||
|
||||
const allFaculties = useGetFacultiesQuery()
|
||||
const faculties = allFaculties?.data
|
||||
const facultyDetails = faculties && faculty && faculties.find(f => f.code === faculty)
|
||||
const facultyName = facultyDetails && getTextIn(facultyDetails.name, language)
|
||||
|
||||
const progressStats = useGetFacultyProgressStatsQuery({
|
||||
id: faculty,
|
||||
studyProgrammeFilter,
|
||||
specialGroups: specials,
|
||||
graduated,
|
||||
})
|
||||
|
||||
const graduationStats = useGetFacultyGraduationTimesQuery({ id: faculty, studyProgrammeFilter })
|
||||
|
||||
const getDivider = (title, toolTipText, content, cypress = undefined) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
</div>
|
||||
{content === 'no-infobox' ? null : <InfoBox content={content} cypress={cypress} />}
|
||||
</>
|
||||
)
|
||||
|
||||
if (allFaculties.isLoading || allFaculties.isFetching) {
|
||||
return <Loader active style={{ marginTop: '10em' }} />
|
||||
}
|
||||
|
||||
const isFetchingOrLoading =
|
||||
progressStats.isLoading || progressStats.isFetching || graduationStats.isLoading || graduationStats.isFetching
|
||||
|
||||
const isError =
|
||||
(progressStats.isError && graduationStats.isError) ||
|
||||
(progressStats.isSuccess && !progressStats.data && graduationStats.isSuccess && !graduationStats.data)
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
return (
|
||||
<>
|
||||
<div align="center" style={{ padding: '30px' }}>
|
||||
<Header textAlign="center">{facultyName}</Header>
|
||||
<span>{faculty}</span>
|
||||
</div>
|
||||
<Message info>
|
||||
<Message.Header>This view is an abridged version of Oodikone's Faculty Overview</Message.Header>
|
||||
<p>
|
||||
In these statistics, all special studyrights have been excluded, eg. exchange students and non-degree
|
||||
students.
|
||||
</p>
|
||||
<p>
|
||||
<b>You can find more statistics on this and all other faculties of the university in main Faculty view. </b>
|
||||
The full view includes details such as: how many have graduated or started in each faculty and in its
|
||||
programmes; credits produced by the faculty; populations backgrounds and enrollment statuses. This view
|
||||
provides statistics both on the whole faculty level and a breakdown of how they are spread among the faculty's
|
||||
programmes.
|
||||
</p>
|
||||
<p>Access the full Faculty view by clicking 'Faculty' in the top navigation bar.</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',
|
||||
toolTips.StudentProgress,
|
||||
'InfoFacultyProgress'
|
||||
)}
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="GraduatedToggle"
|
||||
toolTips={toolTips.GraduatedToggle}
|
||||
firstLabel="Graduated included"
|
||||
secondLabel="Graduated excluded"
|
||||
value={graduatedGroup}
|
||||
setValue={setGraduatedGroup}
|
||||
/>
|
||||
</div>
|
||||
<FacultyProgress
|
||||
faculty={faculty}
|
||||
progressStats={progressStats}
|
||||
language={language}
|
||||
getDivider={getDivider}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{graduationStats.isSuccess && graduationStats.data && (
|
||||
<>
|
||||
{getDivider('Average graduation times', 'AverageGraduationTimes', toolTips.AverageGraduationTimes)}
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="GraduationTimeToggle"
|
||||
firstLabel="Breakdown"
|
||||
secondLabel="Median times"
|
||||
value={showMedian}
|
||||
setValue={setShowMedian}
|
||||
/>
|
||||
</div>
|
||||
<FacultyGraduations
|
||||
faculty={faculty}
|
||||
graduationStats={graduationStats}
|
||||
groupByStartYear={false}
|
||||
showMedian={showMedian}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FacultyView
|
||||
@@ -0,0 +1,194 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Divider, Header, Loader, Message, Segment } from 'semantic-ui-react'
|
||||
import { useGetEvaluationStatsQuery } from 'redux/studyProgramme'
|
||||
import { getTextIn } from '../../common'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import Toggle from '../StudyProgramme/Toggle'
|
||||
import InfoBox from '../Info/InfoBox'
|
||||
import MedianTimeBarChart from '../StudyProgramme/MedianTimeBarChart'
|
||||
import BreakdownBarChart from '../StudyProgramme/BreakdownBarChart'
|
||||
import StackedBarChart from '../StudyProgramme/BasicOverview/StackedBarChart'
|
||||
import BarChart from '../StudyProgramme/StudytrackOverview/BarChart'
|
||||
import BasicDataTable from '../StudyProgramme/StudytrackOverview/BasicDataTable'
|
||||
import DataTable from '../StudyProgramme/BasicOverview/DataTable'
|
||||
import InfotoolTips from '../../common/InfoToolTips'
|
||||
import '../StudyProgramme/studyprogramme.css'
|
||||
|
||||
const ProgrammeView = ({ studyprogramme }) => {
|
||||
const { language } = useLanguage()
|
||||
const [showMedian, setShowMedian] = useState(false)
|
||||
const [academicYear, setAcademicYear] = useState(false)
|
||||
const [graduated, setGraduated] = useState(false)
|
||||
|
||||
const toolTips = InfotoolTips.Studyprogramme
|
||||
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
|
||||
const grad = graduated ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
|
||||
|
||||
const statistics = useGetEvaluationStatsQuery({
|
||||
id: studyprogramme,
|
||||
yearType,
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
graduated: grad,
|
||||
})
|
||||
|
||||
const programmeName = statistics?.data?.programmeName && getTextIn(statistics?.data?.programmeName, language)
|
||||
|
||||
const graduationData = statistics?.data?.graduations
|
||||
const progressData = statistics?.data?.progress
|
||||
|
||||
const doCombo = graduationData?.doCombo
|
||||
const timesData = graduationData?.graduationTimes
|
||||
|
||||
const getDivider = (title, toolTipText) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
</div>
|
||||
<InfoBox content={toolTips[toolTipText]} />
|
||||
</>
|
||||
)
|
||||
|
||||
const displayMedian = () => (
|
||||
<>
|
||||
{doCombo && (
|
||||
<MedianTimeBarChart
|
||||
data={graduationData?.comboTimes?.medians}
|
||||
goal={graduationData?.comboTimes?.goal}
|
||||
title="Bachelor + Master studyright"
|
||||
byStartYear={false}
|
||||
/>
|
||||
)}
|
||||
<MedianTimeBarChart
|
||||
data={graduationData?.graduationTimes?.medians}
|
||||
goal={graduationData?.graduationTimes?.goal}
|
||||
title={doCombo ? 'Master studyright' : ' '}
|
||||
byStartYear={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
const displayBreakdown = () => (
|
||||
<>
|
||||
{doCombo && <BreakdownBarChart data={graduationData?.comboTimes?.medians} title="Bachelor + Master studyright" />}
|
||||
<BreakdownBarChart data={timesData?.medians} title={doCombo ? 'Master studyright' : ' '} />
|
||||
</>
|
||||
)
|
||||
|
||||
const isFetchingOrLoading = statistics.isLoading || statistics.isFetching
|
||||
|
||||
const isError = statistics.isError || (statistics.isSuccess && !statistics.data)
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
return (
|
||||
<>
|
||||
<div align="center" style={{ padding: '30px' }}>
|
||||
<Header textAlign="center">{programmeName}</Header>
|
||||
<span>{studyprogramme}</span>
|
||||
</div>
|
||||
<div className="studyprogramme-overview">
|
||||
{isFetchingOrLoading ? (
|
||||
<Loader active style={{ marginTop: '10em' }} />
|
||||
) : (
|
||||
<Segment className="contentSegment">
|
||||
<Message info>
|
||||
<Message.Header>This view is an abridged version of Oodikone's Studyprogramme Overview</Message.Header>
|
||||
<p>
|
||||
In these statistics, all special studyrights have been excluded, eg. exchange students and non-degree
|
||||
students.
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
You can find more statistics on this and other studyprogrammes of the university in main
|
||||
Studyprogramme Overview.{' '}
|
||||
</b>
|
||||
The full view includes details such as: how many have graduated or started in each programme and in its
|
||||
studytracks; credits produced by the programme; population backgrounds and enrollment statuses.
|
||||
</p>
|
||||
<p>
|
||||
Access the full Studyprogramme Overview by clicking 'Studyprogramme' in the top navigation bar and then
|
||||
'Overview'. Alternatively, you can select 'Search by class' to explore studyprogrammes by starting class
|
||||
with interactive statistics and visualizations.
|
||||
</p>
|
||||
<p>
|
||||
{' '}
|
||||
Note that both views have access restrictions. If you can't access either view and feel you should have
|
||||
access, please contact oodikone@helsinki.fi.
|
||||
</p>
|
||||
</Message>
|
||||
<div>
|
||||
{getDivider(`Progress of students of the studyprogramme by starting year`, 'StudytrackProgress')}
|
||||
<Toggle
|
||||
cypress="GraduatedToggle"
|
||||
toolTips={toolTips.GraduatedToggle}
|
||||
firstLabel="Graduated included"
|
||||
secondLabel="Graduated excluded"
|
||||
value={graduated}
|
||||
setValue={setGraduated}
|
||||
/>
|
||||
<div className="section-container">
|
||||
<BarChart cypress="StudytrackProgress" data={progressData?.graphData} track={studyprogramme} />
|
||||
<BasicDataTable
|
||||
cypress="StudytrackProgress"
|
||||
data={progressData?.creditTableStats}
|
||||
track={studyprogramme}
|
||||
titles={progressData?.creditTableTitles}
|
||||
/>
|
||||
</div>
|
||||
{graduationData?.programmesBeforeOrAfterGraphStats && (
|
||||
<>
|
||||
{getDivider('Graduation times', 'AverageGraduationTimes')}
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="GraduationTimeToggle"
|
||||
firstLabel="Breakdown"
|
||||
secondLabel="Median time"
|
||||
value={showMedian}
|
||||
setValue={setShowMedian}
|
||||
/>
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="YearToggle"
|
||||
toolTips={toolTips.YearToggle}
|
||||
firstLabel="Calendar year"
|
||||
secondLabel="Academic year"
|
||||
value={academicYear}
|
||||
setValue={setAcademicYear}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`section-container${doCombo ? '' : '-centered'}`}>
|
||||
{showMedian ? displayMedian() : displayBreakdown()}
|
||||
</div>
|
||||
{getDivider(
|
||||
studyprogramme.includes('KH')
|
||||
? 'Primary master programme studies after this programme'
|
||||
: 'Primary bachelor programme studies before this programme',
|
||||
'ProgrammesBeforeOrAfter'
|
||||
)}
|
||||
<div className="section-container">
|
||||
<StackedBarChart
|
||||
cypress="ProgrammesBeforeOrAfter"
|
||||
wideTable
|
||||
data={graduationData?.programmesBeforeOrAfterGraphStats}
|
||||
labels={graduationData?.years}
|
||||
/>
|
||||
<DataTable
|
||||
wideTable
|
||||
data={graduationData?.programmesBeforeOrAfterTableStats}
|
||||
titles={graduationData?.programmesBeforeOrAfterTitles}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Segment>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProgrammeView
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
import { Redirect, withRouter } from 'react-router-dom'
|
||||
import { Segment } from 'semantic-ui-react'
|
||||
|
||||
import FacultyView from './FacultyView'
|
||||
import ProgrammeView from './ProgrammeView'
|
||||
|
||||
const EvaluationOverview = props => {
|
||||
const { id, level } = props.match.params
|
||||
|
||||
if (!(level === 'programme' || level === 'faculty') || !id) {
|
||||
return <Redirect to="/" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="segmentContainer">
|
||||
<Segment className="contentSegment">
|
||||
{level === 'programme' ? <ProgrammeView studyprogramme={id} /> : <FacultyView faculty={id} />}
|
||||
</Segment>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(EvaluationOverview)
|
||||
@@ -118,13 +118,11 @@ const Overview = ({
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
let creditShortTitles = []
|
||||
const creditShortTitles = ['Code', 'Total', 'Major', 'Non-major', 'Non-major other', 'Non-degree']
|
||||
let transferShortTitles = []
|
||||
if (special === 'SPECIAL_INCLUDED') {
|
||||
creditShortTitles = ['Code', 'Total', 'Major', 'Non-major', 'Non-major other', 'Non-degree']
|
||||
transferShortTitles = ['Code', 'Started', 'Graduated', 'Transferred in', 'Transferred away', 'Transferred to']
|
||||
} else {
|
||||
creditShortTitles = ['Code', 'Major']
|
||||
transferShortTitles = ['Code', 'Started', 'Graduated']
|
||||
}
|
||||
|
||||
@@ -222,7 +220,8 @@ const Overview = ({
|
||||
)}
|
||||
{special === 'SPECIAL_EXCLUDED' && (
|
||||
<Message data-cy="FacultyExcludeSpecialsInfo">
|
||||
Please note that the data might be inaccurate in some fields. Checking the values is in progress.
|
||||
Please note: exluding the special studyrights does not have any effects to credits produced by faculty
|
||||
-view.
|
||||
</Message>
|
||||
)}
|
||||
{basics.isSuccess && basics.data && (
|
||||
@@ -274,58 +273,6 @@ const Overview = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{credits.isSuccess && credits.data && (
|
||||
<>
|
||||
{getDivider(
|
||||
'Credits produced by the faculty',
|
||||
'CreditsProducedByTheFaculty',
|
||||
credits?.data?.titles,
|
||||
credits?.data?.tableStats,
|
||||
credits?.data?.programmeTableStats,
|
||||
credits?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<div className="graph-container-narrow">
|
||||
<StackedBarChart
|
||||
cypress="CreditsProducedByTheFaculty"
|
||||
data={credits?.data?.graphStats}
|
||||
labels={credits?.data?.years}
|
||||
wideTable="narrow"
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container-wide">
|
||||
<InteractiveDataTable
|
||||
cypress="CreditsProducedByTheFaculty"
|
||||
dataStats={credits?.data?.tableStats}
|
||||
dataProgrammeStats={credits?.data?.programmeTableStats}
|
||||
programmeNames={credits?.data?.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(credits?.data.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
credits?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty.code
|
||||
).map(listObj => listObj[0])}
|
||||
plotLinePlaces={getChartPlotLinePlaces(
|
||||
sortProgrammeKeys(
|
||||
Object.keys(credits?.data?.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
credits?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty.code
|
||||
)
|
||||
)}
|
||||
titles={credits?.data?.titles}
|
||||
extraHeight="EXTRA HEIGHT"
|
||||
sliceStart={2}
|
||||
language={language}
|
||||
yearsVisible={Array(credits?.data?.tableStats.length).fill(false)}
|
||||
shortNames={creditShortTitles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{basics.isSuccess && basics.data && (
|
||||
<>
|
||||
{getDivider(
|
||||
@@ -423,6 +370,58 @@ const Overview = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{credits.isSuccess && credits.data && (
|
||||
<>
|
||||
{getDivider(
|
||||
'Credits produced by the faculty',
|
||||
'CreditsProducedByTheFaculty',
|
||||
credits?.data?.titles,
|
||||
credits?.data?.tableStats,
|
||||
credits?.data?.programmeTableStats,
|
||||
credits?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<div className="graph-container-narrow">
|
||||
<StackedBarChart
|
||||
cypress="CreditsProducedByTheFaculty"
|
||||
data={credits?.data?.graphStats}
|
||||
labels={credits?.data?.years}
|
||||
wideTable="narrow"
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container-wide">
|
||||
<InteractiveDataTable
|
||||
cypress="CreditsProducedByTheFaculty"
|
||||
dataStats={credits?.data?.tableStats}
|
||||
dataProgrammeStats={credits?.data?.programmeTableStats}
|
||||
programmeNames={credits?.data?.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(credits?.data.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
credits?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty.code
|
||||
).map(listObj => listObj[0])}
|
||||
plotLinePlaces={getChartPlotLinePlaces(
|
||||
sortProgrammeKeys(
|
||||
Object.keys(credits?.data?.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
credits?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty.code
|
||||
)
|
||||
)}
|
||||
titles={credits?.data?.titles}
|
||||
extraHeight="EXTRA HEIGHT"
|
||||
sliceStart={2}
|
||||
language={language}
|
||||
yearsVisible={Array(credits?.data?.tableStats.length).fill(false)}
|
||||
shortNames={creditShortTitles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -110,7 +110,7 @@ const FigureContainer = ({ children, style }) => {
|
||||
height: '100% !important',
|
||||
}}
|
||||
>
|
||||
<div style={{ inset: 0, position: 'absolute', zIndex: 900, height: '100000vh' }}>
|
||||
<div style={{ inset: '70px 0px 0px 0px', position: 'absolute', zIndex: 900, height: '100000vh' }}>
|
||||
{isFullscreen && <OutPortal node={portalNode} />}
|
||||
</div>
|
||||
</Portal>
|
||||
|
||||
@@ -74,7 +74,7 @@ const FilterTray = () => {
|
||||
</div>
|
||||
{haveOptionsBeenChanged && (
|
||||
<div style={{ marginTop: '0.5rem', textAlign: 'center' }}>
|
||||
<Button compact size="mini" onClick={resetFilters}>
|
||||
<Button compact size="mini" onClick={resetFilters} data-cy="reset-all-filters">
|
||||
Reset All Filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ const admissionTypeFilter = code => value => student =>
|
||||
student.studyrights.some(
|
||||
sr =>
|
||||
sr.studyright_elements.some(sre => sre.code === code) &&
|
||||
(value === null ? !sr.admission_type : sr.admission_type === value)
|
||||
(value === null || value === 'Ei valintatapaa' ? !sr.admission_type : sr.admission_type === value)
|
||||
)
|
||||
|
||||
const AdmissionTypeFilterCard = ({ options, onOptionsChange, withoutSelf, code }) => {
|
||||
|
||||
@@ -7,26 +7,20 @@ import { FilterType } from './filterType'
|
||||
const filterTexts = {
|
||||
[FilterType.ALL]: { label: 'All' },
|
||||
[FilterType.PASSED]: { label: 'Passed' },
|
||||
[FilterType.PASSED_AFTER_FAILURE]: { label: 'Passed After Failure' },
|
||||
// [FilterType.FAILED]: { label: 'Failed' },
|
||||
// [FilterType.FAILED_MANY_TIMES]: { label: 'Failed Multiple Times' },
|
||||
// [FilterType.NOT_PARTICIPATED]: { label: 'Not Participated' },
|
||||
// [FilterType.DID_NOT_PASS]: { label: "Didn't Pass" },
|
||||
[FilterType.FAILED]: { label: 'Failed' },
|
||||
[FilterType.ENROLLED_NO_GRADE]: { label: 'Enrolled, No Grade' },
|
||||
// [FilterType.NOT_PARTICIPATED]: { label: 'Not Participated'},
|
||||
}
|
||||
// a bandaid solution to prevent oodikone crashing
|
||||
const translate = {
|
||||
ALL: 'all',
|
||||
PASSED: 'passed',
|
||||
PASSED_AFTER_FAILURE: 'retryPassed',
|
||||
FAILED: 'failed',
|
||||
FAILED_MANY_TIMES: 'failedMany',
|
||||
NOT_PARTICIPATED: 'notParticipated',
|
||||
DID_NOT_PASS: 'didNotPass',
|
||||
ENROLLED_NO_GRADE: 'enrolledNoGrade',
|
||||
// NOT_PARTICIPATED: 'notParticipated',
|
||||
}
|
||||
|
||||
const CourseCard = ({ course, filterType, onChange }) => {
|
||||
// Object.entries(filterTexts).map(([type, { label, info }]) => console.log(type, label, translate[type]))
|
||||
// console.log('course', course)
|
||||
const { language } = useLanguage()
|
||||
const name = 'courseFilter'
|
||||
|
||||
@@ -47,7 +41,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
|
||||
fluid
|
||||
className="mini"
|
||||
button
|
||||
data-cy={`${name}-${course.course.code}-dropdown`}
|
||||
data-cy={`${name}-${course?.course?.code}-dropdown`}
|
||||
style={{ marginTop: '0.5rem' }}
|
||||
>
|
||||
<Dropdown.Menu>
|
||||
@@ -74,7 +68,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
|
||||
size="tiny"
|
||||
onClick={clear}
|
||||
icon
|
||||
data-cy={`${name}-${course.course.code}-clear`}
|
||||
data-cy={`${name}-${course?.course?.code}-clear`}
|
||||
style={{ marginTop: '0.5rem' }}
|
||||
>
|
||||
<Icon name="close" />
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
export const FilterType = {
|
||||
ALL: 'ALL',
|
||||
PASSED: 'PASSED',
|
||||
PASSED_AFTER_FAILURE: 'PASSED_AFTER_FAILURE',
|
||||
FAILED: 'FAILED',
|
||||
FAILED_MANY_TIMES: 'FAILED_MANY_TIMES',
|
||||
NOT_PARTICIPATED: 'NOT_PARTICIPATED',
|
||||
DID_NOT_PASS: 'DID_NOT_PASS',
|
||||
ENROLLED_NO_GRADE: 'ENROLLED_NO_GRADE',
|
||||
// NOT_PARTICIPATED: 'NOT_PARTICIPATED',
|
||||
}
|
||||
|
||||
@@ -72,20 +72,17 @@ const CourseFilterCard = ({ courseStats, options, onOptionsChange }) => {
|
||||
}
|
||||
|
||||
const createFilterFunc =
|
||||
(key, invert = false) =>
|
||||
({ studentNumber }, { students }) =>
|
||||
Object.keys(students[key]).includes(studentNumber) !== invert
|
||||
key =>
|
||||
({ studentNumber }, { students }) => {
|
||||
return Object.keys(students[key]).includes(studentNumber)
|
||||
}
|
||||
|
||||
const filterFunctions = {
|
||||
[FilterType.ALL]: createFilterFunc('all'),
|
||||
[FilterType.PASSED]: createFilterFunc('passed'),
|
||||
[FilterType.PASSED_AFTER_FAILURE]: createFilterFunc('retryPassed'),
|
||||
// [FilterType.FAILED]: createFilterFunc('failed'),
|
||||
// [FilterType.FAILED_MANY_TIMES]: createFilterFunc('failedMany'),
|
||||
// [FilterType.DID_NOT_PASS]: createFilterFunc('didNotPass'),
|
||||
// [FilterType.NOT_PARTICIPATED]: createFilterFunc('all', true),
|
||||
// [FilterType.DID_NOT_PASS]: ({ studentNumber }, { students }) =>
|
||||
// !Object.keys(students.all).includes(studentNumber) || Object.keys(students.failed).includes(studentNumber),
|
||||
[FilterType.FAILED]: createFilterFunc('failed'),
|
||||
[FilterType.ENROLLED_NO_GRADE]: createFilterFunc('enrolledNoGrade'),
|
||||
// [FilterType.NOT_PARTICIPATED]: createFilterFunc('not_participated'),
|
||||
}
|
||||
|
||||
const filter = createFilter({
|
||||
|
||||
@@ -7,32 +7,6 @@ import { selectViewFilters, setFilterOptions, resetViewFilters, resetFilter } fr
|
||||
import FilterViewContext from './FilterViewContext'
|
||||
import FilterTray from './FilterTray'
|
||||
|
||||
/* const getFilterContext = (filter, students, options, precomputed) => ({
|
||||
options: options[filter.key] ?? filter.defaultOptions,
|
||||
students,
|
||||
precomputed: precomputed[filter.key],
|
||||
})
|
||||
|
||||
const precompute = (students, filters, options) =>
|
||||
_.chain(filters)
|
||||
.filter(({ precompute }) => precompute)
|
||||
.map(({ key, precompute, defaultOptions }) => precompute({
|
||||
students,
|
||||
options: options[key] ?? defaultOptions,
|
||||
precomputed: null,
|
||||
}))
|
||||
.fromPairs()
|
||||
.value()
|
||||
|
||||
const applyFilters = (students, filters, options, precomputed) => {
|
||||
return filters
|
||||
.map((filter) => [filter, { students, options: options[filter.key], precomputed: precomputed[filter.key] }])
|
||||
.filter(([{ key, isActive }, ctx]) => isActive(options[key], ctx))
|
||||
.reduce((students, [{ key, filter }, ctx]) => {
|
||||
return students.filter(student => filter(student, options[key], precomputed[key]))
|
||||
}, students)
|
||||
} */
|
||||
|
||||
const resolveFilterOptions = (store, filters, initialOptions) => {
|
||||
return fp.flow(
|
||||
fp.map(({ key, defaultOptions }) => [
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Accordion } from 'semantic-ui-react'
|
||||
import useFilters from 'components/FilterView/useFilters'
|
||||
import studyPlanFilter from 'components/FilterView/filters/hops'
|
||||
import { creditDateFilter } from 'components/FilterView/filters'
|
||||
import { useGetProgressCriteriaQuery } from 'redux/programmeProgressCriteria'
|
||||
import { useLocalStorage } from '../../common/hooks'
|
||||
import { useGetAuthorizedUserQuery } from '../../redux/auth'
|
||||
|
||||
import CreditAccumulationGraphHighCharts from '../CreditAccumulationGraphHighCharts'
|
||||
import PopulationStudents from '../PopulationStudents'
|
||||
import PopulationCourses from '../PopulationCourses'
|
||||
@@ -37,6 +37,7 @@ const PopulationDetails = ({
|
||||
const studentTableRef = useRef()
|
||||
const { useFilterSelector } = useFilters()
|
||||
const creditDateFilterOptions = useFilterSelector(creditDateFilter.selectors.selectOptions)
|
||||
const criteria = useGetProgressCriteriaQuery({ programmeCode: query?.studyRights?.programme })
|
||||
|
||||
const handleClick = index => {
|
||||
const indexes = [...activeIndex].sort()
|
||||
@@ -190,6 +191,9 @@ const PopulationDetails = ({
|
||||
language={language}
|
||||
filteredStudents={filteredStudents}
|
||||
dataExport={dataExport}
|
||||
criteria={criteria?.data}
|
||||
programmeCode={query?.studyRights?.programme}
|
||||
year={query?.year}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -435,7 +435,7 @@ const PopulationSearchForm = props => {
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
<Message error color="blue" header={errorText} />
|
||||
<Form.Button onClick={handleSubmit} disabled={isQueryInvalid || query.months < 0}>
|
||||
<Form.Button color="blue" onClick={handleSubmit} disabled={isQueryInvalid || query.months < 0}>
|
||||
See class
|
||||
</Form.Button>
|
||||
<SearchHistory items={searchHistory} updateItem={updateItemInSearchHistory} handleSearch={pushQueryToUrl} />
|
||||
|
||||
@@ -3,14 +3,14 @@ import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Dropdown } from 'semantic-ui-react'
|
||||
import xlsx from 'xlsx'
|
||||
import { getTextIn, reformatDate } from '../../common'
|
||||
import { getTextIn, reformatDate, getStudentTotalCredits } from '../../common'
|
||||
import sendEvent from '../../common/sendEvent'
|
||||
import { PRIORITYCODE_TEXTS } from '../../constants'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
|
||||
const sendAnalytics = sendEvent.populationStudents
|
||||
|
||||
export default ({ students }) => {
|
||||
export default ({ students, programmeCode }) => {
|
||||
const { language } = useLanguage()
|
||||
const mandatoryCourses = useSelector(({ populationMandatoryCourses }) => populationMandatoryCourses.data)
|
||||
const populationStatistics = useSelector(({ populations }) => populations.data)
|
||||
@@ -133,14 +133,46 @@ export default ({ students }) => {
|
||||
return res ? Number(res[0]) : Number.MAX_VALUE
|
||||
},
|
||||
])
|
||||
// Exact same code in GeneralTab REFACTOR (function in common/index.js file?)
|
||||
const studentToStudyrightStartMap = students.reduce((res, sn) => {
|
||||
const currentStudyright = sn.studyrights?.find(studyright =>
|
||||
studyright.studyright_elements.some(e => e.code === programmeCode)
|
||||
)
|
||||
if (currentStudyright?.studyrightid && currentStudyright.studyrightid.slice(-2) === '-2') {
|
||||
const bachelorId = currentStudyright.studyrightid.replace(/-2$/, '-1')
|
||||
const bacherlorStudyright = sn.studyrights.find(studyright => studyright.studyrightid === bachelorId)
|
||||
res[sn.studentNumber] = bacherlorStudyright?.startdate || null
|
||||
} else {
|
||||
res[sn.studentNumber] = currentStudyright?.startdate || null
|
||||
}
|
||||
return res
|
||||
}, {})
|
||||
|
||||
const studentToProgrammeStartMap = students.reduce((res, sn) => {
|
||||
const targetStudyright = flatten(
|
||||
sn.studyrights.reduce((acc, curr) => {
|
||||
acc.push(curr.studyright_elements)
|
||||
return acc
|
||||
}, [])
|
||||
).filter(e => e.code === programmeCode)
|
||||
// clean up odd bachelor start dates, (givendate)
|
||||
res[sn.studentNumber] = new Date(
|
||||
Math.max(new Date(targetStudyright[0]?.startdate), new Date(studentToStudyrightStartMap[sn.studentNumber]))
|
||||
)
|
||||
return res
|
||||
}, {})
|
||||
|
||||
const worksheet = xlsx.utils.json_to_sheet(
|
||||
students.map(s => ({
|
||||
'last name': s.lastname,
|
||||
'given names': s.firstnames,
|
||||
'student number': s.studentNumber,
|
||||
'all credits': s.allCredits,
|
||||
'all credits': s.allCredits ? s.allCredits : s.credits,
|
||||
'hops credits': s.hopsCredits,
|
||||
'credits since start': s.credits,
|
||||
'credits since start': getStudentTotalCredits({
|
||||
...s,
|
||||
courses: s.courses.filter(c => new Date(c.date) >= studentToProgrammeStartMap[s.studentNumber]),
|
||||
}),
|
||||
'phone number': s.phoneNumber,
|
||||
email: s.email,
|
||||
'transferred from': s.transferredStudyright ? transferFrom(s) : '',
|
||||
@@ -152,6 +184,7 @@ export default ({ students }) => {
|
||||
'start of studyright': reformatDate(getStartOfStudyright(s.studyrights), 'YYYY-MM-DD'),
|
||||
'started in programme': reformatDate(getStartedInProgramme(s.studyrights), 'YYYY-MM-DD'),
|
||||
'admission type': parseInt(queryYear, 10 >= 2020) ? getAdmissionType(s.studyrights) : undefined,
|
||||
bachelor: s.option ? getTextIn(s.option.name, language) : '',
|
||||
'updated at': reformatDate(s.updatedAt, 'YYYY-MM-DD hh:mm:ss'),
|
||||
'mandatory total passed': totalMandatoryPassed(s.studentNumber, codes),
|
||||
...sortedMandatory.reduce((acc, m) => {
|
||||
|
||||
@@ -101,7 +101,6 @@ const PopulationStatistics = () => {
|
||||
|
||||
const title =
|
||||
location.search === '' ? 'Class statistics' : `${getTextIn(programmeName, language)} ${getYearText(query?.year)}`
|
||||
|
||||
return (
|
||||
<FilterView
|
||||
name="PopulationStatistics"
|
||||
@@ -138,7 +137,7 @@ const PopulationStatistics = () => {
|
||||
queryIsSet={queryIsSet}
|
||||
query={query}
|
||||
isLoading={isLoading}
|
||||
dataExport={<DataExport students={filteredStudents} />}
|
||||
dataExport={<DataExport students={filteredStudents} programmeCode={query?.studyRights?.programme} />}
|
||||
allStudents={samples}
|
||||
selectedStudentsByYear={selectedStudentsByYear}
|
||||
filteredStudents={filteredStudents}
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Tab, Item, Icon, Message } from 'semantic-ui-react'
|
||||
import SortableTable from 'components/SortableTable'
|
||||
import sendEvent from 'common/sendEvent'
|
||||
import { getTextIn } from 'common'
|
||||
import { keyBy } from 'lodash'
|
||||
|
||||
const sendAnalytics = sendEvent.populationStudents
|
||||
const ProgressTable = ({ criteria, students, months, programme }) => {
|
||||
const mandatoryCourses = useSelector(state => state?.populationMandatoryCourses?.data)
|
||||
const namesVisible = useSelector(state => state?.settings?.namesVisible)
|
||||
const findRowContent = (s, courseCode, year, label) => {
|
||||
if (courseCode.includes('Credits'))
|
||||
return s.criteriaProgress[year] && s.criteriaProgress[year].credits ? (
|
||||
<Icon fitted name="check" title="Checked" color="green" />
|
||||
) : null
|
||||
const courses = s.courses.filter(
|
||||
course => course.course_code === courseCode || criteria.allCourses[label][courseCode].includes(course.course_code)
|
||||
)
|
||||
if (courses && courses.some(course => course.credittypecode === 9))
|
||||
return <Icon name="clipboard check" title="Credit transfer" color="green" />
|
||||
if (courses && courses.some(course => course.passed))
|
||||
return <Icon fitted name="check" title="Passed" color="green" />
|
||||
if (courses && courses.some(course => course.passed === false))
|
||||
return <Icon fitted name="times" title="Failed" color="red" />
|
||||
// if (failedManyTimes) return <Icon fitted name="times" color="red" />
|
||||
if (s.enrollments && s.enrollments.map(course => course.course_code).includes(courseCode))
|
||||
return <Icon fitted name="minus" title="Unfinished" color="grey" />
|
||||
return null
|
||||
}
|
||||
const findCsvText = (s, courseCode, year, label) => {
|
||||
if (courseCode.includes('Credits'))
|
||||
return s.criteriaProgress[year] && s.criteriaProgress[year].credits ? 'Passed' : ''
|
||||
const courses = s.courses.filter(
|
||||
course =>
|
||||
course.course_code === courseCode || criteria?.allCourses[label][courseCode].includes(course.course_code)
|
||||
)
|
||||
if (courses && courses.some(course => course.credittypecode === 9)) return 'Credit transfer'
|
||||
if (courses && courses.some(course => course.passed)) return 'Passed'
|
||||
if (courses && courses.some(course => course.passed === false)) return 'Failed'
|
||||
// if (failedManyTimes) return <Icon fitted name="times" color="red" />
|
||||
if (s.enrollments && s.enrollments.map(course => course.course_code).includes(courseCode)) return 'Unfinished'
|
||||
return ''
|
||||
}
|
||||
const columns = useMemo(() => {
|
||||
const studentColumns = []
|
||||
if (namesVisible) {
|
||||
studentColumns.push(
|
||||
{
|
||||
key: 'lastname_visible',
|
||||
title: 'Last name',
|
||||
getRowVal: s => s.lastname,
|
||||
cellProps: { title: 'last name' },
|
||||
export: false,
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'firstname_visible',
|
||||
title: 'Given names',
|
||||
getRowVal: s => s.firstnames,
|
||||
cellProps: { title: 'first names' },
|
||||
export: false,
|
||||
child: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
studentColumns.push({
|
||||
key: 'studentnumber-parent',
|
||||
title: 'Student Number',
|
||||
cellProps: { title: 'student number', className: 'studentNumber' },
|
||||
getRowVal: s => (s.total ? '*' : s.studentNumber),
|
||||
getRowContent: s => (
|
||||
<div>
|
||||
<span>{s.studentNumber}</span>
|
||||
<Item
|
||||
as={Link}
|
||||
to={`/students/${s.studentNumber}`}
|
||||
onClick={() => {
|
||||
sendAnalytics('Student details button clicked', 'Student progress table')
|
||||
}}
|
||||
>
|
||||
<Icon style={{ borderLeft: '1em' }} name="user outline" />
|
||||
</Item>
|
||||
</div>
|
||||
),
|
||||
child: true,
|
||||
})
|
||||
|
||||
const courses = keyBy(mandatoryCourses, 'code')
|
||||
const labelCriteria = Object.keys(criteria.courses).reduce((acc, year) => {
|
||||
acc[year] = [
|
||||
{ code: `Credits: ${criteria.credits[year]}`, name: '' },
|
||||
...[...criteria.courses[year]]
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map(courseCode => ({
|
||||
code: courseCode,
|
||||
name: courses[courseCode] ? courses[courseCode].name : '',
|
||||
})),
|
||||
{ code: `Total`, name: '' },
|
||||
]
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const criteriaHeaders = [
|
||||
{ title: months < 12 ? 'Academic Year 1 (in progress)' : 'Academic Year 1', year: 'year1', label: 'yearOne' },
|
||||
{ title: months < 24 ? 'Academic Year 2 (in progress)' : 'Academic Year 2', year: 'year2', label: 'yearTwo' },
|
||||
{ title: months < 36 ? 'Academic Year 3 (in progress)' : 'Academic Year 3', year: 'year3', label: 'yearThree' },
|
||||
]
|
||||
|
||||
const getEnrollmentValue = enrollmentObj => {
|
||||
if (enrollmentObj?.enrollmenttype === 1) return 'Present'
|
||||
if (enrollmentObj?.enrollmenttype === 2) return 'Absent'
|
||||
return 'Inactive'
|
||||
}
|
||||
const columns = []
|
||||
const createContent = (labels, year, label, enrollStatusIdx) => {
|
||||
return labels.map(m => ({
|
||||
key: `${year}-${m.code}`,
|
||||
title: (
|
||||
<div
|
||||
key={`${year}-${m.code}-${getTextIn(m.name)}`}
|
||||
style={{ maxWidth: '8em', whiteSpace: 'normal', overflow: 'hidden', width: 'max-content' }}
|
||||
>
|
||||
<div key={`${m.code}-${year}`}>{m.code}</div>
|
||||
<div key={`${getTextIn(m.name)}`} style={{ color: 'gray', fontWeight: 'normal' }}>
|
||||
{getTextIn(m.name)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
textTitle:
|
||||
m.name === ''
|
||||
? `${m.code} ${enrollStatusIdx === 0 ? enrollStatusIdx + 1 : enrollStatusIdx}`
|
||||
: `${m.code}-${getTextIn(m.name)}`,
|
||||
headerProps: { title: `${m.code}, ${year}` },
|
||||
cellProps: {
|
||||
style: { verticalAlign: 'middle', textAlign: 'center' },
|
||||
},
|
||||
getRowVal: s => {
|
||||
if (m.code.includes('Total')) return s.criteriaProgress[year] ? s.criteriaProgress[year].totalSatisfied : 0
|
||||
return findCsvText(s, m.code, year, label)
|
||||
},
|
||||
getRowExportVal: s => {
|
||||
if (m.code.includes('Total')) return s.criteriaProgress[year] ? s.criteriaProgress[year].totalSatisfied : 0
|
||||
return findCsvText(s, m.code, year, label)
|
||||
},
|
||||
getRowContent: s => {
|
||||
if (m.code.includes('Total')) return s.criteriaProgress[year] ? s.criteriaProgress[year].totalSatisfied : 0
|
||||
return findRowContent(s, m.code, year, label)
|
||||
},
|
||||
child: true,
|
||||
}))
|
||||
}
|
||||
|
||||
columns.push(
|
||||
{
|
||||
key: 'general',
|
||||
title: <b>Labels:</b>,
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: studentColumns,
|
||||
},
|
||||
{
|
||||
key: criteriaHeaders[0].title,
|
||||
title: (
|
||||
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
|
||||
<div>{criteriaHeaders[0].title}</div>
|
||||
</div>
|
||||
),
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: createContent(
|
||||
labelCriteria[criteriaHeaders[0].label],
|
||||
criteriaHeaders[0].year,
|
||||
criteriaHeaders[0].label,
|
||||
0
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'hidden-1',
|
||||
textTitle: null,
|
||||
mergeHeader: true,
|
||||
parent: true,
|
||||
children: [
|
||||
{
|
||||
key: `absence-first-Fall`,
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: `Enrollment Status FALL 1`,
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: s => getEnrollmentValue(s.semesterenrollments[0]),
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: `absence-first-Spring`,
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: `Enrollment Status SPRING 1`,
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: s => getEnrollmentValue(s.semesterenrollments[1]),
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'second_academic',
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: ' ',
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: () => ' ',
|
||||
child: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
if (months > 12) {
|
||||
columns.push(
|
||||
{
|
||||
key: criteriaHeaders[1].title,
|
||||
title: (
|
||||
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
|
||||
<div>{criteriaHeaders[1].title}</div>
|
||||
</div>
|
||||
),
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: createContent(
|
||||
labelCriteria[criteriaHeaders[1].label],
|
||||
criteriaHeaders[1].year,
|
||||
criteriaHeaders[1].label,
|
||||
2
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'hidden-2',
|
||||
textTitle: null,
|
||||
mergeHeader: true,
|
||||
parent: true,
|
||||
children: [
|
||||
{
|
||||
key: `absence-second-Fall`,
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: `Enrollment Status FALL 2`,
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: s => getEnrollmentValue(s.semesterenrollments[2]),
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: `absence-second-Spring`,
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: `Enrollment Status SPRING 2`,
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: s => getEnrollmentValue(s.semesterenrollments[3]),
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'third_academic',
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: ' ',
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: () => ' ',
|
||||
child: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
if (months > 24) {
|
||||
columns.push(
|
||||
{
|
||||
key: criteriaHeaders[2].title,
|
||||
title: (
|
||||
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
|
||||
<div>{criteriaHeaders[2].title}</div>
|
||||
</div>
|
||||
),
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: createContent(
|
||||
labelCriteria[criteriaHeaders[2].label],
|
||||
criteriaHeaders[2].year,
|
||||
criteriaHeaders[2].label,
|
||||
4
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'hidden-3',
|
||||
textTitle: null,
|
||||
mergeHeader: true,
|
||||
parent: true,
|
||||
children: [
|
||||
{
|
||||
key: `absence-third-Fall`,
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: `Enrollment Status FALL 3`,
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: s => getEnrollmentValue(s.semesterenrollments[4]),
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: `absence-third-Spring`,
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: `Enrollment Status SPRING 3`,
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: s => getEnrollmentValue(s.semesterenrollments[5]),
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'empty',
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: ' ',
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: () => ' ',
|
||||
child: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
const columnsToHide = [
|
||||
{
|
||||
key: 'hidden-phoneNumber',
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: `Phone number`,
|
||||
headerProps: { style: { display: 'none' } },
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: s => s.phoneNumber,
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'hidden-email',
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: 'Email',
|
||||
headerProps: { style: { display: 'none' } },
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: s => s.email,
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'hidden-secondary-email',
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
textTitle: 'Secondary Email',
|
||||
headerProps: { style: { display: 'none' } },
|
||||
cellProps: { style: { display: 'none' } },
|
||||
getRowVal: s => s.secondaryEmail,
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'lastname-hidden',
|
||||
textTitle: 'Last Name',
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
headerProps: { style: { display: 'none' } },
|
||||
getRowVal: s => s.lastname,
|
||||
cellProps: { style: { display: 'none' } },
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'firstname-hidden',
|
||||
textTitle: 'First Names',
|
||||
export: true,
|
||||
forceToolsMode: 'none',
|
||||
headerProps: { style: { display: 'none' } },
|
||||
getRowVal: s => s.firstnames,
|
||||
cellProps: { style: { display: 'none' } },
|
||||
child: true,
|
||||
},
|
||||
]
|
||||
|
||||
columns.push({
|
||||
key: 'hiddenFiles',
|
||||
title: '',
|
||||
mergeHeader: true,
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: columnsToHide,
|
||||
})
|
||||
|
||||
return columns
|
||||
}, [criteria, students, mandatoryCourses, getTextIn, namesVisible])
|
||||
|
||||
const isCriteriaSet =
|
||||
criteria && Object.keys(criteria.courses).some(yearCourses => criteria.courses[yearCourses].length > 0)
|
||||
const data = useMemo(() => {
|
||||
return students
|
||||
}, [students])
|
||||
return (
|
||||
<Tab.Pane>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ maxHeight: '80vh', width: '100%' }}>
|
||||
<h5>
|
||||
Criteria can be changed{' '}
|
||||
<Link
|
||||
to={`/study-programme/${programme}?p_m_tab=0&p_tab=3`}
|
||||
onClick={() => {
|
||||
sendAnalytics('No criteria defined button clicked', 'Degree courses tab')
|
||||
}}
|
||||
>
|
||||
here.
|
||||
</Link>{' '}
|
||||
Please refresh page after changes.
|
||||
</h5>
|
||||
<Message style={{ maxWidth: '800px', fontSize: '16px' }}>
|
||||
<p>
|
||||
<Icon fitted name="check" color="green" />: Student has passed the course.{' '}
|
||||
<Icon fitted name="clipboard check" color="green" />: Student has credit transfer for the course. <br />
|
||||
<Icon fitted name="times" color="red" />: Student has failed the course.{' '}
|
||||
<Icon fitted name="minus" color="grey" />: Student has enrolled, but has not received any grade from the
|
||||
course.
|
||||
</p>
|
||||
</Message>
|
||||
{isCriteriaSet ? (
|
||||
<SortableTable
|
||||
style={{ height: '80vh' }}
|
||||
tableId="progress-of-population-students"
|
||||
title={`Progress of population's students after predefined criteria`}
|
||||
getRowKey={s => s.studentNumber}
|
||||
tableProps={{
|
||||
collapsing: true,
|
||||
basic: true,
|
||||
compact: 'very',
|
||||
padded: false,
|
||||
celled: true,
|
||||
}}
|
||||
columns={columns}
|
||||
data={data}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<h3>There is no criteria set for this programme.</h3>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Pane>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProgressTable
|
||||
@@ -19,6 +19,7 @@ import InfoBox from '../Info/InfoBox'
|
||||
import CheckStudentList from './CheckStudentList'
|
||||
import TagPopulation from '../TagPopulation'
|
||||
import TagList from '../TagList'
|
||||
import ProgressTable from './ProgressTable'
|
||||
import './populationStudents.css'
|
||||
import GeneralTab from './StudentTable/GeneralTab'
|
||||
import sendEvent, { ANALYTICS_CATEGORIES } from '../../common/sendEvent'
|
||||
@@ -337,6 +338,8 @@ const Panes = ({
|
||||
queryStudyrights,
|
||||
from,
|
||||
to,
|
||||
criteria,
|
||||
months,
|
||||
}) => {
|
||||
const { handleTabChange } = useTabChangeAnalytics(
|
||||
ANALYTICS_CATEGORIES.populationStudents,
|
||||
@@ -383,7 +386,7 @@ const Panes = ({
|
||||
<h3>
|
||||
No tags defined. You can define them{' '}
|
||||
<Link
|
||||
to={`/study-programme/${queryStudyrights[0]}?p_m_tab=0&p_tab=6`}
|
||||
to={`/study-programme/${queryStudyrights[0]}?p_m_tab=0&p_tab=4`}
|
||||
onClick={() => {
|
||||
sendAnalytics('No tags defined button clicked', 'Tags tab')
|
||||
}}
|
||||
@@ -408,6 +411,17 @@ const Panes = ({
|
||||
</Tab.Pane>
|
||||
),
|
||||
},
|
||||
{
|
||||
menuItem: 'Progress',
|
||||
render: () => (
|
||||
<ProgressTable
|
||||
students={filteredStudents}
|
||||
criteria={criteria}
|
||||
months={months}
|
||||
programme={queryStudyrights[0]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const panes = panesAvailable.filter(pane => visiblePanes.includes(pane.menuItem))
|
||||
@@ -441,6 +455,7 @@ const PopulationStudents = ({
|
||||
studyGuidanceGroup,
|
||||
from,
|
||||
to,
|
||||
criteria,
|
||||
}) => {
|
||||
const [state, setState] = useState({})
|
||||
const studentRef = useRef()
|
||||
@@ -449,6 +464,7 @@ const PopulationStudents = ({
|
||||
const { data: tags } = useSelector(({ tags }) => tags)
|
||||
const { query } = useSelector(({ populations }) => populations)
|
||||
const queryStudyrights = query ? Object.values(query.studyRights) : []
|
||||
const months = query ? query.months : 0
|
||||
const prevShowList = usePrevious(showList)
|
||||
const { isAdmin } = useGetAuthorizedUserQuery()
|
||||
const admin = isAdmin
|
||||
@@ -472,7 +488,6 @@ const PopulationStudents = ({
|
||||
}, [prevShowList])
|
||||
|
||||
if (filteredStudents.length === 0) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<span style={{ marginRight: '0.5rem' }} ref={studentRef}>
|
||||
@@ -487,10 +502,12 @@ const PopulationStudents = ({
|
||||
variant={variant}
|
||||
studentToTargetCourseDateMap={studentToTargetCourseDateMap}
|
||||
tags={tags}
|
||||
criteria={criteria}
|
||||
studyGuidanceGroup={studyGuidanceGroup}
|
||||
coursecode={coursecode}
|
||||
from={from}
|
||||
to={to}
|
||||
months={months}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@@ -498,14 +515,16 @@ const PopulationStudents = ({
|
||||
|
||||
const PopulationStudentsContainer = ({ ...props }) => {
|
||||
const { variant } = props
|
||||
|
||||
if (!['population', 'customPopulation', 'coursePopulation', 'studyGuidanceGroupPopulation'].includes(variant)) {
|
||||
throw new Error(`${variant} is not a proper variant!`)
|
||||
}
|
||||
|
||||
const contentByVariant = {
|
||||
population: {
|
||||
panesToInclude: ['General', 'Courses', 'Tags'],
|
||||
panesToInclude:
|
||||
props.year === 'All' || (props.programmeCode && !props.programmeCode.includes('KH'))
|
||||
? ['General', 'Courses', 'Tags']
|
||||
: ['General', 'Courses', 'Tags', 'Progress'],
|
||||
infotoolTipContent: infotoolTips.PopulationStatistics.StudentsClass,
|
||||
},
|
||||
coursePopulation: {
|
||||
|
||||
@@ -41,6 +41,7 @@ const Trends = React.lazy(() => retry(() => import('../Trends')))
|
||||
const StudyGuidanceGroups = React.lazy(() => retry(() => import('../StudyGuidanceGroups')))
|
||||
const FacultyStatistics = React.lazy(() => retry(() => import('../FacultyStatistics')))
|
||||
const CustomOpenUniPopulations = React.lazy(() => retry(() => import('../CustomOpenUniPopulation')))
|
||||
const EvaluationOverview = React.lazy(() => retry(() => import('../EvaluationOverview')))
|
||||
|
||||
const routes = {
|
||||
students: '/students/:studentNumber?',
|
||||
@@ -55,6 +56,7 @@ const routes = {
|
||||
studyGuidanceGroups: '/studyguidancegroups/:groupid?',
|
||||
faculties: '/faculties/:facultyCode?',
|
||||
customOpenUniPopulation: '/openunipopulation',
|
||||
evaluationOverview: '/evaluationoverview/:level?/:id?',
|
||||
}
|
||||
|
||||
const Routes = () => (
|
||||
@@ -113,6 +115,13 @@ const Routes = () => (
|
||||
path={routes.studyGuidanceGroups}
|
||||
component={StudyGuidanceGroups}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
requiredRoles={['admin']}
|
||||
requireUserHasRights
|
||||
exact
|
||||
path={routes.evaluationOverview}
|
||||
component={EvaluationOverview}
|
||||
/>
|
||||
<Redirect from="/cool-data-science" to="/trends" />
|
||||
<Redirect to="/" />
|
||||
</Switch>
|
||||
|
||||
@@ -446,7 +446,7 @@ const ColumnHeaderContent = React.memo(({ column, colSpan, state, dispatch, rowS
|
||||
}}
|
||||
>
|
||||
<SizeMeasurer onSizeChange={onTitleSizeChange} style={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||
<p style={{ whiteSpace: 'pre' }}>{column.title}</p>
|
||||
{typeof column.title === 'string' ? <p style={{ whiteSpace: 'pre' }}>{column.title}</p> : column.title}
|
||||
{column.helpText && !forcedTitleWidth && (
|
||||
<Icon
|
||||
name="question circle outline"
|
||||
|
||||
+3
-3
@@ -27,11 +27,11 @@ const CourseParticipationTable = ({ student, language, clearCourseStats, studyri
|
||||
if (isStudyModuleCredit) {
|
||||
icon = <Icon name="certificate" color="purple" />
|
||||
} else if (c.credittypecode === 9) {
|
||||
icon = <Icon name="file alternative outline" title="Credit transfer" color="green" />
|
||||
icon = <Icon name="clipboard check" title="Credit transfer" color="green" />
|
||||
} else if (passed) {
|
||||
icon = <Icon name="check circle outline" color="green" />
|
||||
icon = <Icon name="check" color="green" />
|
||||
} else {
|
||||
icon = <Icon name="circle outline" color="red" />
|
||||
icon = <Icon name="times" color="red" />
|
||||
}
|
||||
|
||||
if (!courseRowsByAcademicYear[`${new Date(date).getFullYear()}-${new Date(date).getFullYear() + 1}`]) {
|
||||
|
||||
@@ -83,6 +83,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
|
||||
|
||||
const setExclusionButton = course => (
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={() => {
|
||||
setExclusion(studyProgramme, [course.code])
|
||||
sendAnalytics('Set hidden button pressed', studyProgramme, course.name.fi)
|
||||
@@ -93,6 +94,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
|
||||
)
|
||||
const deleteButton = course => (
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={() => {
|
||||
removeExclusion(studyProgramme, [course.visible.id])
|
||||
sendAnalytics('Set visible button pressed', studyProgramme, course.name.fi)
|
||||
@@ -104,6 +106,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
|
||||
|
||||
const setCriteriaButton = (course, criterionYear) => (
|
||||
<Button
|
||||
color="green"
|
||||
onClick={() => {
|
||||
const year = getYear(criterionYear)
|
||||
const courses = criteria.courses ? [...criteria.courses[year], course.code] : [course.code]
|
||||
@@ -115,6 +118,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
|
||||
)
|
||||
const deleteCriteriaButton = (course, criterionYear) => (
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
const year = getYear(criterionYear)
|
||||
const filteredCourses = criteria?.courses[year]?.filter(courseCode => courseCode !== course.code)
|
||||
@@ -177,6 +181,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
|
||||
|
||||
const showAllButton = module => (
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={() => {
|
||||
deleteAll(module)
|
||||
sendAnalytics('ShowAllButton pressed', module)
|
||||
@@ -188,6 +193,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
|
||||
|
||||
const hideAllButton = module => (
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={() => {
|
||||
excludeAll(module)
|
||||
sendAnalytics('HideAllButton pressed', module)
|
||||
@@ -236,9 +242,9 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
|
||||
<Message.Header>Change visibility of degree courses and select criteria for academic years</Message.Header>
|
||||
<p>
|
||||
Here you can change visibility of degree courses as and set course and credits criteria. Each academic
|
||||
year have their own course and credits criteria. The progress of the students by these criteria will be
|
||||
shown in class statistics view. These statistics will be available at the latest at Friday on 10.2..
|
||||
year have their own course and credits criteria.
|
||||
</p>
|
||||
<p>The progress of the students by these criteria will be shown in class statistics view.</p>
|
||||
</Message>
|
||||
<Container>
|
||||
<h5>Credit criteria</h5>
|
||||
@@ -263,7 +269,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
|
||||
onChange={e => setCreditsLimit3(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button content="Save credit changes" onClick={setCreditsLimitCriteria} />
|
||||
<Form.Button color="green" content="Save credit changes" onClick={setCreditsLimitCriteria} />
|
||||
</Form>
|
||||
</Container>
|
||||
</>
|
||||
|
||||
@@ -43,7 +43,7 @@ const TagModal = ({ tags, studytrack, createMultipleStudentTag, pending, success
|
||||
return (
|
||||
<Modal
|
||||
trigger={
|
||||
<Button size="small" disabled={!tags.length} onClick={() => setModalOpen(true)}>
|
||||
<Button size="small" disabled={!tags.length} color="blue" onClick={() => setModalOpen(true)}>
|
||||
Add tags to students
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -53,7 +53,11 @@ const Tags = ({ createTag, deleteTag, getTagsByStudytrack, tags, studyprogramme
|
||||
setTagname(target.value)
|
||||
}
|
||||
|
||||
const deleteButton = tag => <Button onClick={() => setConfirm(tag)}>Delete</Button>
|
||||
const deleteButton = tag => (
|
||||
<Button negative onClick={() => setConfirm(tag)}>
|
||||
Delete
|
||||
</Button>
|
||||
)
|
||||
|
||||
const populationUrl = tag => {
|
||||
if (!tag.year) {
|
||||
@@ -160,7 +164,11 @@ const Tags = ({ createTag, deleteTag, getTagsByStudytrack, tags, studyprogramme
|
||||
<label>Personal tag</label>
|
||||
<Form.Checkbox toggle checked={personal} onClick={() => setPersonal(!personal)} />
|
||||
</Form.Field>
|
||||
<Button disabled={!tagname.trim() || tags.some(t => t.tagname === tagname.trim())} onClick={handleSubmit}>
|
||||
<Button
|
||||
color="green"
|
||||
disabled={!tagname.trim() || tags.some(t => t.tagname === tagname.trim())}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{' '}
|
||||
Create new tag{' '}
|
||||
</Button>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ const CourseTabs = ({ data, showStudents, handleShowStudentsChange }) => {
|
||||
]
|
||||
|
||||
const panes = paneTypes.map(({ icon, label }) => ({
|
||||
menuItem: { icon, content: label },
|
||||
menuItem: { icon, content: label, key: label },
|
||||
render: () => (
|
||||
<Tab.Pane>
|
||||
<Toggle
|
||||
|
||||
+114
-62
@@ -15,83 +15,135 @@ const getColumns = (language, showStudents) => {
|
||||
if (showStudents) {
|
||||
columns = [
|
||||
{
|
||||
key: 'code',
|
||||
mergeHeader: true,
|
||||
merge: true,
|
||||
key: 'Course',
|
||||
title: 'Course info',
|
||||
parent: true,
|
||||
children: [
|
||||
{
|
||||
key: 'course_code',
|
||||
title: 'Code ',
|
||||
export: true,
|
||||
getRowVal: course => course.code,
|
||||
getRowContent: course => course.code,
|
||||
key: 'code',
|
||||
mergeHeader: true,
|
||||
merge: true,
|
||||
children: [
|
||||
{
|
||||
key: 'course_code',
|
||||
title: 'Code ',
|
||||
export: true,
|
||||
getRowVal: course => course.code,
|
||||
getRowContent: course => course.code,
|
||||
},
|
||||
{
|
||||
key: 'go-to-course',
|
||||
export: false,
|
||||
getRowContent: course => (
|
||||
<Item
|
||||
as={Link}
|
||||
to={`/coursestatistics?courseCodes=["${encodeURIComponent(
|
||||
course.code
|
||||
)}"]&separate=false&unifyOpenUniCourses=false`}
|
||||
>
|
||||
<Icon
|
||||
name="level up alternate"
|
||||
onClick={() => sendAnalytics('Courses of Population course stats button clicked', course.code)}
|
||||
/>
|
||||
</Item>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'go-to-course',
|
||||
export: false,
|
||||
getRowContent: course => (
|
||||
<Item
|
||||
as={Link}
|
||||
to={`/coursestatistics?courseCodes=["${encodeURIComponent(
|
||||
course.code
|
||||
)}"]&separate=false&unifyOpenUniCourses=false`}
|
||||
>
|
||||
<Icon
|
||||
name="level up alternate"
|
||||
onClick={() => sendAnalytics('Courses of Population course stats button clicked', course.code)}
|
||||
/>
|
||||
</Item>
|
||||
),
|
||||
key: 'name',
|
||||
title: 'Name',
|
||||
getRowVal: course => course.name[language],
|
||||
getRowContent: course => course.name[language],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: 'Name',
|
||||
getRowVal: course => course.name[language],
|
||||
getRowContent: course => course.name[language],
|
||||
},
|
||||
{
|
||||
key: 'total',
|
||||
title: 'All students',
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalAllStudents,
|
||||
getRowContent: course => course.totalAllStudents,
|
||||
title: '',
|
||||
parent: true,
|
||||
children: [
|
||||
{
|
||||
key: 'total',
|
||||
title: 'Total',
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalAllStudents,
|
||||
getRowContent: course => course.totalAllStudents,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'totalOwnProgramme',
|
||||
title: 'Major students',
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalProgrammeStudents,
|
||||
getRowContent: course => course.totalProgrammeStudents,
|
||||
key: 'breakdown',
|
||||
title: 'Breakdown of Total',
|
||||
parent: true,
|
||||
children: [
|
||||
{
|
||||
key: 'passed',
|
||||
title: 'Passed',
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalAllPassed,
|
||||
getRowContent: course => course.totalAllPassed,
|
||||
},
|
||||
{
|
||||
key: 'not-completed',
|
||||
title: 'Not Completed',
|
||||
helpText: toolTips.NotCompleted,
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalAllNotCompleted,
|
||||
getRowContent: course => course.totalAllNotCompleted,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'totalOtherProgramme',
|
||||
title: 'Non-major students',
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalOtherProgrammeStudents,
|
||||
getRowContent: course => course.totalOtherProgrammeStudents,
|
||||
key: 'breakdown-passed',
|
||||
title: 'Breakdown Statistics of Passed Students',
|
||||
parent: true,
|
||||
children: [
|
||||
{
|
||||
key: 'totalOwnProgramme',
|
||||
title: 'Major students',
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalProgrammeStudents,
|
||||
getRowContent: course => course.totalProgrammeStudents,
|
||||
},
|
||||
{
|
||||
key: 'totalOtherProgramme',
|
||||
title: 'Non-major students',
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalOtherProgrammeStudents,
|
||||
getRowContent: course => course.totalOtherProgrammeStudents,
|
||||
},
|
||||
{
|
||||
key: 'totalWithoutStudyright',
|
||||
title: 'Non-degree students',
|
||||
helpText: toolTips.NonDegree,
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalWithoutStudyrightStudents,
|
||||
getRowContent: course => course.totalWithoutStudyrightStudents,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'totalWithoutStudyright',
|
||||
title: 'Non-degree students',
|
||||
helpText: toolTips.NonDegree,
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalWithoutStudyrightStudents,
|
||||
getRowContent: course => course.totalWithoutStudyrightStudents,
|
||||
},
|
||||
{
|
||||
key: 'transfer',
|
||||
title: 'Transferred students',
|
||||
helpText: toolTips.TransferredCourses,
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalTransferStudents,
|
||||
getRowContent: course => course.totalTransferStudents,
|
||||
key: 'exluded',
|
||||
title: 'Not Included to Passed',
|
||||
parent: true,
|
||||
children: [
|
||||
{
|
||||
key: 'transfer',
|
||||
title: 'Transferred students',
|
||||
helpText: toolTips.TransferredCourses,
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalTransferStudents,
|
||||
getRowContent: course => course.totalTransferStudents,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
} else {
|
||||
|
||||
@@ -56,6 +56,8 @@ const ProgrammeCoursesOverview = ({ studyProgramme, academicYear, setAcademicYea
|
||||
(acc, curr) => {
|
||||
if (yearRange.includes(Number(curr[0]))) {
|
||||
acc.totalAllStudents += curr[1].totalAllStudents
|
||||
acc.totalAllPassed += curr[1].totalPassed
|
||||
acc.totalAllNotCompleted += curr[1].totalNotCompleted
|
||||
acc.totalAllCredits += curr[1].totalAllCredits
|
||||
acc.totalProgrammeStudents += curr[1].totalProgrammeStudents
|
||||
acc.totalProgrammeCredits += curr[1].totalProgrammeCredits
|
||||
@@ -71,6 +73,8 @@ const ProgrammeCoursesOverview = ({ studyProgramme, academicYear, setAcademicYea
|
||||
},
|
||||
{
|
||||
totalAllStudents: 0,
|
||||
totalAllPassed: 0,
|
||||
totalAllNotCompleted: 0,
|
||||
totalAllCredits: 0,
|
||||
totalProgrammeStudents: 0,
|
||||
totalProgrammeCredits: 0,
|
||||
|
||||
@@ -27,6 +27,10 @@ const studyprogrammeApi = RTKApi.injectEndpoints({
|
||||
getProgrammeCoursesStats: builder.query({
|
||||
query: ({ id, academicyear }) => `/v2/studyprogrammes/${id}/coursestats?academicyear=${academicyear}`,
|
||||
}),
|
||||
getEvaluationStats: builder.query({
|
||||
query: ({ id, yearType, specialGroups, graduated }) =>
|
||||
`/v2/studyprogrammes/${id}/evaluationstats?year_type=${yearType}&special_groups=${specialGroups}&graduated=${graduated}`,
|
||||
}),
|
||||
}),
|
||||
overrideExisting: false,
|
||||
})
|
||||
@@ -39,4 +43,5 @@ export const {
|
||||
useUpdateBasicViewQuery,
|
||||
useUpdateStudytrackViewQuery,
|
||||
useGetProgrammeCoursesStatsQuery,
|
||||
useGetEvaluationStatsQuery,
|
||||
} = studyprogrammeApi
|
||||
|
||||
Reference in New Issue
Block a user