mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e4151bd14 | ||
|
|
939fd69e6d | ||
|
|
2786fbfc0f | ||
|
|
8ec998044b | ||
|
|
29c914069e | ||
|
|
24033598f9 | ||
|
|
94584e9faa | ||
|
|
953cf38ffd | ||
|
|
0b16c5eab2 | ||
|
|
58de142232 | ||
|
|
a88e0c0362 | ||
|
|
7cb5f86937 | ||
|
|
e45e4ec239 | ||
|
|
14308236c6 | ||
|
|
d4254cc60f | ||
|
|
4fcb3fcd07 |
@@ -153,7 +153,7 @@ describe('Population Statistics', () => {
|
||||
|
||||
it('Age filter works', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 20, 40, 49)
|
||||
testRangeFilter('Age-filter-card', 20, 40, 47)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -217,7 +217,7 @@ describe('Population Statistics', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
|
||||
getCard().cs('option-graduated-true').click()
|
||||
testRangeFilter('Age-filter-card', 20, 30, 11)
|
||||
testRangeFilter('Age-filter-card', 20, 30, 1)
|
||||
getCard().cs('option-all').click()
|
||||
})
|
||||
})
|
||||
@@ -274,7 +274,7 @@ describe('Course Statistics', () => {
|
||||
|
||||
it('Age filter works', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 20, 40, 35)
|
||||
testRangeFilter('Age-filter-card', 20, 40, 33)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -61,10 +61,10 @@ describe('Studyprogramme overview', () => {
|
||||
// [Year, Total, Major credits, Non-major credits, Non-degree credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
[2021, 0, 0, 0, 0, 0],
|
||||
[2020, 4928, 548, 4213, 0, 167],
|
||||
[2019, 7737, 1644, 5744, 0, 349],
|
||||
[2018, 6730, 1820, 4633, 5, 272],
|
||||
[2017, 2900, 788, 1724, 0, 388],
|
||||
[2020, 5860, 5591, 102, 0, 167],
|
||||
[2019, 8696, 7737, 587, 23, 349],
|
||||
[2018, 7818, 6584, 857, 105, 272],
|
||||
[2017, 3085, 2492, 158, 47, 388],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
@@ -150,9 +150,9 @@ describe('Studyprogramme overview', () => {
|
||||
...years.map(year => [year, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0],
|
||||
['2020 - 2021', 0, 0, 0, 0],
|
||||
['2019 - 2020', 338.5, 0, 221, 110, 7.5],
|
||||
['2018 - 2019', 173, 0, 74, 99, 0],
|
||||
['2017 - 2018', 42, 0, 35, 7, 0],
|
||||
['2019 - 2020', 338.5, 177, 154, 0, 7.5],
|
||||
['2018 - 2019', 173, 0, 173, 0, 0],
|
||||
['2017 - 2018', 42, 0, 42, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
@@ -171,8 +171,8 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', 'Major students credits')
|
||||
.should('contain', 'Non-major students credits')
|
||||
.should('contain', 'Transferred credits')
|
||||
.should('contain', 788)
|
||||
.should('contain', 1820)
|
||||
.should('contain', 857)
|
||||
.should('contain', 587)
|
||||
|
||||
cy.get('[data-cy=Graph-GraduatedAndThesisWritersOfTheProgramme')
|
||||
.should('contain', 'Graduated students')
|
||||
|
||||
@@ -108,6 +108,11 @@ const creditsForCourses = async (codes, anonymizationSalt, unification) => {
|
||||
{
|
||||
model: Semester,
|
||||
attributes: ['semestercode', 'name', 'yearcode', 'yearname'],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.lte]: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
@@ -173,6 +178,11 @@ const enrollmentsForCourses = async (codes, anonymizationSalt, unification) => {
|
||||
{
|
||||
model: Semester,
|
||||
attributes: ['semestercode', 'name', 'yearcode', 'yearname'],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.lte]: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
@@ -187,7 +197,6 @@ const enrollmentsForCourses = async (codes, anonymizationSalt, unification) => {
|
||||
[Op.or]: [{ is_open }, { is_open: null }],
|
||||
},
|
||||
})
|
||||
|
||||
return enrollments.map(enrollment => parseEnrollment(enrollment, anonymizationSalt))
|
||||
}
|
||||
|
||||
|
||||
@@ -929,6 +929,7 @@ const findCourses = async (studentnumbers, beforeDate, courses = [], studentCoun
|
||||
ARRAY_AGG(JSON_BUILD_OBJECT(
|
||||
'grade', grade,
|
||||
'student_studentnumber', student_studentnumber,
|
||||
'attainment_date', attainment_date,
|
||||
'credittypecode', credittypecode,
|
||||
'course_code', course_code
|
||||
)) AS data
|
||||
|
||||
@@ -257,7 +257,7 @@ const followingStudyrights = async (since, programmes, studentnumbers) =>
|
||||
include: [
|
||||
{
|
||||
model: ElementDetail,
|
||||
attributes: ['name'],
|
||||
attributes: ['name', 'code', 'type'],
|
||||
},
|
||||
],
|
||||
attributes: ['code'],
|
||||
@@ -293,7 +293,7 @@ const previousStudyrights = async (programmes, studentnumbers) =>
|
||||
include: [
|
||||
{
|
||||
model: ElementDetail,
|
||||
attributes: ['name'],
|
||||
attributes: ['name', 'code', 'type'],
|
||||
},
|
||||
],
|
||||
attributes: ['code'],
|
||||
@@ -369,7 +369,33 @@ const getProgrammesStudyrights = async studyprogramme =>
|
||||
})
|
||||
).map(formatStudyright)
|
||||
|
||||
const getCreditsForStudyProgramme = async (provider, since) => {
|
||||
const getStudyRights = async students =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
|
||||
where: {
|
||||
studentStudentnumber: students,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: StudyrightElement,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
where: {
|
||||
type: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
).map(formatStudyright)
|
||||
|
||||
const getCreditsForStudyProgramme = async (codes, since) => {
|
||||
const res = await Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'student_studentnumber'],
|
||||
include: {
|
||||
@@ -378,13 +404,7 @@ const getCreditsForStudyProgramme = async (provider, since) => {
|
||||
required: true,
|
||||
where: {
|
||||
is_study_module: false,
|
||||
},
|
||||
include: {
|
||||
model: Organization,
|
||||
required: true,
|
||||
where: {
|
||||
code: provider,
|
||||
},
|
||||
code: codes,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
@@ -402,6 +422,28 @@ const getCreditsForStudyProgramme = async (provider, since) => {
|
||||
return res
|
||||
}
|
||||
|
||||
const getCourseCodesForStudyProgramme = async provider => {
|
||||
const coursesByProvider = await Course.findAll({
|
||||
attributes: ['code', 'substitutions'],
|
||||
where: {
|
||||
is_study_module: false,
|
||||
},
|
||||
include: {
|
||||
model: Organization,
|
||||
required: true,
|
||||
where: {
|
||||
code: provider,
|
||||
},
|
||||
},
|
||||
})
|
||||
const coursesWithOpenUniSubstitutions = coursesByProvider.map(({ code, substitutions }) => {
|
||||
if (!substitutions || !substitutions.length) return [code]
|
||||
const alternatives = [`AY-${code}`, `AY${code}`, `A-${code}`]
|
||||
return [code].concat(substitutions.filter(sub => alternatives.includes(sub)))
|
||||
})
|
||||
return coursesWithOpenUniSubstitutions.flat()
|
||||
}
|
||||
|
||||
const getAllProgrammeCourses = async providerCode => {
|
||||
const res = await Course.findAll({
|
||||
attributes: ['code', 'substitutions'],
|
||||
@@ -676,4 +718,6 @@ module.exports = {
|
||||
getStudentsWithoutStudyrightForProgrammeCourses,
|
||||
getOtherStudentsForProgrammeCourses,
|
||||
getAllProgrammeCourses,
|
||||
getStudyRights,
|
||||
getCourseCodesForStudyProgramme,
|
||||
}
|
||||
|
||||
@@ -5,16 +5,18 @@ const {
|
||||
getStatsBasis,
|
||||
defineYear,
|
||||
isMajorStudentCredit,
|
||||
isNonMajorCredit,
|
||||
getYearsArray,
|
||||
getStartDate,
|
||||
isSpecialGroupCredit,
|
||||
tableTitles,
|
||||
} = require('./studyprogrammeHelpers')
|
||||
const {
|
||||
getProgrammesStudyrights,
|
||||
getStudyRights,
|
||||
getCreditsForStudyProgramme,
|
||||
getTransferredCredits,
|
||||
allTransfers,
|
||||
getCourseCodesForStudyProgramme,
|
||||
} = require('./studyprogramme')
|
||||
|
||||
// Fetches all the credits for the studyprogramme and divides them into major-students and non-major students credits
|
||||
@@ -22,36 +24,38 @@ const {
|
||||
// If special groups are excluded, the transfer students are filtered away from the major-students as well
|
||||
const getRegularCreditStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
|
||||
const providercode = mapToProviders([studyprogramme])[0]
|
||||
let studyrights = await getProgrammesStudyrights(studyprogramme, since)
|
||||
const courses = await getCourseCodesForStudyProgramme(providercode)
|
||||
const credits = await getCreditsForStudyProgramme(courses, since)
|
||||
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
|
||||
|
||||
//let studyrights = await getProgrammesStudyrights(studyprogramme, since)
|
||||
let studyrights = await getStudyRights(students, since)
|
||||
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid)
|
||||
|
||||
if (!includeAllSpecials) {
|
||||
studyrights.filter(s => !transfers.includes(s.studyrightid))
|
||||
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
|
||||
}
|
||||
|
||||
const credits = await getCreditsForStudyProgramme(providercode, since)
|
||||
|
||||
let majors = getStatsBasis(years)
|
||||
let nonMajors = getStatsBasis(years)
|
||||
let nonDegree = getStatsBasis(years)
|
||||
|
||||
credits.forEach(({ student_studentnumber, attainment_date, credits }) => {
|
||||
const studyright = studyrights.find(studyright => studyright.studentnumber == student_studentnumber)
|
||||
const studentStudyrights = studyrights.filter(studyright => studyright.studentnumber === student_studentnumber)
|
||||
const attainmentYear = defineYear(attainment_date, isAcademicYear)
|
||||
|
||||
if (!includeAllSpecials && isSpecialGroupCredit(studyright, attainment_date, transfers)) {
|
||||
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainment_date, transfers)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isMajorStudentCredit(studyright, attainment_date)) {
|
||||
if (isMajorStudentCredit(studentStudyrights, attainment_date, studyprogramme)) {
|
||||
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
majors.tableStats[attainmentYear] += credits || 0
|
||||
} else if (!studyright) {
|
||||
nonDegree.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
nonDegree.tableStats[attainmentYear] += credits || 0
|
||||
} else {
|
||||
} else if (isNonMajorCredit(studentStudyrights, attainment_date, studyprogramme)) {
|
||||
nonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
nonMajors.tableStats[attainmentYear] += credits || 0
|
||||
} else {
|
||||
nonDegree.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
nonDegree.tableStats[attainmentYear] += credits || 0
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -33,6 +33,15 @@ const defineStartDate = (studyright_elements, studystartdate) => {
|
||||
return studystartdate
|
||||
}
|
||||
|
||||
const resolveStudyRightCode = studyright_elements => {
|
||||
if (!studyright_elements) return null
|
||||
const studyRightElement = studyright_elements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]
|
||||
if (studyRightElement) return studyRightElement.code
|
||||
return null
|
||||
}
|
||||
|
||||
const formatStudyright = studyright => {
|
||||
const {
|
||||
studyrightid,
|
||||
@@ -57,7 +66,7 @@ const formatStudyright = studyright => {
|
||||
prioritycode,
|
||||
extentcode,
|
||||
studentnumber: student.studentnumber,
|
||||
code: studyright_elements?.length ? studyright_elements[0].code : null,
|
||||
code: resolveStudyRightCode(studyright_elements),
|
||||
name:
|
||||
studyright_elements?.length && studyright_elements[0].element_detail && studyright_elements[0].element_detail.name
|
||||
? studyright_elements[0].element_detail.name
|
||||
@@ -119,21 +128,36 @@ const getStatsBasis = years => {
|
||||
}
|
||||
}
|
||||
|
||||
const isMajorStudentCredit = (studyright, attainment_date) =>
|
||||
studyright &&
|
||||
(studyright.prioritycode === 1 || studyright.prioritycode === 30) && // Is studyright state = MAIN or state = GRADUATED
|
||||
studyright.studystartdate && // The student has started studying in the programme
|
||||
studyright.studystartdate <= attainment_date && // Has the credit been attained after studying in the programme started
|
||||
studyright.enddate >= attainment_date // Has the credit been attained before the studyright ended
|
||||
const isMajorStudentCredit = (studyrights, attainment_date, code) =>
|
||||
studyrights.some(studyright => {
|
||||
if (!studyright) return false
|
||||
if (studyright.code !== code) return false
|
||||
if (!studyright.graduated) return new Date(attainment_date) >= new Date(studyright.studystartdate)
|
||||
return (
|
||||
new Date(attainment_date) >= new Date(studyright.studystartdate) &&
|
||||
new Date(attainment_date) <= new Date(studyright.enddate)
|
||||
)
|
||||
})
|
||||
|
||||
const isSpecialGroupCredit = (studyright, attainment_date, transfers) => {
|
||||
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 ([7, 9, 16, 34, 33, 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 isNonMajorCredit = (studyrights, attainment_date) =>
|
||||
studyrights.some(studyright => {
|
||||
if (!studyright) return false
|
||||
if (!studyright.graduated) return new Date(attainment_date) >= new Date(studyright.studystartdate)
|
||||
return (
|
||||
new Date(attainment_date) >= new Date(studyright.studystartdate) &&
|
||||
new Date(attainment_date) <= new Date(studyright.enddate)
|
||||
)
|
||||
})
|
||||
|
||||
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 ([7, 9, 16, 34, 33, 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
|
||||
@@ -410,4 +434,5 @@ module.exports = {
|
||||
getCreditThresholds,
|
||||
tableTitles,
|
||||
getCreditProgressTableTitles,
|
||||
isNonMajorCredit,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import _ from 'lodash/fp'
|
||||
import boost from 'highcharts/modules/boost'
|
||||
import ReactHighstock from 'react-highcharts/ReactHighstock'
|
||||
import './creditAccumulationGraphHC.css'
|
||||
import { useSelector } from 'react-redux'
|
||||
import CreditGraphTooltip from '../CreditGraphTooltip'
|
||||
import { reformatDate, getTextIn, getStudyRightElementTargetDates } from '../../common'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
@@ -132,14 +131,21 @@ const filterCoursesByStudyPlan = (plan, courses) =>
|
||||
|
||||
const filterCoursesByDate = (courses, date) => courses.filter(c => moment(c.date).isSameOrAfter(moment(date)))
|
||||
|
||||
const filterCourses = (student, singleStudent, byStudyPlanOfCode, cutStudyPlanCredits, startDate) => {
|
||||
const filterCourses = (
|
||||
student,
|
||||
singleStudent,
|
||||
byStudyPlanOfCode,
|
||||
cutStudyPlanCredits,
|
||||
startDate,
|
||||
customStudyStartYear
|
||||
) => {
|
||||
if (byStudyPlanOfCode && cutStudyPlanCredits)
|
||||
return filterCoursesByDate(
|
||||
filterCoursesByStudyPlan(
|
||||
student.studyplans.find(p => p.programme_code === byStudyPlanOfCode),
|
||||
student.courses
|
||||
),
|
||||
student.studyrightStart
|
||||
customStudyStartYear || student.studyrightStart
|
||||
)
|
||||
if (byStudyPlanOfCode)
|
||||
return filterCoursesByStudyPlan(
|
||||
@@ -273,7 +279,8 @@ const createStudentCreditLines = (
|
||||
studyRightId,
|
||||
studyPlanFilterIsActive,
|
||||
cutStudyPlanCredits,
|
||||
programmeCode
|
||||
programmeCode,
|
||||
customStudyStartYear
|
||||
) =>
|
||||
students.map(student => {
|
||||
const { studyrightStart } = student
|
||||
@@ -285,7 +292,15 @@ const createStudentCreditLines = (
|
||||
const studyPlanProgrammeCode = singleStudent ? code : studyPlanFilterIsActive && programmeCode
|
||||
|
||||
const { points } = _.flow(
|
||||
() => filterCourses(student, singleStudent, studyPlanProgrammeCode, cutStudyPlanCredits, startDate),
|
||||
() =>
|
||||
filterCourses(
|
||||
student,
|
||||
singleStudent,
|
||||
studyPlanProgrammeCode,
|
||||
cutStudyPlanCredits,
|
||||
startDate,
|
||||
customStudyStartYear
|
||||
),
|
||||
courses => [...courses].filter(({ date }) => new Date(date) <= new Date()),
|
||||
sortCoursesByDate,
|
||||
courses => courses.reduce(reduceCreditsToPoints, { credits: 0, points: [], singleStudent })
|
||||
@@ -320,13 +335,13 @@ const CreditAccumulationGraphHighCharts = ({
|
||||
studyRightId,
|
||||
programmeCode,
|
||||
customPopulation = false,
|
||||
studyPlanFilterIsActive,
|
||||
customStudyStartYear,
|
||||
}) => {
|
||||
const history = useHistory()
|
||||
const chartRef = useRef()
|
||||
const language = useLanguage()
|
||||
const { active: studyPlanFilterIsActive } = useSelector(
|
||||
state => state.filters?.views?.PopulationStatistics?.hops?.options ?? {}
|
||||
)
|
||||
|
||||
const [graphHeight, setGraphHeight] = useState(700)
|
||||
const [cutStudyPlanCredits, setCutStudyPlanCredits] = useState(false)
|
||||
const selectedStudyRight =
|
||||
@@ -343,9 +358,19 @@ const CreditAccumulationGraphHighCharts = ({
|
||||
studyRightId,
|
||||
studyPlanFilterIsActive,
|
||||
cutStudyPlanCredits,
|
||||
programmeCode
|
||||
programmeCode,
|
||||
customStudyStartYear
|
||||
),
|
||||
[students, singleStudent, startDate, studyRightId, studyPlanFilterIsActive, programmeCode, cutStudyPlanCredits]
|
||||
[
|
||||
students,
|
||||
singleStudent,
|
||||
startDate,
|
||||
studyRightId,
|
||||
studyPlanFilterIsActive,
|
||||
programmeCode,
|
||||
cutStudyPlanCredits,
|
||||
customStudyStartYear,
|
||||
]
|
||||
)
|
||||
|
||||
if (singleStudent) {
|
||||
@@ -372,6 +397,13 @@ const CreditAccumulationGraphHighCharts = ({
|
||||
|
||||
seriesData.push(createGoalSeries(starting, ending, filteredAbsences))
|
||||
}
|
||||
const getStudyRightStart = () => {
|
||||
if (customStudyStartYear) return new Date(customStudyStartYear).getTime()
|
||||
const studyRightStartFromStudent = new Date(students[0]?.studyrightStart ?? new Date(null))
|
||||
if (studyRightStartFromStudent.getFullYear() < 2000)
|
||||
return Math.min(..._.flatten(students.map(({ courses }) => courses.map(({ date }) => new Date(date).getTime()))))
|
||||
return studyRightStartFromStudent.getTime()
|
||||
}
|
||||
|
||||
const getGraphStart = () => {
|
||||
if (startDate) return new Date(startDate).getTime()
|
||||
@@ -383,23 +415,18 @@ const CreditAccumulationGraphHighCharts = ({
|
||||
})
|
||||
)
|
||||
)
|
||||
const studyRightStart = new Date(students[0]?.studyrightStart ?? new Date()).getTime()
|
||||
const studyRightStart = getStudyRightStart()
|
||||
if (studyPlanFilterIsActive && cutStudyPlanCredits) return studyRightStart
|
||||
if (studyPlanFilterIsActive)
|
||||
return Math.min(..._.flatten(students.map(({ courses }) => courses.map(({ date }) => new Date(date).getTime()))))
|
||||
return Math.min(
|
||||
studyRightStart,
|
||||
..._.flatten(
|
||||
students.map(({ courses }) => {
|
||||
return courses.map(({ date }) => new Date(date).getTime())
|
||||
})
|
||||
)
|
||||
)
|
||||
return studyRightStart
|
||||
}
|
||||
|
||||
const graduations = singleStudent ? filterGraduations(students[0], selectedStudyRight) : []
|
||||
const studyRightStartLine =
|
||||
!singleStudent && studyPlanFilterIsActive ? [new Date(students[0].studyrightStart).getTime()] : []
|
||||
!singleStudent && studyPlanFilterIsActive
|
||||
? [new Date(customStudyStartYear || students[0].studyrightStart).getTime()]
|
||||
: []
|
||||
|
||||
const options = createGraphOptions({
|
||||
singleStudent,
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ const CustomPopulationProgrammeDist = ({
|
||||
studentToTargetCourseDateMap,
|
||||
populationStatistics.elementdetails.data
|
||||
)
|
||||
if (programme && programme.code === '00000') {
|
||||
if (programme && programme.code === '00000' && coursecode) {
|
||||
const filteredEnrollments = (student.enrollments || [])
|
||||
// eslint-disable-next-line camelcase
|
||||
.filter(({ course_code }) => coursecode.includes(course_code))
|
||||
|
||||
@@ -102,7 +102,7 @@ const CustomPopulationContent = ({ students, custompop }) => {
|
||||
const populations = useSelector(state => state.populations)
|
||||
const { customPopulationFlag } = populations
|
||||
|
||||
const { progress } = useProgress(populations.loading)
|
||||
const { progress } = useProgress(populations.pending)
|
||||
|
||||
const { data: courseStats } = useGetStudentListCourseStatisticsQuery({
|
||||
studentNumbers: students.map(s => s.studentNumber),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { Segment, Header } from 'semantic-ui-react'
|
||||
import { useTitle } from '../../common/hooks'
|
||||
|
||||
const FacultyStatistics = () => {
|
||||
useTitle('Faculties')
|
||||
|
||||
return (
|
||||
<div className="segmentContainer">
|
||||
<Header className="segmentTitle" size="large">
|
||||
Faculty statistics
|
||||
</Header>
|
||||
<Segment className="contentSegment">Tosi hienot faculty statsit tähän</Segment>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect()(withRouter(FacultyStatistics))
|
||||
@@ -30,7 +30,7 @@ const CreditDateFilterCard = ({ options, onOptionsChange }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default createFilter({
|
||||
const filter = createFilter({
|
||||
key: 'CreditDate',
|
||||
|
||||
title: 'Date of Course Credits',
|
||||
@@ -62,5 +62,14 @@ export default createFilter({
|
||||
return true
|
||||
},
|
||||
|
||||
selectors: {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
selectedStartDate: ({ startDate }, _) => startDate,
|
||||
},
|
||||
|
||||
component: CreditDateFilterCard,
|
||||
})
|
||||
|
||||
export default filter
|
||||
|
||||
export const { selectedStartDate } = filter.selectors
|
||||
|
||||
@@ -1,17 +1,58 @@
|
||||
import React from 'react'
|
||||
import { Radio } from 'semantic-ui-react'
|
||||
import { Radio, Button } from 'semantic-ui-react'
|
||||
import useFilters from 'components/FilterView/useFilters'
|
||||
import moment from 'moment'
|
||||
import createFilter from './createFilter'
|
||||
import creditDateFilter, { selectedStartDate } from './date'
|
||||
|
||||
const HopsFilterCard = ({ options, onOptionsChange }) => {
|
||||
const { filterDispatch, useFilterSelector } = useFilters()
|
||||
const selectedCreditStartDate = useFilterSelector(selectedStartDate(''))
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '1em', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onOptionsChange({ active: !options.active })
|
||||
}}
|
||||
>
|
||||
<Radio style={{ width: '3.5rem', flexShrink: 0 }} toggle checked={options.active} />
|
||||
<div>Show only credits included in study plan</div>
|
||||
<div>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '1em', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
if (
|
||||
selectedCreditStartDate &&
|
||||
options.studyStart &&
|
||||
options.clearCreditDate &&
|
||||
new Date(selectedCreditStartDate) > new Date(options.studyStart) &&
|
||||
!options.active
|
||||
)
|
||||
filterDispatch(
|
||||
creditDateFilter.actions.setOptions({
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
})
|
||||
)
|
||||
onOptionsChange({ ...options, active: !options.active })
|
||||
}}
|
||||
>
|
||||
<Radio style={{ width: '3.5rem', flexShrink: 0 }} toggle checked={options.active} />
|
||||
<div>Show only credits included in study plan</div>
|
||||
</div>
|
||||
{options.studyStart ? (
|
||||
<Button
|
||||
content="Cut credits to study start"
|
||||
onClick={() =>
|
||||
filterDispatch(
|
||||
creditDateFilter.actions.setOptions({
|
||||
startDate: moment(options.studyStart),
|
||||
endDate: null,
|
||||
})
|
||||
)
|
||||
}
|
||||
disabled={!options.active}
|
||||
className="credit-date-filter-input"
|
||||
size="mini"
|
||||
style={{
|
||||
margin: '0.5rem',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -19,7 +60,7 @@ const HopsFilterCard = ({ options, onOptionsChange }) => {
|
||||
export default createFilter({
|
||||
key: 'hops',
|
||||
|
||||
title: 'HOPS',
|
||||
title: 'Personal Study Plan',
|
||||
|
||||
priority: -200,
|
||||
|
||||
|
||||
@@ -58,5 +58,11 @@ export default createFilter({
|
||||
|
||||
filter: (student, { transferred }) => student.transferredStudyright === transferred,
|
||||
|
||||
actions: {
|
||||
set: (options, value) => {
|
||||
options.transferred = value
|
||||
},
|
||||
},
|
||||
|
||||
component: TransferredToProgrammeFilterCard,
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ import LanguagePicker from '../LanguagePicker'
|
||||
import { isDev, adminerUrls } from '../../conf'
|
||||
|
||||
const allNavigationItems = {
|
||||
faculty: { path: '/faculties', key: 'faculties', label: 'Faculties', reqRights: ['admin'] },
|
||||
populations: {
|
||||
key: 'studyProgramme',
|
||||
label: 'Study programme',
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useRef } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Accordion } from 'semantic-ui-react'
|
||||
import useFilters from 'components/FilterView/useFilters'
|
||||
import studyPlanFilter from 'components/FilterView/filters/hops'
|
||||
import { useLocalStorage } from '../../common/hooks'
|
||||
|
||||
import CreditAccumulationGraphHighCharts from '../CreditAccumulationGraphHighCharts'
|
||||
@@ -53,8 +55,10 @@ const PopulationDetails = ({
|
||||
*/
|
||||
}
|
||||
|
||||
const renderCreditGainGraphs = () => {
|
||||
const RenderCreditGainGraphs = () => {
|
||||
const { CreditAccumulation } = infotooltips.PopulationStatistics
|
||||
const { useFilterSelector } = useFilters()
|
||||
const studyPlanFilterIsActive = useFilterSelector(studyPlanFilter.selectors.isActive)
|
||||
|
||||
const graphs = (
|
||||
<CreditAccumulationGraphHighCharts
|
||||
@@ -63,6 +67,7 @@ const PopulationDetails = ({
|
||||
trayOpen={() => {}}
|
||||
language={language}
|
||||
programmeCode={query?.studyRights?.programme}
|
||||
studyPlanFilterIsActive={studyPlanFilterIsActive}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
@@ -93,7 +98,7 @@ const PopulationDetails = ({
|
||||
},
|
||||
onTitleClick: () => handleClick(0),
|
||||
content: {
|
||||
content: <div ref={creditGraphRef}>{renderCreditGainGraphs()}</div>,
|
||||
content: <div ref={creditGraphRef}>{RenderCreditGainGraphs()}</div>,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ const FilterActiveNote = () => {
|
||||
|
||||
const handleClick = () => {
|
||||
setClicked(true)
|
||||
filterDispatch(transferredToProgrammeFilter.actions.clear())
|
||||
filterDispatch(transferredToProgrammeFilter.actions.set(null))
|
||||
}
|
||||
|
||||
if (clicked) return true
|
||||
|
||||
@@ -103,6 +103,9 @@ const PopulationStatistics = () => {
|
||||
[transferredToProgrammeFilter.key]: {
|
||||
transferred: false,
|
||||
},
|
||||
[hopsFilter.key]: {
|
||||
studyStart: (students[0] || {}).studyrightStart,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{filteredStudents => (
|
||||
|
||||
@@ -39,6 +39,7 @@ const CustomPopulation = React.lazy(() => retry(() => import('../CustomPopulatio
|
||||
const Updater = React.lazy(() => retry(() => import('../Updater')))
|
||||
const Trends = React.lazy(() => retry(() => import('../Trends')))
|
||||
const StudyGuidanceGroups = React.lazy(() => retry(() => import('../StudyGuidanceGroups')))
|
||||
const FacultyStatistics = React.lazy(() => retry(() => import('../FacultyStatistics')))
|
||||
|
||||
const routes = {
|
||||
students: '/students/:studentNumber?',
|
||||
@@ -51,6 +52,7 @@ const routes = {
|
||||
updater: '/updater',
|
||||
trends: '/trends',
|
||||
studyGuidanceGroups: '/studyguidancegroups/:groupid?',
|
||||
faculties: '/faculties/:facultyCode?',
|
||||
}
|
||||
|
||||
const Routes = () => (
|
||||
@@ -60,6 +62,7 @@ const Routes = () => (
|
||||
<Route exact path={routes.feedback} component={Feedback} />
|
||||
<Route path={routes.trends} component={Trends} />
|
||||
<ProtectedRoute requireUserHasRights exact path="/populations" component={Populations} />
|
||||
<ProtectedRoute requiredRoles={['admin']} exact path="/faculties/:facultyCode?" component={FacultyStatistics} />
|
||||
<ProtectedRoute
|
||||
requireUserHasRights
|
||||
exact
|
||||
|
||||
+7
-3
@@ -87,7 +87,11 @@ const StudyrightsTable = ({
|
||||
new Date(c.startdate).getTime() > new Date(c.studystartdate).getTime() ? c.startdate : c.studystartdate
|
||||
|
||||
// End dates of study rights are semi-open intervals, subtract 1 day to get acual end date
|
||||
const getAcualEndDate = endDate => (endDate ? new Date(endDate).setDate(new Date(endDate).getDate() - 1) : null)
|
||||
const getAcualEndDate = (endDate, graduated) => {
|
||||
if (!endDate) return null
|
||||
if (graduated) return new Date(endDate)
|
||||
return new Date(endDate).setDate(new Date(endDate).getDate() - 1)
|
||||
}
|
||||
|
||||
const renderProgrammes = c =>
|
||||
sortBy(c.elements.programmes.filter(filterDuplicates), 'startdate')
|
||||
@@ -95,7 +99,7 @@ const StudyrightsTable = ({
|
||||
.map(programme => (
|
||||
<p key={`${programme.name}-${programme.startdate}`}>
|
||||
{`${programme.name} (${reformatDate(getActualStartDate(programme), 'DD.MM.YYYY')} - ${reformatDate(
|
||||
getAcualEndDate(programme.enddate),
|
||||
getAcualEndDate(programme.enddate, c.graduated),
|
||||
'DD.MM.YYYY'
|
||||
)})`}
|
||||
{programmeCodes.includes(programme.code) && (
|
||||
@@ -113,7 +117,7 @@ const StudyrightsTable = ({
|
||||
.map(studytrack => (
|
||||
<p key={studytrack.name}>
|
||||
{`${studytrack.name} (${reformatDate(studytrack.startdate, 'DD.MM.YYYY')} - ${reformatDate(
|
||||
getAcualEndDate(studytrack.enddate),
|
||||
getAcualEndDate(studytrack.enddate, c.graduated),
|
||||
'DD.MM.YYYY'
|
||||
)})`}
|
||||
<br />{' '}
|
||||
|
||||
@@ -10,6 +10,7 @@ import scrollToComponent from 'react-scroll-to-component'
|
||||
import SegmentDimmer from 'components/SegmentDimmer'
|
||||
import useFilters from 'components/FilterView/useFilters'
|
||||
import creditDateFilter from 'components/FilterView/filters/date'
|
||||
import studyPlanFilter from 'components/FilterView/filters/hops'
|
||||
import { useFilteredAndFormattedElementDetails } from 'redux/elementdetails'
|
||||
import {
|
||||
useGetStudyGuidanceGroupPopulationQuery,
|
||||
@@ -59,6 +60,7 @@ const SingleStudyGroupContent = ({ filteredStudents, population, group, language
|
||||
)
|
||||
|
||||
const creditDateFilterActive = useFilterSelector(creditDateFilter.selectors.isActive)
|
||||
const studyPlanFilterIsActive = useFilterSelector(studyPlanFilter.selectors.isActive)
|
||||
|
||||
const [coursesStructuredByProgramme, toggleCoursesStructuredByProgramme] = useToggleAndSetNewestIndex({
|
||||
defaultValue: !!group.tags?.studyProgramme,
|
||||
@@ -99,6 +101,8 @@ const SingleStudyGroupContent = ({ filteredStudents, population, group, language
|
||||
togglePanel(0)
|
||||
}, [population])
|
||||
|
||||
const customStudyStartYear = group?.tags?.year ? new Date(`${group.tags.year}-07-31`) : null
|
||||
|
||||
const createPanels = students => [
|
||||
{
|
||||
key: 0,
|
||||
@@ -118,7 +122,12 @@ const SingleStudyGroupContent = ({ filteredStudents, population, group, language
|
||||
{creditDateFilterActive ? 'Show all credits' : 'Show starting from associated year'}
|
||||
</Button>
|
||||
)}
|
||||
<CreditAccumulationGraphHighCharts students={students} />
|
||||
<CreditAccumulationGraphHighCharts
|
||||
students={students}
|
||||
studyPlanFilterIsActive={studyPlanFilterIsActive}
|
||||
programmeCode={group?.tags?.studyProgramme}
|
||||
customStudyStartYear={customStudyStartYear}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -201,6 +210,7 @@ const SingleStudyGroupFilterView = props => {
|
||||
language: props.language,
|
||||
}),
|
||||
filters.ageFilter,
|
||||
filters.hopsFilter({ programmeCode: props.group?.tags?.studyProgramme }),
|
||||
filters.genderFilter,
|
||||
filters.startYearAtUniFilter,
|
||||
filters.tagsFilter,
|
||||
@@ -252,6 +262,10 @@ const SingleStudyGroupFilterView = props => {
|
||||
initialOptions[filters.creditDateFilter.key] = {
|
||||
startDate: moment(createAcademicYearStartDate(props.group.tags?.year)),
|
||||
}
|
||||
initialOptions[filters.hopsFilter.key] = {
|
||||
studyStart: props.group?.tags?.year ? `${props.group.tags.year}-07-31` : null,
|
||||
clearCreditDate: true,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+1
-6
@@ -38,12 +38,7 @@ const StudyGuidanceGroupPopulationCourses = ({
|
||||
{showStructured ? (
|
||||
<PopulationCourseStats courses={courses} pending={false} filteredStudents={filteredStudents} />
|
||||
) : (
|
||||
<PopulationCourseStatsFlat
|
||||
courses={courses}
|
||||
pending={false}
|
||||
filteredStudents={filteredStudents}
|
||||
showFilter={false}
|
||||
/>
|
||||
<PopulationCourseStatsFlat courses={courses} pending={false} filteredStudents={filteredStudents} />
|
||||
)}
|
||||
{pending && <SegmentDimmer isLoading={pending} />}
|
||||
</Segment>
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ const getColumns = (language, showStudents) => {
|
||||
title: 'Major credits',
|
||||
cellStyle: { textAlign: 'right' },
|
||||
filterType: 'range',
|
||||
getRowVal: course => course.totalProgrammecredits,
|
||||
getRowVal: course => course.totalProgrammeCredits,
|
||||
getRowContent: course => course.totalProgrammeCredits,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -272,7 +272,7 @@ const updateStudyRightElements = async (
|
||||
const studentnumber = personIdToStudentNumber[mainStudyRight.person_id]
|
||||
|
||||
// according to Eija Airio this is the right way to get the date... at least when studyright has changed
|
||||
let startDate = snapshot.first_snapshot_date_time
|
||||
let startDate = snapshot.first_snapshot_date_time || snapshot.valid.startDate
|
||||
|
||||
// fix for varhaiskasvatus, see https://github.com/UniversityOfHelsinkiCS/oodikone/issues/2741
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user