Merge remote-tracking branch 'refs/remotes/origin/fix/databank-laat-toevoegen-van-meerdere-studenten-met-dezelfde-username-toe-#153' into feat/user-routes
# Conflicts: # backend/src/controllers/students.ts # backend/src/controllers/teachers.ts # backend/src/exceptions.ts # backend/src/interfaces/student.ts # backend/src/routes/router.ts # backend/src/routes/students.ts # backend/src/services/students.ts # backend/src/services/teachers.ts # frontend/src/controllers/controllers.ts
This commit is contained in:
		
						commit
						7b65d2a5b8
					
				
					 78 changed files with 1939 additions and 1094 deletions
				
			
		
							
								
								
									
										15
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										15
									
								
								README.md
									
										
									
									
									
								
							|  | @ -21,14 +21,16 @@ Alternatief kan je één van de volgende methodes gebruiken om de applicatie lok | |||
| 
 | ||||
| ### 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/) | ||||
|    en [Docker Compose](https://docs.docker.com/compose/)). | ||||
| 2. Clone deze repository. | ||||
| 3. In de backend, kopieer `.env.example` (of `.env.development.example`) naar `.env` en pas de variabelen aan waar | ||||
|    nodig. | ||||
| 4. Voer `docker compose up` uit in de root van de repository. | ||||
| 3. In de backend, kopieer `.env.example` naar `.env` en pas de variabelen aan waar nodig. | ||||
| 4. Voer `docker compose -f compose.staging.yml up --build` uit in de root van de repository. | ||||
| 5. Optioneel: Configureer de applicatie aan de hand van | ||||
|    de [configuratiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving#dwengo-1-configuratie). | ||||
| 6. De applicatie is nu beschikbaar op [`http://localhost/`](http://localhost/) en [`http://localhost/api`](http://localhost/api). | ||||
| 
 | ||||
| ```bash | ||||
| docker compose version | ||||
|  | @ -38,14 +40,13 @@ cp .env.example .env | |||
| # Pas .env aan | ||||
| nano .env | ||||
| cd .. | ||||
| docker compose up | ||||
| # Configureer de applicatie | ||||
| docker compose -f compose.staging.yml up --build | ||||
| ``` | ||||
| 
 | ||||
| ### Handmatige installatie | ||||
| ### Handmatige installatie en ontwikkeling | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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_PORT=5431 | ||||
| #DWENGO_DB_NAME=dwengo | ||||
| DWENGO_DB_USERNAME=postgres | ||||
| DWENGO_DB_PASSWORD=postgres | ||||
| DWENGO_DB_UPDATE=true | ||||
| 
 | ||||
| # Auth | ||||
| #DWENGO_DB_CONTENT_PREFIX=u_ | ||||
| 
 | ||||
| DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student | ||||
| 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_CLIENT_ID=dwengo | ||||
| 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_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 | ||||
|  |  | |||
|  | @ -1,27 +1,68 @@ | |||
| # | ||||
| # 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_PORT=5431 | ||||
| 
 | ||||
| # Change this to the actual credentials of the user Dwengo should use in the backend | ||||
| DWENGO_DB_USERNAME=postgres | ||||
| DWENGO_DB_PASSWORD=postgres | ||||
| 
 | ||||
| # The port of the database. | ||||
| #DWENGO_DB_PORT=5432 | ||||
| # The name of the database. | ||||
| #DWENGO_DB_NAME=dwengo | ||||
| # ! 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. | ||||
| 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. | ||||
| DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student | ||||
| # ! Change this! The external URL for student authentication. Should be reachable by the client. | ||||
| # 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_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs | ||||
| 
 | ||||
| # Data for the identity provider via which the teachers authenticate. | ||||
| DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher | ||||
| # ! 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. | ||||
| # E.g. http://idp:7080/realms/student/protocol/openid-connect/certs | ||||
| 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_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 | ||||
| # LOKI_HOST=http://localhost:3102 | ||||
| # Allowed origins for CORS requests. Separate multiple origins with a comma. | ||||
| #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 | ||||
|  |  | |||
|  | @ -1,28 +1,37 @@ | |||
| DWENGO_PORT=3000 # The port the backend will listen on | ||||
| DWENGO_DB_HOST=db # Name of the database container | ||||
| DWENGO_DB_PORT=5431 | ||||
| # | ||||
| # Production environment configuration | ||||
| # | ||||
| # 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_USERNAME=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_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_CLIENT_ID=dwengo | ||||
| 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_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_AUDIENCE=account | ||||
| 
 | ||||
| # | ||||
| # Advanced configuration | ||||
| # | ||||
| #DWENGO_CORS_ALLOWED_ORIGINS= | ||||
| #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 | ||||
|  |  | |||
|  | @ -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_UPDATE=true | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ COPY package-lock.json backend/package.json ./ | |||
| RUN npm install --silent --only=production | ||||
| 
 | ||||
| COPY ./docs /docs | ||||
| COPY ./backend/i18n /app/i18n | ||||
| COPY --from=build-stage /app/backend/dist ./dist/ | ||||
| 
 | ||||
| EXPOSE 3000 | ||||
|  |  | |||
|  | @ -4,23 +4,24 @@ | |||
| 
 | ||||
| ```shell | ||||
| 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 | ||||
| # Omgevingsvariabelen | ||||
| cp .env.development.example .env.development.local | ||||
| 
 | ||||
| npm run dev | ||||
| ``` | ||||
| 
 | ||||
| ### Production | ||||
| 
 | ||||
| ```shell | ||||
| npm run build | ||||
| npm run start | ||||
| ``` | ||||
| 
 | ||||
| ### Tests | ||||
| 
 | ||||
| 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 | ||||
| ``` | ||||
| 
 | ||||
| ### 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 | ||||
| 
 | ||||
| Tijdens development is het voldoende om gebruik te maken van de keycloak configuratie die automatisch ingeladen wordt. | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | |||
| import apiRouter from './routes/router.js'; | ||||
| import swaggerMiddleware from './swagger.js'; | ||||
| import swaggerUi from 'swagger-ui-express'; | ||||
| import { errorHandler } from './middleware/error-handling/error-handler'; | ||||
| 
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
|  | @ -26,6 +27,8 @@ app.use('/api', apiRouter); | |||
| // Swagger
 | ||||
| app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); | ||||
| 
 | ||||
| app.use(errorHandler); | ||||
| 
 | ||||
| async function startServer() { | ||||
|     await initORM(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,12 +1,7 @@ | |||
| import { EnvVars, getEnvVar } from './util/envvars.js'; | ||||
| import { Language } from './entities/content/language.js'; | ||||
| 
 | ||||
| // API
 | ||||
| export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); | ||||
| 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; | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ export async function createAssignmentHandler(req: Request<AssignmentParams>, re | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.status(201).json({ assignment: assignment }); | ||||
|     res.status(201).json(assignment); | ||||
| } | ||||
| 
 | ||||
| export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { | ||||
|  | @ -62,13 +62,14 @@ export async function getAssignmentHandler(req: Request<AssignmentParams>, res: | |||
| export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> { | ||||
|     const classid = req.params.classid; | ||||
|     const assignmentNumber = +req.params.id; | ||||
|     const full = req.query.full === 'true'; | ||||
| 
 | ||||
|     if (isNaN(assignmentNumber)) { | ||||
|         res.status(400).json({ error: 'Assignment id must be a number' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const submissions = await getAssignmentsSubmissions(classid, assignmentNumber); | ||||
|     const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full); | ||||
| 
 | ||||
|     res.json({ | ||||
|         submissions: submissions, | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/class.js'; | ||||
| import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/classes.js'; | ||||
| import { ClassDTO } from '../interfaces/class.js'; | ||||
| 
 | ||||
| export async function getAllClassesHandler(req: Request, res: Response): Promise<void> { | ||||
|  | @ -28,11 +28,10 @@ export async function createClassHandler(req: Request, res: Response): Promise<v | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.status(201).json({ class: cls }); | ||||
|     res.status(201).json(cls); | ||||
| } | ||||
| 
 | ||||
| export async function getClassHandler(req: Request, res: Response): Promise<void> { | ||||
|     try { | ||||
|     const classId = req.params.id; | ||||
|     const cls = await getClass(classId); | ||||
| 
 | ||||
|  | @ -40,18 +39,8 @@ export async function getClassHandler(req: Request, res: Response): Promise<void | |||
|         res.status(404).json({ error: 'Class not found' }); | ||||
|         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' }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> { | ||||
|  | @ -67,7 +56,7 @@ export async function getClassStudentsHandler(req: Request, res: Response): Prom | |||
| 
 | ||||
| export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> { | ||||
|     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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,6 +28,11 @@ export async function getGroupHandler(req: Request<GroupParams>, res: Response): | |||
| 
 | ||||
|     const group = await getGroup(classId, assignmentId, groupId, full); | ||||
| 
 | ||||
|     if (!group) { | ||||
|         res.status(404).json({ error: 'Group not found' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json(group); | ||||
| } | ||||
| 
 | ||||
|  | @ -66,12 +71,12 @@ export async function createGroupHandler(req: Request, res: Response): Promise<v | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.status(201).json({ group: group }); | ||||
|     res.status(201).json(group); | ||||
| } | ||||
| 
 | ||||
| export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> { | ||||
|     const classId = req.params.classid; | ||||
|     // Const full = req.query.full === 'true';
 | ||||
|     const full = req.query.full === 'true'; | ||||
| 
 | ||||
|     const assignmentId = +req.params.assignmentid; | ||||
| 
 | ||||
|  | @ -87,7 +92,7 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const submissions = await getGroupSubmissions(classId, assignmentId, groupId); | ||||
|     const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full); | ||||
| 
 | ||||
|     res.json({ | ||||
|         submissions: submissions, | ||||
|  |  | |||
|  | @ -4,9 +4,9 @@ import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifie | |||
| import learningObjectService from '../services/learning-objects/learning-object-service.js'; | ||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | ||||
| import { Language } from '../entities/content/language.js'; | ||||
| import { BadRequestException } from '../exceptions.js'; | ||||
| import attachmentService from '../services/learning-objects/attachment-service.js'; | ||||
| import { NotFoundError } from '@mikro-orm/core'; | ||||
| import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||
| 
 | ||||
| function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { | ||||
|     if (!req.params.hruid) { | ||||
|  | @ -40,7 +40,7 @@ export async function getAllLearningObjects(req: Request, res: Response): Promis | |||
|         learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId); | ||||
|     } | ||||
| 
 | ||||
|     res.json(learningObjects); | ||||
|     res.json({ learningObjects: learningObjects }); | ||||
| } | ||||
| 
 | ||||
| export async function getLearningObject(req: Request, res: Response): Promise<void> { | ||||
|  |  | |||
|  | @ -2,13 +2,14 @@ import { Request, Response } from 'express'; | |||
| import { themes } from '../data/themes.js'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import learningPathService from '../services/learning-paths/learning-path-service.js'; | ||||
| import { BadRequestException, NotFoundException } from '../exceptions.js'; | ||||
| import { Language } from '../entities/content/language.js'; | ||||
| import { | ||||
|     PersonalizationTarget, | ||||
|     personalizedForGroup, | ||||
|     personalizedForStudent, | ||||
| } from '../services/learning-paths/learning-path-personalization-util.js'; | ||||
| import { BadRequestException } from '../exceptions/bad-request-exception.js'; | ||||
| import { NotFoundException } from '../exceptions/not-found-exception.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Fetch learning paths based on query parameters. | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi | |||
|     if (!questions) { | ||||
|         res.status(404).json({ error: `Questions not found.` }); | ||||
|     } else { | ||||
|         res.json(questions); | ||||
|         res.json({ questions: questions }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -76,12 +76,12 @@ export async function getQuestionAnswersHandler(req: Request, res: Response): Pr | |||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const answers = getAnswersByQuestion(questionId, full); | ||||
|     const answers = await getAnswersByQuestion(questionId, full); | ||||
| 
 | ||||
|     if (!answers) { | ||||
|         res.status(404).json({ error: `Questions not found.` }); | ||||
|         res.status(404).json({ error: `Questions not found` }); | ||||
|     } 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); | ||||
| 
 | ||||
|     if (!question) { | ||||
|         res.status(400).json({ error: 'Could not add question' }); | ||||
|         res.status(400).json({ error: 'Could not create question' }); | ||||
|     } else { | ||||
|         res.json(question); | ||||
|     } | ||||
|  |  | |||
|  | @ -93,9 +93,10 @@ export async function getStudentGroupsHandler(req: Request, res: Response): Prom | |||
| 
 | ||||
| export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> { | ||||
|     const username = req.params.username; | ||||
|     const full = req.query.full === 'true'; | ||||
|     requireFields({ username }); | ||||
| 
 | ||||
|     const submissions = await getStudentSubmissions(username); | ||||
|     const submissions = await getStudentSubmissions(username, full); | ||||
| 
 | ||||
|     res.json({ | ||||
|         submissions, | ||||
|  |  | |||
|  | @ -36,10 +36,11 @@ export async function createSubmissionHandler(req: Request, res: Response) { | |||
|     const submission = await createSubmission(submissionDTO); | ||||
| 
 | ||||
|     if (!submission) { | ||||
|         res.status(404).json({ error: 'Submission not added' }); | ||||
|     } else { | ||||
|         res.json(submission); | ||||
|         res.status(400).json({ error: 'Failed to create submission' }); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json(submission); | ||||
| } | ||||
| 
 | ||||
| export async function deleteSubmissionHandler(req: Request, res: Response) { | ||||
|  | @ -53,7 +54,8 @@ export async function deleteSubmissionHandler(req: Request, res: Response) { | |||
| 
 | ||||
|     if (!submission) { | ||||
|         res.status(404).json({ error: 'Submission not found' }); | ||||
|     } else { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     res.json(submission); | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -1,10 +1,12 @@ | |||
| import { EntityRepository, FilterQuery } from '@mikro-orm/core'; | ||||
| import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception'; | ||||
| 
 | ||||
| export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { | ||||
|     public async save(entity: T) { | ||||
|         const em = this.getEntityManager(); | ||||
|         em.persist(entity); | ||||
|         await em.flush(); | ||||
|     public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> { | ||||
|         if (options?.preventOverwrite && (await this.findOne(entity))) { | ||||
|             throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`); | ||||
|         } | ||||
|         await this.getEntityManager().persistAndFlush(entity); | ||||
|     } | ||||
|     public async deleteWhere(query: FilterQuery<T>) { | ||||
|         const toDelete = await this.findOne(query); | ||||
|  |  | |||
|  | @ -2,8 +2,6 @@ import { AnyEntity, EntityManager, EntityName, EntityRepository } from '@mikro-o | |||
| import { forkEntityManager } from '../orm.js'; | ||||
| import { StudentRepository } from './users/student-repository.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 { TeacherRepository } from './users/teacher-repository.js'; | ||||
| import { Class } from '../entities/classes/class.entity.js'; | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| import { Student } from '../../entities/users/student.entity.js'; | ||||
| import { User } from '../../entities/users/user.entity.js'; | ||||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||
| // Import { UserRepository } from './user-repository.js';
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||
| import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; | ||||
| import { UserRepository } from './user-repository.js'; | ||||
| 
 | ||||
| export class TeacherRepository extends DwengoEntityRepository<Teacher> { | ||||
|     public findByUsername(username: string): Promise<Teacher | null> { | ||||
|  |  | |||
|  | @ -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 { Group } from './group.entity.js'; | ||||
| import { Language } from '../content/language.js'; | ||||
|  |  | |||
|  | @ -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 { Student } from '../users/student.entity.js'; | ||||
| import { GroupRepository } from '../../data/assignments/group-repository.js'; | ||||
|  |  | |||
|  | @ -3,6 +3,12 @@ import { Student } from '../users/student.entity.js'; | |||
| import { Class } from './class.entity.js'; | ||||
| import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; | ||||
| 
 | ||||
| export enum ClassJoinRequestStatus { | ||||
|     Open = 'open', | ||||
|     Accepted = 'accepted', | ||||
|     Declined = 'declined', | ||||
| } | ||||
| 
 | ||||
| @Entity({ | ||||
|     repository: () => ClassJoinRequestRepository, | ||||
| }) | ||||
|  | @ -22,9 +28,3 @@ export class ClassJoinRequest { | |||
|     @Enum(() => ClassJoinRequestStatus) | ||||
|     status!: ClassJoinRequestStatus; | ||||
| } | ||||
| 
 | ||||
| export enum ClassJoinRequestStatus { | ||||
|     Open = 'open', | ||||
|     Accepted = 'accepted', | ||||
|     Declined = 'declined', | ||||
| } | ||||
|  |  | |||
|  | @ -13,12 +13,4 @@ export class Student extends User { | |||
| 
 | ||||
|     @ManyToMany(() => Group) | ||||
|     groups!: Collection<Group>; | ||||
| 
 | ||||
|     constructor( | ||||
|         public username: string, | ||||
|         public firstName: string, | ||||
|         public lastName: string | ||||
|     ) { | ||||
|         super(); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -7,12 +7,4 @@ import { TeacherRepository } from '../../data/users/teacher-repository.js'; | |||
| export class Teacher extends User { | ||||
|     @ManyToMany(() => Class) | ||||
|     classes!: Collection<Class>; | ||||
| 
 | ||||
|     constructor( | ||||
|         public username: string, | ||||
|         public firstName: string, | ||||
|         public lastName: string | ||||
|     ) { | ||||
|         super(); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,56 +0,0 @@ | |||
| /** | ||||
|  * Exception for HTTP 400 Bad Request | ||||
|  */ | ||||
| export class BadRequestException extends Error { | ||||
|     public status = 400; | ||||
| 
 | ||||
|     constructor(error: string) { | ||||
|         super(error); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Exception for HTTP 401 Unauthorized | ||||
|  */ | ||||
| export class UnauthorizedException extends Error { | ||||
|     status = 401; | ||||
|     constructor(message: string = 'Unauthorized') { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Exception for HTTP 403 Forbidden | ||||
|  */ | ||||
| export class ForbiddenException extends Error { | ||||
|     status = 403; | ||||
| 
 | ||||
|     constructor(message: string = 'Forbidden') { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Exception for HTTP 404 Not Found | ||||
|  */ | ||||
| export class NotFoundException extends Error { | ||||
|     public status = 404; | ||||
| 
 | ||||
|     constructor(error: string) { | ||||
|         super(error); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class ConflictException extends Error { | ||||
|     public status = 409; | ||||
|     constructor(message: string = 'Conflict') { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class InternalServerError extends Error { | ||||
|     public status = 500; | ||||
|     constructor(message: string = 'Internal Server Error') { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								backend/src/exceptions/bad-request-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/exceptions/bad-request-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Exception for HTTP 400 Bad Request | ||||
|  */ | ||||
| export class BadRequestException extends ExceptionWithHttpState { | ||||
|     constructor(error: string) { | ||||
|         super(400, error); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										12
									
								
								backend/src/exceptions/conflict-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/conflict-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Exception for HTTP 409 Conflict | ||||
|  */ | ||||
| export class ConflictException extends ExceptionWithHttpState { | ||||
|     public status = 409; | ||||
| 
 | ||||
|     constructor(error: string) { | ||||
|         super(409, error); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,7 @@ | |||
| import { ConflictException } from './conflict-exception'; | ||||
| 
 | ||||
| export class EntityAlreadyExistsException extends ConflictException { | ||||
|     constructor(message: string) { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										11
									
								
								backend/src/exceptions/exception-with-http-state.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								backend/src/exceptions/exception-with-http-state.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| /** | ||||
|  * Exceptions which are associated with a HTTP error code. | ||||
|  */ | ||||
| export abstract class ExceptionWithHttpState extends Error { | ||||
|     constructor( | ||||
|         public status: number, | ||||
|         public error: string | ||||
|     ) { | ||||
|         super(error); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										12
									
								
								backend/src/exceptions/forbidden-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/forbidden-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Exception for HTTP 403 Forbidden | ||||
|  */ | ||||
| export class ForbiddenException extends ExceptionWithHttpState { | ||||
|     status = 403; | ||||
| 
 | ||||
|     constructor(message: string = 'Forbidden') { | ||||
|         super(403, message); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										12
									
								
								backend/src/exceptions/not-found-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/exceptions/not-found-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Exception for HTTP 404 Not Found | ||||
|  */ | ||||
| export class NotFoundException extends ExceptionWithHttpState { | ||||
|     public status = 404; | ||||
| 
 | ||||
|     constructor(error: string) { | ||||
|         super(404, error); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								backend/src/exceptions/unauthorized-exception.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/exceptions/unauthorized-exception.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| import { ExceptionWithHttpState } from './exception-with-http-state.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Exception for HTTP 401 Unauthorized | ||||
|  */ | ||||
| export class UnauthorizedException extends ExceptionWithHttpState { | ||||
|     constructor(message: string = 'Unauthorized') { | ||||
|         super(401, message); | ||||
|     } | ||||
| } | ||||
|  | @ -2,7 +2,7 @@ import { FALLBACK_LANG } from '../config.js'; | |||
| import { Assignment } from '../entities/assignments/assignment.entity.js'; | ||||
| import { Class } from '../entities/classes/class.entity.js'; | ||||
| import { languageMap } from '../entities/content/language.js'; | ||||
| import { GroupDTO, mapToGroupDTO } from './group.js'; | ||||
| import { GroupDTO } from './group.js'; | ||||
| 
 | ||||
| export interface AssignmentDTO { | ||||
|     id: number; | ||||
|  | @ -46,7 +46,5 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi | |||
|     assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; | ||||
|     assignment.within = cls; | ||||
| 
 | ||||
|     console.log(assignment); | ||||
| 
 | ||||
|     return assignment; | ||||
| } | ||||
|  |  | |||
|  | @ -9,12 +9,6 @@ export interface ClassDTO { | |||
|     teachers: string[]; | ||||
|     students: string[]; | ||||
|     joinRequests: string[]; | ||||
|     endpoints?: { | ||||
|         self: string; | ||||
|         invitations: string; | ||||
|         assignments: string; | ||||
|         students: string; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export function mapToClassDTO(cls: Class): ClassDTO { | ||||
|  |  | |||
|  | @ -1,8 +1,6 @@ | |||
| import { Question } from '../entities/questions/question.entity.js'; | ||||
| import { UserDTO } from './user.js'; | ||||
| import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; | ||||
| import { mapToStudentDTO, StudentDTO } from './student.js'; | ||||
| import { TeacherDTO } from './teacher.js'; | ||||
| 
 | ||||
| export interface QuestionDTO { | ||||
|     learningObjectIdentifier: LearningObjectIdentifier; | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| import { Student } from '../entities/users/student.entity.js'; | ||||
| import { getStudentRepository } from '../data/repositories'; | ||||
| 
 | ||||
| export interface StudentDTO { | ||||
|     id?: string; | ||||
|     id: string; | ||||
|     username: string; | ||||
|     firstName: string; | ||||
|     lastName: string; | ||||
|  | @ -17,5 +18,9 @@ export function mapToStudentDTO(student: Student): StudentDTO { | |||
| } | ||||
| 
 | ||||
| export function mapToStudent(studentData: StudentDTO): Student { | ||||
|     return new Student(studentData.username, studentData.firstName, studentData.lastName); | ||||
|     return getStudentRepository().create({ | ||||
|         username: studentData.username, | ||||
|         firstName: studentData.firstName, | ||||
|         lastName: studentData.lastName, | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -2,13 +2,10 @@ import { Submission } from '../entities/assignments/submission.entity.js'; | |||
| import { Language } from '../entities/content/language.js'; | ||||
| import { GroupDTO, mapToGroupDTO } from './group.js'; | ||||
| import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js'; | ||||
| import { mapToUser } from './user'; | ||||
| import { Student } from '../entities/users/student.entity'; | ||||
| import { LearningObjectIdentifier } from './learning-content.js'; | ||||
| 
 | ||||
| export interface SubmissionDTO { | ||||
|     learningObjectHruid: string; | ||||
|     learningObjectLanguage: Language; | ||||
|     learningObjectVersion: number; | ||||
|     learningObjectIdentifier: LearningObjectIdentifier; | ||||
| 
 | ||||
|     submissionNumber?: number; | ||||
|     submitter: StudentDTO; | ||||
|  | @ -17,11 +14,21 @@ export interface SubmissionDTO { | |||
|     content: string; | ||||
| } | ||||
| 
 | ||||
| export interface SubmissionDTOId { | ||||
|     learningObjectHruid: string; | ||||
|     learningObjectLanguage: Language; | ||||
|     learningObjectVersion: number; | ||||
| 
 | ||||
|     submissionNumber?: number; | ||||
| } | ||||
| 
 | ||||
| export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { | ||||
|     return { | ||||
|         learningObjectHruid: submission.learningObjectHruid, | ||||
|         learningObjectLanguage: submission.learningObjectLanguage, | ||||
|         learningObjectVersion: submission.learningObjectVersion, | ||||
|         learningObjectIdentifier: { | ||||
|             hruid: submission.learningObjectHruid, | ||||
|             language: submission.learningObjectLanguage, | ||||
|             version: submission.learningObjectVersion, | ||||
|         }, | ||||
| 
 | ||||
|         submissionNumber: submission.submissionNumber, | ||||
|         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 { | ||||
|     const submission = new Submission(); | ||||
|     submission.learningObjectHruid = submissionDTO.learningObjectHruid; | ||||
|     submission.learningObjectLanguage = submissionDTO.learningObjectLanguage; | ||||
|     submission.learningObjectVersion = submissionDTO.learningObjectVersion; | ||||
|     submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid; | ||||
|     submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language; | ||||
|     submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!; | ||||
|     // Submission.submissionNumber = submissionDTO.submissionNumber;
 | ||||
|     submission.submitter = mapToStudent(submissionDTO.submitter); | ||||
|     // Submission.submissionTime = submissionDTO.time;
 | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| import { Teacher } from '../entities/users/teacher.entity.js'; | ||||
| import { getTeacherRepository } from '../data/repositories'; | ||||
| 
 | ||||
| export interface TeacherDTO { | ||||
|     id: string; | ||||
|  | @ -22,8 +23,10 @@ export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { | |||
|     }; | ||||
| } | ||||
| 
 | ||||
| export function mapToTeacher(TeacherData: TeacherDTO): Teacher { | ||||
|     const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName); | ||||
| 
 | ||||
|     return teacher; | ||||
| export function mapToTeacher(teacherData: TeacherDTO): Teacher { | ||||
|     return getTeacherRepository().create({ | ||||
|         username: teacherData.username, | ||||
|         firstName: teacherData.firstName, | ||||
|         lastName: teacherData.lastName, | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; | ||||
| import LokiTransport from 'winston-loki'; | ||||
| import { LokiLabels } from 'loki-logger-ts'; | ||||
| import { LOG_LEVEL, LOKI_HOST } from '../config.js'; | ||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | ||||
| 
 | ||||
| export class Logger extends WinstonLogger { | ||||
|     constructor() { | ||||
|  | @ -22,10 +22,25 @@ function initializeLogger(): 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({ | ||||
|         host: LOKI_HOST, | ||||
|         host: lokiHost, | ||||
|         labels: Labels, | ||||
|         level: LOG_LEVEL, | ||||
|         level: logLevel, | ||||
|         json: true, | ||||
|         format: format.combine(format.timestamp(), format.json()), | ||||
|         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({ | ||||
|         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; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,8 @@ import * as express from 'express'; | |||
| import * as jwt from 'jsonwebtoken'; | ||||
| import { AuthenticatedRequest } from './authenticated-request.js'; | ||||
| import { AuthenticationInfo } from './authentication-info.js'; | ||||
| import { ForbiddenException, UnauthorizedException } from '../../exceptions.js'; | ||||
| import { UnauthorizedException } from '../../exceptions/unauthorized-exception'; | ||||
| import { ForbiddenException } from '../../exceptions/forbidden-exception'; | ||||
| 
 | ||||
| const JWKS_CACHE = true; | ||||
| const JWKS_RATE_LIMIT = true; | ||||
|  |  | |||
							
								
								
									
										15
									
								
								backend/src/middleware/error-handling/error-handler.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								backend/src/middleware/error-handling/error-handler.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| import { NextFunction, Request, Response } from 'express'; | ||||
| import { getLogger, Logger } from '../../logging/initalize'; | ||||
| import { ExceptionWithHttpState } from '../../exceptions/exception-with-http-state'; | ||||
| 
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
| export function errorHandler(err: unknown, req: Request, res: Response, _: NextFunction): void { | ||||
|     if (err instanceof ExceptionWithHttpState) { | ||||
|         logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`); | ||||
|         res.status(err.status).json(err); | ||||
|     } else { | ||||
|         logger.error(`Unexpected error occurred while handing a request: ${JSON.stringify(err)}`); | ||||
|         res.status(500).json(err); | ||||
|     } | ||||
| } | ||||
|  | @ -3,7 +3,6 @@ import { PostgreSqlDriver } from '@mikro-orm/postgresql'; | |||
| import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; | ||||
| import { SqliteDriver } from '@mikro-orm/sqlite'; | ||||
| import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; | ||||
| import { LOG_LEVEL } from './config.js'; | ||||
| 
 | ||||
| // Import alle entity-bestanden handmatig
 | ||||
| import { User } from './entities/users/user.entity.js'; | ||||
|  | @ -66,10 +65,12 @@ function config(testingMode: boolean = false): Options { | |||
|         user: getEnvVar(EnvVars.DbUsername), | ||||
|         password: getEnvVar(EnvVars.DbPassword), | ||||
|         entities: entities, | ||||
|         persistOnCreate: false, // Entities should not be implicitly persisted when calling create(...), but only after
 | ||||
|         // They were saved explicitly.
 | ||||
|         // EntitiesTs: entitiesTs,
 | ||||
| 
 | ||||
|         // Logging
 | ||||
|         debug: LOG_LEVEL === 'debug', | ||||
|         debug: getEnvVar(EnvVars.LogLevel) === 'debug', | ||||
|         loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), | ||||
|     }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,11 +1,7 @@ | |||
| import { Response, Router } from 'express'; | ||||
| import studentRouter from './students.js'; | ||||
| import teacherRouter from './teachers.js'; | ||||
| import groupRouter from './groups.js'; | ||||
| import assignmentRouter from './assignments.js'; | ||||
| import submissionRouter from './submissions.js'; | ||||
| import classRouter from './classes.js'; | ||||
| import questionRouter from './questions.js'; | ||||
| import authRouter from './auth.js'; | ||||
| import themeRoutes from './themes.js'; | ||||
| import learningPathRoutes from './learning-paths.js'; | ||||
|  | @ -24,11 +20,7 @@ router.get('/', (_, res: Response) => { | |||
| 
 | ||||
| router.use('/student', studentRouter /* #swagger.tags = ['Student'] */); | ||||
| router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */); | ||||
| router.use('/group', groupRouter /* #swagger.tags = ['Group'] */); | ||||
| router.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */); | ||||
| router.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */); | ||||
| router.use('/class', classRouter /* #swagger.tags = ['Class'] */); | ||||
| router.use('/question', questionRouter /* #swagger.tags = ['Question'] */); | ||||
| router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */); | ||||
| router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */); | ||||
| router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */); | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| 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 { 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[]> { | ||||
|     const classRepository = getClassRepository(); | ||||
|  | @ -21,7 +20,7 @@ export async function getAllAssignments(classid: string, full: boolean): Promise | |||
|     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 cls = await classRepository.findById(classid); | ||||
| 
 | ||||
|  | @ -36,8 +35,9 @@ export async function createAssignment(classid: string, assignmentData: Assignme | |||
|         const newAssignment = assignmentRepository.create(assignment); | ||||
|         await assignmentRepository.save(newAssignment); | ||||
| 
 | ||||
|         return newAssignment; | ||||
|         return mapToAssignmentDTO(newAssignment); | ||||
|     } catch (e) { | ||||
|         console.error(e); | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | @ -60,7 +60,11 @@ export async function getAssignment(classid: string, id: number): Promise<Assign | |||
|     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 cls = await classRepository.findById(classid); | ||||
| 
 | ||||
|  | @ -81,5 +85,9 @@ export async function getAssignmentsSubmissions(classid: string, assignmentNumbe | |||
|     const submissionRepository = getSubmissionRepository(); | ||||
|     const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); | ||||
| 
 | ||||
|     if (full) { | ||||
|         return submissions.map(mapToSubmissionDTO); | ||||
|     } | ||||
| 
 | ||||
|     return submissions.map(mapToSubmissionDTOId); | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| 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 { mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; | ||||
| import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js'; | ||||
|  | @ -21,16 +20,14 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[ | |||
|     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 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 studentUsernames = classData.students || []; | ||||
|     const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null); | ||||
| 
 | ||||
|     //Const cls = mapToClass(classData, teachers, students);
 | ||||
|     const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); | ||||
| 
 | ||||
|     const classRepository = getClassRepository(); | ||||
| 
 | ||||
|  | @ -42,7 +39,7 @@ export async function createClass(classData: ClassDTO): Promise<Class | null> { | |||
|         }); | ||||
|         await classRepository.save(newClass); | ||||
| 
 | ||||
|         return newClass; | ||||
|         return mapToClassDTO(newClass); | ||||
|     } catch (e) { | ||||
|         logger.error(e); | ||||
|         return null; | ||||
|  | @ -1,4 +1,3 @@ | |||
| import { GroupRepository } from '../data/assignments/group-repository.js'; | ||||
| import { | ||||
|     getAssignmentRepository, | ||||
|     getClassRepository, | ||||
|  | @ -8,7 +7,7 @@ import { | |||
| } from '../data/repositories.js'; | ||||
| import { Group } from '../entities/assignments/group.entity.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> { | ||||
|     const classRepository = getClassRepository(); | ||||
|  | @ -43,7 +42,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme | |||
|     const studentRepository = getStudentRepository(); | ||||
| 
 | ||||
|     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); | ||||
| 
 | ||||
|  | @ -103,7 +102,12 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu | |||
|     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 cls = await classRepository.findById(classId); | ||||
| 
 | ||||
|  | @ -128,5 +132,9 @@ export async function getGroupSubmissions(classId: string, assignmentNumber: num | |||
|     const submissionRepository = getSubmissionRepository(); | ||||
|     const submissions = await submissionRepository.findAllSubmissionsForGroup(group); | ||||
| 
 | ||||
|     if (full) { | ||||
|         return submissions.map(mapToSubmissionDTO); | ||||
|     } | ||||
| 
 | ||||
|     return submissions.map(mapToSubmissionDTOId); | ||||
| } | ||||
|  |  | |||
|  | @ -45,6 +45,13 @@ export async function getLearningObjectById(hruid: string, language: string): Pr | |||
|     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) | ||||
|  */ | ||||
|  | @ -85,6 +92,3 @@ export async function getLearningObjectsFromPath(hruid: string, language: string | |||
| export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<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.'); | ||||
| } | ||||
|  |  | |||
|  | @ -103,5 +103,5 @@ export async function deleteQuestion(questionId: QuestionId) { | |||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     return question; | ||||
|     return mapToQuestionDTO(question); | ||||
| } | ||||
|  |  | |||
|  | @ -1,21 +1,13 @@ | |||
| import { | ||||
|     getClassJoinRequestRepository, | ||||
|     getClassRepository, | ||||
|     getGroupRepository, | ||||
|     getQuestionRepository, | ||||
|     getStudentRepository, | ||||
|     getSubmissionRepository, | ||||
| } from '../data/repositories.js'; | ||||
| import { getClassRepository, getGroupRepository, getStudentRepository, getSubmissionRepository } from '../data/repositories.js'; | ||||
| import { AssignmentDTO } from '../interfaces/assignment.js'; | ||||
| import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; | ||||
| import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.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 { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question'; | ||||
| import {ClassJoinRequestStatus} from "../entities/classes/class-join-request.entity"; | ||||
| import {ConflictException, NotFoundException} from "../exceptions"; | ||||
| import {Student} from "../entities/users/student.entity"; | ||||
| import {mapToStudentRequestDTO} from "../interfaces/student-request"; | ||||
| 
 | ||||
| export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> { | ||||
|  | @ -47,14 +39,8 @@ export async function getStudent(username: string): Promise<StudentDTO> { | |||
| export async function createStudent(userData: StudentDTO): Promise<void> { | ||||
|     const studentRepository = getStudentRepository(); | ||||
| 
 | ||||
|     const user = await studentRepository.findByUsername(userData.username); | ||||
| 
 | ||||
|     if (user) { | ||||
|         throw new ConflictException("Student with that sername already exists"); | ||||
|     } | ||||
| 
 | ||||
|     const newStudent = studentRepository.create(mapToStudent(userData)); | ||||
|     await studentRepository.save(newStudent); | ||||
|     const newStudent = mapToStudent(userData); | ||||
|     await studentRepository.save(newStudent, { preventOverwrite: true }); | ||||
| } | ||||
| 
 | ||||
| export async function deleteStudent(username: string): Promise<void> { | ||||
|  | @ -100,82 +86,15 @@ export async function getStudentGroups(username: string, full: boolean): Promise | |||
|     return groups.map(mapToGroupDTOId); | ||||
| } | ||||
| 
 | ||||
| export async function getStudentSubmissions(username: string): Promise<SubmissionDTO[]> { | ||||
| export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> { | ||||
|     const student = await fetchStudent(username); | ||||
| 
 | ||||
|     const submissionRepository = getSubmissionRepository(); | ||||
|     const submissions = await submissionRepository.findAllSubmissionsForStudent(student); | ||||
| 
 | ||||
|     if (full) { | ||||
|         return submissions.map(mapToSubmissionDTO); | ||||
|     } | ||||
| 
 | ||||
| export async function getStudentQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> { | ||||
|     const student = await fetchStudent(username); | ||||
| 
 | ||||
|     const questionRepository = getQuestionRepository(); | ||||
|     const questions = await questionRepository.findAllByAuthor(student); | ||||
| 
 | ||||
|     const questionsDTO = questions.map(mapToQuestionDTO); | ||||
| 
 | ||||
|     if (full) | ||||
|         return questionsDTO; | ||||
| 
 | ||||
|     return questionsDTO.map(mapToQuestionId); | ||||
|     return submissions.map(mapToSubmissionDTOId); | ||||
| } | ||||
| 
 | ||||
| export async function createClassJoinRequest(studentUsername: string, classId: string) { | ||||
|     const classRepo = getClassRepository(); | ||||
|     const requestRepo = getClassJoinRequestRepository(); | ||||
| 
 | ||||
|     const student = await fetchStudent(studentUsername); | ||||
|     const cls = await classRepo.findById(classId); | ||||
| 
 | ||||
|     if (!cls){ | ||||
|         throw new NotFoundException("Class with id not found"); | ||||
|     } | ||||
| 
 | ||||
|     const req = await requestRepo.findByStudentAndClass(student, cls); | ||||
| 
 | ||||
|     if (req){ | ||||
|         throw new ConflictException("Request with student and class already exist"); | ||||
|     } | ||||
| 
 | ||||
|     const request = requestRepo.create({ | ||||
|         requester: student, | ||||
|         class: cls, | ||||
|         status: ClassJoinRequestStatus.Open, | ||||
|     }); | ||||
| 
 | ||||
|     await requestRepo.save(request); | ||||
| } | ||||
| 
 | ||||
| export async function getJoinRequestsByStudent(studentUsername: string) { | ||||
|     const requestRepo = getClassJoinRequestRepository(); | ||||
| 
 | ||||
|     const student = await fetchStudent(studentUsername); | ||||
| 
 | ||||
|     const requests = await requestRepo.findAllRequestsBy(student); | ||||
|     return requests.map(mapToStudentRequestDTO); | ||||
| } | ||||
| 
 | ||||
| export async function deleteClassJoinRequest(studentUsername: string, classId: string) { | ||||
|     const requestRepo = getClassJoinRequestRepository(); | ||||
|     const classRepo = getClassRepository(); | ||||
| 
 | ||||
|     const student = await fetchStudent(studentUsername); | ||||
|     const cls = await classRepo.findById(classId); | ||||
| 
 | ||||
|     if (!cls) { | ||||
|         throw new NotFoundException('Class not found'); | ||||
|     } | ||||
| 
 | ||||
|     const request = await requestRepo.findByStudentAndClass(student, cls); | ||||
| 
 | ||||
|     if (!request) { | ||||
|         throw new NotFoundException('Join request not found'); | ||||
|     } | ||||
| 
 | ||||
|     await requestRepo.deleteBy(student, cls); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -32,7 +32,7 @@ export async function createSubmission(submissionDTO: SubmissionDTO) { | |||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     return submission; | ||||
|     return mapToSubmissionDTO(submission); | ||||
| } | ||||
| 
 | ||||
| export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) { | ||||
|  |  | |||
|  | @ -54,14 +54,8 @@ export async function getTeacher(username: string): Promise<TeacherDTO> { | |||
| export async function createTeacher(userData: TeacherDTO): Promise<void> { | ||||
|     const teacherRepository: TeacherRepository = getTeacherRepository(); | ||||
| 
 | ||||
|     const user: Teacher | null = await teacherRepository.findByUsername(userData.username); | ||||
| 
 | ||||
|     if (user){ | ||||
|         throw new ConflictException("Teacher with that username already exists"); | ||||
|     } | ||||
| 
 | ||||
|     const newTeacher: Teacher = teacherRepository.create(mapToTeacher(userData)); | ||||
|     await teacherRepository.save(newTeacher); | ||||
|     const newTeacher = mapToTeacher(userData); | ||||
|     await teacherRepository.save(newTeacher, { preventOverwrite: true }); | ||||
| } | ||||
| 
 | ||||
| export async function deleteTeacher(username: string): Promise<void> { | ||||
|  |  | |||
|  | @ -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); | ||||
|     } | ||||
| } | ||||
|  | @ -4,20 +4,24 @@ const IDP_PREFIX = PREFIX + 'AUTH_'; | |||
| const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_'; | ||||
| const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_'; | ||||
| const CORS_PREFIX = PREFIX + 'CORS_'; | ||||
| const LOGGING_PREFIX = PREFIX + 'LOGGING_'; | ||||
| 
 | ||||
| type EnvVar = { key: string; required?: boolean; defaultValue?: any }; | ||||
| 
 | ||||
| export const EnvVars: { [key: string]: EnvVar } = { | ||||
|     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 }, | ||||
|     DbPort: { key: DB_PREFIX + 'PORT', defaultValue: 5432 }, | ||||
|     DbName: { key: DB_PREFIX + 'NAME', defaultValue: 'dwengo' }, | ||||
|     DbUsername: { key: DB_PREFIX + 'USERNAME', required: true }, | ||||
|     DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true }, | ||||
|     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_' }, | ||||
| 
 | ||||
|     IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true }, | ||||
|     IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', 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 }, | ||||
|     IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, | ||||
|     IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' }, | ||||
| 
 | ||||
|     CorsAllowedOrigins: { key: CORS_PREFIX + 'ALLOWED_ORIGINS', defaultValue: '' }, | ||||
|     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; | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -8,12 +8,12 @@ const logger: Logger = getLogger(); | |||
| 
 | ||||
| export function loadTranslations<T>(language: string): T { | ||||
|     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'); | ||||
|         return yaml.load(yamlFile) as T; | ||||
|     } catch (error) { | ||||
|         logger.warn(`Cannot load translation for ${language}, fallen back to dutch`, error); | ||||
|         const fallbackPath = path.join(process.cwd(), '_i18n', `${FALLBACK_LANG}.yml`); | ||||
|         const fallbackPath = path.join(process.cwd(), 'i18n', `${FALLBACK_LANG}.yml`); | ||||
|         return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| import { setupTestApp } from '../../setup-tests.js'; | ||||
| import { Student } from '../../../src/entities/users/student.entity.js'; | ||||
| import { describe, it, expect, beforeAll } from 'vitest'; | ||||
| import { StudentRepository } from '../../../src/data/users/student-repository.js'; | ||||
| import { getStudentRepository } from '../../../src/data/repositories.js'; | ||||
|  | @ -30,7 +29,7 @@ describe('StudentRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should return the queried student after he was added', async () => { | ||||
|         await studentRepository.insert(new Student(username, firstName, lastName)); | ||||
|         await studentRepository.insert(studentRepository.create({ username, firstName, lastName })); | ||||
| 
 | ||||
|         const retrievedStudent = await studentRepository.findByUsername(username); | ||||
|         expect(retrievedStudent).toBeTruthy(); | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ import { describe, it, expect, beforeAll } from 'vitest'; | |||
| import { TeacherRepository } from '../../../src/data/users/teacher-repository'; | ||||
| import { setupTestApp } from '../../setup-tests'; | ||||
| import { getTeacherRepository } from '../../../src/data/repositories'; | ||||
| import { Teacher } from '../../../src/entities/users/teacher.entity'; | ||||
| 
 | ||||
| const username = 'testteacher'; | ||||
| const firstName = 'John'; | ||||
|  | @ -30,7 +29,7 @@ describe('TeacherRepository', () => { | |||
|     }); | ||||
| 
 | ||||
|     it('should return the queried teacher after he was added', async () => { | ||||
|         await teacherRepository.insert(new Teacher(username, firstName, lastName)); | ||||
|         await teacherRepository.insert(teacherRepository.create({ username, firstName, lastName })); | ||||
| 
 | ||||
|         const retrievedTeacher = await teacherRepository.findByUsername(username); | ||||
|         expect(retrievedTeacher).toBeTruthy(); | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| # | ||||
| # This file is used to define the production environment for the project. | ||||
| # It is used to deploy the project on a server. | ||||
| # Should not be used for local development. | ||||
| # Use this configuration to deploy the project on a server. | ||||
| # | ||||
| # 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: | ||||
|     web: | ||||
|  | @ -35,12 +36,16 @@ services: | |||
|             - 'traefik.http.services.api.loadbalancer.server.port=3000' | ||||
| 
 | ||||
|     db: | ||||
|         # Also see compose.yml | ||||
|         extends: | ||||
|             file: ./compose.yml | ||||
|             service: db | ||||
|         networks: | ||||
|             - dwengo-1 | ||||
| 
 | ||||
|     idp: | ||||
|         # Also see compose.yml | ||||
|         extends: | ||||
|             file: ./compose.yml | ||||
|             service: idp | ||||
|         # TODO Replace with proper production command | ||||
|         command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm'] | ||||
|         networks: | ||||
|  | @ -92,7 +97,15 @@ services: | |||
|             - dwengo-1 | ||||
| 
 | ||||
|     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: | ||||
|             - dwengo-1 | ||||
| 
 | ||||
|  | @ -107,6 +120,7 @@ services: | |||
| volumes: | ||||
|     dwengo_grafana_data: | ||||
|     dwengo_letsencrypt: | ||||
|     dwengo_loki_data: | ||||
| 
 | ||||
| networks: | ||||
|     dwengo-1: | ||||
|  | @ -32,8 +32,15 @@ services: | |||
|             - 'traefik.http.routers.api.rule=PathPrefix(`/api`)' | ||||
|             - 'traefik.http.services.api.loadbalancer.server.port=3000' | ||||
| 
 | ||||
|     db: | ||||
|         extends: | ||||
|             file: ./compose.yml | ||||
|             service: db | ||||
| 
 | ||||
|     idp: | ||||
|         # Also see compose.yml | ||||
|         extends: | ||||
|             file: ./compose.yml | ||||
|             service: idp | ||||
|         labels: | ||||
|             - 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)' | ||||
|             - 'traefik.http.services.idp.loadbalancer.server.port=7080' | ||||
|  | @ -60,6 +67,17 @@ services: | |||
|         volumes: | ||||
|             - /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: | ||||
|         image: grafana/grafana:latest | ||||
|         ports: | ||||
|  | @ -70,3 +88,5 @@ services: | |||
| 
 | ||||
| volumes: | ||||
|     dwengo_grafana_data: | ||||
|     dwengo_loki_data: | ||||
|     dwengo_postgres_data: | ||||
							
								
								
									
										12
									
								
								compose.yml
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								compose.yml
									
										
									
									
									
								
							|  | @ -36,17 +36,5 @@ services: | |||
|             KC_HEALTH_ENABLED: 'true' | ||||
|             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: | ||||
|     dwengo_loki_data: | ||||
|     dwengo_postgres_data: | ||||
|  |  | |||
|  | @ -19,7 +19,16 @@ See [Vite Configuration Reference](https://vite.dev/config/). | |||
| ## Project Setup | ||||
| 
 | ||||
| ```sh | ||||
| # Install dependencies | ||||
| 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 | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import { useThemeQuery } from "@/queries/themes.ts"; | |||
| 
 | ||||
|     const props = defineProps({ | ||||
|         selectedTheme: { type: String, required: true }, | ||||
|     selectedAge: { type: String, required: true } | ||||
|         selectedAge: { type: String, required: true }, | ||||
|     }); | ||||
| 
 | ||||
|     const { locale } = useI18n(); | ||||
|  | @ -23,9 +23,10 @@ watchEffect(() => { | |||
|         allCards.value = themes; | ||||
| 
 | ||||
|         if (props.selectedTheme) { | ||||
|         cards.value = themes.filter((theme) => | ||||
|             cards.value = themes.filter( | ||||
|                 (theme) => | ||||
|                     THEMESITEMS[props.selectedTheme]?.includes(theme.key) && | ||||
|             AGE_TO_THEMES[props.selectedAge]?.includes(theme.key) | ||||
|                     AGE_TO_THEMES[props.selectedAge]?.includes(theme.key), | ||||
|             ); | ||||
|         } else { | ||||
|             cards.value = themes; | ||||
|  | @ -33,15 +34,23 @@ watchEffect(() => { | |||
|     }); | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <template> | ||||
|     <v-container> | ||||
|         <div v-if="isLoading" class="text-center py-10"> | ||||
|             <v-progress-circular indeterminate color="primary" /> | ||||
|         <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"> | ||||
|         <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> | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
|         { name: "English", code: "en" }, | ||||
|         { name: "Nederlands", code: "nl" }, | ||||
|         { name: "Français", code: "fr" }, | ||||
|         { name: "Deutsch", code: "de" } | ||||
|         { name: "Deutsch", code: "de" }, | ||||
|     ]); | ||||
| 
 | ||||
|     // Logic to change the language of the website to the selected language | ||||
|  |  | |||
|  | @ -31,7 +31,10 @@ defineProps<{ | |||
|         </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"> | ||||
|             <v-btn | ||||
|                 :to="`theme/${path}`" | ||||
|                 variant="text" | ||||
|             > | ||||
|                 {{ t("read-more") }} | ||||
|             </v-btn> | ||||
|         </v-card-actions> | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import { StudentController } from "@/controllers/students.ts"; | ||||
| import { TeacherController } from "@/controllers/teachers.ts"; | ||||
| 
 | ||||
| import {ThemeController} from "@/controllers/themes.ts"; | ||||
| 
 | ||||
| export function controllerGetter<T>(Factory: new () => T): () => T { | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ import i18n from "./i18n/i18n.ts"; | |||
| // Components
 | ||||
| import App from "./App.vue"; | ||||
| import router from "./router"; | ||||
| import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query'; | ||||
| import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query"; | ||||
| 
 | ||||
| const app = createApp(App); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,25 +1,22 @@ | |||
| import { useQuery } from '@tanstack/vue-query'; | ||||
| import { getThemeController } from '@/controllers/controllers'; | ||||
| 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], | ||||
| export const useThemeQuery = (language: MaybeRefOrGetter<string>) => | ||||
|     useQuery({ | ||||
|         queryKey: ["themes", language], | ||||
|         queryFn: () => { | ||||
|             const lang = toValue(language); | ||||
|             return themeController.getAll(lang); | ||||
|         }, | ||||
|         enabled: () => !!toValue(language), | ||||
|         enabled: () => Boolean(toValue(language)), | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| export const useThemeHruidsQuery = (themeKey: string | null) => { | ||||
|     return useQuery({ | ||||
|         queryKey: ['theme-hruids', themeKey], | ||||
| export const useThemeHruidsQuery = (themeKey: string | null) => | ||||
|     useQuery({ | ||||
|         queryKey: ["theme-hruids", themeKey], | ||||
|         queryFn: () => themeController.getHruidsByKey(themeKey!), | ||||
|         enabled: !!themeKey, | ||||
|         enabled: Boolean(themeKey), | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,37 +1,64 @@ | |||
| export const THEMES_KEYS = [ | ||||
|     "kiks", "art", "socialrobot", "agriculture", "wegostem", | ||||
|     "computational_thinking", "math_with_python", "python_programming", | ||||
|     "stem", "care", "chatbot", "physical_computing", "algorithms", "basics_ai" | ||||
|     "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"], | ||||
|     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"], | ||||
|     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 AGEITEMS = ["all", "primary-school", "lower-secondary", "upper-secondary", "high-school", "older"]; | ||||
| 
 | ||||
| export const AGE_TO_THEMES: Record<string, string[]> = { | ||||
|     "all": THEMES_KEYS, | ||||
|     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" | ||||
|     "upper-secondary": [ | ||||
|         "kiks", | ||||
|         "art", | ||||
|         "socialrobot", | ||||
|         "agriculture", | ||||
|         "computational_thinking", | ||||
|         "math_with_python", | ||||
|         "python_programming", | ||||
|         "stem", | ||||
|         "care", | ||||
|         "chatbot", | ||||
|         "algorithms", | ||||
|         "basics_ai", | ||||
|     ], | ||||
|     "older": [ | ||||
|         "kiks", "computational_thinking", "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"], | ||||
| }; | ||||
|  |  | |||
|  | @ -1,11 +1,7 @@ | |||
| <script setup lang="ts"> | ||||
| 
 | ||||
| </script> | ||||
| <script setup lang="ts"></script> | ||||
| 
 | ||||
| <template> | ||||
|     <main></main> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
| 
 | ||||
| </style> | ||||
| <style scoped></style> | ||||
|  |  | |||
|  | @ -6,8 +6,8 @@ import {ref, watch} from "vue"; | |||
| 
 | ||||
|     const { t, locale } = useI18n(); | ||||
| 
 | ||||
|     const selectedThemeKey = ref<string>('all'); | ||||
|     const selectedAgeKey = ref<string>('all'); | ||||
|     const selectedThemeKey = ref<string>("all"); | ||||
|     const selectedAgeKey = ref<string>("all"); | ||||
| 
 | ||||
|     const allThemes = ref(Object.keys(THEMESITEMS)); | ||||
|     const availableThemes = ref([...allThemes.value]); | ||||
|  | @ -17,18 +17,17 @@ import {ref, watch} from "vue"; | |||
| 
 | ||||
|     // Reset selection when language changes | ||||
|     watch(locale, () => { | ||||
|         selectedThemeKey.value = 'all'; | ||||
|         selectedAgeKey.value = 'all'; | ||||
|         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)) | ||||
|             availableAges.value = allAges.value.filter((age) => | ||||
|                 AGE_TO_THEMES[age]?.some((theme) => themes.includes(theme)), | ||||
|             ); | ||||
|         } | ||||
|     }); | ||||
|  | @ -38,32 +37,31 @@ import {ref, watch} from "vue"; | |||
|             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)) | ||||
|             availableThemes.value = allThemes.value.filter((theme) => | ||||
|                 THEMESITEMS[theme]?.some((theme) => themes.includes(theme)), | ||||
|             ); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div class="main-container"> | ||||
|         <h1 class="title">{{ t("themes") }}</h1> | ||||
|         <v-container class="dropdowns"> | ||||
|             <v-select class="v-select" | ||||
|             <v-select | ||||
|                 class="v-select" | ||||
|                 :label="t('choose-theme')" | ||||
|                       :items="availableThemes.map(theme => ({ title: t(`theme-options.${theme}`), value: 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 }))" | ||||
|                 :items="availableAges.map((age) => ({ key: age, label: t(`age-options.${age}`), value: age }))" | ||||
|                 v-model="selectedAgeKey" | ||||
|                 item-title="label" | ||||
|                 item-value="key" | ||||
|  | @ -71,7 +69,10 @@ import {ref, watch} from "vue"; | |||
|             ></v-select> | ||||
|         </v-container> | ||||
| 
 | ||||
|         <BrowseThemes :selectedTheme="selectedThemeKey ?? ''" :selectedAge="selectedAgeKey ?? ''"/> | ||||
|         <BrowseThemes | ||||
|             :selectedTheme="selectedThemeKey ?? ''" | ||||
|             :selectedAge="selectedAgeKey ?? ''" | ||||
|         /> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -94,7 +95,6 @@ import {ref, watch} from "vue"; | |||
|         justify-content: center; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     .dropdowns { | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|  | @ -107,7 +107,6 @@ import {ref, watch} from "vue"; | |||
|         min-width: 100px; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @media (max-width: 768px) { | ||||
|         .main-container { | ||||
|             padding: 1rem; | ||||
|  | @ -121,5 +120,4 @@ import {ref, watch} from "vue"; | |||
|             width: 80%; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| </style> | ||||
|  |  | |||
							
								
								
									
										1747
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1747
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Reference in a new issue
	
	 Gabriellvl
						Gabriellvl