Merge remote-tracking branch 'origin/dev' into chore/docker

This commit is contained in:
Timo De Meyst 2025-03-09 23:49:10 +01:00
commit f6859b6748
51 changed files with 212 additions and 591 deletions

View file

@ -10,7 +10,7 @@ Projectopgave</a></span>
</p>
<ul align="center" style="list-style-type: none">
<li>Projectleider: Fransisco Van Langenhove (<a href="https://github.com/Gabriellvl">@Gabriellvl</a>)</li>
<li>Projectleider: Fransisco Gabriel Van Langenhove (<a href="https://github.com/Gabriellvl">@Gabriellvl</a>)</li>
<li>Technische lead: Tibo De Peuter (<a href="https://github.com/tdpeuter">@tdpeuter</a>)</li>
<li>Systeembeheerder: Timo De Meyst (<a href="https://github.com/kloep1">@kloep1</a>)</li>
<li>Customer relations officer: Adriaan Jacquet (<a href="https://github.com/WhisperinCheetah">@WhisperinCheetah</a>)</li>
@ -21,17 +21,28 @@ en lessen kunnen samenstellen hun leerlingen en hun vooruitgang kunnen opvolgen.
## Installatie
Om de applicatie in te stellen voor een productieomgeving, volg de [installatiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving).
Alternatief kan je één van de volgende methodes gebruiken om de applicatie lokaal te draaien.
### Quick start
1. Installeer Docker en Docker Compose op je systeem (zie [Docker](https://docs.docker.com/get-docker/) en [Docker Compose](https://docs.docker.com/compose/)).
2. Clone deze repository.
3. Voer `docker compose up` uit in de root van de repository.
3. In de backend, kopieer `.env.example` (of `.env.development.example`) naar `.env` en pas de variabelen aan waar nodig.
4. Voer `docker compose up` uit in de root van de repository.
5. Optioneel: Configureer de applicatie aan de hand van de [configuratiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving#dwengo-1-configuratie).
```bash
docker compose version
git clone https://github.com/SELab-2/Dwengo-1.git
cd Dwengo-1
cd Dwengo-1/backend
cp .env.example .env
# Pas .env aan
nano .env
cd ..
docker compose up
# Configureer de applicatie
```
### Handmatige installatie
@ -46,8 +57,9 @@ De tech-stack bestaat uit:
- **Frontend**: TypeScript + Vue.js + Vuetify
- **Backend**: TypeScript + Node.js + Express.js + TypeORM + PostgreSQL
- **Identity provider**: Keycloak
Voor meer informatie over de keuze van deze tech-stack, zie [designkeuzes](https://github.com/SELab-2/Dwengo-1/wiki/Design-keuzes).
Voor meer informatie over de keuze van deze tech-stack, zie [designkeuzes](https://github.com/SELab-2/Dwengo-1/wiki/Developer:-Design-keuzes).
## Bijdragen aan Dwengo-1

BIN
assets/img/keycloak.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View file

@ -20,3 +20,10 @@ npm run dev
npm run build
npm run start
```
## Keycloak configuratie
Tijdens development is het voldoende om gebruik te maken van de keycloak configuratie die automatisch ingeladen wordt.
Voor productie is het ten sterkste aangeraden om keycloak manueel te configureren.
Voor meer informatie, zie de [administrator-handleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving#installatie-en-server-configuratie).

View file

@ -1,10 +0,0 @@
// Can be placed in dotenv but found it redundant
// Import dotenv from "dotenv";
// Load .env file
// Dotenv.config();
export const DWENGO_API_BASE = 'https://dwengo.org/backend/api';
export const FALLBACK_LANG = 'nl';

View file

@ -6,7 +6,5 @@ export const DWENGO_API_BASE: string = 'https://dwengo.org/backend/api';
// Logging
export const LOG_LEVEL: string =
'development' === process.env.NODE_ENV ? 'debug' : 'info';
export const LOKI_HOST: string =
process.env.LOKI_HOST || 'http://localhost:3102';
export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info';
export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102';

View file

@ -1,17 +1,10 @@
import { Request, Response } from 'express';
import {
getLearningObjectById,
getLearningObjectIdsFromPath,
getLearningObjectsFromPath,
} from '../services/learningObjects.js';
import { getLearningObjectById, getLearningObjectIdsFromPath, getLearningObjectsFromPath } from '../services/learningObjects.js';
import { FALLBACK_LANG } from '../config.js';
import { FilteredLearningObject } from '../interfaces/learningPath.js';
import { getLogger } from '../logging/initalize.js';
export async function getAllLearningObjects(
req: Request,
res: Response
): Promise<void> {
export async function getAllLearningObjects(req: Request, res: Response): Promise<void> {
try {
const hruid = req.query.hruid as string;
const full = req.query.full === 'true';
@ -26,10 +19,7 @@ export async function getAllLearningObjects(
if (full) {
learningObjects = await getLearningObjectsFromPath(hruid, language);
} else {
learningObjects = await getLearningObjectIdsFromPath(
hruid,
language
);
learningObjects = await getLearningObjectIdsFromPath(hruid, language);
}
res.json(learningObjects);
@ -39,10 +29,7 @@ export async function getAllLearningObjects(
}
}
export async function getLearningObject(
req: Request,
res: Response
): Promise<void> {
export async function getLearningObject(req: Request, res: Response): Promise<void> {
try {
const { hruid } = req.params;
const language = (req.query.language as string) || FALLBACK_LANG;

View file

@ -1,18 +1,12 @@
import { Request, Response } from 'express';
import { themes } from '../data/themes.js';
import { FALLBACK_LANG } from '../config.js';
import {
fetchLearningPaths,
searchLearningPaths,
} from '../services/learningPaths.js';
import { fetchLearningPaths, searchLearningPaths } from '../services/learningPaths.js';
import { getLogger } from '../logging/initalize.js';
/**
* Fetch learning paths based on query parameters.
*/
export async function getLearningPaths(
req: Request,
res: Response
): Promise<void> {
export async function getLearningPaths(req: Request, res: Response): Promise<void> {
try {
const hruids = req.query.hruid;
const themeKey = req.query.theme as string;
@ -22,13 +16,9 @@ export async function getLearningPaths(
let hruidList;
if (hruids) {
hruidList = Array.isArray(hruids)
? hruids.map(String)
: [String(hruids)];
hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)];
} else if (themeKey) {
const theme = themes.find((t) => {
return t.title === themeKey;
});
const theme = themes.find((t) => t.title === themeKey);
if (theme) {
hruidList = theme.hruids;
} else {
@ -38,29 +28,17 @@ export async function getLearningPaths(
return;
}
} else if (searchQuery) {
const searchResults = await searchLearningPaths(
searchQuery,
language
);
const searchResults = await searchLearningPaths(searchQuery, language);
res.json(searchResults);
return;
} else {
hruidList = themes.flatMap((theme) => {
return theme.hruids;
});
hruidList = themes.flatMap((theme) => theme.hruids);
}
const learningPaths = await fetchLearningPaths(
hruidList,
language,
`HRUIDs: ${hruidList.join(', ')}`
);
const learningPaths = await fetchLearningPaths(hruidList, language, `HRUIDs: ${hruidList.join(', ')}`);
res.json(learningPaths.data);
} catch (error) {
getLogger().error(
'❌ Unexpected error fetching learning paths:',
error
);
getLogger().error('❌ Unexpected error fetching learning paths:', error);
res.status(500).json({ error: 'Internal server error' });
}
}

View file

@ -11,24 +11,19 @@ interface Translations {
export function getThemes(req: Request, res: Response) {
const language = (req.query.language as string)?.toLowerCase() || 'nl';
const translations = loadTranslations<Translations>(language);
const themeList = themes.map((theme) => {
return {
key: theme.title,
title:
translations.curricula_page[theme.title]?.title || theme.title,
description: translations.curricula_page[theme.title]?.description,
image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
};
});
const themeList = themes.map((theme) => ({
key: theme.title,
title: translations.curricula_page[theme.title]?.title || theme.title,
description: translations.curricula_page[theme.title]?.description,
image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
}));
res.json(themeList);
}
export function getThemeByTitle(req: Request, res: Response) {
const themeKey = req.params.theme;
const theme = themes.find((t) => {
return t.title === themeKey;
});
const theme = themes.find((t) => t.title === themeKey);
if (theme) {
res.json(theme.hruids);

View file

@ -3,10 +3,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Class } from '../../entities/classes/class.entity.js';
export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
public findByClassAndId(
within: Class,
id: number
): Promise<Assignment | null> {
public findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
return this.findOne({ within: within, id: id });
}
public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {

View file

@ -3,24 +3,16 @@ import { Group } from '../../entities/assignments/group.entity.js';
import { Assignment } from '../../entities/assignments/assignment.entity.js';
export class GroupRepository extends DwengoEntityRepository<Group> {
public findByAssignmentAndGroupNumber(
assignment: Assignment,
groupNumber: number
): Promise<Group | null> {
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
return this.findOne({
assignment: assignment,
groupNumber: groupNumber,
});
}
public findAllGroupsForAssignment(
assignment: Assignment
): Promise<Group[]> {
public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
return this.findAll({ where: { assignment: assignment } });
}
public deleteByAssignmentAndGroupNumber(
assignment: Assignment,
groupNumber: number
) {
public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) {
return this.deleteWhere({
assignment: assignment,
groupNumber: groupNumber,

View file

@ -5,10 +5,7 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object
import { Student } from '../../entities/users/student.entity.js';
export class SubmissionRepository extends DwengoEntityRepository<Submission> {
public findSubmissionByLearningObjectAndSubmissionNumber(
loId: LearningObjectIdentifier,
submissionNumber: number
): Promise<Submission | null> {
public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission | null> {
return this.findOne({
learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language,
@ -17,10 +14,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
});
}
public findMostRecentSubmissionForStudent(
loId: LearningObjectIdentifier,
submitter: Student
): Promise<Submission | null> {
public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> {
return this.findOne(
{
learningObjectHruid: loId.hruid,
@ -32,10 +26,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
);
}
public findMostRecentSubmissionForGroup(
loId: LearningObjectIdentifier,
group: Group
): Promise<Submission | null> {
public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> {
return this.findOne(
{
learningObjectHruid: loId.hruid,
@ -47,10 +38,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
);
}
public deleteSubmissionByLearningObjectAndSubmissionNumber(
loId: LearningObjectIdentifier,
submissionNumber: number
): Promise<void> {
public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
return this.deleteWhere({
learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language,

View file

@ -4,24 +4,16 @@ import { TeacherInvitation } from '../../entities/classes/teacher-invitation.ent
import { Teacher } from '../../entities/users/teacher.entity.js';
export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> {
public findAllInvitationsForClass(
clazz: Class
): Promise<TeacherInvitation[]> {
public findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> {
return this.findAll({ where: { class: clazz } });
}
public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
return this.findAll({ where: { sender: sender } });
}
public findAllInvitationsFor(
receiver: Teacher
): Promise<TeacherInvitation[]> {
public findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> {
return this.findAll({ where: { receiver: receiver } });
}
public deleteBy(
clazz: Class,
sender: Teacher,
receiver: Teacher
): Promise<void> {
public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
return this.deleteWhere({
sender: sender,
receiver: receiver,

View file

@ -3,10 +3,7 @@ import { Attachment } from '../../entities/content/attachment.entity.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js';
export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
public findByLearningObjectAndNumber(
learningObject: LearningObject,
sequenceNumber: number
) {
public findByLearningObjectAndNumber(learningObject: LearningObject, sequenceNumber: number) {
return this.findOne({
learningObject: learningObject,
sequenceNumber: sequenceNumber,

View file

@ -3,9 +3,7 @@ import { LearningObject } from '../../entities/content/learning-object.entity.js
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
public findByIdentifier(
identifier: LearningObjectIdentifier
): Promise<LearningObject | null> {
public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
return this.findOne({
hruid: identifier.hruid,
language: identifier.language,

View file

@ -3,10 +3,7 @@ import { LearningPath } from '../../entities/content/learning-path.entity.js';
import { Language } from '../../entities/content/language.js';
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
public findByHruidAndLanguage(
hruid: string,
language: Language
): Promise<LearningPath | null> {
public findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
return this.findOne({ hruid: hruid, language: language });
}
// This repository is read-only for now since creating own learning object is an extension feature.

View file

@ -1,8 +1,6 @@
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
export abstract class DwengoEntityRepository<
T extends object,
> extends EntityRepository<T> {
export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> {
public async save(entity: T) {
const em = this.getEntityManager();
em.persist(entity);

View file

@ -4,11 +4,7 @@ import { Question } from '../../entities/questions/question.entity.js';
import { Teacher } from '../../entities/users/teacher.entity.js';
export class AnswerRepository extends DwengoEntityRepository<Answer> {
public createAnswer(answer: {
toQuestion: Question;
author: Teacher;
content: string;
}): Promise<Answer> {
public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
const answerEntity = new Answer();
answerEntity.toQuestion = answer.toQuestion;
answerEntity.author = answer.author;
@ -21,10 +17,7 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> {
orderBy: { sequenceNumber: 'ASC' },
});
}
public removeAnswerByQuestionAndSequenceNumber(
question: Question,
sequenceNumber: number
): Promise<void> {
public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
return this.deleteWhere({
toQuestion: question,
sequenceNumber: sequenceNumber,

View file

@ -4,11 +4,7 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object
import { Student } from '../../entities/users/student.entity.js';
export class QuestionRepository extends DwengoEntityRepository<Question> {
public createQuestion(question: {
loId: LearningObjectIdentifier;
author: Student;
content: string;
}): Promise<Question> {
public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
const questionEntity = new Question();
questionEntity.learningObjectHruid = question.loId.hruid;
questionEntity.learningObjectLanguage = question.loId.language;
@ -17,9 +13,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
questionEntity.content = question.content;
return this.insert(questionEntity);
}
public findAllQuestionsAboutLearningObject(
loId: LearningObjectIdentifier
): Promise<Question[]> {
public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
return this.findAll({
where: {
learningObjectHruid: loId.hruid,
@ -31,10 +25,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
},
});
}
public removeQuestionByLearningObjectAndSequenceNumber(
loId: LearningObjectIdentifier,
sequenceNumber: number
): Promise<void> {
public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> {
return this.deleteWhere({
learningObjectHruid: loId.hruid,
learningObjectLanguage: loId.language,

View file

@ -1,9 +1,4 @@
import {
AnyEntity,
EntityManager,
EntityName,
EntityRepository,
} from '@mikro-orm/core';
import { AnyEntity, EntityManager, EntityName, EntityRepository } from '@mikro-orm/core';
import { forkEntityManager } from '../orm.js';
import { StudentRepository } from './users/student-repository.js';
import { Student } from '../entities/users/student.entity.js';
@ -43,9 +38,7 @@ export function transactional<T>(f: () => Promise<T>) {
entityManager?.transactional(f);
}
function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(
entity: EntityName<T>
): () => R {
function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R {
let cachedRepo: R | undefined;
return (): R => {
if (!cachedRepo) {
@ -60,60 +53,24 @@ function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(
/* Users */
export const getUserRepository = repositoryGetter<User, UserRepository>(User);
export const getStudentRepository = repositoryGetter<
Student,
StudentRepository
>(Student);
export const getTeacherRepository = repositoryGetter<
Teacher,
TeacherRepository
>(Teacher);
export const getStudentRepository = repositoryGetter<Student, StudentRepository>(Student);
export const getTeacherRepository = repositoryGetter<Teacher, TeacherRepository>(Teacher);
/* Classes */
export const getClassRepository = repositoryGetter<Class, ClassRepository>(
Class
);
export const getClassJoinRequestRepository = repositoryGetter<
ClassJoinRequest,
ClassJoinRequestRepository
>(ClassJoinRequest);
export const getTeacherInvitationRepository = repositoryGetter<
TeacherInvitation,
TeacherInvitationRepository
>(TeacherInvitationRepository);
export const getClassRepository = repositoryGetter<Class, ClassRepository>(Class);
export const getClassJoinRequestRepository = repositoryGetter<ClassJoinRequest, ClassJoinRequestRepository>(ClassJoinRequest);
export const getTeacherInvitationRepository = repositoryGetter<TeacherInvitation, TeacherInvitationRepository>(TeacherInvitationRepository);
/* Assignments */
export const getAssignmentRepository = repositoryGetter<
Assignment,
AssignmentRepository
>(Assignment);
export const getGroupRepository = repositoryGetter<Group, GroupRepository>(
Group
);
export const getSubmissionRepository = repositoryGetter<
Submission,
SubmissionRepository
>(Submission);
export const getAssignmentRepository = repositoryGetter<Assignment, AssignmentRepository>(Assignment);
export const getGroupRepository = repositoryGetter<Group, GroupRepository>(Group);
export const getSubmissionRepository = repositoryGetter<Submission, SubmissionRepository>(Submission);
/* Questions and answers */
export const getQuestionRepository = repositoryGetter<
Question,
QuestionRepository
>(Question);
export const getAnswerRepository = repositoryGetter<Answer, AnswerRepository>(
Answer
);
export const getQuestionRepository = repositoryGetter<Question, QuestionRepository>(Question);
export const getAnswerRepository = repositoryGetter<Answer, AnswerRepository>(Answer);
/* Learning content */
export const getLearningObjectRepository = repositoryGetter<
LearningObject,
LearningObjectRepository
>(LearningObject);
export const getLearningPathRepository = repositoryGetter<
LearningPath,
LearningPathRepository
>(LearningPath);
export const getAttachmentRepository = repositoryGetter<
Attachment,
AttachmentRepository
>(Assignment);
export const getLearningObjectRepository = repositoryGetter<LearningObject, LearningObjectRepository>(LearningObject);
export const getLearningPathRepository = repositoryGetter<LearningPath, LearningPathRepository>(LearningPath);
export const getAttachmentRepository = repositoryGetter<Attachment, AttachmentRepository>(Assignment);

View file

@ -23,13 +23,7 @@ export const themes: Theme[] = [
},
{
title: 'art',
hruids: [
'pn_werking',
'un_artificiele_intelligentie',
'art1',
'art2',
'art3',
],
hruids: ['pn_werking', 'un_artificiele_intelligentie', 'art1', 'art2', 'art3'],
},
{
title: 'socialrobot',
@ -37,12 +31,7 @@ export const themes: Theme[] = [
},
{
title: 'agriculture',
hruids: [
'pn_werking',
'un_artificiele_intelligentie',
'agri_landbouw',
'agri_lopendeband',
],
hruids: ['pn_werking', 'un_artificiele_intelligentie', 'agri_landbouw', 'agri_lopendeband'],
},
{
title: 'wegostem',
@ -83,16 +72,7 @@ export const themes: Theme[] = [
},
{
title: 'python_programming',
hruids: [
'pn_werking',
'pn_datatypes',
'pn_operatoren',
'pn_structuren',
'pn_functies',
'art2',
'stem_insectbooks',
'un_algoenprog',
],
hruids: ['pn_werking', 'pn_datatypes', 'pn_operatoren', 'pn_structuren', 'pn_functies', 'art2', 'stem_insectbooks', 'un_algoenprog'],
},
{
title: 'stem',
@ -110,15 +90,7 @@ export const themes: Theme[] = [
},
{
title: 'care',
hruids: [
'pn_werking',
'un_artificiele_intelligentie',
'aiz1_zorg',
'aiz2_grafen',
'aiz3_unplugged',
'aiz4_eindtermen',
'aiz5_triage',
],
hruids: ['pn_werking', 'un_artificiele_intelligentie', 'aiz1_zorg', 'aiz2_grafen', 'aiz3_unplugged', 'aiz4_eindtermen', 'aiz5_triage'],
},
{
title: 'chatbot',

View file

@ -1,23 +1,11 @@
import {
Entity,
Enum,
ManyToOne,
OneToMany,
PrimaryKey,
Property,
} from '@mikro-orm/core';
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Class } from '../classes/class.entity.js';
import { Group } from './group.entity.js';
import { Language } from '../content/language.js';
@Entity()
export class Assignment {
@ManyToOne({
entity: () => {
return Class;
},
primary: true,
})
@ManyToOne({ entity: () => Class, primary: true })
within!: Class;
@PrimaryKey({ type: 'number' })
@ -32,18 +20,9 @@ export class Assignment {
@Property({ type: 'string' })
learningPathHruid!: string;
@Enum({
items: () => {
return Language;
},
})
@Enum({ items: () => Language })
learningPathLanguage!: Language;
@OneToMany({
entity: () => {
return Group;
},
mappedBy: 'assignment',
})
@OneToMany({ entity: () => Group, mappedBy: 'assignment' })
groups!: Group[];
}

View file

@ -5,9 +5,7 @@ import { Student } from '../users/student.entity.js';
@Entity()
export class Group {
@ManyToOne({
entity: () => {
return Assignment;
},
entity: () => Assignment,
primary: true,
})
assignment!: Assignment;
@ -16,9 +14,7 @@ export class Group {
groupNumber!: number;
@ManyToMany({
entity: () => {
return Student;
},
entity: () => Student,
})
members!: Student[];
}

View file

@ -9,9 +9,7 @@ export class Submission {
learningObjectHruid!: string;
@Enum({
items: () => {
return Language;
},
items: () => Language,
primary: true,
})
learningObjectLanguage!: Language;
@ -23,9 +21,7 @@ export class Submission {
submissionNumber!: number;
@ManyToOne({
entity: () => {
return Student;
},
entity: () => Student,
})
submitter!: Student;
@ -33,9 +29,7 @@ export class Submission {
submissionTime!: Date;
@ManyToOne({
entity: () => {
return Group;
},
entity: () => Group,
nullable: true,
})
onBehalfOf?: Group;

View file

@ -5,24 +5,18 @@ import { Class } from './class.entity.js';
@Entity()
export class ClassJoinRequest {
@ManyToOne({
entity: () => {
return Student;
},
entity: () => Student,
primary: true,
})
requester!: Student;
@ManyToOne({
entity: () => {
return Class;
},
entity: () => Class,
primary: true,
})
class!: Class;
@Enum(() => {
return ClassJoinRequestStatus;
})
@Enum(() => ClassJoinRequestStatus)
status!: ClassJoinRequestStatus;
}

View file

@ -1,10 +1,4 @@
import {
Collection,
Entity,
ManyToMany,
PrimaryKey,
Property,
} from '@mikro-orm/core';
import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { v4 } from 'uuid';
import { Teacher } from '../users/teacher.entity.js';
import { Student } from '../users/student.entity.js';
@ -17,13 +11,9 @@ export class Class {
@Property({ type: 'string' })
displayName!: string;
@ManyToMany(() => {
return Teacher;
})
@ManyToMany(() => Teacher)
teachers!: Collection<Teacher>;
@ManyToMany(() => {
return Student;
})
@ManyToMany(() => Student)
students!: Collection<Student>;
}

View file

@ -8,25 +8,19 @@ import { Class } from './class.entity.js';
@Entity()
export class TeacherInvitation {
@ManyToOne({
entity: () => {
return Teacher;
},
entity: () => Teacher,
primary: true,
})
sender!: Teacher;
@ManyToOne({
entity: () => {
return Teacher;
},
entity: () => Teacher,
primary: true,
})
receiver!: Teacher;
@ManyToOne({
entity: () => {
return Class;
},
entity: () => Class,
primary: true,
})
class!: Class;

View file

@ -4,9 +4,7 @@ import { LearningObject } from './learning-object.entity.js';
@Entity()
export class Attachment {
@ManyToOne({
entity: () => {
return LearningObject;
},
entity: () => LearningObject,
primary: true,
})
learningObject!: LearningObject;

View file

@ -1,13 +1,4 @@
import {
Embeddable,
Embedded,
Entity,
Enum,
ManyToMany,
OneToMany,
PrimaryKey,
Property,
} from '@mikro-orm/core';
import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Language } from './language.js';
import { Attachment } from './attachment.entity.js';
import { Teacher } from '../users/teacher.entity.js';
@ -18,9 +9,7 @@ export class LearningObject {
hruid!: string;
@Enum({
items: () => {
return Language;
},
items: () => Language,
primary: true,
})
language!: Language;
@ -29,9 +18,7 @@ export class LearningObject {
version: string = '1';
@ManyToMany({
entity: () => {
return Teacher;
},
entity: () => Teacher,
})
admins!: Teacher[];
@ -57,9 +44,7 @@ export class LearningObject {
skosConcepts!: string[];
@Embedded({
entity: () => {
return EducationalGoal;
},
entity: () => EducationalGoal,
array: true,
})
educationalGoals: EducationalGoal[] = [];
@ -77,9 +62,7 @@ export class LearningObject {
estimatedTime!: number;
@Embedded({
entity: () => {
return ReturnValue;
},
entity: () => ReturnValue,
})
returnValue!: ReturnValue;
@ -90,9 +73,7 @@ export class LearningObject {
contentLocation?: string;
@OneToMany({
entity: () => {
return Attachment;
},
entity: () => Attachment,
mappedBy: 'learningObject',
})
attachments: Attachment[] = [];

View file

@ -1,13 +1,4 @@
import {
Embeddable,
Embedded,
Entity,
Enum,
ManyToMany,
OneToOne,
PrimaryKey,
Property,
} from '@mikro-orm/core';
import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToOne, PrimaryKey, Property } from '@mikro-orm/core';
import { Language } from './language.js';
import { Teacher } from '../users/teacher.entity.js';
@ -17,17 +8,13 @@ export class LearningPath {
hruid!: string;
@Enum({
items: () => {
return Language;
},
items: () => Language,
primary: true,
})
language!: Language;
@ManyToMany({
entity: () => {
return Teacher;
},
entity: () => Teacher,
})
admins!: Teacher[];
@ -41,9 +28,7 @@ export class LearningPath {
image!: string;
@Embedded({
entity: () => {
return LearningPathNode;
},
entity: () => LearningPathNode,
array: true,
})
nodes: LearningPathNode[] = [];
@ -55,9 +40,7 @@ export class LearningPathNode {
learningObjectHruid!: string;
@Enum({
items: () => {
return Language;
},
items: () => Language,
})
language!: Language;
@ -71,9 +54,7 @@ export class LearningPathNode {
startNode!: boolean;
@Embedded({
entity: () => {
return LearningPathTransition;
},
entity: () => LearningPathTransition,
array: true,
})
transitions!: LearningPathTransition[];
@ -85,9 +66,7 @@ export class LearningPathTransition {
condition!: string;
@OneToOne({
entity: () => {
return LearningPathNode;
},
entity: () => LearningPathNode,
})
next!: LearningPathNode;
}

View file

@ -5,17 +5,13 @@ import { Teacher } from '../users/teacher.entity.js';
@Entity()
export class Answer {
@ManyToOne({
entity: () => {
return Teacher;
},
entity: () => Teacher,
primary: true,
})
author!: Teacher;
@ManyToOne({
entity: () => {
return Question;
},
entity: () => Question,
primary: true,
})
toQuestion!: Question;

View file

@ -8,9 +8,7 @@ export class Question {
learningObjectHruid!: string;
@Enum({
items: () => {
return Language;
},
items: () => Language,
primary: true,
})
learningObjectLanguage!: Language;
@ -22,9 +20,7 @@ export class Question {
sequenceNumber!: number;
@ManyToOne({
entity: () => {
return Student;
},
entity: () => Student,
})
author!: Student;

View file

@ -5,19 +5,13 @@ import { Group } from '../assignments/group.entity.js';
import { StudentRepository } from '../../data/users/student-repository.js';
@Entity({
repository: () => {
return StudentRepository;
},
repository: () => StudentRepository,
})
export class Student extends User {
@ManyToMany(() => {
return Class;
})
@ManyToMany(() => Class)
classes!: Collection<Class>;
@ManyToMany(() => {
return Group;
})
@ManyToMany(() => Group)
groups!: Collection<Group>;
constructor(

View file

@ -4,8 +4,6 @@ import { Class } from '../classes/class.entity.js';
@Entity()
export class Teacher extends User {
@ManyToMany(() => {
return Class;
})
@ManyToMany(() => Class)
classes!: Collection<Class>;
}

View file

@ -1,9 +1,4 @@
import {
createLogger,
format,
Logger as WinstonLogger,
transports,
} from 'winston';
import { createLogger, format, Logger as WinstonLogger, transports } from 'winston';
import LokiTransport from 'winston-loki';
import { LokiLabels } from 'loki-logger-ts';
import { LOG_LEVEL, LOKI_HOST } from '../config.js';
@ -48,9 +43,7 @@ function initializeLogger(): Logger {
transports: [lokiTransport, consoleTransport],
});
logger.debug(
`Logger initialized with level ${LOG_LEVEL}, Loki host ${LOKI_HOST}`
);
logger.debug(`Logger initialized with level ${LOG_LEVEL}, Loki host ${LOKI_HOST}`);
return logger;
}

View file

@ -12,42 +12,28 @@ export class MikroOrmLogger extends DefaultLogger {
switch (namespace) {
case 'query':
this.logger.debug(
this.createMessage(namespace, message, context)
);
this.logger.debug(this.createMessage(namespace, message, context));
break;
case 'query-params':
// TODO Which log level should this be?
this.logger.info(
this.createMessage(namespace, message, context)
);
this.logger.info(this.createMessage(namespace, message, context));
break;
case 'schema':
this.logger.info(
this.createMessage(namespace, message, context)
);
this.logger.info(this.createMessage(namespace, message, context));
break;
case 'discovery':
this.logger.debug(
this.createMessage(namespace, message, context)
);
this.logger.debug(this.createMessage(namespace, message, context));
break;
case 'info':
this.logger.info(
this.createMessage(namespace, message, context)
);
this.logger.info(this.createMessage(namespace, message, context));
break;
case 'deprecated':
this.logger.warn(
this.createMessage(namespace, message, context)
);
this.logger.warn(this.createMessage(namespace, message, context));
break;
default:
switch (context?.level) {
case 'info':
this.logger.info(
this.createMessage(namespace, message, context)
);
this.logger.info(this.createMessage(namespace, message, context));
break;
case 'warning':
this.logger.warn(message);
@ -62,11 +48,7 @@ export class MikroOrmLogger extends DefaultLogger {
}
}
private createMessage(
namespace: LoggerNamespace,
messageArg: string,
context?: LogContext
) {
private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) {
const labels: LokiLabels = {
service: 'ORM',
};

View file

@ -52,9 +52,7 @@ function config(testingMode: boolean = false): Options {
// Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
// (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
dynamicImportProvider: (id) => {
return import(id);
},
dynamicImportProvider: (id) => import(id),
};
}
@ -70,9 +68,7 @@ function config(testingMode: boolean = false): Options {
// Logging
debug: LOG_LEVEL === 'debug',
loggerFactory: (options: LoggerOptions) => {
return new MikroOrmLogger(options);
},
loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options),
};
}

View file

@ -28,9 +28,7 @@ export async function initORM(testingMode: boolean = false) {
}
export function forkEntityManager(): EntityManager {
if (!orm) {
throw Error(
'Accessing the Entity Manager before the ORM is fully initialized.'
);
throw Error('Accessing the Entity Manager before the ORM is fully initialized.');
}
return orm.em.fork();
}

View file

@ -1,8 +1,5 @@
import express from 'express';
import {
getAllLearningObjects,
getLearningObject,
} from '../controllers/learningObjects.js';
import { getAllLearningObjects, getLearningObject } from '../controllers/learningObjects.js';
const router = express.Router();

View file

@ -15,8 +15,7 @@ router.get('/:id', (req, res) => {
student: '0',
group: '0',
time: new Date(2025, 1, 1),
content:
'Zijn alle gehele getallen groter dan 2 gelijk aan de som van 2 priemgetallen????',
content: 'Zijn alle gehele getallen groter dan 2 gelijk aan de som van 2 priemgetallen????',
learningObject: '0',
links: {
self: `${req.baseUrl}/${req.params.id}`,

View file

@ -1,20 +1,12 @@
import { DWENGO_API_BASE } from '../config.js';
import { fetchWithLogging } from '../util/apiHelper.js';
import {
FilteredLearningObject,
LearningObjectMetadata,
LearningObjectNode,
LearningPathResponse,
} from '../interfaces/learningPath.js';
import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learningPath.js';
import { fetchLearningPaths } from './learningPaths.js';
import { getLogger, Logger } from '../logging/initalize.js';
const logger: Logger = getLogger();
function filterData(
data: LearningObjectMetadata,
htmlUrl: string
): FilteredLearningObject {
function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject {
return {
key: data.hruid, // Hruid learningObject (not path)
_id: data._id,
@ -41,10 +33,7 @@ function filterData(
/**
* Fetches a single learning object by its HRUID
*/
export async function getLearningObjectById(
hruid: string,
language: string
): Promise<FilteredLearningObject | null> {
export async function getLearningObjectById(hruid: string, language: string): Promise<FilteredLearningObject | null> {
const metadataUrl = `${DWENGO_API_BASE}/learningObject/getMetadata?hruid=${hruid}&language=${language}`;
const metadata = await fetchWithLogging<LearningObjectMetadata>(
metadataUrl,
@ -63,49 +52,24 @@ export async function getLearningObjectById(
/**
* Generic function to fetch learning objects (full data or just HRUIDs)
*/
async function fetchLearningObjects(
hruid: string,
full: boolean,
language: string
): Promise<FilteredLearningObject[] | string[]> {
async function fetchLearningObjects(hruid: string, full: boolean, language: string): Promise<FilteredLearningObject[] | string[]> {
try {
const learningPathResponse: LearningPathResponse =
await fetchLearningPaths(
[hruid],
language,
`Learning path for HRUID "${hruid}"`
);
const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`);
if (
!learningPathResponse.success ||
!learningPathResponse.data?.length
) {
logger.warn(
`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`
);
if (!learningPathResponse.success || !learningPathResponse.data?.length) {
logger.warn(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`);
return [];
}
const nodes: LearningObjectNode[] = learningPathResponse.data[0].nodes;
if (!full) {
return nodes.map((node) => {
return node.learningobject_hruid;
});
return nodes.map((node) => node.learningobject_hruid);
}
return await Promise.all(
nodes.map(async (node) => {
return getLearningObjectById(
node.learningobject_hruid,
language
);
})
).then((objects) => {
return objects.filter((obj): obj is FilteredLearningObject => {
return obj !== null;
});
});
return await Promise.all(nodes.map(async (node) => getLearningObjectById(node.learningobject_hruid, language))).then((objects) =>
objects.filter((obj): obj is FilteredLearningObject => obj !== null)
);
} catch (error) {
logger.error('❌ Error fetching learning objects:', error);
return [];
@ -115,23 +79,13 @@ async function fetchLearningObjects(
/**
* Fetch full learning object data (metadata)
*/
export async function getLearningObjectsFromPath(
hruid: string,
language: string
): Promise<FilteredLearningObject[]> {
return (await fetchLearningObjects(
hruid,
true,
language
)) as FilteredLearningObject[];
export async function getLearningObjectsFromPath(hruid: string, language: string): Promise<FilteredLearningObject[]> {
return (await fetchLearningObjects(hruid, true, language)) as FilteredLearningObject[];
}
/**
* Fetch only learning object HRUIDs
*/
export async function getLearningObjectIdsFromPath(
hruid: string,
language: string
): Promise<string[]> {
export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> {
return (await fetchLearningObjects(hruid, false, language)) as string[];
}

View file

@ -1,18 +1,11 @@
import { fetchWithLogging } from '../util/apiHelper.js';
import { DWENGO_API_BASE } from '../config.js';
import {
LearningPath,
LearningPathResponse,
} from '../interfaces/learningPath.js';
import { LearningPath, LearningPathResponse } from '../interfaces/learningPath.js';
import { getLogger, Logger } from '../logging/initalize.js';
const logger: Logger = getLogger();
export async function fetchLearningPaths(
hruids: string[],
language: string,
source: string
): Promise<LearningPathResponse> {
export async function fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse> {
if (hruids.length === 0) {
return {
success: false,
@ -25,11 +18,7 @@ export async function fetchLearningPaths(
const apiUrl = `${DWENGO_API_BASE}/learningPath/getPathsFromIdList`;
const params = { pathIdList: JSON.stringify({ hruids }), language };
const learningPaths = await fetchWithLogging<LearningPath[]>(
apiUrl,
`Learning paths for ${source}`,
params
);
const learningPaths = await fetchWithLogging<LearningPath[]>(apiUrl, `Learning paths for ${source}`, params);
if (!learningPaths || learningPaths.length === 0) {
logger.warn(`⚠️ WARNING: No learning paths found for ${source}.`);
@ -48,17 +37,10 @@ export async function fetchLearningPaths(
};
}
export async function searchLearningPaths(
query: string,
language: string
): Promise<LearningPath[]> {
export async function searchLearningPaths(query: string, language: string): Promise<LearningPath[]> {
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
const params = { all: query, language };
const searchResults = await fetchWithLogging<LearningPath[]>(
apiUrl,
`Search learning paths with query "${query}"`,
params
);
const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, params);
return searchResults ?? [];
}

View file

@ -12,11 +12,7 @@ const logger: Logger = getLogger();
* @param params
* @returns The response data if successful, or null if an error occurs.
*/
export async function fetchWithLogging<T>(
url: string,
description: string,
params?: Record<string, any>
): Promise<T | null> {
export async function fetchWithLogging<T>(url: string, description: string, params?: Record<string, any>): Promise<T | null> {
try {
const config: AxiosRequestConfig = params ? { params } : {};
@ -25,19 +21,14 @@ export async function fetchWithLogging<T>(
} catch (error: any) {
if (error.response) {
if (error.response.status === 404) {
logger.debug(
`❌ ERROR: ${description} not found (404) at "${url}".`
);
logger.debug(`❌ ERROR: ${description} not found (404) at "${url}".`);
} else {
logger.debug(
`❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")`
);
}
} else {
logger.debug(
`❌ ERROR: Network or unexpected error when fetching ${description}:`,
error.message
);
logger.debug(`❌ ERROR: Network or unexpected error when fetching ${description}:`, error.message);
}
return null;
}

View file

@ -36,9 +36,7 @@ export function getNumericEnvVar(envVar: EnvVar): number {
const valueString = getEnvVar(envVar);
const value = parseInt(valueString);
if (isNaN(value)) {
throw new Error(
`Invalid value for environment variable ${envVar.key}: ${valueString}. Expected a number.`
);
throw new Error(`Invalid value for environment variable ${envVar.key}: ${valueString}. Expected a number.`);
} else {
return value;
}

View file

@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import yaml from 'js-yaml';
import { FALLBACK_LANG } from '../../config.js';
import { FALLBACK_LANG } from '../config.js';
import { getLogger, Logger } from '../logging/initalize.js';
const logger: Logger = getLogger();
@ -12,15 +12,8 @@ export function loadTranslations<T>(language: string): T {
const yamlFile = fs.readFileSync(filePath, 'utf8');
return yaml.load(yamlFile) as T;
} catch (error) {
logger.warn(
`Cannot load translation for ${language}, fallen back to dutch`,
error
);
const fallbackPath = path.join(
process.cwd(),
'_i18n',
`${FALLBACK_LANG}.yml`
);
logger.warn(`Cannot load translation for ${language}, fallen back to dutch`, error);
const fallbackPath = path.join(process.cwd(), '_i18n', `${FALLBACK_LANG}.yml`);
return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T;
}
}

View file

@ -16,12 +16,9 @@ describe('StudentRepository', () => {
});
it('should return the queried student after he was added', async () => {
await studentRepository.insert(
new Student(username, firstName, lastName)
);
await studentRepository.insert(new Student(username, firstName, lastName));
const retrievedStudent =
await studentRepository.findByUsername(username);
const retrievedStudent = await studentRepository.findByUsername(username);
expect(retrievedStudent).toBeTruthy();
expect(retrievedStudent?.firstName).toBe(firstName);
expect(retrievedStudent?.lastName).toBe(lastName);
@ -30,8 +27,7 @@ describe('StudentRepository', () => {
it('should no longer return the queried student after he was removed again', async () => {
await studentRepository.deleteByUsername(username);
const retrievedStudent =
await studentRepository.findByUsername(username);
const retrievedStudent = await studentRepository.findByUsername(username);
expect(retrievedStudent).toBeNull();
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Before After
Before After

View file

@ -1,30 +1,49 @@
from diagrams import Cluster, Diagram
from diagrams import Cluster, Diagram, Edge
from diagrams.custom import Custom
from diagrams.onprem.certificates import LetsEncrypt
from diagrams.onprem.container import Docker
from diagrams.onprem.database import PostgreSQL
from diagrams.onprem.logging import Loki
from diagrams.onprem.monitoring import Grafana
from diagrams.onprem.network import Nginx
from diagrams.programming.flowchart import InputOutput
from diagrams.programming.framework import Vue
from diagrams.programming.language import Nodejs
from diagrams.programming.flowchart import InputOutput
with Diagram("Dwengo-1 architectuur", filename="docs/architecture/schema", show=False):
reverse_proxy = Nginx("reverse proxy")
reverse_proxy >> LetsEncrypt("SSL")
ingress = Nginx("Reverse Proxy")
certificates = LetsEncrypt("SSL")
with Cluster("Docker"):
Docker()
frontend = Vue("/")
backend = Nodejs("/api")
reverse_proxy >> frontend
frontend >> backend >> InputOutput("MikroORM") >> PostgreSQL()
backend >> Loki("logging") >> Grafana("monitoring")
with Cluster("Dwengo"):
with Cluster("Dwengo VZW"):
dwengo = Custom("Dwengo", "../../assets/img/dwengo-groen-zwart.png")
backend >> dwengo
with Cluster("Dwengo-1"):
frontend = Vue("/")
backend = Nodejs("/api")
identity_provider = Custom("IDP", "../../assets/img/keycloak.png")
database = PostgreSQL("Database")
orm = InputOutput("MikroORM")
orm >> Edge(label="map") << database
with Cluster("Observability"):
logging = Loki("Logging")
logging << Edge(color="firebrick", style="dashed") << Grafana("Monitoring")
dependencies = [
dwengo,
logging,
orm
]
backend >> dependencies
service = [
frontend,
backend,
identity_provider,
certificates
]
ingress \
>> Edge(color="darkgreen") \
<< service

View file

@ -16,12 +16,7 @@ export default [
prettierConfig,
includeIgnoreFile(gitignorePath),
{
ignores: [
'**/dist/**',
'**/.node_modules/**',
'**/coverage/**',
'**/.github/**',
],
ignores: ['**/dist/**', '**/.node_modules/**', '**/coverage/**', '**/.github/**'],
files: ['**/*.ts', '**/*.cts', '**.*.mts', '**/*.ts'],
},
{
@ -43,8 +38,9 @@ export default [
'no-unreachable-loop': 'warn',
'no-use-before-define': 'error',
'no-useless-assignment': 'error',
'no-unused-vars': 'error',
'arrow-body-style': ['warn', 'always'],
'arrow-body-style': ['warn', 'as-needed'],
'block-scoped-var': 'warn',
camelcase: 'warn',
'capitalized-comments': 'warn',

View file

@ -14,6 +14,9 @@ const vueConfig = defineConfigWithVueTs(
{
name: "app/files-to-lint",
files: ["**/*.{ts,mts,tsx,vue}"],
rules: {
"no-useless-assignment": "off", // Depend on `no-unused-vars` to catch this
},
},
{

View file

@ -22,16 +22,12 @@ const router = createRouter({
{
path: "/",
name: "home",
component: () => {
return import("../views/HomePage.vue");
},
component: () => import("../views/HomePage.vue"),
},
{
path: "/login",
name: "LoginPage",
component: () => {
return import("../views/LoginPage.vue");
},
component: () => import("../views/LoginPage.vue"),
},
{
path: "/student/:id",

View file

@ -2,7 +2,7 @@
* @type {import("prettier").Options}
*/
export default {
printWidth: 80,
printWidth: 150,
semi: true,
singleQuote: true,
trailingComma: 'es5',