Skip to content

Commit

Permalink
Merge pull request #966 from AtCoder-NoviSteps/#779
Browse files Browse the repository at this point in the history
✨ Enable to show and update submission statuses in detail page of workbook (#779)
  • Loading branch information
KATO-Hiro authored Jul 14, 2024
2 parents d64ceff + 1b80443 commit 7d3661b
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 55 deletions.
31 changes: 31 additions & 0 deletions src/lib/actions/update_task_result.ts
Original file line number Diff line number Diff line change
@@ -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);
}
};
28 changes: 28 additions & 0 deletions src/lib/components/SubmissionStatus/SubmissionStatusImage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import { Img } from 'flowbite-svelte';
// @ts-ignore
import ChevronDownOutline from 'flowbite-svelte-icons/ChevronDownOutline.svelte';
import type { TaskResult } from '$lib/types/task';
export let taskResult: TaskResult;
export let isLoggedIn: boolean;
let imagePath = '';
let imageAlt = '';
$: if (taskResult) {
imagePath = `../../${taskResult.submission_status_image_path}`;
imageAlt = taskResult.submission_status_label_name;
}
</script>

<Img src={imagePath} alt={imageAlt} class="h-8 w-8" />
{#if isLoggedIn}
<div class="flex flex-col items-center ml-2 md:ml-4 text-xs">
<div class="mb-1">
{'更新'}
</div>
<ChevronDownOutline class="w-3 h-3 text-primary-600 dark:text-white inline" />
</div>
{/if}
24 changes: 7 additions & 17 deletions src/lib/components/TaskList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,20 @@
import {
AccordionItem,
Accordion,
Img,
Table,
TableBody,
TableBodyCell,
TableBodyRow,
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';
Expand Down Expand Up @@ -84,6 +82,7 @@
</div>
</span>

<!-- FIXME: clickを1回実行するとactionsが2回実行されてしまう。原因と修正方法が分かっていない。 -->
<!-- TODO: 「編集」ボタンを押したときに問題情報を更新できるようにする -->
<!-- TODO: 問題が多くなってきたら、ページネーションを導入する -->
<!-- TODO: 回答状況に応じて、フィルタリングできるようにする -->
Expand All @@ -98,24 +97,15 @@
</TableHead>
<TableBody tableBodyClass="divide-y">
{#each taskResults as taskResult}
<TableBodyRow class={getBackgroundColorFrom(taskResult.status_name)}>
<TableBodyRow
key={taskResult.contest_id + taskResult.task_id}
class={getBackgroundColorFrom(taskResult.status_name)}
>
<TableBodyCell
class="p-3 pl-3 md:pl-6 flex items-center"
on:click={() => updatingModal.openModal(taskResult)}
>
<Img
src="../../{taskResult.submission_status_image_path}"
alt={taskResult.submission_status_label_name}
class="h-8 w-8"
/>
{#if isLoggedIn}
<div class="flex flex-col items-center ml-2 md:ml-4 text-xs">
<div class="mb-1">
{'更新'}
</div>
<ChevronDownOutline class="w-3 h-3 text-primary-600 dark:text-white inline" />
</div>
{/if}
<SubmissionStatusImage {taskResult} {isLoggedIn} />
</TableBodyCell>
<TableBodyCell>
<a
Expand Down
42 changes: 41 additions & 1 deletion src/lib/services/task_results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getTasks, getTask } from '$lib/services/tasks';
import * as answer_crud from './answers';
import type { TaskResult, TaskResults, Tasks } from '$lib/types/task';
import type { Task } from '$lib/types/task';
import type { WorkBookTaskBase } from '$lib/types/workbook';
import { NOT_FOUND } from '$lib/constants/http-response-status-codes';
import { default as db } from '$lib/server/database';
import { getSubmissionStatusMapWithId, getSubmissionStatusMapWithName } from './submission_status';
Expand Down Expand Up @@ -75,6 +76,44 @@ export async function getTaskResultsOnlyResultExists(userId: string): Promise<Ta
return Array.from(taskResultsMap.get(userId).values());
}

export async function getTaskResultsByTaskId(
workBookTasks: WorkBookTaskBase[],
userId: string,
): Promise<Map<string, TaskResult>> {
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<string, TaskResult>(),
);

return taskResultsMap;
}

async function getTaskResultWithErrorHandling(taskId: string, userId: string): Promise<TaskResult> {
try {
return await getTaskResult(taskId, userId);
} catch (error) {
return await handleTaskResultError(taskId, userId);
}
}

async function handleTaskResultError(taskId: string, userId: string): Promise<TaskResult> {
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,
Expand All @@ -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);

Expand All @@ -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;
Expand Down
27 changes: 4 additions & 23 deletions src/routes/problems/+page.server.ts
Original file line number Diff line number Diff line change
@@ -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 }) {
Expand All @@ -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;
26 changes: 21 additions & 5 deletions src/routes/workbooks/[slug]/+page.server.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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<string, TaskResult> = 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;
51 changes: 42 additions & 9 deletions src/routes/workbooks/[slug]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, TaskResult>;
$: 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 => {
Expand All @@ -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.');
}
</script>

<div class="container mx-auto w-5/6 space-y-4">
Expand Down Expand Up @@ -79,8 +99,19 @@
</TableHead>
<TableBody tableBodyClass="divide-y">
{#each workBookTasks as workBookTask}
<TableBodyRow>
<TableBodyCell>{'準備中'}</TableBodyCell>
<TableBodyRow
key={getContestIdFrom(workBookTask.taskId) + workBookTask.taskId}
class={getBackgroundColorFrom(getTaskResult(workBookTask.taskId).status_name)}
>
<TableBodyCell
class="p-3 pl-3 md:pl-6 flex items-center"
on:click={() => handleClick(workBookTask.taskId)}
>
<SubmissionStatusImage
taskResult={getTaskResult(workBookTask.taskId)}
{isLoggedIn}
/>
</TableBodyCell>
<TableBodyCell>
<div class="truncate">
<ExternalLinkWrapper
Expand All @@ -102,6 +133,8 @@
</TableBody>
</Table>
</div>

<UpdatingModal bind:this={updatingModal} {isLoggedIn} />
{:else}
{'問題を1問以上登録してください。'}
{/if}
Expand Down

0 comments on commit 7d3661b

Please sign in to comment.