diff --git a/services/backend/src/routes/teachers.ts b/services/backend/src/routes/teachers.ts index 4120c491d2..5af6e3d37c 100644 --- a/services/backend/src/routes/teachers.ts +++ b/services/backend/src/routes/teachers.ts @@ -45,7 +45,12 @@ router.get('/top', async (req: GetTopTeachersRequest, res: Response) => { } const result = await getTeacherStats(category, Number(yearcode)) - res.json(result) + if (result) { + return res.json(result) + } + await findAndSaveTeachers(Number(yearcode), Number(yearcode)) + const updatedStats = await getTeacherStats(category, Number(yearcode)) + res.json(updatedStats) }) interface PostTopTeachersRequest extends Request { diff --git a/services/backend/src/services/teachers/top.ts b/services/backend/src/services/teachers/top.ts index 10500c6c7a..bef761fb05 100644 --- a/services/backend/src/services/teachers/top.ts +++ b/services/backend/src/services/teachers/top.ts @@ -1,12 +1,11 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { Op } from 'sequelize' +import { col, Op } from 'sequelize' -import { Course, Credit, Semester, Teacher } from '../../models' +import { Credit, Semester, Teacher } from '../../models' import { CreditTypeCode } from '../../types' import logger from '../../util/logger' import { redisClient } from '../redis' import { getCurrentSemester, getSemestersAndYears } from '../semesters' -import { isRegularCourse, TeacherStats } from './helpers' +import { TeacherStats } from './helpers' export enum CategoryID { ALL = 'all', @@ -21,7 +20,7 @@ const categories = { export const getTeacherStats = async (categoryId: string, yearCode: number) => { const { redisKey } = categories[categoryId] const category = await redisClient.hGet(redisKey, `${yearCode}`) - return category ? JSON.parse(category) : [] + return category ? JSON.parse(category) : null } const setTeacherStats = async (categoryId: string, yearCode: number, stats: TeacherStats[]) => { @@ -38,33 +37,41 @@ export const getCategoriesAndYears = async () => { } } -const getCreditsWithTeachersForYear = async (yearCode: number) => { +const getCreditsWithTeachersForYear = async (semesters: string[]) => { const credits = await Credit.findAll({ - attributes: ['id', 'credits', 'credittypecode', 'isStudyModule', 'is_open'], - include: [ - { - model: Semester, - required: true, + attributes: [ + 'credits', + 'credittypecode', + 'isStudyModule', + 'is_open', + [col('teachers.id'), 'teacherId'], + [col('teachers.name'), 'teacherName'], + ], + include: { + model: Teacher, + through: { attributes: [], - where: { - yearcode: { - [Op.eq]: yearCode, - }, - }, }, - { - model: Teacher, - attributes: ['id', 'name'], - required: true, + attributes: [], + where: { + id: { + [Op.notLike]: 'hy-hlo-org%', + }, }, - { - model: Course, - attributes: ['code', 'name', 'coursetypecode'], - required: true, + }, + where: { + semester_composite: { + [Op.in]: semesters, }, - ], + }, + raw: true, }) - return credits + return credits as unknown as Array< + Pick & { + teacherId: string + teacherName: string + } + > } const updatedStats = ( @@ -76,22 +83,17 @@ const updatedStats = ( transferred: boolean ) => { const { id, name } = teacher - const stats = teacherStats[id] || { id, name, passed: 0, failed: 0, credits: 0, transferred: 0 } - if (passed) { - return { - ...stats, - passed: stats.passed + 1, - credits: transferred ? stats.credits : stats.credits + credits, - transferred: transferred ? stats.transferred + credits : stats.transferred, - } + if (!teacherStats[id]) { + teacherStats[id] = { id, name, passed: 0, failed: 0, credits: 0, transferred: 0 } } - if (failed) { - return { - ...stats, - failed: stats.failed + 1, - } + const stats = teacherStats[id] + if (passed) { + stats.passed += 1 + stats.credits = transferred ? stats.credits : stats.credits + credits + stats.transferred = transferred ? stats.transferred + credits : stats.transferred + } else if (failed) { + stats.failed += 1 } - return stats } const filterTopTeachers = (stats: Record, limit: number = 50) => { @@ -105,30 +107,30 @@ const filterTopTeachers = (stats: Record, limit: number = } const findTopTeachers = async (yearCode: number) => { - const credits = await getCreditsWithTeachersForYear(yearCode) - const all = {} as Record - const openuni = {} as Record - credits - .filter(isRegularCourse) - .map(credit => { - const { credits, credittypecode, is_open } = credit - const teachers = credit.teachers - .filter(({ id }) => !id.includes('hy-hlo-org')) // Remove faculties from the leaderboards - .map(({ id, name }) => ({ id, name })) - const passed = Credit.passed(credit) || Credit.improved(credit) - const failed = Credit.failed(credit) - const transferred = credittypecode === CreditTypeCode.APPROVED - return { passed, failed, credits, teachers, is_open, transferred } - }) - .forEach(credit => { - const { passed, failed, credits, teachers, is_open, transferred } = credit - teachers.forEach(teacher => { - all[teacher.id] = updatedStats(all, teacher, credits, passed, failed, transferred) - if (is_open) { - openuni[teacher.id] = updatedStats(openuni, teacher, credits, passed, failed, transferred) - } - }) + const semesters = ( + await Semester.findAll({ + where: { + yearcode: yearCode, + }, + attributes: ['composite'], }) + ).map(({ composite }) => composite) + const credits = await getCreditsWithTeachersForYear(semesters) + const all: Record = {} + const openuni: Record = {} + for (const credit of credits) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { credits, credittypecode, isStudyModule, is_open, teacherId, teacherName } = credit + if (isStudyModule) continue + const passed = Credit.passed(credit) || Credit.improved(credit) + const failed = Credit.failed(credit) + const transferred = credittypecode === CreditTypeCode.APPROVED + const teacher = { id: teacherId, name: teacherName } + updatedStats(all, teacher, credits, passed, failed, transferred) + if (is_open) { + updatedStats(openuni, teacher, credits, passed, failed, transferred) + } + } return { all: filterTopTeachers(all), openuni: filterTopTeachers(openuni), diff --git a/services/frontend/src/components/Teachers/TeacherLeaderBoard/index.jsx b/services/frontend/src/components/Teachers/TeacherLeaderBoard/index.jsx index 66e0cdc8f6..a6cad4e273 100644 --- a/services/frontend/src/components/Teachers/TeacherLeaderBoard/index.jsx +++ b/services/frontend/src/components/Teachers/TeacherLeaderBoard/index.jsx @@ -2,40 +2,34 @@ import moment from 'moment' import { useState } from 'react' import { Message, Segment } from 'semantic-ui-react' -import { useGetTopTeachersCategoriesQuery, useLazyGetTopTeachersQuery } from '@/redux/teachers' +import { useGetTopTeachersCategoriesQuery, useGetTopTeachersQuery } from '@/redux/teachers' import { TeacherStatisticsTable } from '../TeacherStatisticsTable' import { LeaderForm } from './LeaderForm' export const TeacherLeaderBoard = () => { const [selectedYear, setSelectedYear] = useState(null) const [selectedCategory, setSelectedCategory] = useState(null) - const [getTopTeachers, { data: topTeachers = [] }] = useLazyGetTopTeachersQuery() - const { data: yearsAndCategories, isLoading, isFetching } = useGetTopTeachersCategoriesQuery() + const { data: yearsAndCategories, isFetching: categoriesAreLoading } = useGetTopTeachersCategoriesQuery() + const { data: topTeachers = {}, isFetching: statsAreLoading } = useGetTopTeachersQuery( + { yearcode: selectedYear, category: selectedCategory }, + { skip: !selectedYear || !selectedCategory } + ) - const updateAndSubmitForm = args => { - const year = args.selectedYear || selectedYear - const category = args.selectedCategory || selectedCategory - getTopTeachers({ yearcode: year, category }) - } + if (categoriesAreLoading) return const initLeaderboard = (year, category) => { setSelectedYear(year) setSelectedCategory(category) - updateAndSubmitForm({ selectedYear: year, selectedCategory: category }) } const handleYearChange = (_event, { value }) => { setSelectedYear(value) - updateAndSubmitForm({ selectedYear: value }) } const handleCategoryChange = (_event, { value }) => { setSelectedCategory(value) - updateAndSubmitForm({ selectedCategory: value }) } - if (isLoading || isFetching) return - const yearOptions = Object.values(yearsAndCategories.years) .map(({ yearcode, yearname }) => ({ key: yearcode, value: yearcode, text: yearname })) .sort((y1, y2) => y2.value - y1.value) @@ -53,8 +47,13 @@ export const TeacherLeaderBoard = () => { text: name, })) - const lastUpdated = new Date(topTeachers?.updated).toLocaleDateString(undefined, { - dateStyle: 'long', + const lastUpdated = new Date(topTeachers?.updated).toLocaleString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + minute: 'numeric', + hour: 'numeric', + second: 'numeric', }) return ( @@ -72,9 +71,9 @@ export const TeacherLeaderBoard = () => { selectedyear={selectedYear} yearoptions={yearOptions} /> - - {topTeachers.length > 0 && {`Last updated: ${lastUpdated}`}} - + + {topTeachers.stats?.length > 0 && {`Last updated: ${lastUpdated}`}} + ) diff --git a/services/frontend/src/redux/teachers.js b/services/frontend/src/redux/teachers.js index fc40f56cce..fae96f1064 100644 --- a/services/frontend/src/redux/teachers.js +++ b/services/frontend/src/redux/teachers.js @@ -28,6 +28,6 @@ export const { useGetTeacherQuery, useFindTeachersQuery, useLazyGetTeacherStatisticsQuery, - useLazyGetTopTeachersQuery, + useGetTopTeachersQuery, useGetTopTeachersCategoriesQuery, } = teachersApi