merge: Merge conflicts opgelost voor mergen met dev

This commit is contained in:
Adriaan Jacquet 2025-03-29 23:10:25 +01:00
commit 4f0c4241b6
74 changed files with 2193 additions and 633 deletions

View file

@ -21,14 +21,16 @@ Alternatief kan je één van de volgende methodes gebruiken om de applicatie lok
### Quick start ### Quick start
Om de applicatie lokaal te draaien als kant-en-klare Docker-containers:
1. Installeer Docker en Docker Compose op je systeem (zie [Docker](https://docs.docker.com/get-docker/) 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/)). en [Docker Compose](https://docs.docker.com/compose/)).
2. Clone deze repository. 2. Clone deze repository.
3. In de backend, kopieer `.env.example` (of `.env.development.example`) naar `.env` en pas de variabelen aan waar 3. In de backend, kopieer `.env.example` naar `.env` en pas de variabelen aan waar nodig.
nodig. 4. Voer `docker compose -f compose.staging.yml up --build` uit in de root van de repository.
4. Voer `docker compose up` uit in de root van de repository.
5. Optioneel: Configureer de applicatie aan de hand van 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). de [configuratiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving#dwengo-1-configuratie).
6. De applicatie is nu beschikbaar op [`http://localhost/`](http://localhost/) en [`http://localhost/api`](http://localhost/api).
```bash ```bash
docker compose version docker compose version
@ -38,14 +40,13 @@ cp .env.example .env
# Pas .env aan # Pas .env aan
nano .env nano .env
cd .. cd ..
docker compose up docker compose -f compose.staging.yml up --build
# Configureer de applicatie
``` ```
### Handmatige installatie ### Handmatige installatie en ontwikkeling
Zie de submappen voor de installatie-instructies van de [frontend](./frontend/README.md) Zie de submappen voor de installatie-instructies van de [frontend](./frontend/README.md)
en [backend](./backend/README.md). en [backend](./backend/README.md) en instructies voor het opzetten van een ontwikkelomgeving.
## Architectuur ## Architectuur

View file

@ -1,15 +1,24 @@
# #
# Basic configuration # Development environment configuration
#
# You probably don't need to change these values, as this configuration takes
# the docker services and their default ports into account.
# #
DWENGO_PORT=3000 # The port the backend will listen on ### Dwengo ###
#DWENGO_PORT=3000
#DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api
#DWENGO_FALLBACK_LANGUAGE=nl
#DWENGO_RUN_MODE=dev
DWENGO_DB_HOST=localhost DWENGO_DB_HOST=localhost
DWENGO_DB_PORT=5431 DWENGO_DB_PORT=5431
#DWENGO_DB_NAME=dwengo
DWENGO_DB_USERNAME=postgres DWENGO_DB_USERNAME=postgres
DWENGO_DB_PASSWORD=postgres DWENGO_DB_PASSWORD=postgres
DWENGO_DB_UPDATE=true DWENGO_DB_UPDATE=true
#DWENGO_DB_CONTENT_PREFIX=u_
# Auth
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
@ -17,12 +26,12 @@ DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/
DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
#DWENGO_AUTH_AUDIENCE=account
# Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production!
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173 DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173
#DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type
# ### Advanced configuration ###
# Advanced configuration
#
# LOKI_HOST=http://localhost:9001 # The address of the Loki instance, used for logging DWENGO_LOGGING_LEVEL=debug
#DWENGO_LOGGING_LOKI_HOST=http://localhost:3102

View file

@ -1,27 +1,68 @@
# #
# Basic configuration # Basic configuration
# #
# Change the values of the variables below to match your environment!
# Default values are commented out.
#
DWENGO_PORT=3000 # The port the backend will listen on ### Dwengo ###
# Port the backend will listen on
#DWENGO_PORT=3000
# The hostname or IP address of the remote learning content API.
#DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api
# The default fallback language.
#DWENGO_FALLBACK_LANGUAGE=nl
# Whether running in production mode or not. Possible values are "prod" or "dev".
#DWENGO_RUN_MODE=dev
# ! Change this! The hostname or IP address of the database
# If running your stack in docker, this should use the docker service name.
DWENGO_DB_HOST=domain-or-ip-of-database DWENGO_DB_HOST=domain-or-ip-of-database
DWENGO_DB_PORT=5431 # The port of the database.
#DWENGO_DB_PORT=5432
# Change this to the actual credentials of the user Dwengo should use in the backend # The name of the database.
DWENGO_DB_USERNAME=postgres #DWENGO_DB_NAME=dwengo
DWENGO_DB_PASSWORD=postgres # ! Change this! The username of the database user.
DWENGO_DB_USERNAME=username
# ! Change this! The password of the database user.
DWENGO_DB_PASSWORD=password
# Whether the database scheme needs to be updated.
# Set this to true when the database scheme needs to be updated. In that case, take a backup first. # Set this to true when the database scheme needs to be updated. In that case, take a backup first.
DWENGO_DB_UPDATE=false #DWENGO_DB_UPDATE=false
# The prefix used for custom user content.
#DWENGO_DB_CONTENT_PREFIX=u_
# Data for the identity provider via which the students authenticate. # ! Change this! The external URL for student authentication. Should be reachable by the client.
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student # E.g. https://sel2-1.ugent.be/idp/realms/student
DWENGO_AUTH_STUDENT_URL=http://hostname/idp/realms/student
# ! Change this! The client ID for student authentication.
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs # ! Change this! The internal URL for retrieving the JWKS for student authentication.
# Should be reachable by the backend. If running your stack in docker, this should use the docker service name.
# Data for the identity provider via which the teachers authenticate. # E.g. http://idp:7080/realms/student/protocol/openid-connect/certs
DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://hostname/realms/student/protocol/openid-connect/certs
# ! Change this! The external URL for teacher authentication. Should be reachable by the client.
# E.g. https://sel2-1.ugent.be/idp/realms/teacher
DWENGO_AUTH_TEACHER_URL=http://hostname/idp/realms/teacher
# ! Change this! The client ID for teacher authentication.
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs # ! Change this! The internal URL for retrieving the JWKS for teacher authentication.
# Should be reachable by the backend. If running your stack in docker, this should use the docker service name.
# E.g. http://idp:7080/realms/teacher/protocol/openid-connect/certs
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://hostname/realms/teacher/protocol/openid-connect/certs
# The IDP audience
#DWENGO_AUTH_AUDIENCE=account
# The address of the Lokiinstance, used for logging # Allowed origins for CORS requests. Separate multiple origins with a comma.
# LOKI_HOST=http://localhost:3102 #DWENGO_CORS_ALLOWED_ORIGINS=
# Allowed headers for CORS requests. Separate multiple headers with a comma.
#DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type
### Advanced configuration ###
# The logging level. Possible values are "debug", "info", "warn", "error".
#DWENGO_LOGGING_LEVEL=info
# The address of the Loki instance, a log aggregation system.
# If running your stack in docker, this should use the docker service name.
#DWENGO_LOGGING_LOKI_HOST=http://localhost:3102

View file

@ -1,28 +1,37 @@
DWENGO_PORT=3000 # The port the backend will listen on #
DWENGO_DB_HOST=db # Name of the database container # Production environment configuration
DWENGO_DB_PORT=5431 #
# Change the values of the variables below to match your production environment!
# See .env.example for more information.
#
# Change this to the actual credentials of the user Dwengo should use in the backend ### Dwengo ###
DWENGO_PORT=3000
#DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api
#DWENGO_FALLBACK_LANGUAGE=nl
DWENGO_RUN_MODE=prod
DWENGO_DB_HOST=db
DWENGO_DB_PORT=5432
DWENGO_DB_NAME=postgres DWENGO_DB_NAME=postgres
DWENGO_DB_USERNAME=postgres DWENGO_DB_USERNAME=postgres
DWENGO_DB_PASSWORD=postgres DWENGO_DB_PASSWORD=postgres
# Set this to true when the database scheme needs to be updated. In that case, take a backup first.
DWENGO_DB_UPDATE=false DWENGO_DB_UPDATE=false
#DWENGO_DB_CONTENT_PREFIX=u_
# Data for the identity provider via which the students authenticate.
DWENGO_AUTH_STUDENT_URL=https://sel2-1.ugent.be/idp/realms/student DWENGO_AUTH_STUDENT_URL=https://sel2-1.ugent.be/idp/realms/student
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs # Name of the idp container DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs # Name of the idp container
# Data for the identity provider via which the teachers authenticate.
DWENGO_AUTH_TEACHER_URL=https://sel2-1.ugent.be/idp/realms/teacher DWENGO_AUTH_TEACHER_URL=https://sel2-1.ugent.be/idp/realms/teacher
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs # Name of the idp container DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs # Name of the idp container
#DWENGO_AUTH_AUDIENCE=account
# #DWENGO_CORS_ALLOWED_ORIGINS=
# Advanced configuration #DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type
#
# Logging and monitoring ### Advanced configuration ###
# LOKI_HOST=http://logging:3102 # The address of the Loki instance, used for logging DWENGO_LOGGING_LEVEL=info
DWENGO_LOGGING_LOKI_HOST=http://logging:3102

View file

@ -1,3 +1,13 @@
PORT=3000 #
DWENGO_DB_UPDATE=true # Test environment configuration
#
# Should not need to be modified.
# See .env.example for more information.
#
### Dwengo ###
DWENGO_PORT=3000
DWENGO_DB_NAME=":memory:" DWENGO_DB_NAME=":memory:"
DWENGO_DB_UPDATE=true

View file

@ -30,6 +30,7 @@ COPY package-lock.json backend/package.json ./
RUN npm install --silent --only=production RUN npm install --silent --only=production
COPY ./docs /docs COPY ./docs /docs
COPY ./backend/i18n /app/i18n
COPY --from=build-stage /app/backend/dist ./dist/ COPY --from=build-stage /app/backend/dist ./dist/
EXPOSE 3000 EXPOSE 3000

View file

@ -4,23 +4,24 @@
```shell ```shell
npm install npm install
# Start de nodige services voor ontwikkeling
cd ../ # Ga naar de root van de repository
docker compose up -d
``` ```
Setup the environment variables in a `.env` file in the root of the project. You can use the `.env.example` file as a template. Zet de omgevingsvariabelen in een `.env` bestand in de root van het project.
Je kan het `.env.example` bestand als template gebruiken.
### Development ### Ontwikkeling
```shell ```shell
# Omgevingsvariabelen
cp .env.development.example .env.development.local
npm run dev npm run dev
``` ```
### Production
```shell
npm run build
npm run start
```
### Tests ### Tests
Voer volgend commando uit om de unit tests uit te voeren: Voer volgend commando uit om de unit tests uit te voeren:
@ -29,6 +30,18 @@ Voer volgend commando uit om de unit tests uit te voeren:
npm run test:unit npm run test:unit
``` ```
### Productie
```shell
# Omgevingsvariabelen
cp .env.development.example .env
npm run build
npm run start
```
Zie ook de [installatiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving).
## Keycloak configuratie ## Keycloak configuratie
Tijdens development is het voldoende om gebruik te maken van de keycloak configuratie die automatisch ingeladen wordt. Tijdens development is het voldoende om gebruik te maken van de keycloak configuratie die automatisch ingeladen wordt.

View file

@ -28,9 +28,9 @@ curricula_page:
contact: '' contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai: basics_ai:
title: Basisprincipes van AI title: Grundlagen der KI
sub_title: Basisprincipes van AI sub_title: Grundlagen der KI
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.' description: 'Dieses Thema bündelt verschiedene Aktivitäten, in denen die grundlegenden Prinzipien der künstlichen Intelligenz (KI) behandelt werden. Die Schüler lernen, was KI ist, wie sie funktioniert und wie sie in verschiedenen Bereichen angewendet werden kann.'
contact: '' contact: ''
kiks: kiks:
title: KI und Klima title: KI und Klima

View file

@ -28,10 +28,11 @@ curricula_page:
contact: '' contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai: basics_ai:
title: Basisprincipes van AI title: Basics of AI
sub_title: Basisprincipes van AI sub_title: Basics of AI
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.' description: 'This theme brings together various activities covering the basic principles of Artificial Intelligence (AI). Students learn what AI is, how it works, and how it can be applied in different domains.'
contact: '' contact: ''
kiks: kiks:
title: AI and Climate title: AI and Climate
sub_title: KIKS sub_title: KIKS

View file

@ -28,9 +28,9 @@ curricula_page:
contact: '' contact: ''
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
basics_ai: basics_ai:
title: Basisprincipes van AI title: Principes de base de lIA
sub_title: Basisprincipes van AI sub_title: Principes de base de lIA
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.' description: 'Ce thème rassemble différentes activités portant sur les principes fondamentaux de lintelligence artificielle (IA). Les élèves apprennent ce quest lIA, comment elle fonctionne et comment elle peut être appliquée dans divers domaines.'
contact: '' contact: ''
kiks: kiks:
title: 'IA et changement climatique' title: 'IA et changement climatique'

View file

@ -1,12 +1,7 @@
import { EnvVars, getEnvVar } from './util/envvars.js'; import { EnvVars, getEnvVar } from './util/envvars.js';
import { Language } from './entities/content/language.js';
// API // API
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl);
export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage);
// 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 FALLBACK_SEQ_NUM = 1; export const FALLBACK_SEQ_NUM = 1;

View file

@ -32,7 +32,7 @@ export async function createAssignmentHandler(req: Request, res: Response): Prom
return; return;
} }
res.status(201).json({ assignment: assignment }); res.status(201).json(assignment);
} }
export async function getAssignmentHandler(req: Request, res: Response): Promise<void> { export async function getAssignmentHandler(req: Request, res: Response): Promise<void> {
@ -57,13 +57,14 @@ export async function getAssignmentHandler(req: Request, res: Response): Promise
export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise<void> { export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise<void> {
const classid = req.params.classid; const classid = req.params.classid;
const assignmentNumber = +req.params.id; const assignmentNumber = +req.params.id;
const full = req.query.full === 'true';
if (isNaN(assignmentNumber)) { if (isNaN(assignmentNumber)) {
res.status(400).json({ error: 'Assignment id must be a number' }); res.status(400).json({ error: 'Assignment id must be a number' });
return; return;
} }
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber); const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full);
res.json({ res.json({
submissions: submissions, submissions: submissions,

View file

@ -28,30 +28,19 @@ export async function createClassHandler(req: Request, res: Response): Promise<v
return; return;
} }
res.status(201).json({ class: cls }); res.status(201).json(cls);
} }
export async function getClassHandler(req: Request, res: Response): Promise<void> { export async function getClassHandler(req: Request, res: Response): Promise<void> {
try { const classId = req.params.id;
const classId = req.params.id; const cls = await getClass(classId);
const cls = await getClass(classId);
if (!cls) { if (!cls) {
res.status(404).json({ error: 'Class not found' }); res.status(404).json({ error: 'Class not found' });
return; return;
}
cls.endpoints = {
self: `${req.baseUrl}/${req.params.id}`,
invitations: `${req.baseUrl}/${req.params.id}/invitations`,
assignments: `${req.baseUrl}/${req.params.id}/assignments`,
students: `${req.baseUrl}/${req.params.id}/students`,
};
res.json(cls);
} catch (error) {
console.error('Error fetching learning objects:', error);
res.status(500).json({ error: 'Internal server error' });
} }
res.json(cls);
} }
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> { export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
@ -72,7 +61,7 @@ export async function getClassStudentsHandler(req: Request, res: Response): Prom
export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> { export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.id; const classId = req.params.id;
const full = req.query.full === 'true'; // TODO: not implemented yet const full = req.query.full === 'true';
const invitations = await getClassTeacherInvitations(classId, full); const invitations = await getClassTeacherInvitations(classId, full);

View file

@ -23,6 +23,7 @@ export async function getGroupHandler(req: Request, res: Response): Promise<void
if (!group) { if (!group) {
res.status(404).json({ error: 'Group not found' }); res.status(404).json({ error: 'Group not found' });
return;
} }
res.json(group); res.json(group);
@ -63,12 +64,12 @@ export async function createGroupHandler(req: Request, res: Response): Promise<v
return; return;
} }
res.status(201).json({ group: group }); res.status(201).json(group);
} }
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> { export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classid; const classId = req.params.classid;
// Const full = req.query.full === 'true'; const full = req.query.full === 'true';
const assignmentId = +req.params.assignmentid; const assignmentId = +req.params.assignmentid;
@ -84,7 +85,7 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P
return; return;
} }
const submissions = await getGroupSubmissions(classId, assignmentId, groupId); const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full);
res.json({ res.json({
submissions: submissions, submissions: submissions,

View file

@ -40,7 +40,7 @@ export async function getAllLearningObjects(req: Request, res: Response): Promis
learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId); learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId);
} }
res.json(learningObjects); res.json({ learningObjects: learningObjects });
} }
export async function getLearningObject(req: Request, res: Response): Promise<void> { export async function getLearningObject(req: Request, res: Response): Promise<void> {

View file

@ -48,7 +48,7 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi
if (!questions) { if (!questions) {
res.status(404).json({ error: `Questions not found.` }); res.status(404).json({ error: `Questions not found.` });
} else { } else {
res.json(questions); res.json({ questions: questions });
} }
} }
@ -76,12 +76,12 @@ export async function getQuestionAnswersHandler(req: Request, res: Response): Pr
return; return;
} }
const answers = getAnswersByQuestion(questionId, full); const answers = await getAnswersByQuestion(questionId, full);
if (!answers) { if (!answers) {
res.status(404).json({ error: `Questions not found.` }); res.status(404).json({ error: `Questions not found` });
} else { } else {
res.json(answers); res.json({ answers: answers });
} }
} }
@ -96,7 +96,7 @@ export async function createQuestionHandler(req: Request, res: Response): Promis
const question = await createQuestion(questionDTO); const question = await createQuestion(questionDTO);
if (!question) { if (!question) {
res.status(400).json({ error: 'Could not add question' }); res.status(400).json({ error: 'Could not create question' });
} else { } else {
res.json(question); res.json(question);
} }

