fix: problee m met mergen en zorgen student Gerald in db komt bij npm run dev runnen

This commit is contained in:
laurejablonski 2025-04-04 18:18:50 +02:00
parent 1ffd77cce7
commit 8218aae6ec
10 changed files with 372 additions and 62 deletions

View file

@ -7,7 +7,7 @@
"main": "dist/app.js", "main": "dist/app.js",
"scripts": { "scripts": {
"build": "cross-env NODE_ENV=production tsc --build", "build": "cross-env NODE_ENV=production tsc --build",
"dev": "cross-env NODE_ENV=development tsx watch --env-file=.env.development.local src/app.ts", "dev": "cross-env NODE_ENV=development tsx seed.ts; tsx watch --env-file=.env.development.local src/app.ts",
"start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js", "start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js",
"format": "prettier --write src/", "format": "prettier --write src/",
"format-check": "prettier --check src/", "format-check": "prettier --check src/",

69
backend/seed.ts Normal file
View file

@ -0,0 +1,69 @@
import { forkEntityManager, initORM } from "./src/orm.js";
import dotenv from 'dotenv';
import { makeTestAssignemnts } from "./tests/test_assets/assignments/assignments.testdata.js";
import { makeTestGroups } from "./tests/test_assets/assignments/groups.testdata.js";
import { makeTestSubmissions } from "./tests/test_assets/assignments/submission.testdata.js";
import { makeTestClassJoinRequests } from "./tests/test_assets/classes/class-join-requests.testdata.js";
import { makeTestClasses } from "./tests/test_assets/classes/classes.testdata.js";
import { makeTestTeacherInvitations } from "./tests/test_assets/classes/teacher-invitations.testdata.js";
import { makeTestAttachments } from "./tests/test_assets/content/attachments.testdata.js";
import { makeTestLearningObjects } from "./tests/test_assets/content/learning-objects.testdata.js";
import { makeTestLearningPaths } from "./tests/test_assets/content/learning-paths.testdata.js";
import { makeTestAnswers } from "./tests/test_assets/questions/answers.testdata.js";
import { makeTestQuestions } from "./tests/test_assets/questions/questions.testdata.js";
import { makeTestStudents } from "./tests/test_assets/users/students.testdata.js";
import { makeTestTeachers } from "./tests/test_assets/users/teachers.testdata.js";
export async function seedDatabase() {
dotenv.config({ path: '.env.development.local' });
const orm = await initORM();
await orm.schema.clearDatabase();
const em = forkEntityManager();
console.log("seeding database...")
const students = makeTestStudents(em);
const teachers = makeTestTeachers(em);
const learningObjects = makeTestLearningObjects(em);
const learningPaths = makeTestLearningPaths(em);
const classes = makeTestClasses(em, students, teachers);
const assignments = makeTestAssignemnts(em, classes);
const groups = makeTestGroups(em, students, assignments);
assignments[0].groups = groups.slice(0, 3);
assignments[1].groups = groups.slice(3, 4);
const teacherInvitations = makeTestTeacherInvitations(em, teachers, classes);
const classJoinRequests = makeTestClassJoinRequests(em, students, classes);
const attachments = makeTestAttachments(em, learningObjects);
learningObjects[1].attachments = attachments;
const questions = makeTestQuestions(em, students);
const answers = makeTestAnswers(em, teachers, questions);
const submissions = makeTestSubmissions(em, students, groups);
// Persist all entities
await em.persistAndFlush([
...students,
...teachers,
...learningObjects,
...learningPaths,
...classes,
...assignments,
...groups,
...teacherInvitations,
...classJoinRequests,
...attachments,
...questions,
...answers,
...submissions,
]);
console.log('Development database seeded successfully!');
await orm.close();
}
seedDatabase().catch(console.error);

View file

@ -1,10 +1,10 @@
import { EntityManager, MikroORM } from '@mikro-orm/core'; import { Connection, EntityManager, IDatabaseDriver, MikroORM } from '@mikro-orm/core';
import config from './mikro-orm.config.js'; import config from './mikro-orm.config.js';
import { envVars, getEnvVar } from './util/envVars.js'; import { envVars, getEnvVar } from './util/envVars.js';
import { getLogger, Logger } from './logging/initalize.js'; import { getLogger, Logger } from './logging/initalize.js';
let orm: MikroORM | undefined; let orm: MikroORM | undefined;
export async function initORM(testingMode = false): Promise<void> { export async function initORM(testingMode = false): Promise<MikroORM<IDatabaseDriver<Connection>, EntityManager<IDatabaseDriver<Connection>>>> {
const logger: Logger = getLogger(); const logger: Logger = getLogger();
logger.info('Initializing ORM'); logger.info('Initializing ORM');
@ -25,6 +25,8 @@ export async function initORM(testingMode = false): Promise<void> {
); );
} }
} }
return orm;
} }
export function forkEntityManager(): EntityManager { export function forkEntityManager(): EntityManager {
if (!orm) { if (!orm) {

View file

@ -1,5 +1,6 @@
import { EntityManager } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/core';
import { Student } from '../../../src/entities/users/student.entity'; import { Student } from '../../../src/entities/users/student.entity';
import { fixupRule } from '@eslint/compat';
// 🔓 Ruwe testdata array — herbruikbaar in assertions // 🔓 Ruwe testdata array — herbruikbaar in assertions
export const TEST_STUDENTS = [ export const TEST_STUDENTS = [
@ -11,6 +12,8 @@ export const TEST_STUDENTS = [
{ username: 'TheDoors', firstName: 'Jim', lastName: 'Morisson' }, { username: 'TheDoors', firstName: 'Jim', lastName: 'Morisson' },
// ⚠️ Deze mag niet gebruikt worden in elke test! // ⚠️ Deze mag niet gebruikt worden in elke test!
{ username: 'Nirvana', firstName: 'Kurt', lastName: 'Cobain' }, { username: 'Nirvana', firstName: 'Kurt', lastName: 'Cobain' },
// makes sure when logged in as leerling1, there exists a corresponding user
{ username: 'testleerling1', firstName: 'Gerald', lastName: 'Schmittinger'},
]; ];
// 🏗️ Functie die ORM entities maakt uit de data array // 🏗️ Functie die ORM entities maakt uit de data array

View file

@ -1,6 +1,7 @@
import { ThemeController } from "@/controllers/themes.ts"; import { ThemeController } from "@/controllers/themes.ts";
import { LearningObjectController } from "@/controllers/learning-objects.ts"; import { LearningObjectController } from "@/controllers/learning-objects.ts";
import { LearningPathController } from "@/controllers/learning-paths.ts"; import { LearningPathController } from "@/controllers/learning-paths.ts";
import { ClassController } from "@/controllers/classes.ts";
export function controllerGetter<T>(factory: new () => T): () => T { export function controllerGetter<T>(factory: new () => T): () => T {
let instance: T | undefined; let instance: T | undefined;
@ -16,3 +17,4 @@ export function controllerGetter<T>(factory: new () => T): () => T {
export const getThemeController = controllerGetter(ThemeController); export const getThemeController = controllerGetter(ThemeController);
export const getLearningObjectController = controllerGetter(LearningObjectController); export const getLearningObjectController = controllerGetter(LearningObjectController);
export const getLearningPathController = controllerGetter(LearningPathController); export const getLearningPathController = controllerGetter(LearningPathController);
export const getClassController = controllerGetter(ClassController);

View file

@ -56,5 +56,20 @@
"noLearningPathsFoundDescription": "Es gibt keine Lernpfade, die zu Ihrem Suchbegriff passen.", "noLearningPathsFoundDescription": "Es gibt keine Lernpfade, die zu Ihrem Suchbegriff passen.",
"legendNotCompletedYet": "Noch nicht fertig", "legendNotCompletedYet": "Noch nicht fertig",
"legendCompleted": "Fertig", "legendCompleted": "Fertig",
"legendTeacherExclusive": "Information für Lehrkräfte" "legendTeacherExclusive": "Information für Lehrkräfte",
"code": "code",
"class": "klasse",
"invitations": "einladungen",
"createClass" : "Klasse erstellen",
"createClassInstructions": "Geben Sie einen Namen für Ihre Klasse ein und klicken Sie auf „Erstellen“. Es erscheint ein Fenster mit einem Code, den Sie kopieren können. Geben Sie diesen Code an Ihre Schüler weiter und sie können Ihrer Klasse beitreten.",
"classname": "Klassenname",
"EnterNameOfClass": "einen Klassennamen eingeben.",
"create": "erstellen",
"sender": "Absender",
"nameIsMandatory": "classname ist obligatorisch",
"onlyUse" : "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
"close": "schließen",
"copied": "kopiert!",
"accept": "akzeptieren",
"deny": "verweigern"
} }

View file

@ -56,5 +56,20 @@
"high-school": "16-18 years old", "high-school": "16-18 years old",
"older": "18 and older" "older": "18 and older"
}, },
"read-more": "Read more" "read-more": "Read more",
"code" : "code",
"class": "class",
"invitations": "invitations",
"createClass" : "create class",
"classname": "classname",
"EnterNameOfClass": "Enter a classname.",
"create": "create",
"sender": "sender",
"nameIsMandatory": "classname is mandatory",
"onlyUse" : "only use letters, numbers, dashes (-) and underscores (_)",
"close": "close",
"copied": "copied!",
"accept": "accept",
"deny": "deny",
"createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join."
} }

View file

@ -56,5 +56,20 @@
"high-school": "16-18 ans", "high-school": "16-18 ans",
"older": "18 et plus" "older": "18 et plus"
}, },
"read-more": "En savoir plus" "read-more": "En savoir plus",
"code": "code",
"class": "classe",
"invitations": "invitations",
"createClass" : "créer une classe",
"createClassInstructions": "Entrez un nom pour votre classe et cliquez sur créer. Une fenêtre apparaît avec un code que vous pouvez copier. Donnez ce code à vos élèves et ils pourront rejoindre votre classe.",
"classname": "nom de classe",
"EnterNameOfClass": "saisir un nom de classe.",
"create": "créer",
"sender": "expéditeur",
"nameIsMandatory": "le nom de classe est obligatoire",
"onlyUse" : "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
"close": "fermer",
"copied": "copié!",
"accept": "accepter",
"deny": "refuser"
} }

