style: fix linting issues met Prettier

This commit is contained in:
Lint Action 2025-04-18 23:36:22 +00:00
parent af8c783a26
commit 5168ceaee0
56 changed files with 680 additions and 741 deletions

View file

@ -5,8 +5,8 @@ import learningPathService from '../services/learning-paths/learning-path-servic
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { BadRequestException } from '../exceptions/bad-request-exception.js'; import { BadRequestException } from '../exceptions/bad-request-exception.js';
import { NotFoundException } from '../exceptions/not-found-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js';
import {Group} from "../entities/assignments/group.entity"; import { Group } from '../entities/assignments/group.entity';
import {getAssignmentRepository, getGroupRepository} from "../data/repositories"; import { getAssignmentRepository, getGroupRepository } from '../data/repositories';
/** /**
* Fetch learning paths based on query parameters. * Fetch learning paths based on query parameters.
@ -27,13 +27,9 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
if (!assignmentNo || !classId) { if (!assignmentNo || !classId) {
throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.'); throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.');
} }
const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId( const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo));
classId, parseInt(assignmentNo)
);
if (assignment) { if (assignment) {
forGroup = await getGroupRepository().findByAssignmentAndGroupNumber( forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined;
assignment, parseInt(forGroupNo)
) ?? undefined;
} }
} }
@ -56,11 +52,6 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
hruidList = themes.flatMap((theme) => theme.hruids); hruidList = themes.flatMap((theme) => theme.hruids);
} }
const learningPaths = await learningPathService.fetchLearningPaths( const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup);
hruidList,
language as Language,
`HRUIDs: ${hruidList.join(', ')}`,
forGroup
);
res.json(learningPaths.data); res.json(learningPaths.data);
} }

View file

@ -62,18 +62,15 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
/** /**
* Looks up all submissions for the given learning object which were submitted as part of the given assignment. * Looks up all submissions for the given learning object which were submitted as part of the given assignment.
*/ */
public async findAllSubmissionsForLearningObjectAndAssignment( public async findAllSubmissionsForLearningObjectAndAssignment(loId: LearningObjectIdentifier, assignment: Assignment): Promise<Submission[]> {
loId: LearningObjectIdentifier,
assignment: Assignment,
): Promise<Submission[]> {
return this.findAll({ return this.findAll({
where: { where: {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,
learningObjectVersion: loId.version, learningObjectVersion: loId.version,
onBehalfOf: { onBehalfOf: {
assignment assignment,
} },
}, },
}); });
} }
@ -81,16 +78,13 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
/** /**
* Looks up all submissions for the given learning object which were submitted by the given group * Looks up all submissions for the given learning object which were submitted by the given group
*/ */
public async findAllSubmissionsForLearningObjectAndGroup( public async findAllSubmissionsForLearningObjectAndGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission[]> {
loId: LearningObjectIdentifier,
group: Group,
): Promise<Submission[]> {
return this.findAll({ return this.findAll({
where: { where: {
learningObjectHruid: loId.hruid, learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language, learningObjectLanguage: loId.language,
learningObjectVersion: loId.version, learningObjectVersion: loId.version,
onBehalfOf: group onBehalfOf: group,
}, },
}); });
} }

View file

@ -1,10 +1,10 @@
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { LearningPath } from '../../entities/content/learning-path.entity.js'; import { LearningPath } from '../../entities/content/learning-path.entity.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {LearningPathNode} from "../../entities/content/learning-path-node.entity"; import { LearningPathNode } from '../../entities/content/learning-path-node.entity';
import {RequiredEntityData} from "@mikro-orm/core"; import { RequiredEntityData } from '@mikro-orm/core';
import {LearningPathTransition} from "../../entities/content/learning-path-transition.entity"; import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity';
import {EntityAlreadyExistsException} from "../../exceptions/entity-already-exists-exception"; import { EntityAlreadyExistsException } from '../../exceptions/entity-already-exists-exception';
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
@ -28,32 +28,26 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
}); });
} }
public createNode( public createNode(nodeData: RequiredEntityData<LearningPathNode>): LearningPathNode {
nodeData: RequiredEntityData<LearningPathNode>
): LearningPathNode {
return this.em.create(LearningPathNode, nodeData); return this.em.create(LearningPathNode, nodeData);
} }
public createTransition( public createTransition(transitionData: RequiredEntityData<LearningPathTransition>): LearningPathTransition {
transitionData: RequiredEntityData<LearningPathTransition> return this.em.create(LearningPathTransition, transitionData);
): LearningPathTransition {
return this.em.create(LearningPathTransition, transitionData)
} }
public async saveLearningPathNodesAndTransitions( public async saveLearningPathNodesAndTransitions(
path: LearningPath, path: LearningPath,
nodes: LearningPathNode[], nodes: LearningPathNode[],
transitions: LearningPathTransition[], transitions: LearningPathTransition[],
options?: {preventOverwrite?: boolean} options?: { preventOverwrite?: boolean }
): Promise<void> { ): Promise<void> {
if (options?.preventOverwrite && (await this.findOne(path))) { if (options?.preventOverwrite && (await this.findOne(path))) {
throw new EntityAlreadyExistsException( throw new EntityAlreadyExistsException('A learning path with this hruid/language combination already exists.');
"A learning path with this hruid/language combination already exists."
);
} }
const em = this.getEntityManager(); const em = this.getEntityManager();
await em.persistAndFlush(path); await em.persistAndFlush(path);
await Promise.all(nodes.map(async it => em.persistAndFlush(it))); await Promise.all(nodes.map(async (it) => em.persistAndFlush(it)));
await Promise.all(transitions.map(async it => em.persistAndFlush(it))); await Promise.all(transitions.map(async (it) => em.persistAndFlush(it)));
} }
} }

View file

@ -1,4 +1,4 @@
import {ArrayType, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property} from '@mikro-orm/core'; import { ArrayType, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Attachment } from './attachment.entity.js'; import { Attachment } from './attachment.entity.js';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
@ -42,7 +42,7 @@ export class LearningObject {
@Property({ type: 'array' }) @Property({ type: 'array' })
keywords: string[] = []; keywords: string[] = [];
@Property({ type: new ArrayType(i => Number(i)), nullable: true }) @Property({ type: new ArrayType((i) => Number(i)), nullable: true })
targetAges?: number[] = []; targetAges?: number[] = [];
@Property({ type: 'bool' }) @Property({ type: 'bool' })

View file

@ -1,4 +1,4 @@
import {Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel} from '@mikro-orm/core'; import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core';
import { LearningPath } from './learning-path.entity.js'; import { LearningPath } from './learning-path.entity.js';
import { LearningPathTransition } from './learning-path-transition.entity.js'; import { LearningPathTransition } from './learning-path-transition.entity.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';

View file

@ -1,4 +1,4 @@
import {Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property} from '@mikro-orm/core'; import { Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; import { LearningPathRepository } from '../../data/content/learning-path-repository.js';
import { LearningPathNode } from './learning-path-node.entity.js'; import { LearningPathNode } from './learning-path-node.entity.js';

View file

@ -9,7 +9,7 @@ export function errorHandler(err: unknown, _req: Request, res: Response, _: Next
logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`); logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`);
res.status(err.status).json(err); res.status(err.status).json(err);
} else { } else {
logger.error(`Unexpected error occurred while handing a request: ${(err as {stack: string})?.stack ?? JSON.stringify(err)}`); logger.error(`Unexpected error occurred while handing a request: ${(err as { stack: string })?.stack ?? JSON.stringify(err)}`);
res.status(500).json(err); res.status(500).json(err);
} }
} }

View file

