Merge remote-tracking branch 'origin/dev' into feat/indieningen-kunnen-posten-en-bekijken-#194

# Conflicts:
#	backend/tests/setup-tests.ts
This commit is contained in:
Gerald Schmittinger 2025-04-16 16:31:18 +02:00
commit dd2cdf3fe9
46 changed files with 1670 additions and 123 deletions

View file

@ -17,6 +17,7 @@
"test:unit": "vitest --run"
},
"dependencies": {
"@dwengo-1/common": "^0.1.1",
"@tanstack/react-query": "^5.69.0",
"@tanstack/vue-query": "^5.69.0",
"axios": "^1.8.2",

View file

@ -2,8 +2,8 @@ import { BaseController } from "./base-controller";
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import type { StudentsResponse } from "./students";
import type { AssignmentsResponse } from "./assignments";
import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
import type { TeachersResponse } from "@/controllers/teachers.ts";
import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations.ts";
export interface ClassesResponse {
classes: ClassDTO[] | string[];
@ -13,14 +13,6 @@ export interface ClassResponse {
class: ClassDTO;
}
export interface TeacherInvitationsResponse {
invites: TeacherInvitationDTO[];
}
export interface TeacherInvitationResponse {
invite: TeacherInvitationDTO;
}
export class ClassController extends BaseController {
constructor() {
super("class");

View file

@ -36,11 +36,11 @@ export class GroupController extends BaseController {
return this.put<GroupResponse>(`/${num}`, data);
}
async getSubmissions(groupNumber: number, full = true): Promise<SubmissionsResponse> {
return this.get<SubmissionsResponse>(`/${groupNumber}/submissions`, { full });
async getSubmissions(num: number, full = true): Promise<SubmissionsResponse> {
return this.get<SubmissionsResponse>(`/${num}/submissions`, { full });
}
async getQuestions(groupNumber: number, full = true): Promise<QuestionsResponse> {
return this.get<QuestionsResponse>(`/${groupNumber}/questions`, { full });
async getQuestions(num: number, full = true): Promise<QuestionsResponse> {
return this.get<QuestionsResponse>(`/${num}/questions`, { full });
}
}

View file

@ -15,13 +15,14 @@ export class LearningPathController extends BaseController {
async getBy(
hruid: string,
language: Language,
options?: { forGroup?: string; forStudent?: string },
forGroup?: { forGroup: number, assignmentNo: number, classId: string },
): Promise<LearningPath> {
const dtos = await this.get<LearningPathDTO[]>("/", {
hruid,
language,
forGroup: options?.forGroup,
forStudent: options?.forStudent,
forGroup: forGroup?.forGroup,
assignmentNo: forGroup?.assignmentNo,
classId: forGroup?.classId
});
return LearningPath.fromDTO(single(dtos));
}

View file

@ -1,5 +1,6 @@
import { BaseController } from "./base-controller";
import type { SubmissionDTO, SubmissionDTOId } from "@dwengo-1/common/interfaces/submission";
import type {Language} from "@dwengo-1/common/util/language";
export interface SubmissionsResponse {
submissions: SubmissionDTO[] | SubmissionDTOId[];
@ -10,19 +11,35 @@ export interface SubmissionResponse {
}
export class SubmissionController extends BaseController {
constructor(classid: string, assignmentNumber: number, groupNumber: number) {
super(`class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}`);
constructor(hruid: string) {
super(`learningObject/${hruid}/submissions`);
}
async getAll(full = true): Promise<SubmissionsResponse> {
return this.get<SubmissionsResponse>(`/`, { full });
async getAll(
language: Language, version: number, classId: string, assignmentId: number, groupId?: number, full = true
): Promise<SubmissionsResponse> {
return this.get<SubmissionsResponse>(
`/`,
{ language, version, classId, assignmentId, groupId, full }
);
}
async getByNumber(submissionNumber: number): Promise<SubmissionResponse> {
return this.get<SubmissionResponse>(`/${submissionNumber}`);
async getByNumber(
language: Language,
version: number,
classId: string,
assignmentId: number,
groupId: number,
submissionNumber: number
): Promise<SubmissionResponse> {
return this.get<SubmissionResponse>(
`/${submissionNumber}`,
{ language, version, classId, assignmentId, groupId },
);
}
async createSubmission(data: unknown): Promise<SubmissionResponse> {
async createSubmission(data: SubmissionDTO): Promise<SubmissionResponse> {
return this.post<SubmissionResponse>(`/`, data);
}

View file

@ -0,0 +1,36 @@
import { BaseController } from "@/controllers/base-controller.ts";
import type { TeacherInvitationData, TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
export interface TeacherInvitationsResponse {
invitations: TeacherInvitationDTO[];
}
export interface TeacherInvitationResponse {
invitation: TeacherInvitationDTO;
}
export class TeacherInvitationController extends BaseController {
constructor() {
super("teachers/invitations");
}
async getAll(username: string, sent: boolean): Promise<TeacherInvitationsResponse> {
return this.get<TeacherInvitationsResponse>(`/${username}`, { sent });
}
async getBy(data: TeacherInvitationData): Promise<TeacherInvitationResponse> {
return this.get<TeacherInvitationResponse>(`/${data.sender}/${data.receiver}/${data.class}`);
}
async create(data: TeacherInvitationData): Promise<TeacherInvitationResponse> {
return this.post<TeacherInvitationResponse>("/", data);
}
async remove(data: TeacherInvitationData): Promise<TeacherInvitationResponse> {
return this.delete<TeacherInvitationResponse>(`/${data.sender}/${data.receiver}/${data.class}`);
}
async respond(data: TeacherInvitationData): Promise<TeacherInvitationResponse> {
return this.put<TeacherInvitationResponse>("/", data);
}
}

View file

@ -0,0 +1,188 @@
import { AssignmentController, type AssignmentResponse, type AssignmentsResponse } from "@/controllers/assignments";
import type { QuestionsResponse } from "@/controllers/questions";
import type { SubmissionsResponse } from "@/controllers/submissions";
import {
useMutation,
useQuery,
useQueryClient,
type UseMutationReturnType,
type UseQueryReturnType,
} from "@tanstack/vue-query";
import { computed, toValue, type MaybeRefOrGetter } from "vue";
import { groupsQueryKey, invalidateAllGroupKeys } from "./groups";
import type { GroupsResponse } from "@/controllers/groups";
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
import type { QueryClient } from "@tanstack/react-query";
import { invalidateAllSubmissionKeys } from "./submissions";
function assignmentsQueryKey(classid: string, full: boolean) {
return ["assignments", classid, full];
}
function assignmentQueryKey(classid: string, assignmentNumber: number) {
return ["assignment", classid, assignmentNumber];
}
function assignmentSubmissionsQueryKey(classid: string, assignmentNumber: number, full: boolean) {
return ["assignment-submissions", classid, assignmentNumber, full];
}
function assignmentQuestionsQueryKey(classid: string, assignmentNumber: number, full: boolean) {
return ["assignment-questions", classid, assignmentNumber, full];
}
export async function invalidateAllAssignmentKeys(
queryClient: QueryClient,
classid?: string,
assignmentNumber?: number,
) {
const keys = ["assignment", "assignment-submissions", "assignment-questions"];
for (const key of keys) {
const queryKey = [key, classid, assignmentNumber].filter((arg) => arg !== undefined);
await queryClient.invalidateQueries({ queryKey: queryKey });
}
await queryClient.invalidateQueries({ queryKey: ["assignments", classid].filter((arg) => arg !== undefined) });
}
function checkEnabled(
classid: string | undefined,
assignmentNumber: number | undefined,
groupNumber: number | undefined,
): boolean {
return Boolean(classid) && !isNaN(Number(groupNumber)) && !isNaN(Number(assignmentNumber));
}
function toValues(
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
groupNumber: MaybeRefOrGetter<number | undefined>,
full: MaybeRefOrGetter<boolean>,
) {
return { cid: toValue(classid), an: toValue(assignmentNumber), gn: toValue(groupNumber), f: toValue(full) };
}
export function useAssignmentsQuery(
classid: MaybeRefOrGetter<string | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<AssignmentsResponse, Error> {
const { cid, f } = toValues(classid, 1, 1, full);
return useQuery({
queryKey: computed(() => assignmentsQueryKey(cid!, f)),
queryFn: async () => new AssignmentController(cid!).getAll(f),
enabled: () => checkEnabled(cid, 1, 1),
});
}
export function useAssignmentQuery(
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
): UseQueryReturnType<AssignmentsResponse, Error> {
const { cid, an } = toValues(classid, assignmentNumber, 1, true);
return useQuery({
queryKey: computed(() => assignmentQueryKey(cid!, an!)),
queryFn: async () => new AssignmentController(cid!).getByNumber(an!),
enabled: () => checkEnabled(cid, an, 1),
});
}
export function useCreateAssignmentMutation(): UseMutationReturnType<
AssignmentResponse,
Error,
{ cid: string; data: AssignmentDTO },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ cid, data }) => new AssignmentController(cid).createAssignment(data),
onSuccess: async (_) => {
await queryClient.invalidateQueries({ queryKey: ["assignments"] });
},
});
}
export function useDeleteAssignmentMutation(): UseMutationReturnType<
AssignmentResponse,
Error,
{ cid: string; an: number },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ cid, an }) => new AssignmentController(cid).deleteAssignment(an),
onSuccess: async (response) => {
const cid = response.assignment.within;
const an = response.assignment.id;
await invalidateAllAssignmentKeys(queryClient, cid, an);
await invalidateAllGroupKeys(queryClient, cid, an);
await invalidateAllSubmissionKeys(queryClient, cid, an);
},
});
}
export function useUpdateAssignmentMutation(): UseMutationReturnType<
AssignmentResponse,
Error,
{ cid: string; an: number; data: Partial<AssignmentDTO> },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ cid, an, data }) => new AssignmentController(cid).updateAssignment(an, data),
onSuccess: async (response) => {
const cid = response.assignment.within;
const an = response.assignment.id;
await invalidateAllGroupKeys(queryClient, cid, an);
await queryClient.invalidateQueries({ queryKey: ["assignments"] });
},
});
}
export function useAssignmentSubmissionsQuery(
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
groupNumber: MaybeRefOrGetter<number | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<SubmissionsResponse, Error> {
const { cid, an, gn, f } = toValues(classid, assignmentNumber, groupNumber, full);
return useQuery({
queryKey: computed(() => assignmentSubmissionsQueryKey(cid!, an!, f)),
queryFn: async () => new AssignmentController(cid!).getSubmissions(gn!, f),
enabled: () => checkEnabled(cid, an, gn),
});
}
export function useAssignmentQuestionsQuery(
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
groupNumber: MaybeRefOrGetter<number | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<QuestionsResponse, Error> {
const { cid, an, gn, f } = toValues(classid, assignmentNumber, groupNumber, full);
return useQuery({
queryKey: computed(() => assignmentQuestionsQueryKey(cid!, an!, f)),
queryFn: async () => new AssignmentController(cid!).getQuestions(gn!, f),
enabled: () => checkEnabled(cid, an, gn),
});
}
export function useAssignmentGroupsQuery(
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
groupNumber: MaybeRefOrGetter<number | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<GroupsResponse, Error> {
const { cid, an, gn, f } = toValues(classid, assignmentNumber, groupNumber, full);
return useQuery({
queryKey: computed(() => groupsQueryKey(cid!, an!, f)),
queryFn: async () => new AssignmentController(cid!).getQuestions(gn!, f),
enabled: () => checkEnabled(cid, an, gn),
});
}

View file

@ -0,0 +1,224 @@
import { ClassController, type ClassesResponse, type ClassResponse } from "@/controllers/classes";
import type { StudentsResponse } from "@/controllers/students";
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
type UseMutationReturnType,
type UseQueryReturnType,
} from "@tanstack/vue-query";
import { computed, toValue, type MaybeRefOrGetter } from "vue";
import { invalidateAllAssignmentKeys } from "./assignments";
import { invalidateAllGroupKeys } from "./groups";
import { invalidateAllSubmissionKeys } from "./submissions";
const classController = new ClassController();
/* Query cache keys */
function classesQueryKey(full: boolean) {
return ["classes", full];
}
function classQueryKey(classid: string) {
return ["class", classid];
}
function classStudentsKey(classid: string, full: boolean) {
return ["class-students", classid, full];
}
function classTeachersKey(classid: string, full: boolean) {
return ["class-teachers", classid, full];
}
function classTeacherInvitationsKey(classid: string, full: boolean) {
return ["class-teacher-invitations", classid, full];
}
function classAssignmentsKey(classid: string, full: boolean) {
return ["class-assignments", classid, full];
}
export async function invalidateAllClassKeys(queryClient: QueryClient, classid?: string) {
const keys = ["class", "class-students", "class-teachers", "class-teacher-invitations", "class-assignments"];
for (const key of keys) {
const queryKey = [key, classid].filter((arg) => arg !== undefined);
await queryClient.invalidateQueries({ queryKey: queryKey });
}
await queryClient.invalidateQueries({ queryKey: ["classes"] });
}
/* Queries */
export function useClassesQuery(full: MaybeRefOrGetter<boolean> = true): UseQueryReturnType<ClassesResponse, Error> {
return useQuery({
queryKey: computed(() => classesQueryKey(toValue(full))),
queryFn: async () => classController.getAll(toValue(full)),
});
}
export function useClassQuery(id: MaybeRefOrGetter<string | undefined>): UseQueryReturnType<ClassResponse, Error> {
return useQuery({
queryKey: computed(() => classQueryKey(toValue(id)!)),
queryFn: async () => classController.getById(toValue(id)!),
enabled: () => Boolean(toValue(id)),
});
}
export function useCreateClassMutation(): UseMutationReturnType<ClassResponse, Error, ClassDTO, unknown> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data) => classController.createClass(data),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["classes"] });
},
});
}
export function useDeleteClassMutation(): UseMutationReturnType<ClassResponse, Error, string, unknown> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id) => classController.deleteClass(id),
onSuccess: async (data) => {
await invalidateAllClassKeys(queryClient, data.class.id);
await invalidateAllAssignmentKeys(queryClient, data.class.id);
await invalidateAllGroupKeys(queryClient, data.class.id);
await invalidateAllSubmissionKeys(queryClient, data.class.id);
},
});
}
export function useUpdateClassMutation(): UseMutationReturnType<
ClassResponse,
Error,
{ cid: string; data: Partial<ClassDTO> },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ cid, data }) => classController.updateClass(cid, data),
onSuccess: async (data) => {
await invalidateAllClassKeys(queryClient, data.class.id);
await invalidateAllAssignmentKeys(queryClient, data.class.id);
await invalidateAllGroupKeys(queryClient, data.class.id);
await invalidateAllSubmissionKeys(queryClient, data.class.id);
},
});
}
export function useClassStudentsQuery(
id: MaybeRefOrGetter<string | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<StudentsResponse, Error> {
return useQuery({
queryKey: computed(() => classStudentsKey(toValue(id)!, toValue(full))),
queryFn: async () => classController.getStudents(toValue(id)!, toValue(full)),
enabled: () => Boolean(toValue(id)),
});
}
export function useClassAddStudentMutation(): UseMutationReturnType<
ClassResponse,
Error,
{ id: string; username: string },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, username }) => classController.addStudent(id, username),
onSuccess: async (data) => {
await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) });
await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, true) });
await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, false) });
},
});
}
export function useClassDeleteStudentMutation(): UseMutationReturnType<
ClassResponse,
Error,
{ id: string; username: string },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, username }) => classController.deleteStudent(id, username),
onSuccess: async (data) => {
await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) });
await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, true) });
await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, false) });
},
});
}
export function useClassTeachersQuery(
id: MaybeRefOrGetter<string | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<StudentsResponse, Error> {
return useQuery({
queryKey: computed(() => classTeachersKey(toValue(id)!, toValue(full))),
queryFn: async () => classController.getTeachers(toValue(id)!, toValue(full)),
enabled: () => Boolean(toValue(id)),
});
}
export function useClassAddTeacherMutation(): UseMutationReturnType<
ClassResponse,
Error,
{ id: string; username: string },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, username }) => classController.addTeacher(id, username),
onSuccess: async (data) => {
await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) });
await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id, true) });
await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id, false) });
},
});
}
export function useClassDeleteTeacherMutation(): UseMutationReturnType<
ClassResponse,
Error,
{ id: string; username: string },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, username }) => classController.deleteTeacher(id, username),
onSuccess: async (data) => {
await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) });
await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id, true) });
await queryClient.invalidateQueries({ queryKey: classTeachersKey(data.class.id, false) });
},
});
}
export function useClassTeacherInvitationsQuery(
id: MaybeRefOrGetter<string | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<StudentsResponse, Error> {
return useQuery({
queryKey: computed(() => classTeacherInvitationsKey(toValue(id)!, toValue(full))),
queryFn: async () => classController.getTeacherInvitations(toValue(id)!, toValue(full)),
enabled: () => Boolean(toValue(id)),
});
}
export function useClassAssignmentsQuery(
id: MaybeRefOrGetter<string | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<StudentsResponse, Error> {
return useQuery({
queryKey: computed(() => classAssignmentsKey(toValue(id)!, toValue(full))),
queryFn: async () => classController.getAssignments(toValue(id)!, toValue(full)),
enabled: () => Boolean(toValue(id)),
});
}

View file

@ -0,0 +1,191 @@
import type { ClassesResponse } from "@/controllers/classes";
import { GroupController, type GroupResponse, type GroupsResponse } from "@/controllers/groups";
import type { QuestionsResponse } from "@/controllers/questions";
import type { SubmissionsResponse } from "@/controllers/submissions";
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
type UseMutationReturnType,
type UseQueryReturnType,
} from "@tanstack/vue-query";
import { computed, toValue, type MaybeRefOrGetter } from "vue";
import { invalidateAllAssignmentKeys } from "./assignments";
import { invalidateAllSubmissionKeys } from "./submissions";
export function groupsQueryKey(classid: string, assignmentNumber: number, full: boolean) {
return ["groups", classid, assignmentNumber, full];
}
function groupQueryKey(classid: string, assignmentNumber: number, groupNumber: number) {
return ["group", classid, assignmentNumber, groupNumber];
}
function groupSubmissionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number, full: boolean) {
return ["group-submissions", classid, assignmentNumber, groupNumber, full];
}
function groupQuestionsQueryKey(classid: string, assignmentNumber: number, groupNumber: number, full: boolean) {
return ["group-questions", classid, assignmentNumber, groupNumber, full];
}
export async function invalidateAllGroupKeys(
queryClient: QueryClient,
classid?: string,
assignmentNumber?: number,
groupNumber?: number,
) {
const keys = ["group", "group-submissions", "group-questions"];
for (const key of keys) {
const queryKey = [key, classid, assignmentNumber, groupNumber].filter((arg) => arg !== undefined);
await queryClient.invalidateQueries({ queryKey: queryKey });
}
await queryClient.invalidateQueries({
queryKey: ["groups", classid, assignmentNumber].filter((arg) => arg !== undefined),
});
}
function checkEnabled(
classid: string | undefined,
assignmentNumber: number | undefined,
groupNumber: number | undefined,
): boolean {
return Boolean(classid) && !isNaN(Number(groupNumber)) && !isNaN(Number(assignmentNumber));
}
function toValues(
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
groupNumber: MaybeRefOrGetter<number | undefined>,
full: MaybeRefOrGetter<boolean>,
) {
return { cid: toValue(classid), an: toValue(assignmentNumber), gn: toValue(groupNumber), f: toValue(full) };
}
export function useGroupsQuery(
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<GroupsResponse, Error> {
const { cid, an, f } = toValues(classid, assignmentNumber, 1, full);
return useQuery({
queryKey: computed(() => groupsQueryKey(cid!, an!, f)),
queryFn: async () => new GroupController(cid!, an!).getAll(f),
enabled: () => checkEnabled(cid, an, 1),
});
}
export function useGroupQuery(
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
groupNumber: MaybeRefOrGetter<number | undefined>,
): UseQueryReturnType<GroupResponse, Error> {
const { cid, an, gn } = toValues(classid, assignmentNumber, groupNumber, true);
return useQuery({
queryKey: computed(() => groupQueryKey(cid!, an!, gn!)),
queryFn: async () => new GroupController(cid!, an!).getByNumber(gn!),
enabled: () => checkEnabled(cid, an, gn),
});
}
export function useCreateGroupMutation(): UseMutationReturnType<
GroupResponse,
Error,
{ cid: string; an: number; data: GroupDTO },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ cid, an, data }) => new GroupController(cid, an).createGroup(data),
onSuccess: async (response) => {
const cid = typeof response.group.class === "string" ? response.group.class : response.group.class.id;
const an =
typeof response.group.assignment === "number"
? response.group.assignment
: response.group.assignment.id;
await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid, an, true) });
await queryClient.invalidateQueries({ queryKey: groupsQueryKey(cid, an, false) });
},
});
}
export function useDeleteGroupMutation(): UseMutationReturnType<
GroupResponse,
Error,
{ cid: string; an: number; gn: number },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ cid, an, gn }) => new GroupController(cid, an).deleteGroup(gn),
onSuccess: async (response) => {
const cid = typeof response.group.class === "string" ? response.group.class : response.group.class.id;
const an =
typeof response.group.assignment === "number"
? response.group.assignment
: response.group.assignment.id;
const gn = response.group.groupNumber;
await invalidateAllGroupKeys(queryClient, cid, an, gn);
await invalidateAllSubmissionKeys(queryClient, cid, an, gn);
},
});
}
export function useUpdateGroupMutation(): UseMutationReturnType<
GroupResponse,
Error,
{ cid: string; an: number; gn: number; data: Partial<GroupDTO> },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ cid, an, gn, data }) => new GroupController(cid, an).updateGroup(gn, data),
onSuccess: async (response) => {
const cid = typeof response.group.class === "string" ? response.group.class : response.group.class.id;
const an =
typeof response.group.assignment === "number"
? response.group.assignment
: response.group.assignment.id;
const gn = response.group.groupNumber;
await invalidateAllGroupKeys(queryClient, cid, an, gn);
},
});
}
export function useGroupSubmissionsQuery(
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
groupNumber: MaybeRefOrGetter<number | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<SubmissionsResponse, Error> {
const { cid, an, gn, f } = toValues(classid, assignmentNumber, groupNumber, full);
return useQuery({
queryKey: computed(() => groupSubmissionsQueryKey(cid!, an!, gn!, f)),
queryFn: async () => new GroupController(cid!, an!).getSubmissions(gn!, f),
enabled: () => checkEnabled(cid, an, gn),
});
}
export function useGroupQuestionsQuery(
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
groupNumber: MaybeRefOrGetter<number | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<QuestionsResponse, Error> {
const { cid, an, gn, f } = toValues(classid, assignmentNumber, groupNumber, full);
return useQuery({
queryKey: computed(() => groupQuestionsQueryKey(cid!, an!, gn!, f)),
queryFn: async () => new GroupController(cid!, an!).getSubmissions(gn!, f),
enabled: () => checkEnabled(cid, an, gn),
});
}

View file

@ -5,7 +5,7 @@ import { getLearningObjectController } from "@/controllers/controllers.ts";
import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts";
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
const LEARNING_OBJECT_KEY = "learningObject";
export const LEARNING_OBJECT_KEY = "learningObject";
const learningObjectController = getLearningObjectController();
export function useLearningObjectMetadataQuery(

View file

@ -4,19 +4,21 @@ import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query";
import { getLearningPathController } from "@/controllers/controllers";
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
const LEARNING_PATH_KEY = "learningPath";
export const LEARNING_PATH_KEY = "learningPath";
const learningPathController = getLearningPathController();
export function useGetLearningPathQuery(
hruid: MaybeRefOrGetter<string>,
language: MaybeRefOrGetter<Language>,
options?: MaybeRefOrGetter<{ forGroup?: string; forStudent?: string }>,
forGroup?: MaybeRefOrGetter<{forGroup: number, assignmentNo: number, classId: string}>,
): UseQueryReturnType<LearningPath, Error> {
return useQuery({
queryKey: [LEARNING_PATH_KEY, "get", hruid, language, options],
queryKey: [LEARNING_PATH_KEY, "get", toValue(hruid), toValue(language), toValue(forGroup)],
queryFn: async () => {
const [hruidVal, languageVal, optionsVal] = [toValue(hruid), toValue(language), toValue(options)];
return learningPathController.getBy(hruidVal, languageVal, optionsVal);
console.log("queryKey");
console.log([LEARNING_PATH_KEY, "get", toValue(hruid), toValue(language), toValue(forGroup)]);
const [hruidVal, languageVal, forGroupVal] = [toValue(hruid), toValue(language), toValue(forGroup)];
return learningPathController.getBy(hruidVal, languageVal, forGroupVal);
},
enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)),
});

View file

@ -0,0 +1,244 @@
import { SubmissionController, type SubmissionResponse, type SubmissionsResponse } from "@/controllers/submissions";
import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
type UseMutationReturnType,
type UseQueryReturnType,
} from "@tanstack/vue-query";
import { computed, toValue, type MaybeRefOrGetter } from "vue";
import {LEARNING_PATH_KEY} from "@/queries/learning-paths.ts";
import {LEARNING_OBJECT_KEY} from "@/queries/learning-objects.ts";
import type {Language} from "@dwengo-1/common/util/language";
import {getEnvVar} from "@dwengo-1/backend/dist/util/envVars.ts";
function submissionsQueryKey(
hruid: string,
language: Language,
version: number,
classid: string,
assignmentNumber: number,
groupNumber?: number,
full?: boolean
) {
return ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber, full ?? false];
}
function submissionQueryKey(
hruid: string,
language: Language,
version: number,
classid: string,
assignmentNumber: number,
groupNumber: number,
submissionNumber: number
) {
return ["submission", hruid, language, version, classid, assignmentNumber, groupNumber, submissionNumber];
}
export async function invalidateAllSubmissionKeys(
queryClient: QueryClient,
hruid?: string,
language?: Language,
version?: number,
classid?: string,
assignmentNumber?: number,
groupNumber?: number,
submissionNumber?: number,
) {
const keys = ["submission"];
for (const key of keys) {
const queryKey = [
key, hruid, language, version, classid, assignmentNumber, groupNumber, submissionNumber
].filter(
(arg) => arg !== undefined,
);
await queryClient.invalidateQueries({ queryKey: queryKey });
}
await queryClient.invalidateQueries({
queryKey: ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber]
.filter((arg) => arg !== undefined),
});
await queryClient.invalidateQueries({
queryKey: ["group-submissions", hruid, language, version, classid, assignmentNumber, groupNumber]
.filter((arg) => arg !== undefined),
});
await queryClient.invalidateQueries({
queryKey: ["assignment-submissions", hruid, language, version,classid, assignmentNumber]
.filter((arg) => arg !== undefined),
});
}
function checkEnabled(
classid: string | undefined,
assignmentNumber: number | undefined,
groupNumber: number | undefined,
submissionNumber?: number | undefined,
submissionNumberRequired: boolean = false
): boolean {
return (
Boolean(classid) &&
!isNaN(Number(groupNumber)) &&
!isNaN(Number(assignmentNumber)) &&
(!isNaN(Number(submissionNumber)) || !submissionNumberRequired)
);
}
function toValues(
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
groupNumber: MaybeRefOrGetter<number | undefined>,
submissionNumber: MaybeRefOrGetter<number | undefined>,
full: MaybeRefOrGetter<boolean>,
) {
return {
cid: toValue(classid),
an: toValue(assignmentNumber),
gn: toValue(groupNumber),
sn: toValue(submissionNumber),
f: toValue(full),
};
}
export function useSubmissionsQuery(
hruid: MaybeRefOrGetter<string | undefined>,
language: MaybeRefOrGetter<Language | undefined>,
version: MaybeRefOrGetter<number | undefined>,
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
groupNumber: MaybeRefOrGetter<number | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<SubmissionsResponse, Error> {
const hruidVal = toValue(hruid);
const languageVal = toValue(language);
const versionVal = toValue(version);
const classIdVal = toValue(classid);
const assignmentNumberVal = toValue(assignmentNumber);
const groupNumberVal = toValue(groupNumber);
const fullVal = toValue(full);
return useQuery({
queryKey: computed(() =>
submissionsQueryKey(
hruidVal!,
languageVal!,
versionVal!,
classIdVal!,
assignmentNumberVal!,
groupNumberVal,
fullVal
)
),
queryFn: async () => new SubmissionController(hruidVal!).getAll(
languageVal!, versionVal!, classIdVal!, assignmentNumberVal!, groupNumberVal, fullVal
),
enabled: () => !!hruidVal && !!languageVal && !!versionVal && !!classIdVal && !!assignmentNumberVal,
});
}
export function useSubmissionQuery(
hruid: MaybeRefOrGetter<string | undefined>,
language: MaybeRefOrGetter<Language | undefined>,
version: MaybeRefOrGetter<number | undefined>,
classid: MaybeRefOrGetter<string | undefined>,
assignmentNumber: MaybeRefOrGetter<number | undefined>,
groupNumber: MaybeRefOrGetter<number | undefined>,
submissionNumber: MaybeRefOrGetter<number | undefined>,
): UseQueryReturnType<SubmissionResponse, Error> {
const { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, submissionNumber, true);
const hruidVal = toValue(hruid);
const languageVal = toValue(language);
const versionVal = toValue(version);
const classIdVal = toValue(classid);
const assignmentNumberVal = toValue(assignmentNumber);
const groupNumberVal = toValue(groupNumber);
const submissionNumberVal = toValue(submissionNumber);
return useQuery({
queryKey: computed(() => submissionQueryKey(
hruidVal!, languageVal!, versionVal!, classIdVal!, assignmentNumberVal!, groupNumberVal!, submissionNumberVal!
)),
queryFn: async () => new SubmissionController(hruidVal!).getByNumber(
languageVal!, versionVal!, classIdVal!, assignmentNumberVal!, groupNumberVal!, submissionNumberVal!
),
enabled: () => !!hruidVal && !!languageVal && !!versionVal && !!classIdVal && !!assignmentNumberVal && !!submissionNumber,
});
}
export function useCreateSubmissionMutation(): UseMutationReturnType<
SubmissionResponse,
Error,
{ data: SubmissionDTO },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ data }) => new SubmissionController(data.learningObjectIdentifier.hruid).createSubmission(data),
onSuccess: async (response) => {
if (!response.submission.group) {
await invalidateAllSubmissionKeys(queryClient);
} else {
const cls = response.submission.group.class;
const assignment = response.submission.group.assignment;
const cid = typeof cls === "string" ? cls : cls.id;
const an = typeof assignment === "number" ? assignment : assignment.id;
const gn = response.submission.group.groupNumber;
const {hruid, language, version} = response.submission.learningObjectIdentifier;
await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn);
console.log("INVALIDATE");
console.log([
LEARNING_PATH_KEY, "get",
response.submission.learningObjectIdentifier.hruid,
]);
await queryClient.invalidateQueries({queryKey: [LEARNING_PATH_KEY, "get"]});
await queryClient.invalidateQueries({
queryKey: [LEARNING_OBJECT_KEY, "metadata", hruid, language, version]
});
}
},
});
}
export function useDeleteSubmissionMutation(): UseMutationReturnType<
SubmissionResponse,
Error,
{ cid: string; an: number; gn: number; sn: number },
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ cid, an, gn, sn }) => new SubmissionController(cid).deleteSubmission(sn),
onSuccess: async (response) => {
if (!response.submission.group) {
await invalidateAllSubmissionKeys(queryClient);
} else {
const cls = response.submission.group.class;
const assignment = response.submission.group.assignment;
const cid = typeof cls === "string" ? cls : cls.id;
const an = typeof assignment === "number" ? assignment : assignment.id;
const gn = response.submission.group.groupNumber;
const {hruid, language, version} = response.submission.learningObjectIdentifier;
await invalidateAllSubmissionKeys(
queryClient,
hruid,
language,
version,
cid, an, gn
);
}
},
});
}