View file

@ -9,29 +9,21 @@ import {
getStudentGroups, getStudentGroups,
getStudentSubmissions, getStudentSubmissions,
} from '../services/students.js'; } from '../services/students.js';
import { ClassDTO } from '../interfaces/class.js';
import { getAllAssignments } from '../services/assignments.js';
import { getUserHandler } from './users.js';
import { Student } from '../entities/users/student.entity.js';
import { StudentDTO } from '../interfaces/student.js'; import { StudentDTO } from '../interfaces/student.js';
import { getStudentRepository } from '../data/repositories.js';
import { UserDTO } from '../interfaces/user.js';
// TODO: accept arguments (full, ...) // TODO: accept arguments (full, ...)
// TODO: endpoints // TODO: endpoints
export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> { export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const studentRepository = getStudentRepository(); const students = await getAllStudents(full);
const students: StudentDTO[] | string[] = full ? await getAllStudents() : await getAllStudents();
if (!students) { if (!students) {
res.status(404).json({ error: `Student not found.` }); res.status(404).json({ error: `Student not found.` });
return; return;
} }
res.status(201).json(students); res.json({ students: students });
} }
export async function getStudentHandler(req: Request, res: Response): Promise<void> { export async function getStudentHandler(req: Request, res: Response): Promise<void> {
@ -51,7 +43,7 @@ export async function getStudentHandler(req: Request, res: Response): Promise<vo
return; return;
} }
res.status(201).json(user); res.json(user);
} }
export async function createStudentHandler(req: Request, res: Response) { export async function createStudentHandler(req: Request, res: Response) {
@ -65,6 +57,14 @@ export async function createStudentHandler(req: Request, res: Response) {
} }
const newUser = await createStudent(userData); const newUser = await createStudent(userData);
if (!newUser) {
res.status(500).json({
error: 'Something went wrong while creating student'
});
return;
}
res.status(201).json(newUser); res.status(201).json(newUser);
} }
@ -88,25 +88,14 @@ export async function deleteStudentHandler(req: Request, res: Response) {
} }
export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> { export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> {
try { const full = req.query.full === 'true';
const full = req.query.full === 'true'; const username = req.params.id;
const username = req.params.id;
const classes = await getStudentClasses(username, full); const classes = await getStudentClasses(username, full);
res.json({ res.json({
classes: classes, classes: classes,
endpoints: { });
self: `${req.baseUrl}/${req.params.id}`,
classes: `${req.baseUrl}/${req.params.id}/invitations`,
questions: `${req.baseUrl}/${req.params.id}/assignments`,
students: `${req.baseUrl}/${req.params.id}/students`,
},
});
} catch (error) {
console.error('Error fetching learning objects:', error);
res.status(500).json({ error: 'Internal server error' });
}
} }
// TODO // TODO
@ -137,8 +126,9 @@ export async function getStudentGroupsHandler(req: Request, res: Response): Prom
export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> { export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> {
const username = req.params.id; const username = req.params.id;
const full = req.query.full === 'true';
const submissions = await getStudentSubmissions(username); const submissions = await getStudentSubmissions(username, full);
res.json({ res.json({
submissions: submissions, submissions: submissions,

View file

@ -43,10 +43,11 @@ export async function createSubmissionHandler(req: Request, res: Response) {
const submission = await createSubmission(submissionDTO); const submission = await createSubmission(submissionDTO);
if (!submission) { if (!submission) {
res.status(404).json({ error: 'Submission not added' }); res.status(400).json({ error: 'Failed to create submission' });
} else { return;
res.json(submission);
} }
res.json(submission);
} }
export async function deleteSubmissionHandler(req: Request, res: Response) { export async function deleteSubmissionHandler(req: Request, res: Response) {
@ -60,7 +61,8 @@ export async function deleteSubmissionHandler(req: Request, res: Response) {
if (!submission) { if (!submission) {
res.status(404).json({ error: 'Submission not found' }); res.status(404).json({ error: 'Submission not found' });
} else { return;
res.json(submission);
} }
res.json(submission);
} }

View file

@ -4,33 +4,23 @@ import {
deleteTeacher, deleteTeacher,
getAllTeachers, getAllTeachers,
getClassesByTeacher, getClassesByTeacher,
getClassIdsByTeacher,
getQuestionIdsByTeacher,
getQuestionsByTeacher, getQuestionsByTeacher,
getStudentIdsByTeacher,
getStudentsByTeacher, getStudentsByTeacher,
getTeacher, getTeacher,
} from '../services/teachers.js'; } from '../services/teachers.js';
import { ClassDTO } from '../interfaces/class.js';
import { StudentDTO } from '../interfaces/student.js';
import { QuestionDTO, QuestionId } from '../interfaces/question.js';
import { Teacher } from '../entities/users/teacher.entity.js';
import { TeacherDTO } from '../interfaces/teacher.js'; import { TeacherDTO } from '../interfaces/teacher.js';
import { getTeacherRepository } from '../data/repositories.js';
export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> { export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> {
const full = req.query.full === 'true'; const full = req.query.full === 'true';
const teacherRepository = getTeacherRepository(); const teachers = await getAllTeachers(full);
const teachers: TeacherDTO[] | string[] = full ? await getAllTeachers() : await getAllTeachers();
if (!teachers) { if (!teachers) {
res.status(404).json({ error: `Teacher not found.` }); res.status(404).json({ error: `Teacher not found.` });
return; return;
} }
res.status(201).json(teachers); res.json({ teachers: teachers });
} }
export async function getTeacherHandler(req: Request, res: Response): Promise<void> { export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
@ -45,12 +35,12 @@ export async function getTeacherHandler(req: Request, res: Response): Promise<vo
if (!user) { if (!user) {
res.status(404).json({ res.status(404).json({
error: `User with username '${username}' not found.`, error: `Teacher '${username}' not found.`,
}); });
return; return;
} }
res.status(201).json(user); res.json(user);
} }
export async function createTeacherHandler(req: Request, res: Response) { export async function createTeacherHandler(req: Request, res: Response) {
@ -64,6 +54,12 @@ export async function createTeacherHandler(req: Request, res: Response) {
} }
const newUser = await createTeacher(userData); const newUser = await createTeacher(userData);
if (!newUser) {
res.status(400).json({ error: 'Failed to create teacher' });
return;
}
res.status(201).json(newUser); res.status(201).json(newUser);
} }
@ -78,7 +74,7 @@ export async function deleteTeacherHandler(req: Request, res: Response) {
const deletedUser = await deleteTeacher(username); const deletedUser = await deleteTeacher(username);
if (!deletedUser) { if (!deletedUser) {
res.status(404).json({ res.status(404).json({
error: `User with username '${username}' not found.`, error: `User '${username}' not found.`,
}); });
return; return;
} }
@ -87,58 +83,58 @@ export async function deleteTeacherHandler(req: Request, res: Response) {
} }
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> { export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
try { const username = req.params.username as string;
const username = req.params.username as string; const full = req.query.full === 'true';
const full = req.query.full === 'true';
if (!username) { if (!username) {
res.status(400).json({ error: 'Missing required field: username' }); res.status(400).json({ error: 'Missing required field: username' });
return; return;
}
const classes: ClassDTO[] | string[] = full ? await getClassesByTeacher(username) : await getClassIdsByTeacher(username);
res.status(201).json(classes);
} catch (error) {
console.error('Error fetching classes by teacher:', error);
res.status(500).json({ error: 'Internal server error' });
} }
const classes = await getClassesByTeacher(username, full);
if (!classes) {
res.status(404).json({ error: 'Teacher not found' });
return;
}
res.json({ classes: classes });
} }
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> { export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
try { const username = req.params.username as string;
const username = req.params.username as string; const full = req.query.full === 'true';
const full = req.query.full === 'true';
if (!username) { if (!username) {
res.status(400).json({ error: 'Missing required field: username' }); res.status(400).json({ error: 'Missing required field: username' });
return; return;
}
const students: StudentDTO[] | string[] = full ? await getStudentsByTeacher(username) : await getStudentIdsByTeacher(username);
res.status(201).json(students);
} catch (error) {
console.error('Error fetching students by teacher:', error);
res.status(500).json({ error: 'Internal server error' });
} }
const students = await getStudentsByTeacher(username, full);
if (!students) {
res.status(404).json({ error: 'Teacher not found' });
return;
}
res.json({ students: students });
} }
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> { export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
try { const username = req.params.username as string;
const username = req.params.username as string; const full = req.query.full === 'true';
const full = req.query.full === 'true';
if (!username) { if (!username) {
res.status(400).json({ error: 'Missing required field: username' }); res.status(400).json({ error: 'Missing required field: username' });
return; return;
}
const questions: QuestionDTO[] | QuestionId[] = full ? await getQuestionsByTeacher(username) : await getQuestionIdsByTeacher(username);
res.status(201).json(questions);
} catch (error) {
console.error('Error fetching questions by teacher:', error);
res.status(500).json({ error: 'Internal server error' });
} }
const questions = await getQuestionsByTeacher(username, full);
if (!questions) {
res.status(404).json({ error: 'Teacher not found' });
return;
}
res.json({ questions: questions });
} }

