Skip to content
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
20 changes: 12 additions & 8 deletions src/lib/services/task_results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { NOT_FOUND } from '$lib/constants/http-response-status-codes';
const statusById = await getSubmissionStatusMapWithId();
const statusByName = await getSubmissionStatusMapWithName();

export async function getTaskResults(userId: string): Promise<TaskResults> {
export async function getTaskResults(userId: string | undefined): Promise<TaskResults> {
// 問題と特定のユーザの回答状況を使ってデータを結合
// 計算量: 問題数をN、特定のユーザの解答数をMとすると、O(N + M)になるはず。
const mergedTasksMap = await getMergedTasksMap();
Expand Down Expand Up @@ -127,14 +127,16 @@ async function transferAnswers(
// with_mapをtrueにすると、taskIdを使って各TaskResultにO(1)でアクセスできる。
// Why : データ総量を抑えるため。
export async function getTaskResultsOnlyResultExists(
userId: string,
userId: string | undefined,
with_map: boolean = false,
): Promise<TaskResults | Map<string, TaskResult>> {
const taskResultsMap: Map<string, TaskResult> = new Map();

// TODO: answerの降順にしたい
const tasks = await getTasks();
const answers = await answer_crud.getAnswers(userId);
// Skip the DB round-trip for anonymous users: getAnswers(undefined) drops the
// WHERE filter and full-scans taskAnswer.
const answers = userId !== undefined ? await answer_crud.getAnswers(userId) : new Map();
const tasksHasAnswer = tasks.filter((task) => answers.has(task.task_id));
const taskResultsWithAnswer = tasksHasAnswer.map((task: Task) => {
const taskResult = createDefaultTaskResult(userId, task);
Expand Down Expand Up @@ -215,9 +217,11 @@ export async function getTaskResultsByTaskId(
* @param userId - User ID for creating TaskResults
* @returns Promise<TaskResults> - Array of TaskResult objects
*/
async function createTaskResults(tasks: Tasks, userId: string): Promise<TaskResults> {
const answers = await answer_crud.getAnswers(userId);
async function createTaskResults(tasks: Tasks, userId: string | undefined): Promise<TaskResults> {
const isLoggedIn = userId !== undefined;
// Skip the DB round-trip for anonymous users: getAnswers(undefined) drops the
// WHERE filter and full-scans taskAnswer.
const answers = isLoggedIn ? await answer_crud.getAnswers(userId) : new Map();

return tasks.map((task: Task) => {
const answer = isLoggedIn ? answers.get(task.task_id) : null;
Expand All @@ -236,7 +240,7 @@ async function createTaskResults(tasks: Tasks, userId: string): Promise<TaskResu
*/
function mergeTaskAndAnswer(
task: Task,
userId: string,
userId: string | undefined,
answer: TaskAnswer | null | undefined,
): TaskResult {
const taskResult = createDefaultTaskResult(userId, task);
Expand All @@ -263,7 +267,7 @@ function mergeTaskAndAnswer(
return taskResult;
}

export function createDefaultTaskResult(userId: string, task: Task): TaskResult {
export function createDefaultTaskResult(userId: string | undefined, task: Task): TaskResult {
const taskResult: TaskResult = {
contest_id: task.contest_id,
task_id: task.task_id,
Expand Down Expand Up @@ -312,7 +316,7 @@ export async function updateTaskResult(taskId: string, submissionStatus: string,

export async function getTasksWithTagIds(
tagIds_string: string,
userId: string,
userId: string | undefined,
): Promise<TaskResults> {
const tagIds = tagIds_string.split(',');
const taskIdByTagIds = await db.taskTag.groupBy({
Expand Down
8 changes: 7 additions & 1 deletion src/lib/types/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,14 @@ export type TaskGrades = TaskGrade[];

export const taskGradeValues = Object.values(TaskGrade);

/**
* A user's submission result for a single task, extending {@link Task} with status metadata.
*
* Used for both authenticated and anonymous (logged-out) views.
*/
export interface TaskResult extends Task {
user_id: string;
/** Owner of this result; `undefined` for anonymous (logged-out) results. */
user_id: string | undefined;
status_name: string;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
status_id: string;
submission_status_image_path: string;
Expand Down
55 changes: 53 additions & 2 deletions src/test/lib/services/task_results.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@

import { describe, test, expect, vi, beforeEach } from 'vitest';

import { getTaskResults } from '$lib/services/task_results';
import type { TaskResult, TaskResults } from '$lib/types/task';
import { getTasks } from '$lib/services/tasks';
import { getAnswers } from '$lib/services/answers';
import { getTaskResults, getTaskResultsOnlyResultExists } from '$lib/services/task_results';

import type { TaskResult, TaskResults, Tasks } from '$lib/types/task';

import {
MOCK_TASKS_DATA,
Expand Down Expand Up @@ -381,6 +384,27 @@ describe('getTaskResults', () => {
});
});

describe('when anonymous (userId is undefined)', () => {
beforeEach(async () => {
mockAnswersForTest = MOCK_ANSWERS_WITH_ANSWERS;
vi.mocked(getAnswers).mockClear();
taskResults = await getTaskResults(undefined);
});

test('does not call getAnswers (skips the full-scan DB round-trip)', () => {
expect(getAnswers).not.toHaveBeenCalled();
});

test('returns default (未挑戦) results for all tasks', () => {
expect(taskResults.length).toBeGreaterThan(0);
taskResults.forEach((taskResult: TaskResult) => {
expect(taskResult.is_ac).toBe(false);
expect(taskResult.status_name).toBe('ns');
expect(taskResult.submission_status_label_name).toBe('未挑戦');
});
});
});

describe('when answers exist', () => {
beforeEach(async () => {
mockAnswersForTest = MOCK_ANSWERS_WITH_ANSWERS;
Expand Down Expand Up @@ -411,6 +435,33 @@ describe('getTaskResults', () => {
});
});

describe('getTaskResultsOnlyResultExists', () => {
describe('when anonymous (userId is undefined)', () => {
beforeEach(() => {
mockAnswersForTest = MOCK_ANSWERS_WITH_ANSWERS;
vi.mocked(getAnswers).mockClear();
vi.mocked(getTasks).mockResolvedValue([
{
id: '1',
contest_id: 'abc101',
task_id: 'arc099_a',
contest_type: 'ABC',
task_table_index: 'C',
title: 'Minimization',
grade: 'Q3',
},
] as unknown as Tasks);
});

test('does not call getAnswers and returns an empty array', async () => {
const taskResults = await getTaskResultsOnlyResultExists(undefined, false);

expect(getAnswers).not.toHaveBeenCalled();
expect(taskResults).toEqual([]);
});
});
});

describe('mergeTaskAndAnswer', () => {
createMergedTaskResults(
'when no answers exist',
Expand Down
Loading