merge: fixed merge conflicts
This commit is contained in:
		
						commit
						90f43e74ba
					
				
					 101 changed files with 2156 additions and 1470 deletions
				
			
		|  | @ -3,13 +3,10 @@ import { themes } from '../data/themes.js'; | ||||||
| import { FALLBACK_LANG } from '../config.js'; | import { FALLBACK_LANG } from '../config.js'; | ||||||
| import learningPathService from '../services/learning-paths/learning-path-service.js'; | import learningPathService from '../services/learning-paths/learning-path-service.js'; | ||||||
| import { Language } from '@dwengo-1/common/util/language'; | import { Language } from '@dwengo-1/common/util/language'; | ||||||
| import { |  | ||||||
|     PersonalizationTarget, |  | ||||||
|     personalizedForGroup, |  | ||||||
|     personalizedForStudent, |  | ||||||
| } from '../services/learning-paths/learning-path-personalization-util.js'; |  | ||||||
| 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.js'; | ||||||
|  | import { getAssignmentRepository, getGroupRepository } from '../data/repositories.js'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Fetch learning paths based on query parameters. |  * Fetch learning paths based on query parameters. | ||||||
|  | @ -20,20 +17,20 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi | ||||||
|     const searchQuery = req.query.search as string; |     const searchQuery = req.query.search as string; | ||||||
|     const language = (req.query.language as string) || FALLBACK_LANG; |     const language = (req.query.language as string) || FALLBACK_LANG; | ||||||
| 
 | 
 | ||||||
|     const forStudent = req.query.forStudent as string; |  | ||||||
|     const forGroupNo = req.query.forGroup as string; |     const forGroupNo = req.query.forGroup as string; | ||||||
|     const assignmentNo = req.query.assignmentNo as string; |     const assignmentNo = req.query.assignmentNo as string; | ||||||
|     const classId = req.query.classId as string; |     const classId = req.query.classId as string; | ||||||
| 
 | 
 | ||||||
|     let personalizationTarget: PersonalizationTarget | undefined; |     let forGroup: Group | undefined; | ||||||
| 
 | 
 | ||||||
|     if (forStudent) { |     if (forGroupNo) { | ||||||
|         personalizationTarget = await personalizedForStudent(forStudent); |  | ||||||
|     } else if (forGroupNo) { |  | ||||||
|         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.'); | ||||||
|         } |         } | ||||||
|         personalizationTarget = await personalizedForGroup(classId, parseInt(assignmentNo), parseInt(forGroupNo)); |         const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo)); | ||||||
|  |         if (assignment) { | ||||||
|  |             forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let hruidList; |     let hruidList; | ||||||
|  | @ -48,18 +45,13 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi | ||||||
|             throw new NotFoundException(`Theme "${themeKey}" not found.`); |             throw new NotFoundException(`Theme "${themeKey}" not found.`); | ||||||
|         } |         } | ||||||
|     } else if (searchQuery) { |     } else if (searchQuery) { | ||||||
|         const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, personalizationTarget); |         const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, forGroup); | ||||||
|         res.json(searchResults); |         res.json(searchResults); | ||||||
|         return; |         return; | ||||||
|     } else { |     } else { | ||||||
|         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(', ')}`, |  | ||||||
|         personalizationTarget |  | ||||||
|     ); |  | ||||||
|     res.json(learningPaths.data); |     res.json(learningPaths.data); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -17,15 +17,18 @@ export async function getSubmissionsHandler(req: Request, res: Response): Promis | ||||||
|     const lang = languageMap[req.query.language as string] || Language.Dutch; |     const lang = languageMap[req.query.language as string] || Language.Dutch; | ||||||
|     const version = parseInt(req.query.version as string) ?? 1; |     const version = parseInt(req.query.version as string) ?? 1; | ||||||
| 
 | 
 | ||||||
|     const submissions = await getSubmissionsForLearningObjectAndAssignment( |     const forGroup = req.query.forGroup as string | undefined; | ||||||
|  | 
 | ||||||
|  |     const submissions: SubmissionDTO[] = await getSubmissionsForLearningObjectAndAssignment( | ||||||
|         loHruid, |         loHruid, | ||||||
|         lang, |         lang, | ||||||
|         version, |         version, | ||||||
|         req.query.classId as string, |         req.query.classId as string, | ||||||
|         parseInt(req.query.assignmentId as string) |         parseInt(req.query.assignmentId as string), | ||||||
|  |         forGroup ? parseInt(forGroup) : undefined | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     res.json(submissions); |     res.json({ submissions }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function getSubmissionHandler(req: Request, res: Response): Promise<void> { | export async function getSubmissionHandler(req: Request, res: Response): Promise<void> { | ||||||
|  |  | ||||||
|  | @ -61,32 +61,30 @@ 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. | ||||||
|      * When forStudentUsername is set, only the submissions of the given user's group are shown. |  | ||||||
|      */ |      */ | ||||||
|     public async findAllSubmissionsForLearningObjectAndAssignment( |     public async findAllSubmissionsForLearningObjectAndAssignment(loId: LearningObjectIdentifier, assignment: Assignment): Promise<Submission[]> { | ||||||
|         loId: LearningObjectIdentifier, |  | ||||||
|         assignment: Assignment, |  | ||||||
|         forStudentUsername?: string |  | ||||||
|     ): Promise<Submission[]> { |  | ||||||
|         const onBehalfOf = forStudentUsername |  | ||||||
|             ? { |  | ||||||
|                   assignment, |  | ||||||
|                   members: { |  | ||||||
|                       $some: { |  | ||||||
|                           username: forStudentUsername, |  | ||||||
|                       }, |  | ||||||
|                   }, |  | ||||||
|               } |  | ||||||
|             : { |  | ||||||
|                   assignment, |  | ||||||
|               }; |  | ||||||
| 
 |  | ||||||
|         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, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Looks up all submissions for the given learning object which were submitted by the given group | ||||||
|  |      */ | ||||||
|  |     public async findAllSubmissionsForLearningObjectAndGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission[]> { | ||||||
|  |         return this.findAll({ | ||||||
|  |             where: { | ||||||
|  |                 learningObjectHruid: loId.hruid, | ||||||
|  |                 learningObjectLanguage: loId.language, | ||||||
|  |                 learningObjectVersion: loId.version, | ||||||
|  |                 onBehalfOf: group, | ||||||
|             }, |             }, | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,6 +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.js'; | ||||||
|  | import { RequiredEntityData } from '@mikro-orm/core'; | ||||||
|  | import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; | ||||||
|  | import { EntityAlreadyExistsException } from '../../exceptions/entity-already-exists-exception.js'; | ||||||
| 
 | 
 | ||||||
| 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> { | ||||||
|  | @ -23,4 +27,27 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath> | ||||||
|             populate: ['nodes', 'nodes.transitions'], |             populate: ['nodes', 'nodes.transitions'], | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public createNode(nodeData: RequiredEntityData<LearningPathNode>): LearningPathNode { | ||||||
|  |         return this.em.create(LearningPathNode, nodeData); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public createTransition(transitionData: RequiredEntityData<LearningPathTransition>): LearningPathTransition { | ||||||
|  |         return this.em.create(LearningPathTransition, transitionData); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async saveLearningPathNodesAndTransitions( | ||||||
|  |         path: LearningPath, | ||||||
|  |         nodes: LearningPathNode[], | ||||||
|  |         transitions: LearningPathTransition[], | ||||||
|  |         options?: { preventOverwrite?: boolean } | ||||||
|  |     ): Promise<void> { | ||||||
|  |         if (options?.preventOverwrite && (await this.findOne(path))) { | ||||||
|  |             throw new EntityAlreadyExistsException('A learning path with this hruid/language combination already exists.'); | ||||||
|  |         } | ||||||
|  |         const em = this.getEntityManager(); | ||||||
|  |         await em.persistAndFlush(path); | ||||||
|  |         await Promise.all(nodes.map(async (it) => em.persistAndFlush(it))); | ||||||
|  |         await Promise.all(transitions.map(async (it) => em.persistAndFlush(it))); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,6 +6,9 @@ import { Language } from '@dwengo-1/common/util/language'; | ||||||
| 
 | 
 | ||||||
| @Entity({ repository: () => SubmissionRepository }) | @Entity({ repository: () => SubmissionRepository }) | ||||||
| export class Submission { | export class Submission { | ||||||
|  |     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||||
|  |     submissionNumber?: number; | ||||||
|  | 
 | ||||||
|     @PrimaryKey({ type: 'string' }) |     @PrimaryKey({ type: 'string' }) | ||||||
|     learningObjectHruid!: string; |     learningObjectHruid!: string; | ||||||
| 
 | 
 | ||||||
|  | @ -15,12 +18,9 @@ export class Submission { | ||||||
|     }) |     }) | ||||||
|     learningObjectLanguage!: Language; |     learningObjectLanguage!: Language; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'numeric' }) |     @PrimaryKey({ type: 'numeric', autoincrement: false }) | ||||||
|     learningObjectVersion = 1; |     learningObjectVersion = 1; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'integer', autoincrement: true }) |  | ||||||
|     submissionNumber?: number; |  | ||||||
| 
 |  | ||||||
|     @ManyToOne({ |     @ManyToOne({ | ||||||
|         entity: () => Group, |         entity: () => Group, | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { 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: 'array', nullable: true }) |     @Property({ type: new ArrayType((i) => Number(i)), nullable: true }) | ||||||
|     targetAges?: number[] = []; |     targetAges?: number[] = []; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'bool' }) |     @Property({ type: 'bool' }) | ||||||
|  |  | ||||||
|  | @ -1,16 +1,16 @@ | ||||||
| import { 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'; | ||||||
| 
 | 
 | ||||||
| @Entity() | @Entity() | ||||||
| export class LearningPathNode { | export class LearningPathNode { | ||||||
|  |     @PrimaryKey({ type: 'integer', autoincrement: true }) | ||||||
|  |     nodeNumber?: number; | ||||||
|  | 
 | ||||||
|     @ManyToOne({ entity: () => LearningPath, primary: true }) |     @ManyToOne({ entity: () => LearningPath, primary: true }) | ||||||
|     learningPath!: Rel<LearningPath>; |     learningPath!: Rel<LearningPath>; | ||||||
| 
 | 
 | ||||||
|     @PrimaryKey({ type: 'integer', autoincrement: true }) |  | ||||||
|     nodeNumber!: number; |  | ||||||
| 
 |  | ||||||
|     @Property({ type: 'string' }) |     @Property({ type: 'string' }) | ||||||
|     learningObjectHruid!: string; |     learningObjectHruid!: string; | ||||||
| 
 | 
 | ||||||
|  | @ -27,7 +27,7 @@ export class LearningPathNode { | ||||||
|     startNode!: boolean; |     startNode!: boolean; | ||||||
| 
 | 
 | ||||||
|     @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' }) |     @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' }) | ||||||
|     transitions: LearningPathTransition[] = []; |     transitions!: Collection<LearningPathTransition>; | ||||||
| 
 | 
 | ||||||
|     @Property({ length: 3 }) |     @Property({ length: 3 }) | ||||||
|     createdAt: Date = new Date(); |     createdAt: Date = new Date(); | ||||||
|  |  | ||||||
|  | @ -3,12 +3,12 @@ import { LearningPathNode } from './learning-path-node.entity.js'; | ||||||
| 
 | 
 | ||||||
| @Entity() | @Entity() | ||||||
| export class LearningPathTransition { | export class LearningPathTransition { | ||||||
|     @ManyToOne({ entity: () => LearningPathNode, primary: true }) |  | ||||||
|     node!: Rel<LearningPathNode>; |  | ||||||
| 
 |  | ||||||
|     @PrimaryKey({ type: 'numeric' }) |     @PrimaryKey({ type: 'numeric' }) | ||||||
|     transitionNumber!: number; |     transitionNumber!: number; | ||||||
| 
 | 
 | ||||||
|  |     @ManyToOne({ entity: () => LearningPathNode, primary: true }) | ||||||
|  |     node!: Rel<LearningPathNode>; | ||||||
|  | 
 | ||||||
|     @Property({ type: 'string' }) |     @Property({ type: 'string' }) | ||||||
|     condition!: string; |     condition!: string; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { 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'; | ||||||
|  | @ -25,5 +25,5 @@ export class LearningPath { | ||||||
|     image: Buffer | null = null; |     image: Buffer | null = null; | ||||||
| 
 | 
 | ||||||
|     @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' }) |     @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' }) | ||||||
|     nodes: LearningPathNode[] = []; |     nodes: Collection<LearningPathNode> = new Collection<LearningPathNode>(this); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { Submission } from '../entities/assignments/submission.entity.js'; | import { Submission } from '../entities/assignments/submission.entity.js'; | ||||||
| import { mapToGroupDTO } from './group.js'; | import { mapToGroupDTOId } from './group.js'; | ||||||
| import { mapToStudentDTO } from './student.js'; | import { mapToStudentDTO } from './student.js'; | ||||||
| import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; | import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; | ||||||
| import { getSubmissionRepository } from '../data/repositories.js'; | import { getSubmissionRepository } from '../data/repositories.js'; | ||||||
|  | @ -13,11 +13,10 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { | ||||||
|             language: submission.learningObjectLanguage, |             language: submission.learningObjectLanguage, | ||||||
|             version: submission.learningObjectVersion, |             version: submission.learningObjectVersion, | ||||||
|         }, |         }, | ||||||
| 
 |  | ||||||
|         submissionNumber: submission.submissionNumber, |         submissionNumber: submission.submissionNumber, | ||||||
|         submitter: mapToStudentDTO(submission.submitter), |         submitter: mapToStudentDTO(submission.submitter), | ||||||
|         time: submission.submissionTime, |         time: submission.submissionTime, | ||||||
|         group: submission.onBehalfOf ? mapToGroupDTO(submission.onBehalfOf, submission.onBehalfOf.assignment.within) : undefined, |         group: submission.onBehalfOf ? mapToGroupDTOId(submission.onBehalfOf, submission.onBehalfOf.assignment.within) : undefined, | ||||||
|         content: submission.content, |         content: submission.content, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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}`); |         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); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ const router = express.Router({ mergeParams: true }); | ||||||
| // Root endpoint used to search objects
 | // Root endpoint used to search objects
 | ||||||
| router.get('/', getSubmissionsHandler); | router.get('/', getSubmissionsHandler); | ||||||
| 
 | 
 | ||||||
| router.post('/:id', createSubmissionHandler); | router.post('/', createSubmissionHandler); | ||||||
| 
 | 
 | ||||||
| // Information about an submission with id 'id'
 | // Information about an submission with id 'id'
 | ||||||
| router.get('/:id', getSubmissionHandler); | router.get('/:id', getSubmissionHandler); | ||||||
|  |  | ||||||
|  | @ -8,12 +8,13 @@ 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'; | ||||||
| 
 | 
 | ||||||