View file

@ -56,5 +56,20 @@
"high-school": "3e graad secundair", "high-school": "3e graad secundair",
"older": "Hoger onderwijs" "older": "Hoger onderwijs"
}, },
"read-more": "Lees meer" "read-more": "Lees meer",
"code": "code",
"class": "klas",
"invitations": "uitnodigingen",
"createClass" : "klas aanmaken",
"createClassInstructions": "Voer een naam in voor je klas en klik op create. Er verschijnt een venster met een code die je kunt kopiëren. Geef deze code aan je leerlingen en ze kunnen deelnemen aan je klas.",
"classname": "klasnaam",
"EnterNameOfClass": "Geef een klasnaam op.",
"create": "aanmaken",
"sender": "afzender",
"nameIsMandatory": "klasnaam is verplicht",
"onlyUse" : "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
"close": "sluiten",
"copied": "gekopieerd!",
"accept": "accepteren",
"deny": "weigeren"
} }

View file

@ -8,46 +8,11 @@
const role: string = authState.authState.activeRole!; const role: string = authState.authState.activeRole!;
// TODO : temp data until frontend controllers are ready // For students: code that they give in when sending a class join request
type Teacher = { // For teachers: code that they get when they create a new class
username: string;
firstName: string;
lastName: string;
classes: Array<Class>;
};
type Student = {
username: string;
firstName: string;
lastName: string;
classes: Array<Class>;
};
type Class = {
id: string;
displayName: string;
teachers: Array<Teacher>;
students: Array<Student>;
};
const student01: Student = { username: "id01", firstName: "Mark", lastName: "Knopfler", classes: [] };
const student02: Student = { username: "id01", firstName: "John", lastName: "Hiat", classes: [] };
const student03: Student = { username: "id01", firstName: "Aaron", lastName: "Lewis", classes: [] };
const class01: Class = { id: "class01", displayName: "class 01", teachers: [], students: [student01, student02] };
const class02: Class = { id: "class02", displayName: "class 02", teachers: [], students: [student01, student03] };
const class03: Class = { id: "class03", displayName: "class 03", teachers: [], students: [student02, student03] };
student01.classes = [class01, class02];
student02.classes = [class01, class03];
student03.classes = [class02, class03];
const classes: Array<Class> = [class01, class02, class03];
// Handle the class join requests
const code = ref<string>(""); const code = ref<string>("");
// The code needs to be formatted as v4 to be valid // The code a student sends in to join a class needs to be formatted as v4 to be valid
// These rules are used to display a message to the user if they use a code that has an invalid format // These rules are used to display a message to the user if they use a code that has an invalid format
const codeRules = [ const codeRules = [
(value: string | undefined) => { (value: string | undefined) => {
@ -55,19 +20,28 @@
return t("invalidFormat"); return t("invalidFormat");
}, },
]; ];
// Submitting a code will send a request if the code is valid
// function called when a student submits a code to join a class
function submitCode() { function submitCode() {
// check if the code is valid
if (code.value !== undefined && validate(code.value) && version(code.value) === 4) { if (code.value !== undefined && validate(code.value) && version(code.value) === 4) {
// TODO: temp function until frontend controllers are ready // TODO: temp function that does not use the backend
console.log("Code submitted:", code.value); console.log("Code submitted:", code.value);
} }
} }
// Handle dialog for showing members of a class // Boolean that handles visibility for dialogs
// For students: clicking on membercount will show a dialog with all members
// For teachers: creating a class will generate a popup with the generated code
const dialog = ref(false); const dialog = ref(false);
// list of students in the selected class
const students = ref<Array<string>>([]); const students = ref<Array<string>>([]);
// selected class itself
const selectedClass = ref<Class | null>(null); const selectedClass = ref<Class | null>(null);
// function to display all members of a class
function openDialog(c: Class) { function openDialog(c: Class) {
selectedClass.value = c; selectedClass.value = c;
if (selectedClass.value !== undefined) { if (selectedClass.value !== undefined) {
@ -75,33 +49,233 @@
dialog.value = true; dialog.value = true;
} }
} }
// function to handle a accepted invitation request
function acceptRequest() {
//TODO
console.log("request accepted");
}
// function to handle a denied invitation request
function denyRequest() {
//TODO
console.log("request denied");
}
// catch the value a teacher inserts when making a class
const className = ref<string>("");
// The name can only contain dash, underscore letters and numbers
// These rules are used to display a message to the user if the name is not valid
const nameRules = [
(value: string | undefined) => {
if (value) return true;
return t("nameIsMandatory");
},
(value: string | undefined) => {
if (value && /^[a-zA-Z0-9_-]+$/.test(value)) return true;
return t("onlyUse");
},
];
// function called when a teacher creates a class
function createClass() {
// check if the class name is valid
if (className && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) {
//TODO
console.log("created class with name: " + className.value);
// show the generated code to share with the class
dialog.value = true;
code.value = "04c7c759-c41e-4ea9-968a-1e2a987ce0ed";
}
}
// if the unique code is copied, set this to true so it's being displayed for the user
const copied = ref(false);
// copy the generated code to the clipboard
function copyToClipboard() {
navigator.clipboard.writeText(code.value);
copied.value = true;
}
</script> </script>
<template> <template>
<main> <main>
<h1 class="title">{{ t("classes") }}</h1> <h1 class="title">{{ t("classes") }}</h1>
<v-container
fluid
class="ma-4"
>
<v-row
no-gutters
fluid
>
<v-col
cols="12"
sm="6"
md="6"
>
<v-table class="table">
<thead>
<tr>
<th class="header">{{ t("classes") }}</th>
<th
class="header"
v-if="role === 'teacher'"
>
{{ t("code") }}
</th>
<th class="header">{{ t("members") }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="c in classes"
:key="c.id"
>
<td v-if="role === 'student'">{{ c.displayName }}</td>
<td v-else>
<v-btn
:to="`/user/class/${c.id}`"
variant="text"
>
{{ c.displayName }}
<v-icon end> mdi-menu-right </v-icon>
</v-btn>
</td>
<td v-if="role === 'teacher'">{{ c.id }}</td>
<td
v-if="role === 'student'"
class="link"
@click="openDialog(c)"
>
{{ c.students.length }}
</td>
<td v-else>{{ c.students.length }}</td>
</tr>
</tbody>
</v-table>
</v-col>
<v-col
cols="12"
sm="6"
md="6"
>
<div v-if="role === 'teacher'">
<h2>{{ t("createClass") }}</h2>
<v-table class="table"> <v-sheet
class="pa-4 sheet"
max-width="600px"
>
<p>{{ t("createClassInstructions") }}</p>
<v-form @submit.prevent>
<v-text-field
class="mt-4"
:label="`${t('classname')}`"
v-model="className"
:placeholder="`${t('EnterNameOfClass')}`"
:rules="nameRules"
variant="outlined"
></v-text-field>
<v-btn
class="mt-4"
color="#f6faf2"
type="submit"
@click="createClass"
block
>{{ t("create") }}</v-btn
>
</v-form>
</v-sheet>
<v-container>
<v-dialog
v-model="dialog"
max-width="400px"
>
<v-card>
<v-card-title class="headline">code</v-card-title>
<v-card-text>
<v-text-field
v-model="code"
readonly
append-inner-icon="mdi-content-copy"
@click:append-inner="copyToClipboard"
></v-text-field>
<v-slide-y-transition>
<div
v-if="copied"
class="text-center mt-2"
>
{{ t("copied") }}
</div>
</v-slide-y-transition>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text
@click="
dialog = false;
copied = false;
"
> {{ t("close") }} </v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</div>
</v-col>
</v-row>
</v-container>
<h1
v-if="role === 'teacher'"
class="title"
>
{{ t("invitations") }}
</h1>
<v-table
v-if="role === 'teacher'"
class="table"
>
<thead> <thead>
<tr> <tr>
<th class="header">{{ t("classes") }}</th> <th class="header">{{ t("class") }}</th>
<th class="header">{{ t("members") }}</th> <th class="header">{{ t("sender") }}</th>
<th class="header"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr <tr
v-for="c in classes" v-for="i in invitations"
:key="c.id" :key="i.id"
> >
<td>{{ c.displayName }}</td> <td>
<td {{ i.class.displayName }}
class="link" </td>
@click="openDialog(c)" <td>{{ i.sender.firstName + " " + i.sender.lastName }}</td>
> <td class="text-right">
{{ c.students.length }} <div>
<v-btn
color="green"
@click="acceptRequest"
class="mr-2"
> {{ t("accept") }} </v-btn
>
<v-btn
color="red"
@click="denyRequest"
> {{ t("deny") }} </v-btn
>
</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</v-table> </v-table>
<v-dialog <v-dialog
v-model="dialog" v-model="dialog"
width="400" width="400"
@ -146,7 +320,7 @@
></v-text-field> ></v-text-field>
<v-btn <v-btn
class="mt-4" class="mt-4"
style="background-color: #f6faf2" color="#f6faf2"
type="submit" type="submit"
@click="submitCode" @click="submitCode"
block block
@ -190,7 +364,7 @@
} }
.table { .table {
width: 60%; width: 90%;
padding-top: 10px; padding-top: 10px;
border-collapse: collapse; border-collapse: collapse;
} }
@ -248,4 +422,4 @@
margin: 5px; margin: 5px;
} }
} }
</style> </style>