Skip to content

Leaderboard refactor #3274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Aug 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/commons/application/ApplicationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,6 @@ export const defaultAchievement: AchievementState = {
};

export const defaultLeaderboard: LeaderboardState = {
userXp: [],
paginatedUserXp: { rows: [], userCount: 0 },
contestScore: [],
contestPopularVote: [],
Expand Down
57 changes: 31 additions & 26 deletions src/commons/assessmentWorkspace/AssessmentWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@
dispatch(WorkspaceActions.updateEditorValue(workspaceLocation, 0, code));
dispatch(LeaderboardActions.clearCode());
}
}, [dispatch]);

Check warning on line 201 in src/commons/assessmentWorkspace/AssessmentWorkspace.tsx

View workflow job for this annotation

GitHub Actions / lint (eslint)

React Hook useEffect has missing dependencies: 'code', 'initialRunCompleted', 'props.fromContestLeaderboard', and 'votingId'. Either include them or remove the dependency array

useEffect(() => {
if (assessmentOverview && assessmentOverview.maxTeamSize > 1) {
Expand Down Expand Up @@ -242,7 +242,7 @@
if (assessment != undefined && question.type == 'voting') {
dispatch(LeaderboardActions.setWorkspaceInitialRun(votingId));
}
}, [dispatch, assessment]);

Check warning on line 245 in src/commons/assessmentWorkspace/AssessmentWorkspace.tsx

View workflow job for this annotation

GitHub Actions / lint (eslint)

React Hook useEffect has missing dependencies: 'checkWorkspaceReset', 'question.type', and 'votingId'. Either include them or remove the dependency array

/**
* Handles toggling enabling and disabling token counter depending on assessment properties
Expand Down Expand Up @@ -464,6 +464,7 @@
const isTeamAssessment =
assessmentOverview !== undefined ? assessmentOverview.maxTeamSize > 1 : false;
const isContestVoting = question?.type === QuestionTypes.voting;
const isPublished = assessmentOverview?.isPublished;
const handleContestEntryClick = (_submissionId: number, answer: string) => {
// TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode.
handleEditorValueChange(0, answer);
Expand Down Expand Up @@ -538,34 +539,38 @@
/>
),
id: SideContentType.contestVoting
},
{
label: 'Score Leaderboard',
iconName: IconNames.CROWN,
body: (
<SideContentContestLeaderboard
handleContestEntryClick={handleContestEntryClick}
orderedContestEntries={(question as IContestVotingQuestion)?.scoreLeaderboard ?? []}
leaderboardType={SideContentType.scoreLeaderboard}
/>
),
id: SideContentType.scoreLeaderboard
},
{
label: 'Popular Vote Leaderboard',
iconName: IconNames.PEOPLE,
body: (
<SideContentContestLeaderboard
handleContestEntryClick={handleContestEntryClick}
orderedContestEntries={
(question as IContestVotingQuestion)?.popularVoteLeaderboard ?? []
}
leaderboardType={SideContentType.popularVoteLeaderboard}
/>
),
id: SideContentType.popularVoteLeaderboard
}
);
if (isPublished) {
tabs.push(
{
label: 'Score Leaderboard',
iconName: IconNames.CROWN,
body: (
<SideContentContestLeaderboard
handleContestEntryClick={handleContestEntryClick}
orderedContestEntries={(question as IContestVotingQuestion)?.scoreLeaderboard ?? []}
leaderboardType={SideContentType.scoreLeaderboard}
/>
),
id: SideContentType.scoreLeaderboard
},
{
label: 'Popular Vote Leaderboard',
iconName: IconNames.PEOPLE,
body: (
<SideContentContestLeaderboard
handleContestEntryClick={handleContestEntryClick}
orderedContestEntries={
(question as IContestVotingQuestion)?.popularVoteLeaderboard ?? []
}
leaderboardType={SideContentType.popularVoteLeaderboard}
/>
),
id: SideContentType.popularVoteLeaderboard
}
);
}
} else {
tabs.push(
{
Expand Down

Large diffs are not rendered by default.

62 changes: 0 additions & 62 deletions src/commons/dropdown/DropdownCreateCourse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ const DropdownCreateCourse: React.FC<Props> = props => {
enableAchievements: true,
enableSourcecast: true,
enableStories: false,
enableOverallLeaderboard: true,
enableContestLeaderboard: true,
topLeaderboardDisplay: 100,
topContestLeaderboardDisplay: 10,
sourceChapter: Chapter.SOURCE_1,
sourceVariant: Variant.DEFAULT,
moduleHelpText: ''
Expand Down Expand Up @@ -201,28 +197,6 @@ const DropdownCreateCourse: React.FC<Props> = props => {
})
}
/>
<Switch
checked={courseConfig.enableOverallLeaderboard}
inline
label="Enable Overall Leaderboard"
onChange={e =>
setCourseConfig({
...courseConfig,
enableOverallLeaderboard: (e.target as HTMLInputElement).checked
})
}
/>
<Switch
checked={courseConfig.enableContestLeaderboard}
inline
label="Enable Contest Leaderboard"
onChange={e =>
setCourseConfig({
...courseConfig,
enableContestLeaderboard: (e.target as HTMLInputElement).checked
})
}
/>
</div>
<div>
<Switch
Expand Down Expand Up @@ -262,42 +236,6 @@ const DropdownCreateCourse: React.FC<Props> = props => {
/>
</div>
</div>
<div>
<FormGroup
label="Leaderboard Top XX Display"
labelInfo="(configurable later on)"
labelFor="leaderboard-top-display"
>
<InputGroup
id="leaderboard-top-display"
value={String(courseConfig.topLeaderboardDisplay)}
onChange={e =>
setCourseConfig({
...courseConfig,
topLeaderboardDisplay: Number(e.target.value)
})
}
/>
</FormGroup>
</div>
<div>
<FormGroup
label="Contest Leaderboard Top XX Display"
labelInfo="(configurable later on)"
labelFor="contest-leaderboard-top-display"
>
<InputGroup
id="contest-leaderboard-top-display"
value={String(courseConfig.topContestLeaderboardDisplay)}
onChange={e =>
setCourseConfig({
...courseConfig,
topContestLeaderboardDisplay: Number(e.target.value)
})
}
/>
</FormGroup>
</div>
<div>
<FormGroup
label="Default Source Chapter"
Expand Down
19 changes: 4 additions & 15 deletions src/commons/sagas/LeaderboardSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,20 @@ import { actions } from '../utils/ActionsHelper';
import { selectTokens } from './BackendSaga';
import {
getAllContests,
getAllTotalXp,
getContestPopularVoteLeaderboard,
getContestScoreLeaderboard,
getPaginatedTotalXp
getOverallLeaderboardXP
} from './RequestsSaga';

const LeaderboardSaga = combineSagaHandlers({
[LeaderboardActions.getAllUsersXp.type]: function* () {
const tokens: Tokens = yield selectTokens();

const usersXp = yield call(getAllTotalXp, tokens);

if (usersXp) {
yield put(actions.saveAllUsersXp(usersXp));
}
},

[LeaderboardActions.getPaginatedLeaderboardXp.type]: function* (action) {
[LeaderboardActions.getOverallLeaderboardXP.type]: function* (action) {
const tokens: Tokens = yield selectTokens();
const { page, pageSize } = action.payload;

const paginatedUsersXp = yield call(getPaginatedTotalXp, page, pageSize, tokens);
const paginatedUsersXp = yield call(getOverallLeaderboardXP, page, pageSize, tokens);

if (paginatedUsersXp) {
yield put(actions.savePaginatedLeaderboardXp(paginatedUsersXp));
yield put(actions.saveOverallLeaderboardXP(paginatedUsersXp));
}
},

Expand Down
54 changes: 28 additions & 26 deletions src/commons/sagas/RequestsSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,10 +463,12 @@ export const getTotalXp = async (tokens: Tokens, courseRegId?: number): Promise<
};

/**
* GET /courses/{courseId}/all_user_xp
* GET /courses/{courseId}/leaderboards/xp_all
*/
export const getAllTotalXp = async (tokens: Tokens): Promise<number | null> => {
const resp = await request(`${courseId()}/all_users_xp`, 'GET', {
export const getAllOverallLeaderboardXP = async (
tokens: Tokens
): Promise<LeaderboardRow[] | null> => {
const resp = await request(`${courseId()}/leaderboards/xp_all`, 'GET', {
...tokens
});

Expand All @@ -489,16 +491,16 @@ export const getAllTotalXp = async (tokens: Tokens): Promise<number | null> => {
};

/**
* GET /courses/{courseId}/get_paginated_display
* GET /courses/{courseId}/leaderboards/xp
*/
export const getPaginatedTotalXp = async (
export const getOverallLeaderboardXP = async (
page: number,
pageSize: number,
tokens: Tokens
): Promise<{ rows: LeaderboardRow[]; userCount: number } | null> => {
const offset = (page - 1) * pageSize;
const params = new URLSearchParams({ offset: `${offset}`, page_size: `${pageSize}` });
const resp = await request(`${courseId()}/get_paginated_display?${params.toString()}`, 'GET', {
const resp = await request(`${courseId()}/leaderboards/xp?${params.toString()}`, 'GET', {
...tokens
});

Expand All @@ -523,16 +525,16 @@ export const getPaginatedTotalXp = async (
};

/**
* GET /courses/{courseId}/assessments/{assessmentid}/scoreLeaderboard
* GET /courses/{courseId}/assessments/{assessmentid}/contest_score_leaderboard
*/
export const getContestScoreLeaderboard = async (
assessmentId: number,
visibleEntries: number,
count: number,
tokens: Tokens
): Promise<ContestLeaderboardRow[] | null> => {
const params = new URLSearchParams({ visible_entries: `${visibleEntries}` });
const params = new URLSearchParams({ count: `${count}` });
const resp = await request(
`${courseId()}/assessments/${assessmentId}/scoreLeaderboard?${params.toString()}`,
`${courseId()}/assessments/${assessmentId}/contest_score_leaderboard?${params.toString()}`,
'GET',
{
...tokens
Expand Down Expand Up @@ -560,16 +562,16 @@ export const getContestScoreLeaderboard = async (
};

/**
* GET /courses/{courseId}/assessments/{assessmentid}/popularVoteLeaderboard
* GET /courses/{courseId}/assessments/{assessmentid}/contest_popular_leaderboard
*/
export const getContestPopularVoteLeaderboard = async (
assessmentId: number,
visibleEntries: number,
count: number,
tokens: Tokens
): Promise<ContestLeaderboardRow[] | null> => {
const params = new URLSearchParams({ visible_entries: `${visibleEntries}` });
const params = new URLSearchParams({ count: `${count}` });
const resp = await request(
`${courseId()}/assessments/${assessmentId}/popularVoteLeaderboard?${params.toString()}`,
`${courseId()}/assessments/${assessmentId}/contest_popular_leaderboard?${params.toString()}`,
'GET',
{
...tokens
Expand Down Expand Up @@ -1300,14 +1302,14 @@ export const deleteSourcecastEntry = async (
};

/**
* POST /courses/{courseId}/admin/assessments/{assessmentId}/calculateContestScore
* POST /courses/{courseId}/admin/assessments/{assessmentId}/contest_calculate_score
*/
export const calculateContestScore = async (
assessmentId: number,
tokens: Tokens
): Promise<Response | null> => {
const resp = await request(
`${courseId()}/admin/assessments/${assessmentId}/calculateContestScore`,
`${courseId()}/admin/assessments/${assessmentId}/contest_calculate_score`,
'POST',
{
...tokens
Expand All @@ -1318,14 +1320,14 @@ export const calculateContestScore = async (
};

/**
* POST /courses/{courseId}/admin/assessments/{assessmentId}/dispatchContestXp
* POST /courses/{courseId}/admin/assessments/{assessmentId}/contest_dispatch_xp
*/
export const dispatchContestXp = async (
assessmentId: number,
tokens: Tokens
): Promise<Response | null> => {
const resp = await request(
`${courseId()}/admin/assessments/${assessmentId}/dispatchContestXp`,
`${courseId()}/admin/assessments/${assessmentId}/contest_dispatch_xp`,
'POST',
{
...tokens
Expand All @@ -1336,16 +1338,16 @@ export const dispatchContestXp = async (
};

/**
* GET /courses/{courseId}/assessments/{assessmentId}/scoreLeaderboard
* GET /courses/{courseId}/assessments/{assessmentId}/contest_score_leaderboard
*/
export const getScoreLeaderboard = async (
assessmentId: number,
visibleEntries: number | undefined,
count: number | undefined,
tokens: Tokens
): Promise<ContestEntry[] | null> => {
const params = new URLSearchParams({ visible_entries: `${visibleEntries}` });
const params = new URLSearchParams({ count: `${count}` });
const resp = await request(
`${courseId()}/assessments/${assessmentId}/scoreLeaderboard?${params.toString()}`,
`${courseId()}/assessments/${assessmentId}/contest_score_leaderboard?${params.toString()}`,
'GET',
{
...tokens
Expand All @@ -1368,16 +1370,16 @@ export const getScoreLeaderboard = async (
};

/**
* GET /courses/{courseId}/assessments/{assessmentId}/popularVoteLeaderboard
* GET /courses/{courseId}/assessments/{assessmentId}/contest_popular_leaderboard
*/
export const getPopularVoteLeaderboard = async (
assessmentId: number,
visibleEntries: number | undefined,
count: number | undefined,
tokens: Tokens
): Promise<ContestEntry[] | null> => {
const params = new URLSearchParams({ visible_entries: `${visibleEntries}` });
const params = new URLSearchParams({ count: `${count}` });
const resp = await request(
`${courseId()}/assessments/${assessmentId}/popularVoteLeaderboard?${params.toString()}`,
`${courseId()}/assessments/${assessmentId}/contest_popular_leaderboard?${params.toString()}`,
'GET',
{
...tokens
Expand Down
6 changes: 2 additions & 4 deletions src/features/leaderboard/LeaderboardActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import {
} from './LeaderboardTypes';

const LeaderboardActions = createActions('leaderboard', {
getAllUsersXp: 0,
saveAllUsersXp: (userXp: LeaderboardRow[]) => userXp,
getPaginatedLeaderboardXp: (page: number, pageSize: number) => ({ page, pageSize }),
savePaginatedLeaderboardXp: (payload: { rows: LeaderboardRow[]; userCount: number }) => payload,
getOverallLeaderboardXP: (page: number, pageSize: number) => ({ page, pageSize }),
saveOverallLeaderboardXP: (payload: { rows: LeaderboardRow[]; userCount: number }) => payload,
getAllContestScores: (assessmentId: number, visibleEntries: number) => ({
assessmentId,
visibleEntries
Expand Down
5 changes: 1 addition & 4 deletions src/features/leaderboard/LeaderboardReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ export const LeaderboardReducer: Reducer<LeaderboardState, SourceActionType> = c
defaultLeaderboard,
builder => {
builder
.addCase(LeaderboardActions.saveAllUsersXp, (state, action) => {
state.userXp = action.payload;
})
.addCase(LeaderboardActions.savePaginatedLeaderboardXp, (state, action) => {
.addCase(LeaderboardActions.saveOverallLeaderboardXP, (state, action) => {
state.paginatedUserXp = {
rows: action.payload.rows || [],
userCount: action.payload.userCount || 0
Expand Down
1 change: 0 additions & 1 deletion src/features/leaderboard/LeaderboardTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export type ContestLeaderboardRow = {
};

export type LeaderboardState = {
userXp: LeaderboardRow[];
paginatedUserXp: { rows: LeaderboardRow[]; userCount: number };
contestScore: ContestLeaderboardRow[];
contestPopularVote: ContestLeaderboardRow[];
Expand Down
Loading
Loading