Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.

Commit bcda12e

Browse files
committed
feat: assign tasks to groups
1 parent eb3f359 commit bcda12e

File tree

9 files changed

+218
-33
lines changed

9 files changed

+218
-33
lines changed

.prettierrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"singleQuote": true,
44
"trailingComma": "none",
55
"printWidth": 100,
6-
"endOfLine": "crlf",
6+
"endOfLine": "lf",
77
"plugins": ["prettier-plugin-svelte"],
88
"overrides": [
99
{

messages/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
"task_form_task_file_description": "Please upload a file with the task description.",
5757
"task_form_task_language_label": "Language of the solution",
5858
"task_form_submit": "Submit",
59+
"task_assign_groups_form_title": "Assign groups to the task",
60+
"task_assign_group_select_title": "Available groups",
61+
"task_assign_groups_submit": "Assign groups",
5962

6063
"submissions_table_title": "A list of all submissions made by the user.",
6164
"submissions_task_link_label": "Task link",

messages/pl.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
"task_form_task_file_description": "Proszę przesłać plik z opisem zadania.",
5757
"task_form_task_language_label": "Język zadania",
5858
"task_form_submit": "Wyślij",
59+
"task_assign_groups_form_title": "Przypisz grupy do zadania",
60+
"task_assign_group_select_title": "Dostępne grupy",
61+
"task_assign_groups_submit": "Przypisz",
5962

6063
"submissions_table_title": "Lista wszystkich zgłoszeń użytkownika.",
6164
"submissions_task_link_label": "Link do zadania",

src/lib/components/tasks/EditTaskDialog.svelte

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11
<script lang="ts">
2-
import type { TaskData, UserData } from '$lib/backendSchemas';
2+
import type { GroupData, TaskData, UserData } from '$lib/backendSchemas';
33
import * as AlertDialog from '$components/ui/alert-dialog/index.js';
44
import { buttonVariants } from '$components/ui/button/index.js';
55
import * as m from '$lib/paraglide/messages.js';
66
import { fileProxy, superForm, type Infer, type SuperValidated } from 'sveltekit-superforms';
7-
import { editTaskSchema, type EditTaskSchema } from './formSchemas';
7+
import {
8+
assignTaskToGroupsSchema,
9+
editTaskSchema,
10+
type AssingTaskToGroupsSchema,
11+
type EditTaskSchema
12+
} from './formSchemas';
813
import * as Form from '$components/ui/form';
914
import { zodClient } from 'sveltekit-superforms/adapters';
1015
import Input from '../ui/input/input.svelte';
1116
import Separator from '../ui/separator/separator.svelte';
1217
import Label from '../ui/label/label.svelte';
18+
import * as Select from '$lib/components/ui/select/index.js';
1319
1420
let {
15-
localUser,
1621
taskData,
17-
editTaskForm
22+
userGroups,
23+
editTaskForm,
24+
assingTaskToGroupsForm
1825
}: {
19-
localUser: UserData;
2026
taskData: Omit<TaskData, 'description_url'>;
27+
userGroups: GroupData[];
2128
editTaskForm: SuperValidated<Infer<EditTaskSchema>>;
29+
assingTaskToGroupsForm: SuperValidated<Infer<AssingTaskToGroupsSchema>>;
2230
} = $props();
2331
24-
const form = superForm(editTaskForm, {
32+
const editForm = superForm(editTaskForm, {
2533
validators: zodClient(editTaskSchema),
2634
resetForm: false,
2735
onResult: ({ result: { type } }) => {
@@ -32,13 +40,31 @@
3240
}
3341
});
3442
35-
const { form: formData, message, enhance } = form;
43+
const assignToGroupsForm = superForm(assingTaskToGroupsForm, {
44+
validators: zodClient(assignTaskToGroupsSchema),
45+
resetForm: false,
46+
onResult: ({ result: { type } }) => {
47+
if (type === 'success') {
48+
open = false;
49+
location.reload();
50+
}
51+
}
52+
});
53+
54+
const { form: editTaskFormData, message: editTaskMessage, enhance: editTaskEnhance } = editForm;
55+
const {
56+
form: assignToGroupsFormData,
57+
message: assignToGroupsMessage,
58+
enhance: assignToGroupsEnhance
59+
} = assignToGroupsForm;
60+
61+
$editTaskFormData.id = taskData.id;
62+
$editTaskFormData.title = taskData.title;
63+
$editTaskFormData.archive = null;
3664
37-
$formData.id = taskData.id;
38-
$formData.title = taskData.title;
39-
$formData.archive = null;
65+
$assignToGroupsFormData.taskId = taskData.id;
4066
41-
const file = fileProxy(form, 'archive');
67+
const file = fileProxy(editForm, 'archive');
4268
4369
let open = $state(false);
4470
</script>
@@ -51,25 +77,25 @@
5177
<AlertDialog.Header>
5278
<AlertDialog.Title>{m.edit_task_title()}</AlertDialog.Title>
5379
<AlertDialog.Description>
54-
<form enctype="multipart/form-data" action="?/editTask" method="POST" use:enhance>
55-
<Form.Field {form} name="id" hidden>
80+
<form enctype="multipart/form-data" action="?/editTask" method="POST" use:editTaskEnhance>
81+
<Form.Field form={editForm} name="id" hidden>
5682
<Form.Control>
5783
{#snippet children({ props })}
58-
<Input type="number" {...props} bind:value={$formData.id} />
84+
<Input type="number" {...props} bind:value={$editTaskFormData.id} />
5985
{/snippet}
6086
</Form.Control>
6187
<Form.FieldErrors />
6288
</Form.Field>
63-
<Form.Field {form} name="title">
89+
<Form.Field form={editForm} name="title">
6490
<Form.Control>
6591
{#snippet children({ props })}
6692
<Form.Label>{m.edit_task_title_label()}</Form.Label>
67-
<Input {...props} bind:value={$formData.title} />
93+
<Input {...props} bind:value={$editTaskFormData.title} />
6894
{/snippet}
6995
</Form.Control>
7096
<Form.FieldErrors />
7197
</Form.Field>
72-
<Form.Field {form} name="archive">
98+
<Form.Field form={editForm} name="archive">
7399
<Form.Control>
74100
{#snippet children({ props })}
75101
<Label for="archive">{m.task_form_task_file_label()}</Label>
@@ -85,8 +111,8 @@
85111
<Form.Description>{m.task_form_task_file_description()}</Form.Description>
86112
<Form.FieldErrors />
87113
</Form.Field>
88-
{#if $message}
89-
<p class="text-destructive my-2 text-sm font-medium">{$message}</p>
114+
{#if $editTaskMessage}
115+
<p class="text-destructive my-2 text-sm font-medium">{$editTaskMessage}</p>
90116
{/if}
91117
<Separator class="my-4" />
92118
<div class="flex-row w-full">
@@ -96,6 +122,49 @@
96122
<Form.Button type="submit">{m.edit_profile_submit()}</Form.Button>
97123
</div>
98124
</form>
125+
<Separator class="my-4" />
126+
<h2 class="font-semibold text-lg text-foreground">
127+
{m.task_assign_groups_form_title()}
128+
</h2>
129+
<form action="?/assignGroups" method="POST" use:assignToGroupsEnhance>
130+
<Form.Field form={assignToGroupsForm} name="taskId" hidden>
131+
<Form.Control>
132+
{#snippet children({ props })}
133+
<Input type="number" {...props} bind:value={$assignToGroupsFormData.taskId} />
134+
{/snippet}
135+
</Form.Control>
136+
<Form.FieldErrors />
137+
</Form.Field>
138+
<Form.Field form={assignToGroupsForm} name="groupIds" class="my-4">
139+
<Form.Control>
140+
{#snippet children({ props })}
141+
<Select.Root
142+
type="multiple"
143+
bind:value={$assignToGroupsFormData.groupIds}
144+
{...props}
145+
>
146+
<Select.Trigger class="w-[180px]">
147+
{m.task_assign_group_select_title()}
148+
</Select.Trigger>
149+
<Select.Content>
150+
<Select.Group>
151+
{#each userGroups as userGroup}
152+
<Select.Item value={userGroup.id.toString()} label={userGroup.name}
153+
>{userGroup.name}</Select.Item
154+
>
155+
{/each}
156+
</Select.Group>
157+
</Select.Content>
158+
</Select.Root>
159+
{/snippet}
160+
</Form.Control>
161+
<Form.FieldErrors />
162+
</Form.Field>
163+
{#if $assignToGroupsMessage}
164+
<p class="text-destructive my-2 text-sm font-medium">{$assignToGroupsMessage}</p>
165+
{/if}
166+
<Form.Button type="submit">{m.task_assign_groups_submit()}</Form.Button>
167+
</form>
99168
</AlertDialog.Description>
100169
</AlertDialog.Header>
101170
<AlertDialog.Footer>

src/lib/components/tasks/TaskView.svelte

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,33 @@
33
import * as m from '$lib/paraglide/messages.js';
44
import type { Infer, SuperValidated } from 'sveltekit-superforms';
55
import { type UploadTaskSolutionSchema } from './solutions/formSchema';
6-
import { UserRole, type LanguageConfig, type TaskData, type UserData } from '$lib/backendSchemas';
6+
import {
7+
UserRole,
8+
type GroupData,
9+
type LanguageConfig,
10+
type TaskData,
11+
type UserData
12+
} from '$lib/backendSchemas';
713
import EditTaskDialog from './EditTaskDialog.svelte';
8-
import type { EditTaskSchema } from './formSchemas';
14+
import type { AssingTaskToGroupsSchema, EditTaskSchema } from './formSchemas';
915
1016
let {
1117
localUser,
1218
task,
1319
uploadSolutionForm,
1420
editTaskForm,
21+
assingTaskToGroupsForm,
22+
userGroups,
1523
availableLanguages
1624
}: {
1725
localUser: UserData;
1826
task: Omit<TaskData, 'description_url'> & {
1927
description_file: Promise<ArrayBuffer>;
2028
};
2129
editTaskForm: SuperValidated<Infer<EditTaskSchema>>;
30+
assingTaskToGroupsForm: SuperValidated<Infer<AssingTaskToGroupsSchema>>;
2231
uploadSolutionForm: SuperValidated<Infer<UploadTaskSolutionSchema>>;
32+
userGroups: GroupData[];
2333
availableLanguages: LanguageConfig[];
2434
} = $props();
2535
@@ -28,13 +38,20 @@
2838
task_id: task.id,
2939
availableLanguages: availableLanguages
3040
};
41+
42+
function isAllowedToEdit() {
43+
return (
44+
(localUser.role === UserRole.Teacher && task.created_by === localUser.id) ||
45+
localUser.role === UserRole.Admin
46+
);
47+
}
3148
</script>
3249

3350
<div class="container mb-12 flex flex-col flex-1">
3451
<div class="flex justify-between p-4 items-center">
3552
<h1 class="text-2xl font-bold my-4">{task.title}</h1>
36-
{#if localUser.role !== UserRole.Student}
37-
<EditTaskDialog taskData={task} {editTaskForm} {localUser} />
53+
{#if isAllowedToEdit()}
54+
<EditTaskDialog taskData={task} {editTaskForm} {assingTaskToGroupsForm} {userGroups} />
3855
{/if}
3956
</div>
4057
<div class="flex-1 flex overflow-hidden">

src/lib/components/tasks/formSchemas.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ export const editTaskSchema = z.object({
4949
})
5050
});
5151

52+
export const assignTaskToGroupsSchema = z.object({
53+
taskId: z.number().int().positive(),
54+
groupIds: z
55+
.array(z.string())
56+
.refine(
57+
(groupIds) => groupIds.every((id) => !isNaN(Number(id)) && Number.isInteger(Number(id))),
58+
{
59+
message: 'All groupIds must be string representations of integers'
60+
}
61+
)
62+
});
63+
5264
const requiredFolders = ['input', 'output'];
5365
const inFileRegex = /^\/\d+.in$/;
5466
const outFileRegex = /^\/\d+.out$/;
@@ -122,3 +134,4 @@ function verifyTaskInOut(loadedZip: JSZip, mainFolderPath: string) {
122134

123135
export type CreateTaskSchema = typeof createTaskSchema;
124136
export type EditTaskSchema = typeof editTaskSchema;
137+
export type AssingTaskToGroupsSchema = typeof assignTaskToGroupsSchema;

src/lib/components/users/formSchemas.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export const editPasswordSchema = z
1818
currentPassword: z.string().nonempty(m.form_schema_password_required_error_message()),
1919
newPassword: z
2020
.string()
21-
.min(6)
21+
.min(8)
22+
.max(50)
2223
.regex(passwordValidationRegex, m.form_schema_password_regex_error_message()),
2324
confirmPassword: z.string().nonempty(m.form_schema_password_required_error_message())
2425
})

0 commit comments

Comments
 (0)