| function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { | function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { | ||||||
|     return { |     return { | ||||||
|         key: data.hruid, // Hruid learningObject (not path)
 |         key: data.hruid, // Hruid learningObject (not path)
 | ||||||
|         _id: data._id, |         _id: data._id, | ||||||
|         uuid: data.uuid, |         uuid: data.uuid || v4(), | ||||||
|         version: data.version, |         version: data.version, | ||||||
|         title: data.title, |         title: data.title, | ||||||
|         htmlUrl, // Url to fetch html content
 |         htmlUrl, // Url to fetch html content
 | ||||||
|  |  | ||||||
|  | @ -32,7 +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: JSON.parse(learningObject.returnValue.callbackSchema), |             callback_schema: learningObject.returnValue.callbackSchema === '' ? '' : JSON.parse(learningObject.returnValue.callbackSchema), | ||||||
|         }, |         }, | ||||||
|         skosConcepts: learningObject.skosConcepts, |         skosConcepts: learningObject.skosConcepts, | ||||||
|         targetAges: learningObject.targetAges || [], |         targetAges: learningObject.targetAges || [], | ||||||
|  |  | ||||||
|  | @ -11,6 +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'; | ||||||
| 
 | 
 | ||||||
| const logger: Logger = getLogger(); | const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|  | @ -23,7 +24,7 @@ function filterData(data: LearningObjectMetadata): FilteredLearningObject { | ||||||
|     return { |     return { | ||||||
|         key: data.hruid, // Hruid learningObject (not path)
 |         key: data.hruid, // Hruid learningObject (not path)
 | ||||||
|         _id: data._id, |         _id: data._id, | ||||||
|         uuid: data.uuid, |         uuid: data.uuid ?? v4(), | ||||||
|         version: data.version, |         version: data.version, | ||||||
|         title: data.title, |         title: data.title, | ||||||
|         htmlUrl: `/learningObject/${data.hruid}/html?language=${data.language}&version=${data.version}`, // Url to fetch html content
 |         htmlUrl: `/learningObject/${data.hruid}/html?language=${data.language}&version=${data.version}`, // Url to fetch html content
 | ||||||
|  |  | ||||||
|  | @ -38,7 +38,7 @@ class GiftProcessor extends StringProcessor { | ||||||
|         let html = "<div class='learning-object-gift'>\n"; |         let html = "<div class='learning-object-gift'>\n"; | ||||||
|         let i = 1; |         let i = 1; | ||||||
|         for (const question of quizQuestions) { |         for (const question of quizQuestions) { | ||||||
|             html += `    <div class='gift-question' id='gift-q${i}'>\n`; |             html += `    <div class='gift-question gift-question-type-${question.type}' id='gift-q${i}'>\n`; | ||||||
|             html += '        ' + this.renderQuestion(question, i).replaceAll(/\n(.+)/g, '\n        $1'); // Replace for indentation.
 |             html += '        ' + this.renderQuestion(question, i).replaceAll(/\n(.+)/g, '\n        $1'); // Replace for indentation.
 | ||||||
|             html += `    </div>\n`; |             html += `    </div>\n`; | ||||||
|             i++; |             i++; | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<Multipl | ||||||
|         for (const choice of question.choices) { |         for (const choice of question.choices) { | ||||||
|             renderedHtml += `<div class="gift-choice-div">\n`; |             renderedHtml += `<div class="gift-choice-div">\n`; | ||||||
|             renderedHtml += `    <input type='radio' id='gift-q${questionNumber}-choice-${i}' name='gift-q${questionNumber}-choices' value="${i}"/>\n`; |             renderedHtml += `    <input type='radio' id='gift-q${questionNumber}-choice-${i}' name='gift-q${questionNumber}-choices' value="${i}"/>\n`; | ||||||
|             renderedHtml += `    <label for='gift-q${questionNumber}-choice-${i}'>${choice.text}</label>\n`; |             renderedHtml += `    <label for='gift-q${questionNumber}-choice-${i}'>${choice.text.text}</label>\n`; | ||||||
|             renderedHtml += `</div>\n`; |             renderedHtml += `</div>\n`; | ||||||
|             i++; |             i++; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import { getLearningPathRepository } from '../../data/repositories.js'; | ||||||
| import learningObjectService from '../learning-objects/learning-object-service.js'; | import learningObjectService from '../learning-objects/learning-object-service.js'; | ||||||
| import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; | import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; | ||||||
| import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; | import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; | ||||||
| import { getLastSubmissionForCustomizationTarget, isTransitionPossible, PersonalizationTarget } from './learning-path-personalization-util.js'; | import { getLastSubmissionForGroup, isTransitionPossible } from './learning-path-personalization-util.js'; | ||||||
| import { | import { | ||||||
|     FilteredLearningObject, |     FilteredLearningObject, | ||||||
|     LearningObjectNode, |     LearningObjectNode, | ||||||
|  | @ -13,13 +13,16 @@ 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 { Collection } from '@mikro-orm/core'; | ||||||
|  | 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 | ||||||
|  * corresponding learning object. |  * corresponding learning object. | ||||||
|  * @param nodes The nodes to find the learning object for. |  * @param nodes The nodes to find the learning object for. | ||||||
|  */ |  */ | ||||||
| async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Map<LearningPathNode, FilteredLearningObject>> { | async function getLearningObjectsForNodes(nodes: Collection<LearningPathNode>): Promise<Map<LearningPathNode, FilteredLearningObject>> { | ||||||
|     // Fetching the corresponding learning object for each of the nodes and creating a map that maps each node to
 |     // Fetching the corresponding learning object for each of the nodes and creating a map that maps each node to
 | ||||||
|     // Its corresponding learning object.
 |     // Its corresponding learning object.
 | ||||||
|     const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>( |     const nullableNodesToLearningObjects = new Map<LearningPathNode, FilteredLearningObject | null>( | ||||||
|  | @ -44,7 +47,7 @@ async function getLearningObjectsForNodes(nodes: LearningPathNode[]): Promise<Ma | ||||||
| /** | /** | ||||||
|  * Convert the given learning path entity to an object which conforms to the learning path content. |  * Convert the given learning path entity to an object which conforms to the learning path content. | ||||||
|  */ |  */ | ||||||
| async function convertLearningPath(learningPath: LearningPathEntity, order: number, personalizedFor?: PersonalizationTarget): Promise<LearningPath> { | async function convertLearningPath(learningPath: LearningPathEntity, order: number, personalizedFor?: Group): Promise<LearningPath> { | ||||||
|     // Fetch the corresponding learning object for each node since some parts of the expected response contains parts
 |     // Fetch the corresponding learning object for each node since some parts of the expected response contains parts
 | ||||||
|     // With information which is not available in the LearningPathNodes themselves.
 |     // With information which is not available in the LearningPathNodes themselves.
 | ||||||
|     const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes); |     const nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> = await getLearningObjectsForNodes(learningPath.nodes); | ||||||
|  | @ -89,10 +92,10 @@ async function convertLearningPath(learningPath: LearningPathEntity, order: numb | ||||||
| async function convertNode( | async function convertNode( | ||||||
|     node: LearningPathNode, |     node: LearningPathNode, | ||||||
|     learningObject: FilteredLearningObject, |     learningObject: FilteredLearningObject, | ||||||
|     personalizedFor: PersonalizationTarget | undefined, |     personalizedFor: Group | undefined, | ||||||
|     nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> |     nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject> | ||||||
| ): Promise<LearningObjectNode> { | ): Promise<LearningObjectNode> { | ||||||
|     const lastSubmission = personalizedFor ? await getLastSubmissionForCustomizationTarget(node, personalizedFor) : null; |     const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(node, personalizedFor) : null; | ||||||
|     const transitions = node.transitions |     const transitions = node.transitions | ||||||
|         .filter( |         .filter( | ||||||
|             (trans) => |             (trans) => | ||||||
|  | @ -108,6 +111,7 @@ async function convertNode( | ||||||
|         updatedAt: node.updatedAt.toISOString(), |         updatedAt: node.updatedAt.toISOString(), | ||||||
|         learningobject_hruid: node.learningObjectHruid, |         learningobject_hruid: node.learningObjectHruid, | ||||||
|         version: learningObject.version, |         version: learningObject.version, | ||||||
|  |         done: personalizedFor ? lastSubmission !== null : undefined, | ||||||
|         transitions, |         transitions, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | @ -121,7 +125,7 @@ async function convertNode( | ||||||
|  */ |  */ | ||||||
| async function convertNodes( | async function convertNodes( | ||||||
|     nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>, |     nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>, | ||||||
|     personalizedFor?: PersonalizationTarget |     personalizedFor?: Group | ||||||
| ): Promise<LearningObjectNode[]> { | ): Promise<LearningObjectNode[]> { | ||||||
|     const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => |     const nodesPromise = Array.from(nodesToLearningObjects.entries()).map(async (entry) => | ||||||
|         convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects) |         convertNode(entry[0], entry[1], personalizedFor, nodesToLearningObjects) | ||||||
|  | @ -161,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 + index, // 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, | ||||||
|  | @ -177,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?: PersonalizationTarget |  | ||||||
|     ): 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( | ||||||
|  | @ -202,7 +201,7 @@ const databaseLearningPathProvider: LearningPathProvider = { | ||||||
|     /** |     /** | ||||||
|      * Search learning paths in the database using the given search string. |      * Search learning paths in the database using the given search string. | ||||||
|      */ |      */ | ||||||
|     async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> { |     async searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]> { | ||||||
|         const learningPathRepo = getLearningPathRepository(); |         const learningPathRepo = getLearningPathRepository(); | ||||||
| 
 | 
 | ||||||
|         const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language); |         const searchResults = await learningPathRepo.findByQueryStringAndLanguage(query, language); | ||||||
|  |  | ||||||
|  | @ -1,76 +1,22 @@ | ||||||
| import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; | import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; | ||||||
| import { Student } from '../../entities/users/student.entity.js'; |  | ||||||
| import { Group } from '../../entities/assignments/group.entity.js'; | import { Group } from '../../entities/assignments/group.entity.js'; | ||||||
| import { Submission } from '../../entities/assignments/submission.entity.js'; | import { Submission } from '../../entities/assignments/submission.entity.js'; | ||||||
| import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../../data/repositories.js'; | import { getSubmissionRepository } from '../../data/repositories.js'; | ||||||
| import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; | ||||||
| import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; | import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; | ||||||
| import { JSONPath } from 'jsonpath-plus'; | import { JSONPath } from 'jsonpath-plus'; | ||||||
| 
 | 
 | ||||||
| export type PersonalizationTarget = { type: 'student'; student: Student } | { type: 'group'; group: Group }; |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * Shortcut function to easily create a PersonalizationTarget object for a student by his/her username. |  * Returns the last submission for the learning object associated with the given node and for the group | ||||||
|  * @param username Username of the student we want to generate a personalized learning path for. |  | ||||||
|  *                 If there is no student with this username, return undefined. |  | ||||||
|  */ |  */ | ||||||
| export async function personalizedForStudent(username: string): Promise<PersonalizationTarget | undefined> { | export async function getLastSubmissionForGroup(node: LearningPathNode, pathFor: Group): Promise<Submission | null> { | ||||||
|     const student = await getStudentRepository().findByUsername(username); |  | ||||||
|     if (student) { |  | ||||||
|         return { |  | ||||||
|             type: 'student', |  | ||||||
|             student: student, |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
|     return undefined; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Shortcut function to easily create a PersonalizationTarget object for a group by class name, assignment number and |  | ||||||
|  * group number. |  | ||||||
|  * @param classId Id of the class in which this group was created |  | ||||||
|  * @param assignmentNumber Number of the assignment for which this group was created |  | ||||||
|  * @param groupNumber Number of the group for which we want to personalize the learning path. |  | ||||||
|  */ |  | ||||||
| export async function personalizedForGroup( |  | ||||||
|     classId: string, |  | ||||||
|     assignmentNumber: number, |  | ||||||
|     groupNumber: number |  | ||||||
| ): Promise<PersonalizationTarget | undefined> { |  | ||||||
|     const clazz = await getClassRepository().findById(classId); |  | ||||||
|     if (!clazz) { |  | ||||||
|         return undefined; |  | ||||||
|     } |  | ||||||
|     const group = await getGroupRepository().findOne({ |  | ||||||
|         assignment: { |  | ||||||
|             within: clazz, |  | ||||||
|             id: assignmentNumber, |  | ||||||
|         }, |  | ||||||
|         groupNumber: groupNumber, |  | ||||||
|     }); |  | ||||||
|     if (group) { |  | ||||||
|         return { |  | ||||||
|             type: 'group', |  | ||||||
|             group: group, |  | ||||||
|         }; |  | ||||||
|     } |  | ||||||
|     return undefined; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Returns the last submission for the learning object associated with the given node and for the student or group |  | ||||||
|  */ |  | ||||||
| export async function getLastSubmissionForCustomizationTarget(node: LearningPathNode, pathFor: PersonalizationTarget): Promise<Submission | null> { |  | ||||||
|     const submissionRepo = getSubmissionRepository(); |     const submissionRepo = getSubmissionRepository(); | ||||||
|     const learningObjectId: LearningObjectIdentifier = { |     const learningObjectId: LearningObjectIdentifier = { | ||||||
|         hruid: node.learningObjectHruid, |         hruid: node.learningObjectHruid, | ||||||
|         language: node.language, |         language: node.language, | ||||||
|         version: node.version, |         version: node.version, | ||||||
|     }; |     }; | ||||||
|     if (pathFor.type === 'group') { |     return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor); | ||||||
|         return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor.group); |  | ||||||
|     } |  | ||||||
|     return await submissionRepo.findMostRecentSubmissionForStudent(learningObjectId, pathFor.student); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
|  | @ -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 { PersonalizationTarget } from './learning-path-personalization-util.js'; |  | ||||||
| import { Language } from '@dwengo-1/common/util/language'; | import { Language } from '@dwengo-1/common/util/language'; | ||||||
|  | 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. | ||||||
|  | @ -9,10 +9,10 @@ export interface LearningPathProvider { | ||||||
|     /** |     /** | ||||||
|      * Fetch the learning paths with the given hruids from the data source. |      * Fetch the learning paths with the given hruids from the data source. | ||||||
|      */ |      */ | ||||||
|     fetchLearningPaths(hruids: string[], language: Language, source: string, personalizedFor?: PersonalizationTarget): Promise<LearningPathResponse>; |     fetchLearningPaths(hruids: string[], language: Language, source: string, personalizedFor?: Group): Promise<LearningPathResponse>; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Search learning paths in the data source using the given search string. |      * Search learning paths in the data source using the given search string. | ||||||
|      */ |      */ | ||||||
|     searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]>; |     searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]>; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,13 +1,78 @@ | ||||||
| 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 { PersonalizationTarget } from './learning-path-personalization-util.js'; | import { LearningObjectNode, 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.js'; | ||||||
|  | import { LearningPath as LearningPathEntity } from '../../entities/content/learning-path.entity.js'; | ||||||
|  | import { getLearningPathRepository } from '../../data/repositories.js'; | ||||||
|  | import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; | ||||||
|  | import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; | ||||||
|  | import { base64ToArrayBuffer } from '../../util/base64-buffer-conversion.js'; | ||||||
|  | import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; | ||||||
|  | import { mapToTeacher } from '../../interfaces/teacher.js'; | ||||||
|  | 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(dto: LearningPath, adminsDto: TeacherDTO[]): LearningPathEntity { | ||||||
|  |     const admins = adminsDto.map((admin) => mapToTeacher(admin)); | ||||||
|  |     const repo = getLearningPathRepository(); | ||||||
|  |     const path = repo.create({ | ||||||
|  |         hruid: dto.hruid, | ||||||
|  |         language: dto.language as Language, | ||||||
|  |         description: dto.description, | ||||||
|  |         title: dto.title, | ||||||
|  |         admins, | ||||||
|  |         image: dto.image ? Buffer.from(base64ToArrayBuffer(dto.image)) : null, | ||||||
|  |     }); | ||||||
|  |     const nodes = dto.nodes.map((nodeDto: LearningObjectNode, i: number) => | ||||||
|  |         repo.createNode({ | ||||||
|  |             learningPath: path, | ||||||
|  |             learningObjectHruid: nodeDto.learningobject_hruid, | ||||||
|  |             nodeNumber: i, | ||||||
|  |             language: nodeDto.language, | ||||||
|  |             version: nodeDto.version, | ||||||
|  |             startNode: nodeDto.start_node ?? false, | ||||||
|  |             createdAt: new Date(), | ||||||
|  |             updatedAt: new Date(), | ||||||
|  |         }) | ||||||
|  |     ); | ||||||
|  |     dto.nodes.forEach((nodeDto) => { | ||||||
|  |         const fromNode = nodes.find( | ||||||
|  |             (it) => it.learningObjectHruid === nodeDto.learningobject_hruid && it.language === nodeDto.language && it.version === nodeDto.version | ||||||
|  |         )!; | ||||||
|  |         const transitions = nodeDto.transitions | ||||||
|  |             .map((transDto, i) => { | ||||||
|  |                 const toNode = nodes.find( | ||||||
|  |                     (it) => | ||||||
|  |                         it.learningObjectHruid === transDto.next.hruid && | ||||||
|  |                         it.language === transDto.next.language && | ||||||
|  |                         it.version === transDto.next.version | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |                 if (toNode) { | ||||||
|  |                     return repo.createTransition({ | ||||||
|  |                         transitionNumber: i, | ||||||
|  |                         node: fromNode, | ||||||
|  |                         next: toNode, | ||||||
|  |                         condition: transDto.condition ?? 'true', | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |                 return undefined; | ||||||
|  |             }) | ||||||
|  |             .filter((it) => it) | ||||||
|  |             .map((it) => it!); | ||||||
|  | 
 | ||||||
|  |         fromNode.transitions = new Collection<LearningPathTransition>(fromNode, transitions); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     path.nodes = new Collection<LearningPathNode>(path, nodes); | ||||||
|  | 
 | ||||||
|  |     return path; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api) |  * Service providing access to data about learning paths from the appropriate data source (database or Dwengo-api) | ||||||
|  */ |  */ | ||||||
|  | @ -19,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?: PersonalizationTarget |  | ||||||
|     ): 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)); | ||||||
| 
 | 
 | ||||||
|  | @ -48,12 +108,23 @@ const learningPathService = { | ||||||
|     /** |     /** | ||||||
|      * Search learning paths in the data source using the given search string. |      * Search learning paths in the data source using the given search string. | ||||||
|      */ |      */ | ||||||
|     async searchLearningPaths(query: string, language: Language, personalizedFor?: PersonalizationTarget): Promise<LearningPath[]> { |     async searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise<LearningPath[]> { | ||||||
|         const providerResponses = await Promise.all( |         const providerResponses = await Promise.all( | ||||||
|             allProviders.map(async (provider) => provider.searchLearningPaths(query, language, personalizedFor)) |             allProviders.map(async (provider) => provider.searchLearningPaths(query, language, personalizedFor)) | ||||||
|         ); |         ); | ||||||
|         return providerResponses.flat(); |         return providerResponses.flat(); | ||||||
|     }, |     }, | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add a new learning path to the database. | ||||||
|  |      * @param dto Learning path DTO from which the learning path will be created. | ||||||
|  |      * @param admins Teachers who should become an admin of the learning path. | ||||||
|  |      */ | ||||||
|  |     async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<void> { | ||||||
|  |         const repo = getLearningPathRepository(); | ||||||
|  |         const path = mapToLearningPath(dto, admins); | ||||||
|  |         await repo.save(path, { preventOverwrite: true }); | ||||||
|  |     }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default learningPathService; | export default learningPathService; | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { getAssignmentRepository, 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'; | ||||||
|  | @ -37,6 +37,7 @@ export async function createSubmission(submissionDTO: SubmissionDTO): Promise<Su | ||||||
| 
 | 
 | ||||||
|     const submissionRepository = getSubmissionRepository(); |     const submissionRepository = getSubmissionRepository(); | ||||||
|     const submission = mapToSubmission(submissionDTO, submitter, group); |     const submission = mapToSubmission(submissionDTO, submitter, group); | ||||||
|  | 
 | ||||||
|     await submissionRepository.save(submission); |     await submissionRepository.save(submission); | ||||||
| 
 | 
 | ||||||
|     return mapToSubmissionDTO(submission); |     return mapToSubmissionDTO(submission); | ||||||
|  | @ -60,12 +61,18 @@ export async function getSubmissionsForLearningObjectAndAssignment( | ||||||
|     version: number, |     version: number, | ||||||
|     classId: string, |     classId: string, | ||||||
|     assignmentId: number, |     assignmentId: number, | ||||||
|     studentUsername?: string |     groupId?: number | ||||||
| ): Promise<SubmissionDTO[]> { | ): Promise<SubmissionDTO[]> { | ||||||
|     const loId = new LearningObjectIdentifier(learningObjectHruid, language, version); |     const loId = new LearningObjectIdentifier(learningObjectHruid, language, version); | ||||||
|     const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, assignmentId); |     const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, assignmentId); | ||||||
| 
 | 
 | ||||||
|     const submissions = await getSubmissionRepository().findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!, studentUsername); |     let submissions: Submission[]; | ||||||
|  |     if (groupId !== undefined) { | ||||||
|  |         const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, groupId); | ||||||
|  |         submissions = await getSubmissionRepository().findAllSubmissionsForLearningObjectAndGroup(loId, group!); | ||||||
|  |     } else { | ||||||
|  |         submissions = await getSubmissionRepository().findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     return submissions.map((s) => mapToSubmissionDTO(s)); |     return submissions.map((s) => mapToSubmissionDTO(s)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -27,7 +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 (property.primary && property.autoincrement && !(args.entity as Record<string, unknown>)[property.name]) { |             if (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; | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								backend/src/util/base64-buffer-conversion.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/util/base64-buffer-conversion.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | /** | ||||||
|  |  * Convert a Base64-encoded string into a buffer with the same data. | ||||||
|  |  * @param base64 The Base64 encoded string. | ||||||
|  |  */ | ||||||
|  | export function base64ToArrayBuffer(base64: string): ArrayBuffer { | ||||||
|  |     const binaryString = atob(base64); | ||||||
|  |     const bytes = new Uint8Array(binaryString.length); | ||||||
|  |     for (let i = 0; i < binaryString.length; i++) { | ||||||
|  |         bytes[i] = binaryString.charCodeAt(i); | ||||||
|  |     } | ||||||
|  |     return bytes.buffer; | ||||||
|  | } | ||||||
|  | @ -91,9 +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 user's group", async () => { |     it('should find only the submissions for a certain learning object and assignment made for the given group', async () => { | ||||||
|         const result = await submissionRepository.findAllSubmissionsForLearningObjectAndAssignment(loId, assignment!, 'Tool'); |         const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 2); | ||||||
|         // (student Tool is in group #2)
 |         const result = await submissionRepository.findAllSubmissionsForLearningObjectAndGroup(loId, group!); | ||||||
| 
 | 
 | ||||||
|         expect(result).toHaveLength(1); |         expect(result).toHaveLength(1); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,66 +2,57 @@ import { beforeAll, describe, expect, it } from 'vitest'; | ||||||
| import { setupTestApp } from '../../setup-tests.js'; | import { setupTestApp } from '../../setup-tests.js'; | ||||||
| import { getAttachmentRepository, getLearningObjectRepository } from '../../../src/data/repositories.js'; | import { getAttachmentRepository, getLearningObjectRepository } from '../../../src/data/repositories.js'; | ||||||
| import { AttachmentRepository } from '../../../src/data/content/attachment-repository.js'; | import { AttachmentRepository } from '../../../src/data/content/attachment-repository.js'; | ||||||
| import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository.js'; |  | ||||||
| import example from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example.js'; |  | ||||||
| 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'; | ||||||
| const NEWER_TEST_SUFFIX = 'nEweR'; | import { v4 as uuidV4 } from 'uuid'; | ||||||
| 
 |  | ||||||
| async function createTestLearningObjects(learningObjectRepo: LearningObjectRepository): Promise<{ |  | ||||||
|     older: LearningObject; |  | ||||||
|     newer: LearningObject; |  | ||||||
| }> { |  | ||||||
|     const olderExample = example.createLearningObject(); |  | ||||||
|     await learningObjectRepo.save(olderExample); |  | ||||||
| 
 |  | ||||||
|     const newerExample = example.createLearningObject(); |  | ||||||
|     newerExample.title = 'Newer example'; |  | ||||||
|     newerExample.version = 100; |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|         older: olderExample, |  | ||||||
|         newer: newerExample, |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| describe('AttachmentRepository', () => { | describe('AttachmentRepository', () => { | ||||||
|     let attachmentRepo: AttachmentRepository; |     let attachmentRepo: AttachmentRepository; | ||||||
|     let exampleLearningObjects: { older: LearningObject; newer: LearningObject }; |     let newLearningObject: LearningObject; | ||||||
|     let attachmentsOlderLearningObject: Attachment[]; |     let attachmentsOlderLearningObject: Attachment[]; | ||||||
| 
 | 
 | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|         await setupTestApp(); |         await setupTestApp(); | ||||||
|  | 
 | ||||||
|  |         attachmentsOlderLearningObject = testLearningObjectPnNotebooks.attachments as Attachment[]; | ||||||
|  | 
 | ||||||
|         attachmentRepo = getAttachmentRepository(); |         attachmentRepo = getAttachmentRepository(); | ||||||
|         exampleLearningObjects = await createTestLearningObjects(getLearningObjectRepository()); |         const learningObjectRepo = getLearningObjectRepository(); | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     it('can add attachments to learning objects without throwing an error', async () => { |         const newLearningObjectData = structuredClone(testLearningObjectPnNotebooks); | ||||||
|         attachmentsOlderLearningObject = Object.values(example.createAttachment).map((fn) => fn(exampleLearningObjects.older)); |         newLearningObjectData.title = 'Newer example'; | ||||||
|  |         newLearningObjectData.version = 101; | ||||||
|  |         newLearningObjectData.attachments = []; | ||||||
|  |         newLearningObjectData.uuid = uuidV4(); | ||||||
|  |         newLearningObjectData.content = Buffer.from('Content of the newer example'); | ||||||
| 
 | 
 | ||||||
|         await Promise.all(attachmentsOlderLearningObject.map(async (attachment) => attachmentRepo.save(attachment))); |         newLearningObject = learningObjectRepo.create(newLearningObjectData); | ||||||
|  |         await learningObjectRepo.save(newLearningObject); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     let attachmentOnlyNewer: Attachment; |     let attachmentOnlyNewer: Attachment; | ||||||
|     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 = Object.values(example.createAttachment)[0](exampleLearningObjects.newer); |         attachmentOnlyNewer = structuredClone(attachmentsOlderLearningObject[0]); | ||||||
|         attachmentOnlyNewer.content.write(NEWER_TEST_SUFFIX); |         attachmentOnlyNewer.learningObject = newLearningObject; | ||||||
|  |         attachmentOnlyNewer.content = Buffer.from('New attachment content'); | ||||||
| 
 | 
 | ||||||
|         await attachmentRepo.save(attachmentOnlyNewer); |         await attachmentRepo.save(attachmentRepo.create(attachmentOnlyNewer)); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     let olderLearningObjectId: LearningObjectIdentifier; |     let olderLearningObjectId: LearningObjectIdentifier; | ||||||
|     it('returns the correct attachment when queried by learningObjectId and attachment name', async () => { |     it('returns the correct attachment when queried by learningObjectId and attachment name', async () => { | ||||||
|         olderLearningObjectId = { |         olderLearningObjectId = { | ||||||
|             hruid: exampleLearningObjects.older.hruid, |             hruid: testLearningObjectPnNotebooks.hruid, | ||||||
|             language: exampleLearningObjects.older.language, |             language: testLearningObjectPnNotebooks.language, | ||||||
|             version: exampleLearningObjects.older.version, |             version: testLearningObjectPnNotebooks.version, | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         const result = await attachmentRepo.findByLearningObjectIdAndName(olderLearningObjectId, attachmentsOlderLearningObject[0].name); |         const result = await attachmentRepo.findByLearningObjectIdAndName(olderLearningObjectId, attachmentsOlderLearningObject[0].name); | ||||||
|         expect(result).toBe(attachmentsOlderLearningObject[0]); |         expect(result).not.toBeNull(); | ||||||
|  |         expect(result!.name).toEqual(attachmentsOlderLearningObject[0].name); | ||||||
|  |         expect(result!.content).toEqual(attachmentsOlderLearningObject[0].content); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('returns null when queried by learningObjectId and non-existing attachment name', async () => { |     it('returns null when queried by learningObjectId and non-existing attachment name', async () => { | ||||||
|  | @ -71,10 +62,12 @@ describe('AttachmentRepository', () => { | ||||||
| 
 | 
 | ||||||
|     it('returns the newer version of the attachment when only queried by hruid, language and attachment name (but not version)', async () => { |     it('returns the newer version of the attachment when only queried by hruid, language and attachment name (but not version)', async () => { | ||||||
|         const result = await attachmentRepo.findByMostRecentVersionOfLearningObjectAndName( |         const result = await attachmentRepo.findByMostRecentVersionOfLearningObjectAndName( | ||||||
|             exampleLearningObjects.older.hruid, |             testLearningObjectPnNotebooks.hruid, | ||||||
|             exampleLearningObjects.older.language, |             testLearningObjectPnNotebooks.language, | ||||||
|             attachmentOnlyNewer.name |             attachmentOnlyNewer.name | ||||||
|         ); |         ); | ||||||
|         expect(result).toBe(attachmentOnlyNewer); |         expect(result).not.toBeNull(); | ||||||
|  |         expect(result!.name).toEqual(attachmentOnlyNewer.name); | ||||||
|  |         expect(result!.content).toEqual(attachmentOnlyNewer.content); | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,28 +1,21 @@ | ||||||
| import { beforeAll, describe, expect, it } from 'vitest'; | import { beforeAll, describe, expect, it } from 'vitest'; | ||||||
| import { setupTestApp } from '../../setup-tests.js'; | import { setupTestApp } from '../../setup-tests.js'; | ||||||
| import { getAttachmentRepository, getLearningObjectRepository } 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 { LearningObjectRepository } from '../../../src/data/content/learning-object-repository.js'; | import { testLearningObject02 } from '../../test_assets/content/learning-objects.testdata'; | ||||||
| import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier.js'; |  | ||||||
| import { Language } from '@dwengo-1/common/util/language'; |  | ||||||
| 
 | 
 | ||||||
| describe('AttachmentRepository', () => { | describe('AttachmentRepository', () => { | ||||||
|     let attachmentRepository: AttachmentRepository; |     let attachmentRepository: AttachmentRepository; | ||||||
|     let learningObjectRepository: LearningObjectRepository; |  | ||||||
| 
 | 
 | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|         await setupTestApp(); |         await setupTestApp(); | ||||||
|         attachmentRepository = getAttachmentRepository(); |         attachmentRepository = getAttachmentRepository(); | ||||||
|         learningObjectRepository = getLearningObjectRepository(); |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should return the requested attachment', async () => { |     it('should return the requested attachment', async () => { | ||||||
|         const id = new LearningObjectIdentifier('id02', Language.English, 1); |  | ||||||
|         const learningObject = await learningObjectRepository.findByIdentifier(id); |  | ||||||
| 
 |  | ||||||
|         const attachment = await attachmentRepository.findByMostRecentVersionOfLearningObjectAndName( |         const attachment = await attachmentRepository.findByMostRecentVersionOfLearningObjectAndName( | ||||||
|             learningObject!.hruid, |             testLearningObject02.hruid, | ||||||
|             Language.English, |             testLearningObject02.language, | ||||||
|             'attachment01' |             'attachment01' | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,48 +2,33 @@ import { beforeAll, describe, it, expect } from 'vitest'; | ||||||
| import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository.js'; | import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository.js'; | ||||||
| import { setupTestApp } from '../../setup-tests.js'; | import { setupTestApp } from '../../setup-tests.js'; | ||||||
| import { getLearningObjectRepository } from '../../../src/data/repositories.js'; | import { getLearningObjectRepository } from '../../../src/data/repositories.js'; | ||||||
| import example from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example.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 { testLearningObject01, testLearningObject02, testLearningObject03 } from '../../test_assets/content/learning-objects.testdata'; | ||||||
|  | import { v4 } from 'uuid'; | ||||||
| 
 | 
 | ||||||
| describe('LearningObjectRepository', () => { | describe('LearningObjectRepository', () => { | ||||||
|     let learningObjectRepository: LearningObjectRepository; |     let learningObjectRepository: LearningObjectRepository; | ||||||
| 
 | 
 | ||||||
|     let exampleLearningObject: LearningObject; |  | ||||||
| 
 |  | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|         await setupTestApp(); |         await setupTestApp(); | ||||||
|         learningObjectRepository = getLearningObjectRepository(); |         learningObjectRepository = getLearningObjectRepository(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should be able to add a learning object to it without an error', async () => { |     it('should return a learning object when queried by id', async () => { | ||||||
|         exampleLearningObject = example.createLearningObject(); |  | ||||||
|         await learningObjectRepository.insert(exampleLearningObject); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('should return the learning object when queried by id', async () => { |  | ||||||
|         const result = await learningObjectRepository.findByIdentifier({ |         const result = await learningObjectRepository.findByIdentifier({ | ||||||
|             hruid: exampleLearningObject.hruid, |             hruid: testLearningObject01.hruid, | ||||||
|             language: exampleLearningObject.language, |             language: testLearningObject02.language, | ||||||
|             version: exampleLearningObject.version, |             version: testLearningObject03.version, | ||||||
|         }); |         }); | ||||||
|         expect(result).toBeInstanceOf(LearningObject); |         expect(result).toBeInstanceOf(LearningObject); | ||||||
|         expectToBeCorrectEntity( |         expectToBeCorrectEntity(result!, testLearningObject01); | ||||||
|             { |  | ||||||
|                 name: 'actual', |  | ||||||
|                 entity: result!, |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 name: 'expected', |  | ||||||
|                 entity: exampleLearningObject, |  | ||||||
|             } |  | ||||||
|         ); |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should return null when non-existing version is queried', async () => { |     it('should return null when non-existing version is queried', async () => { | ||||||
|         const result = await learningObjectRepository.findByIdentifier({ |         const result = await learningObjectRepository.findByIdentifier({ | ||||||
|             hruid: exampleLearningObject.hruid, |             hruid: testLearningObject01.hruid, | ||||||
|             language: exampleLearningObject.language, |             language: testLearningObject01.language, | ||||||
|             version: 100, |             version: 100, | ||||||
|         }); |         }); | ||||||
|         expect(result).toBe(null); |         expect(result).toBe(null); | ||||||
|  | @ -52,9 +37,12 @@ describe('LearningObjectRepository', () => { | ||||||
|     let newerExample: LearningObject; |     let newerExample: LearningObject; | ||||||
| 
 | 
 | ||||||
|     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 () => { | ||||||
|         newerExample = example.createLearningObject(); |         const testLearningObject01Newer = structuredClone(testLearningObject01); | ||||||
|         newerExample.version = 10; |         testLearningObject01Newer.version = 10; | ||||||
|         newerExample.title += ' (nieuw)'; |         testLearningObject01Newer.title += ' (nieuw)'; | ||||||
|  |         testLearningObject01Newer.uuid = v4(); | ||||||
|  |         testLearningObject01Newer.content = Buffer.from('This is the new content.'); | ||||||
|  |         newerExample = learningObjectRepository.create(testLearningObject01Newer); | ||||||
|         await learningObjectRepository.save(newerExample); |         await learningObjectRepository.save(newerExample); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  | @ -66,7 +54,7 @@ describe('LearningObjectRepository', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     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('something_that_does_not_exist', exampleLearningObject.language); |         const result = await learningObjectRepository.findLatestByHruidAndLanguage('something_that_does_not_exist', testLearningObject01.language); | ||||||
|         expect(result).toBe(null); |         expect(result).toBe(null); | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -4,6 +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'; | ||||||
| 
 | 
 | ||||||
| describe('LearningObjectRepository', () => { | describe('LearningObjectRepository', () => { | ||||||
|     let learningObjectRepository: LearningObjectRepository; |     let learningObjectRepository: LearningObjectRepository; | ||||||
|  | @ -13,8 +14,8 @@ describe('LearningObjectRepository', () => { | ||||||
|         learningObjectRepository = getLearningObjectRepository(); |         learningObjectRepository = getLearningObjectRepository(); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const id01 = new LearningObjectIdentifier('id01', Language.English, 1); |     const id01 = new LearningObjectIdentifier(testLearningObject01.hruid, testLearningObject01.language, testLearningObject01.version); | ||||||
|     const id02 = new LearningObjectIdentifier('test_id', Language.English, 1); |     const id02 = new LearningObjectIdentifier('non_existing_id', Language.English, 1); | ||||||
| 
 | 
 | ||||||
|     it('should return the learning object that matches identifier 1', async () => { |     it('should return the learning object that matches identifier 1', async () => { | ||||||
|         const learningObject = await learningObjectRepository.findByIdentifier(id01); |         const learningObject = await learningObjectRepository.findByIdentifier(id01); | ||||||
|  |  | ||||||
|  | @ -2,41 +2,27 @@ import { beforeAll, describe, expect, it } from 'vitest'; | ||||||
| import { setupTestApp } from '../../setup-tests.js'; | 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 example from '../../test-assets/learning-paths/pn-werking-example.js'; |  | ||||||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity.js'; | import { LearningPath } from '../../../src/entities/content/learning-path.entity.js'; | ||||||
| import { expectToBeCorrectEntity } from '../../test-utils/expectations.js'; | import { 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'; | ||||||
| function expectToHaveFoundPrecisely(expected: LearningPath, result: LearningPath[]): void { | import { mapToLearningPath } from '../../../src/services/learning-paths/learning-path-service'; | ||||||
|     expect(result).toHaveProperty('length'); |  | ||||||
|     expect(result.length).toBe(1); |  | ||||||
|     expectToBeCorrectEntity({ entity: result[0] }, { entity: expected }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function expectToHaveFoundNothing(result: LearningPath[]): void { |  | ||||||
|     expect(result).toHaveProperty('length'); |  | ||||||
|     expect(result.length).toBe(0); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| describe('LearningPathRepository', () => { | describe('LearningPathRepository', () => { | ||||||
|     let learningPathRepo: LearningPathRepository; |     let learningPathRepo: LearningPathRepository; | ||||||
|  |     let examplePath: LearningPath; | ||||||
| 
 | 
 | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|         await setupTestApp(); |         await setupTestApp(); | ||||||
|         learningPathRepo = getLearningPathRepository(); |         learningPathRepo = getLearningPathRepository(); | ||||||
|  | 
 | ||||||
|  |         examplePath = mapToLearningPath(testLearningPath01, []); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     let examplePath: LearningPath; |     it('should return a learning path when it is queried by hruid and language', async () => { | ||||||
| 
 |         const result = await learningPathRepo.findByHruidAndLanguage(testLearningPath01.hruid, testLearningPath01.language as Language); | ||||||
|     it('should be able to add a learning path without throwing an error', async () => { |  | ||||||
|         examplePath = example.createLearningPath(); |  | ||||||
|         await learningPathRepo.insert(examplePath); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('should return the added path when it is queried by hruid and language', async () => { |  | ||||||
|         const result = await learningPathRepo.findByHruidAndLanguage(examplePath.hruid, examplePath.language); |  | ||||||
|         expect(result).toBeInstanceOf(LearningPath); |         expect(result).toBeInstanceOf(LearningPath); | ||||||
|         expectToBeCorrectEntity({ entity: result! }, { entity: examplePath }); |         expectToBeCorrectEntity(result!, examplePath); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should return null to a query on a non-existing hruid or language', async () => { |     it('should return null to a query on a non-existing hruid or language', async () => { | ||||||
|  | @ -45,7 +31,7 @@ describe('LearningPathRepository', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should return the learning path when we search for a search term occurring in its title', async () => { |     it('should return the learning path when we search for a search term occurring in its title', async () => { | ||||||
|         const result = await learningPathRepo.findByQueryStringAndLanguage(examplePath.title.slice(4, 9), examplePath.language); |         const result = await learningPathRepo.findByQueryStringAndLanguage(examplePath.title.slice(9, 13), examplePath.language); | ||||||
|         expectToHaveFoundPrecisely(examplePath, result); |         expectToHaveFoundPrecisely(examplePath, result); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,6 +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'; | ||||||
| 
 | 
 | ||||||
| describe('LearningPathRepository', () => { | describe('LearningPathRepository', () => { | ||||||
|     let learningPathRepository: LearningPathRepository; |     let learningPathRepository: LearningPathRepository; | ||||||
|  | @ -19,10 +20,10 @@ describe('LearningPathRepository', () => { | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('should return requested learning path', async () => { |     it('should return requested learning path', async () => { | ||||||
|         const learningPath = await learningPathRepository.findByHruidAndLanguage('id01', Language.English); |         const learningPath = await learningPathRepository.findByHruidAndLanguage(testLearningPath01.hruid, testLearningPath01.language as Language); | ||||||
| 
 | 
 | ||||||
|         expect(learningPath).toBeTruthy(); |         expect(learningPath).toBeTruthy(); | ||||||
|         expect(learningPath?.title).toBe('repertoire Tool'); |         expect(learningPath?.title).toBe(testLearningPath01.title); | ||||||
|         expect(learningPath?.description).toBe('all about Tool'); |         expect(learningPath?.description).toBe(testLearningPath01.description); | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,37 +1,32 @@ | ||||||
| import { beforeAll, describe, expect, it } from 'vitest'; | import { beforeAll, describe, expect, it } from 'vitest'; | ||||||
| import { setupTestApp } from '../../setup-tests'; | import { setupTestApp } from '../../setup-tests'; | ||||||
| import { getLearningObjectRepository, getLearningPathRepository } from '../../../src/data/repositories'; |  | ||||||
| import example from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example'; |  | ||||||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity'; | import { LearningObject } from '../../../src/entities/content/learning-object.entity'; | ||||||
| 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 learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example'; | import { FilteredLearningObject, LearningObjectNode, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| import learningPathExample from '../../test-assets/learning-paths/pn-werking-example'; | import { testPartiallyDatabaseAndPartiallyDwengoApiLearningPath } from '../../test_assets/content/learning-paths.testdata'; | ||||||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity'; | import { testLearningObjectPnNotebooks } from '../../test_assets/content/learning-objects.testdata'; | ||||||
| import { FilteredLearningObject } from '@dwengo-1/common/interfaces/learning-content'; | import { LearningPath } from '@dwengo-1/common/dist/interfaces/learning-content'; | ||||||
| 
 | import { RequiredEntityData } from '@mikro-orm/core'; | ||||||
| async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { | import { getHtmlRenderingForTestLearningObject } from '../../test-utils/get-html-rendering'; | ||||||
|     const learningObjectRepo = getLearningObjectRepository(); |  | ||||||
|     const learningPathRepo = getLearningPathRepository(); |  | ||||||
|     const learningObject = learningObjectExample.createLearningObject(); |  | ||||||
|     const learningPath = learningPathExample.createLearningPath(); |  | ||||||
|     await learningObjectRepo.save(learningObject); |  | ||||||
|     await learningPathRepo.save(learningPath); |  | ||||||
|     return { learningObject, learningPath }; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| const EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT = 'Notebook opslaan'; | const EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT = 'Notebook opslaan'; | ||||||
| 
 | 
 | ||||||
| describe('DatabaseLearningObjectProvider', () => { | describe('DatabaseLearningObjectProvider', () => { | ||||||
|     let exampleLearningObject: LearningObject; |     let exampleLearningObject: RequiredEntityData<LearningObject>; | ||||||
|     let exampleLearningPath: LearningPath; |     let exampleLearningPath: LearningPath; | ||||||
|  |     let exampleLearningPathId: LearningPathIdentifier; | ||||||
| 
 | 
 | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|         await setupTestApp(); |         await setupTestApp(); | ||||||
|         const exampleData = await initExampleData(); |         exampleLearningObject = testLearningObjectPnNotebooks; | ||||||
|         exampleLearningObject = exampleData.learningObject; |         exampleLearningPath = testPartiallyDatabaseAndPartiallyDwengoApiLearningPath; | ||||||
|         exampleLearningPath = exampleData.learningPath; | 
 | ||||||
|  |         exampleLearningPathId = { | ||||||
|  |             hruid: exampleLearningPath.hruid, | ||||||
|  |             language: exampleLearningPath.language as Language, | ||||||
|  |         }; | ||||||
|     }); |     }); | ||||||
|     describe('getLearningObjectById', () => { |     describe('getLearningObjectById', () => { | ||||||
|         it('should return the learning object when it is queried by its id', async () => { |         it('should return the learning object when it is queried by its id', async () => { | ||||||
|  | @ -61,7 +56,7 @@ describe('DatabaseLearningObjectProvider', () => { | ||||||
|         it('should return the correct rendering of the learning object', async () => { |         it('should return the correct rendering of the learning object', async () => { | ||||||
|             const result = await databaseLearningObjectProvider.getLearningObjectHTML(exampleLearningObject); |             const result = await databaseLearningObjectProvider.getLearningObjectHTML(exampleLearningObject); | ||||||
|             // Set newlines so your tests are platform-independent.
 |             // Set newlines so your tests are platform-independent.
 | ||||||
|             expect(result).toEqual(example.getHTMLRendering().replace(/\r\n/g, '\n')); |             expect(result).toEqual(getHtmlRenderingForTestLearningObject(exampleLearningObject).replace(/\r\n/g, '\n')); | ||||||
|         }); |         }); | ||||||
|         it('should return null for a non-existing learning object', async () => { |         it('should return null for a non-existing learning object', async () => { | ||||||
|             const result = await databaseLearningObjectProvider.getLearningObjectHTML({ |             const result = await databaseLearningObjectProvider.getLearningObjectHTML({ | ||||||
|  | @ -73,8 +68,8 @@ 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(exampleLearningPath); |             const result = await databaseLearningObjectProvider.getLearningObjectIdsFromPath(exampleLearningPathId); | ||||||
|             expect(new Set(result)).toEqual(new Set(exampleLearningPath.nodes.map((it) => it.learningObjectHruid))); |             expect(new Set(result)).toEqual(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( | ||||||
|  | @ -89,9 +84,11 @@ describe('DatabaseLearningObjectProvider', () => { | ||||||
|     }); |     }); | ||||||
|     describe('getLearningObjectsFromPath', () => { |     describe('getLearningObjectsFromPath', () => { | ||||||
|         it('should correctly return all learning objects which are on the path, even those who are not in the database', async () => { |         it('should correctly return all learning objects which are on the path, even those who are not in the database', async () => { | ||||||
|             const result = await databaseLearningObjectProvider.getLearningObjectsFromPath(exampleLearningPath); |             const result = await databaseLearningObjectProvider.getLearningObjectsFromPath(exampleLearningPathId); | ||||||
|             expect(result.length).toBe(exampleLearningPath.nodes.length); |             expect(result.length).toBe(exampleLearningPath.nodes.length); | ||||||
|             expect(new Set(result.map((it) => it.key))).toEqual(new Set(exampleLearningPath.nodes.map((it) => it.learningObjectHruid))); |             expect(new Set(result.map((it) => it.key))).toEqual( | ||||||
|  |                 new Set(exampleLearningPath.nodes.map((it: LearningObjectNode) => it.learningobject_hruid)) | ||||||
|  |             ); | ||||||
| 
 | 
 | ||||||
|             expect(result.map((it) => it.title)).toContainEqual(EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT); |             expect(result.map((it) => it.title)).toContainEqual(EXPECTED_TITLE_FROM_DWENGO_LEARNING_OBJECT); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  | @ -1,14 +1,14 @@ | ||||||
| import { beforeAll, describe, expect, it } from 'vitest'; | import { beforeAll, describe, expect, it } from 'vitest'; | ||||||
| import { setupTestApp } from '../../setup-tests'; | import { setupTestApp } from '../../setup-tests'; | ||||||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity'; | import { LearningObject } from '../../../src/entities/content/learning-object.entity'; | ||||||
| import { getLearningObjectRepository, getLearningPathRepository } from '../../../src/data/repositories'; |  | ||||||
| import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example'; |  | ||||||
| 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 { LearningPath } from '../../../src/entities/content/learning-path.entity'; | import { LearningObjectIdentifierDTO, LearningPath as LearningPathDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
| import learningPathExample from '../../test-assets/learning-paths/pn-werking-example'; |  | ||||||
| import { LearningObjectIdentifierDTO, 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 { testPartiallyDatabaseAndPartiallyDwengoApiLearningPath } from '../../test_assets/content/learning-paths.testdata'; | ||||||
|  | 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 = { | ||||||
|  | @ -23,25 +23,20 @@ const DWENGO_TEST_LEARNING_PATH_ID: LearningPathIdentifier = { | ||||||
| }; | }; | ||||||
| const DWENGO_TEST_LEARNING_PATH_HRUIDS = new Set(['pn_werkingnotebooks', 'pn_werkingnotebooks2', 'pn_werkingnotebooks3']); | const DWENGO_TEST_LEARNING_PATH_HRUIDS = new Set(['pn_werkingnotebooks', 'pn_werkingnotebooks2', 'pn_werkingnotebooks3']); | ||||||
| 
 | 
 | ||||||
| async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { |  | ||||||
|     const learningObjectRepo = getLearningObjectRepository(); |  | ||||||
|     const learningPathRepo = getLearningPathRepository(); |  | ||||||
|     const learningObject = learningObjectExample.createLearningObject(); |  | ||||||
|     const learningPath = learningPathExample.createLearningPath(); |  | ||||||
|     await learningObjectRepo.save(learningObject); |  | ||||||
|     await learningPathRepo.save(learningPath); |  | ||||||
|     return { learningObject, learningPath }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| describe('LearningObjectService', () => { | describe('LearningObjectService', () => { | ||||||
|     let exampleLearningObject: LearningObject; |     let exampleLearningObject: RequiredEntityData<LearningObject>; | ||||||
|     let exampleLearningPath: LearningPath; |     let exampleLearningPath: LearningPathDTO; | ||||||
|  |     let exampleLearningPathId: LearningPathIdentifier; | ||||||
| 
 | 
 | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|         await setupTestApp(); |         await setupTestApp(); | ||||||
|         const exampleData = await initExampleData(); |         exampleLearningObject = testLearningObjectPnNotebooks; | ||||||
|         exampleLearningObject = exampleData.learningObject; |         exampleLearningPath = testPartiallyDatabaseAndPartiallyDwengoApiLearningPath; | ||||||
|         exampleLearningPath = exampleData.learningPath; | 
 | ||||||
|  |         exampleLearningPathId = { | ||||||
|  |             hruid: exampleLearningPath.hruid, | ||||||
|  |             language: exampleLearningPath.language as Language, | ||||||
|  |         }; | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     describe('getLearningObjectById', () => { |     describe('getLearningObjectById', () => { | ||||||
|  | @ -69,7 +64,7 @@ describe('LearningObjectService', () => { | ||||||
|             const result = await learningObjectService.getLearningObjectHTML(exampleLearningObject); |             const result = await learningObjectService.getLearningObjectHTML(exampleLearningObject); | ||||||
|             expect(result).not.toBeNull(); |             expect(result).not.toBeNull(); | ||||||
|             // Set newlines so your tests are platform-independent.
 |             // Set newlines so your tests are platform-independent.
 | ||||||
|             expect(result).toEqual(learningObjectExample.getHTMLRendering().replace(/\r\n/g, '\n')); |             expect(result).toEqual(getHtmlRenderingForTestLearningObject(exampleLearningObject).replace(/\r\n/g, '\n')); | ||||||
|         }); |         }); | ||||||
|         it( |         it( | ||||||
|             'returns the same HTML as the Dwengo API when queried with the identifier of a learning object that does ' + |             'returns the same HTML as the Dwengo API when queried with the identifier of a learning object that does ' + | ||||||
|  | @ -97,8 +92,8 @@ 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(exampleLearningPath); |             const result = await learningObjectService.getLearningObjectsFromPath(exampleLearningPathId); | ||||||
|             expect(result.map((it) => it.key)).toEqual(exampleLearningPath.nodes.map((it) => it.learningObjectHruid)); |             expect(result.map((it) => it.key)).toEqual(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); | ||||||
|  | @ -115,8 +110,8 @@ 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(exampleLearningPath); |             const result = await learningObjectService.getLearningObjectIdsFromPath(exampleLearningPathId); | ||||||
|             expect(result).toEqual(exampleLearningPath.nodes.map((it) => it.learningObjectHruid)); |             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); | ||||||
|  |  | ||||||
|  | @ -1,26 +1,35 @@ | ||||||
| import { describe, expect, it } from 'vitest'; | import { beforeAll, describe, expect, it } from 'vitest'; | ||||||
| import mdExample from '../../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example'; |  | ||||||
| import multipleChoiceExample from '../../../test-assets/learning-objects/test-multiple-choice/test-multiple-choice-example'; |  | ||||||
| import essayExample from '../../../test-assets/learning-objects/test-essay/test-essay-example'; |  | ||||||
| import processingService from '../../../../src/services/learning-objects/processing/processing-service'; | import processingService from '../../../../src/services/learning-objects/processing/processing-service'; | ||||||
|  | import { | ||||||
|  |     testLearningObjectEssayQuestion, | ||||||
|  |     testLearningObjectMultipleChoice, | ||||||
|  |     testLearningObjectPnNotebooks, | ||||||
|  | } from '../../../test_assets/content/learning-objects.testdata'; | ||||||
|  | import { getHtmlRenderingForTestLearningObject } from '../../../test-utils/get-html-rendering'; | ||||||
|  | import { getLearningObjectRepository } from '../../../../src/data/repositories'; | ||||||
|  | import { setupTestApp } from '../../../setup-tests'; | ||||||
| 
 | 
 | ||||||
| describe('ProcessingService', () => { | describe('ProcessingService', () => { | ||||||
|  |     beforeAll(async () => { | ||||||
|  |         await setupTestApp(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     it('renders a markdown learning object correctly', async () => { |     it('renders a markdown learning object correctly', async () => { | ||||||
|         const markdownLearningObject = mdExample.createLearningObject(); |         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(mdExample.getHTMLRendering().replace(/\r\n/g, '\n')); |         expect(result).toEqual(getHtmlRenderingForTestLearningObject(markdownLearningObject).replace(/\r\n/g, '\n')); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('renders a multiple choice question correctly', async () => { |     it('renders a multiple choice question correctly', async () => { | ||||||
|         const multipleChoiceLearningObject = multipleChoiceExample.createLearningObject(); |         const testLearningObject = getLearningObjectRepository().create(testLearningObjectMultipleChoice); | ||||||
|         const result = await processingService.render(multipleChoiceLearningObject); |         const result = await processingService.render(testLearningObject); | ||||||
|         expect(result).toEqual(multipleChoiceExample.getHTMLRendering().replace(/\r\n/g, '\n')); |         expect(result).toEqual(getHtmlRenderingForTestLearningObject(testLearningObjectMultipleChoice).replace(/\r\n/g, '\n')); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('renders an essay question correctly', async () => { |     it('renders an essay question correctly', async () => { | ||||||
|         const essayLearningObject = essayExample.createLearningObject(); |         const essayLearningObject = getLearningObjectRepository().create(testLearningObjectEssayQuestion); | ||||||
|         const result = await processingService.render(essayLearningObject); |         const result = await processingService.render(essayLearningObject); | ||||||
|         expect(result).toEqual(essayExample.getHTMLRendering().replace(/\r\n/g, '\n')); |         expect(result).toEqual(getHtmlRenderingForTestLearningObject(essayLearningObject).replace(/\r\n/g, '\n')); | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -2,227 +2,112 @@ import { beforeAll, describe, expect, it } from 'vitest'; | ||||||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity.js'; | import { LearningObject } from '../../../src/entities/content/learning-object.entity.js'; | ||||||
| import { setupTestApp } from '../../setup-tests.js'; | import { setupTestApp } from '../../setup-tests.js'; | ||||||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity.js'; | import { LearningPath } from '../../../src/entities/content/learning-path.entity.js'; | ||||||
| import { | import { getSubmissionRepository } from '../../../src/data/repositories.js'; | ||||||
|     getAssignmentRepository, | 
 | ||||||
|     getClassRepository, |  | ||||||
|     getGroupRepository, |  | ||||||
|     getLearningObjectRepository, |  | ||||||
|     getLearningPathRepository, |  | ||||||
|     getStudentRepository, |  | ||||||
|     getSubmissionRepository, |  | ||||||
| } from '../../../src/data/repositories.js'; |  | ||||||
| import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example.js'; |  | ||||||
| import learningPathExample from '../../test-assets/learning-paths/pn-werking-example.js'; |  | ||||||
| import databaseLearningPathProvider from '../../../src/services/learning-paths/database-learning-path-provider.js'; | import databaseLearningPathProvider from '../../../src/services/learning-paths/database-learning-path-provider.js'; | ||||||
| import { expectToBeCorrectLearningPath } from '../../test-utils/expectations.js'; | import { expectToBeCorrectLearningPath } from '../../test-utils/expectations.js'; | ||||||
| import learningObjectService from '../../../src/services/learning-objects/learning-object-service.js'; |  | ||||||
| import { Language } from '@dwengo-1/common/util/language'; | import { Language } from '@dwengo-1/common/util/language'; | ||||||
| import { |  | ||||||
|     ConditionTestLearningPathAndLearningObjects, |  | ||||||
|     createConditionTestLearningPathAndLearningObjects, |  | ||||||
| } from '../../test-assets/learning-paths/test-conditions-example.js'; |  | ||||||
| import { Student } from '../../../src/entities/users/student.entity.js'; |  | ||||||
| 
 | 
 | ||||||
| import { LearningObjectNode, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; | import { LearningObjectNode, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
|  | import { | ||||||
|  |     testLearningObject01, | ||||||
|  |     testLearningObjectEssayQuestion, | ||||||
|  |     testLearningObjectMultipleChoice, | ||||||
|  | } from '../../test_assets/content/learning-objects.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 { RequiredEntityData } from '@mikro-orm/core'; | ||||||
| 
 | 
 | ||||||
| const STUDENT_A_USERNAME = 'student_a'; | function expectBranchingObjectNode(result: LearningPathResponse): LearningObjectNode { | ||||||
| const STUDENT_B_USERNAME = 'student_b'; |     const branchingObjectMatches = result.data![0].nodes.filter((it) => it.learningobject_hruid === testLearningObjectMultipleChoice.hruid); | ||||||
| const CLASS_NAME = 'test_class'; |  | ||||||
| 
 |  | ||||||
| async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { |  | ||||||
|     const learningObjectRepo = getLearningObjectRepository(); |  | ||||||
|     const learningPathRepo = getLearningPathRepository(); |  | ||||||
|     const learningObject = learningObjectExample.createLearningObject(); |  | ||||||
|     const learningPath = learningPathExample.createLearningPath(); |  | ||||||
|     await learningObjectRepo.save(learningObject); |  | ||||||
|     await learningPathRepo.save(learningPath); |  | ||||||
|     return { learningObject, learningPath }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| async function initPersonalizationTestData(): Promise<{ |  | ||||||
|     learningContent: ConditionTestLearningPathAndLearningObjects; |  | ||||||
|     studentA: Student; |  | ||||||
|     studentB: Student; |  | ||||||
| }> { |  | ||||||
|     const studentRepo = getStudentRepository(); |  | ||||||
|     const classRepo = getClassRepository(); |  | ||||||
|     const assignmentRepo = getAssignmentRepository(); |  | ||||||
|     const groupRepo = getGroupRepository(); |  | ||||||
|     const submissionRepo = getSubmissionRepository(); |  | ||||||
|     const learningPathRepo = getLearningPathRepository(); |  | ||||||
|     const learningObjectRepo = getLearningObjectRepository(); |  | ||||||
|     const learningContent = createConditionTestLearningPathAndLearningObjects(); |  | ||||||
|     await learningObjectRepo.save(learningContent.branchingObject); |  | ||||||
|     await learningObjectRepo.save(learningContent.finalObject); |  | ||||||
|     await learningObjectRepo.save(learningContent.extraExerciseObject); |  | ||||||
|     await learningPathRepo.save(learningContent.learningPath); |  | ||||||
| 
 |  | ||||||
|     // Create students
 |  | ||||||
|     const studentA = studentRepo.create({ |  | ||||||
|         username: STUDENT_A_USERNAME, |  | ||||||
|         firstName: 'Aron', |  | ||||||
|         lastName: 'Student', |  | ||||||
|     }); |  | ||||||
|     await studentRepo.save(studentA); |  | ||||||
| 
 |  | ||||||
|     const studentB = studentRepo.create({ |  | ||||||
|         username: STUDENT_B_USERNAME, |  | ||||||
|         firstName: 'Bill', |  | ||||||
|         lastName: 'Student', |  | ||||||
|     }); |  | ||||||
|     await studentRepo.save(studentB); |  | ||||||
| 
 |  | ||||||
|     // Create class for students
 |  | ||||||
|     const testClass = classRepo.create({ |  | ||||||
|         classId: CLASS_NAME, |  | ||||||
|         displayName: 'Test class', |  | ||||||
|     }); |  | ||||||
|     await classRepo.save(testClass); |  | ||||||
| 
 |  | ||||||
|     // Create assignment for students and assign them to different groups
 |  | ||||||
|     const assignment = assignmentRepo.create({ |  | ||||||
|         id: 0, |  | ||||||
|         title: 'Test assignment', |  | ||||||
|         description: 'Test description', |  | ||||||
|         learningPathHruid: learningContent.learningPath.hruid, |  | ||||||
|         learningPathLanguage: learningContent.learningPath.language, |  | ||||||
|         within: testClass, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const groupA = groupRepo.create({ |  | ||||||
|         groupNumber: 0, |  | ||||||
|         members: [studentA], |  | ||||||
|         assignment, |  | ||||||
|     }); |  | ||||||
|     await groupRepo.save(groupA); |  | ||||||
| 
 |  | ||||||
|     const groupB = groupRepo.create({ |  | ||||||
|         groupNumber: 1, |  | ||||||
|         members: [studentB], |  | ||||||
|         assignment, |  | ||||||
|     }); |  | ||||||
|     await groupRepo.save(groupB); |  | ||||||
| 
 |  | ||||||
|     // Let each of the students make a submission in his own group.
 |  | ||||||
|     const submissionA = submissionRepo.create({ |  | ||||||
|         learningObjectHruid: learningContent.branchingObject.hruid, |  | ||||||
|         learningObjectLanguage: learningContent.branchingObject.language, |  | ||||||
|         learningObjectVersion: learningContent.branchingObject.version, |  | ||||||
|         onBehalfOf: groupA, |  | ||||||
|         submitter: studentA, |  | ||||||
|         submissionTime: new Date(), |  | ||||||
|         content: '[0]', |  | ||||||
|     }); |  | ||||||
|     await submissionRepo.save(submissionA); |  | ||||||
| 
 |  | ||||||
|     const submissionB = submissionRepo.create({ |  | ||||||
|         learningObjectHruid: learningContent.branchingObject.hruid, |  | ||||||
|         learningObjectLanguage: learningContent.branchingObject.language, |  | ||||||
|         learningObjectVersion: learningContent.branchingObject.version, |  | ||||||
|         onBehalfOf: groupA, |  | ||||||
|         submitter: studentB, |  | ||||||
|         submissionTime: new Date(), |  | ||||||
|         content: '[1]', |  | ||||||
|     }); |  | ||||||
|     await submissionRepo.save(submissionB); |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|         learningContent: learningContent, |  | ||||||
|         studentA: studentA, |  | ||||||
|         studentB: studentB, |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function expectBranchingObjectNode( |  | ||||||
|     result: LearningPathResponse, |  | ||||||
|     persTestData: { |  | ||||||
|         learningContent: ConditionTestLearningPathAndLearningObjects; |  | ||||||
|         studentA: Student; |  | ||||||
|         studentB: Student; |  | ||||||
|     } |  | ||||||
| ): LearningObjectNode { |  | ||||||
|     const branchingObjectMatches = result.data![0].nodes.filter( |  | ||||||
|         (it) => it.learningobject_hruid === persTestData.learningContent.branchingObject.hruid |  | ||||||
|     ); |  | ||||||
|     expect(branchingObjectMatches.length).toBe(1); |     expect(branchingObjectMatches.length).toBe(1); | ||||||
|     return branchingObjectMatches[0]; |     return branchingObjectMatches[0]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| describe('DatabaseLearningPathProvider', () => { | describe('DatabaseLearningPathProvider', () => { | ||||||
|     let example: { learningObject: LearningObject; learningPath: LearningPath }; |     let testLearningPath: LearningPath; | ||||||
|     let persTestData: { learningContent: ConditionTestLearningPathAndLearningObjects; studentA: Student; studentB: Student }; |     let branchingLearningObject: RequiredEntityData<LearningObject>; | ||||||
|  |     let extraExerciseLearningObject: RequiredEntityData<LearningObject>; | ||||||
|  |     let finalLearningObject: RequiredEntityData<LearningObject>; | ||||||
|  |     let groupA: Group; | ||||||
|  |     let groupB: Group; | ||||||
| 
 | 
 | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|         await setupTestApp(); |         await setupTestApp(); | ||||||
|         example = await initExampleData(); |         testLearningPath = mapToLearningPath(testLearningPathWithConditions, []); | ||||||
|         persTestData = await initPersonalizationTestData(); |         branchingLearningObject = testLearningObjectMultipleChoice; | ||||||
|  |         extraExerciseLearningObject = testLearningObject01; | ||||||
|  |         finalLearningObject = testLearningObjectEssayQuestion; | ||||||
|  |         groupA = getTestGroup01(); | ||||||
|  |         groupB = getTestGroup02(); | ||||||
|  | 
 | ||||||
|  |         // Place different submissions for group A and B.
 | ||||||
|  |         const submissionRepo = getSubmissionRepository(); | ||||||
|  |         const submissionA = submissionRepo.create({ | ||||||
|  |             learningObjectHruid: branchingLearningObject.hruid, | ||||||
|  |             learningObjectLanguage: branchingLearningObject.language, | ||||||
|  |             learningObjectVersion: branchingLearningObject.version, | ||||||
|  |             content: '[0]', | ||||||
|  |             onBehalfOf: groupA, | ||||||
|  |             submissionTime: new Date(), | ||||||
|  |             submitter: groupA.members[0], | ||||||
|  |         }); | ||||||
|  |         await submissionRepo.save(submissionA); | ||||||
|  | 
 | ||||||
|  |         const submissionB = submissionRepo.create({ | ||||||
|  |             learningObjectHruid: branchingLearningObject.hruid, | ||||||
|  |             learningObjectLanguage: branchingLearningObject.language, | ||||||
|  |             learningObjectVersion: branchingLearningObject.version, | ||||||
|  |             content: '[1]', | ||||||
|  |             onBehalfOf: groupB, | ||||||
|  |             submissionTime: new Date(), | ||||||
|  |             submitter: groupB.members[0], | ||||||
|  |         }); | ||||||
|  |         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'); | ||||||
|                 [example.learningPath.hruid], |  | ||||||
|                 example.learningPath.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); | ||||||
| 
 | 
 | ||||||
|             const learningObjectsOnPath = ( |             expectToBeCorrectLearningPath(result.data![0], testLearningPathWithConditions); | ||||||
|                 await Promise.all( |  | ||||||
|                     example.learningPath.nodes.map(async (node) => |  | ||||||
|                         learningObjectService.getLearningObjectById({ |  | ||||||
|                             hruid: node.learningObjectHruid, |  | ||||||
|                             version: node.version, |  | ||||||
|                             language: node.language, |  | ||||||
|                         }) |  | ||||||
|                     ) |  | ||||||
|                 ) |  | ||||||
|             ).filter((it) => it !== null); |  | ||||||
| 
 |  | ||||||
|             expectToBeCorrectLearningPath(result.data![0], example.learningPath, learningObjectsOnPath); |  | ||||||
|         }); |         }); | ||||||
|         it('returns the correct personalized learning path', async () => { |         it('returns the correct personalized learning path', async () => { | ||||||
|             // For student A:
 |             // For student A:
 | ||||||
|             let result = await databaseLearningPathProvider.fetchLearningPaths( |             let result = await databaseLearningPathProvider.fetchLearningPaths( | ||||||
|                 [persTestData.learningContent.learningPath.hruid], |                 [testLearningPath.hruid], | ||||||
|                 persTestData.learningContent.learningPath.language, |                 testLearningPath.language, | ||||||
|                 'the source', |                 'the source', | ||||||
|                 { type: 'student', student: persTestData.studentA } |                 groupA | ||||||
|             ); |             ); | ||||||
|             expect(result.success).toBeTruthy(); |             expect(result.success).toBeTruthy(); | ||||||
|             expect(result.data?.length).toBe(1); |             expect(result.data?.length).toBe(1); | ||||||
| 
 | 
 | ||||||
|             // There should be exactly one branching object
 |             // There should be exactly one branching object
 | ||||||
|             let branchingObject = expectBranchingObjectNode(result, persTestData); |             let branchingObject = expectBranchingObjectNode(result); | ||||||
| 
 | 
 | ||||||
|             expect(branchingObject.transitions.filter((it) => it.next.hruid === persTestData.learningContent.finalObject.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 === 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 === persTestData.learningContent.extraExerciseObject.hruid).length).toBe( |             expect(branchingObject.transitions.filter((it) => it.next.hruid === extraExerciseLearningObject.hruid).length).toBe(1); // There should however be a path to the extra exercise object.
 | ||||||
|                 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); | ||||||
|                 [persTestData.learningContent.learningPath.hruid], |  | ||||||
|                 persTestData.learningContent.learningPath.language, |  | ||||||
|                 'the source', |  | ||||||
|                 { type: 'student', student: persTestData.studentB } |  | ||||||
|             ); |  | ||||||
|             expect(result.success).toBeTruthy(); |             expect(result.success).toBeTruthy(); | ||||||
|             expect(result.data?.length).toBe(1); |             expect(result.data?.length).toBe(1); | ||||||
| 
 | 
 | ||||||
|             // There should still be exactly one branching object
 |             // There should still be exactly one branching object
 | ||||||
|             branchingObject = expectBranchingObjectNode(result, persTestData); |             branchingObject = expectBranchingObjectNode(result); | ||||||
| 
 | 
 | ||||||
|             // However, now the student picks the other option.
 |             // However, now the student picks the other option.
 | ||||||
|             expect(branchingObject.transitions.filter((it) => it.next.hruid === persTestData.learningContent.finalObject.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 === 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 === persTestData.learningContent.extraExerciseObject.hruid).length).toBe( |             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.
 | ||||||
|                 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( | ||||||
|                 [example.learningPath.hruid], |                 [testLearningPath.hruid], | ||||||
|                 Language.Abkhazian, // Wrong language
 |                 Language.Abkhazian, // Wrong language
 | ||||||
|                 'the source' |                 'the source' | ||||||
|             ); |             ); | ||||||
|  | @ -233,27 +118,24 @@ 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); | ||||||
|                 example.learningPath.title.substring(2, 6), |  | ||||||
|                 example.learningPath.language |  | ||||||
|             ); |  | ||||||
|             expect(result.length).toBe(1); |             expect(result.length).toBe(1); | ||||||
|             expect(result[0].title).toBe(example.learningPath.title); |             expect(result[0].title).toBe(testLearningPath.title); | ||||||
|             expect(result[0].description).toBe(example.learningPath.description); |             expect(result[0].description).toBe(testLearningPath.description); | ||||||
|         }); |         }); | ||||||
|         it('returns the correct learning path when queried with a substring of the description', async () => { |         it('returns the correct learning path when queried with a substring of the description', async () => { | ||||||
|             const result = await databaseLearningPathProvider.searchLearningPaths( |             const result = await databaseLearningPathProvider.searchLearningPaths( | ||||||
|                 example.learningPath.description.substring(5, 12), |                 testLearningPath.description.substring(5, 12), | ||||||
|                 example.learningPath.language |                 testLearningPath.language | ||||||
|             ); |             ); | ||||||
|             expect(result.length).toBe(1); |             expect(result.length).toBe(1); | ||||||
|             expect(result[0].title).toBe(example.learningPath.title); |             expect(result[0].title).toBe(testLearningPath.title); | ||||||
|             expect(result[0].description).toBe(example.learningPath.description); |             expect(result[0].description).toBe(testLearningPath.description); | ||||||
|         }); |         }); | ||||||
|         it('returns an empty result when queried with a text which is not a substring of the title or the description of a learning path', async () => { |         it('returns an empty result when queried with a text which is not a substring of the title or the description of a learning path', async () => { | ||||||
|             const result = await databaseLearningPathProvider.searchLearningPaths( |             const result = await databaseLearningPathProvider.searchLearningPaths( | ||||||
|                 'substring which does not occur in the title or the description of a learning object', |                 'substring which does not occur in the title or the description of a learning object', | ||||||
|                 example.learningPath.language |                 testLearningPath.language | ||||||
|             ); |             ); | ||||||
|             expect(result.length).toBe(0); |             expect(result.length).toBe(0); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  | @ -1,22 +1,9 @@ | ||||||
| import { beforeAll, describe, expect, it } from 'vitest'; | import { beforeAll, describe, expect, it } from 'vitest'; | ||||||
| import { setupTestApp } from '../../setup-tests'; | import { setupTestApp } from '../../setup-tests'; | ||||||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity'; |  | ||||||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity'; |  | ||||||
| import { getLearningObjectRepository, getLearningPathRepository } from '../../../src/data/repositories'; |  | ||||||
| import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example'; |  | ||||||
| import learningPathExample from '../../test-assets/learning-paths/pn-werking-example'; |  | ||||||
| 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 { testPartiallyDatabaseAndPartiallyDwengoApiLearningPath } from '../../test_assets/content/learning-paths.testdata'; | ||||||
| async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { | import { LearningPath as LearningPathDTO } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
|     const learningObjectRepo = getLearningObjectRepository(); |  | ||||||
|     const learningPathRepo = getLearningPathRepository(); |  | ||||||
|     const learningObject = learningObjectExample.createLearningObject(); |  | ||||||
|     const learningPath = learningPathExample.createLearningPath(); |  | ||||||
|     await learningObjectRepo.save(learningObject); |  | ||||||
|     await learningPathRepo.save(learningPath); |  | ||||||
|     return { learningObject, learningPath }; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| 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'; | ||||||
|  | @ -24,42 +11,49 @@ const TEST_DWENGO_EXCLUSIVE_LEARNING_PATH_SEARCH_QUERY = 'Microscopie'; | ||||||
| const TEST_SEARCH_QUERY_EXPECTING_NO_MATCHES = 'su$m8f9usf89ud<p9<U8SDP8UP9'; | const TEST_SEARCH_QUERY_EXPECTING_NO_MATCHES = 'su$m8f9usf89ud<p9<U8SDP8UP9'; | ||||||
| 
 | 
 | ||||||
| describe('LearningPathService', () => { | describe('LearningPathService', () => { | ||||||
|     let example: { learningObject: LearningObject; learningPath: LearningPath }; |     let testLearningPath: LearningPathDTO; | ||||||
|     beforeAll(async () => { |     beforeAll(async () => { | ||||||
|         await setupTestApp(); |         await setupTestApp(); | ||||||
|         example = await initExampleData(); |         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 () => { | ||||||
|             const result = await learningPathService.fetchLearningPaths( |             const result = await learningPathService.fetchLearningPaths( | ||||||
|                 [example.learningPath.hruid, TEST_DWENGO_LEARNING_PATH_HRUID], |                 [testLearningPath.hruid, TEST_DWENGO_LEARNING_PATH_HRUID], | ||||||
|                 example.learningPath.language, |                 testLearningPath.language as Language, | ||||||
|                 'the source' |                 'the source' | ||||||
|             ); |             ); | ||||||
|             expect(result.success).toBeTruthy(); |             expect(result.success).toBeTruthy(); | ||||||
|             expect(result.data?.filter((it) => it.hruid === TEST_DWENGO_LEARNING_PATH_HRUID).length).not.toBe(0); |             expect(result.data?.filter((it) => it.hruid === TEST_DWENGO_LEARNING_PATH_HRUID).length).not.toBe(0); | ||||||
|             expect(result.data?.filter((it) => it.hruid === example.learningPath.hruid).length).not.toBe(0); |             expect(result.data?.filter((it) => it.hruid === testLearningPath.hruid).length).not.toBe(0); | ||||||
|             expect(result.data?.find((it) => it.hruid === TEST_DWENGO_LEARNING_PATH_HRUID)?.title).toEqual(TEST_DWENGO_LEARNING_PATH_TITLE); |             expect(result.data?.find((it) => it.hruid === TEST_DWENGO_LEARNING_PATH_HRUID)?.title).toEqual(TEST_DWENGO_LEARNING_PATH_TITLE); | ||||||
|             expect(result.data?.find((it) => it.hruid === example.learningPath.hruid)?.title).toEqual(example.learningPath.title); |             expect(result.data?.find((it) => it.hruid === testLearningPath.hruid)?.title).toEqual(testLearningPath.title); | ||||||
|         }); |         }); | ||||||
|         it('should include both the learning objects from the Dwengo API and learning objects from the database in its response', async () => { |         it('should include both the learning objects from the Dwengo API and learning objects from the database in its response', async () => { | ||||||
|             const result = await learningPathService.fetchLearningPaths([example.learningPath.hruid], example.learningPath.language, 'the source'); |             const result = await learningPathService.fetchLearningPaths( | ||||||
|  |                 [testLearningPath.hruid], | ||||||
|  |                 testLearningPath.language as Language, | ||||||
|  |                 'the source' | ||||||
|  |             ); | ||||||
|             expect(result.success).toBeTruthy(); |             expect(result.success).toBeTruthy(); | ||||||
|             expect(result.data?.length).toBe(1); |             expect(result.data?.length).toBe(1); | ||||||
| 
 | 
 | ||||||
|             // Should include all the nodes, even those pointing to foreign learning objects.
 |             // Should include all the nodes, even those pointing to foreign learning objects.
 | ||||||
|             expect([...result.data![0].nodes.map((it) => it.learningobject_hruid)].sort((a, b) => a.localeCompare(b))).toEqual( |             expect([...result.data![0].nodes.map((it) => it.learningobject_hruid)].sort((a, b) => a.localeCompare(b))).toEqual( | ||||||
|                 example.learningPath.nodes.map((it) => it.learningObjectHruid).sort((a, b) => a.localeCompare(b)) |                 testLearningPath.nodes.map((it) => it.learningobject_hruid).sort((a, b) => a.localeCompare(b)) | ||||||
|             ); |             ); | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
|     describe('searchLearningPath', () => { |     describe('searchLearningPath', () => { | ||||||
|         it('should include both the learning paths from the Dwengo API and those from the database in its response', async () => { |         it('should include both the learning paths from the Dwengo API and those from the database in its response', async () => { | ||||||
|             // This matches the learning object in the database, but definitely also some learning objects in the Dwengo API.
 |             // This matches the learning object in the database, but definitely also some learning objects in the Dwengo API.
 | ||||||
|             const result = await learningPathService.searchLearningPaths(example.learningPath.title.substring(2, 3), example.learningPath.language); |             const result = await learningPathService.searchLearningPaths( | ||||||
|  |                 testLearningPath.title.substring(2, 3), | ||||||
|  |                 testLearningPath.language as Language | ||||||
|  |             ); | ||||||
| 
 | 
 | ||||||
|             // Should find the one from the database
 |             // Should find the one from the database
 | ||||||
|             expect(result.filter((it) => it.hruid === example.learningPath.hruid && it.title === example.learningPath.title).length).toBe(1); |             expect(result.filter((it) => it.hruid === testLearningPath.hruid && it.title === testLearningPath.title).length).toBe(1); | ||||||
| 
 | 
 | ||||||
|             // But should not only find that one.
 |             // But should not only find that one.
 | ||||||
|             expect(result.length).not.toBeLessThan(2); |             expect(result.length).not.toBeLessThan(2); | ||||||
|  | @ -71,7 +65,7 @@ describe('LearningPathService', () => { | ||||||
|             expect(result.length).not.toBe(0); |             expect(result.length).not.toBe(0); | ||||||
| 
 | 
 | ||||||
|             // But not the example learning path.
 |             // But not the example learning path.
 | ||||||
|             expect(result.filter((it) => it.hruid === example.learningPath.hruid && it.title === example.learningPath.title).length).toBe(0); |             expect(result.filter((it) => it.hruid === testLearningPath.hruid && it.title === testLearningPath.title).length).toBe(0); | ||||||
|         }); |         }); | ||||||
|         it('should return an empty list if neither the Dwengo API nor the database contains matches', async () => { |         it('should return an empty list if neither the Dwengo API nor the database contains matches', async () => { | ||||||
|             const result = await learningPathService.searchLearningPaths(TEST_SEARCH_QUERY_EXPECTING_NO_MATCHES, Language.Dutch); |             const result = await learningPathService.searchLearningPaths(TEST_SEARCH_QUERY_EXPECTING_NO_MATCHES, Language.Dutch); | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ 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'; | ||||||
| 
 | 
 | ||||||
| export async function setupTestApp(): Promise<void> { | export async function setupTestApp(): Promise<void> { | ||||||
|     dotenv.config({ path: '.env.test' }); |     dotenv.config({ path: '.env.test' }); | ||||||
|  | @ -29,8 +30,8 @@ export async function setupTestApp(): Promise<void> { | ||||||
|     const assignments = makeTestAssignemnts(em, classes); |     const assignments = makeTestAssignemnts(em, classes); | ||||||
|     const groups = makeTestGroups(em, students, assignments); |     const groups = makeTestGroups(em, students, assignments); | ||||||
| 
 | 
 | ||||||
|     assignments[0].groups = new Collection(groups.slice(0, 3)); |     assignments[0].groups = new Collection<Group>(groups.slice(0, 3)); | ||||||
|     assignments[1].groups = new Collection(groups.slice(3, 4)); |     assignments[1].groups = new Collection<Group>(groups.slice(3, 4)); | ||||||
| 
 | 
 | ||||||
|     const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes); |     const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes); | ||||||
|     const classJoinRequests = makeTestClassJoinRequests(em, students, classes); |     const classJoinRequests = makeTestClassJoinRequests(em, students, classes); | ||||||
|  |  | ||||||
|  | @ -1,10 +0,0 @@ | ||||||
| import { LearningObjectExample } from './learning-object-example'; |  | ||||||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity'; |  | ||||||
| 
 |  | ||||||
| export function createExampleLearningObjectWithAttachments(example: LearningObjectExample): LearningObject { |  | ||||||
|     const learningObject = example.createLearningObject(); |  | ||||||
|     for (const creationFn of Object.values(example.createAttachment)) { |  | ||||||
|         learningObject.attachments.push(creationFn(learningObject)); |  | ||||||
|     } |  | ||||||
|     return learningObject; |  | ||||||
| } |  | ||||||
|  | @ -1,32 +0,0 @@ | ||||||
| import { LearningObjectExample } from '../learning-object-example'; |  | ||||||
| import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; |  | ||||||
| import { Language } from '@dwengo-1/common/util/language'; |  | ||||||
| import { loadTestAsset } from '../../../test-utils/load-test-asset'; |  | ||||||
| import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; |  | ||||||
| import { envVars, getEnvVar } from '../../../../src/util/envVars'; |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Create a dummy learning object to be used in tests where multiple learning objects are needed (for example for use |  | ||||||
|  * on a path), but where the precise contents of the learning object are not important. |  | ||||||
|  */ |  | ||||||
| export function dummyLearningObject(hruid: string, language: Language, title: string): LearningObjectExample { |  | ||||||
|     return { |  | ||||||
|         createLearningObject: (): LearningObject => { |  | ||||||
|             const learningObject = new LearningObject(); |  | ||||||
|             learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + hruid; |  | ||||||
|             learningObject.language = language; |  | ||||||
|             learningObject.version = 1; |  | ||||||
|             learningObject.title = title; |  | ||||||
|             learningObject.description = 'Just a dummy learning object for testing purposes'; |  | ||||||
|             learningObject.contentType = DwengoContentType.TEXT_PLAIN; |  | ||||||
|             learningObject.content = Buffer.from('Dummy content'); |  | ||||||
|             learningObject.returnValue = { |  | ||||||
|                 callbackUrl: `/learningObject/${hruid}/submissions`, |  | ||||||
|                 callbackSchema: '[]', |  | ||||||
|             }; |  | ||||||
|             return learningObject; |  | ||||||
|         }, |  | ||||||
|         createAttachment: {}, |  | ||||||
|         getHTMLRendering: () => loadTestAsset('learning-objects/dummy/rendering.txt').toString(), |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity'; |  | ||||||
| import { Attachment } from '../../../src/entities/content/attachment.entity'; |  | ||||||
| 
 |  | ||||||
| interface LearningObjectExample { |  | ||||||
|     createLearningObject: () => LearningObject; |  | ||||||
|     createAttachment: Record<string, (owner: LearningObject) => Attachment>; |  | ||||||
|     getHTMLRendering: () => string; |  | ||||||
| } |  | ||||||
|  | @ -1,74 +0,0 @@ | ||||||
| import { LearningObjectExample } from '../learning-object-example'; |  | ||||||
| import { Language } from '@dwengo-1/common/util/language'; |  | ||||||
| import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; |  | ||||||
| import { loadTestAsset } from '../../../test-utils/load-test-asset'; |  | ||||||
| import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; |  | ||||||
| import { Attachment } from '../../../../src/entities/content/attachment.entity'; |  | ||||||
| import { envVars, getEnvVar } from '../../../../src/util/envVars'; |  | ||||||
| import { EducationalGoal } from '../../../../src/entities/content/educational-goal.entity'; |  | ||||||
| import { ReturnValue } from '../../../../src/entities/content/return-value.entity'; |  | ||||||
| 
 |  | ||||||
| const ASSETS_PREFIX = 'learning-objects/pn-werkingnotebooks/'; |  | ||||||
| 
 |  | ||||||
| const example: LearningObjectExample = { |  | ||||||
|     createLearningObject: () => { |  | ||||||
|         const learningObject = new LearningObject(); |  | ||||||
|         learningObject.hruid = `${getEnvVar(envVars.UserContentPrefix)}pn_werkingnotebooks`; |  | ||||||
|         learningObject.version = 3; |  | ||||||
|         learningObject.language = Language.Dutch; |  | ||||||
|         learningObject.title = 'Werken met notebooks'; |  | ||||||
|         learningObject.description = 'Leren werken met notebooks'; |  | ||||||
|         learningObject.keywords = ['Python', 'KIKS', 'Wiskunde', 'STEM', 'AI']; |  | ||||||
| 
 |  | ||||||
|         const educationalGoal1 = new EducationalGoal(); |  | ||||||
|         educationalGoal1.source = 'Source'; |  | ||||||
|         educationalGoal1.id = 'id'; |  | ||||||
| 
 |  | ||||||
|         const educationalGoal2 = new EducationalGoal(); |  | ||||||
|         educationalGoal2.source = 'Source2'; |  | ||||||
|         educationalGoal2.id = 'id2'; |  | ||||||
| 
 |  | ||||||
|         learningObject.educationalGoals = [educationalGoal1, educationalGoal2]; |  | ||||||
|         learningObject.admins = []; |  | ||||||
|         learningObject.contentType = DwengoContentType.TEXT_MARKDOWN; |  | ||||||
|         learningObject.teacherExclusive = false; |  | ||||||
|         learningObject.skosConcepts = [ |  | ||||||
|             '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-computers-en-systemen', |  | ||||||
|         ]; |  | ||||||
|         learningObject.copyright = 'dwengo'; |  | ||||||
|         learningObject.license = 'dwengo'; |  | ||||||
|         learningObject.estimatedTime = 10; |  | ||||||
| 
 |  | ||||||
|         const returnValue = new ReturnValue(); |  | ||||||
|         returnValue.callbackUrl = 'callback_url_example'; |  | ||||||
|         returnValue.callbackSchema = '{"att": "test", "att2": "test2"}'; |  | ||||||
| 
 |  | ||||||
|         learningObject.returnValue = returnValue; |  | ||||||
|         learningObject.available = true; |  | ||||||
|         learningObject.content = loadTestAsset(`${ASSETS_PREFIX}/content.md`); |  | ||||||
| 
 |  | ||||||
|         return learningObject; |  | ||||||
|     }, |  | ||||||
|     createAttachment: { |  | ||||||
|         dwengoLogo: (learningObject) => { |  | ||||||
|             const att = new Attachment(); |  | ||||||
|             att.learningObject = learningObject; |  | ||||||
|             att.name = 'dwengo.png'; |  | ||||||
|             att.mimeType = 'image/png'; |  | ||||||
|             att.content = loadTestAsset(`${ASSETS_PREFIX}/dwengo.png`); |  | ||||||
|             return att; |  | ||||||
|         }, |  | ||||||
|         knop: (learningObject) => { |  | ||||||
|             const att = new Attachment(); |  | ||||||
|             att.learningObject = learningObject; |  | ||||||
|             att.name = 'Knop.png'; |  | ||||||
|             att.mimeType = 'image/png'; |  | ||||||
|             att.content = loadTestAsset(`${ASSETS_PREFIX}/Knop.png`); |  | ||||||
|             return att; |  | ||||||
|         }, |  | ||||||
|     }, |  | ||||||
|     getHTMLRendering: () => loadTestAsset(`${ASSETS_PREFIX}/rendering.txt`).toString(), |  | ||||||
| }; |  | ||||||
| export default example; |  | ||||||
|  | @ -1,2 +0,0 @@ | ||||||
| ::MC basic:: |  | ||||||
| How are you? {} |  | ||||||
|  | @ -1,7 +0,0 @@ | ||||||
| <div class="learning-object-gift"> |  | ||||||
|     <div id="gift-q1" class="gift-question"> |  | ||||||
|         <h2 id="gift-q1-title" class="gift-title">MC basic</h2> |  | ||||||
|         <p id="gift-q1-stem" class="gift-stem">How are you?</p> |  | ||||||
|         <textarea id="gift-q1-answer" class="gift-essay-answer"></textarea> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
|  | @ -1,28 +0,0 @@ | ||||||
| import { LearningObjectExample } from '../learning-object-example'; |  | ||||||
| import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; |  | ||||||
| import { loadTestAsset } from '../../../test-utils/load-test-asset'; |  | ||||||
| import { envVars, getEnvVar } from '../../../../src/util/envVars'; |  | ||||||
| import { Language } from '@dwengo-1/common/util/language'; |  | ||||||
| import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; |  | ||||||
| 
 |  | ||||||
| const example: LearningObjectExample = { |  | ||||||
|     createLearningObject: () => { |  | ||||||
|         const learningObject = new LearningObject(); |  | ||||||
|         learningObject.hruid = `${getEnvVar(envVars.UserContentPrefix)}test_essay`; |  | ||||||
|         learningObject.language = Language.English; |  | ||||||
|         learningObject.version = 1; |  | ||||||
|         learningObject.title = 'Essay question for testing'; |  | ||||||
|         learningObject.description = 'This essay question was only created for testing purposes.'; |  | ||||||
|         learningObject.contentType = DwengoContentType.GIFT; |  | ||||||
|         learningObject.returnValue = { |  | ||||||
|             callbackUrl: `/learningObject/${learningObject.hruid}/submissions`, |  | ||||||
|             callbackSchema: '["antwoord vraag 1"]', |  | ||||||
|         }; |  | ||||||
|         learningObject.content = loadTestAsset('learning-objects/test-essay/content.txt'); |  | ||||||
|         return learningObject; |  | ||||||
|     }, |  | ||||||
|     createAttachment: {}, |  | ||||||
|     getHTMLRendering: () => loadTestAsset('learning-objects/test-essay/rendering.txt').toString(), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export default example; |  | ||||||
|  | @ -1,28 +0,0 @@ | ||||||
| import { LearningObjectExample } from '../learning-object-example'; |  | ||||||
| import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; |  | ||||||
| import { loadTestAsset } from '../../../test-utils/load-test-asset'; |  | ||||||
| import { envVars, getEnvVar } from '../../../../src/util/envVars'; |  | ||||||
| import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; |  | ||||||
| import { Language } from '@dwengo-1/common/util/language'; |  | ||||||
| 
 |  | ||||||
| const example: LearningObjectExample = { |  | ||||||
|     createLearningObject: () => { |  | ||||||
|         const learningObject = new LearningObject(); |  | ||||||
|         learningObject.hruid = `${getEnvVar(envVars.UserContentPrefix)}test_multiple_choice`; |  | ||||||
|         learningObject.language = Language.English; |  | ||||||
|         learningObject.version = 1; |  | ||||||
|         learningObject.title = 'Multiple choice question for testing'; |  | ||||||
|         learningObject.description = 'This multiple choice question was only created for testing purposes.'; |  | ||||||
|         learningObject.contentType = DwengoContentType.GIFT; |  | ||||||
|         learningObject.returnValue = { |  | ||||||
|             callbackUrl: `/learningObject/${learningObject.hruid}/submissions`, |  | ||||||
|             callbackSchema: '["antwoord vraag 1"]', |  | ||||||
|         }; |  | ||||||
|         learningObject.content = loadTestAsset('learning-objects/test-multiple-choice/content.txt'); |  | ||||||
|         return learningObject; |  | ||||||
|     }, |  | ||||||
|     createAttachment: {}, |  | ||||||
|     getHTMLRendering: () => loadTestAsset('learning-objects/test-multiple-choice/rendering.txt').toString(), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export default example; |  | ||||||
|  | @ -1,3 +0,0 @@ | ||||||
| interface LearningPathExample { |  | ||||||
|     createLearningPath: () => LearningPath; |  | ||||||
| } |  | ||||||
|  | @ -1,36 +0,0 @@ | ||||||
| import { Language } from '@dwengo-1/common/util/language'; |  | ||||||
| import { LearningPathTransition } from '../../../src/entities/content/learning-path-transition.entity'; |  | ||||||
| import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity'; |  | ||||||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity'; |  | ||||||
| 
 |  | ||||||
| export function createLearningPathTransition( |  | ||||||
|     node: LearningPathNode, |  | ||||||
|     transitionNumber: number, |  | ||||||
|     condition: string | null, |  | ||||||
|     to: LearningPathNode |  | ||||||
| ): LearningPathTransition { |  | ||||||
|     const trans = new LearningPathTransition(); |  | ||||||
|     trans.node = node; |  | ||||||
|     trans.transitionNumber = transitionNumber; |  | ||||||
|     trans.condition = condition || 'true'; |  | ||||||
|     trans.next = to; |  | ||||||
|     return trans; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function createLearningPathNode( |  | ||||||
|     learningPath: LearningPath, |  | ||||||
|     nodeNumber: number, |  | ||||||
|     learningObjectHruid: string, |  | ||||||
|     version: number, |  | ||||||
|     language: Language, |  | ||||||
|     startNode: boolean |  | ||||||
| ): LearningPathNode { |  | ||||||
|     const node = new LearningPathNode(); |  | ||||||
|     node.learningPath = learningPath; |  | ||||||
|     node.nodeNumber = nodeNumber; |  | ||||||
|     node.learningObjectHruid = learningObjectHruid; |  | ||||||
|     node.version = version; |  | ||||||
|     node.language = language; |  | ||||||
|     node.startNode = startNode; |  | ||||||
|     return node; |  | ||||||
| } |  | ||||||
|  | @ -1,30 +0,0 @@ | ||||||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity'; |  | ||||||
| import { Language } from '@dwengo-1/common/util/language'; |  | ||||||
| import { envVars, getEnvVar } from '../../../src/util/envVars'; |  | ||||||
| import { createLearningPathNode, createLearningPathTransition } from './learning-path-utils'; |  | ||||||
| import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity'; |  | ||||||
| 
 |  | ||||||
| function createNodes(learningPath: LearningPath): LearningPathNode[] { |  | ||||||
|     const nodes = [ |  | ||||||
|         createLearningPathNode(learningPath, 0, 'u_pn_werkingnotebooks', 3, Language.Dutch, true), |  | ||||||
|         createLearningPathNode(learningPath, 1, 'pn_werkingnotebooks2', 3, Language.Dutch, false), |  | ||||||
|         createLearningPathNode(learningPath, 2, 'pn_werkingnotebooks3', 3, Language.Dutch, false), |  | ||||||
|     ]; |  | ||||||
|     nodes[0].transitions.push(createLearningPathTransition(nodes[0], 0, 'true', nodes[1])); |  | ||||||
|     nodes[1].transitions.push(createLearningPathTransition(nodes[1], 0, 'true', nodes[2])); |  | ||||||
|     return nodes; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const example: LearningPathExample = { |  | ||||||
|     createLearningPath: () => { |  | ||||||
|         const path = new LearningPath(); |  | ||||||
|         path.language = Language.Dutch; |  | ||||||
|         path.hruid = `${getEnvVar(envVars.UserContentPrefix)}pn_werking`; |  | ||||||
|         path.title = 'Werken met notebooks'; |  | ||||||
|         path.description = 'Een korte inleiding tot Python notebooks. Hoe ga je gemakkelijk en efficiënt met de notebooks aan de slag?'; |  | ||||||
|         path.nodes = createNodes(path); |  | ||||||
|         return path; |  | ||||||
|     }, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export default example; |  | ||||||
|  | @ -1,80 +0,0 @@ | ||||||
| import { LearningPath } from '../../../src/entities/content/learning-path.entity'; |  | ||||||
| import { Language } from '@dwengo-1/common/util/language'; |  | ||||||
| import testMultipleChoiceExample from '../learning-objects/test-multiple-choice/test-multiple-choice-example'; |  | ||||||
| import { dummyLearningObject } from '../learning-objects/dummy/dummy-learning-object-example'; |  | ||||||
| import { createLearningPathNode, createLearningPathTransition } from './learning-path-utils'; |  | ||||||
| import { LearningObject } from '../../../src/entities/content/learning-object.entity'; |  | ||||||
| import { envVars, getEnvVar } from '../../../src/util/envVars'; |  | ||||||
| 
 |  | ||||||
| export interface ConditionTestLearningPathAndLearningObjects { |  | ||||||
|     branchingObject: LearningObject; |  | ||||||
|     extraExerciseObject: LearningObject; |  | ||||||
|     finalObject: LearningObject; |  | ||||||
|     learningPath: LearningPath; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function createConditionTestLearningPathAndLearningObjects(): ConditionTestLearningPathAndLearningObjects { |  | ||||||
|     const learningPath = new LearningPath(); |  | ||||||
|     learningPath.hruid = `${getEnvVar(envVars.UserContentPrefix)}test_conditions`; |  | ||||||
|     learningPath.language = Language.English; |  | ||||||
|     learningPath.title = 'Example learning path with conditional transitions'; |  | ||||||
|     learningPath.description = 'This learning path was made for the purpose of testing conditional transitions'; |  | ||||||
| 
 |  | ||||||
|     const branchingLearningObject = testMultipleChoiceExample.createLearningObject(); |  | ||||||
|     const extraExerciseLearningObject = dummyLearningObject( |  | ||||||
|         'test_extra_exercise', |  | ||||||
|         Language.English, |  | ||||||
|         'Extra exercise (for students with difficulties)' |  | ||||||
|     ).createLearningObject(); |  | ||||||
|     const finalLearningObject = dummyLearningObject( |  | ||||||
|         'test_final_learning_object', |  | ||||||
|         Language.English, |  | ||||||
|         'Final exercise (for everyone)' |  | ||||||
|     ).createLearningObject(); |  | ||||||
| 
 |  | ||||||
|     const branchingNode = createLearningPathNode( |  | ||||||
|         learningPath, |  | ||||||
|         0, |  | ||||||
|         branchingLearningObject.hruid, |  | ||||||
|         branchingLearningObject.version, |  | ||||||
|         branchingLearningObject.language, |  | ||||||
|         true |  | ||||||
|     ); |  | ||||||
|     const extraExerciseNode = createLearningPathNode( |  | ||||||
|         learningPath, |  | ||||||
|         1, |  | ||||||
|         extraExerciseLearningObject.hruid, |  | ||||||
|         extraExerciseLearningObject.version, |  | ||||||
|         extraExerciseLearningObject.language, |  | ||||||
|         false |  | ||||||
|     ); |  | ||||||
|     const finalNode = createLearningPathNode( |  | ||||||
|         learningPath, |  | ||||||
|         2, |  | ||||||
|         finalLearningObject.hruid, |  | ||||||
|         finalLearningObject.version, |  | ||||||
|         finalLearningObject.language, |  | ||||||
|         false |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     const transitionToExtraExercise = createLearningPathTransition( |  | ||||||
|         branchingNode, |  | ||||||
|         0, |  | ||||||
|         '$[?(@[0] == 0)]', // The answer to the first question was the first one, which says that it is difficult for the student to follow along.
 |  | ||||||
|         extraExerciseNode |  | ||||||
|     ); |  | ||||||
|     const directTransitionToFinal = createLearningPathTransition(branchingNode, 1, '$[?(@[0] == 1)]', finalNode); |  | ||||||
|     const transitionExtraExerciseToFinal = createLearningPathTransition(extraExerciseNode, 0, 'true', finalNode); |  | ||||||
| 
 |  | ||||||
|     branchingNode.transitions = [transitionToExtraExercise, directTransitionToFinal]; |  | ||||||
|     extraExerciseNode.transitions = [transitionExtraExerciseToFinal]; |  | ||||||
| 
 |  | ||||||
|     learningPath.nodes = [branchingNode, extraExerciseNode, finalNode]; |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|         branchingObject: branchingLearningObject, |  | ||||||
|         finalObject: finalLearningObject, |  | ||||||
|         extraExerciseObject: extraExerciseLearningObject, |  | ||||||
|         learningPath: learningPath, |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { AssertionError } from 'node:assert'; | import { AssertionError } from 'node:assert'; | ||||||
| import { LearningObject } from '../../src/entities/content/learning-object.entity'; | import { LearningObject } from '../../src/entities/content/learning-object.entity'; | ||||||
| import { LearningPath as LearningPathEntity } from '../../src/entities/content/learning-path.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'; | ||||||
| 
 | 
 | ||||||
| // 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']; | ||||||
|  | @ -11,65 +11,56 @@ const IGNORE_PROPERTIES = ['parent']; | ||||||
|  * Checks if the actual entity from the database conforms to the entity that was added previously. |  * Checks if the actual entity from the database conforms to the entity that was added previously. | ||||||
|  * @param actual The actual entity retrieved from the database |  * @param actual The actual entity retrieved from the database | ||||||
|  * @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. | ||||||
|  */ |  */ | ||||||
| export function expectToBeCorrectEntity<T extends object>(actual: { entity: T; name?: string }, expected: { entity: T; name?: string }): void { | export function expectToBeCorrectEntity<T extends object>(actual: T, expected: T, propertyPrefix = ''): void { | ||||||
|     if (!actual.name) { |     for (const property in expected) { | ||||||
|         actual.name = 'actual'; |         if (Object.prototype.hasOwnProperty.call(expected, property)) { | ||||||
|     } |             const prefixedProperty = propertyPrefix + property; | ||||||
|     if (!expected.name) { |  | ||||||
|         expected.name = 'expected'; |  | ||||||
|     } |  | ||||||
|     for (const property in expected.entity) { |  | ||||||
|             if ( |             if ( | ||||||
|                 property in IGNORE_PROPERTIES && |                 property in IGNORE_PROPERTIES && | ||||||
|             expected.entity[property] !== undefined && // If we don't expect a certain value for a property, we assume it can be filled in by the database however it wants.
 |                 expected[property] !== undefined && // If we don't expect a certain value for a property, we assume it can be filled in by the database however it wants.
 | ||||||
|             typeof expected.entity[property] !== 'function' // Functions obviously are not persisted via the database
 |                 typeof expected[property] !== 'function' // Functions obviously are not persisted via the database
 | ||||||
|             ) { |             ) { | ||||||
|             if (!Object.prototype.hasOwnProperty.call(actual.entity, property)) { |                 if (!Object.prototype.hasOwnProperty.call(actual, property)) { | ||||||
|                     throw new AssertionError({ |                     throw new AssertionError({ | ||||||
|                     message: `${expected.name} has defined property ${property}, but ${actual.name} is missing it.`, |                         message: `Expected property ${prefixedProperty}, but it is missing.`, | ||||||
|                     }); |                     }); | ||||||
|                 } |                 } | ||||||
|             if (typeof expected.entity[property] === 'boolean') { |                 if (typeof expected[property] === 'boolean') { | ||||||
|                     // Sometimes, booleans get represented by numbers 0 and 1 in the objects actual from the database.
 |                     // Sometimes, booleans get represented by numbers 0 and 1 in the objects actual from the database.
 | ||||||
|                 if (Boolean(expected.entity[property]) !== Boolean(actual.entity[property])) { |                     if (Boolean(expected[property]) !== Boolean(actual[property])) { | ||||||
|                         throw new AssertionError({ |                         throw new AssertionError({ | ||||||
|                         message: `${property} was ${expected.entity[property]} in ${expected.name},
 |                             message: `Expected ${prefixedProperty} to be ${expected[property]},
 | ||||||
|                         but ${actual.entity[property]} (${Boolean(expected.entity[property])}) in ${actual.name}`,
 |                         but was ${actual[property]} (${Boolean(expected[property])}).`,
 | ||||||
|                         }); |                         }); | ||||||
|                     } |                     } | ||||||
|             } else if (typeof expected.entity[property] !== typeof actual.entity[property]) { |                 } else if (typeof expected[property] !== typeof actual[property]) { | ||||||
|                     throw new AssertionError({ |                     throw new AssertionError({ | ||||||
|                     message: `${property} has type ${typeof expected.entity[property]} in ${expected.name}, but type ${typeof actual.entity[property]} in ${actual.name}.`, |                         message: | ||||||
|  |                             `${prefixedProperty} was expected to have type ${typeof expected[property]},` + | ||||||
|  |                             `but had type ${typeof actual[property]}.`, | ||||||
|                     }); |                     }); | ||||||
|             } else if (typeof expected.entity[property] === 'object') { |                 } else if (typeof expected[property] === 'object') { | ||||||
|                 expectToBeCorrectEntity( |                     expectToBeCorrectEntity(actual[property] as object, expected[property] as object, property); | ||||||
|                     { |  | ||||||
|                         name: actual.name + '.' + property, |  | ||||||
|                         entity: actual.entity[property] as object, |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: expected.name + '.' + property, |  | ||||||
|                         entity: expected.entity[property] as object, |  | ||||||
|                     } |  | ||||||
|                 ); |  | ||||||
|                 } else { |                 } else { | ||||||
|                 if (expected.entity[property] !== actual.entity[property]) { |                     if (expected[property] !== actual[property]) { | ||||||
|                         throw new AssertionError({ |                         throw new AssertionError({ | ||||||
|                         message: `${property} was ${expected.entity[property]} in ${expected.name}, but ${actual.entity[property]} in ${actual.name}`, |                             message: `${prefixedProperty} was expected to be ${expected[property]}, ` + `but was ${actual[property]}.`, | ||||||
|                         }); |                         }); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Checks that filtered is the correct representation of original as FilteredLearningObject. |  * Checks that filtered is the correct representation of original as FilteredLearningObject. | ||||||
|  * @param filtered the representation as FilteredLearningObject |  * @param filtered the representation as FilteredLearningObject | ||||||
|  * @param original the original entity added to the database |  * @param original the data of the entity in the database that was filtered. | ||||||
|  */ |  */ | ||||||
| export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearningObject, original: LearningObject): void { | export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearningObject, original: RequiredEntityData<LearningObject>): void { | ||||||
|     expect(filtered.uuid).toEqual(original.uuid); |     expect(filtered.uuid).toEqual(original.uuid); | ||||||
|     expect(filtered.version).toEqual(original.version); |     expect(filtered.version).toEqual(original.version); | ||||||
|     expect(filtered.language).toEqual(original.language); |     expect(filtered.language).toEqual(original.language); | ||||||
|  | @ -97,54 +88,55 @@ export function expectToBeCorrectFilteredLearningObject(filtered: FilteredLearni | ||||||
|  * is a correct representation of the given learning path entity. |  * is a correct representation of the given learning path entity. | ||||||
|  * |  * | ||||||
|  * @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 expectedEntity The expected entity |  * @param expected The learning path that should have been returned. | ||||||
|  * @param learningObjectsOnPath The learning objects on LearningPath. Necessary since some information in |  | ||||||
|  *                              the learning path returned from the API endpoint |  | ||||||
|  */ |  */ | ||||||
| export function expectToBeCorrectLearningPath( | export function expectToBeCorrectLearningPath(learningPath: LearningPath, expected: LearningPath): void { | ||||||
|     learningPath: LearningPath, |     expect(learningPath.hruid).toEqual(expected.hruid); | ||||||
|     expectedEntity: LearningPathEntity, |     expect(learningPath.language).toEqual(expected.language); | ||||||
|     learningObjectsOnPath: FilteredLearningObject[] |     expect(learningPath.description).toEqual(expected.description); | ||||||
| ): void { |     expect(learningPath.title).toEqual(expected.title); | ||||||
|     expect(learningPath.hruid).toEqual(expectedEntity.hruid); |  | ||||||
|     expect(learningPath.language).toEqual(expectedEntity.language); |  | ||||||
|     expect(learningPath.description).toEqual(expectedEntity.description); |  | ||||||
|     expect(learningPath.title).toEqual(expectedEntity.title); |  | ||||||
| 
 | 
 | ||||||
|     const keywords = new Set(learningObjectsOnPath.flatMap((it) => it.keywords || [])); |     expect(new Set(learningPath.keywords.split(' '))).toEqual(new Set(learningPath.keywords.split(' '))); | ||||||
|     expect(new Set(learningPath.keywords.split(' '))).toEqual(keywords); |  | ||||||
| 
 | 
 | ||||||
|     const targetAges = new Set(learningObjectsOnPath.flatMap((it) => it.targetAges || [])); |     expect(new Set(learningPath.target_ages)).toEqual(new Set(expected.target_ages)); | ||||||
|     expect(new Set(learningPath.target_ages)).toEqual(targetAges); |     expect(learningPath.min_age).toEqual(Math.min(...expected.target_ages)); | ||||||
|     expect(learningPath.min_age).toEqual(Math.min(...targetAges)); |     expect(learningPath.max_age).toEqual(Math.max(...expected.target_ages)); | ||||||
|     expect(learningPath.max_age).toEqual(Math.max(...targetAges)); |  | ||||||
| 
 | 
 | ||||||
|     expect(learningPath.num_nodes).toEqual(expectedEntity.nodes.length); |     expect(learningPath.num_nodes).toEqual(expected.nodes.length); | ||||||
|     expect(learningPath.image || null).toEqual(expectedEntity.image); |     expect(learningPath.image ?? null).toEqual(expected.image ?? null); | ||||||
| 
 | 
 | ||||||
|     const expectedLearningPathNodes = new Map( |     for (const node of expected.nodes) { | ||||||
|         expectedEntity.nodes.map((node) => [ |         const correspondingNode = learningPath.nodes.find( | ||||||
|             { learningObjectHruid: node.learningObjectHruid, language: node.language, version: node.version }, |             (it) => node.learningobject_hruid === it.learningobject_hruid && node.language === it.language && node.version === it.version | ||||||
|             { startNode: node.startNode, transitions: node.transitions }, |  | ||||||
|         ]) |  | ||||||
|         ); |         ); | ||||||
|  |         expect(correspondingNode).toBeTruthy(); | ||||||
|  |         expect(Boolean(correspondingNode!.start_node)).toEqual(Boolean(node.start_node)); | ||||||
| 
 | 
 | ||||||
|     for (const node of learningPath.nodes) { |         for (const transition of node.transitions) { | ||||||
|         const nodeKey = { |             const correspondingTransition = correspondingNode!.transitions.find( | ||||||
|             learningObjectHruid: node.learningobject_hruid, |                 (it) => | ||||||
|             language: node.language, |                     it.next.hruid === transition.next.hruid && | ||||||
|             version: node.version, |                     it.next.language === transition.next.language && | ||||||
|         }; |                     it.next.version === transition.next.version | ||||||
|         expect(expectedLearningPathNodes.keys()).toContainEqual(nodeKey); |  | ||||||
|         const expectedNode = [...expectedLearningPathNodes.entries()].find( |  | ||||||
|             ([key, _]) => key.learningObjectHruid === nodeKey.learningObjectHruid && key.language === node.language && key.version === node.version |  | ||||||
|         )![1]; |  | ||||||
|         expect(node.start_node).toEqual(expectedNode.startNode); |  | ||||||
| 
 |  | ||||||
|         expect(new Set(node.transitions.map((it) => it.next.hruid))).toEqual( |  | ||||||
|             new Set(expectedNode.transitions.map((it) => it.next.learningObjectHruid)) |  | ||||||
|             ); |             ); | ||||||
|         expect(new Set(node.transitions.map((it) => it.next.language))).toEqual(new Set(expectedNode.transitions.map((it) => it.next.language))); |             expect(correspondingTransition).toBeTruthy(); | ||||||
|         expect(new Set(node.transitions.map((it) => it.next.version))).toEqual(new Set(expectedNode.transitions.map((it) => it.next.version))); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Expect that the given result is a singleton list with exactly the given element. | ||||||
|  |  */ | ||||||
|  | export function expectToHaveFoundPrecisely<T extends object>(expected: T, result: T[]): void { | ||||||
|  |     expect(result).toHaveProperty('length'); | ||||||
|  |     expect(result.length).toBe(1); | ||||||
|  |     expectToBeCorrectEntity(result[0], expected); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Expect that the given result is an empty list. | ||||||
|  |  */ | ||||||
|  | export function expectToHaveFoundNothing<T>(result: T[]): void { | ||||||
|  |     expect(result).toHaveProperty('length'); | ||||||
|  |     expect(result.length).toBe(0); | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								backend/tests/test-utils/get-html-rendering.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/tests/test-utils/get-html-rendering.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | import { RequiredEntityData } from '@mikro-orm/core'; | ||||||
|  | import { loadTestAsset } from './load-test-asset'; | ||||||
|  | import { LearningObject } from '../../src/entities/content/learning-object.entity'; | ||||||
|  | import { envVars, getEnvVar } from '../../src/util/envVars'; | ||||||
|  | 
 | ||||||
|  | export function getHtmlRenderingForTestLearningObject(learningObject: RequiredEntityData<LearningObject>): string { | ||||||
|  |     const userPrefix = getEnvVar(envVars.UserContentPrefix); | ||||||
|  |     const cleanedHruid = learningObject.hruid.startsWith(userPrefix) ? learningObject.hruid.substring(userPrefix.length) : learningObject.hruid; | ||||||
|  |     return loadTestAsset(`/content/learning-object-resources/${cleanedHruid}/rendering.txt`).toString(); | ||||||
|  | } | ||||||
|  | @ -1,10 +1,14 @@ | ||||||
| import fs from 'fs'; | import fs from 'fs'; | ||||||
| import path from 'node:path'; | import path from 'node:path'; | ||||||
|  | import { fileURLToPath } from 'node:url'; | ||||||
|  | 
 | ||||||
|  | const fileName = fileURLToPath(import.meta.url); | ||||||
|  | const dirName = path.dirname(fileName); | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Load the asset at the given path. |  * Load the asset at the given path. | ||||||
|  * @param relPath Path of the asset relative to the test-assets folder. |  * @param relPath Path of the asset relative to the test-assets folder. | ||||||
|  */ |  */ | ||||||
| export function loadTestAsset(relPath: string): Buffer { | export function loadTestAsset(relPath: string): Buffer { | ||||||
|     return fs.readFileSync(path.resolve(__dirname, `../test-assets/${relPath}`)); |     return fs.readFileSync(path.resolve(dirName, `../test_assets/${relPath}`)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,9 +2,11 @@ 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 { getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata'; | ||||||
| 
 | 
 | ||||||
| export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] { | export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] { | ||||||
|     const assignment01 = em.create(Assignment, { |     assignment01 = em.create(Assignment, { | ||||||
|         id: 21000, |         id: 21000, | ||||||
|         within: classes[0], |         within: classes[0], | ||||||
|         title: 'dire straits', |         title: 'dire straits', | ||||||
|  | @ -14,7 +16,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign | ||||||
|         groups: [], |         groups: [], | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const assignment02 = em.create(Assignment, { |     assignment02 = em.create(Assignment, { | ||||||
|         id: 21001, |         id: 21001, | ||||||
|         within: classes[1], |         within: classes[1], | ||||||
|         title: 'tool', |         title: 'tool', | ||||||
|  | @ -24,7 +26,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign | ||||||
|         groups: [], |         groups: [], | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const assignment03 = em.create(Assignment, { |     assignment03 = em.create(Assignment, { | ||||||
|         id: 21002, |         id: 21002, | ||||||
|         within: classes[0], |         within: classes[0], | ||||||
|         title: 'delete', |         title: 'delete', | ||||||
|  | @ -34,7 +36,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign | ||||||
|         groups: [], |         groups: [], | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const assignment04 = em.create(Assignment, { |     assignment04 = em.create(Assignment, { | ||||||
|         id: 21003, |         id: 21003, | ||||||
|         within: classes[0], |         within: classes[0], | ||||||
|         title: 'another assignment', |         title: 'another assignment', | ||||||
|  | @ -44,5 +46,41 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign | ||||||
|         groups: [], |         groups: [], | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return [assignment01, assignment02, assignment03, assignment04]; |     conditionalPathAssignment = em.create(Assignment, { | ||||||
|  |         within: getClassWithTestleerlingAndTestleerkracht(), | ||||||
|  |         id: 1, | ||||||
|  |         title: 'Assignment: Conditional Learning Path', | ||||||
|  |         description: 'You have to do the testing learning path with a condition.', | ||||||
|  |         learningPathHruid: testLearningPathWithConditions.hruid, | ||||||
|  |         learningPathLanguage: testLearningPathWithConditions.language as Language, | ||||||
|  |         groups: [], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return [assignment01, assignment02, assignment03, assignment04, conditionalPathAssignment]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let assignment01: Assignment; | ||||||
|  | let assignment02: Assignment; | ||||||
|  | let assignment03: Assignment; | ||||||
|  | let assignment04: Assignment; | ||||||
|  | let conditionalPathAssignment: Assignment; | ||||||
|  | 
 | ||||||
|  | export function getAssignment01(): Assignment { | ||||||
|  |     return assignment01; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getAssignment02(): Assignment { | ||||||
|  |     return assignment02; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getAssignment03(): Assignment { | ||||||
|  |     return assignment03; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getAssignment04(): Assignment { | ||||||
|  |     return assignment04; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getConditionalPathAssignment(): Assignment { | ||||||
|  |     return conditionalPathAssignment; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,13 +2,15 @@ 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 { 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[] { | ||||||
|     /* |     /* | ||||||
|      * Group #1 for Assignment #1 in class 'id01' |      * Group #1 for Assignment #1 in class 'id01' | ||||||
|      * => Assigned to do learning path 'id02' |      * => Assigned to do learning path 'id02' | ||||||
|      */ |      */ | ||||||
|     const group01 = em.create(Group, { |     group01 = em.create(Group, { | ||||||
|         assignment: assignments[0], |         assignment: assignments[0], | ||||||
|         groupNumber: 21001, |         groupNumber: 21001, | ||||||
|         members: students.slice(0, 2), |         members: students.slice(0, 2), | ||||||
|  | @ -18,7 +20,7 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen | ||||||
|      * Group #2 for Assignment #1 in class 'id01' |      * Group #2 for Assignment #1 in class 'id01' | ||||||
|      * => Assigned to do learning path 'id02' |      * => Assigned to do learning path 'id02' | ||||||
|      */ |      */ | ||||||
|     const group02 = em.create(Group, { |     group02 = em.create(Group, { | ||||||
|         assignment: assignments[0], |         assignment: assignments[0], | ||||||
|         groupNumber: 21002, |         groupNumber: 21002, | ||||||
|         members: students.slice(2, 4), |         members: students.slice(2, 4), | ||||||
|  | @ -28,7 +30,7 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen | ||||||
|      * Group #3 for Assignment #1 in class 'id01' |      * Group #3 for Assignment #1 in class 'id01' | ||||||
|      * => Assigned to do learning path 'id02' |      * => Assigned to do learning path 'id02' | ||||||
|      */ |      */ | ||||||
|     const group03 = em.create(Group, { |     group03 = em.create(Group, { | ||||||
|         assignment: assignments[0], |         assignment: assignments[0], | ||||||
|         groupNumber: 21003, |         groupNumber: 21003, | ||||||
|         members: students.slice(4, 6), |         members: students.slice(4, 6), | ||||||
|  | @ -38,7 +40,7 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen | ||||||
|      * Group #4 for Assignment #2 in class 'id02' |      * Group #4 for Assignment #2 in class 'id02' | ||||||
|      * => Assigned to do learning path 'id01' |      * => Assigned to do learning path 'id01' | ||||||
|      */ |      */ | ||||||
|     const group04 = em.create(Group, { |     group04 = em.create(Group, { | ||||||
|         assignment: assignments[1], |         assignment: assignments[1], | ||||||
|         groupNumber: 21004, |         groupNumber: 21004, | ||||||
|         members: students.slice(3, 4), |         members: students.slice(3, 4), | ||||||
|  | @ -48,11 +50,51 @@ export function makeTestGroups(em: EntityManager, students: Student[], assignmen | ||||||
|      * Group #5 for Assignment #4 in class 'id01' |      * Group #5 for Assignment #4 in class 'id01' | ||||||
|      * => Assigned to do learning path 'id01' |      * => Assigned to do learning path 'id01' | ||||||
|      */ |      */ | ||||||
|     const group05 = em.create(Group, { |     group05 = em.create(Group, { | ||||||
|         assignment: assignments[3], |         assignment: assignments[3], | ||||||
|         groupNumber: 21001, |         groupNumber: 21001, | ||||||
|         members: students.slice(0, 2), |         members: students.slice(0, 2), | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return [group01, group02, group03, group04, group05]; |     /** | ||||||
|  |      * Group 1 for the assignment of the testing learning path with conditions. | ||||||
|  |      */ | ||||||
|  |     group1ConditionalLearningPath = em.create(Group, { | ||||||
|  |         assignment: getConditionalPathAssignment(), | ||||||
|  |         groupNumber: 1, | ||||||
|  |         members: [getTestleerling1()], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return [group01, group02, group03, group04, group05, group1ConditionalLearningPath]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let group01: Group; | ||||||
|  | let group02: Group; | ||||||
|  | let group03: Group; | ||||||
|  | let group04: Group; | ||||||
|  | let group05: Group; | ||||||
|  | let group1ConditionalLearningPath: Group; | ||||||
|  | 
 | ||||||
|  | export function getTestGroup01(): Group { | ||||||
|  |     return group01; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getTestGroup02(): Group { | ||||||
|  |     return group02; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getTestGroup03(): Group { | ||||||
|  |     return group03; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getTestGroup04(): Group { | ||||||
|  |     return group04; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getTestGroup05(): Group { | ||||||
|  |     return group05; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getGroup1ConditionalLearningPath(): Group { | ||||||
|  |     return group1ConditionalLearningPath; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,12 +2,14 @@ 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 { 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); | ||||||
|     const teacherClass01: Teacher[] = teachers.slice(4, 5); |     const teacherClass01: Teacher[] = teachers.slice(4, 5); | ||||||
| 
 | 
 | ||||||
|     const class01 = em.create(Class, { |     class01 = em.create(Class, { | ||||||
|         classId: '8764b861-90a6-42e5-9732-c0d9eb2f55f9', |         classId: '8764b861-90a6-42e5-9732-c0d9eb2f55f9', | ||||||
|         displayName: 'class01', |         displayName: 'class01', | ||||||
|         teachers: teacherClass01, |         teachers: teacherClass01, | ||||||
|  | @ -17,7 +19,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers | ||||||
|     const studentsClass02: Student[] = students.slice(0, 2).concat(students.slice(3, 4)); |     const studentsClass02: Student[] = students.slice(0, 2).concat(students.slice(3, 4)); | ||||||
|     const teacherClass02: Teacher[] = teachers.slice(1, 2); |     const teacherClass02: Teacher[] = teachers.slice(1, 2); | ||||||
| 
 | 
 | ||||||
|     const class02 = em.create(Class, { |     class02 = em.create(Class, { | ||||||
|         classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', |         classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', | ||||||
|         displayName: 'class02', |         displayName: 'class02', | ||||||
|         teachers: teacherClass02, |         teachers: teacherClass02, | ||||||
|  | @ -27,7 +29,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers | ||||||
|     const studentsClass03: Student[] = students.slice(1, 4); |     const studentsClass03: Student[] = students.slice(1, 4); | ||||||
|     const teacherClass03: Teacher[] = teachers.slice(2, 3); |     const teacherClass03: Teacher[] = teachers.slice(2, 3); | ||||||
| 
 | 
 | ||||||
|     const class03 = em.create(Class, { |     class03 = em.create(Class, { | ||||||
|         classId: '80dcc3e0-1811-4091-9361-42c0eee91cfa', |         classId: '80dcc3e0-1811-4091-9361-42c0eee91cfa', | ||||||
|         displayName: 'class03', |         displayName: 'class03', | ||||||
|         teachers: teacherClass03, |         teachers: teacherClass03, | ||||||
|  | @ -37,12 +39,45 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers | ||||||
|     const studentsClass04: Student[] = students.slice(0, 2); |     const studentsClass04: Student[] = students.slice(0, 2); | ||||||
|     const teacherClass04: Teacher[] = teachers.slice(2, 3); |     const teacherClass04: Teacher[] = teachers.slice(2, 3); | ||||||
| 
 | 
 | ||||||
|     const class04 = em.create(Class, { |     class04 = em.create(Class, { | ||||||
|         classId: '33d03536-83b8-4880-9982-9bbf2f908ddf', |         classId: '33d03536-83b8-4880-9982-9bbf2f908ddf', | ||||||
|         displayName: 'class04', |         displayName: 'class04', | ||||||
|         teachers: teacherClass04, |         teachers: teacherClass04, | ||||||
|         students: studentsClass04, |         students: studentsClass04, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return [class01, class02, class03, class04]; |     classWithTestleerlingAndTestleerkracht = em.create(Class, { | ||||||
|  |         classId: 'a75298b5-18aa-471d-8eeb-5d77eb989393', | ||||||
|  |         displayName: 'Testklasse', | ||||||
|  |         teachers: [getTestleerkracht1()], | ||||||
|  |         students: [getTestleerling1()], | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return [class01, class02, class03, class04, classWithTestleerlingAndTestleerkracht]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let class01: Class; | ||||||
|  | let class02: Class; | ||||||
|  | let class03: Class; | ||||||
|  | let class04: Class; | ||||||
|  | let classWithTestleerlingAndTestleerkracht: Class; | ||||||
|  | 
 | ||||||
|  | export function getClass01(): Class { | ||||||
|  |     return class01; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getClass02(): Class { | ||||||
|  |     return class02; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getClass03(): Class { | ||||||
|  |     return class03; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getClass04(): Class { | ||||||
|  |     return class04; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getClassWithTestleerlingAndTestleerkracht(): Class { | ||||||
|  |     return classWithTestleerlingAndTestleerkracht; | ||||||
| } | } | ||||||
|  |  | ||||||
| Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB | 
| Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB | 
|  | @ -0,0 +1,2 @@ | ||||||
|  | ::Reflection:: | ||||||
|  | Reflect on this learning path. What have you learned today? {} | ||||||
|  | @ -0,0 +1,7 @@ | ||||||
|  | <div class="learning-object-gift"> | ||||||
|  |     <div id="gift-q1" class="gift-question gift-question-type-Essay"> | ||||||
|  |         <h2 id="gift-q1-title" class="gift-title">Reflection</h2> | ||||||
|  |         <p id="gift-q1-stem" class="gift-stem">Reflect on this learning path. What have you learned today?</p> | ||||||
|  |         <textarea id="gift-q1-answer" class="gift-essay-answer"></textarea> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| ::MC basic:: | ::Self-evaluation:: | ||||||
| Are you following along well with the class?  { | Are you following along well?  { | ||||||
| 	~No, it's very difficult to follow along. | 	~No, it's very difficult to follow along. | ||||||
|     =Yes, no problem! |     =Yes, no problem! | ||||||
| } | } | ||||||
|  | @ -1,14 +1,14 @@ | ||||||
| <div class="learning-object-gift"> | <div class="learning-object-gift"> | ||||||
|     <div id="gift-q1" class="gift-question"> |     <div id="gift-q1" class="gift-question gift-question-type-MC"> | ||||||
|         <h2 id="gift-q1-title" class="gift-title">MC basic</h2> |         <h2 id="gift-q1-title" class="gift-title">Self-evaluation</h2> | ||||||
|         <p id="gift-q1-stem" class="gift-stem">Are you following along well with the class?</p> |         <p id="gift-q1-stem" class="gift-stem">Are you following along well?</p> | ||||||
|         <div class="gift-choice-div"> |         <div class="gift-choice-div"> | ||||||
|             <input value="0" name="gift-q1-choices" id="gift-q1-choice-0" type="radio"> |             <input value="0" name="gift-q1-choices" id="gift-q1-choice-0" type="radio"> | ||||||
|             <label for="gift-q1-choice-0">[object Object]</label> |             <label for="gift-q1-choice-0">No, it's very difficult to follow along.</label> | ||||||
|         </div> |         </div> | ||||||
|         <div class="gift-choice-div"> |         <div class="gift-choice-div"> | ||||||
|             <input value="1" name="gift-q1-choices" id="gift-q1-choice-1" type="radio"> |             <input value="1" name="gift-q1-choices" id="gift-q1-choice-1" type="radio"> | ||||||
|             <label for="gift-q1-choice-1">[object Object]</label> |             <label for="gift-q1-choice-1">Yes, no problem!</label> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
| </div> | </div> | ||||||
|  | @ -1,16 +1,49 @@ | ||||||
| import { EntityManager } 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 { loadTestAsset } from '../../test-utils/load-test-asset'; | ||||||
|  | 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(); | ||||||
|     returnValue.callbackSchema = ''; |     returnValue.callbackSchema = ''; | ||||||
|     returnValue.callbackUrl = ''; |     returnValue.callbackUrl = ''; | ||||||
| 
 | 
 | ||||||
|     const learningObject01 = em.create(LearningObject, { |     const learningObject01 = em.create(LearningObject, testLearningObject01); | ||||||
|         hruid: 'id01', |     const learningObject02 = em.create(LearningObject, testLearningObject02); | ||||||
|  |     const learningObject03 = em.create(LearningObject, testLearningObject03); | ||||||
|  |     const learningObject04 = em.create(LearningObject, testLearningObject04); | ||||||
|  |     const learningObject05 = em.create(LearningObject, testLearningObject05); | ||||||
|  | 
 | ||||||
|  |     const learningObjectMultipleChoice = em.create(LearningObject, testLearningObjectMultipleChoice); | ||||||
|  |     const learningObjectEssayQuestion = em.create(LearningObject, testLearningObjectEssayQuestion); | ||||||
|  | 
 | ||||||
|  |     const learningObjectPnNotebooks = em.create(LearningObject, testLearningObjectPnNotebooks); | ||||||
|  | 
 | ||||||
|  |     return [ | ||||||
|  |         learningObject01, | ||||||
|  |         learningObject02, | ||||||
|  |         learningObject03, | ||||||
|  |         learningObject04, | ||||||
|  |         learningObject05, | ||||||
|  |         learningObjectMultipleChoice, | ||||||
|  |         learningObjectEssayQuestion, | ||||||
|  |         learningObjectPnNotebooks, | ||||||
|  |     ]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function createReturnValue(): ReturnValue { | ||||||
|  |     const returnValue: ReturnValue = new ReturnValue(); | ||||||
|  |     returnValue.callbackSchema = '[]'; | ||||||
|  |     returnValue.callbackUrl = '%SUBMISSION%'; | ||||||
|  |     return returnValue; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const testLearningObject01: RequiredEntityData<LearningObject> = { | ||||||
|  |     hruid: `${getEnvVar(envVars.UserContentPrefix)}id01`, | ||||||
|     language: Language.English, |     language: Language.English, | ||||||
|     version: 1, |     version: 1, | ||||||
|     admins: [], |     admins: [], | ||||||
|  | @ -18,21 +51,23 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | ||||||
|     description: 'debute', |     description: 'debute', | ||||||
|     contentType: DwengoContentType.TEXT_MARKDOWN, |     contentType: DwengoContentType.TEXT_MARKDOWN, | ||||||
|     keywords: [], |     keywords: [], | ||||||
|  |     uuid: v4(), | ||||||
|  |     targetAges: [16, 17, 18], | ||||||
|     teacherExclusive: false, |     teacherExclusive: false, | ||||||
|     skosConcepts: [], |     skosConcepts: [], | ||||||
|     educationalGoals: [], |     educationalGoals: [], | ||||||
|     copyright: '', |     copyright: '', | ||||||
|     license: '', |     license: '', | ||||||
|     estimatedTime: 45, |     estimatedTime: 45, | ||||||
|         returnValue: returnValue, |     returnValue: createReturnValue(), | ||||||
|     available: true, |     available: true, | ||||||
|     contentLocation: '', |     contentLocation: '', | ||||||
|     attachments: [], |     attachments: [], | ||||||
|     content: Buffer.from("there's a shadow just behind me, shrouding every step i take, making every promise empty pointing every finger at me"), |     content: Buffer.from("there's a shadow just behind me, shrouding every step i take, making every promise empty pointing every finger at me"), | ||||||
|     }); | }; | ||||||
| 
 | 
 | ||||||
|     const learningObject02 = em.create(LearningObject, { | export const testLearningObject02: RequiredEntityData<LearningObject> = { | ||||||
|         hruid: 'id02', |     hruid: `${getEnvVar(envVars.UserContentPrefix)}id02`, | ||||||
|     language: Language.English, |     language: Language.English, | ||||||
|     version: 1, |     version: 1, | ||||||
|     admins: [], |     admins: [], | ||||||
|  | @ -46,17 +81,17 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | ||||||
|     copyright: '', |     copyright: '', | ||||||
|     license: '', |     license: '', | ||||||
|     estimatedTime: 80, |     estimatedTime: 80, | ||||||
|         returnValue: returnValue, |     returnValue: createReturnValue(), | ||||||
|     available: true, |     available: true, | ||||||
|     contentLocation: '', |     contentLocation: '', | ||||||
|     attachments: [], |     attachments: [], | ||||||
|     content: Buffer.from( |     content: Buffer.from( | ||||||
|         "I've been crawling on my belly clearing out what could've been I've been wallowing in my own confused and insecure delusions" |         "I've been crawling on my belly clearing out what could've been I've been wallowing in my own confused and insecure delusions" | ||||||
|     ), |     ), | ||||||
|     }); | }; | ||||||
| 
 | 
 | ||||||
|     const learningObject03 = em.create(LearningObject, { | export const testLearningObject03: RequiredEntityData<LearningObject> = { | ||||||
|         hruid: 'id03', |     hruid: `${getEnvVar(envVars.UserContentPrefix)}id03`, | ||||||
|     language: Language.English, |     language: Language.English, | ||||||
|     version: 1, |     version: 1, | ||||||
|     admins: [], |     admins: [], | ||||||
|  | @ -70,7 +105,7 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | ||||||
|     copyright: '', |     copyright: '', | ||||||
|     license: '', |     license: '', | ||||||
|     estimatedTime: 55, |     estimatedTime: 55, | ||||||
|         returnValue: returnValue, |     returnValue: createReturnValue(), | ||||||
|     available: true, |     available: true, | ||||||
|     contentLocation: '', |     contentLocation: '', | ||||||
|     attachments: [], |     attachments: [], | ||||||
|  | @ -80,10 +115,10 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | ||||||
|          come back and see me later next patient please \ |          come back and see me later next patient please \ | ||||||
|          send in another victim of industrial disease' |          send in another victim of industrial disease' | ||||||
|     ), |     ), | ||||||
|     }); | }; | ||||||
| 
 | 
 | ||||||
|     const learningObject04 = em.create(LearningObject, { | export const testLearningObject04: RequiredEntityData<LearningObject> = { | ||||||
|         hruid: 'id04', |     hruid: `${getEnvVar(envVars.UserContentPrefix)}id04`, | ||||||
|     language: Language.English, |     language: Language.English, | ||||||
|     version: 1, |     version: 1, | ||||||
|     admins: [], |     admins: [], | ||||||
|  | @ -97,7 +132,7 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | ||||||
|     copyright: '', |     copyright: '', | ||||||
|     license: '', |     license: '', | ||||||
|     estimatedTime: 55, |     estimatedTime: 55, | ||||||
|         returnValue: returnValue, |     returnValue: createReturnValue(), | ||||||
|     available: true, |     available: true, | ||||||
|     contentLocation: '', |     contentLocation: '', | ||||||
|     attachments: [], |     attachments: [], | ||||||
|  | @ -107,10 +142,10 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | ||||||
|          I had the one-arm bandit fever \ |          I had the one-arm bandit fever \ | ||||||
|          There was an arrow through my heart and my soul' |          There was an arrow through my heart and my soul' | ||||||
|     ), |     ), | ||||||
|     }); | }; | ||||||
| 
 | 
 | ||||||
|     const learningObject05 = em.create(LearningObject, { | export const testLearningObject05: RequiredEntityData<LearningObject> = { | ||||||
|         hruid: 'id05', |     hruid: `${getEnvVar(envVars.UserContentPrefix)}id05`, | ||||||
|     language: Language.English, |     language: Language.English, | ||||||
|     version: 1, |     version: 1, | ||||||
|     admins: [], |     admins: [], | ||||||
|  | @ -124,12 +159,103 @@ export function makeTestLearningObjects(em: EntityManager): LearningObject[] { | ||||||
|     copyright: '', |     copyright: '', | ||||||
|     license: '', |     license: '', | ||||||
|     estimatedTime: 55, |     estimatedTime: 55, | ||||||
|         returnValue: returnValue, |     returnValue: createReturnValue(), | ||||||
|     available: true, |     available: true, | ||||||
|     contentLocation: '', |     contentLocation: '', | ||||||
|     attachments: [], |     attachments: [], | ||||||
|     content: Buffer.from('calling Elvis, is anybody home, calling elvis, I am here all alone'), |     content: Buffer.from('calling Elvis, is anybody home, calling elvis, I am here all alone'), | ||||||
|     }); | }; | ||||||
| 
 | 
 | ||||||
|     return [learningObject01, learningObject02, learningObject03, learningObject04, learningObject05]; | export const testLearningObjectMultipleChoice: RequiredEntityData<LearningObject> = { | ||||||
| } |     hruid: `${getEnvVar(envVars.UserContentPrefix)}test_multiple_choice`, | ||||||
|  |     language: Language.English, | ||||||
|  |     version: 1, | ||||||
|  |     title: 'Self-evaluation', | ||||||
|  |     description: "Time to evaluate how well you understand what you've learned so far.", | ||||||
|  |     keywords: ['test'], | ||||||
|  |     teacherExclusive: false, | ||||||
|  |     skosConcepts: [], | ||||||
|  |     educationalGoals: [], | ||||||
|  |     copyright: 'Groep 1 SEL-2 2025', | ||||||
|  |     license: 'CC0', | ||||||
|  |     difficulty: 1, | ||||||
|  |     estimatedTime: 1, | ||||||
|  |     attachments: [], | ||||||
|  |     available: true, | ||||||
|  |     targetAges: [10, 11, 12, 13, 14, 15, 16, 17, 18], | ||||||
|  |     admins: [], | ||||||
|  |     contentType: DwengoContentType.GIFT, | ||||||
|  |     content: loadTestAsset('content/learning-object-resources/test_multiple_choice/content.txt'), | ||||||
|  |     returnValue: { | ||||||
|  |         callbackUrl: `%SUBMISSION%`, | ||||||
|  |         callbackSchema: '["antwoord vraag 1"]', | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const testLearningObjectEssayQuestion: RequiredEntityData<LearningObject> = { | ||||||
|  |     hruid: `${getEnvVar(envVars.UserContentPrefix)}test_essay_question`, | ||||||
|  |     language: Language.English, | ||||||
|  |     version: 1, | ||||||
|  |     title: 'Reflection', | ||||||
|  |     description: 'Reflect on your learning progress.', | ||||||
|  |     keywords: ['test'], | ||||||
|  |     teacherExclusive: false, | ||||||
|  |     skosConcepts: [], | ||||||
|  |     educationalGoals: [], | ||||||
|  |     copyright: 'Groep 1 SEL-2 2025', | ||||||
|  |     license: 'CC0', | ||||||
|  |     difficulty: 1, | ||||||
|  |     estimatedTime: 1, | ||||||
|  |     attachments: [], | ||||||
|  |     available: true, | ||||||
|  |     targetAges: [10, 11, 12, 13, 14, 15, 16, 17, 18], | ||||||
|  |     admins: [], | ||||||
|  |     contentType: DwengoContentType.GIFT, | ||||||
|  |     content: loadTestAsset('content/learning-object-resources/test_essay_question/content.txt'), | ||||||
|  |     returnValue: { | ||||||
|  |         callbackUrl: `%SUBMISSION%`, | ||||||
|  |         callbackSchema: '["antwoord vraag 1"]', | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const testLearningObjectPnNotebooks: RequiredEntityData<LearningObject> = { | ||||||
|  |     hruid: `${getEnvVar(envVars.UserContentPrefix)}pn_werkingnotebooks`, | ||||||
|  |     version: 3, | ||||||
|  |     language: Language.Dutch, | ||||||
|  |     title: 'Werken met notebooks', | ||||||
|  |     description: 'Leren werken met notebooks', | ||||||
|  |     keywords: ['Python', 'KIKS', 'Wiskunde', 'STEM', 'AI'], | ||||||
|  |     targetAges: [14, 15, 16, 17, 18], | ||||||
|  |     admins: [], | ||||||
|  |     copyright: 'dwengo', | ||||||
|  |     educationalGoals: [], | ||||||
|  |     license: 'dwengo', | ||||||
|  |     contentType: DwengoContentType.TEXT_MARKDOWN, | ||||||
|  |     difficulty: 3, | ||||||
|  |     estimatedTime: 10, | ||||||
|  |     uuid: '2adf9929-b424-4650-bf60-186f730d38ab', | ||||||
|  |     teacherExclusive: false, | ||||||
|  |     skosConcepts: [ | ||||||
|  |         '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-computers-en-systemen', | ||||||
|  |     ], | ||||||
|  |     attachments: [ | ||||||
|  |         { | ||||||
|  |             name: 'dwengo.png', | ||||||
|  |             mimeType: 'image/png', | ||||||
|  |             content: loadTestAsset('/content/learning-object-resources/pn_werkingnotebooks/dwengo.png'), | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name: 'Knop.png', | ||||||
|  |             mimeType: 'image/png', | ||||||
|  |             content: loadTestAsset('/content/learning-object-resources/pn_werkingnotebooks/Knop.png'), | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  |     available: false, | ||||||
|  |     content: loadTestAsset('/content/learning-object-resources/pn_werkingnotebooks/content.md'), | ||||||
|  |     returnValue: { | ||||||
|  |         callbackUrl: '%SUBMISSION%', | ||||||
|  |         callbackSchema: '[]', | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -1,100 +1,236 @@ | ||||||
| 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 { LearningPathTransition } from '../../../src/entities/content/learning-path-transition.entity'; | import { mapToLearningPath } from '../../../src/services/learning-paths/learning-path-service'; | ||||||
| import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity'; | import { envVars, getEnvVar } from '../../../src/util/envVars'; | ||||||
|  | import { LearningPath as LearningPathDTO } from '@dwengo-1/common/interfaces/learning-content'; | ||||||
|  | import { | ||||||
|  |     testLearningObject01, | ||||||
|  |     testLearningObject02, | ||||||
|  |     testLearningObject03, | ||||||
|  |     testLearningObject04, | ||||||
|  |     testLearningObject05, | ||||||
|  |     testLearningObjectEssayQuestion, | ||||||
|  |     testLearningObjectMultipleChoice, | ||||||
|  |     testLearningObjectPnNotebooks, | ||||||
|  | } from './learning-objects.testdata'; | ||||||
| 
 | 
 | ||||||
| export function makeTestLearningPaths(em: EntityManager): LearningPath[] { | export function makeTestLearningPaths(_em: EntityManager): LearningPath[] { | ||||||
|     const learningPathNode01: LearningPathNode = new LearningPathNode(); |     const learningPath01 = mapToLearningPath(testLearningPath01, []); | ||||||
|     const learningPathNode02: LearningPathNode = new LearningPathNode(); |     const learningPath02 = mapToLearningPath(testLearningPath02, []); | ||||||
|     const learningPathNode03: LearningPathNode = new LearningPathNode(); |  | ||||||
|     const learningPathNode04: LearningPathNode = new LearningPathNode(); |  | ||||||
|     const learningPathNode05: LearningPathNode = new LearningPathNode(); |  | ||||||
| 
 | 
 | ||||||
|     const transitions01: LearningPathTransition = new LearningPathTransition(); |     const partiallyDatabasePartiallyDwengoApiLearningPath = mapToLearningPath(testPartiallyDatabaseAndPartiallyDwengoApiLearningPath, []); | ||||||
|     const transitions02: LearningPathTransition = new LearningPathTransition(); |     const learningPathWithConditions = mapToLearningPath(testLearningPathWithConditions, []); | ||||||
|     const transitions03: LearningPathTransition = new LearningPathTransition(); |  | ||||||
|     const transitions04: LearningPathTransition = new LearningPathTransition(); |  | ||||||
|     const transitions05: LearningPathTransition = new LearningPathTransition(); |  | ||||||
| 
 | 
 | ||||||
|     transitions01.condition = 'true'; |     return [learningPath01, learningPath02, partiallyDatabasePartiallyDwengoApiLearningPath, learningPathWithConditions]; | ||||||
|     transitions01.next = learningPathNode02; | } | ||||||
| 
 | 
 | ||||||
|     transitions02.condition = 'true'; | const nowString = new Date().toString(); | ||||||
|     transitions02.next = learningPathNode02; |  | ||||||
| 
 | 
 | ||||||
|     transitions03.condition = 'true'; | export const testLearningPath01: LearningPathDTO = { | ||||||
|     transitions03.next = learningPathNode04; |     keywords: 'test', | ||||||
| 
 |     target_ages: [16, 17, 18], | ||||||
|     transitions04.condition = 'true'; |     hruid: `${getEnvVar(envVars.UserContentPrefix)}id01`, | ||||||
|     transitions04.next = learningPathNode05; |  | ||||||
| 
 |  | ||||||
|     transitions05.condition = 'true'; |  | ||||||
|     transitions05.next = learningPathNode05; |  | ||||||
| 
 |  | ||||||
|     learningPathNode01.instruction = ''; |  | ||||||
|     learningPathNode01.language = Language.English; |  | ||||||
|     learningPathNode01.learningObjectHruid = 'id01'; |  | ||||||
|     learningPathNode01.startNode = true; |  | ||||||
|     learningPathNode01.transitions = [transitions01]; |  | ||||||
|     learningPathNode01.version = 1; |  | ||||||
| 
 |  | ||||||
|     learningPathNode02.instruction = ''; |  | ||||||
|     learningPathNode02.language = Language.English; |  | ||||||
|     learningPathNode02.learningObjectHruid = 'id02'; |  | ||||||
|     learningPathNode02.startNode = false; |  | ||||||
|     learningPathNode02.transitions = [transitions02]; |  | ||||||
|     learningPathNode02.version = 1; |  | ||||||
| 
 |  | ||||||
|     learningPathNode03.instruction = ''; |  | ||||||
|     learningPathNode03.language = Language.English; |  | ||||||
|     learningPathNode03.learningObjectHruid = 'id03'; |  | ||||||
|     learningPathNode03.startNode = true; |  | ||||||
|     learningPathNode03.transitions = [transitions03]; |  | ||||||
|     learningPathNode03.version = 1; |  | ||||||
| 
 |  | ||||||
|     learningPathNode04.instruction = ''; |  | ||||||
|     learningPathNode04.language = Language.English; |  | ||||||
|     learningPathNode04.learningObjectHruid = 'id04'; |  | ||||||
|     learningPathNode04.startNode = false; |  | ||||||
|     learningPathNode04.transitions = [transitions04]; |  | ||||||
|     learningPathNode04.version = 1; |  | ||||||
| 
 |  | ||||||
|     learningPathNode05.instruction = ''; |  | ||||||
|     learningPathNode05.language = Language.English; |  | ||||||
|     learningPathNode05.learningObjectHruid = 'id05'; |  | ||||||
|     learningPathNode05.startNode = false; |  | ||||||
|     learningPathNode05.transitions = [transitions05]; |  | ||||||
|     learningPathNode05.version = 1; |  | ||||||
| 
 |  | ||||||
|     const nodes01: LearningPathNode[] = [ |  | ||||||
|         // LearningPathNode01,
 |  | ||||||
|         // LearningPathNode02,
 |  | ||||||
|     ]; |  | ||||||
|     const learningPath01 = em.create(LearningPath, { |  | ||||||
|         hruid: 'id01', |  | ||||||
|     language: Language.English, |     language: Language.English, | ||||||
|         admins: [], |  | ||||||
|     title: 'repertoire Tool', |     title: 'repertoire Tool', | ||||||
|     description: 'all about Tool', |     description: 'all about Tool', | ||||||
|         image: null, |     nodes: [ | ||||||
|         nodes: nodes01, |         { | ||||||
|     }); |             learningobject_hruid: testLearningObject01.hruid, | ||||||
|  |             language: testLearningObject01.language, | ||||||
|  |             version: testLearningObject01.version, | ||||||
|  |             start_node: true, | ||||||
|  |             created_at: nowString, | ||||||
|  |             updatedAt: nowString, | ||||||
|  |             transitions: [ | ||||||
|  |                 { | ||||||
|  |                     next: { | ||||||
|  |                         hruid: testLearningObject02.hruid, | ||||||
|  |                         language: testLearningObject02.language, | ||||||
|  |                         version: testLearningObject02.version, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             learningobject_hruid: testLearningObject02.hruid, | ||||||
|  |             language: testLearningObject02.language, | ||||||
|  |             version: testLearningObject02.version, | ||||||
|  |             created_at: nowString, | ||||||
|  |             updatedAt: nowString, | ||||||
|  |             transitions: [], | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
|     const nodes02: LearningPathNode[] = [ | export const testLearningPath02: LearningPathDTO = { | ||||||
|         // LearningPathNode03,
 |     keywords: 'test', | ||||||
|         // LearningPathNode04,
 |     target_ages: [16, 17, 18], | ||||||
|         // LearningPathNode05,
 |     hruid: `${getEnvVar(envVars.UserContentPrefix)}id02`, | ||||||
|     ]; |  | ||||||
|     const learningPath02 = em.create(LearningPath, { |  | ||||||
|         hruid: 'id02', |  | ||||||
|     language: Language.English, |     language: Language.English, | ||||||
|         admins: [], |  | ||||||
|     title: 'repertoire Dire Straits', |     title: 'repertoire Dire Straits', | ||||||
|     description: 'all about Dire Straits', |     description: 'all about Dire Straits', | ||||||
|         image: null, |     nodes: [ | ||||||
|         nodes: nodes02, |         { | ||||||
|     }); |             learningobject_hruid: testLearningObject03.hruid, | ||||||
|  |             language: testLearningObject03.language, | ||||||
|  |             version: testLearningObject03.version, | ||||||
|  |             start_node: true, | ||||||
|  |             created_at: nowString, | ||||||
|  |             updatedAt: nowString, | ||||||
|  |             transitions: [ | ||||||
|  |                 { | ||||||
|  |                     next: { | ||||||
|  |                         hruid: testLearningObject04.hruid, | ||||||
|  |                         language: testLearningObject04.language, | ||||||
|  |                         version: testLearningObject04.version, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             learningobject_hruid: testLearningObject04.hruid, | ||||||
|  |             language: testLearningObject04.language, | ||||||
|  |             version: testLearningObject04.version, | ||||||
|  |             created_at: nowString, | ||||||
|  |             updatedAt: nowString, | ||||||
|  |             transitions: [ | ||||||
|  |                 { | ||||||
|  |                     next: { | ||||||
|  |                         hruid: testLearningObject05.hruid, | ||||||
|  |                         language: testLearningObject05.language, | ||||||
|  |                         version: testLearningObject05.version, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             learningobject_hruid: testLearningObject05.hruid, | ||||||
|  |             language: testLearningObject05.language, | ||||||
|  |             version: testLearningObject05.version, | ||||||
|  |             created_at: nowString, | ||||||
|  |             updatedAt: nowString, | ||||||
|  |             transitions: [], | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
|     return [learningPath01, learningPath02]; | export const testPartiallyDatabaseAndPartiallyDwengoApiLearningPath: LearningPathDTO = { | ||||||
| } |     hruid: `${getEnvVar(envVars.UserContentPrefix)}pn_werking`, | ||||||
|  |     title: 'Werken met notebooks', | ||||||
|  |     language: Language.Dutch, | ||||||
|  |     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', | ||||||
|  |     target_ages: [14, 15, 16, 17, 18], | ||||||
|  |     nodes: [ | ||||||
|  |         { | ||||||
|  |             learningobject_hruid: testLearningObjectPnNotebooks.hruid, | ||||||
|  |             language: testLearningObjectPnNotebooks.language, | ||||||
|  |             version: testLearningObjectPnNotebooks.version, | ||||||
|  |             start_node: true, | ||||||
|  |             created_at: nowString, | ||||||
|  |             updatedAt: nowString, | ||||||
|  |             transitions: [ | ||||||
|  |                 { | ||||||
|  |                     default: true, | ||||||
|  |                     next: { | ||||||
|  |                         hruid: 'pn_werkingnotebooks2', | ||||||
|  |                         language: Language.Dutch, | ||||||
|  |                         version: 3, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             learningobject_hruid: 'pn_werkingnotebooks2', | ||||||
|  |             language: Language.Dutch, | ||||||
|  |             version: 3, | ||||||
|  |             created_at: nowString, | ||||||
|  |             updatedAt: nowString, | ||||||
|  |             transitions: [ | ||||||
|  |                 { | ||||||
|  |                     default: true, | ||||||
|  |                     next: { | ||||||
|  |                         hruid: 'pn_werkingnotebooks3', | ||||||
|  |                         language: Language.Dutch, | ||||||
|  |                         version: 3, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             learningobject_hruid: 'pn_werkingnotebooks3', | ||||||
|  |             language: Language.Dutch, | ||||||
|  |             version: 3, | ||||||
|  |             created_at: nowString, | ||||||
|  |             updatedAt: nowString, | ||||||
|  |             transitions: [], | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const testLearningPathWithConditions: LearningPathDTO = { | ||||||
|  |     hruid: `${getEnvVar(envVars.UserContentPrefix)}test_conditions`, | ||||||
|  |     language: Language.English, | ||||||
|  |     title: 'Example learning path with conditional transitions', | ||||||
|  |     description: 'This learning path was made for the purpose of testing conditional transitions', | ||||||
|  |     keywords: 'test', | ||||||
|  |     target_ages: [10, 11, 12, 13, 14, 15, 16, 17, 18], | ||||||
|  |     nodes: [ | ||||||
|  |         { | ||||||
|  |             learningobject_hruid: testLearningObjectMultipleChoice.hruid, | ||||||
|  |             language: testLearningObjectMultipleChoice.language, | ||||||
|  |             version: testLearningObjectMultipleChoice.version, | ||||||
|  |             start_node: true, | ||||||
|  |             created_at: nowString, | ||||||
|  |             updatedAt: nowString, | ||||||
|  |             transitions: [ | ||||||
|  |                 { | ||||||
|  |                     // If the answer to the first question was the first one (It's difficult to follow along):
 | ||||||
|  |                     condition: '$[?(@[0] == 0)]', | ||||||
|  |                     next: { | ||||||
|  |                         //... we let the student do an extra exercise.
 | ||||||
|  |                         hruid: testLearningObject01.hruid, | ||||||
|  |                         language: testLearningObject01.language, | ||||||
|  |                         version: testLearningObject01.version, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     // If the answer to the first question was the second one (I can follow along):
 | ||||||
|  |                     condition: '$[?(@[0] == 1)]', | ||||||
|  |                     next: { | ||||||
|  |                         //... we let the student right through to the final question.
 | ||||||
|  |                         hruid: testLearningObjectEssayQuestion.hruid, | ||||||
|  |                         language: testLearningObjectEssayQuestion.language, | ||||||
|  |                         version: testLearningObjectEssayQuestion.version, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             learningobject_hruid: testLearningObject01.hruid, | ||||||
|  |             language: testLearningObject01.language, | ||||||
|  |             version: testLearningObject01.version, | ||||||
|  |             created_at: nowString, | ||||||
|  |             updatedAt: nowString, | ||||||
|  |             transitions: [ | ||||||
|  |                 { | ||||||
|  |                     default: true, | ||||||
|  |                     next: { | ||||||
|  |                         hruid: testLearningObjectEssayQuestion.hruid, | ||||||
|  |                         language: testLearningObjectEssayQuestion.language, | ||||||
|  |                         version: testLearningObjectEssayQuestion.version, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             learningobject_hruid: testLearningObjectEssayQuestion.hruid, | ||||||
|  |             language: testLearningObjectEssayQuestion.language, | ||||||
|  |             version: testLearningObjectEssayQuestion.version, | ||||||
|  |             created_at: nowString, | ||||||
|  |             updatedAt: nowString, | ||||||
|  |             transitions: [], | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -15,7 +15,14 @@ export const TEST_STUDENTS = [ | ||||||
|     { username: 'testleerling1', firstName: 'Gerald', lastName: 'Schmittinger' }, |     { username: 'testleerling1', firstName: 'Gerald', lastName: 'Schmittinger' }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  | let testStudents: Student[]; | ||||||
|  | 
 | ||||||
| // 🏗️ Functie die ORM entities maakt uit de data array
 | // 🏗️ Functie die ORM entities maakt uit de data array
 | ||||||
| export function makeTestStudents(em: EntityManager): Student[] { | export function makeTestStudents(em: EntityManager): Student[] { | ||||||
|     return TEST_STUDENTS.map((data) => em.create(Student, data)); |     testStudents = TEST_STUDENTS.map((data) => em.create(Student, data)); | ||||||
|  |     return testStudents; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getTestleerling1(): Student { | ||||||
|  |     return testStudents.find((it) => it.username === 'testleerling1'); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,37 +2,63 @@ import { Teacher } from '../../../src/entities/users/teacher.entity'; | ||||||
| import { EntityManager } from '@mikro-orm/core'; | import { EntityManager } from '@mikro-orm/core'; | ||||||
| 
 | 
 | ||||||
| export function makeTestTeachers(em: EntityManager): Teacher[] { | export function makeTestTeachers(em: EntityManager): Teacher[] { | ||||||
|     const teacher01 = em.create(Teacher, { |     teacher01 = em.create(Teacher, { | ||||||
|         username: 'FooFighters', |         username: 'FooFighters', | ||||||
|         firstName: 'Dave', |         firstName: 'Dave', | ||||||
|         lastName: 'Grohl', |         lastName: 'Grohl', | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const teacher02 = em.create(Teacher, { |     teacher02 = em.create(Teacher, { | ||||||
|         username: 'LimpBizkit', |         username: 'LimpBizkit', | ||||||
|         firstName: 'Fred', |         firstName: 'Fred', | ||||||
|         lastName: 'Durst', |         lastName: 'Durst', | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const teacher03 = em.create(Teacher, { |     teacher03 = em.create(Teacher, { | ||||||
|         username: 'Staind', |         username: 'Staind', | ||||||
|         firstName: 'Aaron', |         firstName: 'Aaron', | ||||||
|         lastName: 'Lewis', |         lastName: 'Lewis', | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     // Should not be used, gets deleted in a unit test
 |     // Should not be used, gets deleted in a unit test
 | ||||||
|     const teacher04 = em.create(Teacher, { |     teacher04 = em.create(Teacher, { | ||||||
|         username: 'ZesdeMetaal', |         username: 'ZesdeMetaal', | ||||||
|         firstName: 'Wannes', |         firstName: 'Wannes', | ||||||
|         lastName: 'Cappelle', |         lastName: 'Cappelle', | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     // Makes sure when logged in as testleerkracht1, there exists a corresponding user
 |     // Makes sure when logged in as testleerkracht1, there exists a corresponding user
 | ||||||
|     const teacher05 = em.create(Teacher, { |     testleerkracht1 = em.create(Teacher, { | ||||||
|         username: 'testleerkracht1', |         username: 'testleerkracht1', | ||||||
|         firstName: 'Bob', |         firstName: 'Kris', | ||||||
|         lastName: 'Dylan', |         lastName: 'Coolsaet', | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return [teacher01, teacher02, teacher03, teacher04, teacher05]; |     return [teacher01, teacher02, teacher03, teacher04, testleerkracht1]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | let teacher01: Teacher; | ||||||
|  | let teacher02: Teacher; | ||||||
|  | let teacher03: Teacher; | ||||||
|  | let teacher04: Teacher; | ||||||
|  | let testleerkracht1: Teacher; | ||||||
|  | 
 | ||||||
|  | export function getTeacher01(): Teacher { | ||||||
|  |     return teacher01; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getTeacher02(): Teacher { | ||||||
|  |     return teacher02; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getTeacher03(): Teacher { | ||||||
|  |     return teacher03; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getTeacher04(): Teacher { | ||||||
|  |     return teacher04; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function getTestleerkracht1(): Teacher { | ||||||
|  |     return testleerkracht1; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ import { makeTestStudents } from '../tests/test_assets/users/students.testdata.j | ||||||
| import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js'; | import { makeTestTeachers } from '../tests/test_assets/users/teachers.testdata.js'; | ||||||
| import { getLogger, Logger } from '../src/logging/initalize.js'; | import { getLogger, Logger } from '../src/logging/initalize.js'; | ||||||
| import { Collection } from '@mikro-orm/core'; | import { Collection } from '@mikro-orm/core'; | ||||||
| import { Group } from '../dist/entities/assignments/group.entity.js'; | import { Group } from '../src/entities/assignments/group.entity'; | ||||||
| 
 | 
 | ||||||
| const logger: Logger = getLogger(); | const logger: Logger = getLogger(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| import { Language } from '../util/language'; | import { Language } from '../util/language'; | ||||||
| 
 | 
 | ||||||
| export interface Transition { | export interface Transition { | ||||||
|     default: boolean; |     default?: boolean; | ||||||
|     _id: string; |     _id?: string; | ||||||
|     next: { |     next: { | ||||||
|         _id: string; |         _id?: string; | ||||||
|         hruid: string; |         hruid: string; | ||||||
|         version: number; |         version: number; | ||||||
|         language: string; |         language: string; | ||||||
|     }; |     }; | ||||||
|  |     condition?: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface LearningObjectIdentifierDTO { | export interface LearningObjectIdentifierDTO { | ||||||
|  | @ -18,7 +19,7 @@ export interface LearningObjectIdentifierDTO { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface LearningObjectNode { | export interface LearningObjectNode { | ||||||
|     _id: string; |     _id?: string; | ||||||
|     learningobject_hruid: string; |     learningobject_hruid: string; | ||||||
|     version: number; |     version: number; | ||||||
|     language: Language; |     language: Language; | ||||||
|  | @ -30,20 +31,20 @@ export interface LearningObjectNode { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface LearningPath { | export interface LearningPath { | ||||||
|     _id: string; |     _id?: string; | ||||||
|     language: string; |     language: string; | ||||||
|     hruid: string; |     hruid: string; | ||||||
|     title: string; |     title: string; | ||||||
|     description: string; |     description: string; | ||||||
|     image?: string; // Image might be missing, so it's optional
 |     image?: string; // Image might be missing, so it's optional
 | ||||||
|     num_nodes: number; |     num_nodes?: number; | ||||||
|     num_nodes_left: number; |     num_nodes_left?: number; | ||||||
|     nodes: LearningObjectNode[]; |     nodes: LearningObjectNode[]; | ||||||
|     keywords: string; |     keywords: string; | ||||||
|     target_ages: number[]; |     target_ages: number[]; | ||||||
|     min_age: number; |     min_age?: number; | ||||||
|     max_age: number; |     max_age?: number; | ||||||
|     __order: number; |     __order?: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface LearningPathIdentifier { | export interface LearningPathIdentifier { | ||||||
|  | @ -62,8 +63,8 @@ export interface ReturnValue { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface LearningObjectMetadata { | export interface LearningObjectMetadata { | ||||||
|     _id: string; |     _id?: string; | ||||||
|     uuid: string; |     uuid?: string; | ||||||
|     hruid: string; |     hruid: string; | ||||||
|     version: number; |     version: number; | ||||||
|     language: Language; |     language: Language; | ||||||
|  | @ -84,7 +85,7 @@ export interface LearningObjectMetadata { | ||||||
| 
 | 
 | ||||||
| export interface FilteredLearningObject { | export interface FilteredLearningObject { | ||||||
|     key: string; |     key: string; | ||||||
|     _id: string; |     _id?: string; | ||||||
|     uuid: string; |     uuid: string; | ||||||
|     version: number; |     version: number; | ||||||
|     title: string; |     title: string; | ||||||
|  |  | ||||||
|  | @ -71,7 +71,7 @@ export default [ | ||||||
|             'init-declarations': 'off', |             'init-declarations': 'off', | ||||||
|             '@typescript-eslint/init-declarations': 'off', |             '@typescript-eslint/init-declarations': 'off', | ||||||
|             'max-params': 'off', |             'max-params': 'off', | ||||||
|             '@typescript-eslint/max-params': ['error', { max: 6 }], |             '@typescript-eslint/max-params': 'off', | ||||||
|             '@typescript-eslint/member-ordering': 'error', |             '@typescript-eslint/member-ordering': 'error', | ||||||
|             '@typescript-eslint/method-signature-style': 'off', // Don't care about TypeScript strict mode.
 |             '@typescript-eslint/method-signature-style': 'off', // Don't care about TypeScript strict mode.
 | ||||||
|             '@typescript-eslint/naming-convention': [ |             '@typescript-eslint/naming-convention': [ | ||||||
|  | @ -87,6 +87,7 @@ export default [ | ||||||
|                     modifiers: ['const'], |                     modifiers: ['const'], | ||||||
|                     format: ['camelCase', 'UPPER_CASE'], |                     format: ['camelCase', 'UPPER_CASE'], | ||||||
|                     trailingUnderscore: 'allow', |                     trailingUnderscore: 'allow', | ||||||
|  |                     leadingUnderscore: 'allow', | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     // Enforce that private members are prefixed with an underscore
 |                     // Enforce that private members are prefixed with an underscore
 | ||||||
|  |  | ||||||
|  | @ -10,12 +10,14 @@ | ||||||
|         "preview": "vite preview", |         "preview": "vite preview", | ||||||
|         "type-check": "vue-tsc --build", |         "type-check": "vue-tsc --build", | ||||||
|         "format": "prettier --write src/", |         "format": "prettier --write src/", | ||||||
|  |         "test:e2e": "playwright test", | ||||||
|         "format-check": "prettier --check src/", |         "format-check": "prettier --check src/", | ||||||
|         "lint": "eslint . --fix", |         "lint": "eslint . --fix", | ||||||
|         "test:unit": "vitest --run", |         "pretest:unit": "tsx ../docs/api/generate.ts && npm run build", | ||||||
|         "test:e2e": "playwright test" |         "test:unit": "vitest --run" | ||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|  |         "@dwengo-1/common": "^0.1.1", | ||||||
|         "@tanstack/react-query": "^5.69.0", |         "@tanstack/react-query": "^5.69.0", | ||||||
|         "@tanstack/vue-query": "^5.69.0", |         "@tanstack/vue-query": "^5.69.0", | ||||||
|         "axios": "^1.8.2", |         "axios": "^1.8.2", | ||||||
|  |  | ||||||
|  | @ -8,20 +8,21 @@ export class LearningPathController extends BaseController { | ||||||
|     constructor() { |     constructor() { | ||||||
|         super("learningPath"); |         super("learningPath"); | ||||||
|     } |     } | ||||||
|     async search(query: string): Promise<LearningPath[]> { |     async search(query: string, language: string): Promise<LearningPath[]> { | ||||||
|         const dtos = await this.get<LearningPathDTO[]>("/", { search: query }); |         const dtos = await this.get<LearningPathDTO[]>("/", { search: query, language }); | ||||||
|         return dtos.map((dto) => LearningPath.fromDTO(dto)); |         return dtos.map((dto) => LearningPath.fromDTO(dto)); | ||||||
|     } |     } | ||||||
|     async getBy( |     async getBy( | ||||||
|         hruid: string, |         hruid: string, | ||||||
|         language: Language, |         language: Language, | ||||||
|         options?: { forGroup?: string; forStudent?: 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: options?.forGroup, |             forGroup: forGroup?.forGroup, | ||||||
|             forStudent: options?.forStudent, |             assignmentNo: forGroup?.assignmentNo, | ||||||
|  |             classId: forGroup?.classId, | ||||||
|         }); |         }); | ||||||
|         return LearningPath.fromDTO(single(dtos)); |         return LearningPath.fromDTO(single(dtos)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -1,5 +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"; | ||||||
| 
 | 
 | ||||||
| export interface SubmissionsResponse { | export interface SubmissionsResponse { | ||||||
|     submissions: SubmissionDTO[] | SubmissionDTOId[]; |     submissions: SubmissionDTO[] | SubmissionDTOId[]; | ||||||
|  | @ -10,16 +11,36 @@ export interface SubmissionResponse { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class SubmissionController extends BaseController { | export class SubmissionController extends BaseController { | ||||||
|     constructor(classid: string, assignmentNumber: number, groupNumber: number) { |     constructor(hruid: string) { | ||||||
|         super(`class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}/submissions`); |         super(`learningObject/${hruid}/submissions`); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getAll(full = true): Promise<SubmissionsResponse> { |     async getAll( | ||||||
|         return this.get<SubmissionsResponse>(`/`, { full }); |         language: Language, | ||||||
|  |         version: number, | ||||||
|  |         classId: string, | ||||||
|  |         assignmentId: number, | ||||||
|  |         groupId?: number, | ||||||
|  |         full = true, | ||||||
|  |     ): Promise<SubmissionsResponse> { | ||||||
|  |         return this.get<SubmissionsResponse>(`/`, { language, version, classId, assignmentId, groupId, full }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getByNumber(submissionNumber: number): Promise<SubmissionResponse> { |     async getByNumber( | ||||||
|         return this.get<SubmissionResponse>(`/${submissionNumber}`); |         language: Language, | ||||||
|  |         version: number, | ||||||
|  |         classId: string, | ||||||
|  |         assignmentId: number, | ||||||
|  |         groupId: number, | ||||||
|  |         submissionNumber: number, | ||||||
|  |     ): Promise<SubmissionResponse> { | ||||||
|  |         return this.get<SubmissionResponse>(`/${submissionNumber}`, { | ||||||
|  |             language, | ||||||
|  |             version, | ||||||
|  |             classId, | ||||||
|  |             assignmentId, | ||||||
|  |             groupId, | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async createSubmission(data: SubmissionDTO): Promise<SubmissionResponse> { |     async createSubmission(data: SubmissionDTO): Promise<SubmissionResponse> { | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| { | { | ||||||
|     "welcome": "Willkommen", |     "welcome": "Willkommen", | ||||||
|     "student": "schüler", |     "student": "Schüler", | ||||||
|     "teacher": "lehrer", |     "teacher": "Lehrer", | ||||||
|     "assignments": "Aufgaben", |     "assignments": "Aufgaben", | ||||||
|     "classes": "Klasses", |     "classes": "Klassen", | ||||||
|     "discussions": "Diskussionen", |     "discussions": "Diskussionen", | ||||||
|     "login": "einloggen", |     "login": "einloggen", | ||||||
|     "logout": "ausloggen", |     "logout": "ausloggen", | ||||||
|     "cancel": "kündigen", |     "cancel": "abbrechen", | ||||||
|     "logoutVerification": "Sind Sie sicher, dass Sie sich abmelden wollen?", |     "logoutVerification": "Sind Sie sicher, dass Sie sich abmelden wollen?", | ||||||
|     "homeTitle": "Unsere Stärken", |     "homeTitle": "Unsere Stärken", | ||||||
|     "homeIntroduction1": "Wir entwickeln innovative Workshops und Bildungsressourcen, die wir in Zusammenarbeit mit Lehrern und Freiwilligen Schülern auf der ganzen Welt zur Verfügung stellen. Unsere Train-the-Trainer-Sitzungen ermöglichen es ihnen, unsere praktischen Workshops an die Schüler weiterzugeben.", |     "homeIntroduction1": "Wir entwickeln innovative Workshops und Bildungsressourcen, die wir in Zusammenarbeit mit Lehrern und Freiwilligen Schülern auf der ganzen Welt zur Verfügung stellen. Unsere Train-the-Trainer-Sitzungen ermöglichen es ihnen, unsere praktischen Workshops an die Schüler weiterzugeben.", | ||||||
|  | @ -23,10 +23,10 @@ | ||||||
|     "submitCode": "senden", |     "submitCode": "senden", | ||||||
|     "members": "Mitglieder", |     "members": "Mitglieder", | ||||||
|     "themes": "Themen", |     "themes": "Themen", | ||||||
|     "choose-theme": "Wähle ein thema", |     "choose-theme": "Wählen Sie ein Thema", | ||||||
|     "choose-age": "Alter auswählen", |     "choose-age": "Alter auswählen", | ||||||
|     "theme-options": { |     "theme-options": { | ||||||
|         "all": "Alle themen", |         "all": "Alle Themen", | ||||||
|         "culture": "Kultur", |         "culture": "Kultur", | ||||||
|         "electricity-and-mechanics": "Elektrizität und Mechanik", |         "electricity-and-mechanics": "Elektrizität und Mechanik", | ||||||
|         "nature-and-climate": "Natur und Klima", |         "nature-and-climate": "Natur und Klima", | ||||||
|  | @ -37,11 +37,11 @@ | ||||||
|         "algorithms": "Algorithmisches Denken" |         "algorithms": "Algorithmisches Denken" | ||||||
|     }, |     }, | ||||||
|     "age-options": { |     "age-options": { | ||||||
|         "all": "Alle altersgruppen", |         "all": "Alle Altersgruppen", | ||||||
|         "primary-school": "Grundschule", |         "primary-school": "Grundschule", | ||||||
|         "lower-secondary": "12-14 jahre alt", |         "lower-secondary": "12-14 Jahre alt", | ||||||
|         "upper-secondary": "14-16 jahre alt", |         "upper-secondary": "14-16 Jahre alt", | ||||||
|         "high-school": "16-18 jahre alt", |         "high-school": "16-18 Jahre alt", | ||||||
|         "older": "18 und älter" |         "older": "18 und älter" | ||||||
|     }, |     }, | ||||||
|     "read-more": "Mehr lesen", |     "read-more": "Mehr lesen", | ||||||
|  | @ -73,7 +73,18 @@ | ||||||
|     "accept": "akzeptieren", |     "accept": "akzeptieren", | ||||||
|     "deny": "ablehnen", |     "deny": "ablehnen", | ||||||
|     "sent": "sent", |     "sent": "sent", | ||||||
|     "failed": "gescheitert", |     "failed": "fehlgeschlagen", | ||||||
|     "wrong": "etwas ist schief gelaufen", |     "wrong": "etwas ist schief gelaufen", | ||||||
|     "created": "erstellt" |     "created": "erstellt", | ||||||
|  |     "submitSolution": "Lösung einreichen", | ||||||
|  |     "submitNewSolution": "Neue Lösung einreichen", | ||||||
|  |     "markAsDone": "Als fertig markieren", | ||||||
|  |     "groupSubmissions": "Einreichungen dieser Gruppe", | ||||||
|  |     "taskCompleted": "Aufgabe erledigt.", | ||||||
|  |     "submittedBy": "Eingereicht von", | ||||||
|  |     "timestamp": "Zeitpunkt", | ||||||
|  |     "loadSubmission": "Einladen", | ||||||
|  |     "noSubmissionsYet": "Noch keine Lösungen eingereicht.", | ||||||
|  |     "viewAsGroup": "Fortschritt ansehen von Gruppe...", | ||||||
|  |     "assignLearningPath": "Als Aufgabe geben" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -75,5 +75,16 @@ | ||||||
|     "sent": "sent", |     "sent": "sent", | ||||||
|     "failed": "failed", |     "failed": "failed", | ||||||
|     "wrong": "something went wrong", |     "wrong": "something went wrong", | ||||||
|     "created": "created" |     "created": "created", | ||||||
|  |     "submitSolution": "Submit solution", | ||||||
|  |     "submitNewSolution": "Submit new solution", | ||||||
|  |     "markAsDone": "Mark as completed", | ||||||
|  |     "groupSubmissions": "This group's submissions", | ||||||
|  |     "taskCompleted": "Task completed.", | ||||||
|  |     "submittedBy": "Submitted by", | ||||||
|  |     "timestamp": "Timestamp", | ||||||
|  |     "loadSubmission": "Load", | ||||||
|  |     "noSubmissionsYet": "No submissions yet.", | ||||||
|  |     "viewAsGroup": "View progress of group...", | ||||||
|  |     "assignLearningPath": "assign" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -75,5 +75,16 @@ | ||||||
|     "sent": "envoyé", |     "sent": "envoyé", | ||||||
|     "failed": "échoué", |     "failed": "échoué", | ||||||
|     "wrong": "quelque chose n'a pas fonctionné", |     "wrong": "quelque chose n'a pas fonctionné", | ||||||
|     "created": "créé" |     "created": "créé", | ||||||
|  |     "submitSolution": "Soumettre la solution", | ||||||
|  |     "submitNewSolution": "Soumettre une nouvelle solution", | ||||||
|  |     "markAsDone": "Marquer comme terminé", | ||||||
|  |     "groupSubmissions": "Soumissions de ce groupe", | ||||||
|  |     "taskCompleted": "Tâche terminée.", | ||||||
|  |     "submittedBy": "Soumis par", | ||||||
|  |     "timestamp": "Horodatage", | ||||||
|  |     "loadSubmission": "Charger", | ||||||
|  |     "noSubmissionsYet": "Pas encore de soumissions.", | ||||||
|  |     "viewAsGroup": "Voir la progression du groupe...", | ||||||
|  |     "assignLearningPath": "donner comme tâche" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -75,5 +75,16 @@ | ||||||
|     "sent": "verzonden", |     "sent": "verzonden", | ||||||
|     "failed": "mislukt", |     "failed": "mislukt", | ||||||
|     "wrong": "er ging iets verkeerd", |     "wrong": "er ging iets verkeerd", | ||||||
|     "created": "gecreëerd" |     "created": "gecreëerd", | ||||||
|  |     "submitSolution": "Oplossing indienen", | ||||||
|  |     "submitNewSolution": "Nieuwe oplossing indienen", | ||||||
|  |     "markAsDone": "Markeren als afgewerkt", | ||||||
|  |     "groupSubmissions": "Indieningen van deze groep", | ||||||
|  |     "taskCompleted": "Taak afgewerkt.", | ||||||
|  |     "submittedBy": "Ingediend door", | ||||||
|  |     "timestamp": "Tijdstip", | ||||||
|  |     "loadSubmission": "Inladen", | ||||||
|  |     "noSubmissionsYet": "Nog geen indieningen.", | ||||||
|  |     "viewAsGroup": "Vooruitgang bekijken van groep...", | ||||||
|  |     "assignLearningPath": "Als opdracht geven" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ import { | ||||||
|     useQueryClient, |     useQueryClient, | ||||||
|     type UseQueryReturnType, |     type UseQueryReturnType, | ||||||
| } from "@tanstack/vue-query"; | } from "@tanstack/vue-query"; | ||||||
| import { computed, type MaybeRefOrGetter, toValue } from "vue"; | import { computed, toValue, type MaybeRefOrGetter } from "vue"; | ||||||
| import { invalidateAllSubmissionKeys } from "./submissions"; | import { invalidateAllSubmissionKeys } from "./submissions"; | ||||||
| 
 | 
 | ||||||
| type GroupsQueryKey = ["groups", string, number, boolean]; | type GroupsQueryKey = ["groups", string, number, boolean]; | ||||||
|  | @ -160,7 +160,7 @@ export function useDeleteGroupMutation(): UseMutationReturnType< | ||||||
|             const gn = response.group.groupNumber; |             const gn = response.group.groupNumber; | ||||||
| 
 | 
 | ||||||
|             await invalidateAllGroupKeys(queryClient, cid, an, gn); |             await invalidateAllGroupKeys(queryClient, cid, an, gn); | ||||||
|             await invalidateAllSubmissionKeys(queryClient, cid, an, gn); |             await invalidateAllSubmissionKeys(queryClient, undefined, undefined, undefined, cid, an, gn); | ||||||
|         }, |         }, | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ import { getLearningObjectController } from "@/controllers/controllers.ts"; | ||||||
| import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; | import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; | ||||||
| import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||||
| 
 | 
 | ||||||
| const LEARNING_OBJECT_KEY = "learningObject"; | export const LEARNING_OBJECT_KEY = "learningObject"; | ||||||
| const learningObjectController = getLearningObjectController(); | const learningObjectController = getLearningObjectController(); | ||||||
| 
 | 
 | ||||||
| export function useLearningObjectMetadataQuery( | export function useLearningObjectMetadataQuery( | ||||||
|  |  | ||||||
|  | @ -4,19 +4,19 @@ import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; | ||||||
| import { getLearningPathController } from "@/controllers/controllers"; | import { getLearningPathController } from "@/controllers/controllers"; | ||||||
| import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||||
| 
 | 
 | ||||||
| const LEARNING_PATH_KEY = "learningPath"; | export const LEARNING_PATH_KEY = "learningPath"; | ||||||
| const learningPathController = getLearningPathController(); | const learningPathController = getLearningPathController(); | ||||||
| 
 | 
 | ||||||
| export function useGetLearningPathQuery( | export function useGetLearningPathQuery( | ||||||
|     hruid: MaybeRefOrGetter<string>, |     hruid: MaybeRefOrGetter<string>, | ||||||
|     language: MaybeRefOrGetter<Language>, |     language: MaybeRefOrGetter<Language>, | ||||||
|     options?: MaybeRefOrGetter<{ forGroup?: string; forStudent?: string }>, |     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, options], |         queryKey: [LEARNING_PATH_KEY, "get", hruid, language, forGroup], | ||||||
|         queryFn: async () => { |         queryFn: async () => { | ||||||
|             const [hruidVal, languageVal, optionsVal] = [toValue(hruid), toValue(language), toValue(options)]; |             const [hruidVal, languageVal, forGroupVal] = [toValue(hruid), toValue(language), toValue(forGroup)]; | ||||||
|             return learningPathController.getBy(hruidVal, languageVal, optionsVal); |             return learningPathController.getBy(hruidVal, languageVal, forGroupVal); | ||||||
|         }, |         }, | ||||||
|         enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)), |         enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)), | ||||||
|     }); |     }); | ||||||
|  | @ -34,12 +34,14 @@ export function useGetAllLearningPathsByThemeQuery( | ||||||
| 
 | 
 | ||||||
| export function useSearchLearningPathQuery( | export function useSearchLearningPathQuery( | ||||||
|     query: MaybeRefOrGetter<string | undefined>, |     query: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     language: MaybeRefOrGetter<string | undefined>, | ||||||
| ): UseQueryReturnType<LearningPath[], Error> { | ): UseQueryReturnType<LearningPath[], Error> { | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryKey: [LEARNING_PATH_KEY, "search", query], |         queryKey: [LEARNING_PATH_KEY, "search", query, language], | ||||||
|         queryFn: async () => { |         queryFn: async () => { | ||||||
|             const queryVal = toValue(query)!; |             const queryVal = toValue(query)!; | ||||||
|             return learningPathController.search(queryVal); |             const languageVal = toValue(language)!; | ||||||
|  |             return learningPathController.search(queryVal, languageVal); | ||||||
|         }, |         }, | ||||||
|         enabled: () => Boolean(toValue(query)), |         enabled: () => Boolean(toValue(query)), | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -1,39 +1,39 @@ | ||||||
| import { SubmissionController, type SubmissionResponse, type SubmissionsResponse } from "@/controllers/submissions"; | import { SubmissionController, type SubmissionResponse } from "@/controllers/submissions"; | ||||||
| import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission"; | import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission"; | ||||||
| import { | import { | ||||||
|     QueryClient, |     QueryClient, | ||||||
|     useMutation, |     useMutation, | ||||||
|  |     type UseMutationReturnType, | ||||||
|     useQuery, |     useQuery, | ||||||
|     useQueryClient, |     useQueryClient, | ||||||
|     type UseMutationReturnType, |  | ||||||
|     type UseQueryReturnType, |     type UseQueryReturnType, | ||||||
| } from "@tanstack/vue-query"; | } from "@tanstack/vue-query"; | ||||||
| import { computed, toValue, type MaybeRefOrGetter } from "vue"; | import { computed, type MaybeRefOrGetter, toValue } from "vue"; | ||||||
|  | import { LEARNING_PATH_KEY } from "@/queries/learning-paths.ts"; | ||||||
|  | import { LEARNING_OBJECT_KEY } from "@/queries/learning-objects.ts"; | ||||||
|  | import type { Language } from "@dwengo-1/common/util/language"; | ||||||
| 
 | 
 | ||||||
| type SubmissionsQueryKey = ["submissions", string, number, number, boolean]; | export const SUBMISSION_KEY = "submissions"; | ||||||
| 
 | 
 | ||||||
| function submissionsQueryKey( | type SubmissionQueryKey = ["submission", string, Language | undefined, number, string, number, number, number]; | ||||||
|     classid: string, |  | ||||||
|     assignmentNumber: number, |  | ||||||
|     groupNumber: number, |  | ||||||
|     full: boolean, |  | ||||||
| ): SubmissionsQueryKey { |  | ||||||
|     return ["submissions", classid, assignmentNumber, groupNumber, full]; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type SubmissionQueryKey = ["submission", string, number, number, number]; |  | ||||||
| 
 | 
 | ||||||
| function submissionQueryKey( | function submissionQueryKey( | ||||||
|  |     hruid: string, | ||||||
|  |     language: Language, | ||||||
|  |     version: number, | ||||||
|     classid: string, |     classid: string, | ||||||
|     assignmentNumber: number, |     assignmentNumber: number, | ||||||
|     groupNumber: number, |     groupNumber: number, | ||||||
|     submissionNumber: number, |     submissionNumber: number, | ||||||
| ): SubmissionQueryKey { | ): SubmissionQueryKey { | ||||||
|     return ["submission", classid, assignmentNumber, groupNumber, submissionNumber]; |     return ["submission", hruid, language, version, classid, assignmentNumber, groupNumber, submissionNumber]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function invalidateAllSubmissionKeys( | export async function invalidateAllSubmissionKeys( | ||||||
|     queryClient: QueryClient, |     queryClient: QueryClient, | ||||||
|  |     hruid?: string, | ||||||
|  |     language?: Language, | ||||||
|  |     version?: number, | ||||||
|     classid?: string, |     classid?: string, | ||||||
|     assignmentNumber?: number, |     assignmentNumber?: number, | ||||||
|     groupNumber?: number, |     groupNumber?: number, | ||||||
|  | @ -43,101 +43,134 @@ export async function invalidateAllSubmissionKeys( | ||||||
| 
 | 
 | ||||||
|     await Promise.all( |     await Promise.all( | ||||||
|         keys.map(async (key) => { |         keys.map(async (key) => { | ||||||
|             const queryKey = [key, classid, assignmentNumber, groupNumber, submissionNumber].filter( |             const queryKey = [ | ||||||
|                 (arg) => arg !== undefined, |                 key, | ||||||
|             ); |                 hruid, | ||||||
|  |                 language, | ||||||
|  |                 version, | ||||||
|  |                 classid, | ||||||
|  |                 assignmentNumber, | ||||||
|  |                 groupNumber, | ||||||
|  |                 submissionNumber, | ||||||
|  |             ].filter((arg) => arg !== undefined); | ||||||
|             return queryClient.invalidateQueries({ queryKey: queryKey }); |             return queryClient.invalidateQueries({ queryKey: queryKey }); | ||||||
|         }), |         }), | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     await queryClient.invalidateQueries({ |     await queryClient.invalidateQueries({ | ||||||
|         queryKey: ["submissions", classid, assignmentNumber, groupNumber].filter((arg) => arg !== undefined), |         queryKey: ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber].filter( | ||||||
|  |             (arg) => arg !== undefined, | ||||||
|  |         ), | ||||||
|     }); |     }); | ||||||
|     await queryClient.invalidateQueries({ |     await queryClient.invalidateQueries({ | ||||||
|         queryKey: ["group-submissions", classid, assignmentNumber, groupNumber].filter((arg) => arg !== undefined), |         queryKey: ["group-submissions", hruid, language, version, classid, assignmentNumber, groupNumber].filter( | ||||||
|  |             (arg) => arg !== undefined, | ||||||
|  |         ), | ||||||
|     }); |     }); | ||||||
|     await queryClient.invalidateQueries({ |     await queryClient.invalidateQueries({ | ||||||
|         queryKey: ["assignment-submissions", classid, assignmentNumber].filter((arg) => arg !== undefined), |         queryKey: ["assignment-submissions", hruid, language, version, classid, assignmentNumber].filter( | ||||||
|  |             (arg) => arg !== undefined, | ||||||
|  |         ), | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function checkEnabled( | function checkEnabled(properties: MaybeRefOrGetter<unknown>[]): boolean { | ||||||
|     classid: string | undefined, |     return properties.every((prop) => Boolean(toValue(prop))); | ||||||
|     assignmentNumber: number | undefined, |  | ||||||
|     groupNumber: number | undefined, |  | ||||||
|     submissionNumber: number | undefined, |  | ||||||
| ): boolean { |  | ||||||
|     return ( |  | ||||||
|         Boolean(classid) && |  | ||||||
|         !isNaN(Number(groupNumber)) && |  | ||||||
|         !isNaN(Number(assignmentNumber)) && |  | ||||||
|         !isNaN(Number(submissionNumber)) |  | ||||||
|     ); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| interface Values { |  | ||||||
|     cid: string | undefined; |  | ||||||
|     an: number | undefined; |  | ||||||
|     gn: number | undefined; |  | ||||||
|     sn: number | undefined; |  | ||||||
|     f: boolean; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function toValues( |  | ||||||
|     classid: MaybeRefOrGetter<string | undefined>, |  | ||||||
|     assignmentNumber: MaybeRefOrGetter<number | undefined>, |  | ||||||
|     groupNumber: MaybeRefOrGetter<number | undefined>, |  | ||||||
|     submissionNumber: MaybeRefOrGetter<number | undefined>, |  | ||||||
|     full: MaybeRefOrGetter<boolean>, |  | ||||||
| ): Values { |  | ||||||
|     return { |  | ||||||
|         cid: toValue(classid), |  | ||||||
|         an: toValue(assignmentNumber), |  | ||||||
|         gn: toValue(groupNumber), |  | ||||||
|         sn: toValue(submissionNumber), |  | ||||||
|         f: toValue(full), |  | ||||||
|     }; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useSubmissionsQuery( | export function useSubmissionsQuery( | ||||||
|  |     hruid: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     language: MaybeRefOrGetter<Language | undefined>, | ||||||
|  |     version: MaybeRefOrGetter<number | undefined>, | ||||||
|     classid: MaybeRefOrGetter<string | undefined>, |     classid: MaybeRefOrGetter<string | undefined>, | ||||||
|     assignmentNumber: MaybeRefOrGetter<number | undefined>, |     assignmentNumber: MaybeRefOrGetter<number | undefined>, | ||||||
|     groupNumber: MaybeRefOrGetter<number | undefined>, |     groupNumber: MaybeRefOrGetter<number | undefined>, | ||||||
|     full: MaybeRefOrGetter<boolean> = true, |     full: MaybeRefOrGetter<boolean> = true, | ||||||
| ): UseQueryReturnType<SubmissionsResponse, Error> { | ): UseQueryReturnType<SubmissionDTO[], Error> { | ||||||
|     const { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, 1, full); |  | ||||||
| 
 |  | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryKey: computed(() => submissionsQueryKey(cid!, an!, gn!, f)), |         queryKey: ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber, full], | ||||||
|         queryFn: async () => new SubmissionController(cid!, an!, gn!).getAll(f), |         queryFn: async () => { | ||||||
|         enabled: () => checkEnabled(cid, an, gn, sn), |             const hruidVal = toValue(hruid); | ||||||
|  |             const languageVal = toValue(language); | ||||||
|  |             const versionVal = toValue(version); | ||||||
|  |             const classIdVal = toValue(classid); | ||||||
|  |             const assignmentNumberVal = toValue(assignmentNumber); | ||||||
|  |             const groupNumberVal = toValue(groupNumber); | ||||||
|  |             const fullVal = toValue(full); | ||||||
|  | 
 | ||||||
|  |             const response = await new SubmissionController(hruidVal!).getAll( | ||||||
|  |                 languageVal, | ||||||
|  |                 versionVal!, | ||||||
|  |                 classIdVal!, | ||||||
|  |                 assignmentNumberVal!, | ||||||
|  |                 groupNumberVal, | ||||||
|  |                 fullVal, | ||||||
|  |             ); | ||||||
|  |             return response ? (response.submissions as SubmissionDTO[]) : undefined; | ||||||
|  |         }, | ||||||
|  |         enabled: () => checkEnabled([hruid, language, version, classid, assignmentNumber]), | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useSubmissionQuery( | export function useSubmissionQuery( | ||||||
|  |     hruid: MaybeRefOrGetter<string | undefined>, | ||||||
|  |     language: MaybeRefOrGetter<Language | undefined>, | ||||||
|  |     version: MaybeRefOrGetter<number | undefined>, | ||||||
|     classid: MaybeRefOrGetter<string | undefined>, |     classid: MaybeRefOrGetter<string | undefined>, | ||||||
|     assignmentNumber: MaybeRefOrGetter<number | undefined>, |     assignmentNumber: MaybeRefOrGetter<number | undefined>, | ||||||
|     groupNumber: MaybeRefOrGetter<number | undefined>, |     groupNumber: MaybeRefOrGetter<number | undefined>, | ||||||
|  |     submissionNumber: MaybeRefOrGetter<number | undefined>, | ||||||
| ): UseQueryReturnType<SubmissionResponse, Error> { | ): UseQueryReturnType<SubmissionResponse, Error> { | ||||||
|     const { cid, an, gn, sn } = toValues(classid, assignmentNumber, groupNumber, 1, true); |     const hruidVal = toValue(hruid); | ||||||
|  |     const languageVal = toValue(language); | ||||||
|  |     const versionVal = toValue(version); | ||||||
|  |     const classIdVal = toValue(classid); | ||||||
|  |     const assignmentNumberVal = toValue(assignmentNumber); | ||||||
|  |     const groupNumberVal = toValue(groupNumber); | ||||||
|  |     const submissionNumberVal = toValue(submissionNumber); | ||||||
| 
 | 
 | ||||||
|     return useQuery({ |     return useQuery({ | ||||||
|         queryKey: computed(() => submissionQueryKey(cid!, an!, gn!, sn!)), |         queryKey: computed(() => | ||||||
|         queryFn: async () => new SubmissionController(cid!, an!, gn!).getByNumber(sn!), |             submissionQueryKey( | ||||||
|         enabled: () => checkEnabled(cid, an, gn, sn), |                 hruidVal!, | ||||||
|  |                 languageVal, | ||||||
|  |                 versionVal!, | ||||||
|  |                 classIdVal!, | ||||||
|  |                 assignmentNumberVal!, | ||||||
|  |                 groupNumberVal!, | ||||||
|  |                 submissionNumberVal!, | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |         queryFn: async () => | ||||||
|  |             new SubmissionController(hruidVal!).getByNumber( | ||||||
|  |                 languageVal, | ||||||
|  |                 versionVal!, | ||||||
|  |                 classIdVal!, | ||||||
|  |                 assignmentNumberVal!, | ||||||
|  |                 groupNumberVal!, | ||||||
|  |                 submissionNumberVal!, | ||||||
|  |             ), | ||||||
|  |         enabled: () => | ||||||
|  |             Boolean(hruidVal) && | ||||||
|  |             Boolean(languageVal) && | ||||||
|  |             Boolean(versionVal) && | ||||||
|  |             Boolean(classIdVal) && | ||||||
|  |             Boolean(assignmentNumberVal) && | ||||||
|  |             Boolean(submissionNumber), | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useCreateSubmissionMutation(): UseMutationReturnType< | export function useCreateSubmissionMutation(): UseMutationReturnType< | ||||||
|     SubmissionResponse, |     SubmissionResponse, | ||||||
|     Error, |     Error, | ||||||
|     { cid: string; an: number; gn: number; data: SubmissionDTO }, |     { data: SubmissionDTO }, | ||||||
|     unknown |     unknown | ||||||
| > { | > { | ||||||
|     const queryClient = useQueryClient(); |     const queryClient = useQueryClient(); | ||||||
| 
 | 
 | ||||||
|     return useMutation({ |     return useMutation({ | ||||||
|         mutationFn: async ({ cid, an, gn, data }) => new SubmissionController(cid, an, gn).createSubmission(data), |         mutationFn: async ({ data }) => | ||||||
|  |             new SubmissionController(data.learningObjectIdentifier.hruid).createSubmission(data), | ||||||
|         onSuccess: async (response) => { |         onSuccess: async (response) => { | ||||||
|             if (!response.submission.group) { |             if (!response.submission.group) { | ||||||
|                 await invalidateAllSubmissionKeys(queryClient); |                 await invalidateAllSubmissionKeys(queryClient); | ||||||
|  | @ -149,7 +182,14 @@ 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; | ||||||
| 
 | 
 | ||||||
|                 await invalidateAllSubmissionKeys(queryClient, cid, an, gn); |                 const { hruid, language, version } = response.submission.learningObjectIdentifier; | ||||||
|  |                 await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn); | ||||||
|  | 
 | ||||||
|  |                 await queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY, "get"] }); | ||||||
|  | 
 | ||||||
|  |                 await queryClient.invalidateQueries({ | ||||||
|  |                     queryKey: [LEARNING_OBJECT_KEY, "metadata", hruid, language, version], | ||||||
|  |                 }); | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|     }); |     }); | ||||||
|  | @ -164,7 +204,7 @@ export function useDeleteSubmissionMutation(): UseMutationReturnType< | ||||||
|     const queryClient = useQueryClient(); |     const queryClient = useQueryClient(); | ||||||
| 
 | 
 | ||||||
|     return useMutation({ |     return useMutation({ | ||||||
|         mutationFn: async ({ cid, an, gn, sn }) => new SubmissionController(cid, an, gn).deleteSubmission(sn), |         mutationFn: async ({ cid, sn }) => new SubmissionController(cid).deleteSubmission(sn), | ||||||
|         onSuccess: async (response) => { |         onSuccess: async (response) => { | ||||||
|             if (!response.submission.group) { |             if (!response.submission.group) { | ||||||
|                 await invalidateAllSubmissionKeys(queryClient); |                 await invalidateAllSubmissionKeys(queryClient); | ||||||
|  | @ -176,7 +216,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; | ||||||
| 
 | 
 | ||||||
|                 await invalidateAllSubmissionKeys(queryClient, cid, an, gn); |                 const { hruid, language, version } = response.submission.learningObjectIdentifier; | ||||||
|  | 
 | ||||||
|  |                 await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn); | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue"; | ||||||
| import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue"; | import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue"; | ||||||
| import UserHomePage from "@/views/homepage/UserHomePage.vue"; | import UserHomePage from "@/views/homepage/UserHomePage.vue"; | ||||||
| import SingleTheme from "@/views/SingleTheme.vue"; | import SingleTheme from "@/views/SingleTheme.vue"; | ||||||
| import LearningObjectView from "@/views/learning-paths/LearningObjectView.vue"; | import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; | ||||||
| 
 | 
 | ||||||
| const router = createRouter({ | const router = createRouter({ | ||||||
|     history: createWebHistory(import.meta.env.BASE_URL), |     history: createWebHistory(import.meta.env.BASE_URL), | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								frontend/src/utils/array-utils.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/utils/array-utils.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | export function copyArrayWith<T>(index: number, newValue: T, array: T[]): T[] { | ||||||
|  |     const copy = [...array]; | ||||||
|  |     copy[index] = newValue; | ||||||
|  |     return copy; | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								frontend/src/utils/deep-equals.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/utils/deep-equals.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | ||||||
|  | export function deepEquals<T>(a: T, b: T): boolean { | ||||||
|  |     if (a === b) { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (Array.isArray(a) !== Array.isArray(b)) { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (Array.isArray(a) && Array.isArray(b)) { | ||||||
|  |         if (a.length !== b.length) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         return a.every((val, i) => deepEquals(val, b[i])); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const keysA = Object.keys(a) as (keyof T)[]; | ||||||
|  |     const keysB = Object.keys(b) as (keyof T)[]; | ||||||
|  | 
 | ||||||
|  |     if (keysA.length !== keysB.length) { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return keysA.every((key) => deepEquals(a[key], b[key])); | ||||||
|  | } | ||||||
|  | @ -1,7 +1,14 @@ | ||||||
| <script setup lang="ts"></script> | <script setup lang="ts"> | ||||||
|  |     import { useRoute } from "vue-router"; | ||||||
|  | 
 | ||||||
|  |     const route = useRoute(); | ||||||
|  | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <main></main> |     <main> | ||||||
|  |         Hier zou de pagina staan om een assignment aan te maken voor de leerpad met hruid {{ route.query.hruid }} en | ||||||
|  |         language {{ route.query.language }}. (Overschrijf dit) | ||||||
|  |     </main> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped></style> | <style scoped></style> | ||||||
|  |  | ||||||
|  | @ -1,51 +0,0 @@ | ||||||
| <script setup lang="ts"> |  | ||||||
|     import { Language } from "@/data-objects/language.ts"; |  | ||||||
|     import type { UseQueryReturnType } from "@tanstack/vue-query"; |  | ||||||
|     import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts"; |  | ||||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; |  | ||||||
| 
 |  | ||||||
|     const props = defineProps<{ hruid: string; language: Language; version: number }>(); |  | ||||||
| 
 |  | ||||||
|     const learningObjectHtmlQueryResult: UseQueryReturnType<Document, Error> = useLearningObjectHTMLQuery( |  | ||||||
|         () => props.hruid, |  | ||||||
|         () => props.language, |  | ||||||
|         () => props.version, |  | ||||||
|     ); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <template> |  | ||||||
|     <using-query-result |  | ||||||
|         :query-result="learningObjectHtmlQueryResult as UseQueryReturnType<Document, Error>" |  | ||||||
|         v-slot="learningPathHtml: { data: Document }" |  | ||||||
|     > |  | ||||||
|         <div |  | ||||||
|             class="learning-object-container" |  | ||||||
|             v-html="learningPathHtml.data.body.innerHTML" |  | ||||||
|         ></div> |  | ||||||
|     </using-query-result> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
|     .learning-object-container { |  | ||||||
|         padding: 20px; |  | ||||||
|     } |  | ||||||
|     :deep(hr) { |  | ||||||
|         margin-top: 10px; |  | ||||||
|         margin-bottom: 10px; |  | ||||||
|     } |  | ||||||
|     :deep(li) { |  | ||||||
|         margin-left: 30px; |  | ||||||
|         margin-top: 5px; |  | ||||||
|         margin-bottom: 5px; |  | ||||||
|     } |  | ||||||
|     :deep(img) { |  | ||||||
|         max-width: 80%; |  | ||||||
|     } |  | ||||||
|     :deep(h2), |  | ||||||
|     :deep(h3), |  | ||||||
|     :deep(h4), |  | ||||||
|     :deep(h5), |  | ||||||
|     :deep(h6) { |  | ||||||
|         margin-top: 10px; |  | ||||||
|     } |  | ||||||
| </style> |  | ||||||
|  | @ -0,0 +1,55 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|  |     import { useGroupsQuery } from "@/queries/groups.ts"; | ||||||
|  |     import type { GroupsResponse } from "@/controllers/groups.ts"; | ||||||
|  |     import { useI18n } from "vue-i18n"; | ||||||
|  |     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         classId: string; | ||||||
|  |         assignmentNumber: number; | ||||||
|  |     }>(); | ||||||
|  | 
 | ||||||
|  |     const model = defineModel<number | undefined>({ default: undefined }); | ||||||
|  | 
 | ||||||
|  |     const groupsQuery = useGroupsQuery(props.classId, props.assignmentNumber, true); | ||||||
|  | 
 | ||||||
|  |     interface GroupSelectorOption { | ||||||
|  |         groupNumber: number | undefined; | ||||||
|  |         label: string; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function groupOptions(groups: GroupDTO[]): GroupSelectorOption[] { | ||||||
|  |         return [...groups] | ||||||
|  |             .sort((a, b) => a.groupNumber - b.groupNumber) | ||||||
|  |             .map((group, index) => ({ | ||||||
|  |                 groupNumber: group.groupNumber, | ||||||
|  |                 label: `${index + 1}`, | ||||||
|  |             })); | ||||||
|  |     } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <using-query-result | ||||||
|  |         :query-result="groupsQuery" | ||||||
|  |         v-slot="{ data }: { data: GroupsResponse }" | ||||||
|  |     > | ||||||
|  |         <v-select | ||||||
|  |             :label="t('viewAsGroup')" | ||||||
|  |             :items="groupOptions(data.groups)" | ||||||
|  |             v-model="model" | ||||||
|  |             item-title="label" | ||||||
|  |             class="group-selector-cb" | ||||||
|  |             variant="outlined" | ||||||
|  |             clearable | ||||||
|  |         ></v-select> | ||||||
|  |     </using-query-result> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  |     .group-selector-cb { | ||||||
|  |         margin-top: 10px; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  | @ -3,8 +3,8 @@ | ||||||
|     import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; |     import 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 } from "vue-router"; |     import { useRoute, useRouter } from "vue-router"; | ||||||
|     import LearningObjectView from "@/views/learning-paths/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"; | ||||||
|     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; |     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||||
|  | @ -12,30 +12,38 @@ | ||||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|     import authService from "@/services/auth/auth-service.ts"; |     import authService from "@/services/auth/auth-service.ts"; | ||||||
|     import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts"; |     import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts"; | ||||||
|  |     import LearningPathGroupSelector from "@/views/learning-paths/LearningPathGroupSelector.vue"; | ||||||
| 
 | 
 | ||||||
|  |     const router = useRouter(); | ||||||
|     const route = useRoute(); |     const route = useRoute(); | ||||||
|     const { t } = useI18n(); |     const { t } = useI18n(); | ||||||
| 
 | 
 | ||||||
|     const props = defineProps<{ hruid: string; language: Language; learningObjectHruid?: string }>(); |     const props = defineProps<{ | ||||||
|  |         hruid: string; | ||||||
|  |         language: Language; | ||||||
|  |         learningObjectHruid?: string; | ||||||
|  |     }>(); | ||||||
| 
 | 
 | ||||||
|     interface Personalization { |     interface LearningPathPageQuery { | ||||||
|         forStudent?: string; |  | ||||||
|         forGroup?: string; |         forGroup?: string; | ||||||
|  |         assignmentNo?: string; | ||||||
|  |         classId?: string; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const personalization = computed(() => { |     const query = computed(() => route.query as LearningPathPageQuery); | ||||||
|         if (route.query.forStudent || route.query.forGroup) { | 
 | ||||||
|  |     const forGroup = computed(() => { | ||||||
|  |         if (query.value.forGroup && query.value.assignmentNo && query.value.classId) { | ||||||
|             return { |             return { | ||||||
|                 forStudent: route.query.forStudent, |                 forGroup: parseInt(query.value.forGroup), | ||||||
|                 forGroup: route.query.forGroup, |                 assignmentNo: parseInt(query.value.assignmentNo), | ||||||
|             } as Personalization; |                 classId: query.value.classId, | ||||||
|  |             }; | ||||||
|         } |         } | ||||||
|         return { |         return undefined; | ||||||
|             forStudent: authService.authState.user?.profile?.preferred_username, |  | ||||||
|         } as Personalization; |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const learningPathQueryResult = useGetLearningPathQuery(props.hruid, props.language, personalization); |     const learningPathQueryResult = useGetLearningPathQuery(props.hruid, props.language, forGroup); | ||||||
| 
 | 
 | ||||||
|     const learningObjectListQueryResult = useLearningObjectListForPathQuery(learningPathQueryResult.data); |     const learningObjectListQueryResult = useLearningObjectListForPathQuery(learningPathQueryResult.data); | ||||||
| 
 | 
 | ||||||
|  | @ -98,6 +106,25 @@ | ||||||
|         } |         } | ||||||
|         return "notCompleted"; |         return "notCompleted"; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     const forGroupQueryParam = computed<number | undefined>({ | ||||||
|  |         get: () => route.query.forGroup, | ||||||
|  |         set: async (value: number | undefined) => { | ||||||
|  |             const query = structuredClone(route.query); | ||||||
|  |             query.forGroup = value; | ||||||
|  |             await router.push({ query }); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     async function assign(): Promise<void> { | ||||||
|  |         await router.push({ | ||||||
|  |             path: "/assignment/create", | ||||||
|  |             query: { | ||||||
|  |                 hruid: props.hruid, | ||||||
|  |                 language: props.language, | ||||||
|  |             }, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  | @ -109,6 +136,7 @@ | ||||||
|             v-model="navigationDrawerShown" |             v-model="navigationDrawerShown" | ||||||
|             :width="350" |             :width="350" | ||||||
|         > |         > | ||||||
|  |             <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> | ||||||
|  | @ -142,6 +170,17 @@ | ||||||
|                         </p> |                         </p> | ||||||
|                     </template> |                     </template> | ||||||
|                 </v-list-item> |                 </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> |                 <v-divider></v-divider> | ||||||
|                 <div v-if="props.learningObjectHruid"> |                 <div v-if="props.learningObjectHruid"> | ||||||
|                     <using-query-result |                     <using-query-result | ||||||
|  | @ -168,6 +207,17 @@ | ||||||
|                         </template> |                         </template> | ||||||
|                     </using-query-result> |                     </using-query-result> | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <v-spacer></v-spacer> | ||||||
|  |                 <v-list-item v-if="authService.authState.activeRole === 'teacher'"> | ||||||
|  |                     <template v-slot:default> | ||||||
|  |                         <v-btn | ||||||
|  |                             class="button-in-nav" | ||||||
|  |                             @click="assign()" | ||||||
|  |                             >{{ t("assignLearningPath") }}</v-btn | ||||||
|  |                         > | ||||||
|  |                     </template> | ||||||
|  |                 </v-list-item> | ||||||
|  |             </div> | ||||||
|         </v-navigation-drawer> |         </v-navigation-drawer> | ||||||
|         <div class="control-bar-above-content"> |         <div class="control-bar-above-content"> | ||||||
|             <v-btn |             <v-btn | ||||||
|  | @ -180,12 +230,15 @@ | ||||||
|                 <learning-path-search-field></learning-path-search-field> |                 <learning-path-search-field></learning-path-search-field> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|  |         <div class="learning-object-view-container"> | ||||||
|             <learning-object-view |             <learning-object-view | ||||||
|                 :hruid="currentNode.learningobjectHruid" |                 :hruid="currentNode.learningobjectHruid" | ||||||
|                 :language="currentNode.language" |                 :language="currentNode.language" | ||||||
|                 :version="currentNode.version" |                 :version="currentNode.version" | ||||||
|  |                 :group="forGroup" | ||||||
|                 v-if="currentNode" |                 v-if="currentNode" | ||||||
|             ></learning-object-view> |             ></learning-object-view> | ||||||
|  |         </div> | ||||||
|         <div class="navigation-buttons-container"> |         <div class="navigation-buttons-container"> | ||||||
|             <v-btn |             <v-btn | ||||||
|                 prepend-icon="mdi-chevron-left" |                 prepend-icon="mdi-chevron-left" | ||||||
|  | @ -221,9 +274,18 @@ | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: space-between; |         justify-content: space-between; | ||||||
|     } |     } | ||||||
|  |     .learning-object-view-container { | ||||||
|  |         padding-left: 20px; | ||||||
|  |         padding-right: 20px; | ||||||
|  |         padding-bottom: 20px; | ||||||
|  |     } | ||||||
|     .navigation-buttons-container { |     .navigation-buttons-container { | ||||||
|         padding: 20px; |         padding: 20px; | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: space-between; |         justify-content: space-between; | ||||||
|     } |     } | ||||||
|  |     .button-in-nav { | ||||||
|  |         margin-top: 10px; | ||||||
|  |         margin-bottom: 10px; | ||||||
|  |     } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -9,11 +9,11 @@ | ||||||
|     import LearningPathsGrid from "@/components/LearningPathsGrid.vue"; |     import LearningPathsGrid from "@/components/LearningPathsGrid.vue"; | ||||||
| 
 | 
 | ||||||
|     const route = useRoute(); |     const route = useRoute(); | ||||||
|     const { t } = useI18n(); |     const { t, locale } = useI18n(); | ||||||
| 
 | 
 | ||||||
|     const query = computed(() => route.query.query as string | undefined); |     const query = computed(() => route.query.query as string | undefined); | ||||||
| 
 | 
 | ||||||
|     const searchQueryResults = useSearchLearningPathQuery(query); |     const searchQueryResults = useSearchLearningPathQuery(query, locale); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,18 @@ | ||||||
|  | export const essayQuestionAdapter: GiftAdapter = { | ||||||
|  |     questionType: "Essay", | ||||||
|  | 
 | ||||||
|  |     installListener( | ||||||
|  |         questionElement: Element, | ||||||
|  |         answerUpdateCallback: (newAnswer: string | number | object) => void, | ||||||
|  |     ): void { | ||||||
|  |         const textArea = questionElement.querySelector("textarea")!; | ||||||
|  |         textArea.addEventListener("input", () => { | ||||||
|  |             answerUpdateCallback(textArea.value); | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     setAnswer(questionElement: Element, answer: string | number | object): void { | ||||||
|  |         const textArea = questionElement.querySelector("textarea")!; | ||||||
|  |         textArea.value = String(answer); | ||||||
|  |     }, | ||||||
|  | }; | ||||||
							
								
								
									
										8
									
								
								frontend/src/views/learning-paths/gift-adapters/gift-adapter.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/src/views/learning-paths/gift-adapters/gift-adapter.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | interface GiftAdapter { | ||||||
|  |     questionType: string; | ||||||
|  |     installListener( | ||||||
|  |         questionElement: Element, | ||||||
|  |         answerUpdateCallback: (newAnswer: string | number | object) => void, | ||||||
|  |     ): void; | ||||||
|  |     setAnswer(questionElement: Element, answer: string | number | object): void; | ||||||
|  | } | ||||||
|  | @ -0,0 +1,8 @@ | ||||||
|  | import { multipleChoiceQuestionAdapter } from "@/views/learning-paths/gift-adapters/multiple-choice-question-adapter.ts"; | ||||||
|  | import { essayQuestionAdapter } from "@/views/learning-paths/gift-adapters/essay-question-adapter.ts"; | ||||||
|  | 
 | ||||||
|  | export const giftAdapters = [multipleChoiceQuestionAdapter, essayQuestionAdapter]; | ||||||
|  | 
 | ||||||
|  | export function getGiftAdapterForType(questionType: string): GiftAdapter | undefined { | ||||||
|  |     return giftAdapters.find((it) => it.questionType === questionType); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | export const multipleChoiceQuestionAdapter: GiftAdapter = { | ||||||
|  |     questionType: "MC", | ||||||
|  | 
 | ||||||
|  |     installListener( | ||||||
|  |         questionElement: Element, | ||||||
|  |         answerUpdateCallback: (newAnswer: string | number | object) => void, | ||||||
|  |     ): void { | ||||||
|  |         questionElement.querySelectorAll("input[type=radio]").forEach((element) => { | ||||||
|  |             const input = element as HTMLInputElement; | ||||||
|  | 
 | ||||||
|  |             input.addEventListener("change", () => { | ||||||
|  |                 answerUpdateCallback(parseInt(input.value)); | ||||||
|  |             }); | ||||||
|  |             // Optional: initialize value if already selected
 | ||||||
|  |             if (input.checked) { | ||||||
|  |                 answerUpdateCallback(parseInt(input.value)); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     setAnswer(questionElement: Element, answer: string | number | object): void { | ||||||
|  |         questionElement.querySelectorAll("input[type=radio]").forEach((element) => { | ||||||
|  |             const input = element as HTMLInputElement; | ||||||
|  |             input.checked = String(answer) === String(input.value); | ||||||
|  |         }); | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  | @ -0,0 +1,73 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import { Language } from "@/data-objects/language.ts"; | ||||||
|  |     import type { UseQueryReturnType } from "@tanstack/vue-query"; | ||||||
|  |     import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts"; | ||||||
|  |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|  |     import { computed, ref } from "vue"; | ||||||
|  |     import authService from "@/services/auth/auth-service.ts"; | ||||||
|  |     import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data"; | ||||||
|  |     import LearningObjectContentView from "@/views/learning-paths/learning-object/content/LearningObjectContentView.vue"; | ||||||
|  |     import LearningObjectSubmissionsView from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue"; | ||||||
|  | 
 | ||||||
|  |     const _isStudent = computed(() => authService.authState.activeRole === "student"); | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         hruid: string; | ||||||
|  |         language: Language; | ||||||
|  |         version: number; | ||||||
|  |         group?: { forGroup: number; assignmentNo: number; classId: string }; | ||||||
|  |     }>(); | ||||||
|  | 
 | ||||||
|  |     const learningObjectHtmlQueryResult: UseQueryReturnType<Document, Error> = useLearningObjectHTMLQuery( | ||||||
|  |         () => props.hruid, | ||||||
|  |         () => props.language, | ||||||
|  |         () => props.version, | ||||||
|  |     ); | ||||||
|  |     const currentSubmission = ref<SubmissionData>([]); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <using-query-result | ||||||
|  |         :query-result="learningObjectHtmlQueryResult as UseQueryReturnType<Document, Error>" | ||||||
|  |         v-slot="learningPathHtml: { data: Document }" | ||||||
|  |     > | ||||||
|  |         <learning-object-content-view | ||||||
|  |             :learning-object-content="learningPathHtml.data" | ||||||
|  |             v-model:submission-data="currentSubmission" | ||||||
|  |         /> | ||||||
|  |         <div class="content-submissions-spacer" /> | ||||||
|  |         <learning-object-submissions-view | ||||||
|  |             v-if="props.group" | ||||||
|  |             :group="props.group" | ||||||
|  |             :hruid="props.hruid" | ||||||
|  |             :language="props.language" | ||||||
|  |             :version="props.version" | ||||||
|  |             v-model:submission-data="currentSubmission" | ||||||
|  |         /> | ||||||
|  |     </using-query-result> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  |     :deep(hr) { | ||||||
|  |         margin-top: 10px; | ||||||
|  |         margin-bottom: 10px; | ||||||
|  |     } | ||||||
|  |     :deep(li) { | ||||||
|  |         margin-left: 30px; | ||||||
|  |         margin-top: 5px; | ||||||
|  |         margin-bottom: 5px; | ||||||
|  |     } | ||||||
|  |     :deep(img) { | ||||||
|  |         max-width: 80%; | ||||||
|  |     } | ||||||
|  |     :deep(h2), | ||||||
|  |     :deep(h3), | ||||||
|  |     :deep(h4), | ||||||
|  |     :deep(h5), | ||||||
|  |     :deep(h6) { | ||||||
|  |         margin-top: 10px; | ||||||
|  |     } | ||||||
|  |     .content-submissions-spacer { | ||||||
|  |         height: 20px; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,90 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data"; | ||||||
|  |     import { getGiftAdapterForType } from "@/views/learning-paths/gift-adapters/gift-adapters.ts"; | ||||||
|  |     import { computed, nextTick, onMounted, watch } from "vue"; | ||||||
|  |     import { copyArrayWith } from "@/utils/array-utils.ts"; | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         learningObjectContent: Document; | ||||||
|  |         submissionData?: SubmissionData; | ||||||
|  |     }>(); | ||||||
|  | 
 | ||||||
|  |     const emit = defineEmits<(e: "update:submissionData", value: SubmissionData) => void>(); | ||||||
|  | 
 | ||||||
|  |     const submissionData = computed<SubmissionData | undefined>({ | ||||||
|  |         get: () => props.submissionData, | ||||||
|  |         set: (v?: SubmissionData): void => { | ||||||
|  |             if (v) emit("update:submissionData", v); | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     function forEachQuestion( | ||||||
|  |         doAction: (questionIndex: number, questionName: string, questionType: string, questionElement: Element) => void, | ||||||
|  |     ): void { | ||||||
|  |         const questions = document.querySelectorAll(".gift-question"); | ||||||
|  |         questions.forEach((question) => { | ||||||
|  |             const name = question.id.match(/gift-q(\d+)/)?.[1]; | ||||||
|  |             const questionType = question.className | ||||||
|  |                 .split(" ") | ||||||
|  |                 .find((it) => it.startsWith("gift-question-type")) | ||||||
|  |                 ?.match(/gift-question-type-([^ ]*)/)?.[1]; | ||||||
|  | 
 | ||||||
|  |             if (!name || isNaN(parseInt(name)) || !questionType) return; | ||||||
|  | 
 | ||||||
|  |             const index = parseInt(name) - 1; | ||||||
|  | 
 | ||||||
|  |             doAction(index, name, questionType, question); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function attachQuestionListeners(): void { | ||||||
|  |         forEachQuestion((index, _name, type, element) => { | ||||||
|  |             getGiftAdapterForType(type)?.installListener(element, (newAnswer) => { | ||||||
|  |                 submissionData.value = copyArrayWith(index, newAnswer, submissionData.value ?? []); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function setAnswers(answers: SubmissionData): void { | ||||||
|  |         forEachQuestion((index, _name, type, element) => { | ||||||
|  |             const answer = answers[index]; | ||||||
|  |             if (answer !== null && answer !== undefined) { | ||||||
|  |                 getGiftAdapterForType(type)?.setAnswer(element, answer); | ||||||
|  |             } else if (answer === undefined) { | ||||||
|  |                 answers[index] = null; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         submissionData.value = answers; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     onMounted(async () => | ||||||
|  |         nextTick(() => { | ||||||
|  |             attachQuestionListeners(); | ||||||
|  |             setAnswers(props.submissionData ?? []); | ||||||
|  |         }), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     watch( | ||||||
|  |         () => props.learningObjectContent, | ||||||
|  |         async () => { | ||||||
|  |             await nextTick(); | ||||||
|  |             attachQuestionListeners(); | ||||||
|  |         }, | ||||||
|  |     ); | ||||||
|  |     watch( | ||||||
|  |         () => props.submissionData, | ||||||
|  |         async () => { | ||||||
|  |             await nextTick(); | ||||||
|  |             setAnswers(props.submissionData ?? []); | ||||||
|  |         }, | ||||||
|  |     ); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <div | ||||||
|  |         class="learning-object-container" | ||||||
|  |         v-html="learningObjectContent.body.innerHTML" | ||||||
|  |     ></div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped></style> | ||||||
							
								
								
									
										1
									
								
								frontend/src/views/learning-paths/learning-object/submission-data.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/views/learning-paths/learning-object/submission-data.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | export type SubmissionData = (string | number | object | null)[]; | ||||||
|  | @ -0,0 +1,61 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission"; | ||||||
|  |     import { computed } from "vue"; | ||||||
|  |     import { useI18n } from "vue-i18n"; | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         allSubmissions: SubmissionDTO[]; | ||||||
|  |     }>(); | ||||||
|  |     const emit = defineEmits<(e: "submission-selected", submission: SubmissionDTO) => void>(); | ||||||
|  | 
 | ||||||
|  |     const headers = computed(() => [ | ||||||
|  |         { title: "#", value: "submissionNo", width: "50px" }, | ||||||
|  |         { title: t("submittedBy"), value: "submittedBy" }, | ||||||
|  |         { title: t("timestamp"), value: "timestamp" }, | ||||||
|  |         { title: "", key: "action", width: "70px", sortable: false }, | ||||||
|  |     ]); | ||||||
|  | 
 | ||||||
|  |     const data = computed(() => | ||||||
|  |         [...props.allSubmissions] | ||||||
|  |             .sort((a, b) => (a.submissionNumber ?? 0) - (b.submissionNumber ?? 0)) | ||||||
|  |             .map((submission, index) => ({ | ||||||
|  |                 submissionNo: index + 1, | ||||||
|  |                 submittedBy: `${submission.submitter.firstName} ${submission.submitter.lastName}`, | ||||||
|  |                 timestamp: submission.time ? new Date(submission.time).toLocaleString() : "-", | ||||||
|  |                 dto: submission, | ||||||
|  |             })), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     function selectSubmission(submission: SubmissionDTO): void { | ||||||
|  |         emit("submission-selected", submission); | ||||||
|  |     } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <v-card> | ||||||
|  |         <v-card-title>{{ t("groupSubmissions") }}</v-card-title> | ||||||
|  |         <v-card-text> | ||||||
|  |             <v-data-table | ||||||
|  |                 :headers="headers" | ||||||
|  |                 :items="data" | ||||||
|  |                 density="compact" | ||||||
|  |                 hide-default-footer | ||||||
|  |                 :no-data-text="t('noSubmissionsYet')" | ||||||
|  |             > | ||||||
|  |                 <template v-slot:[`item.action`]="{ item }"> | ||||||
|  |                     <v-btn | ||||||
|  |                         density="compact" | ||||||
|  |                         variant="plain" | ||||||
|  |                         @click="selectSubmission(item.dto)" | ||||||
|  |                     > | ||||||
|  |                         {{ t("loadSubmission") }} | ||||||
|  |                     </v-btn> | ||||||
|  |                 </template> | ||||||
|  |             </v-data-table> | ||||||
|  |         </v-card-text> | ||||||
|  |     </v-card> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped></style> | ||||||
|  | @ -0,0 +1,97 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data"; | ||||||
|  |     import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission"; | ||||||
|  |     import { Language } from "@/data-objects/language.ts"; | ||||||
|  |     import { useSubmissionsQuery } from "@/queries/submissions.ts"; | ||||||
|  |     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||||
|  |     import SubmitButton from "@/views/learning-paths/learning-object/submissions/SubmitButton.vue"; | ||||||
|  |     import { computed, watch } from "vue"; | ||||||
|  |     import LearningObjectSubmissionsTable from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsTable.vue"; | ||||||
|  |     import { useI18n } from "vue-i18n"; | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         submissionData?: SubmissionData; | ||||||
|  |         hruid: string; | ||||||
|  |         language: Language; | ||||||
|  |         version: number; | ||||||
|  |         group: { forGroup: number; assignmentNo: number; classId: string }; | ||||||
|  |     }>(); | ||||||
|  |     const emit = defineEmits<(e: "update:submissionData", value: SubmissionData) => void>(); | ||||||
|  | 
 | ||||||
|  |     const submissionQuery = useSubmissionsQuery( | ||||||
|  |         () => props.hruid, | ||||||
|  |         () => props.language, | ||||||
|  |         () => props.version, | ||||||
|  |         () => props.group.classId, | ||||||
|  |         () => props.group.assignmentNo, | ||||||
|  |         () => props.group.forGroup, | ||||||
|  |         () => true, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     function emitSubmissionData(submissionData: SubmissionData): void { | ||||||
|  |         emit("update:submissionData", submissionData); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function emitSubmission(submission: SubmissionDTO): void { | ||||||
|  |         emitSubmissionData(JSON.parse(submission.content)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     watch(submissionQuery.data, () => { | ||||||
|  |         const submissions = submissionQuery.data.value; | ||||||
|  |         if (submissions && submissions.length > 0) { | ||||||
|  |             emitSubmission(submissions[submissions.length - 1]); | ||||||
|  |         } else { | ||||||
|  |             emitSubmissionData([]); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const lastSubmission = computed<SubmissionData>(() => { | ||||||
|  |         const submissions = submissionQuery.data.value; | ||||||
|  |         if (!submissions || submissions.length === 0) { | ||||||
|  |             return undefined; | ||||||
|  |         } | ||||||
|  |         return JSON.parse(submissions[submissions.length - 1].content); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const showSubmissionTable = computed(() => props.submissionData !== undefined && props.submissionData.length > 0); | ||||||
|  | 
 | ||||||
|  |     const showIsDoneMessage = computed(() => lastSubmission.value !== undefined && lastSubmission.value.length === 0); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <using-query-result | ||||||
|  |         :query-result="submissionQuery" | ||||||
|  |         v-slot="submissions: { data: SubmissionDTO[] }" | ||||||
|  |     > | ||||||
|  |         <submit-button | ||||||
|  |             :hruid="props.hruid" | ||||||
|  |             :language="props.language" | ||||||
|  |             :version="props.version" | ||||||
|  |             :group="props.group" | ||||||
|  |             :submission-data="props.submissionData" | ||||||
|  |             :submissions="submissions.data" | ||||||
|  |         /> | ||||||
|  |         <div class="submit-submissions-spacer"></div> | ||||||
|  |         <v-alert | ||||||
|  |             icon="mdi-check" | ||||||
|  |             :text="t('taskCompleted')" | ||||||
|  |             type="success" | ||||||
|  |             variant="tonal" | ||||||
|  |             density="compact" | ||||||
|  |             v-if="showIsDoneMessage" | ||||||
|  |         ></v-alert> | ||||||
|  |         <learning-object-submissions-table | ||||||
|  |             v-if="submissionQuery.data && showSubmissionTable" | ||||||
|  |             :all-submissions="submissions.data" | ||||||
|  |             @submission-selected="emitSubmission" | ||||||
|  |         /> | ||||||
|  |     </using-query-result> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  |     .submit-submissions-spacer { | ||||||
|  |         height: 20px; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,98 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  |     import { computed } from "vue"; | ||||||
|  |     import authService from "@/services/auth/auth-service.ts"; | ||||||
|  |     import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data"; | ||||||
|  |     import { Language } from "@/data-objects/language.ts"; | ||||||
|  |     import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission"; | ||||||
|  |     import { useCreateSubmissionMutation } from "@/queries/submissions.ts"; | ||||||
|  |     import { deepEquals } from "@/utils/deep-equals.ts"; | ||||||
|  |     import type { UserProfile } from "oidc-client-ts"; | ||||||
|  |     import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; | ||||||
|  |     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||||
|  |     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||||
|  |     import { useI18n } from "vue-i18n"; | ||||||
|  | 
 | ||||||
|  |     const { t } = useI18n(); | ||||||
|  | 
 | ||||||
|  |     const props = defineProps<{ | ||||||
|  |         submissionData?: SubmissionData; | ||||||
|  |         submissions: SubmissionDTO[]; | ||||||
|  |         hruid: string; | ||||||
|  |         language: Language; | ||||||
|  |         version: number; | ||||||
|  |         group: { forGroup: number; assignmentNo: number; classId: string }; | ||||||
|  |     }>(); | ||||||
|  | 
 | ||||||
|  |     const { | ||||||
|  |         isPending: submissionIsPending, | ||||||
|  |         // - isError: submissionFailed, | ||||||
|  |         // - error: submissionError, | ||||||
|  |         // - isSuccess: submissionSuccess, | ||||||
|  |         mutate: submitSolution, | ||||||
|  |     } = useCreateSubmissionMutation(); | ||||||
|  | 
 | ||||||
|  |     const isStudent = computed(() => authService.authState.activeRole === "student"); | ||||||
|  | 
 | ||||||
|  |     const isSubmitDisabled = computed(() => { | ||||||
|  |         if (!props.submissionData || props.submissions === undefined) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         if (props.submissionData.some((answer) => answer === null)) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         if (props.submissions.length === 0) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         return deepEquals(JSON.parse(props.submissions[props.submissions.length - 1].content), props.submissionData); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     function submitCurrentAnswer(): void { | ||||||
|  |         const { forGroup, assignmentNo, classId } = props.group; | ||||||
|  |         const currentUser: UserProfile = authService.authState.user!.profile; | ||||||
|  |         const learningObjectIdentifier: LearningObjectIdentifierDTO = { | ||||||
|  |             hruid: props.hruid, | ||||||
|  |             language: props.language, | ||||||
|  |             version: props.version, | ||||||
|  |         }; | ||||||
|  |         const submitter: StudentDTO = { | ||||||
|  |             id: currentUser.preferred_username!, | ||||||
|  |             username: currentUser.preferred_username!, | ||||||
|  |             firstName: currentUser.given_name!, | ||||||
|  |             lastName: currentUser.family_name!, | ||||||
|  |         }; | ||||||
|  |         const group: GroupDTO = { | ||||||
|  |             class: classId, | ||||||
|  |             assignment: assignmentNo, | ||||||
|  |             groupNumber: forGroup, | ||||||
|  |         }; | ||||||
|  |         const submission: SubmissionDTO = { | ||||||
|  |             learningObjectIdentifier, | ||||||
|  |             submitter, | ||||||
|  |             group, | ||||||
|  |             content: JSON.stringify(props.submissionData), | ||||||
|  |         }; | ||||||
|  |         submitSolution({ data: submission }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const buttonText = computed(() => { | ||||||
|  |         if (props.submissionData && props.submissionData.length === 0) { | ||||||
|  |             return t("markAsDone"); | ||||||
|  |         } | ||||||
|  |         return t(props.submissions.length > 0 ? "submitNewSolution" : "submitSolution"); | ||||||
|  |     }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |     <v-btn | ||||||
|  |         v-if="isStudent && !isSubmitDisabled" | ||||||
|  |         prepend-icon="mdi-check" | ||||||
|  |         variant="elevated" | ||||||
|  |         :loading="submissionIsPending" | ||||||
|  |         :disabled="isSubmitDisabled" | ||||||
|  |         @click="submitCurrentAnswer()" | ||||||
|  |     > | ||||||
|  |         {{ buttonText }} | ||||||
|  |     </v-btn> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <style scoped></style> | ||||||
Some files were not shown because too many files have changed in this diff Show more
		Reference in a new issue
	
	 Adriaan Jacquet
						Adriaan Jacquet