View file

@ -8,7 +8,7 @@ interface Translations {
}; };
} }
export function getThemes(req: Request, res: Response) { export function getThemesHandler(req: Request, res: Response) {
const language = (req.query.language as string)?.toLowerCase() || 'nl'; const language = (req.query.language as string)?.toLowerCase() || 'nl';
const translations = loadTranslations<Translations>(language); const translations = loadTranslations<Translations>(language);
const themeList = themes.map((theme) => ({ const themeList = themes.map((theme) => ({
@ -21,8 +21,14 @@ export function getThemes(req: Request, res: Response) {
res.json(themeList); res.json(themeList);
} }
export function getThemeByTitle(req: Request, res: Response) { export function getHruidsByThemeHandler(req: Request, res: Response) {
const themeKey = req.params.theme; const themeKey = req.params.theme;
if (!themeKey) {
res.status(400).json({ error: 'Missing required field: theme' });
return;
}
const theme = themes.find((t) => t.title === themeKey); const theme = themes.find((t) => t.title === themeKey);
if (theme) { if (theme) {

View file

@ -1,91 +0,0 @@
import { Request, Response } from 'express';
import { UserService } from '../services/users.js';
import { UserDTO } from '../interfaces/user.js';
import { User } from '../entities/users/user.entity.js';
export async function getAllUsersHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> {
try {
const full = req.query.full === 'true';
const users: UserDTO[] | string[] = full ? await service.getAllUsers() : await service.getAllUserIds();
if (!users) {
res.status(404).json({ error: `Users not found.` });
return;
}
res.status(201).json(users);
} catch (error) {
console.error('❌ Error fetching users:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
export async function getUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> {
try {
const username = req.params.username as string;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const user = await service.getUserByUsername(username);
if (!user) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(201).json(user);
} catch (error) {
console.error('❌ Error fetching users:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
export async function createUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>, UserClass: new () => T) {
try {
console.log('req', req);
const userData = req.body as UserDTO;
if (!userData.username || !userData.firstName || !userData.lastName) {
res.status(400).json({
error: 'Missing required fields: username, firstName, lastName',
});
return;
}
const newUser = await service.createUser(userData, UserClass);
res.status(201).json(newUser);
} catch (error) {
console.error('❌ Error creating user:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
export async function deleteUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>) {
try {
const username = req.params.username;
if (!username) {
res.status(400).json({ error: 'Missing required field: username' });
return;
}
const deletedUser = await service.deleteUser(username);
if (!deletedUser) {
res.status(404).json({
error: `User with username '${username}' not found.`,
});
return;
}
res.status(200).json(deletedUser);
} catch (error) {
console.error('❌ Error deleting user:', error);
res.status(500).json({ error: 'Internal server error' });
}
}

View file

@ -2,8 +2,6 @@ import { AnyEntity, EntityManager, EntityName, EntityRepository } from '@mikro-o
import { forkEntityManager } from '../orm.js'; import { forkEntityManager } from '../orm.js';
import { StudentRepository } from './users/student-repository.js'; import { StudentRepository } from './users/student-repository.js';
import { Student } from '../entities/users/student.entity.js'; import { Student } from '../entities/users/student.entity.js';
import { User } from '../entities/users/user.entity.js';
import { UserRepository } from './users/user-repository.js';
import { Teacher } from '../entities/users/teacher.entity.js'; import { Teacher } from '../entities/users/teacher.entity.js';
import { TeacherRepository } from './users/teacher-repository.js'; import { TeacherRepository } from './users/teacher-repository.js';
import { Class } from '../entities/classes/class.entity.js'; import { Class } from '../entities/classes/class.entity.js';

View file

@ -1,6 +1,5 @@
import { Class } from '../../entities/classes/class.entity.js'; import { Class } from '../../entities/classes/class.entity.js';
import { Student } from '../../entities/users/student.entity.js'; import { Student } from '../../entities/users/student.entity.js';
import { User } from '../../entities/users/user.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
// Import { UserRepository } from './user-repository.js'; // Import { UserRepository } from './user-repository.js';

View file

@ -1,6 +1,5 @@
import { Teacher } from '../../entities/users/teacher.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js';
import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { UserRepository } from './user-repository.js';
export class TeacherRepository extends DwengoEntityRepository<Teacher> { export class TeacherRepository extends DwengoEntityRepository<Teacher> {
public findByUsername(username: string): Promise<Teacher | null> { public findByUsername(username: string): Promise<Teacher | null> {

View file

@ -1,4 +1,4 @@
import { Collection, 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 { Class } from '../classes/class.entity.js';
import { Group } from './group.entity.js'; import { Group } from './group.entity.js';
import { Language } from '../content/language.js'; import { Language } from '../content/language.js';

View file

@ -1,4 +1,4 @@
import { Collection, Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core'; import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
import { Assignment } from './assignment.entity.js'; import { Assignment } from './assignment.entity.js';
import { Student } from '../users/student.entity.js'; import { Student } from '../users/student.entity.js';
import { GroupRepository } from '../../data/assignments/group-repository.js'; import { GroupRepository } from '../../data/assignments/group-repository.js';

View file

@ -3,6 +3,12 @@ import { Student } from '../users/student.entity.js';
import { Class } from './class.entity.js'; import { Class } from './class.entity.js';
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
export enum ClassJoinRequestStatus {
Open = 'open',
Accepted = 'accepted',
Declined = 'declined',
}
@Entity({ @Entity({
repository: () => ClassJoinRequestRepository, repository: () => ClassJoinRequestRepository,
}) })
@ -21,10 +27,4 @@ export class ClassJoinRequest {
@Enum(() => ClassJoinRequestStatus) @Enum(() => ClassJoinRequestStatus)
status!: ClassJoinRequestStatus; status!: ClassJoinRequestStatus;
} }
export enum ClassJoinRequestStatus {
Open = 'open',
Accepted = 'accepted',
Declined = 'declined',
}

View file

@ -2,7 +2,7 @@ import { FALLBACK_LANG } from '../config.js';
import { Assignment } from '../entities/assignments/assignment.entity.js'; import { Assignment } from '../entities/assignments/assignment.entity.js';
import { Class } from '../entities/classes/class.entity.js'; import { Class } from '../entities/classes/class.entity.js';
import { languageMap } from '../entities/content/language.js'; import { languageMap } from '../entities/content/language.js';
import { GroupDTO, mapToGroupDTO } from './group.js'; import { GroupDTO } from './group.js';
export interface AssignmentDTO { export interface AssignmentDTO {
id: number; id: number;
@ -46,7 +46,5 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
assignment.within = cls; assignment.within = cls;
console.log(assignment);
return assignment; return assignment;
} }

View file

@ -9,12 +9,6 @@ export interface ClassDTO {
teachers: string[]; teachers: string[];
students: string[]; students: string[];
joinRequests: string[]; joinRequests: string[];
endpoints?: {
self: string;
invitations: string;
assignments: string;
students: string;
};
} }
export function mapToClassDTO(cls: Class): ClassDTO { export function mapToClassDTO(cls: Class): ClassDTO {

View file

@ -1,8 +1,6 @@
import { Question } from '../entities/questions/question.entity.js'; import { Question } from '../entities/questions/question.entity.js';
import { UserDTO } from './user.js';
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
import { mapToStudentDTO, StudentDTO } from './student.js'; import { mapToStudentDTO, StudentDTO } from './student.js';
import { TeacherDTO } from './teacher.js';
export interface QuestionDTO { export interface QuestionDTO {
learningObjectIdentifier: LearningObjectIdentifier; learningObjectIdentifier: LearningObjectIdentifier;

View file

@ -2,13 +2,10 @@ import { Submission } from '../entities/assignments/submission.entity.js';
import { Language } from '../entities/content/language.js'; import { Language } from '../entities/content/language.js';
import { GroupDTO, mapToGroupDTO } from './group.js'; import { GroupDTO, mapToGroupDTO } from './group.js';
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js'; import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js';
import { mapToUser } from './user'; import { LearningObjectIdentifier } from './learning-content.js';
import { Student } from '../entities/users/student.entity';
export interface SubmissionDTO { export interface SubmissionDTO {
learningObjectHruid: string; learningObjectIdentifier: LearningObjectIdentifier;
learningObjectLanguage: Language;
learningObjectVersion: number;
submissionNumber?: number; submissionNumber?: number;
submitter: StudentDTO; submitter: StudentDTO;
@ -17,11 +14,21 @@ export interface SubmissionDTO {
content: string; content: string;
} }
export interface SubmissionDTOId {
learningObjectHruid: string;
learningObjectLanguage: Language;
learningObjectVersion: number;
submissionNumber?: number;
}
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
return { return {
learningObjectHruid: submission.learningObjectHruid, learningObjectIdentifier: {
learningObjectLanguage: submission.learningObjectLanguage, hruid: submission.learningObjectHruid,
learningObjectVersion: submission.learningObjectVersion, language: submission.learningObjectLanguage,
version: submission.learningObjectVersion,
},
submissionNumber: submission.submissionNumber, submissionNumber: submission.submissionNumber,
submitter: mapToStudentDTO(submission.submitter), submitter: mapToStudentDTO(submission.submitter),
@ -31,11 +38,21 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
}; };
} }
export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId {
return {
learningObjectHruid: submission.learningObjectHruid,
learningObjectLanguage: submission.learningObjectLanguage,
learningObjectVersion: submission.learningObjectVersion,
submissionNumber: submission.submissionNumber,
};
}
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission { export function mapToSubmission(submissionDTO: SubmissionDTO): Submission {
const submission = new Submission(); const submission = new Submission();
submission.learningObjectHruid = submissionDTO.learningObjectHruid; submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid;
submission.learningObjectLanguage = submissionDTO.learningObjectLanguage; submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language;
submission.learningObjectVersion = submissionDTO.learningObjectVersion; submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!;
// Submission.submissionNumber = submissionDTO.submissionNumber; // Submission.submissionNumber = submissionDTO.submissionNumber;
submission.submitter = mapToStudent(submissionDTO.submitter); submission.submitter = mapToStudent(submissionDTO.submitter);
// Submission.submissionTime = submissionDTO.time; // Submission.submissionTime = submissionDTO.time;

View file

@ -1,7 +1,7 @@
import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; import { createLogger, format, Logger as WinstonLogger, transports } from 'winston';
import LokiTransport from 'winston-loki'; import LokiTransport from 'winston-loki';
import { LokiLabels } from 'loki-logger-ts'; import { LokiLabels } from 'loki-logger-ts';
import { LOG_LEVEL, LOKI_HOST } from '../config.js'; import { EnvVars, getEnvVar } from '../util/envvars.js';
export class Logger extends WinstonLogger { export class Logger extends WinstonLogger {
constructor() { constructor() {
@ -22,10 +22,25 @@ function initializeLogger(): Logger {
return logger; return logger;
} }
const logLevel = getEnvVar(EnvVars.LogLevel);
const consoleTransport = new transports.Console({
level: getEnvVar(EnvVars.LogLevel),
format: format.combine(format.cli(), format.colorize()),
});
if (getEnvVar(EnvVars.RunMode) === 'dev') {
return createLogger({
transports: [consoleTransport],
});
}
const lokiHost = getEnvVar(EnvVars.LokiHost);
const lokiTransport: LokiTransport = new LokiTransport({ const lokiTransport: LokiTransport = new LokiTransport({
host: LOKI_HOST, host: lokiHost,
labels: Labels, labels: Labels,
level: LOG_LEVEL, level: logLevel,
json: true, json: true,
format: format.combine(format.timestamp(), format.json()), format: format.combine(format.timestamp(), format.json()),
onConnectionError: (err) => { onConnectionError: (err) => {
@ -34,16 +49,11 @@ function initializeLogger(): Logger {
}, },
}); });
const consoleTransport = new transports.Console({
level: LOG_LEVEL,
format: format.combine(format.cli(), format.colorize()),
});
logger = createLogger({ logger = createLogger({
transports: [lokiTransport, consoleTransport], transports: [lokiTransport, consoleTransport],
}); });
logger.debug(`Logger initialized with level ${LOG_LEVEL}, Loki host ${LOKI_HOST}`); logger.debug(`Logger initialized with level ${logLevel} to Loki host ${lokiHost}`);
return logger; return logger;
} }

View file

@ -3,7 +3,6 @@ import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js';
import { SqliteDriver } from '@mikro-orm/sqlite'; import { SqliteDriver } from '@mikro-orm/sqlite';
import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; import { MikroOrmLogger } from './logging/mikroOrmLogger.js';
import { LOG_LEVEL } from './config.js';
// Import alle entity-bestanden handmatig // Import alle entity-bestanden handmatig
import { User } from './entities/users/user.entity.js'; import { User } from './entities/users/user.entity.js';
@ -69,7 +68,7 @@ function config(testingMode: boolean = false): Options {
// EntitiesTs: entitiesTs, // EntitiesTs: entitiesTs,
// Logging // Logging
debug: LOG_LEVEL === 'debug', debug: getEnvVar(EnvVars.LogLevel) === 'debug',
loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options),
}; };
} }

View file

@ -1,10 +1,7 @@
import { Response, Router } from 'express'; import { Response, Router } from 'express';
import studentRouter from './students.js'; import studentRouter from './students.js';
import groupRouter from './groups.js'; import teacherRouter from './teachers.js';
import assignmentRouter from './assignments.js';
import submissionRouter from './submissions.js';
import classRouter from './classes.js'; import classRouter from './classes.js';
import questionRouter from './questions.js';
import authRouter from './auth.js'; import authRouter from './auth.js';
import themeRoutes from './themes.js'; import themeRoutes from './themes.js';
import learningPathRoutes from './learning-paths.js'; import learningPathRoutes from './learning-paths.js';
@ -22,11 +19,8 @@ router.get('/', (_, res: Response) => {
}); });
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */); router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
router.use('/group', groupRouter /* #swagger.tags = ['Group'] */); router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */);
router.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */);
router.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */);
router.use('/class', classRouter /* #swagger.tags = ['Class'] */); router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
router.use('/question', questionRouter /* #swagger.tags = ['Question'] */);
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */); router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */); router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */); router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);

View file

@ -9,7 +9,6 @@ import {
getStudentHandler, getStudentHandler,
getStudentSubmissionsHandler, getStudentSubmissionsHandler,
} from '../controllers/students.js'; } from '../controllers/students.js';
import { getStudentGroups } from '../services/students.js';
const router = express.Router(); const router = express.Router();
// Root endpoint used to search objects // Root endpoint used to search objects

View file

@ -1,14 +1,14 @@
import express from 'express'; import express from 'express';
import { getThemes, getThemeByTitle } from '../controllers/themes.js'; import { getThemesHandler, getHruidsByThemeHandler } from '../controllers/themes.js';
const router = express.Router(); const router = express.Router();
// Query: language // Query: language
// Route to fetch list of {key, title, description, image} themes in their respective language // Route to fetch list of {key, title, description, image} themes in their respective language
router.get('/', getThemes); router.get('/', getThemesHandler);
// Arg: theme (key) // Arg: theme (key)
// Route to fetch list of hruids based on theme // Route to fetch list of hruids based on theme
router.get('/:theme', getThemeByTitle); router.get('/:theme', getHruidsByThemeHandler);
export default router; export default router;

View file

@ -1,7 +1,6 @@
import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
import { Assignment } from '../entities/assignments/assignment.entity.js';
import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> { export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
@ -21,7 +20,7 @@ export async function getAllAssignments(classid: string, full: boolean): Promise
return assignments.map(mapToAssignmentDTOId); return assignments.map(mapToAssignmentDTOId);
} }
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<Assignment | null> { export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<AssignmentDTO | null> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
const cls = await classRepository.findById(classid); const cls = await classRepository.findById(classid);
@ -36,8 +35,9 @@ export async function createAssignment(classid: string, assignmentData: Assignme
const newAssignment = assignmentRepository.create(assignment); const newAssignment = assignmentRepository.create(assignment);
await assignmentRepository.save(newAssignment); await assignmentRepository.save(newAssignment);
return newAssignment; return mapToAssignmentDTO(newAssignment);
} catch (e) { } catch (e) {
console.error(e);
return null; return null;
} }
} }
@ -60,7 +60,11 @@ export async function getAssignment(classid: string, id: number): Promise<Assign
return mapToAssignmentDTO(assignment); return mapToAssignmentDTO(assignment);
} }
export async function getAssignmentsSubmissions(classid: string, assignmentNumber: number): Promise<SubmissionDTO[]> { export async function getAssignmentsSubmissions(
classid: string,
assignmentNumber: number,
full: boolean
): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
const cls = await classRepository.findById(classid); const cls = await classRepository.findById(classid);
@ -81,5 +85,9 @@ export async function getAssignmentsSubmissions(classid: string, assignmentNumbe
const submissionRepository = getSubmissionRepository(); const submissionRepository = getSubmissionRepository();
const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
return submissions.map(mapToSubmissionDTO); if (full) {
return submissions.map(mapToSubmissionDTO);
}
return submissions.map(mapToSubmissionDTOId);
} }

View file

@ -1,5 +1,4 @@
import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js'; import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js';
import { Class } from '../entities/classes/class.entity.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js'; import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js';
@ -22,16 +21,14 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[
return classes.map((cls) => cls.classId!); return classes.map((cls) => cls.classId!);
} }
export async function createClass(classData: ClassDTO): Promise<Class | null> { export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> {
const teacherRepository = getTeacherRepository(); const teacherRepository = getTeacherRepository();
const teacherUsernames = classData.teachers || []; const teacherUsernames = classData.teachers || [];
const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher != null); const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher !== null);
const studentRepository = getStudentRepository(); const studentRepository = getStudentRepository();
const studentUsernames = classData.students || []; const studentUsernames = classData.students || [];
const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null); const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null);
//Const cls = mapToClass(classData, teachers, students);
const classRepository = getClassRepository(); const classRepository = getClassRepository();
@ -43,7 +40,7 @@ export async function createClass(classData: ClassDTO): Promise<Class | null> {
}); });
await classRepository.save(newClass); await classRepository.save(newClass);
return newClass; return mapToClassDTO(newClass);
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);
return null; return null;

View file

@ -1,4 +1,3 @@
import { GroupRepository } from '../data/assignments/group-repository.js';
import { import {
getAssignmentRepository, getAssignmentRepository,
getClassRepository, getClassRepository,
@ -8,7 +7,7 @@ import {
} from '../data/repositories.js'; } from '../data/repositories.js';
import { Group } from '../entities/assignments/group.entity.js'; import { Group } from '../entities/assignments/group.entity.js';
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> { export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
@ -43,7 +42,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
const studentRepository = getStudentRepository(); const studentRepository = getStudentRepository();
const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null); const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null);
console.log(members); console.log(members);
@ -103,7 +102,12 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu
return groups.map(mapToGroupDTOId); return groups.map(mapToGroupDTOId);
} }
export async function getGroupSubmissions(classId: string, assignmentNumber: number, groupNumber: number): Promise<SubmissionDTO[]> { export async function getGroupSubmissions(
classId: string,
assignmentNumber: number,
groupNumber: number,
full: boolean
): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const classRepository = getClassRepository(); const classRepository = getClassRepository();
const cls = await classRepository.findById(classId); const cls = await classRepository.findById(classId);
@ -128,5 +132,9 @@ export async function getGroupSubmissions(classId: string, assignmentNumber: num
const submissionRepository = getSubmissionRepository(); const submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findAllSubmissionsForGroup(group); const submissions = await submissionRepository.findAllSubmissionsForGroup(group);
return submissions.map(mapToSubmissionDTO); if (full) {
return submissions.map(mapToSubmissionDTO);
}
return submissions.map(mapToSubmissionDTOId);
} }

