From a4f8835c6628790b216d6b5f43ed5ec11f5c03f1 Mon Sep 17 00:00:00 2001 From: positivelyjon Date: Sat, 30 Mar 2024 13:44:33 +0800 Subject: [PATCH 1/9] Create export functions for leaderboards --- .../GroundControlConfigureCell.tsx | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx index e8a300ab2e..c7fd7b6eb9 100644 --- a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx +++ b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx @@ -9,9 +9,14 @@ import { Switch } from '@blueprintjs/core'; import { IconNames, Team } from '@blueprintjs/icons'; +import { createGrid, GridOptions } from 'ag-grid-community'; import React, { useCallback, useState } from 'react'; +import { useTypedSelector } from 'src/commons/utils/Hooks'; -import { AssessmentOverview } from '../../../../commons/assessment/AssessmentTypes'; +import { + AssessmentOverview, + IContestVotingQuestion +} from '../../../../commons/assessment/AssessmentTypes'; import ControlButton from '../../../../commons/ControlButton'; type Props = { @@ -42,6 +47,36 @@ const ConfigureCell: React.FC = ({ handleConfigureAssessment, data }) => const toggleVotingFeatures = useCallback(() => setHasVotingFeatures(prev => !prev), []); const toggleIsTeamAssessment = useCallback(() => setIsTeamAssessment(prev => !prev), []); + const assessment = useTypedSelector(state => state.session.assessments.get(data.id)); + // Currently, all voting assignments have only one voting question, and so retrieving the leaderboards + // for the first leaderboard is sufficient. However, if there are multiple voting questions in the same + // assessment, this might not work. + const question = assessment!.questions[0] as IContestVotingQuestion; + const scoreLeaderboard = question.scoreLeaderboard; + const popularVoteLeaderboard = question.popularVoteLeaderboard; + + const exportScoreLeaderboardToCsv = () => { + const gridContainer = document.createElement('div'); + const gridOptions: GridOptions = { + rowData: scoreLeaderboard, + columnDefs: [{ field: 'student_name' }, { field: 'answer' }, { field: 'final_score' }] + }; + const api = createGrid(gridContainer, gridOptions); + api.exportDataAsCsv(); + api.destroy(); + }; + + const exportPopularVoteLeaderboardToCsv = () => { + const gridContainer = document.createElement('div'); + const gridOptions: GridOptions = { + rowData: popularVoteLeaderboard, + columnDefs: [{ field: 'student_name' }, { field: 'answer' }, { field: 'final_score' }] + }; + const api = createGrid(gridContainer, gridOptions); + api.exportDataAsCsv(); + api.destroy(); + }; + return ( <> @@ -106,15 +141,15 @@ const ConfigureCell: React.FC = ({ handleConfigureAssessment, data }) =>
Date: Mon, 1 Apr 2024 15:56:01 +0800 Subject: [PATCH 2/9] Add scoreleaderboard attribute to assessment overviews --- src/commons/assessment/AssessmentTypes.ts | 2 ++ .../GroundControlConfigureCell.tsx | 19 ++++++------------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/commons/assessment/AssessmentTypes.ts b/src/commons/assessment/AssessmentTypes.ts index 2ace796bed..acf3fb0491 100644 --- a/src/commons/assessment/AssessmentTypes.ts +++ b/src/commons/assessment/AssessmentTypes.ts @@ -64,6 +64,8 @@ export type AssessmentOverview = { isPublished?: boolean; hasVotingFeatures: boolean; hasTokenCounter?: boolean; + scoreLeaderboard?: ContestEntry[]; + popularVoteLeaderboard?: ContestEntry[]; maxXp: number; earlySubmissionXp: number; number?: string; // For mission control diff --git a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx index c7fd7b6eb9..b873e0f08f 100644 --- a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx +++ b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx @@ -15,7 +15,8 @@ import { useTypedSelector } from 'src/commons/utils/Hooks'; import { AssessmentOverview, - IContestVotingQuestion + IContestVotingQuestion, + QuestionTypes } from '../../../../commons/assessment/AssessmentTypes'; import ControlButton from '../../../../commons/ControlButton'; @@ -47,18 +48,10 @@ const ConfigureCell: React.FC = ({ handleConfigureAssessment, data }) => const toggleVotingFeatures = useCallback(() => setHasVotingFeatures(prev => !prev), []); const toggleIsTeamAssessment = useCallback(() => setIsTeamAssessment(prev => !prev), []); - const assessment = useTypedSelector(state => state.session.assessments.get(data.id)); - // Currently, all voting assignments have only one voting question, and so retrieving the leaderboards - // for the first leaderboard is sufficient. However, if there are multiple voting questions in the same - // assessment, this might not work. - const question = assessment!.questions[0] as IContestVotingQuestion; - const scoreLeaderboard = question.scoreLeaderboard; - const popularVoteLeaderboard = question.popularVoteLeaderboard; - - const exportScoreLeaderboardToCsv = () => { + const exportPopularVoteLeaderboardToCsv = () => { const gridContainer = document.createElement('div'); const gridOptions: GridOptions = { - rowData: scoreLeaderboard, + rowData: data.popularVoteLeaderboard, columnDefs: [{ field: 'student_name' }, { field: 'answer' }, { field: 'final_score' }] }; const api = createGrid(gridContainer, gridOptions); @@ -66,10 +59,10 @@ const ConfigureCell: React.FC = ({ handleConfigureAssessment, data }) => api.destroy(); }; - const exportPopularVoteLeaderboardToCsv = () => { + const exportScoreLeaderboardToCsv = () => { const gridContainer = document.createElement('div'); const gridOptions: GridOptions = { - rowData: popularVoteLeaderboard, + rowData: data.scoreLeaderboard, columnDefs: [{ field: 'student_name' }, { field: 'answer' }, { field: 'final_score' }] }; const api = createGrid(gridContainer, gridOptions); From a5b16a06dda5a8c30b3db8a6eeb0731a856eb2e6 Mon Sep 17 00:00:00 2001 From: positivelyjon Date: Mon, 1 Apr 2024 15:56:39 +0800 Subject: [PATCH 3/9] Fix imports --- .../subcomponents/GroundControlConfigureCell.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx index b873e0f08f..2de6d321e0 100644 --- a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx +++ b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx @@ -11,13 +11,8 @@ import { import { IconNames, Team } from '@blueprintjs/icons'; import { createGrid, GridOptions } from 'ag-grid-community'; import React, { useCallback, useState } from 'react'; -import { useTypedSelector } from 'src/commons/utils/Hooks'; -import { - AssessmentOverview, - IContestVotingQuestion, - QuestionTypes -} from '../../../../commons/assessment/AssessmentTypes'; +import { AssessmentOverview } from '../../../../commons/assessment/AssessmentTypes'; import ControlButton from '../../../../commons/ControlButton'; type Props = { From 048c45756c9797bc700263a6e2813442a13bb43b Mon Sep 17 00:00:00 2001 From: positivelyjon Date: Tue, 2 Apr 2024 01:05:37 +0800 Subject: [PATCH 4/9] Create new route for fetching assessment leaderboard --- src/commons/assessment/AssessmentTypes.ts | 2 - src/commons/sagas/RequestsSaga.ts | 52 +++++++++++++++++++ .../groundControl/GroundControlActions.ts | 13 +++++ .../groundControl/GroundControlTypes.ts | 2 + .../academy/groundControl/GroundControl.tsx | 8 ++- .../groundControl/GroundControlContainer.ts | 6 ++- .../GroundControlConfigureCell.tsx | 21 +++++--- 7 files changed, 93 insertions(+), 11 deletions(-) diff --git a/src/commons/assessment/AssessmentTypes.ts b/src/commons/assessment/AssessmentTypes.ts index acf3fb0491..2ace796bed 100644 --- a/src/commons/assessment/AssessmentTypes.ts +++ b/src/commons/assessment/AssessmentTypes.ts @@ -64,8 +64,6 @@ export type AssessmentOverview = { isPublished?: boolean; hasVotingFeatures: boolean; hasTokenCounter?: boolean; - scoreLeaderboard?: ContestEntry[]; - popularVoteLeaderboard?: ContestEntry[]; maxXp: number; earlySubmissionXp: number; number?: string; // For mission control diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 8654b249db..6199d9d150 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -442,6 +442,58 @@ export const getAssessmentOverviews = async ( }); }; +/** + * GET /courses/{courseId}/assessments/{assessmentId}/scoreLeaderboard + */ +export const getScoreLeaderboard = async (tokens: Tokens): Promise => { + const resp = await request(`${courseId()}/assessments`, 'GET', { + ...tokens + }); + if (!resp || !resp.ok) { + return null; // invalid accessToken _and_ refreshToken + } + const assessmentOverviews = await resp.json(); + + return assessmentOverviews.map((overview: any) => { + overview.gradingStatus = computeGradingStatus( + overview.isManuallyGraded, + overview.status, + overview.gradedCount, + overview.questionCount + ); + delete overview.gradedCount; + delete overview.questionCount; + + return overview as AssessmentOverview; + }); +}; + +/** + * GET /courses/{courseId}/assessments/{assessmentId}/popularVoteLeaderboard + */ +export const getPopularVoteLeaderboard = async (tokens: Tokens): Promise => { + const resp = await request(`${courseId()}/assessments`, 'GET', { + ...tokens + }); + if (!resp || !resp.ok) { + return null; // invalid accessToken _and_ refreshToken + } + const assessmentOverviews = await resp.json(); + + return assessmentOverviews.map((overview: any) => { + overview.gradingStatus = computeGradingStatus( + overview.isManuallyGraded, + overview.status, + overview.gradedCount, + overview.questionCount + ); + delete overview.gradedCount; + delete overview.questionCount; + + return overview as AssessmentOverview; + }); +}; + /** * GET /courses/{courseId}/user/total_xp */ diff --git a/src/features/groundControl/GroundControlActions.ts b/src/features/groundControl/GroundControlActions.ts index 24c11f8966..e4beda2b94 100644 --- a/src/features/groundControl/GroundControlActions.ts +++ b/src/features/groundControl/GroundControlActions.ts @@ -5,6 +5,8 @@ import { CHANGE_TEAM_SIZE_ASSESSMENT, CONFIGURE_ASSESSMENT, DELETE_ASSESSMENT, + FETCH_POPULAR_VOTE_LEADERBOARD, + FETCH_SCORE_LEADERBOARD, PUBLISH_ASSESSMENT, UPLOAD_ASSESSMENT } from './GroundControlTypes'; @@ -39,3 +41,14 @@ export const configureAssessment = createAction( payload: { id, hasVotingFeatures, hasTokenCounter } }) ); + +export const fetchScoreLeaderboard = createAction(FETCH_SCORE_LEADERBOARD, (id: number) => ({ + payload: { id } +})); + +export const fetchPopularVoteLeaderboard = createAction( + FETCH_POPULAR_VOTE_LEADERBOARD, + (id: number) => ({ + payload: { id } + }) +); diff --git a/src/features/groundControl/GroundControlTypes.ts b/src/features/groundControl/GroundControlTypes.ts index fe33589af5..8b8131b097 100644 --- a/src/features/groundControl/GroundControlTypes.ts +++ b/src/features/groundControl/GroundControlTypes.ts @@ -4,3 +4,5 @@ export const DELETE_ASSESSMENT = 'DELETE_ASSESSMENT'; export const PUBLISH_ASSESSMENT = 'PUBLISH_ASSESSMENT'; export const UPLOAD_ASSESSMENT = 'UPLOAD_ASSESSMENT'; export const CONFIGURE_ASSESSMENT = 'CONFIGURE_ASSESSMENT'; +export const FETCH_SCORE_LEADERBOARD = 'FETCH_SCORE_LEADERBOARD'; +export const FETCH_POPULAR_VOTE_LEADERBOARD = 'FETCH_POPULAR_VOTE_LEADERBOARD'; diff --git a/src/pages/academy/groundControl/GroundControl.tsx b/src/pages/academy/groundControl/GroundControl.tsx index 4a5e2ccd0b..4b9545a66f 100644 --- a/src/pages/academy/groundControl/GroundControl.tsx +++ b/src/pages/academy/groundControl/GroundControl.tsx @@ -16,7 +16,7 @@ import { AgGridReact } from 'ag-grid-react'; import React, { useState } from 'react'; import { useSession } from 'src/commons/utils/Hooks'; -import { AssessmentOverview } from '../../../commons/assessment/AssessmentTypes'; +import { AssessmentOverview, ContestEntry } from '../../../commons/assessment/AssessmentTypes'; import ContentDisplay from '../../../commons/ContentDisplay'; import DefaultChapterSelect from './subcomponents/DefaultChapterSelect'; import ConfigureCell from './subcomponents/GroundControlConfigureCell'; @@ -40,6 +40,8 @@ export type DispatchProps = { hasVotingFeatures: boolean, hasTokenCounter: boolean ) => void; + handleFetchScoreLeaderboard: (id: number) => ContestEntry[]; + handleFetchPopularVoteLeaderboard: (id: number) => ContestEntry[]; handleFetchCourseConfigs: () => void; }; @@ -163,7 +165,9 @@ const GroundControl: React.FC = props => { field: 'placeholderConfigure' as any, cellRenderer: ConfigureCell, cellRendererParams: { - handleConfigureAssessment: props.handleConfigureAssessment + handleConfigureAssessment: props.handleConfigureAssessment, + handleFetchScoreLeaderboard: props.handleFetchScoreLeaderboard, + handleFetchPopularVoteLeaderboard: props.handleFetchPopularVoteLeaderboard }, width: 80, filter: false, diff --git a/src/pages/academy/groundControl/GroundControlContainer.ts b/src/pages/academy/groundControl/GroundControlContainer.ts index 1eec25b7b6..dc5ff4b3e6 100644 --- a/src/pages/academy/groundControl/GroundControlContainer.ts +++ b/src/pages/academy/groundControl/GroundControlContainer.ts @@ -11,6 +11,8 @@ import { changeTeamSizeAssessment, configureAssessment, deleteAssessment, + fetchPopularVoteLeaderboard, + fetchScoreLeaderboard, publishAssessment, uploadAssessment } from '../../../features/groundControl/GroundControlActions'; @@ -28,7 +30,9 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis handleUploadAssessment: uploadAssessment, handlePublishAssessment: publishAssessment, handleFetchCourseConfigs: fetchCourseConfig, - handleConfigureAssessment: configureAssessment + handleConfigureAssessment: configureAssessment, + handleFetchScoreLeaderboard: fetchScoreLeaderboard, + handleFetchPopularVoteLeaderboard: fetchPopularVoteLeaderboard }, dispatch ); diff --git a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx index 2de6d321e0..74b6d873b5 100644 --- a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx +++ b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx @@ -12,7 +12,7 @@ import { IconNames, Team } from '@blueprintjs/icons'; import { createGrid, GridOptions } from 'ag-grid-community'; import React, { useCallback, useState } from 'react'; -import { AssessmentOverview } from '../../../../commons/assessment/AssessmentTypes'; +import { AssessmentOverview, ContestEntry } from '../../../../commons/assessment/AssessmentTypes'; import ControlButton from '../../../../commons/ControlButton'; type Props = { @@ -21,10 +21,17 @@ type Props = { hasVotingFeatures: boolean, hasTokenCounter: boolean ) => void; + handleFetchScoreLeaderboard: (id: number) => ContestEntry[]; + handleFetchPopularVoteLeaderboard: (id: number) => ContestEntry[]; data: AssessmentOverview; }; -const ConfigureCell: React.FC = ({ handleConfigureAssessment, data }) => { +const ConfigureCell: React.FC = ({ + handleConfigureAssessment, + handleFetchScoreLeaderboard, + handleFetchPopularVoteLeaderboard, + data +}) => { const [isDialogOpen, setDialogState] = useState(false); const [hasVotingFeatures, setHasVotingFeatures] = useState(!!data.hasVotingFeatures); const [hasTokenCounter, setHasTokenCounter] = useState(!!data.hasTokenCounter); @@ -43,10 +50,11 @@ const ConfigureCell: React.FC = ({ handleConfigureAssessment, data }) => const toggleVotingFeatures = useCallback(() => setHasVotingFeatures(prev => !prev), []); const toggleIsTeamAssessment = useCallback(() => setIsTeamAssessment(prev => !prev), []); - const exportPopularVoteLeaderboardToCsv = () => { + const exportPopularVoteLeaderboardToCsv = async () => { + const popularVoteLeaderboard = await handleFetchPopularVoteLeaderboard(data.id); const gridContainer = document.createElement('div'); const gridOptions: GridOptions = { - rowData: data.popularVoteLeaderboard, + rowData: popularVoteLeaderboard, columnDefs: [{ field: 'student_name' }, { field: 'answer' }, { field: 'final_score' }] }; const api = createGrid(gridContainer, gridOptions); @@ -54,10 +62,11 @@ const ConfigureCell: React.FC = ({ handleConfigureAssessment, data }) => api.destroy(); }; - const exportScoreLeaderboardToCsv = () => { + const exportScoreLeaderboardToCsv = async () => { + const scoreLeaderboard = await handleFetchScoreLeaderboard(data.id); const gridContainer = document.createElement('div'); const gridOptions: GridOptions = { - rowData: data.scoreLeaderboard, + rowData: scoreLeaderboard, columnDefs: [{ field: 'student_name' }, { field: 'answer' }, { field: 'final_score' }] }; const api = createGrid(gridContainer, gridOptions); From a02c36ee1998cc6aaa53b052b5bcfb3018a9e47a Mon Sep 17 00:00:00 2001 From: positivelyjon Date: Tue, 2 Apr 2024 01:53:30 +0800 Subject: [PATCH 5/9] Remove ground control actions and types --- .../groundControl/GroundControlActions.ts | 13 -------- .../groundControl/GroundControlTypes.ts | 2 -- .../academy/groundControl/GroundControl.tsx | 8 ++--- .../groundControl/GroundControlContainer.ts | 6 +--- .../ExportScoreLeaderboardButton.tsx | 0 .../ExportVoteLeaderboardButton.tsx | 30 +++++++++++++++++++ .../GroundControlConfigureCell.tsx | 19 ------------ 7 files changed, 33 insertions(+), 45 deletions(-) create mode 100644 src/pages/academy/groundControl/configureControls/ExportScoreLeaderboardButton.tsx create mode 100644 src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx diff --git a/src/features/groundControl/GroundControlActions.ts b/src/features/groundControl/GroundControlActions.ts index e4beda2b94..24c11f8966 100644 --- a/src/features/groundControl/GroundControlActions.ts +++ b/src/features/groundControl/GroundControlActions.ts @@ -5,8 +5,6 @@ import { CHANGE_TEAM_SIZE_ASSESSMENT, CONFIGURE_ASSESSMENT, DELETE_ASSESSMENT, - FETCH_POPULAR_VOTE_LEADERBOARD, - FETCH_SCORE_LEADERBOARD, PUBLISH_ASSESSMENT, UPLOAD_ASSESSMENT } from './GroundControlTypes'; @@ -41,14 +39,3 @@ export const configureAssessment = createAction( payload: { id, hasVotingFeatures, hasTokenCounter } }) ); - -export const fetchScoreLeaderboard = createAction(FETCH_SCORE_LEADERBOARD, (id: number) => ({ - payload: { id } -})); - -export const fetchPopularVoteLeaderboard = createAction( - FETCH_POPULAR_VOTE_LEADERBOARD, - (id: number) => ({ - payload: { id } - }) -); diff --git a/src/features/groundControl/GroundControlTypes.ts b/src/features/groundControl/GroundControlTypes.ts index 8b8131b097..fe33589af5 100644 --- a/src/features/groundControl/GroundControlTypes.ts +++ b/src/features/groundControl/GroundControlTypes.ts @@ -4,5 +4,3 @@ export const DELETE_ASSESSMENT = 'DELETE_ASSESSMENT'; export const PUBLISH_ASSESSMENT = 'PUBLISH_ASSESSMENT'; export const UPLOAD_ASSESSMENT = 'UPLOAD_ASSESSMENT'; export const CONFIGURE_ASSESSMENT = 'CONFIGURE_ASSESSMENT'; -export const FETCH_SCORE_LEADERBOARD = 'FETCH_SCORE_LEADERBOARD'; -export const FETCH_POPULAR_VOTE_LEADERBOARD = 'FETCH_POPULAR_VOTE_LEADERBOARD'; diff --git a/src/pages/academy/groundControl/GroundControl.tsx b/src/pages/academy/groundControl/GroundControl.tsx index 4b9545a66f..4a5e2ccd0b 100644 --- a/src/pages/academy/groundControl/GroundControl.tsx +++ b/src/pages/academy/groundControl/GroundControl.tsx @@ -16,7 +16,7 @@ import { AgGridReact } from 'ag-grid-react'; import React, { useState } from 'react'; import { useSession } from 'src/commons/utils/Hooks'; -import { AssessmentOverview, ContestEntry } from '../../../commons/assessment/AssessmentTypes'; +import { AssessmentOverview } from '../../../commons/assessment/AssessmentTypes'; import ContentDisplay from '../../../commons/ContentDisplay'; import DefaultChapterSelect from './subcomponents/DefaultChapterSelect'; import ConfigureCell from './subcomponents/GroundControlConfigureCell'; @@ -40,8 +40,6 @@ export type DispatchProps = { hasVotingFeatures: boolean, hasTokenCounter: boolean ) => void; - handleFetchScoreLeaderboard: (id: number) => ContestEntry[]; - handleFetchPopularVoteLeaderboard: (id: number) => ContestEntry[]; handleFetchCourseConfigs: () => void; }; @@ -165,9 +163,7 @@ const GroundControl: React.FC = props => { field: 'placeholderConfigure' as any, cellRenderer: ConfigureCell, cellRendererParams: { - handleConfigureAssessment: props.handleConfigureAssessment, - handleFetchScoreLeaderboard: props.handleFetchScoreLeaderboard, - handleFetchPopularVoteLeaderboard: props.handleFetchPopularVoteLeaderboard + handleConfigureAssessment: props.handleConfigureAssessment }, width: 80, filter: false, diff --git a/src/pages/academy/groundControl/GroundControlContainer.ts b/src/pages/academy/groundControl/GroundControlContainer.ts index dc5ff4b3e6..1eec25b7b6 100644 --- a/src/pages/academy/groundControl/GroundControlContainer.ts +++ b/src/pages/academy/groundControl/GroundControlContainer.ts @@ -11,8 +11,6 @@ import { changeTeamSizeAssessment, configureAssessment, deleteAssessment, - fetchPopularVoteLeaderboard, - fetchScoreLeaderboard, publishAssessment, uploadAssessment } from '../../../features/groundControl/GroundControlActions'; @@ -30,9 +28,7 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis handleUploadAssessment: uploadAssessment, handlePublishAssessment: publishAssessment, handleFetchCourseConfigs: fetchCourseConfig, - handleConfigureAssessment: configureAssessment, - handleFetchScoreLeaderboard: fetchScoreLeaderboard, - handleFetchPopularVoteLeaderboard: fetchPopularVoteLeaderboard + handleConfigureAssessment: configureAssessment }, dispatch ); diff --git a/src/pages/academy/groundControl/configureControls/ExportScoreLeaderboardButton.tsx b/src/pages/academy/groundControl/configureControls/ExportScoreLeaderboardButton.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx b/src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx new file mode 100644 index 0000000000..988b8f5355 --- /dev/null +++ b/src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx @@ -0,0 +1,30 @@ +import { IconNames } from '@blueprintjs/icons'; +import ControlButton from 'src/commons/ControlButton'; + +type Props = { + assessmentId: number; +}; + +const ExportVoteLeaderboardButton: React.FC = ({ assessmentId }) => { + const exportPopularVoteLeaderboardToCsv = async () => { + const popularVoteLeaderboard = await handleFetchPopularVoteLeaderboard(data.id); + const gridContainer = document.createElement('div'); + const gridOptions: GridOptions = { + rowData: popularVoteLeaderboard, + columnDefs: [{ field: 'student_name' }, { field: 'answer' }, { field: 'final_score' }] + }; + const api = createGrid(gridContainer, gridOptions); + api.exportDataAsCsv(); + api.destroy(); + }; + + return ( +
+ +
+ ); +}; diff --git a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx index 74b6d873b5..a25cbda783 100644 --- a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx +++ b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx @@ -50,18 +50,6 @@ const ConfigureCell: React.FC = ({ const toggleVotingFeatures = useCallback(() => setHasVotingFeatures(prev => !prev), []); const toggleIsTeamAssessment = useCallback(() => setIsTeamAssessment(prev => !prev), []); - const exportPopularVoteLeaderboardToCsv = async () => { - const popularVoteLeaderboard = await handleFetchPopularVoteLeaderboard(data.id); - const gridContainer = document.createElement('div'); - const gridOptions: GridOptions = { - rowData: popularVoteLeaderboard, - columnDefs: [{ field: 'student_name' }, { field: 'answer' }, { field: 'final_score' }] - }; - const api = createGrid(gridContainer, gridOptions); - api.exportDataAsCsv(); - api.destroy(); - }; - const exportScoreLeaderboardToCsv = async () => { const scoreLeaderboard = await handleFetchScoreLeaderboard(data.id); const gridContainer = document.createElement('div'); @@ -135,13 +123,6 @@ const ConfigureCell: React.FC = ({ />
-
- -
Date: Tue, 2 Apr 2024 01:59:20 +0800 Subject: [PATCH 6/9] Abstract out export buttons --- .../ExportScoreLeaderboardButton.tsx | 35 +++++++++++++++++++ .../ExportVoteLeaderboardButton.tsx | 7 +++- .../GroundControlConfigureCell.tsx | 35 ++++--------------- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/pages/academy/groundControl/configureControls/ExportScoreLeaderboardButton.tsx b/src/pages/academy/groundControl/configureControls/ExportScoreLeaderboardButton.tsx index e69de29bb2..52d298f9ab 100644 --- a/src/pages/academy/groundControl/configureControls/ExportScoreLeaderboardButton.tsx +++ b/src/pages/academy/groundControl/configureControls/ExportScoreLeaderboardButton.tsx @@ -0,0 +1,35 @@ +import { IconNames } from '@blueprintjs/icons'; +import { createGrid,GridOptions } from 'ag-grid-community'; +import ControlButton from 'src/commons/ControlButton'; + +type Props = { + assessmentId: number; +}; + +const ExportScoreLeaderboardButton: React.FC = ({ assessmentId }) => { + + // onClick handler for fetching score leaderboard, putting it into a grid and exporting data + const exportScoreVoteLeaderboardToCsv = async () => { + const scoreLeaderbaord = await {1, 3, 4} + const gridContainer = document.createElement('div'); + const gridOptions: GridOptions = { + rowData: scoreLeaderbaord, + columnDefs: [{ field: 'student_name' }, { field: 'answer' }, { field: 'final_score' }] + }; + const api = createGrid(gridContainer, gridOptions); + api.exportDataAsCsv(); + api.destroy(); + }; + + return ( +
+ +
+ ); +}; + +export default ExportScoreLeaderboardButton; \ No newline at end of file diff --git a/src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx b/src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx index 988b8f5355..82f1403a3c 100644 --- a/src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx +++ b/src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx @@ -1,4 +1,5 @@ import { IconNames } from '@blueprintjs/icons'; +import { createGrid,GridOptions } from 'ag-grid-community'; import ControlButton from 'src/commons/ControlButton'; type Props = { @@ -6,8 +7,10 @@ type Props = { }; const ExportVoteLeaderboardButton: React.FC = ({ assessmentId }) => { + + // onClick handler for fetching popular vote leaderboard, putting it into a grid and exporting data const exportPopularVoteLeaderboardToCsv = async () => { - const popularVoteLeaderboard = await handleFetchPopularVoteLeaderboard(data.id); + const popularVoteLeaderboard = await {1, 3, 4} const gridContainer = document.createElement('div'); const gridOptions: GridOptions = { rowData: popularVoteLeaderboard, @@ -28,3 +31,5 @@ const ExportVoteLeaderboardButton: React.FC = ({ assessmentId }) => {
); }; + +export default ExportVoteLeaderboardButton; \ No newline at end of file diff --git a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx index a25cbda783..6158bf26a3 100644 --- a/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx +++ b/src/pages/academy/groundControl/subcomponents/GroundControlConfigureCell.tsx @@ -9,11 +9,12 @@ import { Switch } from '@blueprintjs/core'; import { IconNames, Team } from '@blueprintjs/icons'; -import { createGrid, GridOptions } from 'ag-grid-community'; import React, { useCallback, useState } from 'react'; -import { AssessmentOverview, ContestEntry } from '../../../../commons/assessment/AssessmentTypes'; +import { AssessmentOverview } from '../../../../commons/assessment/AssessmentTypes'; import ControlButton from '../../../../commons/ControlButton'; +import ExportScoreLeaderboardButton from '../configureControls/ExportScoreLeaderboardButton'; +import ExportVoteLeaderboardButton from '../configureControls/ExportVoteLeaderboardButton'; type Props = { handleConfigureAssessment: ( @@ -21,17 +22,10 @@ type Props = { hasVotingFeatures: boolean, hasTokenCounter: boolean ) => void; - handleFetchScoreLeaderboard: (id: number) => ContestEntry[]; - handleFetchPopularVoteLeaderboard: (id: number) => ContestEntry[]; data: AssessmentOverview; }; -const ConfigureCell: React.FC = ({ - handleConfigureAssessment, - handleFetchScoreLeaderboard, - handleFetchPopularVoteLeaderboard, - data -}) => { +const ConfigureCell: React.FC = ({ handleConfigureAssessment, data }) => { const [isDialogOpen, setDialogState] = useState(false); const [hasVotingFeatures, setHasVotingFeatures] = useState(!!data.hasVotingFeatures); const [hasTokenCounter, setHasTokenCounter] = useState(!!data.hasTokenCounter); @@ -50,18 +44,6 @@ const ConfigureCell: React.FC = ({ const toggleVotingFeatures = useCallback(() => setHasVotingFeatures(prev => !prev), []); const toggleIsTeamAssessment = useCallback(() => setIsTeamAssessment(prev => !prev), []); - const exportScoreLeaderboardToCsv = async () => { - const scoreLeaderboard = await handleFetchScoreLeaderboard(data.id); - const gridContainer = document.createElement('div'); - const gridOptions: GridOptions = { - rowData: scoreLeaderboard, - columnDefs: [{ field: 'student_name' }, { field: 'answer' }, { field: 'final_score' }] - }; - const api = createGrid(gridContainer, gridOptions); - api.exportDataAsCsv(); - api.destroy(); - }; - return ( <> @@ -123,13 +105,8 @@ const ConfigureCell: React.FC = ({ />
-
- -
+ + Date: Wed, 3 Apr 2024 09:05:44 +0800 Subject: [PATCH 7/9] Create new request for leaderboard --- src/commons/sagas/RequestsSaga.ts | 54 ++++++++++++------------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 6199d9d150..74d08f2b95 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -445,53 +445,39 @@ export const getAssessmentOverviews = async ( /** * GET /courses/{courseId}/assessments/{assessmentId}/scoreLeaderboard */ -export const getScoreLeaderboard = async (tokens: Tokens): Promise => { - const resp = await request(`${courseId()}/assessments`, 'GET', { +export const getScoreLeaderboard = async ( + assessmentId: number, + tokens: Tokens +): Promise => { + const resp = await request(`${courseId()}/assessments/${assessmentId}/scoreLeaderboard`, 'GET', { ...tokens }); if (!resp || !resp.ok) { return null; // invalid accessToken _and_ refreshToken } - const assessmentOverviews = await resp.json(); - - return assessmentOverviews.map((overview: any) => { - overview.gradingStatus = computeGradingStatus( - overview.isManuallyGraded, - overview.status, - overview.gradedCount, - overview.questionCount - ); - delete overview.gradedCount; - delete overview.questionCount; - - return overview as AssessmentOverview; - }); + const scoreLeaderboard = await resp.json(); + return scoreLeaderboard as ContestEntry[]; }; /** * GET /courses/{courseId}/assessments/{assessmentId}/popularVoteLeaderboard */ -export const getPopularVoteLeaderboard = async (tokens: Tokens): Promise => { - const resp = await request(`${courseId()}/assessments`, 'GET', { - ...tokens - }); +export const getPopularVoteLeaderboard = async ( + assessmentId: number, + tokens: Tokens +): Promise => { + const resp = await request( + `${courseId()}/assessments/${assessmentId}/popularVoteLeaderboard`, + 'GET', + { + ...tokens + } + ); if (!resp || !resp.ok) { return null; // invalid accessToken _and_ refreshToken } - const assessmentOverviews = await resp.json(); - - return assessmentOverviews.map((overview: any) => { - overview.gradingStatus = computeGradingStatus( - overview.isManuallyGraded, - overview.status, - overview.gradedCount, - overview.questionCount - ); - delete overview.gradedCount; - delete overview.questionCount; - - return overview as AssessmentOverview; - }); + const popularVoteLeaderboard = await resp.json(); + return popularVoteLeaderboard as ContestEntry[]; }; /** From 0796117011d923f6c53274593f42daa03167585d Mon Sep 17 00:00:00 2001 From: positivelyjon Date: Thu, 4 Apr 2024 13:10:43 +0800 Subject: [PATCH 8/9] Bypass saga to fetch leaderboards --- .../ExportScoreLeaderboardButton.tsx | 17 +++++++++++------ .../ExportVoteLeaderboardButton.tsx | 13 +++++++++---- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/pages/academy/groundControl/configureControls/ExportScoreLeaderboardButton.tsx b/src/pages/academy/groundControl/configureControls/ExportScoreLeaderboardButton.tsx index 52d298f9ab..76c00fceda 100644 --- a/src/pages/academy/groundControl/configureControls/ExportScoreLeaderboardButton.tsx +++ b/src/pages/academy/groundControl/configureControls/ExportScoreLeaderboardButton.tsx @@ -1,16 +1,21 @@ import { IconNames } from '@blueprintjs/icons'; -import { createGrid,GridOptions } from 'ag-grid-community'; +import { createGrid, GridOptions } from 'ag-grid-community'; +import { Tokens } from 'src/commons/application/types/SessionTypes'; import ControlButton from 'src/commons/ControlButton'; +import { getScoreLeaderboard } from 'src/commons/sagas/RequestsSaga'; +import { useSession } from 'src/commons/utils/Hooks'; type Props = { assessmentId: number; }; const ExportScoreLeaderboardButton: React.FC = ({ assessmentId }) => { + const { accessToken, refreshToken } = useSession(); + const tokens = { accessToken, refreshToken } as Tokens; - // onClick handler for fetching score leaderboard, putting it into a grid and exporting data - const exportScoreVoteLeaderboardToCsv = async () => { - const scoreLeaderbaord = await {1, 3, 4} + // onClick handler for fetching score leaderboard, putting it into a grid and exporting data + const exportScoreLeaderboardToCsv = async () => { + const scoreLeaderbaord = await getScoreLeaderboard(assessmentId, tokens); const gridContainer = document.createElement('div'); const gridOptions: GridOptions = { rowData: scoreLeaderbaord, @@ -25,11 +30,11 @@ const ExportScoreLeaderboardButton: React.FC = ({ assessmentId }) => {
); }; -export default ExportScoreLeaderboardButton; \ No newline at end of file +export default ExportScoreLeaderboardButton; diff --git a/src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx b/src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx index 82f1403a3c..b0cc8cf286 100644 --- a/src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx +++ b/src/pages/academy/groundControl/configureControls/ExportVoteLeaderboardButton.tsx @@ -1,16 +1,21 @@ import { IconNames } from '@blueprintjs/icons'; -import { createGrid,GridOptions } from 'ag-grid-community'; +import { createGrid, GridOptions } from 'ag-grid-community'; +import { Tokens } from 'src/commons/application/types/SessionTypes'; import ControlButton from 'src/commons/ControlButton'; +import { getPopularVoteLeaderboard } from 'src/commons/sagas/RequestsSaga'; +import { useSession } from 'src/commons/utils/Hooks'; type Props = { assessmentId: number; }; const ExportVoteLeaderboardButton: React.FC = ({ assessmentId }) => { + const { accessToken, refreshToken } = useSession(); + const tokens = { accessToken, refreshToken } as Tokens; - // onClick handler for fetching popular vote leaderboard, putting it into a grid and exporting data + // onClick handler for fetching popular vote leaderboard, putting it into a grid and exporting data const exportPopularVoteLeaderboardToCsv = async () => { - const popularVoteLeaderboard = await {1, 3, 4} + const popularVoteLeaderboard = await getPopularVoteLeaderboard(assessmentId, tokens); const gridContainer = document.createElement('div'); const gridOptions: GridOptions = { rowData: popularVoteLeaderboard, @@ -32,4 +37,4 @@ const ExportVoteLeaderboardButton: React.FC = ({ assessmentId }) => { ); }; -export default ExportVoteLeaderboardButton; \ No newline at end of file +export default ExportVoteLeaderboardButton; From 93d813a416120c5dc5ddd75b4372956387d631bf Mon Sep 17 00:00:00 2001 From: positivelyjon Date: Fri, 5 Apr 2024 22:24:51 +0800 Subject: [PATCH 9/9] Edit route to be part of admin endpoint --- src/commons/sagas/RequestsSaga.ts | 80 ++++++++++++++++--------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 74d08f2b95..d7d25db24d 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -442,44 +442,6 @@ export const getAssessmentOverviews = async ( }); }; -/** - * GET /courses/{courseId}/assessments/{assessmentId}/scoreLeaderboard - */ -export const getScoreLeaderboard = async ( - assessmentId: number, - tokens: Tokens -): Promise => { - const resp = await request(`${courseId()}/assessments/${assessmentId}/scoreLeaderboard`, 'GET', { - ...tokens - }); - if (!resp || !resp.ok) { - return null; // invalid accessToken _and_ refreshToken - } - const scoreLeaderboard = await resp.json(); - return scoreLeaderboard as ContestEntry[]; -}; - -/** - * GET /courses/{courseId}/assessments/{assessmentId}/popularVoteLeaderboard - */ -export const getPopularVoteLeaderboard = async ( - assessmentId: number, - tokens: Tokens -): Promise => { - const resp = await request( - `${courseId()}/assessments/${assessmentId}/popularVoteLeaderboard`, - 'GET', - { - ...tokens - } - ); - if (!resp || !resp.ok) { - return null; // invalid accessToken _and_ refreshToken - } - const popularVoteLeaderboard = await resp.json(); - return popularVoteLeaderboard as ContestEntry[]; -}; - /** * GET /courses/{courseId}/user/total_xp */ @@ -1166,6 +1128,48 @@ export const deleteSourcecastEntry = async ( return resp; }; +/** + * GET /courses/{courseId}/admin/assessments/{assessmentId}/scoreLeaderboard + */ +export const getScoreLeaderboard = async ( + assessmentId: number, + tokens: Tokens +): Promise => { + const resp = await request( + `${courseId()}/admin/assessments/${assessmentId}/scoreLeaderboard`, + 'GET', + { + ...tokens + } + ); + if (!resp || !resp.ok) { + return null; // invalid accessToken _and_ refreshToken + } + const scoreLeaderboard = await resp.json(); + return scoreLeaderboard as ContestEntry[]; +}; + +/** + * GET /courses/{courseId}/admin/assessments/{assessmentId}/popularVoteLeaderboard + */ +export const getPopularVoteLeaderboard = async ( + assessmentId: number, + tokens: Tokens +): Promise => { + const resp = await request( + `${courseId()}/admin/assessments/${assessmentId}/popularVoteLeaderboard`, + 'GET', + { + ...tokens + } + ); + if (!resp || !resp.ok) { + return null; // invalid accessToken _and_ refreshToken + } + const popularVoteLeaderboard = await resp.json(); + return popularVoteLeaderboard as ContestEntry[]; +}; + /** * POST /courses/{courseId}/admin/assessments/{assessmentId} */