@ -8,7 +8,7 @@ import {
LearningPathResponse, LearningPathResponse,
} from '@dwengo-1/common/interfaces/learning-content'; } from '@dwengo-1/common/interfaces/learning-content';
import { getLogger } from '../logging/initalize.js'; import { getLogger } from '../logging/initalize.js';
import {v4} from "uuid"; import { v4 } from 'uuid';
function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject {
return { return {

View file

@ -32,8 +32,7 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL
educationalGoals: learningObject.educationalGoals, educationalGoals: learningObject.educationalGoals,
returnValue: { returnValue: {
callback_url: learningObject.returnValue.callbackUrl, callback_url: learningObject.returnValue.callbackUrl,
callback_schema: learningObject.returnValue.callbackSchema === "" ? "" callback_schema: learningObject.returnValue.callbackSchema === '' ? '' : JSON.parse(learningObject.returnValue.callbackSchema),
: JSON.parse(learningObject.returnValue.callbackSchema),
}, },
skosConcepts: learningObject.skosConcepts, skosConcepts: learningObject.skosConcepts,
targetAges: learningObject.targetAges || [], targetAges: learningObject.targetAges || [],

View file

@ -11,7 +11,7 @@ import {
LearningPathIdentifier, LearningPathIdentifier,
LearningPathResponse, LearningPathResponse,
} from '@dwengo-1/common/interfaces/learning-content'; } from '@dwengo-1/common/interfaces/learning-content';
import {v4} from "uuid"; import { v4 } from 'uuid';
const logger: Logger = getLogger(); const logger: Logger = getLogger();

View file

@ -13,9 +13,9 @@ import {
Transition, Transition,
} from '@dwengo-1/common/interfaces/learning-content'; } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {Group} from "../../entities/assignments/group.entity"; import { Group } from '../../entities/assignments/group.entity';
import {Collection} from "@mikro-orm/core"; import { Collection } from '@mikro-orm/core';
import {v4} from "uuid"; import { v4 } from 'uuid';
/** /**
* Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its * Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its
@ -165,7 +165,7 @@ function convertTransition(
_id: String(index), // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path. _id: String(index), // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
default: false, // We don't work with default transitions but retain this for backwards compatibility. default: false, // We don't work with default transitions but retain this for backwards compatibility.
next: { next: {
_id: nextNode._id ? (nextNode._id + index) : v4(), // Construct a unique ID for the transition for backwards compatibility. _id: nextNode._id ? nextNode._id + index : v4(), // Construct a unique ID for the transition for backwards compatibility.
hruid: transition.next.learningObjectHruid, hruid: transition.next.learningObjectHruid,
language: nextNode.language, language: nextNode.language,
version: nextNode.version, version: nextNode.version,
@ -181,12 +181,7 @@ const databaseLearningPathProvider: LearningPathProvider = {
/** /**
* Fetch the learning paths with the given hruids from the database. * Fetch the learning paths with the given hruids from the database.
*/ */
async fetchLearningPaths( async fetchLearningPaths(hruids: string[], language: Language, source: string, personalizedFor?: Group): Promise<LearningPathResponse> {
hruids: string[],
language: Language,
source: string,
personalizedFor?: Group
): Promise<LearningPathResponse> {
const learningPathRepo = getLearningPathRepository(); const learningPathRepo = getLearningPathRepository();
const learningPaths = (await Promise.all(hruids.map(async (hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter( const learningPaths = (await Promise.all(hruids.map(async (hruid) => learningPathRepo.findByHruidAndLanguage(hruid, language)))).filter(
@ -211,7 +206,7 @@ const databaseLearningPathProvider: LearningPathProvider = {
const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language); const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language);
return await Promise.all(searchResults.map(async (result, index) => convertLearningPath(result, index, personalizedFor))); return await Promise.all(searchResults.map(async (result, index) => convertLearningPath(result, index, personalizedFor)));
} },
}; };
export default databaseLearningPathProvider; export default databaseLearningPathProvider;

View file

@ -1,6 +1,6 @@
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { Group } from "../../entities/assignments/group.entity"; import { Group } from '../../entities/assignments/group.entity';
/** /**
* Generic interface for a service which provides access to learning paths from a data source. * Generic interface for a service which provides access to learning paths from a data source.

View file

@ -1,25 +1,23 @@
import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js';
import databaseLearningPathProvider from './database-learning-path-provider.js'; import databaseLearningPathProvider from './database-learning-path-provider.js';
import { envVars, getEnvVar } from '../../util/envVars.js'; import { envVars, getEnvVar } from '../../util/envVars.js';
import {LearningObjectNode, LearningPath, LearningPathResponse} from '@dwengo-1/common/interfaces/learning-content'; import { LearningObjectNode, LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {Group} from "../../entities/assignments/group.entity"; import { Group } from '../../entities/assignments/group.entity';
import {LearningPath as LearningPathEntity} from "../../entities/content/learning-path.entity"; import { LearningPath as LearningPathEntity } from '../../entities/content/learning-path.entity';
import {getLearningPathRepository} from "../../data/repositories"; import { getLearningPathRepository } from '../../data/repositories';
import {LearningPathNode} from "../../entities/content/learning-path-node.entity"; import { LearningPathNode } from '../../entities/content/learning-path-node.entity';
import {LearningPathTransition} from "../../entities/content/learning-path-transition.entity"; import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity';
import {base64ToArrayBuffer} from "../../util/base64-buffer-conversion"; import { base64ToArrayBuffer } from '../../util/base64-buffer-conversion';
import {TeacherDTO} from "@dwengo-1/common/interfaces/teacher"; import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
import {mapToTeacher} from "../../interfaces/teacher"; import { mapToTeacher } from '../../interfaces/teacher';
import {Collection} from "@mikro-orm/core"; import { Collection } from '@mikro-orm/core';
const userContentPrefix = getEnvVar(envVars.UserContentPrefix); const userContentPrefix = getEnvVar(envVars.UserContentPrefix);
const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider]; const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider];
export function mapToLearningPath( export function mapToLearningPath(dto: LearningPath, adminsDto: TeacherDTO[]): LearningPathEntity {
dto: LearningPath, adminsDto: TeacherDTO[] const admins = adminsDto.map((admin) => mapToTeacher(admin));
): LearningPathEntity {
const admins = adminsDto.map(admin => mapToTeacher(admin));
const repo = getLearningPathRepository(); const repo = getLearningPathRepository();
const path = repo.create({ const path = repo.create({
hruid: dto.hruid, hruid: dto.hruid,
@ -27,7 +25,7 @@ export function mapToLearningPath(
description: dto.description, description: dto.description,
title: dto.title, title: dto.title,
admins, admins,
image: dto.image ? Buffer.from(base64ToArrayBuffer(dto.image)) : null image: dto.image ? Buffer.from(base64ToArrayBuffer(dto.image)) : null,
}); });
const nodes = dto.nodes.map((nodeDto: LearningObjectNode, i: number) => const nodes = dto.nodes.map((nodeDto: LearningObjectNode, i: number) =>
repo.createNode({ repo.createNode({
@ -38,33 +36,34 @@ export function mapToLearningPath(
version: nodeDto.version, version: nodeDto.version,
startNode: nodeDto.start_node ?? false, startNode: nodeDto.start_node ?? false,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
}) })
); );
dto.nodes.forEach(nodeDto => { dto.nodes.forEach((nodeDto) => {
const fromNode = nodes.find(it => const fromNode = nodes.find(
it.learningObjectHruid === nodeDto.learningobject_hruid (it) => it.learningObjectHruid === nodeDto.learningobject_hruid && it.language === nodeDto.language && it.version === nodeDto.version
&& it.language === nodeDto.language
&& it.version === nodeDto.version
)!; )!;
const transitions = nodeDto.transitions.map((transDto, i) => { const transitions = nodeDto.transitions
const toNode = nodes.find(it => .map((transDto, i) => {
it.learningObjectHruid === transDto.next.hruid const toNode = nodes.find(
&& it.language === transDto.next.language (it) =>
&& it.version === transDto.next.version it.learningObjectHruid === transDto.next.hruid &&
); it.language === transDto.next.language &&
it.version === transDto.next.version
);
if (toNode) { if (toNode) {
return repo.createTransition({ return repo.createTransition({
transitionNumber: i, transitionNumber: i,
node: fromNode, node: fromNode,
next: toNode, next: toNode,
condition: transDto.condition ?? "true" condition: transDto.condition ?? 'true',
}); });
} }
return undefined; return undefined;
})
}).filter(it => it).map(it => it!); .filter((it) => it)
.map((it) => it!);
fromNode.transitions = new Collection<LearningPathTransition>(fromNode, transitions); fromNode.transitions = new Collection<LearningPathTransition>(fromNode, transitions);
}); });
@ -85,12 +84,7 @@ const learningPathService = {
* @param source * @param source
* @param personalizedFor If this is set, a learning path personalized for the given group or student will be returned. * @param personalizedFor If this is set, a learning path personalized for the given group or student will be returned.
*/ */
async fetchLearningPaths( async fetchLearningPaths(hruids: string[], language: Language, source: string, personalizedFor?: Group): Promise<LearningPathResponse> {
hruids: string[],
language: Language,
source: string,
personalizedFor?: Group
): Promise<LearningPathResponse> {
const userContentHruids = hruids.filter((hruid) => hruid.startsWith(userContentPrefix)); const userContentHruids = hruids.filter((hruid) => hruid.startsWith(userContentPrefix));
const nonUserContentHruids = hruids.filter((hruid) => !hruid.startsWith(userContentPrefix)); const nonUserContentHruids = hruids.filter((hruid) => !hruid.startsWith(userContentPrefix));
@ -129,8 +123,8 @@ const learningPathService = {
async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<void> { async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<void> {
const repo = getLearningPathRepository(); const repo = getLearningPathRepository();
const path = mapToLearningPath(dto, admins); const path = mapToLearningPath(dto, admins);
await repo.save(path, {preventOverwrite: true}) await repo.save(path, { preventOverwrite: true });
} },
}; };
export default learningPathService; export default learningPathService;

View file

@ -1,4 +1,4 @@
import {getAssignmentRepository, getGroupRepository, getSubmissionRepository} from '../data/repositories.js'; import { getAssignmentRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { NotFoundException } from '../exceptions/not-found-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js';
import { mapToSubmission, mapToSubmissionDTO } from '../interfaces/submission.js'; import { mapToSubmission, mapToSubmissionDTO } from '../interfaces/submission.js';

View file

@ -27,10 +27,7 @@ export class SqliteAutoincrementSubscriber implements EventSubscriber {
for (const prop of Object.values(args.meta.properties)) { for (const prop of Object.values(args.meta.properties)) {
const property = prop as EntityProperty<T>; const property = prop as EntityProperty<T>;
if ( if (property.primary && property.autoincrement && (args.entity as Record<string, unknown>)[property.name] === undefined) {
property.primary && property.autoincrement
&& (args.entity as Record<string, unknown>)[property.name] === undefined
) {
// Obtain and increment sequence number of this entity. // Obtain and increment sequence number of this entity.
const propertyKey = args.meta.class.name + '.' + property.name; const propertyKey = args.meta.class.name + '.' + property.name;
const nextSeqNumber = this.sequenceNumbersForEntityType.get(propertyKey) || 0; const nextSeqNumber = this.sequenceNumbersForEntityType.get(propertyKey) || 0;

View file

@ -33,8 +33,7 @@ describe('AssignmentRepository', () => {
it('should find all by username of the responsible teacher', async () => { it('should find all by username of the responsible teacher', async () => {
const result = await assignmentRepository.findAllByResponsibleTeacher('testleerkracht1'); const result = await assignmentRepository.findAllByResponsibleTeacher('testleerkracht1');
const resultIds = result.map((it) => it.id) const resultIds = result.map((it) => it.id).sort((a, b) => (a ?? 0) - (b ?? 0));
.sort((a, b) => (a ?? 0) - (b ?? 0));
expect(resultIds).toEqual([1, 1, 3, 4]); expect(resultIds).toEqual([1, 1, 3, 4]);
}); });

View file

@ -91,11 +91,9 @@ describe('SubmissionRepository', () => {
expect(result[2].submissionNumber).toBe(3); expect(result[2].submissionNumber).toBe(3);
}); });
it("should find only the submissions for a certain learning object and assignment made for the given group", async () => { it('should find only the submissions for a certain learning object and assignment made for the given group', async () => {
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 2); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 2);
const result = await submissionRepository.findAllSubmissionsForLearningObjectAndGroup( const result = await submissionRepository.findAllSubmissionsForLearningObjectAndGroup(loId, group!);
loId, group!
);
expect(result).toHaveLength(1); expect(result).toHaveLength(1);

View file

@ -5,8 +5,8 @@ import { AttachmentRepository } from '../../../src/data/content/attachment-repos
import { LearningObject } from '../../../src/entities/content/learning-object.entity.js'; import { LearningObject } from '../../../src/entities/content/learning-object.entity.js';
import { Attachment } from '../../../src/entities/content/attachment.entity.js'; import { Attachment } from '../../../src/entities/content/attachment.entity.js';
import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier.js';
import {testLearningObjectPnNotebooks} from "../../test_assets/content/learning-objects.testdata"; import { testLearningObjectPnNotebooks } from '../../test_assets/content/learning-objects.testdata';
import {v4 as uuidV4} from "uuid"; import { v4 as uuidV4 } from 'uuid';
describe('AttachmentRepository', () => { describe('AttachmentRepository', () => {
let attachmentRepo: AttachmentRepository; let attachmentRepo: AttachmentRepository;
@ -26,7 +26,7 @@ describe('AttachmentRepository', () => {
newLearningObjectData.version = 101; newLearningObjectData.version = 101;
newLearningObjectData.attachments = []; newLearningObjectData.attachments = [];
newLearningObjectData.uuid = uuidV4(); newLearningObjectData.uuid = uuidV4();
newLearningObjectData.content = Buffer.from("Content of the newer example"); newLearningObjectData.content = Buffer.from('Content of the newer example');
newLearningObject = learningObjectRepo.create(newLearningObjectData); newLearningObject = learningObjectRepo.create(newLearningObjectData);
await learningObjectRepo.save(newLearningObject); await learningObjectRepo.save(newLearningObject);
@ -36,7 +36,7 @@ describe('AttachmentRepository', () => {
it('allows us to add attachments with the same name to a different learning object without throwing an error', async () => { it('allows us to add attachments with the same name to a different learning object without throwing an error', async () => {
attachmentOnlyNewer = structuredClone(attachmentsOlderLearningObject[0]); attachmentOnlyNewer = structuredClone(attachmentsOlderLearningObject[0]);
attachmentOnlyNewer.learningObject = newLearningObject; attachmentOnlyNewer.learningObject = newLearningObject;
attachmentOnlyNewer.content = Buffer.from("New attachment content"); attachmentOnlyNewer.content = Buffer.from('New attachment content');
await attachmentRepo.save(attachmentRepo.create(attachmentOnlyNewer)); await attachmentRepo.save(attachmentRepo.create(attachmentOnlyNewer));
}); });
@ -49,10 +49,7 @@ describe('AttachmentRepository', () => {
version: testLearningObjectPnNotebooks.version, version: testLearningObjectPnNotebooks.version,
}; };
const result = await attachmentRepo.findByLearningObjectIdAndName( const result = await attachmentRepo.findByLearningObjectIdAndName(olderLearningObjectId, attachmentsOlderLearningObject[0].name);
olderLearningObjectId,
attachmentsOlderLearningObject[0].name
);
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(result!.name).toEqual(attachmentsOlderLearningObject[0].name); expect(result!.name).toEqual(attachmentsOlderLearningObject[0].name);
expect(result!.content).toEqual(attachmentsOlderLearningObject[0].content); expect(result!.content).toEqual(attachmentsOlderLearningObject[0].content);

View file

@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
import { setupTestApp } from '../../setup-tests.js'; import { setupTestApp } from '../../setup-tests.js';
import { getAttachmentRepository } from '../../../src/data/repositories.js'; import { getAttachmentRepository } from '../../../src/data/repositories.js';
import { AttachmentRepository } from '../../../src/data/content/attachment-repository.js'; import { AttachmentRepository } from '../../../src/data/content/attachment-repository.js';
import { testLearningObject02 } from "../../test_assets/content/learning-objects.testdata"; import { testLearningObject02 } from '../../test_assets/content/learning-objects.testdata';
describe('AttachmentRepository', () => { describe('AttachmentRepository', () => {
let attachmentRepository: AttachmentRepository; let attachmentRepository: AttachmentRepository;

View file

@ -4,12 +4,8 @@ import { setupTestApp } from '../../setup-tests.js';
import { getLearningObjectRepository } from '../../../src/data/repositories.js'; import { getLearningObjectRepository } from '../../../src/data/repositories.js';
import { LearningObject } from '../../../src/entities/content/learning-object.entity.js'; import { LearningObject } from '../../../src/entities/content/learning-object.entity.js';
import { expectToBeCorrectEntity } from '../../test-utils/expectations.js'; import { expectToBeCorrectEntity } from '../../test-utils/expectations.js';
import { import { testLearningObject01, testLearningObject02, testLearningObject03 } from '../../test_assets/content/learning-objects.testdata';
testLearningObject01, import { v4 } from 'uuid';
testLearningObject02,
testLearningObject03
} from "../../test_assets/content/learning-objects.testdata";
import {v4} from "uuid";
describe('LearningObjectRepository', () => { describe('LearningObjectRepository', () => {
let learningObjectRepository: LearningObjectRepository; let learningObjectRepository: LearningObjectRepository;
@ -43,26 +39,22 @@ describe('LearningObjectRepository', () => {
it('should allow a learning object with the same id except a different version to be added', async () => { it('should allow a learning object with the same id except a different version to be added', async () => {
const testLearningObject01Newer = structuredClone(testLearningObject01); const testLearningObject01Newer = structuredClone(testLearningObject01);
testLearningObject01Newer.version = 10; testLearningObject01Newer.version = 10;
testLearningObject01Newer.title += " (nieuw)"; testLearningObject01Newer.title += ' (nieuw)';
testLearningObject01Newer.uuid = v4(); testLearningObject01Newer.uuid = v4();
testLearningObject01Newer.content = Buffer.from("This is the new content."); testLearningObject01Newer.content = Buffer.from('This is the new content.');
newerExample = learningObjectRepository.create(testLearningObject01Newer); newerExample = learningObjectRepository.create(testLearningObject01Newer);
await learningObjectRepository.save(newerExample); await learningObjectRepository.save(newerExample);
}); });
it('should return the newest version of the learning object when queried by only hruid and language', async () => { it('should return the newest version of the learning object when queried by only hruid and language', async () => {
const result = await learningObjectRepository.findLatestByHruidAndLanguage( const result = await learningObjectRepository.findLatestByHruidAndLanguage(newerExample.hruid, newerExample.language);
newerExample.hruid, newerExample.language
);
expect(result).toBeInstanceOf(LearningObject); expect(result).toBeInstanceOf(LearningObject);
expect(result?.version).toBe(10); expect(result?.version).toBe(10);
expect(result?.title).toContain('(nieuw)'); expect(result?.title).toContain('(nieuw)');
}); });
it('should return null when queried by non-existing hruid or language', async () => { it('should return null when queried by non-existing hruid or language', async () => {
const result = await learningObjectRepository.findLatestByHruidAndLanguage( const result = await learningObjectRepository.findLatestByHruidAndLanguage('something_that_does_not_exist', testLearningObject01.language);
'something_that_does_not_exist', testLearningObject01.language
);
expect(result).toBe(null); expect(result).toBe(null);
}); });
}); });

View file

@ -4,7 +4,7 @@ import { getLearningObjectRepository } from '../../../src/data/repositories';
import { setupTestApp } from '../../setup-tests'; import { setupTestApp } from '../../setup-tests';
import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {testLearningObject01} from "../../test_assets/content/learning-objects.testdata"; import { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata';
describe('LearningObjectRepository', () => { describe('LearningObjectRepository', () => {
let learningObjectRepository: LearningObjectRepository; let learningObjectRepository: LearningObjectRepository;

View file

@ -3,14 +3,10 @@ import { setupTestApp } from '../../setup-tests.js';
import { getLearningPathRepository } from '../../../src/data/repositories.js'; import { getLearningPathRepository } from '../../../src/data/repositories.js';
import { LearningPathRepository } from '../../../src/data/content/learning-path-repository.js'; import { LearningPathRepository } from '../../../src/data/content/learning-path-repository.js';
import { LearningPath } from '../../../src/entities/content/learning-path.entity.js'; import { LearningPath } from '../../../src/entities/content/learning-path.entity.js';
import { import { expectToBeCorrectEntity, expectToHaveFoundNothing, expectToHaveFoundPrecisely } from '../../test-utils/expectations.js';
expectToBeCorrectEntity,
expectToHaveFoundNothing,
expectToHaveFoundPrecisely
} from '../../test-utils/expectations.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {testLearningPath01} from "../../test_assets/content/learning-paths.testdata"; import { testLearningPath01 } from '../../test_assets/content/learning-paths.testdata';
import {mapToLearningPath} from "../../../src/services/learning-paths/learning-path-service"; import { mapToLearningPath } from '../../../src/services/learning-paths/learning-path-service';
describe('LearningPathRepository', () => { describe('LearningPathRepository', () => {
let learningPathRepo: LearningPathRepository; let learningPathRepo: LearningPathRepository;
@ -24,10 +20,7 @@ describe('LearningPathRepository', () => {
}); });
it('should return a learning path when it is queried by hruid and language', async () => { it('should return a learning path when it is queried by hruid and language', async () => {
const result = await learningPathRepo.findByHruidAndLanguage( const result = await learningPathRepo.findByHruidAndLanguage(testLearningPath01.hruid, testLearningPath01.language as Language);
testLearningPath01.hruid,
testLearningPath01.language as Language
);
expect(result).toBeInstanceOf(LearningPath); expect(result).toBeInstanceOf(LearningPath);
expectToBeCorrectEntity(result!, examplePath); expectToBeCorrectEntity(result!, examplePath);
}); });

View file

@ -3,7 +3,7 @@ import { getLearningPathRepository } from '../../../src/data/repositories';
import { LearningPathRepository } from '../../../src/data/content/learning-path-repository'; import { LearningPathRepository } from '../../../src/data/content/learning-path-repository';
import { setupTestApp } from '../../setup-tests'; import { setupTestApp } from '../../setup-tests';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {testLearningPath01} from "../../test_assets/content/learning-paths.testdata"; import { testLearningPath01 } from '../../test_assets/content/learning-paths.testdata';
describe('LearningPathRepository', () => { describe('LearningPathRepository', () => {
let learningPathRepository: LearningPathRepository; let learningPathRepository: LearningPathRepository;
@ -20,9 +20,7 @@ describe('LearningPathRepository', () => {
}); });
it('should return requested learning path', async () => { it('should return requested learning path', async () => {
const learningPath = await learningPathRepository.findByHruidAndLanguage( const learningPath = await learningPathRepository.findByHruidAndLanguage(testLearningPath01.hruid, testLearningPath01.language as Language);
testLearningPath01.hruid, testLearningPath01.language as Language
);
expect(learningPath).toBeTruthy(); expect(learningPath).toBeTruthy();
expect(learningPath?.title).toBe(testLearningPath01.title); expect(learningPath?.title).toBe(testLearningPath01.title);

View file

@ -4,18 +4,12 @@ import { LearningObject } from '../../../src/entities/content/learning-object.en
import databaseLearningObjectProvider from '../../../src/services/learning-objects/database-learning-object-provider'; import databaseLearningObjectProvider from '../../../src/services/learning-objects/database-learning-object-provider';
import { expectToBeCorrectFilteredLearningObject } from '../../test-utils/expectations'; import { expectToBeCorrectFilteredLearningObject } from '../../test-utils/expectations';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { import { FilteredLearningObject, LearningObjectNode, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
FilteredLearningObject, import { testPartiallyDatabaseAndPartiallyDwengoApiLearningPath } from '../../test_assets/content/learning-paths.testdata';
LearningObjectNode, import { testLearningObjectPnNotebooks } from '../../test_assets/content/learning-objects.testdata';
LearningPathIdentifier
} from '@dwengo-1/common/interfaces/learning-content';
import {
testPartiallyDatabaseAndPartiallyDwengoApiLearningPath
} from "../../test_assets/content/learning-paths.testdata";
import {testLearningObjectPnNotebooks} from "../../test_assets/content/learning-objects.testdata";
import { LearningPath } from '@dwengo-1/common/dist/interfaces/learning-content'; import { LearningPath } from '@dwengo-1/common/dist/interfaces/learning-content';
import {RequiredEntityData} from "@mikro-orm/core"; import { RequiredEntityData } from '@mikro-orm/core';
import {getHtmlRenderingForTestLearningObject} from "../../test-utils/get-html-rendering"; import { getHtmlRenderingForTestLearningObject } from '../../test-utils/get-html-rendering';
const EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT = 'Notebook opslaan'; const EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT = 'Notebook opslaan';
@ -31,7 +25,7 @@ describe('DatabaseLearningObjectProvider', () => {
exampleLearningPathId = { exampleLearningPathId = {
hruid: exampleLearningPath.hruid, hruid: exampleLearningPath.hruid,
language: exampleLearningPath.language as Language language: exampleLearningPath.language as Language,
}; };
}); });
describe('getLearningObjectById', () => { describe('getLearningObjectById', () => {
@ -75,9 +69,7 @@ describe('DatabaseLearningObjectProvider', () => {
describe('getLearningObjectIdsFromPath', () => { describe('getLearningObjectIdsFromPath', () => {
it('should return all learning object IDs from a path', async () => { it('should return all learning object IDs from a path', async () => {
const result = await databaseLearningObjectProvider.getLearningObjectIdsFromPath(exampleLearningPathId); const result = await databaseLearningObjectProvider.getLearningObjectIdsFromPath(exampleLearningPathId);
expect(new Set(result)).toEqual( expect(new Set(result)).toEqual(new Set(exampleLearningPath.nodes.map((it: LearningObjectNode) => it.learningobject_hruid)));
new Set(exampleLearningPath.nodes.map((it: LearningObjectNode) => it.learningobject_hruid))
);
}); });
it('should throw an error if queried with a path identifier for which there is no learning path', async () => { it('should throw an error if queried with a path identifier for which there is no learning path', async () => {
await expect( await expect(

View file

@ -3,18 +3,12 @@ import { setupTestApp } from '../../setup-tests';
import { LearningObject } from '../../../src/entities/content/learning-object.entity'; import { LearningObject } from '../../../src/entities/content/learning-object.entity';
import learningObjectService from '../../../src/services/learning-objects/learning-object-service'; import learningObjectService from '../../../src/services/learning-objects/learning-object-service';
import { envVars, getEnvVar } from '../../../src/util/envVars'; import { envVars, getEnvVar } from '../../../src/util/envVars';
import { import { LearningObjectIdentifierDTO, LearningPath as LearningPathDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
LearningObjectIdentifierDTO,
LearningPath as LearningPathDTO,
LearningPathIdentifier
} from '@dwengo-1/common/interfaces/learning-content';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {testLearningObjectPnNotebooks} from "../../test_assets/content/learning-objects.testdata"; import { testLearningObjectPnNotebooks } from '../../test_assets/content/learning-objects.testdata';
import { import { testPartiallyDatabaseAndPartiallyDwengoApiLearningPath } from '../../test_assets/content/learning-paths.testdata';
testPartiallyDatabaseAndPartiallyDwengoApiLearningPath import { RequiredEntityData } from '@mikro-orm/core';
} from "../../test_assets/content/learning-paths.testdata"; import { getHtmlRenderingForTestLearningObject } from '../../test-utils/get-html-rendering';
import {RequiredEntityData} from "@mikro-orm/core";
import {getHtmlRenderingForTestLearningObject} from "../../test-utils/get-html-rendering";
const EXPECTED_DWENGO_LEARNING_OBJECT_TITLE = 'Werken met notebooks'; const EXPECTED_DWENGO_LEARNING_OBJECT_TITLE = 'Werken met notebooks';
const DWENGO_TEST_LEARNING_OBJECT_ID: LearningObjectIdentifierDTO = { const DWENGO_TEST_LEARNING_OBJECT_ID: LearningObjectIdentifierDTO = {
@ -41,8 +35,8 @@ describe('LearningObjectService', () => {
exampleLearningPathId = { exampleLearningPathId = {
hruid: exampleLearningPath.hruid, hruid: exampleLearningPath.hruid,
language: exampleLearningPath.language as Language language: exampleLearningPath.language as Language,
} };
}); });
describe('getLearningObjectById', () => { describe('getLearningObjectById', () => {
@ -99,9 +93,7 @@ describe('LearningObjectService', () => {
describe('getLearningObjectsFromPath', () => { describe('getLearningObjectsFromPath', () => {
it('returns all learning objects when a learning path in the database is queried', async () => { it('returns all learning objects when a learning path in the database is queried', async () => {
const result = await learningObjectService.getLearningObjectsFromPath(exampleLearningPathId); const result = await learningObjectService.getLearningObjectsFromPath(exampleLearningPathId);
expect(result.map(it=> it.key)).toEqual( expect(result.map((it) => it.key)).toEqual(exampleLearningPath.nodes.map((it) => it.learningobject_hruid));
exampleLearningPath.nodes.map(it => it.learningobject_hruid)
);
}); });
it('also returns all learning objects when a learning path from the Dwengo API is queried', async () => { it('also returns all learning objects when a learning path from the Dwengo API is queried', async () => {
const result = await learningObjectService.getLearningObjectsFromPath(DWENGO_TEST_LEARNING_PATH_ID); const result = await learningObjectService.getLearningObjectsFromPath(DWENGO_TEST_LEARNING_PATH_ID);
@ -119,7 +111,7 @@ describe('LearningObjectService', () => {
describe('getLearningObjectIdsFromPath', () => { describe('getLearningObjectIdsFromPath', () => {
it('returns all learning objects when a learning path in the database is queried', async () => { it('returns all learning objects when a learning path in the database is queried', async () => {
const result = await learningObjectService.getLearningObjectIdsFromPath(exampleLearningPathId); const result = await learningObjectService.getLearningObjectIdsFromPath(exampleLearningPathId);
expect(result).toEqual(exampleLearningPath.nodes.map(it => it.learningobject_hruid)); expect(result).toEqual(exampleLearningPath.nodes.map((it) => it.learningobject_hruid));
}); });
it('also returns all learning object hruids when a learning path from the Dwengo API is queried', async () => { it('also returns all learning object hruids when a learning path from the Dwengo API is queried', async () => {
const result = await learningObjectService.getLearningObjectIdsFromPath(DWENGO_TEST_LEARNING_PATH_ID); const result = await learningObjectService.getLearningObjectIdsFromPath(DWENGO_TEST_LEARNING_PATH_ID);

View file

@ -1,12 +1,13 @@
import {beforeAll, describe, expect, it} from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
import processingService from '../../../../src/services/learning-objects/processing/processing-service'; import processingService from '../../../../src/services/learning-objects/processing/processing-service';
import { import {
testLearningObjectEssayQuestion, testLearningObjectEssayQuestion,
testLearningObjectMultipleChoice, testLearningObjectPnNotebooks testLearningObjectMultipleChoice,
} from "../../../test_assets/content/learning-objects.testdata"; testLearningObjectPnNotebooks,
import {getHtmlRenderingForTestLearningObject} from "../../../test-utils/get-html-rendering"; } from '../../../test_assets/content/learning-objects.testdata';
import {getLearningObjectRepository} from "../../../../src/data/repositories"; import { getHtmlRenderingForTestLearningObject } from '../../../test-utils/get-html-rendering';
import {setupTestApp} from "../../../setup-tests"; import { getLearningObjectRepository } from '../../../../src/data/repositories';
import { setupTestApp } from '../../../setup-tests';
describe('ProcessingService', () => { describe('ProcessingService', () => {
beforeAll(async () => { beforeAll(async () => {
@ -17,24 +18,18 @@ describe('ProcessingService', () => {
const markdownLearningObject = getLearningObjectRepository().create(testLearningObjectPnNotebooks); const markdownLearningObject = getLearningObjectRepository().create(testLearningObjectPnNotebooks);
const result = await processingService.render(markdownLearningObject); const result = await processingService.render(markdownLearningObject);
// Set newlines so your tests are platform-independent. // Set newlines so your tests are platform-independent.
expect(result).toEqual( expect(result).toEqual(getHtmlRenderingForTestLearningObject(markdownLearningObject).replace(/\r\n/g, '\n'));
getHtmlRenderingForTestLearningObject(markdownLearningObject).replace(/\r\n/g, '\n')
);
}); });
it('renders a multiple choice question correctly', async () => { it('renders a multiple choice question correctly', async () => {
const testLearningObject = getLearningObjectRepository().create(testLearningObjectMultipleChoice); const testLearningObject = getLearningObjectRepository().create(testLearningObjectMultipleChoice);
const result = await processingService.render(testLearningObject); const result = await processingService.render(testLearningObject);
expect(result).toEqual( expect(result).toEqual(getHtmlRenderingForTestLearningObject(testLearningObjectMultipleChoice).replace(/\r\n/g, '\n'));
getHtmlRenderingForTestLearningObject(testLearningObjectMultipleChoice).replace(/\r\n/g, '\n')
);
}); });
it('renders an essay question correctly', async () => { it('renders an essay question correctly', async () => {
const essayLearningObject = getLearningObjectRepository().create(testLearningObjectEssayQuestion); const essayLearningObject = getLearningObjectRepository().create(testLearningObjectEssayQuestion);
const result = await processingService.render(essayLearningObject); const result = await processingService.render(essayLearningObject);
expect(result).toEqual( expect(result).toEqual(getHtmlRenderingForTestLearningObject(essayLearningObject).replace(/\r\n/g, '\n'));
getHtmlRenderingForTestLearningObject(essayLearningObject).replace(/\r\n/g, '\n')
);
}); });
}); });

View file

@ -8,26 +8,20 @@ import databaseLearningPathProvider from '../../../src/services/learning-paths/d
import { expectToBeCorrectLearningPath } from '../../test-utils/expectations.js'; import { expectToBeCorrectLearningPath } from '../../test-utils/expectations.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { LearningObjectNode, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
import { import {
LearningObjectNode, testLearningObject01,
LearningPathResponse testLearningObjectEssayQuestion,
} from '@dwengo-1/common/interfaces/learning-content'; testLearningObjectMultipleChoice,
import { } from '../../test_assets/content/learning-objects.testdata';
testLearningObject01, testLearningObjectEssayQuestion, import { testLearningPathWithConditions } from '../../test_assets/content/learning-paths.testdata';
testLearningObjectMultipleChoice import { mapToLearningPath } from '../../../src/services/learning-paths/learning-path-service';
} from "../../test_assets/content/learning-objects.testdata"; import { getTestGroup01, getTestGroup02 } from '../../test_assets/assignments/groups.testdata';
import {testLearningPathWithConditions} from "../../test_assets/content/learning-paths.testdata";
import {mapToLearningPath} from "../../../src/services/learning-paths/learning-path-service";
import {getTestGroup01, getTestGroup02} from "../../test_assets/assignments/groups.testdata";
import { Group } from '../../../src/entities/assignments/group.entity.js'; import { Group } from '../../../src/entities/assignments/group.entity.js';
import {RequiredEntityData} from "@mikro-orm/core"; import { RequiredEntityData } from '@mikro-orm/core';
function expectBranchingObjectNode( function expectBranchingObjectNode(result: LearningPathResponse): LearningObjectNode {
result: LearningPathResponse const branchingObjectMatches = result.data![0].nodes.filter((it) => it.learningobject_hruid === testLearningObjectMultipleChoice.hruid);
): LearningObjectNode {
const branchingObjectMatches = result.data![0].nodes.filter(
(it) => it.learningobject_hruid === testLearningObjectMultipleChoice.hruid
);
expect(branchingObjectMatches.length).toBe(1); expect(branchingObjectMatches.length).toBe(1);
return branchingObjectMatches[0]; return branchingObjectMatches[0];
} }
@ -36,7 +30,7 @@ describe('DatabaseLearningPathProvider', () => {
let testLearningPath: LearningPath; let testLearningPath: LearningPath;
let branchingLearningObject: RequiredEntityData<LearningObject>; let branchingLearningObject: RequiredEntityData<LearningObject>;
let extraExerciseLearningObject: RequiredEntityData<LearningObject>; let extraExerciseLearningObject: RequiredEntityData<LearningObject>;
let finalLearningObject: RequiredEntityData<LearningObject>; let finalLearningObject: RequiredEntityData<LearningObject>;
let groupA: Group; let groupA: Group;
let groupB: Group; let groupB: Group;
@ -55,10 +49,10 @@ describe('DatabaseLearningPathProvider', () => {
learningObjectHruid: branchingLearningObject.hruid, learningObjectHruid: branchingLearningObject.hruid,
learningObjectLanguage: branchingLearningObject.language, learningObjectLanguage: branchingLearningObject.language,
learningObjectVersion: branchingLearningObject.version, learningObjectVersion: branchingLearningObject.version,
content: "[0]", content: '[0]',
onBehalfOf: groupA, onBehalfOf: groupA,
submissionTime: new Date(), submissionTime: new Date(),
submitter: groupA.members[0] submitter: groupA.members[0],
}); });
await submissionRepo.save(submissionA); await submissionRepo.save(submissionA);
@ -66,21 +60,17 @@ describe('DatabaseLearningPathProvider', () => {
learningObjectHruid: branchingLearningObject.hruid, learningObjectHruid: branchingLearningObject.hruid,
learningObjectLanguage: branchingLearningObject.language, learningObjectLanguage: branchingLearningObject.language,
learningObjectVersion: branchingLearningObject.version, learningObjectVersion: branchingLearningObject.version,
content: "[1]", content: '[1]',
onBehalfOf: groupB, onBehalfOf: groupB,
submissionTime: new Date(), submissionTime: new Date(),
submitter: groupB.members[0] submitter: groupB.members[0],
}); });
await submissionRepo.save(submissionB); await submissionRepo.save(submissionB);
}); });
describe('fetchLearningPaths', () => { describe('fetchLearningPaths', () => {
it('returns the learning path correctly', async () => { it('returns the learning path correctly', async () => {
const result = await databaseLearningPathProvider.fetchLearningPaths( const result = await databaseLearningPathProvider.fetchLearningPaths([testLearningPath.hruid], testLearningPath.language, 'the source');
[testLearningPath.hruid],
testLearningPath.language,
'the source'
);
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(result.data?.length).toBe(1); expect(result.data?.length).toBe(1);
@ -100,24 +90,11 @@ describe('DatabaseLearningPathProvider', () => {
// There should be exactly one branching object // There should be exactly one branching object
let branchingObject = expectBranchingObjectNode(result); let branchingObject = expectBranchingObjectNode(result);
expect( expect(branchingObject.transitions.filter((it) => it.next.hruid === finalLearningObject.hruid).length).toBe(0); // StudentA picked the first option, therefore, there should be no direct path to the final object.
branchingObject.transitions.filter( expect(branchingObject.transitions.filter((it) => it.next.hruid === extraExerciseLearningObject.hruid).length).toBe(1); // There should however be a path to the extra exercise object.
it => it.next.hruid === finalLearningObject.hruid
).length
).toBe(0); // StudentA picked the first option, therefore, there should be no direct path to the final object.
expect(
branchingObject.transitions.filter(
it => it.next.hruid === extraExerciseLearningObject.hruid
).length
).toBe(1); // There should however be a path to the extra exercise object.
// For student B: // For student B:
result = await databaseLearningPathProvider.fetchLearningPaths( result = await databaseLearningPathProvider.fetchLearningPaths([testLearningPath.hruid], testLearningPath.language, 'the source', groupB);
[testLearningPath.hruid],
testLearningPath.language,
'the source',
groupB
);
expect(result.success).toBeTruthy(); expect(result.success).toBeTruthy();
expect(result.data?.length).toBe(1); expect(result.data?.length).toBe(1);
@ -125,16 +102,8 @@ describe('DatabaseLearningPathProvider', () => {
branchingObject = expectBranchingObjectNode(result); branchingObject = expectBranchingObjectNode(result);
// However, now the student picks the other option. // However, now the student picks the other option.
expect( expect(branchingObject.transitions.filter((it) => it.next.hruid === finalLearningObject.hruid).length).toBe(1); // StudentB picked the second option, therefore, there should be a direct path to the final object.
branchingObject.transitions.filter( expect(branchingObject.transitions.filter((it) => it.next.hruid === extraExerciseLearningObject.hruid).length).toBe(0); // There should not be a path anymore to the extra exercise object.
(it) => it.next.hruid === finalLearningObject.hruid
).length
).toBe(1); // StudentB picked the second option, therefore, there should be a direct path to the final object.
expect(
branchingObject.transitions.filter(
(it) => it.next.hruid === extraExerciseLearningObject.hruid
).length
).toBe(0); // There should not be a path anymore to the extra exercise object.
}); });
it('returns a non-successful response if a non-existing learning path is queried', async () => { it('returns a non-successful response if a non-existing learning path is queried', async () => {
const result = await databaseLearningPathProvider.fetchLearningPaths( const result = await databaseLearningPathProvider.fetchLearningPaths(
@ -149,10 +118,7 @@ describe('DatabaseLearningPathProvider', () => {
describe('searchLearningPaths', () => { describe('searchLearningPaths', () => {
it('returns the correct learning path when queried with a substring of its title', async () => { it('returns the correct learning path when queried with a substring of its title', async () => {
const result = await databaseLearningPathProvider.searchLearningPaths( const result = await databaseLearningPathProvider.searchLearningPaths(testLearningPath.title.substring(2, 6), testLearningPath.language);
testLearningPath.title.substring(2, 6),
testLearningPath.language
);
expect(result.length).toBe(1); expect(result.length).toBe(1);
expect(result[0].title).toBe(testLearningPath.title); expect(result[0].title).toBe(testLearningPath.title);
expect(result[0].description).toBe(testLearningPath.description); expect(result[0].description).toBe(testLearningPath.description);

View file

@ -2,10 +2,8 @@ import { beforeAll, describe, expect, it } from 'vitest';
import { setupTestApp } from '../../setup-tests'; import { setupTestApp } from '../../setup-tests';
import learningPathService from '../../../src/services/learning-paths/learning-path-service'; import learningPathService from '../../../src/services/learning-paths/learning-path-service';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { import { testPartiallyDatabaseAndPartiallyDwengoApiLearningPath } from '../../test_assets/content/learning-paths.testdata';
testPartiallyDatabaseAndPartiallyDwengoApiLearningPath import { LearningPath as LearningPathDTO } from '@dwengo-1/common/interfaces/learning-content';
} from "../../test_assets/content/learning-paths.testdata";
import {LearningPath as LearningPathDTO} from "@dwengo-1/common/interfaces/learning-content";
const TEST_DWENGO_LEARNING_PATH_HRUID = 'pn_werking'; const TEST_DWENGO_LEARNING_PATH_HRUID = 'pn_werking';
const TEST_DWENGO_LEARNING_PATH_TITLE = 'Werken met notebooks'; const TEST_DWENGO_LEARNING_PATH_TITLE = 'Werken met notebooks';
@ -16,7 +14,7 @@ describe('LearningPathService', () => {
let testLearningPath: LearningPathDTO; let testLearningPath: LearningPathDTO;
beforeAll(async () => { beforeAll(async () => {
await setupTestApp(); await setupTestApp();
testLearningPath = testPartiallyDatabaseAndPartiallyDwengoApiLearningPath testLearningPath = testPartiallyDatabaseAndPartiallyDwengoApiLearningPath;
}); });
describe('fetchLearningPaths', () => { describe('fetchLearningPaths', () => {
it('should return learning paths both from the database and from the Dwengo API', async () => { it('should return learning paths both from the database and from the Dwengo API', async () => {

View file

@ -13,8 +13,8 @@ import { makeTestAttachments } from './test_assets/content/attachments.testdata.
import { makeTestQuestions } from './test_assets/questions/questions.testdata.js'; import { makeTestQuestions } from './test_assets/questions/questions.testdata.js';
import { makeTestAnswers } from './test_assets/questions/answers.testdata.js'; import { makeTestAnswers } from './test_assets/questions/answers.testdata.js';
import { makeTestSubmissions } from './test_assets/assignments/submission.testdata.js'; import { makeTestSubmissions } from './test_assets/assignments/submission.testdata.js';
import {Collection} from "@mikro-orm/core"; import { Collection } from '@mikro-orm/core';
import {Group} from "../src/entities/assignments/group.entity"; import { Group } from '../src/entities/assignments/group.entity';
export async function setupTestApp(): Promise<void> { export async function setupTestApp(): Promise<void> {
dotenv.config({ path: '.env.test' }); dotenv.config({ path: '.env.test' });

View file

@ -2,7 +2,7 @@ import { AssertionError } from 'node:assert';
import { LearningObject } from '../../src/entities/content/learning-object.entity'; import { LearningObject } from '../../src/entities/content/learning-object.entity';
import { expect } from 'vitest'; import { expect } from 'vitest';
import { FilteredLearningObject, LearningPath } from '@dwengo-1/common/interfaces/learning-content'; import { FilteredLearningObject, LearningPath } from '@dwengo-1/common/interfaces/learning-content';
import {RequiredEntityData} from "@mikro-orm/core"; import { RequiredEntityData } from '@mikro-orm/core';
// Ignored properties because they belang for example to the class, not to the entity itself. // Ignored properties because they belang for example to the class, not to the entity itself.
const IGNORE_PROPERTIES = ['parent']; const IGNORE_PROPERTIES = ['parent'];
@ -13,11 +13,7 @@ const IGNORE_PROPERTIES = ['parent'];
* @param expected The (previously added) entity we would expect to retrieve * @param expected The (previously added) entity we would expect to retrieve
* @param propertyPrefix Prefix to append to property in error messages. * @param propertyPrefix Prefix to append to property in error messages.
*/ */
export function expectToBeCorrectEntity<T extends object>( export function expectToBeCorrectEntity<T extends object>(actual: T, expected: T, propertyPrefix = ''): void {
actual: T,
expected: T,
propertyPrefix = ""
): void {
for (const property in expected) { for (const property in expected) {
if (Object.prototype.hasOwnProperty.call(expected, property)) { if (Object.prototype.hasOwnProperty.call(expected, property)) {
const prefixedProperty = propertyPrefix + property; const prefixedProperty = propertyPrefix + property;
@ -41,16 +37,16 @@ export function expectToBeCorrectEntity<T extends object>(
} }
} else if (typeof expected[property] !== typeof actual[property]) { } else if (typeof expected[property] !== typeof actual[property]) {
throw new AssertionError({ throw new AssertionError({
message: `${prefixedProperty} was expected to have type ${typeof expected[property]},` message:
+ `but had type ${typeof actual[property]}.`, `${prefixedProperty} was expected to have type ${typeof expected[property]},` +
`but had type ${typeof actual[property]}.`,
}); });
} else if (typeof expected[property] === 'object') { } else if (typeof expected[property] === 'object') {
expectToBeCorrectEntity(actual[property] as object, expected[property] as object, property); expectToBeCorrectEntity(actual[property] as object, expected[property] as object, property);
} else { } else {
if (expected[property] !== actual[property]) { if (expected[property] !== actual[property]) {
throw new AssertionError({ throw new AssertionError({
message: `${prefixedProperty} was expected to be ${expected[property]}, ` message: `${prefixedProperty} was expected to be ${expected[property]}, ` + `but was ${actual[property]}.`,
+ `but was ${actual[property]}.`,
}); });
} }
} }
@ -94,10 +90,7 @@ export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearni
* @param learningPath The learning path returned by the retriever, service or endpoint * @param learningPath The learning path returned by the retriever, service or endpoint
* @param expected The learning path that should have been returned. * @param expected The learning path that should have been returned.
*/ */
export function expectToBeCorrectLearningPath( export function expectToBeCorrectLearningPath(learningPath: LearningPath, expected: LearningPath): void {
learningPath: LearningPath,
expected: LearningPath
): void {
expect(learningPath.hruid).toEqual(expected.hruid); expect(learningPath.hruid).toEqual(expected.hruid);
expect(learningPath.language).toEqual(expected.language); expect(learningPath.language).toEqual(expected.language);
expect(learningPath.description).toEqual(expected.description); expect(learningPath.description).toEqual(expected.description);
@ -113,17 +106,18 @@ export function expectToBeCorrectLearningPath(
expect(learningPath.image ?? null).toEqual(expected.image ?? null); expect(learningPath.image ?? null).toEqual(expected.image ?? null);
for (const node of expected.nodes) { for (const node of expected.nodes) {
const correspondingNode = learningPath.nodes.find(it => const correspondingNode = learningPath.nodes.find(
node.learningobject_hruid === it.learningobject_hruid && node.language === it.language (it) => node.learningobject_hruid === it.learningobject_hruid && node.language === it.language && node.version === it.version
&& node.version === it.version
); );
expect(correspondingNode).toBeTruthy(); expect(correspondingNode).toBeTruthy();
expect(Boolean(correspondingNode!.start_node)).toEqual(Boolean(node.start_node)); expect(Boolean(correspondingNode!.start_node)).toEqual(Boolean(node.start_node));
for (const transition of node.transitions) { for (const transition of node.transitions) {
const correspondingTransition = correspondingNode!.transitions.find(it => const correspondingTransition = correspondingNode!.transitions.find(
it.next.hruid === transition.next.hruid && it.next.language === transition.next.language (it) =>
&& it.next.version === transition.next.version it.next.hruid === transition.next.hruid &&
it.next.language === transition.next.language &&
it.next.version === transition.next.version
); );
expect(correspondingTransition).toBeTruthy(); expect(correspondingTransition).toBeTruthy();
} }

View file

@ -1,12 +1,10 @@
import {RequiredEntityData} from "@mikro-orm/core"; import { RequiredEntityData } from '@mikro-orm/core';
import {loadTestAsset} from "./load-test-asset"; import { loadTestAsset } from './load-test-asset';
import {LearningObject} from "../../src/entities/content/learning-object.entity"; import { LearningObject } from '../../src/entities/content/learning-object.entity';
import {envVars, getEnvVar} from "../../src/util/envVars"; import { envVars, getEnvVar } from '../../src/util/envVars';
export function getHtmlRenderingForTestLearningObject(learningObject: RequiredEntityData<LearningObject>): string { export function getHtmlRenderingForTestLearningObject(learningObject: RequiredEntityData<LearningObject>): string {
const userPrefix = getEnvVar(envVars.UserContentPrefix); const userPrefix = getEnvVar(envVars.UserContentPrefix);
const cleanedHruid = learningObject.hruid.startsWith(userPrefix) const cleanedHruid = learningObject.hruid.startsWith(userPrefix) ? learningObject.hruid.substring(userPrefix.length) : learningObject.hruid;
? learningObject.hruid.substring(userPrefix.length)
: learningObject.hruid;
return loadTestAsset(`/content/learning-object-resources/${cleanedHruid}/rendering.txt`).toString(); return loadTestAsset(`/content/learning-object-resources/${cleanedHruid}/rendering.txt`).toString();
} }

View file

@ -1,6 +1,6 @@
import fs from 'fs'; import fs from 'fs';
import path from 'node:path'; import path from 'node:path';
import {fileURLToPath} from "node:url"; import { fileURLToPath } from 'node:url';
const fileName = fileURLToPath(import.meta.url); const fileName = fileURLToPath(import.meta.url);
const dirName = path.dirname(fileName); const dirName = path.dirname(fileName);

View file

@ -2,8 +2,8 @@ import { EntityManager } from '@mikro-orm/core';
import { Assignment } from '../../../src/entities/assignments/assignment.entity'; import { Assignment } from '../../../src/entities/assignments/assignment.entity';
import { Class } from '../../../src/entities/classes/class.entity'; import { Class } from '../../../src/entities/classes/class.entity';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {testLearningPathWithConditions} from "../content/learning-paths.testdata"; import { testLearningPathWithConditions } from '../content/learning-paths.testdata';
import {getClassWithTestleerlingAndTestleerkracht} from "../classes/classes.testdata"; import { getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata';
export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] { export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] {
assignment01 = em.create(Assignment, { assignment01 = em.create(Assignment, {

View file

@ -1,9 +1,9 @@
import {EntityManager} from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/core';
import { Group } from '../../../src/entities/assignments/group.entity'; import { Group } from '../../../src/entities/assignments/group.entity';
import { Assignment } from '../../../src/entities/assignments/assignment.entity'; import { Assignment } from '../../../src/entities/assignments/assignment.entity';
import { Student } from '../../../src/entities/users/student.entity'; import { Student } from '../../../src/entities/users/student.entity';
import {getConditionalPathAssignment} from "./assignments.testdata"; import { getConditionalPathAssignment } from './assignments.testdata';
import {getTestleerling1} from "../users/students.testdata"; import { getTestleerling1 } from '../users/students.testdata';
export function makeTestGroups(em: EntityManager, students: Student[], assignments: Assignment[]): Group[] { export function makeTestGroups(em: EntityManager, students: Student[], assignments: Assignment[]): Group[] {
/* /*
@ -62,8 +62,8 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen
group1ConditionalLearningPath = em.create(Group, { group1ConditionalLearningPath = em.create(Group, {
assignment: getConditionalPathAssignment(), assignment: getConditionalPathAssignment(),
groupNumber: 1, groupNumber: 1,
members: [getTestleerling1()] members: [getTestleerling1()],
}) });
return [group01, group02, group03, group04, group05, group1ConditionalLearningPath]; return [group01, group02, group03, group04, group05, group1ConditionalLearningPath];
} }

View file

@ -2,8 +2,8 @@ import { EntityManager } from '@mikro-orm/core';
import { Class } from '../../../src/entities/classes/class.entity'; import { Class } from '../../../src/entities/classes/class.entity';
import { Student } from '../../../src/entities/users/student.entity'; import { Student } from '../../../src/entities/users/student.entity';
import { Teacher } from '../../../src/entities/users/teacher.entity'; import { Teacher } from '../../../src/entities/users/teacher.entity';
import {getTestleerkracht1} from "../users/teachers.testdata"; import { getTestleerkracht1 } from '../users/teachers.testdata';
import {getTestleerling1} from "../users/students.testdata"; import { getTestleerling1 } from '../users/students.testdata';
export function makeTestClasses(em: EntityManager, students: Student[], teachers: Teacher[]): Class[] { export function makeTestClasses(em: EntityManager, students: Student[], teachers: Teacher[]): Class[] {
const studentsClass01 = students.slice(0, 8); const studentsClass01 = students.slice(0, 8);
@ -47,10 +47,10 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
}); });
classWithTestleerlingAndTestleerkracht = em.create(Class, { classWithTestleerlingAndTestleerkracht = em.create(Class, {
classId: "a75298b5-18aa-471d-8eeb-5d77eb989393", classId: 'a75298b5-18aa-471d-8eeb-5d77eb989393',
displayName: 'Testklasse', displayName: 'Testklasse',
teachers: [getTestleerkracht1()], teachers: [getTestleerkracht1()],
students: [getTestleerling1()] students: [getTestleerling1()],
}); });
return [class01, class02, class03, class04, classWithTestleerlingAndTestleerkracht]; return [class01, class02, class03, class04, classWithTestleerlingAndTestleerkracht];

View file

@ -1,11 +1,11 @@
import {EntityManager, RequiredEntityData} from '@mikro-orm/core'; import { EntityManager, RequiredEntityData } from '@mikro-orm/core';
import {LearningObject} from '../../../src/entities/content/learning-object.entity'; import { LearningObject } from '../../../src/entities/content/learning-object.entity';
import {Language} from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {DwengoContentType} from '../../../src/services/learning-objects/processing/content-type'; import { DwengoContentType } from '../../../src/services/learning-objects/processing/content-type';
import {ReturnValue} from '../../../src/entities/content/return-value.entity'; import { ReturnValue } from '../../../src/entities/content/return-value.entity';
import {envVars, getEnvVar} from "../../../src/util/envVars"; import { envVars, getEnvVar } from '../../../src/util/envVars';
import {loadTestAsset} from "../../test-utils/load-test-asset"; import { loadTestAsset } from '../../test-utils/load-test-asset';
import {v4} from "uuid"; import { v4 } from 'uuid';
export function makeTestLearningObjects(em: EntityManager): LearningObject[] { export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
const returnValue: ReturnValue = new ReturnValue(); const returnValue: ReturnValue = new ReturnValue();
@ -19,13 +19,19 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] {
const learningObject05 = em.create(LearningObject, testLearningObject05); const learningObject05 = em.create(LearningObject, testLearningObject05);
const learningObjectMultipleChoice = em.create(LearningObject, testLearningObjectMultipleChoice); const learningObjectMultipleChoice = em.create(LearningObject, testLearningObjectMultipleChoice);
const learningObjectEssayQuestion= em.create(LearningObject, testLearningObjectEssayQuestion); const learningObjectEssayQuestion = em.create(LearningObject, testLearningObjectEssayQuestion);
const learningObjectPnNotebooks = em.create(LearningObject, testLearningObjectPnNotebooks); const learningObjectPnNotebooks = em.create(LearningObject, testLearningObjectPnNotebooks);
return [ return [
learningObject01, learningObject02, learningObject03, learningObject04, learningObject05, learningObject01,
learningObjectMultipleChoice, learningObjectEssayQuestion, learningObjectPnNotebooks learningObject02,
learningObject03,
learningObject04,
learningObject05,
learningObjectMultipleChoice,
learningObjectEssayQuestion,
learningObjectPnNotebooks,
]; ];
} }
@ -164,14 +170,14 @@ export const testLearningObjectMultipleChoice: RequiredEntityData<LearningObject
hruid: `${getEnvVar(envVars.UserContentPrefix)}test_multiple_choice`, hruid: `${getEnvVar(envVars.UserContentPrefix)}test_multiple_choice`,
language: Language.English, language: Language.English,
version: 1, version: 1,
title: "Self-evaluation", title: 'Self-evaluation',
description: "Time to evaluate how well you understand what you've learned so far.", description: "Time to evaluate how well you understand what you've learned so far.",
keywords: ["test"], keywords: ['test'],
teacherExclusive: false, teacherExclusive: false,
skosConcepts: [], skosConcepts: [],
educationalGoals: [], educationalGoals: [],
copyright: "Groep 1 SEL-2 2025", copyright: 'Groep 1 SEL-2 2025',
license: "CC0", license: 'CC0',
difficulty: 1, difficulty: 1,
estimatedTime: 1, estimatedTime: 1,
attachments: [], attachments: [],
@ -183,21 +189,21 @@ export const testLearningObjectMultipleChoice: RequiredEntityData<LearningObject
returnValue: { returnValue: {
callbackUrl: `%SUBMISSION%`, callbackUrl: `%SUBMISSION%`,
callbackSchema: '["antwoord vraag 1"]', callbackSchema: '["antwoord vraag 1"]',
} },
}; };
export const testLearningObjectEssayQuestion: RequiredEntityData<LearningObject> = { export const testLearningObjectEssayQuestion: RequiredEntityData<LearningObject> = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}test_essay_question`, hruid: `${getEnvVar(envVars.UserContentPrefix)}test_essay_question`,
language: Language.English, language: Language.English,
version: 1, version: 1,
title: "Reflection", title: 'Reflection',
description: "Reflect on your learning progress.", description: 'Reflect on your learning progress.',
keywords: ["test"], keywords: ['test'],
teacherExclusive: false, teacherExclusive: false,
skosConcepts: [], skosConcepts: [],
educationalGoals: [], educationalGoals: [],
copyright: "Groep 1 SEL-2 2025", copyright: 'Groep 1 SEL-2 2025',
license: "CC0", license: 'CC0',
difficulty: 1, difficulty: 1,
estimatedTime: 1, estimatedTime: 1,
attachments: [], attachments: [],
@ -209,47 +215,47 @@ export const testLearningObjectEssayQuestion: RequiredEntityData<LearningObject>
returnValue: { returnValue: {
callbackUrl: `%SUBMISSION%`, callbackUrl: `%SUBMISSION%`,
callbackSchema: '["antwoord vraag 1"]', callbackSchema: '["antwoord vraag 1"]',
} },
}; };
export const testLearningObjectPnNotebooks: RequiredEntityData<LearningObject> = { export const testLearningObjectPnNotebooks: RequiredEntityData<LearningObject> = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}pn_werkingnotebooks`, hruid: `${getEnvVar(envVars.UserContentPrefix)}pn_werkingnotebooks`,
version: 3, version: 3,
language: Language.Dutch, language: Language.Dutch,
title: "Werken met notebooks", title: 'Werken met notebooks',
description: "Leren werken met notebooks", description: 'Leren werken met notebooks',
keywords: ["Python", "KIKS", "Wiskunde", "STEM", "AI"], keywords: ['Python', 'KIKS', 'Wiskunde', 'STEM', 'AI'],
targetAges: [14, 15, 16, 17, 18], targetAges: [14, 15, 16, 17, 18],
admins: [], admins: [],
copyright: "dwengo", copyright: 'dwengo',
educationalGoals: [], educationalGoals: [],
license: "dwengo", license: 'dwengo',
contentType: DwengoContentType.TEXT_MARKDOWN, contentType: DwengoContentType.TEXT_MARKDOWN,
difficulty: 3, difficulty: 3,
estimatedTime: 10, estimatedTime: 10,
uuid: "2adf9929-b424-4650-bf60-186f730d38ab", uuid: '2adf9929-b424-4650-bf60-186f730d38ab',
teacherExclusive: false, teacherExclusive: false,
skosConcepts: [ skosConcepts: [
"http://ilearn.ilabt.imec.be/vocab/curr1/s-vaktaal", 'http://ilearn.ilabt.imec.be/vocab/curr1/s-vaktaal',
"http://ilearn.ilabt.imec.be/vocab/curr1/s-digitale-media-en-toepassingen", 'http://ilearn.ilabt.imec.be/vocab/curr1/s-digitale-media-en-toepassingen',
"http://ilearn.ilabt.imec.be/vocab/curr1/s-computers-en-systemen", 'http://ilearn.ilabt.imec.be/vocab/curr1/s-computers-en-systemen',
], ],
attachments: [ attachments: [
{ {
name: "dwengo.png", name: 'dwengo.png',
mimeType: "image/png", mimeType: 'image/png',
content: loadTestAsset("/content/learning-object-resources/pn_werkingnotebooks/dwengo.png") content: loadTestAsset('/content/learning-object-resources/pn_werkingnotebooks/dwengo.png'),
}, },
{ {
name: "Knop.png", name: 'Knop.png',
mimeType: "image/png", mimeType: 'image/png',
content: loadTestAsset("/content/learning-object-resources/pn_werkingnotebooks/Knop.png") content: loadTestAsset('/content/learning-object-resources/pn_werkingnotebooks/Knop.png'),
} },
], ],
available: false, available: false,
content: loadTestAsset("/content/learning-object-resources/pn_werkingnotebooks/content.md"), content: loadTestAsset('/content/learning-object-resources/pn_werkingnotebooks/content.md'),
returnValue: { returnValue: {
callbackUrl: "%SUBMISSION%", callbackUrl: '%SUBMISSION%',
callbackSchema: "[]" callbackSchema: '[]',
} },
} };

View file

@ -1,40 +1,39 @@
import {EntityManager} from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/core';
import {LearningPath} from '../../../src/entities/content/learning-path.entity'; import { LearningPath } from '../../../src/entities/content/learning-path.entity';
import {Language} from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import {mapToLearningPath} from "../../../src/services/learning-paths/learning-path-service"; import { mapToLearningPath } from '../../../src/services/learning-paths/learning-path-service';
import {envVars, getEnvVar} from "../../../src/util/envVars"; import { envVars, getEnvVar } from '../../../src/util/envVars';
import {LearningPath as LearningPathDTO} from "@dwengo-1/common/interfaces/learning-content"; import { LearningPath as LearningPathDTO } from '@dwengo-1/common/interfaces/learning-content';
import { import {
testLearningObject01, testLearningObject02, testLearningObject03, testLearningObject04, testLearningObject05, testLearningObject01,
testLearningObject02,
testLearningObject03,
testLearningObject04,
testLearningObject05,
testLearningObjectEssayQuestion, testLearningObjectEssayQuestion,
testLearningObjectMultipleChoice, testLearningObjectPnNotebooks testLearningObjectMultipleChoice,
} from "./learning-objects.testdata"; testLearningObjectPnNotebooks,
} from './learning-objects.testdata';
export function makeTestLearningPaths(_em: EntityManager): LearningPath[] { export function makeTestLearningPaths(_em: EntityManager): LearningPath[] {
const learningPath01 = mapToLearningPath(testLearningPath01, []); const learningPath01 = mapToLearningPath(testLearningPath01, []);
const learningPath02 = mapToLearningPath(testLearningPath02, []); const learningPath02 = mapToLearningPath(testLearningPath02, []);
const partiallyDatabasePartiallyDwengoApiLearningPath const partiallyDatabasePartiallyDwengoApiLearningPath = mapToLearningPath(testPartiallyDatabaseAndPartiallyDwengoApiLearningPath, []);
= mapToLearningPath(testPartiallyDatabaseAndPartiallyDwengoApiLearningPath, []) const learningPathWithConditions = mapToLearningPath(testLearningPathWithConditions, []);
const learningPathWithConditions = mapToLearningPath(testLearningPathWithConditions, [])
return [ return [learningPath01, learningPath02, partiallyDatabasePartiallyDwengoApiLearningPath, learningPathWithConditions];
learningPath01,
learningPath02,
partiallyDatabasePartiallyDwengoApiLearningPath,
learningPathWithConditions
];
} }
const nowString = new Date().toString(); const nowString = new Date().toString();
export const testLearningPath01: LearningPathDTO = { export const testLearningPath01: LearningPathDTO = {
keywords: "test", keywords: 'test',
target_ages: [16, 17, 18], target_ages: [16, 17, 18],
hruid: `${getEnvVar(envVars.UserContentPrefix)}id01`, hruid: `${getEnvVar(envVars.UserContentPrefix)}id01`,
language: Language.English, language: Language.English,
title: "repertoire Tool", title: 'repertoire Tool',
description: "all about Tool", description: 'all about Tool',
nodes: [ nodes: [
{ {
learningobject_hruid: testLearningObject01.hruid, learningobject_hruid: testLearningObject01.hruid,
@ -48,10 +47,10 @@ export const testLearningPath01: LearningPathDTO = {
next: { next: {
hruid: testLearningObject02.hruid, hruid: testLearningObject02.hruid,
language: testLearningObject02.language, language: testLearningObject02.language,
version: testLearningObject02.version version: testLearningObject02.version,
} },
} },
] ],
}, },
{ {
learningobject_hruid: testLearningObject02.hruid, learningobject_hruid: testLearningObject02.hruid,
@ -59,18 +58,18 @@ export const testLearningPath01: LearningPathDTO = {
version: testLearningObject02.version, version: testLearningObject02.version,
created_at: nowString, created_at: nowString,
updatedAt: nowString, updatedAt: nowString,
transitions: [] transitions: [],
} },
] ],
}; };
export const testLearningPath02: LearningPathDTO = { export const testLearningPath02: LearningPathDTO = {
keywords: "test", keywords: 'test',
target_ages: [16, 17, 18], target_ages: [16, 17, 18],
hruid: `${getEnvVar(envVars.UserContentPrefix)}id02`, hruid: `${getEnvVar(envVars.UserContentPrefix)}id02`,
language: Language.English, language: Language.English,
title: "repertoire Dire Straits", title: 'repertoire Dire Straits',
description: "all about Dire Straits", description: 'all about Dire Straits',
nodes: [ nodes: [
{ {
learningobject_hruid: testLearningObject03.hruid, learningobject_hruid: testLearningObject03.hruid,
@ -84,10 +83,10 @@ export const testLearningPath02: LearningPathDTO = {
next: { next: {
hruid: testLearningObject04.hruid, hruid: testLearningObject04.hruid,
language: testLearningObject04.language, language: testLearningObject04.language,
version: testLearningObject04.version version: testLearningObject04.version,
} },
} },
] ],
}, },
{ {
learningobject_hruid: testLearningObject04.hruid, learningobject_hruid: testLearningObject04.hruid,
@ -100,10 +99,10 @@ export const testLearningPath02: LearningPathDTO = {
next: { next: {
hruid: testLearningObject05.hruid, hruid: testLearningObject05.hruid,
language: testLearningObject05.language, language: testLearningObject05.language,
version: testLearningObject05.version version: testLearningObject05.version,
} },
} },
] ],
}, },
{ {
learningobject_hruid: testLearningObject05.hruid, learningobject_hruid: testLearningObject05.hruid,
@ -111,17 +110,17 @@ export const testLearningPath02: LearningPathDTO = {
version: testLearningObject05.version, version: testLearningObject05.version,
created_at: nowString, created_at: nowString,
updatedAt: nowString, updatedAt: nowString,
transitions: [] transitions: [],
} },
] ],
}; };
export const testPartiallyDatabaseAndPartiallyDwengoApiLearningPath: LearningPathDTO = { export const testPartiallyDatabaseAndPartiallyDwengoApiLearningPath: LearningPathDTO = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}pn_werking`, hruid: `${getEnvVar(envVars.UserContentPrefix)}pn_werking`,
title: "Werken met notebooks", title: 'Werken met notebooks',
language: Language.Dutch, language: Language.Dutch,
description: 'Een korte inleiding tot Python notebooks. Hoe ga je gemakkelijk en efficiënt met de notebooks aan de slag?', description: 'Een korte inleiding tot Python notebooks. Hoe ga je gemakkelijk en efficiënt met de notebooks aan de slag?',
keywords: "Python KIKS Wiskunde STEM AI", keywords: 'Python KIKS Wiskunde STEM AI',
target_ages: [14, 15, 16, 17, 18], target_ages: [14, 15, 16, 17, 18],
nodes: [ nodes: [
{ {
@ -135,15 +134,15 @@ export const testPartiallyDatabaseAndPartiallyDwengoApiLearningPath: LearningPat
{ {
default: true, default: true,
next: { next: {
hruid: "pn_werkingnotebooks2", hruid: 'pn_werkingnotebooks2',
language: Language.Dutch, language: Language.Dutch,
version: 3 version: 3,
} },
} },
] ],
}, },
{ {
learningobject_hruid: "pn_werkingnotebooks2", learningobject_hruid: 'pn_werkingnotebooks2',
language: Language.Dutch, language: Language.Dutch,
version: 3, version: 3,
created_at: nowString, created_at: nowString,
@ -152,30 +151,30 @@ export const testPartiallyDatabaseAndPartiallyDwengoApiLearningPath: LearningPat
{ {
default: true, default: true,
next: { next: {
hruid: "pn_werkingnotebooks3", hruid: 'pn_werkingnotebooks3',
language: Language.Dutch, language: Language.Dutch,
version: 3 version: 3,
} },
} },
] ],
}, },
{ {
learningobject_hruid: "pn_werkingnotebooks3", learningobject_hruid: 'pn_werkingnotebooks3',
language: Language.Dutch, language: Language.Dutch,
version: 3, version: 3,
created_at: nowString, created_at: nowString,
updatedAt: nowString, updatedAt: nowString,
transitions: [] transitions: [],
} },
] ],
} };
export const testLearningPathWithConditions: LearningPathDTO = { export const testLearningPathWithConditions: LearningPathDTO = {
hruid: `${getEnvVar(envVars.UserContentPrefix)}test_conditions`, hruid: `${getEnvVar(envVars.UserContentPrefix)}test_conditions`,
language: Language.English, language: Language.English,
title: 'Example learning path with conditional transitions', title: 'Example learning path with conditional transitions',
description: 'This learning path was made for the purpose of testing conditional transitions', description: 'This learning path was made for the purpose of testing conditional transitions',
keywords: "test", keywords: 'test',
target_ages: [10, 11, 12, 13, 14, 15, 16, 17, 18], target_ages: [10, 11, 12, 13, 14, 15, 16, 17, 18],
nodes: [ nodes: [
{ {
@ -193,8 +192,8 @@ export const testLearningPathWithConditions: LearningPathDTO = {
//... we let the student do an extra exercise. //... we let the student do an extra exercise.
hruid: testLearningObject01.hruid, hruid: testLearningObject01.hruid,
language: testLearningObject01.language, language: testLearningObject01.language,
version: testLearningObject01.version version: testLearningObject01.version,
} },
}, },
{ {
// If the answer to the first question was the second one (I can follow along): // If the answer to the first question was the second one (I can follow along):
@ -203,10 +202,10 @@ export const testLearningPathWithConditions: LearningPathDTO = {
//... we let the student right through to the final question. //... we let the student right through to the final question.
hruid: testLearningObjectEssayQuestion.hruid, hruid: testLearningObjectEssayQuestion.hruid,
language: testLearningObjectEssayQuestion.language, language: testLearningObjectEssayQuestion.language,
version: testLearningObjectEssayQuestion.version version: testLearningObjectEssayQuestion.version,
} },
} },
] ],
}, },
{ {
learningobject_hruid: testLearningObject01.hruid, learningobject_hruid: testLearningObject01.hruid,
@ -220,10 +219,10 @@ export const testLearningPathWithConditions: LearningPathDTO = {
next: { next: {
hruid: testLearningObjectEssayQuestion.hruid, hruid: testLearningObjectEssayQuestion.hruid,
language: testLearningObjectEssayQuestion.language, language: testLearningObjectEssayQuestion.language,
version: testLearningObjectEssayQuestion.version version: testLearningObjectEssayQuestion.version,
} },
} },
] ],
}, },
{ {
learningobject_hruid: testLearningObjectEssayQuestion.hruid, learningobject_hruid: testLearningObjectEssayQuestion.hruid,
@ -231,7 +230,7 @@ export const testLearningPathWithConditions: LearningPathDTO = {
version: testLearningObjectEssayQuestion.version, version: testLearningObjectEssayQuestion.version,
created_at: nowString, created_at: nowString,
updatedAt: nowString, updatedAt: nowString,
transitions: [] transitions: [],
} },
] ],
} };

View file

@ -24,5 +24,5 @@ export function makeTestStudents(em: EntityManager): Student[] {
} }
export function getTestleerling1(): Student { export function getTestleerling1(): Student {
return testStudents.find(it => it.username === "testleerling1"); return testStudents.find((it) => it.username === 'testleerling1');
} }

View file

@ -62,4 +62,3 @@ export function getTeacher04(): Teacher {
export function getTestleerkracht1(): Teacher { export function getTestleerkracht1(): Teacher {
return testleerkracht1; return testleerkracht1;
} }

View file

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

View file

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

View file

@ -10,7 +10,7 @@ const learningPathController = getLearningPathController();
export function useGetLearningPathQuery( export function useGetLearningPathQuery(
hruid: MaybeRefOrGetter<string>, hruid: MaybeRefOrGetter<string>,
language: MaybeRefOrGetter<Language>, language: MaybeRefOrGetter<Language>,
forGroup?: MaybeRefOrGetter<{forGroup: number, assignmentNo: number, classId: string} | undefined>, forGroup?: MaybeRefOrGetter<{ forGroup: number; assignmentNo: number; classId: string } | undefined>,
): UseQueryReturnType<LearningPath, Error> { ): UseQueryReturnType<LearningPath, Error> {
return useQuery({ return useQuery({
queryKey: [LEARNING_PATH_KEY, "get", hruid, language, forGroup], queryKey: [LEARNING_PATH_KEY, "get", hruid, language, forGroup],
@ -34,7 +34,7 @@ export function useGetAllLearningPathsByThemeQuery(
export function useSearchLearningPathQuery( export function useSearchLearningPathQuery(
query: MaybeRefOrGetter<string | undefined>, query: MaybeRefOrGetter<string | undefined>,
language: MaybeRefOrGetter<string | undefined> language: MaybeRefOrGetter<string | undefined>,
): UseQueryReturnType<LearningPath[], Error> { ): UseQueryReturnType<LearningPath[], Error> {
return useQuery({ return useQuery({
queryKey: [LEARNING_PATH_KEY, "search", query, language], queryKey: [LEARNING_PATH_KEY, "search", query, language],

View file

@ -9,9 +9,9 @@ import {
type UseQueryReturnType, type UseQueryReturnType,
} from "@tanstack/vue-query"; } from "@tanstack/vue-query";
import { computed, toValue, type MaybeRefOrGetter } from "vue"; import { computed, toValue, type MaybeRefOrGetter } from "vue";
import {LEARNING_PATH_KEY} from "@/queries/learning-paths.ts"; import { LEARNING_PATH_KEY } from "@/queries/learning-paths.ts";
import {LEARNING_OBJECT_KEY} from "@/queries/learning-objects.ts"; import { LEARNING_OBJECT_KEY } from "@/queries/learning-objects.ts";
import type {Language} from "@dwengo-1/common/util/language"; import type { Language } from "@dwengo-1/common/util/language";
export const SUBMISSION_KEY = "submissions"; export const SUBMISSION_KEY = "submissions";
@ -22,7 +22,7 @@ function submissionQueryKey(
classid: string, classid: string,
assignmentNumber: number, assignmentNumber: number,
groupNumber: number, groupNumber: number,
submissionNumber: number submissionNumber: number,
) { ) {
return ["submission", hruid, language, version, classid, assignmentNumber, groupNumber, submissionNumber]; return ["submission", hruid, language, version, classid, assignmentNumber, groupNumber, submissionNumber];
} }
@ -41,29 +41,37 @@ export async function invalidateAllSubmissionKeys(
for (const key of keys) { for (const key of keys) {
const queryKey = [ const queryKey = [
key, hruid, language, version, classid, assignmentNumber, groupNumber, submissionNumber key,
].filter( hruid,
(arg) => arg !== undefined, language,
); version,
classid,
assignmentNumber,
groupNumber,
submissionNumber,
].filter((arg) => arg !== undefined);
await queryClient.invalidateQueries({ queryKey: queryKey }); await queryClient.invalidateQueries({ queryKey: queryKey });
} }
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber] queryKey: ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber].filter(
.filter((arg) => arg !== undefined), (arg) => arg !== undefined,
),
}); });
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["group-submissions", hruid, language, version, classid, assignmentNumber, groupNumber] queryKey: ["group-submissions", hruid, language, version, classid, assignmentNumber, groupNumber].filter(
.filter((arg) => arg !== undefined), (arg) => arg !== undefined,
),
}); });
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["assignment-submissions", hruid, language, version,classid, assignmentNumber] queryKey: ["assignment-submissions", hruid, language, version, classid, assignmentNumber].filter(
.filter((arg) => arg !== undefined), (arg) => arg !== undefined,
),
}); });
} }
function checkEnabled(properties: MaybeRefOrGetter<unknown>[]): boolean { function checkEnabled(properties: MaybeRefOrGetter<unknown>[]): boolean {
return properties.every(prop => Boolean(toValue(prop))); return properties.every((prop) => Boolean(toValue(prop)));
} }
export function useSubmissionsQuery( export function useSubmissionsQuery(
@ -87,9 +95,14 @@ export function useSubmissionsQuery(
const fullVal = toValue(full); const fullVal = toValue(full);
const response = await new SubmissionController(hruidVal!).getAll( const response = await new SubmissionController(hruidVal!).getAll(
languageVal, versionVal!, classIdVal!, assignmentNumberVal!, groupNumberVal, fullVal languageVal,
versionVal!,
classIdVal!,
assignmentNumberVal!,
groupNumberVal,
fullVal,
); );
return response ? response.submissions as SubmissionDTO[] : undefined; return response ? (response.submissions as SubmissionDTO[]) : undefined;
}, },
enabled: () => checkEnabled([hruid, language, version, classid, assignmentNumber]), enabled: () => checkEnabled([hruid, language, version, classid, assignmentNumber]),
}); });
@ -113,13 +126,33 @@ export function useSubmissionQuery(
const submissionNumberVal = toValue(submissionNumber); const submissionNumberVal = toValue(submissionNumber);
return useQuery({ return useQuery({
queryKey: computed(() => submissionQueryKey( queryKey: computed(() =>
hruidVal!, languageVal, versionVal!, classIdVal!, assignmentNumberVal!, groupNumberVal!, submissionNumberVal! submissionQueryKey(
)), hruidVal!,
queryFn: async () => new SubmissionController(hruidVal!).getByNumber( languageVal,
languageVal, versionVal!, classIdVal!, assignmentNumberVal!, groupNumberVal!, submissionNumberVal! versionVal!,
classIdVal!,
assignmentNumberVal!,
groupNumberVal!,
submissionNumberVal!,
),
), ),
enabled: () => Boolean(hruidVal) && Boolean(languageVal) && Boolean(versionVal) && Boolean(classIdVal) && Boolean(assignmentNumberVal) && Boolean(submissionNumber), 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),
}); });
} }
@ -132,7 +165,8 @@ export function useCreateSubmissionMutation(): UseMutationReturnType<
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ data }) => new SubmissionController(data.learningObjectIdentifier.hruid).createSubmission(data), mutationFn: async ({ data }) =>
new SubmissionController(data.learningObjectIdentifier.hruid).createSubmission(data),
onSuccess: async (response) => { onSuccess: async (response) => {
if (!response.submission.group) { if (!response.submission.group) {
await invalidateAllSubmissionKeys(queryClient); await invalidateAllSubmissionKeys(queryClient);
@ -144,13 +178,13 @@ export function useCreateSubmissionMutation(): UseMutationReturnType<
const an = typeof assignment === "number" ? assignment : assignment.id; const an = typeof assignment === "number" ? assignment : assignment.id;
const gn = response.submission.group.groupNumber; const gn = response.submission.group.groupNumber;
const {hruid, language, version} = response.submission.learningObjectIdentifier; const { hruid, language, version } = response.submission.learningObjectIdentifier;
await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn); await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn);
await queryClient.invalidateQueries({queryKey: [LEARNING_PATH_KEY, "get"]}); await queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY, "get"] });
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: [LEARNING_OBJECT_KEY, "metadata", hruid, language, version] queryKey: [LEARNING_OBJECT_KEY, "metadata", hruid, language, version],
}); });
} }
}, },
@ -178,15 +212,9 @@ export function useDeleteSubmissionMutation(): UseMutationReturnType<
const an = typeof assignment === "number" ? assignment : assignment.id; const an = typeof assignment === "number" ? assignment : assignment.id;
const gn = response.submission.group.groupNumber; const gn = response.submission.group.groupNumber;
const {hruid, language, version} = response.submission.learningObjectIdentifier; const { hruid, language, version } = response.submission.learningObjectIdentifier;
await invalidateAllSubmissionKeys( await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn);
queryClient,
hruid,
language,
version,
cid, an, gn
);
} }
}, },
}); });

View file

@ -1,20 +1,29 @@
export function deepEquals<T>(a: T, b: T): boolean { export function deepEquals<T>(a: T, b: T): boolean {
if (a === b) {return true;} if (a === b) {
return true;
}
if (typeof a !== 'object' || typeof b !== 'object' || a == null || b == null) if (typeof a !== "object" || typeof b !== "object" || a == null || b == null) {
{return false;} return false;
}
if (Array.isArray(a) !== Array.isArray(b)) {return false;} if (Array.isArray(a) !== Array.isArray(b)) {
return false;
}
if (Array.isArray(a) && Array.isArray(b)) { if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) {return false;} if (a.length !== b.length) {
return false;
}
return a.every((val, i) => deepEquals(val, b[i])); return a.every((val, i) => deepEquals(val, b[i]));
} }
const keysA = Object.keys(a) as (keyof T)[]; const keysA = Object.keys(a) as (keyof T)[];
const keysB = Object.keys(b) as (keyof T)[]; const keysB = Object.keys(b) as (keyof T)[];
if (keysA.length !== keysB.length) {return false;} if (keysA.length !== keysB.length) {
return false;
}
return keysA.every(key => deepEquals(a[key], b[key])); return keysA.every((key) => deepEquals(a[key], b[key]));
} }

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import {useRoute} from "vue-router"; import { useRoute } from "vue-router";
const route = useRoute(); const route = useRoute();
</script> </script>

View file

@ -1,24 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import UsingQueryResult from "@/components/UsingQueryResult.vue"; import UsingQueryResult from "@/components/UsingQueryResult.vue";
import {useGroupsQuery} from "@/queries/groups.ts"; import { useGroupsQuery } from "@/queries/groups.ts";
import type {GroupsResponse} from "@/controllers/groups.ts"; import type { GroupsResponse } from "@/controllers/groups.ts";
import {useI18n} from "vue-i18n"; import { useI18n } from "vue-i18n";
import type {GroupDTO} from "@dwengo-1/common/interfaces/group"; import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
classId: string, classId: string;
assignmentNumber: number assignmentNumber: number;
}>(); }>();
const model = defineModel<number | undefined>({default: undefined}); const model = defineModel<number | undefined>({ default: undefined });
const groupsQuery = useGroupsQuery(props.classId, props.assignmentNumber, true); const groupsQuery = useGroupsQuery(props.classId, props.assignmentNumber, true);
interface GroupSelectorOption { interface GroupSelectorOption {
groupNumber: number | undefined, groupNumber: number | undefined;
label: string label: string;
} }
function groupOptions(groups: GroupDTO[]): GroupSelectorOption[] { function groupOptions(groups: GroupDTO[]): GroupSelectorOption[] {
@ -26,7 +26,7 @@
.sort((a, b) => a.groupNumber - b.groupNumber) .sort((a, b) => a.groupNumber - b.groupNumber)
.map((group, index) => ({ .map((group, index) => ({
groupNumber: group.groupNumber, groupNumber: group.groupNumber,
label: `${index + 1}` label: `${index + 1}`,
})); }));
} }
</script> </script>

View file

@ -3,7 +3,7 @@
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
import { computed, type ComputedRef, ref } from "vue"; import { computed, type ComputedRef, ref } from "vue";
import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts";
import {useRoute, useRouter} from "vue-router"; import { useRoute, useRouter } from "vue-router";
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import LearningPathSearchField from "@/components/LearningPathSearchField.vue"; import LearningPathSearchField from "@/components/LearningPathSearchField.vue";
@ -21,7 +21,7 @@
const props = defineProps<{ const props = defineProps<{
hruid: string; hruid: string;
language: Language; language: Language;
learningObjectHruid?: string, learningObjectHruid?: string;
}>(); }>();
interface LearningPathPageQuery { interface LearningPathPageQuery {
@ -37,7 +37,7 @@
return { return {
forGroup: parseInt(query.value.forGroup), forGroup: parseInt(query.value.forGroup),
assignmentNo: parseInt(query.value.assignmentNo), assignmentNo: parseInt(query.value.assignmentNo),
classId: query.value.classId classId: query.value.classId,
}; };
} }
}); });
@ -112,7 +112,7 @@
let query = structuredClone(route.query); let query = structuredClone(route.query);
query.forGroup = value; query.forGroup = value;
router.push({ query }); router.push({ query });
} },
}); });
function assign() { function assign() {
@ -120,8 +120,8 @@
path: "/assignment/create", path: "/assignment/create",
query: { query: {
hruid: props.hruid, hruid: props.hruid,
language: props.language language: props.language,
} },
}); });
} }
</script> </script>
@ -136,80 +136,86 @@
:width="350" :width="350"
> >
<div class="d-flex flex-column h-100"> <div class="d-flex flex-column h-100">
<v-list-item> <v-list-item>
<template v-slot:title> <template v-slot:title>
<div class="learning-path-title">{{ learningPath.data.title }}</div> <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-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> </template>
</using-query-result> <template v-slot:subtitle>
</div> <div>{{ learningPath.data.description }}</div>
<v-spacer></v-spacer> </template>
<v-list-item v-if="authService.authState.activeRole === 'teacher'"> </v-list-item>
<template v-slot:default> <v-list-item>
<v-btn class="button-in-nav" @click="assign()">{{ t("assignLearningPath") }}</v-btn> <template v-slot:subtitle>
</template> <p>
</v-list-item> <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> </div>
</v-navigation-drawer> </v-navigation-drawer>
<div class="control-bar-above-content"> <div class="control-bar-above-content">

View file

@ -1,13 +1,18 @@
export const essayQuestionAdapter: GiftAdapter = { export const essayQuestionAdapter: GiftAdapter = {
questionType: "Essay", questionType: "Essay",
installListener(questionElement: Element, answerUpdateCallback: (newAnswer: string | number | object) => void): void { installListener(
const textArea = questionElement.querySelector('textarea')!; questionElement: Element,
textArea.addEventListener('input', () => { answerUpdateCallback(textArea.value); }); 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 { setAnswer(questionElement: Element, answer: string | number | object): void {
const textArea = questionElement.querySelector('textarea')!; const textArea = questionElement.querySelector("textarea")!;
textArea.value = String(answer); textArea.value = String(answer);
} },
} };

View file

@ -1,5 +1,8 @@
interface GiftAdapter { interface GiftAdapter {
questionType: string; questionType: string;
installListener(questionElement: Element, answerUpdateCallback: (newAnswer: string | number | object) => void): void; installListener(
questionElement: Element,
answerUpdateCallback: (newAnswer: string | number | object) => void,
): void;
setAnswer(questionElement: Element, answer: string | number | object): void; setAnswer(questionElement: Element, answer: string | number | object): void;
} }

View file

@ -1,8 +1,8 @@
import {multipleChoiceQuestionAdapter} from "@/views/learning-paths/gift-adapters/multiple-choice-question-adapter.ts"; 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"; import { essayQuestionAdapter } from "@/views/learning-paths/gift-adapters/essay-question-adapter.ts";
export const giftAdapters = [multipleChoiceQuestionAdapter, essayQuestionAdapter]; export const giftAdapters = [multipleChoiceQuestionAdapter, essayQuestionAdapter];
export function getGiftAdapterForType(questionType: string): GiftAdapter | undefined { export function getGiftAdapterForType(questionType: string): GiftAdapter | undefined {
return giftAdapters.find(it => it.questionType === questionType); return giftAdapters.find((it) => it.questionType === questionType);
} }

View file

@ -1,11 +1,14 @@
export const multipleChoiceQuestionAdapter: GiftAdapter = { export const multipleChoiceQuestionAdapter: GiftAdapter = {
questionType: "MC", questionType: "MC",
installListener(questionElement: Element, answerUpdateCallback: (newAnswer: string | number | object) => void): void { installListener(
questionElement.querySelectorAll('input[type=radio]').forEach(element => { questionElement: Element,
answerUpdateCallback: (newAnswer: string | number | object) => void,
): void {
questionElement.querySelectorAll("input[type=radio]").forEach((element) => {
const input = element as HTMLInputElement; const input = element as HTMLInputElement;
input.addEventListener('change', () => { input.addEventListener("change", () => {
answerUpdateCallback(parseInt(input.value)); answerUpdateCallback(parseInt(input.value));
}); });
// Optional: initialize value if already selected // Optional: initialize value if already selected
@ -16,9 +19,9 @@ export const multipleChoiceQuestionAdapter: GiftAdapter = {
}, },
setAnswer(questionElement: Element, answer: string | number | object): void { setAnswer(questionElement: Element, answer: string | number | object): void {
questionElement.querySelectorAll('input[type=radio]').forEach(element => { questionElement.querySelectorAll("input[type=radio]").forEach((element) => {
const input = element as HTMLInputElement; const input = element as HTMLInputElement;
input.checked = String(answer) === String(input.value); input.checked = String(answer) === String(input.value);
}); });
} },
} };

View file

@ -3,9 +3,9 @@
import type { UseQueryReturnType } from "@tanstack/vue-query"; import type { UseQueryReturnType } from "@tanstack/vue-query";
import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts"; import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts";
import UsingQueryResult from "@/components/UsingQueryResult.vue"; import UsingQueryResult from "@/components/UsingQueryResult.vue";
import {computed, ref} from "vue"; import { computed, ref } from "vue";
import authService from "@/services/auth/auth-service.ts"; import authService from "@/services/auth/auth-service.ts";
import type {SubmissionData} from "@/views/learning-paths/learning-object/submission-data"; import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
import LearningObjectContentView from "@/views/learning-paths/learning-object/content/LearningObjectContentView.vue"; import LearningObjectContentView from "@/views/learning-paths/learning-object/content/LearningObjectContentView.vue";
import LearningObjectSubmissionsView from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue"; import LearningObjectSubmissionsView from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue";
@ -14,8 +14,8 @@
const props = defineProps<{ const props = defineProps<{
hruid: string; hruid: string;
language: Language; language: Language;
version: number, version: number;
group?: {forGroup: number, assignmentNo: number, classId: string} group?: { forGroup: number; assignmentNo: number; classId: string };
}>(); }>();
const learningObjectHtmlQueryResult: UseQueryReturnType<Document, Error> = useLearningObjectHTMLQuery( const learningObjectHtmlQueryResult: UseQueryReturnType<Document, Error> = useLearningObjectHTMLQuery(
@ -35,7 +35,7 @@
:learning-object-content="learningPathHtml.data" :learning-object-content="learningPathHtml.data"
v-model:submission-data="currentSubmission" v-model:submission-data="currentSubmission"
/> />
<div class="content-submissions-spacer"/> <div class="content-submissions-spacer" />
<learning-object-submissions-view <learning-object-submissions-view
v-if="props.group" v-if="props.group"
:group="props.group" :group="props.group"
@ -48,26 +48,26 @@
</template> </template>
<style scoped> <style scoped>
:deep(hr) { :deep(hr) {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }
:deep(li) { :deep(li) {
margin-left: 30px; margin-left: 30px;
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;
} }
:deep(img) { :deep(img) {
max-width: 80%; max-width: 80%;
} }
:deep(h2), :deep(h2),
:deep(h3), :deep(h3),
:deep(h4), :deep(h4),
:deep(h5), :deep(h5),
:deep(h6) { :deep(h6) {
margin-top: 10px; margin-top: 10px;
} }
.content-submissions-spacer { .content-submissions-spacer {
height: 20px; height: 20px;
} }
</style> </style>

View file

@ -1,32 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
import type {SubmissionData} from "@/views/learning-paths/learning-object/submission-data"; import { getGiftAdapterForType } from "@/views/learning-paths/gift-adapters/gift-adapters.ts";
import {getGiftAdapterForType} from "@/views/learning-paths/gift-adapters/gift-adapters.ts"; import { computed, nextTick, onMounted, watch } from "vue";
import {computed, nextTick, onMounted, watch} from "vue"; import { copyArrayWith } from "@/utils/array-utils.ts";
import {copyArrayWith} from "@/utils/array-utils.ts";
const props = defineProps<{ const props = defineProps<{
learningObjectContent: Document learningObjectContent: Document;
submissionData?: SubmissionData submissionData?: SubmissionData;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:submissionData", value: SubmissionData): void (e: "update:submissionData", value: SubmissionData): void;
}>(); }>();
const submissionData = computed<SubmissionData | undefined>({ const submissionData = computed<SubmissionData | undefined>({
get: () => props.submissionData, get: () => props.submissionData,
set: (v?: SubmissionData) => v ? emit('update:submissionData', v) : undefined, set: (v?: SubmissionData) => (v ? emit("update:submissionData", v) : undefined),
}); });
function forEachQuestion( function forEachQuestion(
doAction: (questionIndex: number, questionName: string, questionType: string, questionElement: Element) => void doAction: (questionIndex: number, questionName: string, questionType: string, questionElement: Element) => void,
) { ) {
const questions = document.querySelectorAll(".gift-question"); const questions = document.querySelectorAll(".gift-question");
questions.forEach(question => { questions.forEach((question) => {
const name = question.id.match(/gift-q(\d+)/)?.[1] const name = question.id.match(/gift-q(\d+)/)?.[1];
const questionType = question.className.split(" ") const questionType = question.className
.find(it => it.startsWith("gift-question-type")) .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; if (!name || isNaN(parseInt(name)) || !questionType) return;
@ -39,12 +39,9 @@
function attachQuestionListeners(): void { function attachQuestionListeners(): void {
forEachQuestion((index, _name, type, element) => { forEachQuestion((index, _name, type, element) => {
getGiftAdapterForType(type)?.installListener( getGiftAdapterForType(type)?.installListener(element, (newAnswer) => {
element, submissionData.value = copyArrayWith(index, newAnswer, submissionData.value ?? []);
(newAnswer) => { });
submissionData.value = copyArrayWith(index, newAnswer, submissionData.value ?? [])
}
);
}); });
} }
@ -62,19 +59,25 @@
onMounted(() => onMounted(() =>
nextTick(() => { nextTick(() => {
attachQuestionListeners() attachQuestionListeners();
setAnswers(props.submissionData ?? []); setAnswers(props.submissionData ?? []);
}) }),
); );
watch(() => props.learningObjectContent, async () => { watch(
await nextTick(); () => props.learningObjectContent,
attachQuestionListeners(); async () => {
}); await nextTick();
watch(() => props.submissionData, async () => { attachQuestionListeners();
await nextTick(); },
setAnswers(props.submissionData ?? []); );
}); watch(
() => props.submissionData,
async () => {
await nextTick();
setAnswers(props.submissionData ?? []);
},
);
</script> </script>
<template> <template>
@ -84,5 +87,4 @@
></div> ></div>
</template> </template>
<style scoped> <style scoped></style>
</style>

View file

@ -1,36 +1,37 @@
<script setup lang="ts"> <script setup lang="ts">
import type {SubmissionDTO} from "@dwengo-1/common/interfaces/submission"; import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
import {computed} from "vue"; import { computed } from "vue";
import {useI18n} from "vue-i18n"; import { useI18n } from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
allSubmissions: SubmissionDTO[] allSubmissions: SubmissionDTO[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: "submission-selected", submission: SubmissionDTO): void (e: "submission-selected", submission: SubmissionDTO): void;
}>(); }>();
const headers = computed(() => [ const headers = computed(() => [
{ title: "#", value: "submissionNo", width: "50px" }, { title: "#", value: "submissionNo", width: "50px" },
{ title: t("submittedBy"), value: "submittedBy" }, { title: t("submittedBy"), value: "submittedBy" },
{ title: t("timestamp"), value: "timestamp"}, { title: t("timestamp"), value: "timestamp" },
{ title: "", key: "action", width: "70px", sortable: false }, { title: "", key: "action", width: "70px", sortable: false },
]); ]);
const data = computed(() => [...props.allSubmissions] const data = computed(() =>
.sort((a, b) => (a.submissionNumber ?? 0) - (b.submissionNumber ?? 0)) [...props.allSubmissions]
.map((submission, index) => ({ .sort((a, b) => (a.submissionNumber ?? 0) - (b.submissionNumber ?? 0))
submissionNo: index + 1, .map((submission, index) => ({
submittedBy: `${submission.submitter.firstName} ${submission.submitter.lastName}`, submissionNo: index + 1,
timestamp: submission.time ? new Date(submission.time).toLocaleString(): "-", submittedBy: `${submission.submitter.firstName} ${submission.submitter.lastName}`,
dto: submission timestamp: submission.time ? new Date(submission.time).toLocaleString() : "-",
}) dto: submission,
)); })),
);
function selectSubmission(submission: SubmissionDTO) { function selectSubmission(submission: SubmissionDTO) {
emit('submission-selected', submission); emit("submission-selected", submission);
} }
</script> </script>
@ -38,14 +39,19 @@
<v-card> <v-card>
<v-card-title>{{ t("groupSubmissions") }}</v-card-title> <v-card-title>{{ t("groupSubmissions") }}</v-card-title>
<v-card-text> <v-card-text>
<v-data-table :headers="headers" <v-data-table
:items="data" :headers="headers"
density="compact" :items="data"
hide-default-footer density="compact"
:no-data-text="t('noSubmissionsYet')" hide-default-footer
:no-data-text="t('noSubmissionsYet')"
> >
<template v-slot:item.action="{ item }"> <template v-slot:item.action="{ item }">
<v-btn density="compact" variant="plain" @click="selectSubmission(item.dto)"> <v-btn
density="compact"
variant="plain"
@click="selectSubmission(item.dto)"
>
{{ t("loadSubmission") }} {{ t("loadSubmission") }}
</v-btn> </v-btn>
</template> </template>
@ -54,6 +60,4 @@
</v-card> </v-card>
</template> </template>
<style scoped> <style scoped></style>
</style>

View file

@ -1,26 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import type {SubmissionData} from "@/views/learning-paths/learning-object/submission-data"; import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
import type {SubmissionDTO} from "@dwengo-1/common/interfaces/submission"; import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
import {Language} from "@/data-objects/language.ts"; import { Language } from "@/data-objects/language.ts";
import {useSubmissionsQuery} from "@/queries/submissions.ts"; import { useSubmissionsQuery } from "@/queries/submissions.ts";
import UsingQueryResult from "@/components/UsingQueryResult.vue"; import UsingQueryResult from "@/components/UsingQueryResult.vue";
import SubmitButton from "@/views/learning-paths/learning-object/submissions/SubmitButton.vue"; import SubmitButton from "@/views/learning-paths/learning-object/submissions/SubmitButton.vue";
import {computed, watch} from "vue"; import { computed, watch } from "vue";
import LearningObjectSubmissionsTable import LearningObjectSubmissionsTable from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsTable.vue";
from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsTable.vue"; import { useI18n } from "vue-i18n";
import {useI18n} from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
submissionData?: SubmissionData, submissionData?: SubmissionData;
hruid: string; hruid: string;
language: Language; language: Language;
version: number, version: number;
group: {forGroup: number, assignmentNo: number, classId: string} group: { forGroup: number; assignmentNo: number; classId: string };
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:submissionData", value: SubmissionData): void (e: "update:submissionData", value: SubmissionData): void;
}>(); }>();
const submissionQuery = useSubmissionsQuery( const submissionQuery = useSubmissionsQuery(
@ -30,7 +29,7 @@
() => props.group.classId, () => props.group.classId,
() => props.group.assignmentNo, () => props.group.assignmentNo,
() => props.group.forGroup, () => props.group.forGroup,
() => true () => true,
); );
function emitSubmissionData(submissionData: SubmissionData) { function emitSubmissionData(submissionData: SubmissionData) {
@ -58,17 +57,16 @@
return JSON.parse(submissions[submissions.length - 1].content); return JSON.parse(submissions[submissions.length - 1].content);
}); });
const showSubmissionTable = computed(() => const showSubmissionTable = computed(() => props.submissionData !== undefined && props.submissionData.length > 0);
props.submissionData !== undefined && props.submissionData.length > 0
);
const showIsDoneMessage = computed(() => const showIsDoneMessage = computed(() => lastSubmission.value !== undefined && lastSubmission.value.length === 0);
lastSubmission.value !== undefined && lastSubmission.value.length === 0
);
</script> </script>
<template> <template>
<using-query-result :query-result="submissionQuery" v-slot="submissions: { data: SubmissionDTO[] }"> <using-query-result
:query-result="submissionQuery"
v-slot="submissions: { data: SubmissionDTO[] }"
>
<submit-button <submit-button
:hruid="props.hruid" :hruid="props.hruid"
:language="props.language" :language="props.language"
@ -78,12 +76,13 @@
:submissions="submissions.data" :submissions="submissions.data"
/> />
<div class="submit-submissions-spacer"></div> <div class="submit-submissions-spacer"></div>
<v-alert icon="mdi-check" <v-alert
:text="t('taskCompleted')" icon="mdi-check"
type="success" :text="t('taskCompleted')"
variant="tonal" type="success"
density="compact" variant="tonal"
v-if="showIsDoneMessage" density="compact"
v-if="showIsDoneMessage"
></v-alert> ></v-alert>
<learning-object-submissions-table <learning-object-submissions-table
v-if="submissionQuery.data && showSubmissionTable" v-if="submissionQuery.data && showSubmissionTable"
@ -94,7 +93,7 @@
</template> </template>
<style scoped> <style scoped>
.submit-submissions-spacer { .submit-submissions-spacer {
height: 20px; height: 20px;
} }
</style> </style>

View file

@ -1,26 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed} from "vue"; import { computed } from "vue";
import authService from "@/services/auth/auth-service.ts"; import authService from "@/services/auth/auth-service.ts";
import type {SubmissionData} from "@/views/learning-paths/learning-object/submission-data"; import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
import {Language} from "@/data-objects/language.ts"; import { Language } from "@/data-objects/language.ts";
import type {SubmissionDTO} from "@dwengo-1/common/interfaces/submission"; import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
import {useCreateSubmissionMutation} from "@/queries/submissions.ts"; import { useCreateSubmissionMutation } from "@/queries/submissions.ts";
import {deepEquals} from "@/utils/deep-equals.ts"; import { deepEquals } from "@/utils/deep-equals.ts";
import type {UserProfile} from "oidc-client-ts"; import type { UserProfile } from "oidc-client-ts";
import type {LearningObjectIdentifierDTO} from "@dwengo-1/common/interfaces/learning-content"; import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content";
import type {StudentDTO} from "@dwengo-1/common/interfaces/student"; import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
import type {GroupDTO} from "@dwengo-1/common/interfaces/group"; import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
import {useI18n} from "vue-i18n"; import { useI18n } from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
submissionData?: SubmissionData, submissionData?: SubmissionData;
submissions: SubmissionDTO[], submissions: SubmissionDTO[];
hruid: string; hruid: string;
language: Language; language: Language;
version: number, version: number;
group: {forGroup: number, assignmentNo: number, classId: string} group: { forGroup: number; assignmentNo: number; classId: string };
}>(); }>();
const { const {
@ -28,7 +28,7 @@
isError: submissionFailed, isError: submissionFailed,
error: submissionError, error: submissionError,
isSuccess: submissionSuccess, isSuccess: submissionSuccess,
mutate: submitSolution mutate: submitSolution,
} = useCreateSubmissionMutation(); } = useCreateSubmissionMutation();
const isStudent = computed(() => authService.authState.activeRole === "student"); const isStudent = computed(() => authService.authState.activeRole === "student");
@ -37,16 +37,13 @@
if (!props.submissionData || props.submissions === undefined) { if (!props.submissionData || props.submissions === undefined) {
return true; return true;
} }
if (props.submissionData.some(answer => answer === null)) { if (props.submissionData.some((answer) => answer === null)) {
return false; return false;
} }
if (props.submissions.length === 0) { if (props.submissions.length === 0) {
return false; return false;
} }
return deepEquals( return deepEquals(JSON.parse(props.submissions[props.submissions.length - 1].content), props.submissionData);
JSON.parse(props.submissions[props.submissions.length - 1].content),
props.submissionData
);
}); });
function submitCurrentAnswer(): void { function submitCurrentAnswer(): void {
@ -55,25 +52,25 @@
const learningObjectIdentifier: LearningObjectIdentifierDTO = { const learningObjectIdentifier: LearningObjectIdentifierDTO = {
hruid: props.hruid, hruid: props.hruid,
language: props.language as Language, language: props.language as Language,
version: props.version version: props.version,
}; };
const submitter: StudentDTO = { const submitter: StudentDTO = {
id: currentUser.preferred_username!, id: currentUser.preferred_username!,
username: currentUser.preferred_username!, username: currentUser.preferred_username!,
firstName: currentUser.given_name!, firstName: currentUser.given_name!,
lastName: currentUser.family_name! lastName: currentUser.family_name!,
}; };
const group: GroupDTO = { const group: GroupDTO = {
class: classId, class: classId,
assignment: assignmentNo, assignment: assignmentNo,
groupNumber: forGroup groupNumber: forGroup,
} };
const submission: SubmissionDTO = { const submission: SubmissionDTO = {
learningObjectIdentifier, learningObjectIdentifier,
submitter, submitter,
group, group,
content: JSON.stringify(props.submissionData) content: JSON.stringify(props.submissionData),
} };
submitSolution({ data: submission }); submitSolution({ data: submission });
} }
@ -86,17 +83,16 @@
</script> </script>
<template> <template>
<v-btn v-if="isStudent && !isSubmitDisabled" <v-btn
prepend-icon="mdi-check" v-if="isStudent && !isSubmitDisabled"
variant="elevated" prepend-icon="mdi-check"
:loading="submissionIsPending" variant="elevated"
:disabled="isSubmitDisabled" :loading="submissionIsPending"
@click="submitCurrentAnswer()" :disabled="isSubmitDisabled"
@click="submitCurrentAnswer()"
> >
{{ buttonText }} {{ buttonText }}
</v-btn> </v-btn>
</template> </template>
<style scoped> <style scoped></style>
</style>