View file

@ -45,6 +45,13 @@ export async function getLearningObjectById(hruid: string, language: string): Pr
return filterData(metadata, htmlUrl); return filterData(metadata, htmlUrl);
} }
/**
* Generic function to fetch learning paths
*/
function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
throw new Error('Function not implemented.');
}
/** /**
* Generic function to fetch learning objects (full data or just HRUIDs) * Generic function to fetch learning objects (full data or just HRUIDs)
*/ */
@ -85,6 +92,4 @@ export async function getLearningObjectsFromPath(hruid: string, language: string
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[]; return (await fetchLearningObjects(hruid, false, language)) as string[];
} }
function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
throw new Error('Function not implemented.');
}

View file

@ -103,5 +103,5 @@ export async function deleteQuestion(questionId: QuestionId) {
return null; return null;
} }
return question; return mapToQuestionDTO(question);
} }

View file

@ -5,19 +5,18 @@ import { AssignmentDTO } from '../interfaces/assignment.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
import { getAllAssignments } from './assignments.js'; import { getAllAssignments } from './assignments.js';
import { UserService } from './users.js';
export async function getAllStudents(): Promise<StudentDTO[]> { export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
const studentRepository = getStudentRepository(); const studentRepository = getStudentRepository();
const users = await studentRepository.findAll(); const students = await studentRepository.findAll();
return users.map(mapToStudentDTO);
}
export async function getAllStudentIds(): Promise<string[]> { if (full) {
const users = await getAllStudents(); return students.map(mapToStudentDTO);
return users.map((user) => user.username); }
return students.map((student) => student.username);
} }
export async function getStudent(username: string): Promise<StudentDTO | null> { export async function getStudent(username: string): Promise<StudentDTO | null> {
@ -111,7 +110,7 @@ export async function getStudentGroups(username: string, full: boolean): Promise
return groups.map(mapToGroupDTOId); return groups.map(mapToGroupDTOId);
} }
export async function getStudentSubmissions(username: string): Promise<SubmissionDTO[]> { export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
const studentRepository = getStudentRepository(); const studentRepository = getStudentRepository();
const student = await studentRepository.findByUsername(username); const student = await studentRepository.findByUsername(username);
@ -122,5 +121,9 @@ export async function getStudentSubmissions(username: string): Promise<Submissio
const submissionRepository = getSubmissionRepository(); const submissionRepository = getSubmissionRepository();
const submissions = await submissionRepository.findAllSubmissionsForStudent(student); const submissions = await submissionRepository.findAllSubmissionsForStudent(student);
return submissions.map(mapToSubmissionDTO); if (full) {
return submissions.map(mapToSubmissionDTO);
}
return submissions.map(mapToSubmissionDTOId);
} }

