Merge branch 'dev' into feat/class-functionality
This commit is contained in:
commit
aaaacd181a
102 changed files with 2153 additions and 1467 deletions
|
@ -10,12 +10,14 @@
|
|||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --build",
|
||||
"format": "prettier --write src/",
|
||||
"test:e2e": "playwright test",
|
||||
"format-check": "prettier --check src/",
|
||||
"lint": "eslint . --fix",
|
||||
"test:unit": "vitest --run",
|
||||
"test:e2e": "playwright test"
|
||||
"pretest:unit": "tsx ../docs/api/generate.ts && npm run build",
|
||||
"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",
|
||||
|
|
|
@ -8,20 +8,21 @@ export class LearningPathController extends BaseController {
|
|||
constructor() {
|
||||
super("learningPath");
|
||||
}
|
||||
async search(query: string): Promise<LearningPath[]> {
|
||||
const dtos = await this.get<LearningPathDTO[]>("/", { search: query });
|
||||
async search(query: string, language: string): Promise<LearningPath[]> {
|
||||
const dtos = await this.get<LearningPathDTO[]>("/", { search: query, language });
|
||||
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
|
|
@ -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,16 +11,36 @@ export interface SubmissionResponse {
|
|||
}
|
||||
|
||||
export class SubmissionController extends BaseController {
|
||||
constructor(classid: string, assignmentNumber: number, groupNumber: number) {
|
||||
super(`class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}/submissions`);
|
||||
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: SubmissionDTO): Promise<SubmissionResponse> {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"welcome": "Willkommen",
|
||||
"student": "schüler",
|
||||
"teacher": "lehrer",
|
||||
"student": "Schüler",
|
||||
"teacher": "Lehrer",
|
||||
"assignments": "Aufgaben",
|
||||
"classes": "Klasses",
|
||||
"classes": "Klassen",
|
||||
"discussions": "Diskussionen",
|
||||
"login": "einloggen",
|
||||
"logout": "ausloggen",
|
||||
"cancel": "kündigen",
|
||||
"cancel": "abbrechen",
|
||||
"logoutVerification": "Sind Sie sicher, dass Sie sich abmelden wollen?",
|
||||
"homeTitle": "Unsere Stärken",
|
||||
"homeIntroduction1": "Wir entwickeln innovative Workshops und Bildungsressourcen, die wir in Zusammenarbeit mit Lehrern und Freiwilligen Schülern auf der ganzen Welt zur Verfügung stellen. Unsere Train-the-Trainer-Sitzungen ermöglichen es ihnen, unsere praktischen Workshops an die Schüler weiterzugeben.",
|
||||
|
@ -23,10 +23,10 @@
|
|||
"submitCode": "senden",
|
||||
"members": "Mitglieder",
|
||||
"themes": "Themen",
|
||||
"choose-theme": "Wähle ein thema",
|
||||
"choose-theme": "Wählen Sie ein Thema",
|
||||
"choose-age": "Alter auswählen",
|
||||
"theme-options": {
|
||||
"all": "Alle themen",
|
||||
"all": "Alle Themen",
|
||||
"culture": "Kultur",
|
||||
"electricity-and-mechanics": "Elektrizität und Mechanik",
|
||||
"nature-and-climate": "Natur und Klima",
|
||||
|
@ -37,11 +37,11 @@
|
|||
"algorithms": "Algorithmisches Denken"
|
||||
},
|
||||
"age-options": {
|
||||
"all": "Alle altersgruppen",
|
||||
"all": "Alle Altersgruppen",
|
||||
"primary-school": "Grundschule",
|
||||
"lower-secondary": "12-14 jahre alt",
|
||||
"upper-secondary": "14-16 jahre alt",
|
||||
"high-school": "16-18 jahre alt",
|
||||
"lower-secondary": "12-14 Jahre alt",
|
||||
"upper-secondary": "14-16 Jahre alt",
|
||||
"high-school": "16-18 Jahre alt",
|
||||
"older": "18 und älter"
|
||||
},
|
||||
"read-more": "Mehr lesen",
|
||||
|
@ -73,8 +73,19 @@
|
|||
"accept": "akzeptieren",
|
||||
"deny": "ablehnen",
|
||||
"sent": "sent",
|
||||
"failed": "gescheitert",
|
||||
"failed": "fehlgeschlagen",
|
||||
"wrong": "etwas ist schief gelaufen",
|
||||
"submitSolution": "Lösung einreichen",
|
||||
"submitNewSolution": "Neue Lösung einreichen",
|
||||
"markAsDone": "Als fertig markieren",
|
||||
"groupSubmissions": "Einreichungen dieser Gruppe",
|
||||
"taskCompleted": "Aufgabe erledigt.",
|
||||
"submittedBy": "Eingereicht von",
|
||||
"timestamp": "Zeitpunkt",
|
||||
"loadSubmission": "Einladen",
|
||||
"noSubmissionsYet": "Noch keine Lösungen eingereicht.",
|
||||
"viewAsGroup": "Fortschritt ansehen von Gruppe...",
|
||||
"assignLearningPath": "Als Aufgabe geben",
|
||||
"created": "erstellt",
|
||||
"remove": "entfernen",
|
||||
"students": "Studenten",
|
||||
|
|
|
@ -75,6 +75,17 @@
|
|||
"sent": "sent",
|
||||
"failed": "failed",
|
||||
"wrong": "something went wrong",
|
||||
"submitSolution": "Submit solution",
|
||||
"submitNewSolution": "Submit new solution",
|
||||
"markAsDone": "Mark as completed",
|
||||
"groupSubmissions": "This group's submissions",
|
||||
"taskCompleted": "Task completed.",
|
||||
"submittedBy": "Submitted by",
|
||||
"timestamp": "Timestamp",
|
||||
"loadSubmission": "Load",
|
||||
"noSubmissionsYet": "No submissions yet.",
|
||||
"viewAsGroup": "View progress of group...",
|
||||
"assignLearningPath": "assign",
|
||||
"created": "created",
|
||||
"remove": "remove",
|
||||
"students": "students",
|
||||
|
|
|
@ -76,6 +76,17 @@
|
|||
"failed": "échoué",
|
||||
"wrong": "quelque chose n'a pas fonctionné",
|
||||
"created": "créé",
|
||||
"submitSolution": "Soumettre la solution",
|
||||
"submitNewSolution": "Soumettre une nouvelle solution",
|
||||
"markAsDone": "Marquer comme terminé",
|
||||
"groupSubmissions": "Soumissions de ce groupe",
|
||||
"taskCompleted": "Tâche terminée.",
|
||||
"submittedBy": "Soumis par",
|
||||
"timestamp": "Horodatage",
|
||||
"loadSubmission": "Charger",
|
||||
"noSubmissionsYet": "Pas encore de soumissions.",
|
||||
"viewAsGroup": "Voir la progression du groupe...",
|
||||
"assignLearningPath": "donner comme tâche",
|
||||
"remove": "supprimer",
|
||||
"students": "étudiants",
|
||||
"classJoinRequests": "demandes d'adhésion",
|
||||
|
|
|
@ -76,6 +76,17 @@
|
|||
"failed": "mislukt",
|
||||
"wrong": "er ging iets verkeerd",
|
||||
"created": "gecreëerd",
|
||||
"submitSolution": "Oplossing indienen",
|
||||
"submitNewSolution": "Nieuwe oplossing indienen",
|
||||
"markAsDone": "Markeren als afgewerkt",
|
||||
"groupSubmissions": "Indieningen van deze groep",
|
||||
"taskCompleted": "Taak afgewerkt.",
|
||||
"submittedBy": "Ingediend door",
|
||||
"timestamp": "Tijdstip",
|
||||
"loadSubmission": "Inladen",
|
||||
"noSubmissionsYet": "Nog geen indieningen.",
|
||||
"viewAsGroup": "Vooruitgang bekijken van groep...",
|
||||
"assignLearningPath": "Als opdracht geven",
|
||||
"remove": "verwijder",
|
||||
"students": "studenten",
|
||||
"classJoinRequests": "deelname verzoeken",
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
useQueryClient,
|
||||
type UseQueryReturnType,
|
||||
} from "@tanstack/vue-query";
|
||||
import { computed, type MaybeRefOrGetter, toValue } from "vue";
|
||||
import { computed, toValue, type MaybeRefOrGetter } from "vue";
|
||||
import { invalidateAllSubmissionKeys } from "./submissions";
|
||||
|
||||
type GroupsQueryKey = ["groups", string, number, boolean];
|
||||
|
@ -160,7 +160,7 @@ export function useDeleteGroupMutation(): UseMutationReturnType<
|
|||
const gn = response.group.groupNumber;
|
||||
|
||||
await invalidateAllGroupKeys(queryClient, cid, an, gn);
|
||||
await invalidateAllSubmissionKeys(queryClient, cid, an, gn);
|
||||
await invalidateAllSubmissionKeys(queryClient, undefined, undefined, undefined, cid, an, gn);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -4,19 +4,19 @@ 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 } | undefined>,
|
||||
): UseQueryReturnType<LearningPath, Error> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_PATH_KEY, "get", hruid, language, options],
|
||||
queryKey: [LEARNING_PATH_KEY, "get", hruid, language, forGroup],
|
||||
queryFn: async () => {
|
||||
const [hruidVal, languageVal, optionsVal] = [toValue(hruid), toValue(language), toValue(options)];
|
||||
return learningPathController.getBy(hruidVal, languageVal, optionsVal);
|
||||
const [hruidVal, languageVal, forGroupVal] = [toValue(hruid), toValue(language), toValue(forGroup)];
|
||||
return learningPathController.getBy(hruidVal, languageVal, forGroupVal);
|
||||
},
|
||||
enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)),
|
||||
});
|
||||
|
@ -34,12 +34,14 @@ export function useGetAllLearningPathsByThemeQuery(
|
|||
|
||||
export function useSearchLearningPathQuery(
|
||||
query: MaybeRefOrGetter<string | undefined>,
|
||||
language: MaybeRefOrGetter<string | undefined>,
|
||||
): UseQueryReturnType<LearningPath[], Error> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_PATH_KEY, "search", query],
|
||||
queryKey: [LEARNING_PATH_KEY, "search", query, language],
|
||||
queryFn: async () => {
|
||||
const queryVal = toValue(query)!;
|
||||
return learningPathController.search(queryVal);
|
||||
const languageVal = toValue(language)!;
|
||||
return learningPathController.search(queryVal, languageVal);
|
||||
},
|
||||
enabled: () => Boolean(toValue(query)),
|
||||
});
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
import { SubmissionController, type SubmissionResponse, type SubmissionsResponse } from "@/controllers/submissions";
|
||||
import { SubmissionController, type SubmissionResponse } from "@/controllers/submissions";
|
||||
import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
|
||||
import {
|
||||
QueryClient,
|
||||
useMutation,
|
||||
type UseMutationReturnType,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseMutationReturnType,
|
||||
type UseQueryReturnType,
|
||||
} from "@tanstack/vue-query";
|
||||
import { computed, toValue, type MaybeRefOrGetter } from "vue";
|
||||
import { computed, type MaybeRefOrGetter, toValue } 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";
|
||||
|
||||
type SubmissionsQueryKey = ["submissions", string, number, number, boolean];
|
||||
export const SUBMISSION_KEY = "submissions";
|
||||
|
||||
function submissionsQueryKey(
|
||||
classid: string,
|
||||
assignmentNumber: number,
|
||||
groupNumber: number,
|
||||
full: boolean,
|
||||
): SubmissionsQueryKey {
|
||||
return ["submissions", classid, assignmentNumber, groupNumber, full];
|
||||
}
|
||||
|
||||
type SubmissionQueryKey = ["submission", string, number, number, number];
|
||||
type SubmissionQueryKey = ["submission", string, Language | undefined, number, string, number, number, number];
|
||||
|
||||
function submissionQueryKey(
|
||||
hruid: string,
|
||||
language: Language,
|
||||
version: number,
|
||||
classid: string,
|
||||
assignmentNumber: number,
|
||||
groupNumber: number,
|
||||
submissionNumber: number,
|
||||
): SubmissionQueryKey {
|
||||
return ["submission", classid, assignmentNumber, groupNumber, submissionNumber];
|
||||
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,
|
||||
|
@ -43,101 +43,134 @@ export async function invalidateAllSubmissionKeys(
|
|||
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
const queryKey = [key, classid, assignmentNumber, groupNumber, submissionNumber].filter(
|
||||
(arg) => arg !== undefined,
|
||||
);
|
||||
const queryKey = [
|
||||
key,
|
||||
hruid,
|
||||
language,
|
||||
version,
|
||||
classid,
|
||||
assignmentNumber,
|
||||
groupNumber,
|
||||
submissionNumber,
|
||||
].filter((arg) => arg !== undefined);
|
||||
return queryClient.invalidateQueries({ queryKey: queryKey });
|
||||
}),
|
||||
);
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["submissions", classid, assignmentNumber, groupNumber].filter((arg) => arg !== undefined),
|
||||
queryKey: ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber].filter(
|
||||
(arg) => arg !== undefined,
|
||||
),
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["group-submissions", classid, assignmentNumber, groupNumber].filter((arg) => arg !== undefined),
|
||||
queryKey: ["group-submissions", hruid, language, version, classid, assignmentNumber, groupNumber].filter(
|
||||
(arg) => arg !== undefined,
|
||||
),
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["assignment-submissions", classid, assignmentNumber].filter((arg) => arg !== undefined),
|
||||
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,
|
||||
): boolean {
|
||||
return (
|
||||
Boolean(classid) &&
|
||||
!isNaN(Number(groupNumber)) &&
|
||||
!isNaN(Number(assignmentNumber)) &&
|
||||
!isNaN(Number(submissionNumber))
|
||||
);
|
||||
}
|
||||
|
||||
interface Values {
|
||||
cid: string | undefined;
|
||||
an: number | undefined;
|
||||
gn: number | undefined;
|
||||
sn: number | undefined;
|
||||
f: boolean;
|
||||
}
|
||||
|
||||
function toValues(
|
||||
classid: MaybeRefOrGetter<string | undefined>,
|
||||
assignmentNumber: MaybeRefOrGetter<number | undefined>,
|
||||
groupNumber: MaybeRefOrGetter<number | undefined>,
|
||||
submissionNumber: MaybeRefOrGetter<number | undefined>,
|
||||
full: MaybeRefOrGetter<boolean>,
|
||||
): Values {
|
||||
return {
|
||||
cid: toValue(classid),
|
||||
an: toValue(assignmentNumber),
|
||||
gn: toValue(groupNumber),
|
||||
sn: toValue(submissionNumber),
|
||||
f: toValue(full),
|
||||
};
|
||||
function checkEnabled(properties: MaybeRefOrGetter<unknown>[]): boolean {
|
||||
return properties.every((prop) => Boolean(toValue(prop)));
|
||||
}
|
||||
|
||||
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 { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, 1, full);
|
||||
|
||||
): UseQueryReturnType<SubmissionDTO[], Error> {
|
||||
return useQuery({
|
||||
queryKey: computed(() => submissionsQueryKey(cid!, an!, gn!, f)),
|
||||
queryFn: async () => new SubmissionController(cid!, an!, gn!).getAll(f),
|
||||
enabled: () => checkEnabled(cid, an, gn, sn),
|
||||
queryKey: ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber, full],
|
||||
queryFn: async () => {
|
||||
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);
|
||||
|
||||
const response = await new SubmissionController(hruidVal!).getAll(
|
||||
languageVal,
|
||||
versionVal!,
|
||||
classIdVal!,
|
||||
assignmentNumberVal!,
|
||||
groupNumberVal,
|
||||
fullVal,
|
||||
);
|
||||
return response ? (response.submissions as SubmissionDTO[]) : undefined;
|
||||
},
|
||||
enabled: () => checkEnabled([hruid, language, version, classid, assignmentNumber]),
|
||||
});
|
||||
}
|
||||
|
||||
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 } = toValues(classid, assignmentNumber, groupNumber, 1, 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(cid!, an!, gn!, sn!)),
|
||||
queryFn: async () => new SubmissionController(cid!, an!, gn!).getByNumber(sn!),
|
||||
enabled: () => checkEnabled(cid, an, gn, sn),
|
||||
queryKey: computed(() =>
|
||||
submissionQueryKey(
|
||||
hruidVal!,
|
||||
languageVal,
|
||||
versionVal!,
|
||||
classIdVal!,
|
||||
assignmentNumberVal!,
|
||||
groupNumberVal!,
|
||||
submissionNumberVal!,
|
||||
),
|
||||
),
|
||||
queryFn: async () =>
|
||||
new SubmissionController(hruidVal!).getByNumber(
|
||||
languageVal,
|
||||
versionVal!,
|
||||
classIdVal!,
|
||||
assignmentNumberVal!,
|
||||
groupNumberVal!,
|
||||
submissionNumberVal!,
|
||||
),
|
||||
enabled: () =>
|
||||
Boolean(hruidVal) &&
|
||||
Boolean(languageVal) &&
|
||||
Boolean(versionVal) &&
|
||||
Boolean(classIdVal) &&
|
||||
Boolean(assignmentNumberVal) &&
|
||||
Boolean(submissionNumber),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateSubmissionMutation(): UseMutationReturnType<
|
||||
SubmissionResponse,
|
||||
Error,
|
||||
{ cid: string; an: number; gn: number; data: SubmissionDTO },
|
||||
{ data: SubmissionDTO },
|
||||
unknown
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ cid, an, gn, data }) => new SubmissionController(cid, an, gn).createSubmission(data),
|
||||
mutationFn: async ({ data }) =>
|
||||
new SubmissionController(data.learningObjectIdentifier.hruid).createSubmission(data),
|
||||
onSuccess: async (response) => {
|
||||
if (!response.submission.group) {
|
||||
await invalidateAllSubmissionKeys(queryClient);
|
||||
|
@ -149,7 +182,14 @@ export function useCreateSubmissionMutation(): UseMutationReturnType<
|
|||
const an = typeof assignment === "number" ? assignment : assignment.id;
|
||||
const gn = response.submission.group.groupNumber;
|
||||
|
||||
await invalidateAllSubmissionKeys(queryClient, cid, an, gn);
|
||||
const { hruid, language, version } = response.submission.learningObjectIdentifier;
|
||||
await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn);
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY, "get"] });
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [LEARNING_OBJECT_KEY, "metadata", hruid, language, version],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -164,7 +204,7 @@ export function useDeleteSubmissionMutation(): UseMutationReturnType<
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ cid, an, gn, sn }) => new SubmissionController(cid, an, gn).deleteSubmission(sn),
|
||||
mutationFn: async ({ cid, sn }) => new SubmissionController(cid).deleteSubmission(sn),
|
||||
onSuccess: async (response) => {
|
||||
if (!response.submission.group) {
|
||||
await invalidateAllSubmissionKeys(queryClient);
|
||||
|
@ -176,7 +216,9 @@ export function useDeleteSubmissionMutation(): UseMutationReturnType<
|
|||
const an = typeof assignment === "number" ? assignment : assignment.id;
|
||||
const gn = response.submission.group.groupNumber;
|
||||
|
||||
await invalidateAllSubmissionKeys(queryClient, cid, an, gn);
|
||||
const { hruid, language, version } = response.submission.learningObjectIdentifier;
|
||||
|
||||
await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue";
|
|||
import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue";
|
||||
import UserHomePage from "@/views/homepage/UserHomePage.vue";
|
||||
import SingleTheme from "@/views/SingleTheme.vue";
|
||||
import LearningObjectView from "@/views/learning-paths/LearningObjectView.vue";
|
||||
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
|
5
frontend/src/utils/array-utils.ts
Normal file
5
frontend/src/utils/array-utils.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export function copyArrayWith<T>(index: number, newValue: T, array: T[]): T[] {
|
||||
const copy = [...array];
|
||||
copy[index] = newValue;
|
||||
return copy;
|
||||
}
|
29
frontend/src/utils/deep-equals.ts
Normal file
29
frontend/src/utils/deep-equals.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
export function deepEquals<T>(a: T, b: T): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(a) !== Array.isArray(b)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
return a.every((val, i) => deepEquals(val, b[i]));
|
||||
}
|
||||
|
||||
const keysA = Object.keys(a) as (keyof T)[];
|
||||
const keysB = Object.keys(b) as (keyof T)[];
|
||||
|
||||
if (keysA.length !== keysB.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return keysA.every((key) => deepEquals(a[key], b[key]));
|
||||
}
|
|
@ -1,7 +1,14 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
<main>
|
||||
Hier zou de pagina staan om een assignment aan te maken voor de leerpad met hruid {{ route.query.hruid }} en
|
||||
language {{ route.query.language }}. (Overschrijf dit)
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { Language } from "@/data-objects/language.ts";
|
||||
import type { UseQueryReturnType } from "@tanstack/vue-query";
|
||||
import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
|
||||
const props = defineProps<{ hruid: string; language: Language; version: number }>();
|
||||
|
||||
const learningObjectHtmlQueryResult: UseQueryReturnType<Document, Error> = useLearningObjectHTMLQuery(
|
||||
() => props.hruid,
|
||||
() => props.language,
|
||||
() => props.version,
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<using-query-result
|
||||
:query-result="learningObjectHtmlQueryResult as UseQueryReturnType<Document, Error>"
|
||||
v-slot="learningPathHtml: { data: Document }"
|
||||
>
|
||||
<div
|
||||
class="learning-object-container"
|
||||
v-html="learningPathHtml.data.body.innerHTML"
|
||||
></div>
|
||||
</using-query-result>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.learning-object-container {
|
||||
padding: 20px;
|
||||
}
|
||||
:deep(hr) {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
:deep(li) {
|
||||
margin-left: 30px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
:deep(img) {
|
||||
max-width: 80%;
|
||||
}
|
||||
:deep(h2),
|
||||
:deep(h3),
|
||||
:deep(h4),
|
||||
:deep(h5),
|
||||
:deep(h6) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,55 @@
|
|||
<script setup lang="ts">
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import { useGroupsQuery } from "@/queries/groups.ts";
|
||||
import type { GroupsResponse } from "@/controllers/groups.ts";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
classId: string;
|
||||
assignmentNumber: number;
|
||||
}>();
|
||||
|
||||
const model = defineModel<number | undefined>({ default: undefined });
|
||||
|
||||
const groupsQuery = useGroupsQuery(props.classId, props.assignmentNumber, true);
|
||||
|
||||
interface GroupSelectorOption {
|
||||
groupNumber: number | undefined;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function groupOptions(groups: GroupDTO[]): GroupSelectorOption[] {
|
||||
return [...groups]
|
||||
.sort((a, b) => a.groupNumber - b.groupNumber)
|
||||
.map((group, index) => ({
|
||||
groupNumber: group.groupNumber,
|
||||
label: `${index + 1}`,
|
||||
}));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<using-query-result
|
||||
:query-result="groupsQuery"
|
||||
v-slot="{ data }: { data: GroupsResponse }"
|
||||
>
|
||||
<v-select
|
||||
:label="t('viewAsGroup')"
|
||||
:items="groupOptions(data.groups)"
|
||||
v-model="model"
|
||||
item-title="label"
|
||||
class="group-selector-cb"
|
||||
variant="outlined"
|
||||
clearable
|
||||
></v-select>
|
||||
</using-query-result>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.group-selector-cb {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
|
@ -3,8 +3,8 @@
|
|||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import { computed, type ComputedRef, ref } from "vue";
|
||||
import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts";
|
||||
import { useRoute } from "vue-router";
|
||||
import LearningObjectView from "@/views/learning-paths/LearningObjectView.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import LearningPathSearchField from "@/components/LearningPathSearchField.vue";
|
||||
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
||||
|
@ -12,30 +12,38 @@
|
|||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import authService from "@/services/auth/auth-service.ts";
|
||||
import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts";
|
||||
import LearningPathGroupSelector from "@/views/learning-paths/LearningPathGroupSelector.vue";
|
||||
|
||||
const router = useRouter();
|
||||
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;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const learningPathQueryResult = useGetLearningPathQuery(props.hruid, props.language, personalization);
|
||||
const learningPathQueryResult = useGetLearningPathQuery(props.hruid, props.language, forGroup);
|
||||
|
||||
const learningObjectListQueryResult = useLearningObjectListForPathQuery(learningPathQueryResult.data);
|
||||
|
||||
|
@ -98,6 +106,25 @@
|
|||
}
|
||||
return "notCompleted";
|
||||
}
|
||||
|
||||
const forGroupQueryParam = computed<number | undefined>({
|
||||
get: () => route.query.forGroup,
|
||||
set: async (value: number | undefined) => {
|
||||
const query = structuredClone(route.query);
|
||||
query.forGroup = value;
|
||||
await router.push({ query });
|
||||
},
|
||||
});
|
||||
|
||||
async function assign(): Promise<void> {
|
||||
await router.push({
|
||||
path: "/assignment/create",
|
||||
query: {
|
||||
hruid: props.hruid,
|
||||
language: props.language,
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -109,64 +136,87 @@
|
|||
v-model="navigationDrawerShown"
|
||||
:width="350"
|
||||
>
|
||||
<v-list-item>
|
||||
<template v-slot:title>
|
||||
<div class="learning-path-title">{{ learningPath.data.title }}</div>
|
||||
</template>
|
||||
<template v-slot:subtitle>
|
||||
<div>{{ learningPath.data.description }}</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template v-slot:subtitle>
|
||||
<p>
|
||||
<v-icon
|
||||
:color="COLORS.notCompleted"
|
||||
:icon="ICONS.notCompleted"
|
||||
></v-icon>
|
||||
{{ t("legendNotCompletedYet") }}
|
||||
</p>
|
||||
<p>
|
||||
<v-icon
|
||||
:color="COLORS.completed"
|
||||
:icon="ICONS.completed"
|
||||
></v-icon>
|
||||
{{ t("legendCompleted") }}
|
||||
</p>
|
||||
<p>
|
||||
<v-icon
|
||||
:color="COLORS.teacherExclusive"
|
||||
:icon="ICONS.teacherExclusive"
|
||||
></v-icon>
|
||||
{{ t("legendTeacherExclusive") }}
|
||||
</p>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<div v-if="props.learningObjectHruid">
|
||||
<using-query-result
|
||||
:query-result="learningObjectListQueryResult"
|
||||
v-slot="learningObjects: { data: LearningObject[] }"
|
||||
>
|
||||
<template v-for="node in learningObjects.data">
|
||||
<v-list-item
|
||||
link
|
||||
:to="{ path: node.key, query: route.query }"
|
||||
:title="node.title"
|
||||
:active="node.key === props.learningObjectHruid"
|
||||
:key="node.key"
|
||||
v-if="!node.teacherExclusive || authService.authState.activeRole === 'teacher'"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon
|
||||
:color="COLORS[getNavItemState(node)]"
|
||||
:icon="ICONS[getNavItemState(node)]"
|
||||
></v-icon>
|
||||
</template>
|
||||
<template v-slot:append> {{ node.estimatedTime }}' </template>
|
||||
</v-list-item>
|
||||
<div class="d-flex flex-column h-100">
|
||||
<v-list-item>
|
||||
<template v-slot:title>
|
||||
<div class="learning-path-title">{{ learningPath.data.title }}</div>
|
||||
</template>
|
||||
</using-query-result>
|
||||
<template v-slot:subtitle>
|
||||
<div>{{ learningPath.data.description }}</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<template v-slot:subtitle>
|
||||
<p>
|
||||
<v-icon
|
||||
:color="COLORS.notCompleted"
|
||||
:icon="ICONS.notCompleted"
|
||||
></v-icon>
|
||||
{{ t("legendNotCompletedYet") }}
|
||||
</p>
|
||||
<p>
|
||||
<v-icon
|
||||
:color="COLORS.completed"
|
||||
:icon="ICONS.completed"
|
||||
></v-icon>
|
||||
{{ t("legendCompleted") }}
|
||||
</p>
|
||||
<p>
|
||||
<v-icon
|
||||
:color="COLORS.teacherExclusive"
|
||||
:icon="ICONS.teacherExclusive"
|
||||
></v-icon>
|
||||
{{ t("legendTeacherExclusive") }}
|
||||
</p>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="query.classId && query.assignmentNo && authService.authState.activeRole === 'teacher'"
|
||||
>
|
||||
<template v-slot:default>
|
||||
<learning-path-group-selector
|
||||
:class-id="query.classId"
|
||||
:assignment-number="parseInt(query.assignmentNo)"
|
||||
v-model="forGroupQueryParam"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<div v-if="props.learningObjectHruid">
|
||||
<using-query-result
|
||||
:query-result="learningObjectListQueryResult"
|
||||
v-slot="learningObjects: { data: LearningObject[] }"
|
||||
>
|
||||
<template v-for="node in learningObjects.data">
|
||||
<v-list-item
|
||||
link
|
||||
:to="{ path: node.key, query: route.query }"
|
||||
:title="node.title"
|
||||
:active="node.key === props.learningObjectHruid"
|
||||
:key="node.key"
|
||||
v-if="!node.teacherExclusive || authService.authState.activeRole === 'teacher'"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon
|
||||
:color="COLORS[getNavItemState(node)]"
|
||||
:icon="ICONS[getNavItemState(node)]"
|
||||
></v-icon>
|
||||
</template>
|
||||
<template v-slot:append> {{ node.estimatedTime }}' </template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</using-query-result>
|
||||
</div>
|
||||
<v-spacer></v-spacer>
|
||||
<v-list-item v-if="authService.authState.activeRole === 'teacher'">
|
||||
<template v-slot:default>
|
||||
<v-btn
|
||||
class="button-in-nav"
|
||||
@click="assign()"
|
||||
>{{ t("assignLearningPath") }}</v-btn
|
||||
>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</v-navigation-drawer>
|
||||
<div class="control-bar-above-content">
|
||||
|
@ -180,12 +230,15 @@
|
|||
<learning-path-search-field></learning-path-search-field>
|
||||
</div>
|
||||
</div>
|
||||
<learning-object-view
|
||||
:hruid="currentNode.learningobjectHruid"
|
||||
:language="currentNode.language"
|
||||
:version="currentNode.version"
|
||||
v-if="currentNode"
|
||||
></learning-object-view>
|
||||
<div class="learning-object-view-container">
|
||||
<learning-object-view
|
||||
:hruid="currentNode.learningobjectHruid"
|
||||
:language="currentNode.language"
|
||||
:version="currentNode.version"
|
||||
:group="forGroup"
|
||||
v-if="currentNode"
|
||||
></learning-object-view>
|
||||
</div>
|
||||
<div class="navigation-buttons-container">
|
||||
<v-btn
|
||||
prepend-icon="mdi-chevron-left"
|
||||
|
@ -221,9 +274,18 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.learning-object-view-container {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.navigation-buttons-container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.button-in-nav {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,11 +9,11 @@
|
|||
import LearningPathsGrid from "@/components/LearningPathsGrid.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const query = computed(() => route.query.query as string | undefined);
|
||||
|
||||
const searchQueryResults = useSearchLearningPathQuery(query);
|
||||
const searchQueryResults = useSearchLearningPathQuery(query, locale);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
export const essayQuestionAdapter: GiftAdapter = {
|
||||
questionType: "Essay",
|
||||
|
||||
installListener(
|
||||
questionElement: Element,
|
||||
answerUpdateCallback: (newAnswer: string | number | object) => void,
|
||||
): void {
|
||||
const textArea = questionElement.querySelector("textarea")!;
|
||||
textArea.addEventListener("input", () => {
|
||||
answerUpdateCallback(textArea.value);
|
||||
});
|
||||
},
|
||||
|
||||
setAnswer(questionElement: Element, answer: string | number | object): void {
|
||||
const textArea = questionElement.querySelector("textarea")!;
|
||||
textArea.value = String(answer);
|
||||
},
|
||||
};
|
8
frontend/src/views/learning-paths/gift-adapters/gift-adapter.d.ts
vendored
Normal file
8
frontend/src/views/learning-paths/gift-adapters/gift-adapter.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
interface GiftAdapter {
|
||||
questionType: string;
|
||||
installListener(
|
||||
questionElement: Element,
|
||||
answerUpdateCallback: (newAnswer: string | number | object) => void,
|
||||
): void;
|
||||
setAnswer(questionElement: Element, answer: string | number | object): void;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { multipleChoiceQuestionAdapter } from "@/views/learning-paths/gift-adapters/multiple-choice-question-adapter.ts";
|
||||
import { essayQuestionAdapter } from "@/views/learning-paths/gift-adapters/essay-question-adapter.ts";
|
||||
|
||||
export const giftAdapters = [multipleChoiceQuestionAdapter, essayQuestionAdapter];
|
||||
|
||||
export function getGiftAdapterForType(questionType: string): GiftAdapter | undefined {
|
||||
return giftAdapters.find((it) => it.questionType === questionType);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
export const multipleChoiceQuestionAdapter: GiftAdapter = {
|
||||
questionType: "MC",
|
||||
|
||||
installListener(
|
||||
questionElement: Element,
|
||||
answerUpdateCallback: (newAnswer: string | number | object) => void,
|
||||
): void {
|
||||
questionElement.querySelectorAll("input[type=radio]").forEach((element) => {
|
||||
const input = element as HTMLInputElement;
|
||||
|
||||
input.addEventListener("change", () => {
|
||||
answerUpdateCallback(parseInt(input.value));
|
||||
});
|
||||
// Optional: initialize value if already selected
|
||||
if (input.checked) {
|
||||
answerUpdateCallback(parseInt(input.value));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setAnswer(questionElement: Element, answer: string | number | object): void {
|
||||
questionElement.querySelectorAll("input[type=radio]").forEach((element) => {
|
||||
const input = element as HTMLInputElement;
|
||||
input.checked = String(answer) === String(input.value);
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
<script setup lang="ts">
|
||||
import { Language } from "@/data-objects/language.ts";
|
||||
import type { UseQueryReturnType } from "@tanstack/vue-query";
|
||||
import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import authService from "@/services/auth/auth-service.ts";
|
||||
import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
|
||||
import LearningObjectContentView from "@/views/learning-paths/learning-object/content/LearningObjectContentView.vue";
|
||||
import LearningObjectSubmissionsView from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue";
|
||||
|
||||
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,
|
||||
() => props.language,
|
||||
() => props.version,
|
||||
);
|
||||
const currentSubmission = ref<SubmissionData>([]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<using-query-result
|
||||
:query-result="learningObjectHtmlQueryResult as UseQueryReturnType<Document, Error>"
|
||||
v-slot="learningPathHtml: { data: Document }"
|
||||
>
|
||||
<learning-object-content-view
|
||||
:learning-object-content="learningPathHtml.data"
|
||||
v-model:submission-data="currentSubmission"
|
||||
/>
|
||||
<div class="content-submissions-spacer" />
|
||||
<learning-object-submissions-view
|
||||
v-if="props.group"
|
||||
:group="props.group"
|
||||
:hruid="props.hruid"
|
||||
:language="props.language"
|
||||
:version="props.version"
|
||||
v-model:submission-data="currentSubmission"
|
||||
/>
|
||||
</using-query-result>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(hr) {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
:deep(li) {
|
||||
margin-left: 30px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
:deep(img) {
|
||||
max-width: 80%;
|
||||
}
|
||||
:deep(h2),
|
||||
:deep(h3),
|
||||
:deep(h4),
|
||||
:deep(h5),
|
||||
:deep(h6) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.content-submissions-spacer {
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,90 @@
|
|||
<script setup lang="ts">
|
||||
import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
|
||||
import { getGiftAdapterForType } from "@/views/learning-paths/gift-adapters/gift-adapters.ts";
|
||||
import { computed, nextTick, onMounted, watch } from "vue";
|
||||
import { copyArrayWith } from "@/utils/array-utils.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
learningObjectContent: Document;
|
||||
submissionData?: SubmissionData;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<(e: "update:submissionData", value: SubmissionData) => void>();
|
||||
|
||||
const submissionData = computed<SubmissionData | undefined>({
|
||||
get: () => props.submissionData,
|
||||
set: (v?: SubmissionData): void => {
|
||||
if (v) emit("update:submissionData", v);
|
||||
},
|
||||
});
|
||||
|
||||
function forEachQuestion(
|
||||
doAction: (questionIndex: number, questionName: string, questionType: string, questionElement: Element) => void,
|
||||
): void {
|
||||
const questions = document.querySelectorAll(".gift-question");
|
||||
questions.forEach((question) => {
|
||||
const name = question.id.match(/gift-q(\d+)/)?.[1];
|
||||
const questionType = question.className
|
||||
.split(" ")
|
||||
.find((it) => it.startsWith("gift-question-type"))
|
||||
?.match(/gift-question-type-([^ ]*)/)?.[1];
|
||||
|
||||
if (!name || isNaN(parseInt(name)) || !questionType) return;
|
||||
|
||||
const index = parseInt(name) - 1;
|
||||
|
||||
doAction(index, name, questionType, question);
|
||||
});
|
||||
}
|
||||
|
||||
function attachQuestionListeners(): void {
|
||||
forEachQuestion((index, _name, type, element) => {
|
||||
getGiftAdapterForType(type)?.installListener(element, (newAnswer) => {
|
||||
submissionData.value = copyArrayWith(index, newAnswer, submissionData.value ?? []);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setAnswers(answers: SubmissionData): void {
|
||||
forEachQuestion((index, _name, type, element) => {
|
||||
const answer = answers[index];
|
||||
if (answer !== null && answer !== undefined) {
|
||||
getGiftAdapterForType(type)?.setAnswer(element, answer);
|
||||
} else if (answer === undefined) {
|
||||
answers[index] = null;
|
||||
}
|
||||
});
|
||||
submissionData.value = answers;
|
||||
}
|
||||
|
||||
onMounted(async () =>
|
||||
nextTick(() => {
|
||||
attachQuestionListeners();
|
||||
setAnswers(props.submissionData ?? []);
|
||||
}),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.learningObjectContent,
|
||||
async () => {
|
||||
await nextTick();
|
||||
attachQuestionListeners();
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.submissionData,
|
||||
async () => {
|
||||
await nextTick();
|
||||
setAnswers(props.submissionData ?? []);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="learning-object-container"
|
||||
v-html="learningObjectContent.body.innerHTML"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
1
frontend/src/views/learning-paths/learning-object/submission-data.d.ts
vendored
Normal file
1
frontend/src/views/learning-paths/learning-object/submission-data.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
export type SubmissionData = (string | number | object | null)[];
|
|
@ -0,0 +1,61 @@
|
|||
<script setup lang="ts">
|
||||
import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
allSubmissions: SubmissionDTO[];
|
||||
}>();
|
||||
const emit = defineEmits<(e: "submission-selected", submission: SubmissionDTO) => void>();
|
||||
|
||||
const headers = computed(() => [
|
||||
{ title: "#", value: "submissionNo", width: "50px" },
|
||||
{ title: t("submittedBy"), value: "submittedBy" },
|
||||
{ title: t("timestamp"), value: "timestamp" },
|
||||
{ title: "", key: "action", width: "70px", sortable: false },
|
||||
]);
|
||||
|
||||
const data = computed(() =>
|
||||
[...props.allSubmissions]
|
||||
.sort((a, b) => (a.submissionNumber ?? 0) - (b.submissionNumber ?? 0))
|
||||
.map((submission, index) => ({
|
||||
submissionNo: index + 1,
|
||||
submittedBy: `${submission.submitter.firstName} ${submission.submitter.lastName}`,
|
||||
timestamp: submission.time ? new Date(submission.time).toLocaleString() : "-",
|
||||
dto: submission,
|
||||
})),
|
||||
);
|
||||
|
||||
function selectSubmission(submission: SubmissionDTO): void {
|
||||
emit("submission-selected", submission);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>{{ t("groupSubmissions") }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="data"
|
||||
density="compact"
|
||||
hide-default-footer
|
||||
:no-data-text="t('noSubmissionsYet')"
|
||||
>
|
||||
<template v-slot:[`item.action`]="{ item }">
|
||||
<v-btn
|
||||
density="compact"
|
||||
variant="plain"
|
||||
@click="selectSubmission(item.dto)"
|
||||
>
|
||||
{{ t("loadSubmission") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,97 @@
|
|||
<script setup lang="ts">
|
||||
import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
|
||||
import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
|
||||
import { Language } from "@/data-objects/language.ts";
|
||||
import { useSubmissionsQuery } from "@/queries/submissions.ts";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import SubmitButton from "@/views/learning-paths/learning-object/submissions/SubmitButton.vue";
|
||||
import { computed, watch } from "vue";
|
||||
import LearningObjectSubmissionsTable from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsTable.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
submissionData?: SubmissionData;
|
||||
hruid: string;
|
||||
language: Language;
|
||||
version: number;
|
||||
group: { forGroup: number; assignmentNo: number; classId: string };
|
||||
}>();
|
||||
const emit = defineEmits<(e: "update:submissionData", value: SubmissionData) => void>();
|
||||
|
||||
const submissionQuery = useSubmissionsQuery(
|
||||
() => props.hruid,
|
||||
() => props.language,
|
||||
() => props.version,
|
||||
() => props.group.classId,
|
||||
() => props.group.assignmentNo,
|
||||
() => props.group.forGroup,
|
||||
() => true,
|
||||
);
|
||||
|
||||
function emitSubmissionData(submissionData: SubmissionData): void {
|
||||
emit("update:submissionData", submissionData);
|
||||
}
|
||||
|
||||
function emitSubmission(submission: SubmissionDTO): void {
|
||||
emitSubmissionData(JSON.parse(submission.content));
|
||||
}
|
||||
|
||||
watch(submissionQuery.data, () => {
|
||||
const submissions = submissionQuery.data.value;
|
||||
if (submissions && submissions.length > 0) {
|
||||
emitSubmission(submissions[submissions.length - 1]);
|
||||
} else {
|
||||
emitSubmissionData([]);
|
||||
}
|
||||
});
|
||||
|
||||
const lastSubmission = computed<SubmissionData>(() => {
|
||||
const submissions = submissionQuery.data.value;
|
||||
if (!submissions || submissions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(submissions[submissions.length - 1].content);
|
||||
});
|
||||
|
||||
const showSubmissionTable = computed(() => props.submissionData !== undefined && props.submissionData.length > 0);
|
||||
|
||||
const showIsDoneMessage = computed(() => lastSubmission.value !== undefined && lastSubmission.value.length === 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<using-query-result
|
||||
:query-result="submissionQuery"
|
||||
v-slot="submissions: { data: SubmissionDTO[] }"
|
||||
>
|
||||
<submit-button
|
||||
:hruid="props.hruid"
|
||||
:language="props.language"
|
||||
:version="props.version"
|
||||
:group="props.group"
|
||||
:submission-data="props.submissionData"
|
||||
:submissions="submissions.data"
|
||||
/>
|
||||
<div class="submit-submissions-spacer"></div>
|
||||
<v-alert
|
||||
icon="mdi-check"
|
||||
:text="t('taskCompleted')"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
v-if="showIsDoneMessage"
|
||||
></v-alert>
|
||||
<learning-object-submissions-table
|
||||
v-if="submissionQuery.data && showSubmissionTable"
|
||||
:all-submissions="submissions.data"
|
||||
@submission-selected="emitSubmission"
|
||||
/>
|
||||
</using-query-result>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.submit-submissions-spacer {
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,98 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import authService from "@/services/auth/auth-service.ts";
|
||||
import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
|
||||
import { Language } from "@/data-objects/language.ts";
|
||||
import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
|
||||
import { useCreateSubmissionMutation } from "@/queries/submissions.ts";
|
||||
import { deepEquals } from "@/utils/deep-equals.ts";
|
||||
import type { UserProfile } from "oidc-client-ts";
|
||||
import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
submissionData?: SubmissionData;
|
||||
submissions: SubmissionDTO[];
|
||||
hruid: string;
|
||||
language: Language;
|
||||
version: number;
|
||||
group: { forGroup: number; assignmentNo: number; classId: string };
|
||||
}>();
|
||||
|
||||
const {
|
||||
isPending: submissionIsPending,
|
||||
// - isError: submissionFailed,
|
||||
// - error: submissionError,
|
||||
// - isSuccess: submissionSuccess,
|
||||
mutate: submitSolution,
|
||||
} = useCreateSubmissionMutation();
|
||||
|
||||
const isStudent = computed(() => authService.authState.activeRole === "student");
|
||||
|
||||
const isSubmitDisabled = computed(() => {
|
||||
if (!props.submissionData || props.submissions === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (props.submissionData.some((answer) => answer === null)) {
|
||||
return false;
|
||||
}
|
||||
if (props.submissions.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return deepEquals(JSON.parse(props.submissions[props.submissions.length - 1].content), props.submissionData);
|
||||
});
|
||||
|
||||
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,
|
||||
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(props.submissionData),
|
||||
};
|
||||
submitSolution({ data: submission });
|
||||
}
|
||||
|
||||
const buttonText = computed(() => {
|
||||
if (props.submissionData && props.submissionData.length === 0) {
|
||||
return t("markAsDone");
|
||||
}
|
||||
return t(props.submissions.length > 0 ? "submitNewSolution" : "submitSolution");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
v-if="isStudent && !isSubmitDisabled"
|
||||
prepend-icon="mdi-check"
|
||||
variant="elevated"
|
||||
:loading="submissionIsPending"
|
||||
:disabled="isSubmitDisabled"
|
||||
@click="submitCurrentAnswer()"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
Loading…
Add table
Add a link
Reference in a new issue