diff --git a/src/lib/actions/update_task_result.ts b/src/lib/actions/update_task_result.ts new file mode 100644 index 000000000..35eb98827 --- /dev/null +++ b/src/lib/actions/update_task_result.ts @@ -0,0 +1,31 @@ +import { fail } from '@sveltejs/kit'; + +import * as crud from '$lib/services/task_results'; +import { BAD_REQUEST, UNAUTHORIZED } from '$lib/constants/http-response-status-codes'; + +// HACK: clickを1回実行するとactionsが2回実行されてしまう。原因と修正方法が分かっていない状態。 +export const updateTaskResult = async ( + { request, locals }: { request: Request; locals: App.Locals }, + operationLog: string, +) => { + console.log(operationLog); + const response = await request.formData(); + const session = await locals.auth.validate(); + + if (!session || !session.user || !session.user.userId) { + return fail(UNAUTHORIZED, { + message: 'ログインしていないか、もしくは、ログイン情報が不正です。', + }); + } + + const userId = session.user.userId; + + try { + const taskId = response.get('taskId') as string; + const submissionStatus = response.get('submissionStatus') as string; + + await crud.updateTaskResult(taskId, submissionStatus, userId); + } catch (error) { + return fail(BAD_REQUEST); + } +}; diff --git a/src/lib/components/SubmissionStatus/SubmissionStatusImage.svelte b/src/lib/components/SubmissionStatus/SubmissionStatusImage.svelte new file mode 100644 index 000000000..e18dd3bab --- /dev/null +++ b/src/lib/components/SubmissionStatus/SubmissionStatusImage.svelte @@ -0,0 +1,28 @@ + + +{imageAlt} +{#if isLoggedIn} +
+
+ {'更新'} +
+ +
+{/if} diff --git a/src/lib/components/TaskList.svelte b/src/lib/components/TaskList.svelte index c81ec4e35..24670a43d 100644 --- a/src/lib/components/TaskList.svelte +++ b/src/lib/components/TaskList.svelte @@ -2,7 +2,6 @@ import { AccordionItem, Accordion, - Img, Table, TableBody, TableBodyCell, @@ -10,14 +9,13 @@ TableHead, TableHeadCell, } from 'flowbite-svelte'; - // @ts-ignore - import ChevronDownOutline from 'flowbite-svelte-icons/ChevronDownOutline.svelte'; import type { TaskResult, TaskResults } from '$lib/types/task'; import type { SubmissionRatios } from '$lib/types/submission'; import ThermometerProgressBar from '$lib/components/ThermometerProgressBar.svelte'; import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte'; + import SubmissionStatusImage from '$lib/components/SubmissionStatus/SubmissionStatusImage.svelte'; import { getBackgroundColorFrom, submission_statuses } from '$lib/services/submission_status'; import { ATCODER_BASE_CONTEST_URL } from '$lib/constants/urls'; import { getContestNameLabel } from '$lib/utils/contest'; @@ -84,6 +82,7 @@ + @@ -98,24 +97,15 @@ {#each taskResults as taskResult} - + updatingModal.openModal(taskResult)} > - {taskResult.submission_status_label_name} - {#if isLoggedIn} -
-
- {'更新'} -
- -
- {/if} +
> { + const taskResultsWithTaskId = workBookTasks.map((workBookTask: WorkBookTaskBase) => + getTaskResultWithErrorHandling(workBookTask.taskId, userId).then((taskResult: TaskResult) => ({ + taskId: workBookTask.taskId, + taskResult: taskResult, + })), + ); + + const taskResultsMap = (await Promise.all(taskResultsWithTaskId)).reduce( + (map, { taskId, taskResult }: { taskId: string; taskResult: TaskResult }) => + map.set(taskId, taskResult), + new Map(), + ); + + return taskResultsMap; +} + +async function getTaskResultWithErrorHandling(taskId: string, userId: string): Promise { + try { + return await getTaskResult(taskId, userId); + } catch (error) { + return await handleTaskResultError(taskId, userId); + } +} + +async function handleTaskResultError(taskId: string, userId: string): Promise { + try { + const task: Tasks = await getTask(taskId); + return await createDefaultTaskResult(userId, task[0]); + } catch (innerError) { + console.error(`Failed to create a default task result for taskId ${taskId}:`, innerError); + throw new Error(`問題id: ${taskId} の作成に失敗しました。`); + } +} + export function createDefaultTaskResult(userId: string, task: Task): TaskResult { const taskResult: TaskResult = { contest_id: task.contest_id, @@ -93,6 +132,7 @@ export function createDefaultTaskResult(userId: string, task: Task): TaskResult return taskResult; } + export async function getTaskResult(slug: string, userId: string) { const task = await getTask(slug); @@ -108,7 +148,7 @@ export async function getTaskResult(slug: string, userId: string) { } const status = statusById.get(taskanswer.status_id); - taskResult.status_id = status.status_id; + taskResult.status_id = status.id; taskResult.status_name = status.status_name; taskResult.submission_status_image_path = status.image_path; taskResult.submission_status_label_name = status.label_name; diff --git a/src/routes/problems/+page.server.ts b/src/routes/problems/+page.server.ts index 6ffdf6a7f..7c053eb24 100644 --- a/src/routes/problems/+page.server.ts +++ b/src/routes/problems/+page.server.ts @@ -1,9 +1,9 @@ -import { fail, type Actions } from '@sveltejs/kit'; +import { type Actions } from '@sveltejs/kit'; import * as crud from '$lib/services/task_results'; import type { TaskResults } from '$lib/types/task'; import { Roles } from '$lib/types/user'; -import { BAD_REQUEST, UNAUTHORIZED } from '$lib/constants/http-response-status-codes'; +import * as action from '$lib/actions/update_task_result'; // 問題一覧ページは、ログインしていなくても閲覧できるようにする export async function load({ locals, url }) { @@ -30,28 +30,9 @@ export async function load({ locals, url }) { } } -// HACK: Actionを切り出すことができれば、問題集の回答状況の更新でほぼそのまま利用できる export const actions = { update: async ({ request, locals }) => { - console.log('problems -> actions -> update'); - const response = await request.formData(); - const session = await locals.auth.validate(); - - if (!session || !session.user || !session.user.userId) { - return fail(UNAUTHORIZED, { - message: 'ログインしていないか、もしくは、ログイン情報が不正です。', - }); - } - - const userId = session.user.userId; - - try { - const taskId = response.get('taskId') as string; - const submissionStatus = response.get('submissionStatus') as string; - - await crud.updateTaskResult(taskId, submissionStatus, userId); - } catch (error) { - return fail(BAD_REQUEST); - } + const operationLog = 'problems -> actions -> update'; + return await action.updateTaskResult({ request, locals }, operationLog); }, } satisfies Actions; diff --git a/src/routes/workbooks/[slug]/+page.server.ts b/src/routes/workbooks/[slug]/+page.server.ts index 22f3f4529..83db2e1b0 100644 --- a/src/routes/workbooks/[slug]/+page.server.ts +++ b/src/routes/workbooks/[slug]/+page.server.ts @@ -1,10 +1,12 @@ -import { error } from '@sveltejs/kit'; +import { error, type Actions } from '@sveltejs/kit'; import { getLoggedInUser, isAdmin, canRead } from '$lib/utils/authorship'; import { Roles } from '$lib/types/user'; import { getWorkbookWithAuthor, parseWorkBookId } from '$lib/utils/workbook'; -import * as taskCrud from '$lib/services/tasks'; +import * as taskResultsCrud from '$lib/services/task_results'; +import type { TaskResult } from '$lib/types/task'; import { BAD_REQUEST, FORBIDDEN } from '$lib/constants/http-response-status-codes'; +import * as action from '$lib/actions/update_task_result'; export async function load({ locals, params }) { const loggedInUser = await getLoggedInUser(locals); @@ -24,8 +26,22 @@ export async function load({ locals, params }) { error(FORBIDDEN, `問題集id: ${params.slug} にアクセスする権限がありません。`); } - // FIXME: ユーザの回答状況を反映させるため、taskResultsに置き換え - const tasks = await taskCrud.getTasksByTaskId(); + const taskResults: Map = await taskResultsCrud.getTaskResultsByTaskId( + workBook.workBookTasks, + loggedInUser?.id as string, + ); - return { loggedInAsAdmin: loggedInAsAdmin, ...workbookWithAuthor, tasks: tasks }; + return { + isLoggedIn: loggedInUser !== null, + loggedInAsAdmin: loggedInAsAdmin, + ...workbookWithAuthor, + taskResults: taskResults, + }; } + +export const actions = { + update: async ({ request, locals }) => { + const operationLog = 'workbook -> actions -> update'; + return await action.updateTaskResult({ request, locals }, operationLog); + }, +} satisfies Actions; diff --git a/src/routes/workbooks/[slug]/+page.svelte b/src/routes/workbooks/[slug]/+page.svelte index c41f02e39..7fd705499 100644 --- a/src/routes/workbooks/[slug]/+page.svelte +++ b/src/routes/workbooks/[slug]/+page.svelte @@ -11,26 +11,31 @@ } from 'flowbite-svelte'; import HeadingOne from '$lib/components/HeadingOne.svelte'; + import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte'; + import SubmissionStatusImage from '$lib/components/SubmissionStatus/SubmissionStatusImage.svelte'; import ExternalLinkWrapper from '$lib/components/ExternalLinkWrapper.svelte'; + import { getBackgroundColorFrom } from '$lib/services/submission_status'; import { getContestUrl } from '$lib/utils/contest'; import { taskUrl } from '$lib/utils/task'; import { getContestNameLabel } from '$lib/utils/contest'; import type { WorkBookTaskBase } from '$lib/types/workbook'; - import type { Task } from '$lib/types/task'; + import type { TaskResult } from '$lib/types/task'; export let data; let workBook = data.workBook; - let workBookTasks: WorkBookTaskBase[] = workBook.workBookTasks; - let tasks = data.tasks; // workBookTasksのtaskIdから問題情報を取得 + let workBookTasks: WorkBookTaskBase[]; + let taskResults: Map; + $: taskResults = data.taskResults; + let isLoggedIn = data.isLoggedIn; // TODO: 関数をutilへ移動させる - const getTask = (taskId: string): Task | undefined => { - return tasks.get(taskId); + const getTaskResult = (taskId: string): TaskResult => { + return taskResults.get(taskId)!; }; const getContestIdFrom = (taskId: string): string => { - return getTask(taskId)?.contest_id as string; + return getTaskResult(taskId)?.contest_id as string; }; const getContestNameFrom = (taskId: string): string => { @@ -39,8 +44,23 @@ }; const getTaskName = (taskId: string): string => { - return getTask(taskId)?.title as string; + return getTaskResult(taskId)?.title as string; }; + + let updatingModal: UpdatingModal; + + // FIXME: clickを1回実行するとactionsが2回実行されてしまう。原因と修正方法が分かっていない。 + function handleClick(taskId: string) { + updatingModal.openModal(getTaskResult(taskId)); + } + + $: if (taskResults && workBook && Array.isArray(workBook.workBookTasks)) { + workBookTasks = workBook.workBookTasks; + } else if (!taskResults) { + console.error('Not found taskResults.'); + } else if (!workBook || !Array.isArray(workBook.workBookTasks)) { + console.error('Not found workBook or workBook.workBookTasks is not an array.'); + }
@@ -79,8 +99,19 @@ {#each workBookTasks as workBookTask} - - {'準備中'} + + handleClick(workBookTask.taskId)} + > + +
+ + {:else} {'問題を1問以上登録してください。'} {/if}