View file

@ -45,7 +45,7 @@ export async function createSubmission(submissionDTO: SubmissionDTO) {
return null; return null;
} }
return submission; return mapToSubmissionDTO(submission);
} }
export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) { export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) {

View file

@ -7,22 +7,22 @@ import {
} from '../data/repositories.js'; } from '../data/repositories.js';
import { Teacher } from '../entities/users/teacher.entity.js'; import { Teacher } from '../entities/users/teacher.entity.js';
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
import { getClassStudents } from './class.js'; import { getClassStudents } from './classes.js';
import { StudentDTO } from '../interfaces/student.js'; import { StudentDTO } from '../interfaces/student.js';
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
import { UserService } from './users.js';
import { mapToUser } from '../interfaces/user.js'; import { mapToUser } from '../interfaces/user.js';
import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js'; import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js';
import { teachersOnly } from '../middleware/auth/auth.js';
export async function getAllTeachers(): Promise<TeacherDTO[]> { export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> {
const teacherRepository = getTeacherRepository(); const teacherRepository = getTeacherRepository();
const users = await teacherRepository.findAll(); const teachers = await teacherRepository.findAll();
return users.map(mapToTeacherDTO);
}
export async function getAllTeacherIds(): Promise<string[]> { if (full) {
const users = await getAllTeachers(); return teachers.map(mapToTeacherDTO);
return users.map((user) => user.username); }
return teachers.map((teacher) => teacher.username);
} }
export async function getTeacher(username: string): Promise<TeacherDTO | null> { export async function getTeacher(username: string): Promise<TeacherDTO | null> {
@ -64,11 +64,11 @@ export async function deleteTeacher(username: string): Promise<TeacherDTO | null
} }
} }
export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[]> { export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[] | null> {
const teacherRepository = getTeacherRepository(); const teacherRepository = getTeacherRepository();
const teacher = await teacherRepository.findByUsername(username); const teacher = await teacherRepository.findByUsername(username);
if (!teacher) { if (!teacher) {
return []; return null;
} }
const classRepository = getClassRepository(); const classRepository = getClassRepository();
@ -76,35 +76,49 @@ export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[
return classes.map(mapToClassDTO); return classes.map(mapToClassDTO);
} }
export async function getClassesByTeacher(username: string): Promise<ClassDTO[]> { export async function getClassesByTeacher(username: string, full: boolean): Promise<ClassDTO[] | string[] | null> {
return await fetchClassesByTeacher(username);
}
export async function getClassIdsByTeacher(username: string): Promise<string[]> {
const classes = await fetchClassesByTeacher(username); const classes = await fetchClassesByTeacher(username);
if (!classes) {
return null;
}
if (full) {
return classes;
}
return classes.map((cls) => cls.id); return classes.map((cls) => cls.id);
} }
export async function fetchStudentsByTeacher(username: string) { export async function fetchStudentsByTeacher(username: string): Promise<StudentDTO[] | null> {
const classes = await getClassIdsByTeacher(username); const classes = (await getClassesByTeacher(username, false)) as string[];
if (!classes) {
return null;
}
return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat(); return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat();
} }
export async function getStudentsByTeacher(username: string): Promise<StudentDTO[]> { export async function getStudentsByTeacher(username: string, full: boolean): Promise<StudentDTO[] | string[] | null> {
return await fetchStudentsByTeacher(username);
}
export async function getStudentIdsByTeacher(username: string): Promise<string[]> {
const students = await fetchStudentsByTeacher(username); const students = await fetchStudentsByTeacher(username);
if (!students) {
return null;
}
if (full) {
return students;
}
return students.map((student) => student.username); return students.map((student) => student.username);
} }
export async function fetchTeacherQuestions(username: string): Promise<QuestionDTO[]> { export async function fetchTeacherQuestions(username: string): Promise<QuestionDTO[] | null> {
const teacherRepository = getTeacherRepository(); const teacherRepository = getTeacherRepository();
const teacher = await teacherRepository.findByUsername(username); const teacher = await teacherRepository.findByUsername(username);
if (!teacher) { if (!teacher) {
throw new Error(`Teacher with username '${username}' not found.`); return null;
} }
// Find all learning objects that this teacher manages // Find all learning objects that this teacher manages
@ -118,12 +132,16 @@ export async function fetchTeacherQuestions(username: string): Promise<QuestionD
return questions.map(mapToQuestionDTO); return questions.map(mapToQuestionDTO);
} }
export async function getQuestionsByTeacher(username: string): Promise<QuestionDTO[]> { export async function getQuestionsByTeacher(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[] | null> {
return await fetchTeacherQuestions(username);
}
export async function getQuestionIdsByTeacher(username: string): Promise<QuestionId[]> {
const questions = await fetchTeacherQuestions(username); const questions = await fetchTeacherQuestions(username);
if (!questions) {
return null;
}
if (full) {
return questions;
}
return questions.map(mapToQuestionId); return questions.map(mapToQuestionId);
} }

View file

@ -1,41 +0,0 @@
import { UserRepository } from '../data/users/user-repository.js';
import { UserDTO, mapToUser, mapToUserDTO } from '../interfaces/user.js';
import { User } from '../entities/users/user.entity.js';
export class UserService<T extends User> {
protected repository: UserRepository<T>;
constructor(repository: UserRepository<T>) {
this.repository = repository;
}
async getAllUsers(): Promise<UserDTO[]> {
const users = await this.repository.findAll();
return users.map(mapToUserDTO);
}
async getAllUserIds(): Promise<string[]> {
const users = await this.getAllUsers();
return users.map((user) => user.username);
}
async getUserByUsername(username: string): Promise<UserDTO | null> {
const user = await this.repository.findByUsername(username);
return user ? mapToUserDTO(user) : null;
}
async createUser(userData: UserDTO, UserClass: new () => T): Promise<T> {
const newUser = mapToUser(userData, new UserClass());
await this.repository.save(newUser);
return newUser;
}
async deleteUser(username: string): Promise<UserDTO | null> {
const user = await this.getUserByUsername(username);
if (!user) {
return null;
}
await this.repository.deleteByUsername(username);
return mapToUserDTO(user);
}
}

View file

@ -4,20 +4,24 @@ const IDP_PREFIX = PREFIX + 'AUTH_';
const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_'; const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_';
const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_'; const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_';
const CORS_PREFIX = PREFIX + 'CORS_'; const CORS_PREFIX = PREFIX + 'CORS_';
const LOGGING_PREFIX = PREFIX + 'LOGGING_';
type EnvVar = { key: string; required?: boolean; defaultValue?: any }; type EnvVar = { key: string; required?: boolean; defaultValue?: any };
export const EnvVars: { [key: string]: EnvVar } = { export const EnvVars: { [key: string]: EnvVar } = {
Port: { key: PREFIX + 'PORT', defaultValue: 3000 }, Port: { key: PREFIX + 'PORT', defaultValue: 3000 },
LearningContentRepoApiBaseUrl: { key: PREFIX + 'LEARNING_CONTENT_REPO_API_BASE_URL', defaultValue: 'https://dwengo.org/backend/api' },
FallbackLanguage: { key: PREFIX + 'FALLBACK_LANGUAGE', defaultValue: 'nl' },
RunMode: { key: PREFIX + 'RUN_MODE', defaultValue: 'dev' },
DbHost: { key: DB_PREFIX + 'HOST', required: true }, DbHost: { key: DB_PREFIX + 'HOST', required: true },
DbPort: { key: DB_PREFIX + 'PORT', defaultValue: 5432 }, DbPort: { key: DB_PREFIX + 'PORT', defaultValue: 5432 },
DbName: { key: DB_PREFIX + 'NAME', defaultValue: 'dwengo' }, DbName: { key: DB_PREFIX + 'NAME', defaultValue: 'dwengo' },
DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, DbUsername: { key: DB_PREFIX + 'USERNAME', required: true },
DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true },
DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false }, DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false },
LearningContentRepoApiBaseUrl: { key: PREFIX + 'LEARNING_CONTENT_REPO_API_BASE_URL', defaultValue: 'https://dwengo.org/backend/api' },
FallbackLanguage: { key: PREFIX + 'FALLBACK_LANGUAGE', defaultValue: 'nl' },
UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: 'u_' }, UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: 'u_' },
IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true }, IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true },
IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', required: true }, IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', required: true },
IdpStudentJwksEndpoint: { key: STUDENT_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, IdpStudentJwksEndpoint: { key: STUDENT_IDP_PREFIX + 'JWKS_ENDPOINT', required: true },
@ -25,8 +29,12 @@ export const EnvVars: { [key: string]: EnvVar } = {
IdpTeacherClientId: { key: TEACHER_IDP_PREFIX + 'CLIENT_ID', required: true }, IdpTeacherClientId: { key: TEACHER_IDP_PREFIX + 'CLIENT_ID', required: true },
IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true },
IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' }, IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' },
CorsAllowedOrigins: { key: CORS_PREFIX + 'ALLOWED_ORIGINS', defaultValue: '' }, CorsAllowedOrigins: { key: CORS_PREFIX + 'ALLOWED_ORIGINS', defaultValue: '' },
CorsAllowedHeaders: { key: CORS_PREFIX + 'ALLOWED_HEADERS', defaultValue: 'Authorization,Content-Type' }, CorsAllowedHeaders: { key: CORS_PREFIX + 'ALLOWED_HEADERS', defaultValue: 'Authorization,Content-Type' },
LogLevel: { key: LOGGING_PREFIX + 'LEVEL', defaultValue: 'info' },
LokiHost: { key: LOGGING_PREFIX + 'LOKI_HOST', defaultValue: 'http://localhost:3102' },
} as const; } as const;
/** /**

View file

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

View file

@ -1,7 +1,8 @@
# #
# This file is used to define the production environment for the project. # Use this configuration to deploy the project on a server.
# It is used to deploy the project on a server. #
# Should not be used for local development. # This configuration builds the frontend and backend services as Docker images,
# and uses the paths for the services, instead of ports, and enables SSL.
# #
services: services:
web: web:
@ -35,12 +36,16 @@ services:
- 'traefik.http.services.api.loadbalancer.server.port=3000' - 'traefik.http.services.api.loadbalancer.server.port=3000'
db: db:
# Also see compose.yml extends:
file: ./compose.yml
service: db
networks: networks:
- dwengo-1 - dwengo-1
idp: idp:
# Also see compose.yml extends:
file: ./compose.yml
service: idp
# TODO Replace with proper production command # TODO Replace with proper production command
command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm']
networks: networks:
@ -92,7 +97,15 @@ services:
- dwengo-1 - dwengo-1
logging: logging:
# Also see compose.yml image: grafana/loki:latest
ports:
- '9001:3102'
- '9095:9095'
command: -config.file=/etc/loki/config.yaml
restart: unless-stopped
volumes:
- ./config/loki/config.yml:/etc/loki/config.yaml
- dwengo_loki_data:/loki
networks: networks:
- dwengo-1 - dwengo-1
@ -107,6 +120,7 @@ services:
volumes: volumes:
dwengo_grafana_data: dwengo_grafana_data:
dwengo_letsencrypt: dwengo_letsencrypt:
dwengo_loki_data:
networks: networks:
dwengo-1: dwengo-1:

View file

@ -32,8 +32,15 @@ services:
- 'traefik.http.routers.api.rule=PathPrefix(`/api`)' - 'traefik.http.routers.api.rule=PathPrefix(`/api`)'
- 'traefik.http.services.api.loadbalancer.server.port=3000' - 'traefik.http.services.api.loadbalancer.server.port=3000'
db:
extends:
file: ./compose.yml
service: db
idp: idp:
# Also see compose.yml extends:
file: ./compose.yml
service: idp
labels: labels:
- 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)' - 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)'
- 'traefik.http.services.idp.loadbalancer.server.port=7080' - 'traefik.http.services.idp.loadbalancer.server.port=7080'
@ -60,6 +67,17 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
logging:
image: grafana/loki:latest
ports:
- '9001:3102'
- '9095:9095'
command: -config.file=/etc/loki/config.yaml
restart: unless-stopped
volumes:
- ./config/loki/config.yml:/etc/loki/config.yaml
- dwengo_loki_data:/loki
dashboards: dashboards:
image: grafana/grafana:latest image: grafana/grafana:latest
ports: ports:
@ -70,3 +88,5 @@ services:
volumes: volumes:
dwengo_grafana_data: dwengo_grafana_data:
dwengo_loki_data:
dwengo_postgres_data:

View file

@ -36,17 +36,5 @@ services:
KC_HEALTH_ENABLED: 'true' KC_HEALTH_ENABLED: 'true'
KC_LOG_LEVEL: info KC_LOG_LEVEL: info
logging:
image: grafana/loki:latest
ports:
- '9001:3102'
- '9095:9095'
command: -config.file=/etc/loki/config.yaml
restart: unless-stopped
volumes:
- ./config/loki/config.yml:/etc/loki/config.yaml
- dwengo_loki_data:/loki
volumes: volumes:
dwengo_loki_data:
dwengo_postgres_data: dwengo_postgres_data:

View file

@ -19,7 +19,16 @@ See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup ## Project Setup
```sh ```sh
# Install dependencies
npm install npm install
# Start necessary services for development
cd ../ # Go to the root of the repository
docker compose up -d
# Start the backend
cd backend
cp .env.development.example .env.development.local
npm run dev # or npm run build && npm run start
``` ```
### Compile and Hot-Reload for Development ### Compile and Hot-Reload for Development

View file

@ -16,12 +16,14 @@
"test:e2e": "playwright test" "test:e2e": "playwright test"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.69.0",
"@tanstack/vue-query": "^5.69.0",
"axios": "^1.8.2",
"oidc-client-ts": "^3.1.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^11.1.2", "vue-i18n": "^11.1.2",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"vuetify": "^3.7.12", "vuetify": "^3.7.12"
"oidc-client-ts": "^3.1.0",
"axios": "^1.8.2"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.50.1", "@playwright/test": "^1.50.1",

View file

@ -1,9 +1,71 @@
<script setup lang="ts"> <script setup lang="ts">
// This component contains a list with all themes and will be shown on a student's and teacher's homepage. import ThemeCard from "@/components/ThemeCard.vue";
import { ref, watchEffect, computed } from "vue";
import { useI18n } from "vue-i18n";
import { AGE_TO_THEMES, THEMESITEMS } from "@/utils/constants.ts";
import { useThemeQuery } from "@/queries/themes.ts";
const props = defineProps({
selectedTheme: { type: String, required: true },
selectedAge: { type: String, required: true }
});
const { locale } = useI18n();
const language = computed(() => locale.value);
const { data: allThemes, isLoading, error } = useThemeQuery(language);
const allCards = ref([]);
const cards = ref([]);
watchEffect(() => {
const themes = allThemes.value ?? [];
allCards.value = themes;
if (props.selectedTheme) {
cards.value = themes.filter((theme) =>
THEMESITEMS[props.selectedTheme]?.includes(theme.key) &&
AGE_TO_THEMES[props.selectedAge]?.includes(theme.key)
);
} else {
cards.value = themes;
}
});
</script> </script>
<template> <template>
<main></main> <v-container>
<div v-if="isLoading" class="text-center py-10">
<v-progress-circular indeterminate color="primary" />
<p>Loading...</p>
</div>
<div v-else-if="error" class="text-center py-10 text-error">
<v-icon large>mdi-alert-circle</v-icon>
<p>Error loading: {{ error.message }}</p>
</div>
<v-row v-else>
<v-col
v-for="card in cards"
:key="card.key"
cols="12"
sm="6"
md="4"
lg="4"
class="d-flex"
>
<ThemeCard
:path="card.key"
:title="card.title"
:description="card.description"
:image="card.image"
class="fill-height"
/>
</v-col>
</v-row>
</v-container>
</template> </template>
<style scoped></style> <style scoped></style>

View file

@ -9,15 +9,10 @@
const { t, locale } = useI18n(); const { t, locale } = useI18n();
// Instantiate variables to use in html to render right
// Links and content dependent on the role (student or teacher)
const path = "/user";
const role = auth.authState.activeRole; const role = auth.authState.activeRole;
//TODO: use authState form services map to get user token const name: string = auth.authState.user!.profile.name!;
const name = "Kurt Cobain"; const initials: string = name
const initials = name
.split(" ") .split(" ")
.map((n) => n[0]) .map((n) => n[0])
.join(""); .join("");
@ -26,24 +21,147 @@
const languages = ref([ const languages = ref([
{ name: "English", code: "en" }, { name: "English", code: "en" },
{ name: "Nederlands", code: "nl" }, { name: "Nederlands", code: "nl" },
{ name: "Français", code: "fr" },
{ name: "Deutsch", code: "de" }
]); ]);
// Logic to change the language of the website to the selected language // Logic to change the language of the website to the selected language
const changeLanguage = (langCode: string) => { const changeLanguage = (langCode: string) => {
locale.value = langCode; locale.value = langCode;
localStorage.setItem("user-lang", langCode); localStorage.setItem("user-lang", langCode);
console.log(langCode); };
// contains functionality to let the collapsed menu appear and disappear
// when the screen size varies
const drawer = ref(false);
// when the user wants to logout, a popup is shown to verify this
// if verified, the user should be logged out
const performLogout = () => {
auth.logout();
}; };
</script> </script>
<template> <template>
<main> <main>
<v-app class="menu_collapsed">
<v-app-bar
app
style="background-color: #f6faf2"
>
<template v-slot:prepend>
<v-app-bar-nav-icon @click="drawer = !drawer" />
</template>
<v-app-bar-title>
<router-link
to="/user"
class="dwengo_home"
>
<div>
<img
class="dwengo_logo"
:src="dwengoLogo"
style="width: 100px"
/>
<p
class="caption"
style="font-size: smaller"
>
{{ t(`${role}`) }}
</p>
</div>
</router-link>
</v-app-bar-title>
<v-spacer></v-spacer>
<v-menu open-on-hover>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon
variant="text"
>
<v-icon
icon="mdi-translate"
size="small"
color="#0e6942"
></v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
v-for="(language, index) in languages"
:key="index"
@click="changeLanguage(language.code)"
>
<v-list-item-title>{{ language.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-btn
@click="performLogout"
text
>
<v-tooltip
:text="t('logout')"
location="bottom"
>
<template v-slot:activator="{ props }">
<v-icon
v-bind="props"
icon="mdi-logout"
size="x-large"
color="#0e6942"
/>
</template>
</v-tooltip>
</v-btn>
</v-app-bar>
<v-navigation-drawer
v-model="drawer"
app
>
<v-list>
<v-list-item
to="/user/assignment"
link
>
<v-list-item-content>
<v-list-item-title class="menu_item">{{ t("assignments") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
to="/user/class"
link
>
<v-list-item-content>
<v-list-item-title class="menu_item">{{ t("classes") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
to="/user/discussion"
link
>
<v-list-item-content>
<v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
</v-app>
<nav class="menu"> <nav class="menu">
<div class="left"> <div class="left">
<ul> <ul>
<li> <li>
<router-link <router-link
:to="`${path}`" to="/user"
class="dwengo_home" class="dwengo_home"
> >
<img <img
@ -65,14 +183,14 @@
</li> </li>
<li> <li>
<router-link <router-link
:to="`${path}/class`" to="/user/class"
class="menu_item" class="menu_item"
>{{ t("classes") }}</router-link >{{ t("classes") }}</router-link
> >
</li> </li>
<li> <li>
<router-link <router-link
:to="`${path}/discussion`" to="/user/discussion"
class="menu_item" class="menu_item"
>{{ t("discussions") }} >{{ t("discussions") }}
</router-link> </router-link>
@ -107,7 +225,11 @@
</div> </div>
<div class="right"> <div class="right">
<li> <li>
<router-link :to="`/login`"> <!-- <v-btn
@click="performLogout"
to="/login"
style="background-color: transparent; box-shadow: none !important"
>
<v-tooltip <v-tooltip
:text="t('logout')" :text="t('logout')"
location="bottom" location="bottom"
@ -121,7 +243,48 @@
></v-icon> ></v-icon>
</template> </template>
</v-tooltip> </v-tooltip>
</router-link> </v-btn> -->
<v-dialog max-width="500">
<template v-slot:activator="{ props: activatorProps }">
<v-btn
v-bind="activatorProps"
style="background-color: transparent; box-shadow: none !important"
>
<v-tooltip
:text="t('logout')"
location="bottom"
>
<template v-slot:activator="{ props }">
<v-icon
v-bind="props"
icon="mdi-logout"
size="x-large"
color="#0e6942"
>
</v-icon>
</template>
</v-tooltip>
</v-btn>
</template>
<template v-slot:default="{ isActive }">
<v-card :title="t('logoutVerification')">
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
:text="t('cancel')"
@click="isActive.value = false"
></v-btn>
<v-btn
:text="t('logout')"
@click="performLogout"
to="/login"
></v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</li> </li>
<li> <li>
<v-avatar <v-avatar
@ -133,6 +296,7 @@
</li> </li>
</div> </div>
</nav> </nav>
<router-view />
</main> </main>
</template> </template>
@ -188,4 +352,16 @@
nav a.router-link-active { nav a.router-link-active {
font-weight: bold; font-weight: bold;
} }
@media (max-width: 700px) {
.menu {
display: none;
}
}
@media (min-width: 701px) {
.menu_collapsed {
display: none;
}
}
</style> </style>

View file

@ -0,0 +1,74 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
const { t } = useI18n();
defineProps<{
path: string;
title: string;
description: string;
image: string;
}>();
</script>
<template>
<v-card
variant="outlined"
class="theme-card d-flex flex-column"
:to="`theme/${path}`"
link
>
<v-card-title class="title-container">
<v-img
v-if="image"
:src="image"
height="40px"
width="40px"
contain
class="title-image"
></v-img>
<span class="title">{{ title }}</span>
</v-card-title>
<v-card-text class="description flex-grow-1">{{ description }}</v-card-text>
<v-card-actions>
<v-btn :to="`theme/${path}`" variant="text">
{{ t("read-more") }}
</v-btn>
</v-card-actions>
</v-card>
</template>
<style scoped>
.theme-card {
display: flex;
flex-direction: column;
height: 100%;
padding: 1rem;
cursor: pointer;
}
.theme-card:hover {
background-color: rgba(0, 0, 0, 0.03);
}
.title-container {
display: flex;
align-items: center;
gap: 10px;
text-align: left;
justify-content: flex-start;
}
.title-image {
flex-shrink: 0;
border-radius: 5px;
margin-left: 0;
}
.title {
flex-grow: 1;
white-space: normal;
overflow-wrap: break-word;
word-break: break-word;
}
</style>

View file

@ -0,0 +1,73 @@
import {apiConfig} from "@/config.ts";
export class BaseController {
protected baseUrl: string;
constructor(basePath: string) {
this.baseUrl = `${apiConfig.baseUrl}/${basePath}`;
}
protected async get<T>(path: string, queryParams?: Record<string, any>): Promise<T> {
let url = `${this.baseUrl}${path}`;
if (queryParams) {
const query = new URLSearchParams();
Object.entries(queryParams).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
query.append(key, value.toString());
}
});
url += `?${query.toString()}`;
}
const res = await fetch(url);
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData?.error || `Error ${res.status}: ${res.statusText}`);
}
return res.json();
}
protected async post<T>(path: string, body: unknown): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData?.error || `Error ${res.status}: ${res.statusText}`);
}
return res.json();
}
protected async delete<T>(path: string): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
method: "DELETE",
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData?.error || `Error ${res.status}: ${res.statusText}`);
}
return res.json();
}
protected async put<T>(path: string, body: unknown): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData?.error || `Error ${res.status}: ${res.statusText}`);
}
return res.json();
}
}

View file

@ -0,0 +1,14 @@
import {ThemeController} from "@/controllers/themes.ts";
export function controllerGetter<T>(Factory: new () => T): () => T {
let instance: T | undefined;
return (): T => {
if (!instance) {
instance = new Factory();
}
return instance;
};
}
export const getThemeController = controllerGetter(ThemeController);

View file

@ -0,0 +1,16 @@
import {BaseController} from "@/controllers/base-controller.ts";
export class ThemeController extends BaseController {
constructor() {
super("theme");
}
getAll(language: string | null = null) {
const query = language ? { language } : undefined;
return this.get<any[]>("/", query);
}
getHruidsByKey(themeKey: string) {
return this.get<string[]>(`/${encodeURIComponent(themeKey)}`);
}
}

View file

@ -1,3 +1,43 @@
{ {
"welcome": "Willkommen" "welcome": "Willkommen",
"student": "schüler",
"teacher": "lehrer",
"assignments": "Aufgaben",
"classes": "Klasses",
"discussions": "Diskussionen",
"login": "einloggen",
"logout": "ausloggen",
"cancel": "kündigen",
"logoutVerification": "Sind Sie sicher, dass Sie sich abmelden wollen?",
"homeTitle": "Unsere Stärken",
"homeIntroduction1": "Wir entwickeln innovative Workshops und Bildungsressourcen, die wir in Zusammenarbeit mit Lehrern und Freiwilligen Schülern auf der ganzen Welt zur Verfügung stellen. Unsere Train-the-Trainer-Sitzungen ermöglichen es ihnen, unsere praktischen Workshops an die Schüler weiterzugeben.",
"homeIntroduction2": "Wir fügen allen unseren Projekten ständig neue Projekte und Methoden hinzu. Für diese Projekte suchen wir immer nach einem gesellschaftlich relevanten Thema. Darüber hinaus stellen wir sicher, dass unser didaktisches Material auf wissenschaftlicher Forschung basiert, und achten stets auf die Inklusion.",
"innovative": "Innovativ",
"researchBased": "Forschungsbasiert",
"inclusive": "Inclusiv",
"sociallyRelevant": "Gesellschaftlich relevant",
"translate": "übersetzen",
"themes": "Themen",
"choose-theme": "Wähle ein thema",
"choose-age": "Alter auswählen",
"theme-options": {
"all": "Alle themen",
"culture": "Kultur",
"electricity-and-mechanics": "Elektrizität und Mechanik",
"nature-and-climate": "Natur und Klima",
"agriculture": "Landwirtschaft",
"society": "Gesellschaft",
"math": "Mathematik",
"technology": "Technologie",
"algorithms": "Algorithmisches Denken"
},
"age-options": {
"all": "Alle altersgruppen",
"primary-school": "Grundschule",
"lower-secondary": "12-14 jahre alt",
"upper-secondary": "14-16 jahre alt",
"high-school": "16-18 jahre alt",
"older": "18 und älter"
},
"read-more": "Mehr lesen"
} }

View file

@ -2,8 +2,42 @@
"welcome": "Welcome", "welcome": "Welcome",
"student": "student", "student": "student",
"teacher": "teacher", "teacher": "teacher",
"assignments": "assignments", "assignments": "Assignments",
"classes": "classes", "classes": "Classes",
"discussions": "discussions", "discussions": "Discussions",
"logout": "log out" "logout": "log out",
"cancel": "cancel",
"logoutVerification": "Are you sure you want to log out?",
"homeTitle": "Our strengths",
"homeIntroduction1": "We develop innovative workshops and educational resources, and we provide them to students around the globe in collaboration with teachers and volunteers. Our train-the-trainer sessions enable them to bring our hands-on workshops to the students.",
"homeIntroduction2": "We continuously add new projects and methodologies to all our projects. For these projects, we always look for a socially relevant theme. Additionally, we ensure that our didactic material is based on scientific research and always keep an eye on inclusivity.",
"innovative": "Innovative",
"researchBased": "Research-based",
"inclusive": "Inclusive",
"sociallyRelevant": "Socially relevant",
"login": "log in",
"translate": "translate",
"themes": "Themes",
"choose-theme": "Select a theme",
"choose-age": "Select age",
"theme-options": {
"all": "All themes",
"culture": "Culture",
"electricity-and-mechanics": "Electricity and mechanics",
"nature-and-climate": "Nature and climate",
"agriculture": "Agriculture",
"society": "Society",
"math": "Math",
"technology": "Technology",
"algorithms": "Algorithms"
},
"age-options": {
"all": "All ages",
"primary-school": "Primary school",
"lower-secondary": "12-14 years old",
"upper-secondary": "14-16 years old",
"high-school": "16-18 years old",
"older": "18 and older"
},
"read-more": "Read more"
} }

View file

@ -1,3 +1,43 @@
{ {
"welcome": "Bienvenue" "welcome": "Bienvenue",
"student": "élève",
"teacher": "enseignant",
"assignments": "Travails",
"classes": "Classes",
"discussions": "Discussions",
"login": "se connecter",
"logout": "se déconnecter",
"cancel": "annuler",
"logoutVerification": "Êtes-vous sûr de vouloir vous déconnecter ?",
"homeTitle": "Nos atouts",
"homeIntroduction1": "Nous développons des ateliers innovants et des ressources éducatives que nous mettons à la disposition des élèves du monde entier en collaboration avec des enseignants et des bénévoles. Nos sessions de formation des formateurs leur permettent d'offrir nos ateliers pratiques aux élèves.",
"homeIntroduction2": "Nous ajoutons continuellement de nouveaux projets et de nouvelles méthodologies à tous nos projets. Pour ces projets, nous recherchons toujours un thème socialement pertinent. En outre, nous veillons à ce que notre matériel didactique soit basé sur la recherche scientifique et nous gardons toujours un œil sur l'inclusivité.",
"innovative": "Innovatif",
"researchBased": "Fondé sur la recherche",
"inclusive": "Inclusif",
"sociallyRelevant": "Socialement pertinent",
"translate": "traduire",
"themes": "Thèmes",
"choose-theme": "Choisis un thème",
"choose-age": "Choisis un âge",
"theme-options": {
"all": "Tous les thèmes",
"culture": "Culture",
"electricity-and-mechanics": "Electricité et méchanique",
"nature-and-climate": "Nature et climat",
"agriculture": "Agriculture",
"society": "Société",
"math": "Math",
"technology": "Technologie",
"algorithms": "Algorithmes"
},
"age-options": {
"all": "Tous les âges",
"primary-school": "Ecole primaire",
"lower-secondary": "12-14 ans",
"upper-secondary": "14-16 ans",
"high-school": "16-18 ans",
"older": "18 et plus"
},
"read-more": "En savoir plus"
} }

View file

@ -2,8 +2,42 @@
"welcome": "Welkom", "welcome": "Welkom",
"student": "leerling", "student": "leerling",
"teacher": "leerkracht", "teacher": "leerkracht",
"assignments": "opdrachten", "assignments": "Opdrachten",
"classes": "klassen", "classes": "Klassen",
"discussions": "discussies", "discussions": "Discussies",
"logout": "log uit" "logout": "log uit",
"cancel": "annuleren",
"logoutVerification": "Bent u zeker dat u wilt uitloggen?",
"homeTitle": "Onze sterke punten",
"homeIntroduction1": "We ontwikkelen innovatieve workshops en leermiddelen en bieden deze aan studenten over de hele wereld in samenwerking met leerkrachten en vrijwilligers. Onze train-de-trainer sessies stellen hen in staat om onze hands-on workshops naar de leerlingen te brengen.",
"homeIntroduction2": "We voegen voortdurend nieuwe projecten en methodologieën toe aan al onze projecten. Voor deze projecten zoeken we altijd een maatschappelijk relevant thema. Daarnaast zorgen we ervoor dat ons didactisch materiaal gebaseerd is op wetenschappelijk onderzoek en houden we inclusiviteit altijd in het oog.",
"innovative": "Innovatief",
"researchBased": "Onderzoeksgedreven",
"inclusive": "Inclusief",
"sociallyRelevant": "Maatschappelijk relevant",
"login": "log in",
"translate": "vertalen",
"themes": "Lesthema's",
"choose-theme": "Kies een thema",
"choose-age": "Kies een leeftijd",
"theme-options": {
"all": "Alle thema's",
"culture": "Taal en kunst",
"electricity-and-mechanics": "Elektriciteit en mechanica",
"nature-and-climate": "Natuur en klimaat",
"agriculture": "Land-en tuinbouw",
"society": "Maatschappij en welzijn",
"math": "Wiskunde",
"technology": "Technologie",
"algorithms": "Algoritmes"
},
"age-options": {
"all": "Alle leeftijden",
"primary-school": "Lagere school",
"lower-secondary": "1e graad secundair",
"upper-secondary": "2e graad secundair",
"high-school": "3e graad secundair",
"older": "Hoger onderwijs"
},
"read-more": "Lees meer"
} }

View file

@ -10,6 +10,7 @@ import i18n from "./i18n/i18n.ts";
// Components // Components
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from "./router";
import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query';
const app = createApp(App); const app = createApp(App);
@ -24,6 +25,18 @@ const vuetify = createVuetify({
components, components,
directives, directives,
}); });
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
},
},
});
app.use(vuetify); app.use(vuetify);
app.use(i18n); app.use(i18n);
app.use(VueQueryPlugin, { queryClient });
app.mount("#app"); app.mount("#app");

View file

@ -0,0 +1,25 @@
import { useQuery } from '@tanstack/vue-query';
import { getThemeController } from '@/controllers/controllers';
import {type MaybeRefOrGetter, toValue} from "vue";
const themeController = getThemeController();
export const useThemeQuery = (language: MaybeRefOrGetter<string>) => {
return useQuery({
queryKey: ['themes', language],
queryFn: () => {
const lang = toValue(language);
return themeController.getAll(lang);
},
enabled: () => !!toValue(language),
});
};
export const useThemeHruidsQuery = (themeKey: string | null) => {
return useQuery({
queryKey: ['theme-hruids', themeKey],
queryFn: () => themeController.getHruidsByKey(themeKey!),
enabled: !!themeKey,
});
};

View file

@ -1,6 +1,5 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import MenuBar from "@/components/MenuBar.vue"; import MenuBar from "@/components/MenuBar.vue";
import StudentHomepage from "@/views/homepage/StudentHomepage.vue";
import SingleAssignment from "@/views/assignments/SingleAssignment.vue"; import SingleAssignment from "@/views/assignments/SingleAssignment.vue";
import SingleClass from "@/views/classes/SingleClass.vue"; import SingleClass from "@/views/classes/SingleClass.vue";
import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue"; import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue";
@ -13,6 +12,8 @@ import UserDiscussions from "@/views/discussions/UserDiscussions.vue";
import UserClasses from "@/views/classes/UserClasses.vue"; import UserClasses from "@/views/classes/UserClasses.vue";
import UserAssignments from "@/views/classes/UserAssignments.vue"; import UserAssignments from "@/views/classes/UserAssignments.vue";
import authState from "@/services/auth/auth-service.ts"; import authState from "@/services/auth/auth-service.ts";
import UserHomePage from "@/views/homepage/UserHomePage.vue";
import SingleTheme from "@/views/SingleTheme.vue";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -41,9 +42,9 @@ const router = createRouter({
meta: { requiresAuth: true }, meta: { requiresAuth: true },
children: [ children: [
{ {
path: "home", path: "",
name: "UserHomePage", name: "UserHomePage",
component: StudentHomepage, component: UserHomePage,
}, },
{ {
path: "assignment", path: "assignment",
@ -63,6 +64,12 @@ const router = createRouter({
], ],
}, },
{
path: "/theme/:id",
name: "Theme",
component: SingleTheme,
meta: { requiresAuth: true },
},
{ {
path: "/assignment/create", path: "/assignment/create",
name: "CreateAssigment", name: "CreateAssigment",
@ -112,7 +119,8 @@ router.beforeEach(async (to, from, next) => {
// Verify if user is logged in before accessing certain routes // Verify if user is logged in before accessing certain routes
if (to.meta.requiresAuth) { if (to.meta.requiresAuth) {
if (!authState.isLoggedIn.value) { if (!authState.isLoggedIn.value) {
next("/login"); //Next("/login");
next();
} else { } else {
next(); next();
} }

View file

@ -0,0 +1,37 @@
export const THEMES_KEYS = [
"kiks", "art", "socialrobot", "agriculture", "wegostem",
"computational_thinking", "math_with_python", "python_programming",
"stem", "care", "chatbot", "physical_computing", "algorithms", "basics_ai"
];
export const THEMESITEMS: Record<string, string[]> = {
"all": THEMES_KEYS,
"culture": ["art", "wegostem", "chatbot"],
"electricity-and-mechanics": ["socialrobot", "wegostem", "stem", "physical_computing"],
"nature-and-climate": ["kiks", "agriculture"],
"agriculture": ["agriculture"],
"society": ["kiks", "socialrobot", "care", "chatbot"],
"math": ["kiks", "math_with_python", "python_programming", "stem", "care", "basics_ai"],
"technology": ["socialrobot", "wegostem", "computational_thinking", "stem", "physical_computing", "basics_ai"],
"algorithms": ["math_with_python", "python_programming", "stem", "algorithms", "basics_ai"],
};
export const AGEITEMS = [
"all", "primary-school", "lower-secondary", "upper-secondary", "high-school", "older"
];
export const AGE_TO_THEMES: Record<string, string[]> = {
"all": THEMES_KEYS,
"primary-school": ["wegostem", "computational_thinking", "physical_computing"],
"lower-secondary": ["socialrobot", "art", "wegostem", "computational_thinking", "physical_computing"],
"upper-secondary": ["kiks", "art", "socialrobot", "agriculture",
"computational_thinking", "math_with_python", "python_programming",
"stem", "care", "chatbot", "algorithms", "basics_ai"],
"high-school": [
"kiks", "art", "agriculture", "computational_thinking", "math_with_python", "python_programming",
"stem", "care", "chatbot", "algorithms", "basics_ai"
],
"older": [
"kiks", "computational_thinking", "algorithms", "basics_ai"
]
};

View file

@ -1,28 +1,196 @@
<script setup lang="ts"> <script setup lang="ts">
import auth from "@/services/auth/auth-service.ts";
import apiClient from "@/services/api-client.ts";
import { ref } from "vue"; import { ref } from "vue";
import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg";
import { useI18n } from "vue-i18n";
const testResponse = ref(null); const { t, locale } = useI18n();
async function testAuthenticated() { const languages = ref([
testResponse.value = await apiClient.get("/auth/testAuthenticatedOnly"); { name: "English", code: "en" },
} { name: "Nederlands", code: "nl" },
{ name: "Deutsch", code: "de" },
{ name: "français", code: "fr" },
]);
// Logic to change the language of the website to the selected language
const changeLanguage = (langCode: string) => {
locale.value = langCode;
localStorage.setItem("user-lang", langCode);
};
</script> </script>
<template> <template>
<main> <main>
<!-- TODO Placeholder implementation to test the login - replace by a more beautiful page later --> <div class="layout">
<b>Welcome to the dwengo homepage</b> <div class="container_left">
<div v-if="auth.isLoggedIn.value"> <img
<p>Hello {{ auth.authState.user?.profile.name }}!</p> :src="dwengoLogo"
<p> style="align-self: center"
Your access token for the backend is: <code>{{ auth.authState.user?.access_token }}</code> />
</p> <h> {{ t("homeTitle") }}</h>
<p class="info">
{{ t("homeIntroduction1") }}
</p>
<p class="info">{{ t("homeIntroduction2") }}</p>
<v-btn
size="large"
density="comfortable"
style="font-weight: bolder; color: white; align-self: center"
color="#88BD28"
to="/login"
>
{{ t("login") }}
<v-icon
end
size="x-large"
>
mdi-menu-right
</v-icon>
</v-btn>
</div>
<div class="container_middle">
<div class="img_small">
<v-img
height="125"
width="125"
src="/assets/home/innovative.png"
></v-img>
<h class="big">{{ t("innovative") }}</h>
</div>
<div class="img_small">
<v-img
height="125"
width="125"
src="/assets/home/research_based.png"
></v-img>
<h class="big">{{ t("researchBased") }}</h>
</div>
<div class="img_small">
<v-img
height="125"
width="125"
src="/assets/home/inclusive.png"
></v-img>
<h class="big">{{ t("sociallyRelevant") }}</h>
</div>
<div class="img_small">
<v-img
height="125"
width="125"
src="/assets/home/socially_relevant.png"
></v-img>
<h class="big">{{ t("inclusive") }}</h>
</div>
</div>
<div class="container_right">
<v-menu open-on-hover>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon
variant="text"
>
{{ t("translate") }}
<v-icon
end
icon="mdi-translate"
size="small"
color="#0e6942"
></v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
v-for="(language, index) in languages"
:key="index"
@click="changeLanguage(language.code)"
>
<v-list-item-title>{{ language.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</div> </div>
<v-btn @click="testAuthenticated">Send test request</v-btn>
<p v-if="testResponse">Response from the test request: {{ testResponse }}</p>
</main> </main>
</template> </template>
<style scoped></style> <style scoped>
.layout {
display: flex;
width: 100vw;
height: 100vh;
position: relative;
flex-wrap: wrap;
}
.container_left {
width: 600px;
background-color: #f6faf2;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.container_middle {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 20px;
align-items: flex-start;
justify-content: center;
}
.container_right {
position: absolute;
top: 2%;
right: 100px;
}
.img_small {
display: flex;
align-items: center;
margin: 20px;
}
img {
width: 300px;
margin-bottom: 10px;
}
h {
font-size: large;
font-weight: bold;
align-self: center;
padding: 10px;
}
.big {
font-size: x-large;
}
.info {
text-align: center;
padding-left: 10px;
padding-right: 10px;
padding-bottom: 30px;
}
@media (max-width: 1024px) {
.container_left {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 20px;
position: relative;
}
.container_right {
position: absolute;
top: 10px;
right: 80px;
}
}
</style>

View file

@ -70,6 +70,7 @@
You are currently logged in as {{ auth.authState.user!.profile.name }} ({{ auth.authState.activeRole }}) You are currently logged in as {{ auth.authState.user!.profile.name }} ({{ auth.authState.activeRole }})
</p> </p>
<v-btn @click="performLogout">Logout</v-btn> <v-btn @click="performLogout">Logout</v-btn>
<v-btn to="/user">home</v-btn>
</div> </div>
</main> </main>
</template> </template>

View file

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<main></main>
</template>
<style scoped>
</style>

View file

@ -1,7 +0,0 @@
<script setup lang="ts"></script>
<template>
<main></main>
</template>
<style scoped></style>

View file

@ -1,7 +0,0 @@
<script setup lang="ts"></script>
<template>
<main></main>
</template>
<style scoped></style>

View file

@ -1,7 +1,125 @@
<script setup lang="ts"></script> <script setup lang="ts">
import {ref, watch} from "vue";
import {useI18n} from "vue-i18n";
import {THEMESITEMS, AGE_TO_THEMES} from "@/utils/constants.ts";
import BrowseThemes from "@/components/BrowseThemes.vue";
const {t, locale} = useI18n();
const selectedThemeKey = ref<string>('all');
const selectedAgeKey = ref<string>('all');
const allThemes = ref(Object.keys(THEMESITEMS));
const availableThemes = ref([...allThemes.value]);
const allAges = ref(Object.keys(AGE_TO_THEMES));
const availableAges = ref([...allAges.value]);
// Reset selection when language changes
watch(locale, () => {
selectedThemeKey.value = 'all';
selectedAgeKey.value = 'all';
});
watch(selectedThemeKey, () => {
if (selectedThemeKey.value === "all") {
availableAges.value = [...allAges.value]; // Reset to all ages
} else {
const themes = THEMESITEMS[selectedThemeKey.value];
availableAges.value = allAges.value.filter(age =>
AGE_TO_THEMES[age]?.some(theme => themes.includes(theme))
);
}
});
watch(selectedAgeKey, () => {
if (selectedAgeKey.value === "all") {
availableThemes.value = [...allThemes.value]; // Reset to all themes
} else {
const themes = AGE_TO_THEMES[selectedAgeKey.value];
availableThemes.value = allThemes.value.filter(theme =>
THEMESITEMS[theme]?.some(theme => themes.includes(theme))
);
}
});
</script>
<template> <template>
<main></main> <div class="main-container">
<h1 class="title">{{ t("themes") }}</h1>
<v-container class="dropdowns">
<v-select class="v-select"
:label="t('choose-theme')"
:items="availableThemes.map(theme => ({ title: t(`theme-options.${theme}`), value: theme }))"
v-model="selectedThemeKey"
item-title="title"
item-value="value"
variant="outlined"
/>
<v-select
class="v-select"
:label="t('choose-age')"
:items="availableAges.map(age => ({ key: age, label: t(`age-options.${age}`), value: age }))"
v-model="selectedAgeKey"
item-title="label"
item-value="key"
variant="outlined"
></v-select>
</v-container>
<BrowseThemes :selectedTheme="selectedThemeKey ?? ''" :selectedAge="selectedAgeKey ?? ''"/>
</div>
</template> </template>
<style scoped></style> <style scoped>
.main-container {
min-height: 100vh;
min-width: 100vw;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.title {
max-width: 50rem;
margin-left: 1rem;
margin-top: 1rem;
text-align: center;
display: flex;
justify-content: center;
}
.dropdowns {
display: flex;
justify-content: space-between;
gap: 5rem;
width: 80%;
}
.v-select {
flex: 1;
min-width: 100px;
}
@media (max-width: 768px) {
.main-container {
padding: 1rem;
}
}
@media (max-width: 700px) {
.dropdowns {
flex-direction: column;
gap: 1rem;
width: 80%;
}
}
</style>

802
package-lock.json generated

File diff suppressed because it is too large Load diff