Skip to content
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

✨ Enable to show and update submission statuses in detail page of workbook (#779) #966

Merged
merged 5 commits into from
Jul 14, 2024
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
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
Loading