View file

@ -0,0 +1,78 @@
import { useMutation, useQuery, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query";
import { computed, toValue } from "vue";
import type { MaybeRefOrGetter } from "vue";
import {
TeacherInvitationController,
type TeacherInvitationResponse,
type TeacherInvitationsResponse,
} from "@/controllers/teacher-invitations.ts";
import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation";
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
const controller = new TeacherInvitationController();
/**
All the invitations the teacher sent
**/
export function useTeacherInvitationsSentQuery(
username: MaybeRefOrGetter<string | undefined>,
): UseQueryReturnType<TeacherInvitationsResponse, Error> {
return useQuery({
queryFn: computed(async () => controller.getAll(toValue(username), true)),
enabled: () => Boolean(toValue(username)),
});
}
/**
All the pending invitations sent to this teacher
*/
export function useTeacherInvitationsReceivedQuery(
username: MaybeRefOrGetter<string | undefined>,
): UseQueryReturnType<TeacherInvitationsResponse, Error> {
return useQuery({
queryFn: computed(async () => controller.getAll(toValue(username), false)),
enabled: () => Boolean(toValue(username)),
});
}
export function useTeacherInvitationQuery(
data: MaybeRefOrGetter<TeacherInvitationData | undefined>,
): UseQueryReturnType<TeacherInvitationResponse, Error> {
return useQuery({
queryFn: computed(async () => controller.getBy(toValue(data))),
enabled: () => Boolean(toValue(data)),
});
}
export function useCreateTeacherInvitationMutation(): UseMutationReturnType<
TeacherInvitationResponse,
Error,
TeacherDTO,
unknown
> {
return useMutation({
mutationFn: async (data: TeacherInvitationData) => controller.create(data),
});
}
export function useRespondTeacherInvitationMutation(): UseMutationReturnType<
TeacherInvitationResponse,
Error,
TeacherDTO,
unknown
> {
return useMutation({
mutationFn: async (data: TeacherInvitationData) => controller.respond(data),
});
}
export function useDeleteTeacherInvitationMutation(): UseMutationReturnType<
TeacherInvitationResponse,
Error,
TeacherDTO,
unknown
> {
return useMutation({
mutationFn: async (data: TeacherInvitationData) => controller.remove(data),
});
}

View file

@ -276,10 +276,11 @@
<tbody>
<tr
v-for="i in invitations"
:key="(i.class as ClassDTO).id"
:key="i.classId"
>
<td>
{{ (i.class as ClassDTO).displayName }}
{{ i.classId }}
<!-- TODO fetch display name via classId because db only returns classId field -->
</td>
<td>{{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}</td>
<td class="text-right">

View file

@ -3,10 +3,24 @@
import type { UseQueryReturnType } from "@tanstack/vue-query";
import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts";
import UsingQueryResult from "@/components/UsingQueryResult.vue";
import {nextTick, onMounted, reactive, watch} from "vue";
import {computed, nextTick, onMounted, reactive, watch} from "vue";
import {getGiftAdapterForType} from "@/views/learning-paths/gift-adapters/gift-adapters.ts";
import authService from "@/services/auth/auth-service.ts";
import {useCreateSubmissionMutation, useSubmissionsQuery} from "@/queries/submissions.ts";
import type {SubmissionDTO} from "@dwengo-1/common/dist/interfaces/submission.d.ts";
import type {GroupDTO} from "@dwengo-1/common/interfaces/group";
import type {StudentDTO} from "@dwengo-1/common/interfaces/student";
import type {LearningObjectIdentifierDTO} from "@dwengo-1/common/interfaces/learning-content";
import type {User, UserProfile} from "oidc-client-ts";
const props = defineProps<{ hruid: string; language: Language; version: number }>();
const isStudent = computed(() => authService.authState.activeRole === "student");
const props = defineProps<{
hruid: string;
language: Language;
version: number,
group?: {forGroup: number, assignmentNo: number, classId: string}
}>();
const learningObjectHtmlQueryResult: UseQueryReturnType<Document, Error> = useLearningObjectHTMLQuery(
() => props.hruid,
@ -14,7 +28,61 @@
() => props.version,
);
const currentAnswer = reactive([]);
const currentAnswer = reactive(<(string | number | object)[]>[]);
const {
isPending: submissionIsPending,
isError: submissionFailed,
error: submissionError,
isSuccess: submissionSuccess,
mutate: submitSolution
} = useCreateSubmissionMutation();
const {
isPending: existingSubmissionsIsPending,
isError: existingSubmissionsFailed,
error: existingSubmissionsError,
isSuccess: existingSubmissionsSuccess,
data: existingSubmissions
} = useSubmissionsQuery(
props.hruid,
props.language,
props.version,
props.group?.classId,
props.group?.assignmentNo,
props.group?.forGroup,
true
);
function submitCurrentAnswer(): void {
const { forGroup, assignmentNo, classId } = props.group!;
const currentUser: UserProfile = authService.authState.user!.profile;
const learningObjectIdentifier: LearningObjectIdentifierDTO = {
hruid: props.hruid,
language: props.language as Language,
version: props.version
};
const submitter: StudentDTO = {
id: currentUser.preferred_username!,
username: currentUser.preferred_username!,
firstName: currentUser.given_name!,
lastName: currentUser.family_name!
};
const group: GroupDTO = {
class: classId,
assignment: assignmentNo,
groupNumber: forGroup
}
const submission: SubmissionDTO = {
learningObjectIdentifier,
submitter,
group,
content: JSON.stringify(currentAnswer)
}
submitSolution({ data: submission });
}
function forEachQuestion(
doAction: (questionIndex: number, questionName: string, questionType: string, questionElement: Element) => void
@ -22,9 +90,9 @@
const questions = document.querySelectorAll(".gift-question");
questions.forEach(question => {
const name = question.id.match(/gift-q(\d+)/)?.[1]
const questionType = question.classList.values()
const questionType = question.className.split(" ")
.find(it => it.startsWith("gift-question-type"))
.match(/gift-question-type-([^ ]*)/)?.[1];
?.match(/gift-question-type-([^ ]*)/)?.[1];
if (!name || isNaN(parseInt(name)) || !questionType) return;
@ -46,7 +114,7 @@
forEachQuestion((index, name, type, element) => {
getGiftAdapterForType(type)?.setAnswer(element, answers[index]);
});
currentAnswer.fill(answers);
currentAnswer.splice(0, currentAnswer.length, ...answers);
}
onMounted(() => nextTick(() => attachQuestionListeners()));
@ -68,6 +136,14 @@
v-html="learningPathHtml.data.body.innerHTML"
></div>
{{ currentAnswer }}
<v-btn v-if="isStudent && props.group"
prepend-icon="mdi-check"
variant="elevated"
:loading="submissionIsPending"
@click="submitCurrentAnswer()"
>
Submit
</v-btn>
</using-query-result>
</template>

View file

@ -16,26 +16,31 @@
const route = useRoute();
const { t } = useI18n();
const props = defineProps<{ hruid: string; language: Language; learningObjectHruid?: string }>();
const props = defineProps<{
hruid: string;
language: Language;
learningObjectHruid?: string,
}>();
interface Personalization {
forStudent?: string;
interface LearningPathPageQuery {
forGroup?: string;
assignmentNo?: string;
classId?: string;
}
const personalization = computed(() => {
if (route.query.forStudent || route.query.forGroup) {
const query = computed(() => route.query as LearningPathPageQuery);
const forGroup = computed(() => {
if (query.value.forGroup && query.value.assignmentNo && query.value.classId) {
return {
forStudent: route.query.forStudent,
forGroup: route.query.forGroup,
} as Personalization;
forGroup: parseInt(query.value.forGroup),
assignmentNo: parseInt(query.value.assignmentNo),
classId: query.value.classId
};
}
return {
forStudent: authService.authState.user?.profile?.preferred_username,
} as Personalization;
});
const learningPathQueryResult = useGetLearningPathQuery(props.hruid, props.language, personalization);
const learningPathQueryResult = useGetLearningPathQuery(props.hruid, props.language, forGroup);
const learningObjectListQueryResult = useLearningObjectListForPathQuery(learningPathQueryResult.data);
@ -184,6 +189,7 @@
:hruid="currentNode.learningobjectHruid"
:language="currentNode.language"
:version="currentNode.version"
:group="forGroup"
v-if="currentNode"
></learning-object-view>
<div class="navigation-buttons-container">