Skip to content
Open
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
113 changes: 68 additions & 45 deletions senatus/src/api/event/controllers/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isRegistrationOpenNow,
} from '../../../utils/event-registration';
import { getCurrentUser } from '../../../utils/current-user';
import { calculateSubmissionScore, calculateProblemSubmissionStats } from '../../../utils/scoring';

type LeaderboardVisibilityMode = 'public_only_live' | 'full_live';

Expand Down Expand Up @@ -50,10 +51,11 @@ type SubmissionLike = {
username?: string;
email?: string;
};
problem?: {
problem?: ProblemLike;
executions?: ExecutionLike[];
event?: {
documentId: string;
};
executions?: ExecutionLike[];
};

export type TrainingProblemLike = {
Expand Down Expand Up @@ -103,12 +105,12 @@ type EventQuestionLike = {

const DEFAULT_POINTS = 100;

const getSafeWeight = (value?: number) =>
export const getSafeWeight = (value?: number) =>
typeof value === 'number' && value > 0 ? value : 1;

const round2 = (value: number) => Math.round(value * 100) / 100;
export const round2 = (value: number) => Math.round(value * 100) / 100;

const isExecutionPassed = (execution?: ExecutionLike) => {
export const isExecutionPassed = (execution?: ExecutionLike) => {
if (!execution?.processed) {
return false;
}
Expand All @@ -122,7 +124,7 @@ const isExecutionPassed = (execution?: ExecutionLike) => {
return stdout.length > 0 && stdout === expected;
};

const shouldUseTestCaseInLiveScore = (
export const shouldUseTestCaseInLiveScore = (
problem: ProblemLike,
testCase: TestCaseLike,
eventEnded: boolean,
Expand Down Expand Up @@ -278,9 +280,63 @@ export default factories.createCoreController('api::event.event', ({ strapi }) =
return ctx.notFound('Event not found');
}

response.data = sanitizeEventForCompetitionView(event) as any;
const eventEnded = event.end ? new Date(event.end).getTime() <= Date.now() : false;

const submissions = (await strapi.documents('api::submission.submission').findMany({
filters: {
event: {
documentId: event.documentId
},
user: {
documentId: user.documentId
}
},
populate: {
event:true,
problem: {
populate: {
testCases: true
}
},
executions: {
populate: {
testCase: true
}
}
},
sort: ['createdAt:asc'],
pagination: {
page: 1,
pageSize: 10000,
},
}) as SubmissionLike[]);

const sanitizedEvent = sanitizeEventForCompetitionView(event) as any;
sanitizedEvent.problems = (sanitizedEvent.problems || []).map((problem: any) => {
const submissionsForProblem=submissions
.filter((submission) => submission.problem?.documentId === problem.documentId)
.map((submission: any) => {
const result = calculateSubmissionScore(submission.problem, submission.executions || [], eventEnded);
return {
...submission,
score: result.score,
maxScore: result.problemMaxScore
}
}
);
return {
...problem,
...calculateProblemSubmissionStats(submissionsForProblem)
}
});

response.data=sanitizedEvent;

return response;

/*response.data = sanitizeEventForCompetitionView(event) as any;

return response;*/
},

async registrationStatus(ctx) {
Expand Down Expand Up @@ -831,45 +887,12 @@ export default factories.createCoreController('api::event.event', ({ strapi }) =
if (!problem) {
continue;
}

const scopedCases = (problem.testCases || []).filter((testCase) =>
shouldUseTestCaseInLiveScore(problem, testCase, eventEnded),
);

const totalWeight = scopedCases.reduce(
(sum, testCase) => sum + getSafeWeight(testCase.weight),
0,
);

const executionMap = new Map(
(submission.executions || [])
.filter((execution) => execution.testCase?.documentId)
.map((execution) => [execution.testCase!.documentId, execution]),

const { score, totalExecutionTime, problemMaxScore } = calculateSubmissionScore(
problem,
submission.executions || [],
eventEnded,
);

let passedWeight = 0;
let totalExecutionTime = 0;

for (const testCase of scopedCases) {
const execution = executionMap.get(testCase.documentId);
if (!execution?.processed) {
continue;
}

const testWeight = getSafeWeight(testCase.weight);

if (isExecutionPassed(execution)) {
passedWeight += testWeight;
}

if (typeof execution.executionTime === 'number' && execution.executionTime >= 0) {
totalExecutionTime += execution.executionTime;
}
}

const problemMaxScore = problem.points || DEFAULT_POINTS;
const rawScore = totalWeight > 0 ? (passedWeight / totalWeight) * problemMaxScore : 0;
const score = round2(rawScore);
const createdAtTs = submission.createdAt ? new Date(submission.createdAt).getTime() : Number.MAX_SAFE_INTEGER;

const key = `${submission.user.documentId}::${problem.documentId}`;
Expand Down
56 changes: 53 additions & 3 deletions senatus/src/api/submission/controllers/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { connect } from '@colosseum/queue';
import { factories } from '@strapi/strapi'
import { canUserRegisterForEvent, normalizeComparableIdentifiers } from '../../../utils/event-registration';
import { getCurrentUser } from '../../../utils/current-user';
import { calculateSubmissionScore } from '../../../utils/scoring';

let queueClientPromise: Promise<Awaited<ReturnType<typeof connect>>> | null = null;

Expand Down Expand Up @@ -263,6 +264,32 @@ const sanitizeExecutionForParticipant = (execution: any) => {

return nextExecution;
};
const isEventEnded = (event: any) => {
if (!event?.end)
return false;
const endMs = new Date(event.end).getTime();
return !Number.isNaN(endMs) && endMs <= Date.now();
};

const submissionWithScore = (submission: any) => {
if (!submission?.problem) {
return submission;
}

const eventEnded = isEventEnded(submission.problem.event);

const { score, problemMaxScore } = calculateSubmissionScore(
submission.problem,
submission.executions || [],
eventEnded,
);

return {
...submission,
score,
maxScore: problemMaxScore,
};
};

const sanitizeExecutionsForParticipant = (executions: any[]) =>
(executions || [])
Expand All @@ -274,9 +301,16 @@ const sanitizeSubmissionForParticipant = (submission: any) => {
return submission;
}

const withScore =submissionWithScore(submission);

if(!Array.isArray(withScore.executions))
{
return withScore;
}

return {
...submission,
executions: sanitizeExecutionsForParticipant(submission.executions),
...withScore,
executions: sanitizeExecutionsForParticipant(withScore.executions),
};
};

Expand All @@ -299,7 +333,23 @@ export default factories.createCoreController('api::submission.submission', ({ s
if (!user) {
return ctx.unauthorized('Authentication required');
}

ctx.query = {
...ctx.query,
populate: {
...(typeof ctx.query.populate === 'object' ? ctx.query.populate : {}),
language: true,
user: true,
problem: {
populate: {
testCases: true,
event: { fields: ['end'] },
},
},
executions: {
populate: { testCase: true },
},
},
};
const response = (await super.find(ctx)) as any;
const entries = Array.isArray(response?.data)
? response.data
Expand Down
52 changes: 50 additions & 2 deletions senatus/src/api/training/controllers/training.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

import { factories } from '@strapi/strapi';
import { TrainingProblemLike } from '../../event/controllers/event';
import { calculateSubmissionScore } from '../../../utils/scoring';
import { calculateProblemSubmissionStats } from '../../../utils/scoring';
import { getCurrentUser } from '../../../utils/current-user';

export default {
async trainingProblems(ctx) {
const now = Date.now();
const user = await getCurrentUser(strapi, ctx);

const endedEvents = await strapi.documents('api::event.event').findMany({
filters: {
Expand All @@ -19,10 +23,54 @@ export default {
});
const problems = ((endedEvents || []) as any[])
.flatMap((event) => (event.problems || [])
.map((problem: any) => ({...problem, eventId: event.id, eventTitle: event.title})))
.map((problem: any) => ({...problem, eventId: event.documentId, eventTitle: event.title})))
.filter((problem: any) => !!problem?.documentId) as TrainingProblemLike[];
//*/

ctx.body = problems;
const rawSubmissions = await strapi.documents('api::submission.submission').findMany({
filters: {
user: {
documentId: user.documentId,
},
},
populate: {
problem: {
fields: ['documentId', 'points'],
populate: {
testCases: true,
event: {
fields: ['end']
}
}
},
executions: {
populate: {
testCase: true
}
}
},
sort: ['createdAt:asc']
});
const submissions = rawSubmissions.filter((submission: any) => submission?.metadata?.mode !== 'competition')

const submissionsWithScore = submissions.map((submission : any) => {
const result = calculateSubmissionScore(submission.problem, submission.executions || [], true);
return {
...submission,
score: result.score,
maxScore: result.problemMaxScore
}
});
const problemsWithScores = problems.map((problem) => {
const submissionsForProblem = submissionsWithScore.filter(
(submission : any) => submission.problem.documentId === problem.documentId
);
const bestSubmission = calculateProblemSubmissionStats(submissionsForProblem);
return {
...problem,
...bestSubmission
}
})
ctx.body = problemsWithScores;
},
};
78 changes: 78 additions & 0 deletions senatus/src/utils/scoring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { isExecutionPassed, shouldUseTestCaseInLiveScore, getSafeWeight, round2 } from '../api/event/controllers/event';
export const DEFAULT_POINTS = 100;
export const calculateSubmissionScore = (
problem: { documentId: string; points?: number; testCases?: any[] },
executions: any[],
eventEnded: boolean,
) => {
const scopedCases = (problem.testCases || []).filter((testCase) =>
shouldUseTestCaseInLiveScore(problem, testCase, eventEnded),
);

const totalWeight = scopedCases.reduce(
(sum, testCase) => sum + getSafeWeight(testCase.weight),
0,
);

const executionMap = new Map(
(executions || [])
.filter((execution) => execution.testCase?.documentId)
.map((execution) => [execution.testCase!.documentId, execution]),
);

let passedWeight = 0;
let totalExecutionTime = 0;
let allProcessed = true;

for (const testCase of scopedCases) {
const execution = executionMap.get(testCase.documentId);
if (!execution?.processed) {
allProcessed=false;
continue;
}

const testWeight = getSafeWeight(testCase.weight);

if (isExecutionPassed(execution)) {
passedWeight += testWeight;
}

if (typeof execution.executionTime === 'number' && execution.executionTime >= 0) {
totalExecutionTime += execution.executionTime;
}
}

const problemMaxScore = problem.points || DEFAULT_POINTS;
const rawScore = totalWeight > 0 ? (passedWeight / totalWeight) * problemMaxScore : 0;
return {
score: round2(rawScore),
problemMaxScore,
totalExecutionTime,
allProcessed
}
};
export const calculateProblemSubmissionStats = (submissions: any[]) => {
if (submissions.length === 0) {
return {
lastSubmission: null,
bestSubmission: null
}
}
let lastSubmission = submissions[submissions.length-1];
let bestSubmission = submissions[0];
for(const submission of submissions)
{
if(bestSubmission.score<submission.score)
bestSubmission=submission;
}
return {
lastSubmission: lastSubmission ? {
score: lastSubmission.score,
maxScore: lastSubmission.maxScore,
} : null,
bestSubmission: bestSubmission ? {
score: bestSubmission.score,
maxScore: bestSubmission.maxScore,
} : null,
}
};
Loading
Loading