Merge remote-tracking branch 'origin/dev' into feat/pagina-om-leerpaden-te-bekijken-#41
# Conflicts: # frontend/src/App.vue # frontend/src/components/MenuBar.vue # frontend/src/main.ts
This commit is contained in:
commit
8522cde18d
68 changed files with 868 additions and 896 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
|
### Quick start
|
||||||
|
|
||||||
|
Om de applicatie lokaal te draaien als kant-en-klare Docker-containers:
|
||||||
|
|
||||||
1. Installeer Docker en Docker Compose op je systeem (zie [Docker](https://docs.docker.com/get-docker/)
|
1. Installeer Docker en Docker Compose op je systeem (zie [Docker](https://docs.docker.com/get-docker/)
|
||||||
en [Docker Compose](https://docs.docker.com/compose/)).
|
en [Docker Compose](https://docs.docker.com/compose/)).
|
||||||
2. Clone deze repository.
|
2. Clone deze repository.
|
||||||
3. In de backend, kopieer `.env.example` (of `.env.development.example`) naar `.env` en pas de variabelen aan waar
|
3. In de backend, kopieer `.env.example` naar `.env` en pas de variabelen aan waar nodig.
|
||||||
nodig.
|
4. Voer `docker compose -f compose.staging.yml up --build` uit in de root van de repository.
|
||||||
4. Voer `docker compose up` uit in de root van de repository.
|
|
||||||
5. Optioneel: Configureer de applicatie aan de hand van
|
5. Optioneel: Configureer de applicatie aan de hand van
|
||||||
de [configuratiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving#dwengo-1-configuratie).
|
de [configuratiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving#dwengo-1-configuratie).
|
||||||
|
6. De applicatie is nu beschikbaar op [`http://localhost/`](http://localhost/) en [`http://localhost/api`](http://localhost/api).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose version
|
docker compose version
|
||||||
|
@ -38,14 +40,13 @@ cp .env.example .env
|
||||||
# Pas .env aan
|
# Pas .env aan
|
||||||
nano .env
|
nano .env
|
||||||
cd ..
|
cd ..
|
||||||
docker compose up
|
docker compose -f compose.staging.yml up --build
|
||||||
# Configureer de applicatie
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Handmatige installatie
|
### Handmatige installatie en ontwikkeling
|
||||||
|
|
||||||
Zie de submappen voor de installatie-instructies van de [frontend](./frontend/README.md)
|
Zie de submappen voor de installatie-instructies van de [frontend](./frontend/README.md)
|
||||||
en [backend](./backend/README.md).
|
en [backend](./backend/README.md) en instructies voor het opzetten van een ontwikkelomgeving.
|
||||||
|
|
||||||
## Architectuur
|
## Architectuur
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
#
|
#
|
||||||
# Basic configuration
|
# Development environment configuration
|
||||||
|
#
|
||||||
|
# You probably don't need to change these values, as this configuration takes
|
||||||
|
# the docker services and their default ports into account.
|
||||||
#
|
#
|
||||||
|
|
||||||
DWENGO_PORT=3000 # The port the backend will listen on
|
### Dwengo ###
|
||||||
|
|
||||||
|
#DWENGO_PORT=3000
|
||||||
|
#DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api
|
||||||
|
#DWENGO_FALLBACK_LANGUAGE=nl
|
||||||
|
#DWENGO_RUN_MODE=dev
|
||||||
|
|
||||||
DWENGO_DB_HOST=localhost
|
DWENGO_DB_HOST=localhost
|
||||||
DWENGO_DB_PORT=5431
|
DWENGO_DB_PORT=5431
|
||||||
|
#DWENGO_DB_NAME=dwengo
|
||||||
DWENGO_DB_USERNAME=postgres
|
DWENGO_DB_USERNAME=postgres
|
||||||
DWENGO_DB_PASSWORD=postgres
|
DWENGO_DB_PASSWORD=postgres
|
||||||
DWENGO_DB_UPDATE=true
|
DWENGO_DB_UPDATE=true
|
||||||
|
#DWENGO_DB_CONTENT_PREFIX=u_
|
||||||
# Auth
|
|
||||||
|
|
||||||
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
|
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
|
||||||
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||||
|
@ -17,12 +26,12 @@ DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/
|
||||||
DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
|
DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
|
||||||
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
||||||
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
|
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
|
||||||
|
#DWENGO_AUTH_AUDIENCE=account
|
||||||
|
|
||||||
# Allow Vite dev-server to access the backend (for testing purposes). Don't forget to remove this in production!
|
|
||||||
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173
|
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173
|
||||||
|
#DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type
|
||||||
|
|
||||||
#
|
### Advanced configuration ###
|
||||||
# Advanced configuration
|
|
||||||
#
|
|
||||||
|
|
||||||
# LOKI_HOST=http://localhost:9001 # The address of the Loki instance, used for logging
|
DWENGO_LOGGING_LEVEL=debug
|
||||||
|
#DWENGO_LOGGING_LOKI_HOST=http://localhost:3102
|
||||||
|
|
|
@ -1,27 +1,68 @@
|
||||||
#
|
#
|
||||||
# Basic configuration
|
# Basic configuration
|
||||||
#
|
#
|
||||||
|
# Change the values of the variables below to match your environment!
|
||||||
|
# Default values are commented out.
|
||||||
|
#
|
||||||
|
|
||||||
DWENGO_PORT=3000 # The port the backend will listen on
|
### Dwengo ###
|
||||||
|
|
||||||
|
# Port the backend will listen on
|
||||||
|
#DWENGO_PORT=3000
|
||||||
|
# The hostname or IP address of the remote learning content API.
|
||||||
|
#DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api
|
||||||
|
# The default fallback language.
|
||||||
|
#DWENGO_FALLBACK_LANGUAGE=nl
|
||||||
|
# Whether running in production mode or not. Possible values are "prod" or "dev".
|
||||||
|
#DWENGO_RUN_MODE=dev
|
||||||
|
|
||||||
|
# ! Change this! The hostname or IP address of the database
|
||||||
|
# If running your stack in docker, this should use the docker service name.
|
||||||
DWENGO_DB_HOST=domain-or-ip-of-database
|
DWENGO_DB_HOST=domain-or-ip-of-database
|
||||||
DWENGO_DB_PORT=5431
|
# The port of the database.
|
||||||
|
#DWENGO_DB_PORT=5432
|
||||||
# Change this to the actual credentials of the user Dwengo should use in the backend
|
# The name of the database.
|
||||||
DWENGO_DB_USERNAME=postgres
|
#DWENGO_DB_NAME=dwengo
|
||||||
DWENGO_DB_PASSWORD=postgres
|
# ! Change this! The username of the database user.
|
||||||
|
DWENGO_DB_USERNAME=username
|
||||||
|
# ! Change this! The password of the database user.
|
||||||
|
DWENGO_DB_PASSWORD=password
|
||||||
|
# Whether the database scheme needs to be updated.
|
||||||
# Set this to true when the database scheme needs to be updated. In that case, take a backup first.
|
# Set this to true when the database scheme needs to be updated. In that case, take a backup first.
|
||||||
DWENGO_DB_UPDATE=false
|
#DWENGO_DB_UPDATE=false
|
||||||
|
# The prefix used for custom user content.
|
||||||
|
#DWENGO_DB_CONTENT_PREFIX=u_
|
||||||
|
|
||||||
# Data for the identity provider via which the students authenticate.
|
# ! Change this! The external URL for student authentication. Should be reachable by the client.
|
||||||
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
|
# E.g. https://sel2-1.ugent.be/idp/realms/student
|
||||||
|
DWENGO_AUTH_STUDENT_URL=http://hostname/idp/realms/student
|
||||||
|
# ! Change this! The client ID for student authentication.
|
||||||
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||||
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs
|
# ! Change this! The internal URL for retrieving the JWKS for student authentication.
|
||||||
|
# Should be reachable by the backend. If running your stack in docker, this should use the docker service name.
|
||||||
# Data for the identity provider via which the teachers authenticate.
|
# E.g. http://idp:7080/realms/student/protocol/openid-connect/certs
|
||||||
DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
|
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://hostname/realms/student/protocol/openid-connect/certs
|
||||||
|
# ! Change this! The external URL for teacher authentication. Should be reachable by the client.
|
||||||
|
# E.g. https://sel2-1.ugent.be/idp/realms/teacher
|
||||||
|
DWENGO_AUTH_TEACHER_URL=http://hostname/idp/realms/teacher
|
||||||
|
# ! Change this! The client ID for teacher authentication.
|
||||||
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
||||||
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
|
# ! Change this! The internal URL for retrieving the JWKS for teacher authentication.
|
||||||
|
# Should be reachable by the backend. If running your stack in docker, this should use the docker service name.
|
||||||
|
# E.g. http://idp:7080/realms/teacher/protocol/openid-connect/certs
|
||||||
|
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://hostname/realms/teacher/protocol/openid-connect/certs
|
||||||
|
# The IDP audience
|
||||||
|
#DWENGO_AUTH_AUDIENCE=account
|
||||||
|
|
||||||
# The address of the Lokiinstance, used for logging
|
# Allowed origins for CORS requests. Separate multiple origins with a comma.
|
||||||
# LOKI_HOST=http://localhost:3102
|
#DWENGO_CORS_ALLOWED_ORIGINS=
|
||||||
|
# Allowed headers for CORS requests. Separate multiple headers with a comma.
|
||||||
|
#DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type
|
||||||
|
|
||||||
|
### Advanced configuration ###
|
||||||
|
|
||||||
|
# The logging level. Possible values are "debug", "info", "warn", "error".
|
||||||
|
#DWENGO_LOGGING_LEVEL=info
|
||||||
|
# The address of the Loki instance, a log aggregation system.
|
||||||
|
# If running your stack in docker, this should use the docker service name.
|
||||||
|
#DWENGO_LOGGING_LOKI_HOST=http://localhost:3102
|
||||||
|
|
|
@ -1,28 +1,37 @@
|
||||||
DWENGO_PORT=3000 # The port the backend will listen on
|
#
|
||||||
DWENGO_DB_HOST=db # Name of the database container
|
# Production environment configuration
|
||||||
DWENGO_DB_PORT=5431
|
#
|
||||||
|
# Change the values of the variables below to match your production environment!
|
||||||
|
# See .env.example for more information.
|
||||||
|
#
|
||||||
|
|
||||||
# Change this to the actual credentials of the user Dwengo should use in the backend
|
### Dwengo ###
|
||||||
|
|
||||||
|
DWENGO_PORT=3000
|
||||||
|
#DWENGO_LEARNING_CONTENT_REPO_API_BASE_URL=https://dwengo.org/backend/api
|
||||||
|
#DWENGO_FALLBACK_LANGUAGE=nl
|
||||||
|
DWENGO_RUN_MODE=prod
|
||||||
|
|
||||||
|
DWENGO_DB_HOST=db
|
||||||
|
DWENGO_DB_PORT=5432
|
||||||
DWENGO_DB_NAME=postgres
|
DWENGO_DB_NAME=postgres
|
||||||
DWENGO_DB_USERNAME=postgres
|
DWENGO_DB_USERNAME=postgres
|
||||||
DWENGO_DB_PASSWORD=postgres
|
DWENGO_DB_PASSWORD=postgres
|
||||||
|
|
||||||
# Set this to true when the database scheme needs to be updated. In that case, take a backup first.
|
|
||||||
DWENGO_DB_UPDATE=false
|
DWENGO_DB_UPDATE=false
|
||||||
|
#DWENGO_DB_CONTENT_PREFIX=u_
|
||||||
|
|
||||||
# Data for the identity provider via which the students authenticate.
|
|
||||||
DWENGO_AUTH_STUDENT_URL=https://sel2-1.ugent.be/idp/realms/student
|
DWENGO_AUTH_STUDENT_URL=https://sel2-1.ugent.be/idp/realms/student
|
||||||
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||||
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs # Name of the idp container
|
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs # Name of the idp container
|
||||||
# Data for the identity provider via which the teachers authenticate.
|
|
||||||
DWENGO_AUTH_TEACHER_URL=https://sel2-1.ugent.be/idp/realms/teacher
|
DWENGO_AUTH_TEACHER_URL=https://sel2-1.ugent.be/idp/realms/teacher
|
||||||
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
||||||
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs # Name of the idp container
|
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs # Name of the idp container
|
||||||
|
#DWENGO_AUTH_AUDIENCE=account
|
||||||
|
|
||||||
#
|
#DWENGO_CORS_ALLOWED_ORIGINS=
|
||||||
# Advanced configuration
|
#DWENGO_CORS_ALLOWED_HEADERS=Authorization,Content-Type
|
||||||
#
|
|
||||||
|
|
||||||
# Logging and monitoring
|
### Advanced configuration ###
|
||||||
|
|
||||||
# LOKI_HOST=http://logging:3102 # The address of the Loki instance, used for logging
|
DWENGO_LOGGING_LEVEL=info
|
||||||
|
DWENGO_LOGGING_LOKI_HOST=http://logging:3102
|
||||||
|
|
|
@ -1,3 +1,13 @@
|
||||||
PORT=3000
|
#
|
||||||
DWENGO_DB_UPDATE=true
|
# Test environment configuration
|
||||||
|
#
|
||||||
|
# Should not need to be modified.
|
||||||
|
# See .env.example for more information.
|
||||||
|
#
|
||||||
|
|
||||||
|
### Dwengo ###
|
||||||
|
|
||||||
|
DWENGO_PORT=3000
|
||||||
|
|
||||||
DWENGO_DB_NAME=":memory:"
|
DWENGO_DB_NAME=":memory:"
|
||||||
|
DWENGO_DB_UPDATE=true
|
||||||
|
|
|
@ -30,6 +30,7 @@ COPY package-lock.json backend/package.json ./
|
||||||
RUN npm install --silent --only=production
|
RUN npm install --silent --only=production
|
||||||
|
|
||||||
COPY ./docs /docs
|
COPY ./docs /docs
|
||||||
|
COPY ./backend/i18n /app/i18n
|
||||||
COPY --from=build-stage /app/backend/dist ./dist/
|
COPY --from=build-stage /app/backend/dist ./dist/
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
|
@ -4,23 +4,24 @@
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
|
# Start de nodige services voor ontwikkeling
|
||||||
|
cd ../ # Ga naar de root van de repository
|
||||||
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Setup the environment variables in a `.env` file in the root of the project. You can use the `.env.example` file as a template.
|
Zet de omgevingsvariabelen in een `.env` bestand in de root van het project.
|
||||||
|
Je kan het `.env.example` bestand als template gebruiken.
|
||||||
|
|
||||||
### Development
|
### Ontwikkeling
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
# Omgevingsvariabelen
|
||||||
|
cp .env.development.example .env.development.local
|
||||||
|
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Production
|
|
||||||
|
|
||||||
```shell
|
|
||||||
npm run build
|
|
||||||
npm run start
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
Voer volgend commando uit om de unit tests uit te voeren:
|
Voer volgend commando uit om de unit tests uit te voeren:
|
||||||
|
@ -29,6 +30,18 @@ Voer volgend commando uit om de unit tests uit te voeren:
|
||||||
npm run test:unit
|
npm run test:unit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Productie
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Omgevingsvariabelen
|
||||||
|
cp .env.development.example .env
|
||||||
|
|
||||||
|
npm run build
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
Zie ook de [installatiehandleiding](https://github.com/SELab-2/Dwengo-1/wiki/Administrator:-Productie-omgeving).
|
||||||
|
|
||||||
## Keycloak configuratie
|
## Keycloak configuratie
|
||||||
|
|
||||||
Tijdens development is het voldoende om gebruik te maken van de keycloak configuratie die automatisch ingeladen wordt.
|
Tijdens development is het voldoende om gebruik te maken van de keycloak configuratie die automatisch ingeladen wordt.
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import { EnvVars, getEnvVar } from './util/envvars.js';
|
import { EnvVars, getEnvVar } from './util/envvars.js';
|
||||||
import { Language } from './entities/content/language.js';
|
|
||||||
|
|
||||||
// API
|
// API
|
||||||
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl);
|
export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl);
|
||||||
export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage);
|
export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage);
|
||||||
|
|
||||||
// Logging
|
|
||||||
export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info';
|
|
||||||
export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102';
|
|
||||||
|
|
||||||
export const FALLBACK_SEQ_NUM = 1;
|
export const FALLBACK_SEQ_NUM = 1;
|
||||||
|
|
|
@ -37,7 +37,7 @@ export async function createAssignmentHandler(req: Request<AssignmentParams>, re
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ assignment: assignment });
|
res.status(201).json(assignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
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> {
|
export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
||||||
const classid = req.params.classid;
|
const classid = req.params.classid;
|
||||||
const assignmentNumber = +req.params.id;
|
const assignmentNumber = +req.params.id;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
if (isNaN(assignmentNumber)) {
|
if (isNaN(assignmentNumber)) {
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
res.status(400).json({ error: 'Assignment id must be a number' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber);
|
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
submissions: submissions,
|
submissions: submissions,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Request, Response } from 'express';
|
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';
|
import { ClassDTO } from '../interfaces/class.js';
|
||||||
|
|
||||||
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
|
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
@ -28,30 +28,19 @@ export async function createClassHandler(req: Request, res: Response): Promise<v
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ class: cls });
|
res.status(201).json(cls);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getClassHandler(req: Request, res: Response): Promise<void> {
|
export async function getClassHandler(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
const classId = req.params.id;
|
||||||
const classId = req.params.id;
|
const cls = await getClass(classId);
|
||||||
const cls = await getClass(classId);
|
|
||||||
|
|
||||||
if (!cls) {
|
if (!cls) {
|
||||||
res.status(404).json({ error: 'Class not found' });
|
res.status(404).json({ error: 'Class not found' });
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
cls.endpoints = {
|
|
||||||
self: `${req.baseUrl}/${req.params.id}`,
|
|
||||||
invitations: `${req.baseUrl}/${req.params.id}/invitations`,
|
|
||||||
assignments: `${req.baseUrl}/${req.params.id}/assignments`,
|
|
||||||
students: `${req.baseUrl}/${req.params.id}/students`,
|
|
||||||
};
|
|
||||||
|
|
||||||
res.json(cls);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching learning objects:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.json(cls);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
|
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
@ -67,7 +56,7 @@ export async function getClassStudentsHandler(req: Request, res: Response): Prom
|
||||||
|
|
||||||
export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> {
|
export async function getTeacherInvitationsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const classId = req.params.id;
|
const classId = req.params.id;
|
||||||
const full = req.query.full === 'true'; // TODO: not implemented yet
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
const invitations = await getClassTeacherInvitations(classId, full);
|
const invitations = await getClassTeacherInvitations(classId, full);
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,11 @@ export async function getGroupHandler(req: Request<GroupParams>, res: Response):
|
||||||
|
|
||||||
const group = await getGroup(classId, assignmentId, groupId, full);
|
const group = await getGroup(classId, assignmentId, groupId, full);
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
res.status(404).json({ error: 'Group not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
res.json(group);
|
res.json(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,12 +71,12 @@ export async function createGroupHandler(req: Request, res: Response): Promise<v
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ group: group });
|
res.status(201).json(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const classId = req.params.classid;
|
const classId = req.params.classid;
|
||||||
// Const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
const assignmentId = +req.params.assignmentid;
|
const assignmentId = +req.params.assignmentid;
|
||||||
|
|
||||||
|
@ -87,7 +92,7 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const submissions = await getGroupSubmissions(classId, assignmentId, groupId);
|
const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
submissions: submissions,
|
submissions: submissions,
|
||||||
|
|
|
@ -39,7 +39,7 @@ export async function getAllLearningObjects(req: Request, res: Response): Promis
|
||||||
learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId);
|
learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(learningObjects);
|
res.json({ learningObjects: learningObjects });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLearningObject(req: Request, res: Response): Promise<void> {
|
export async function getLearningObject(req: Request, res: Response): Promise<void> {
|
||||||
|
|
|
@ -48,7 +48,7 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi
|
||||||
if (!questions) {
|
if (!questions) {
|
||||||
res.status(404).json({ error: `Questions not found.` });
|
res.status(404).json({ error: `Questions not found.` });
|
||||||
} else {
|
} else {
|
||||||
res.json(questions);
|
res.json({ questions: questions });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,12 +76,12 @@ export async function getQuestionAnswersHandler(req: Request, res: Response): Pr
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const answers = getAnswersByQuestion(questionId, full);
|
const answers = await getAnswersByQuestion(questionId, full);
|
||||||
|
|
||||||
if (!answers) {
|
if (!answers) {
|
||||||
res.status(404).json({ error: `Questions not found.` });
|
res.status(404).json({ error: `Questions not found` });
|
||||||
} else {
|
} else {
|
||||||
res.json(answers);
|
res.json({ answers: answers });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ export async function createQuestionHandler(req: Request, res: Response): Promis
|
||||||
const question = await createQuestion(questionDTO);
|
const question = await createQuestion(questionDTO);
|
||||||
|
|
||||||
if (!question) {
|
if (!question) {
|
||||||
res.status(400).json({ error: 'Could not add question' });
|
res.status(400).json({ error: 'Could not create question' });
|
||||||
} else {
|
} else {
|
||||||
res.json(question);
|
res.json(question);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,29 +9,21 @@ import {
|
||||||
getStudentGroups,
|
getStudentGroups,
|
||||||
getStudentSubmissions,
|
getStudentSubmissions,
|
||||||
} from '../services/students.js';
|
} from '../services/students.js';
|
||||||
import { ClassDTO } from '../interfaces/class.js';
|
|
||||||
import { getAllAssignments } from '../services/assignments.js';
|
|
||||||
import { getUserHandler } from './users.js';
|
|
||||||
import { Student } from '../entities/users/student.entity.js';
|
|
||||||
import { StudentDTO } from '../interfaces/student.js';
|
import { StudentDTO } from '../interfaces/student.js';
|
||||||
import { getStudentRepository } from '../data/repositories.js';
|
|
||||||
import { UserDTO } from '../interfaces/user.js';
|
|
||||||
|
|
||||||
// TODO: accept arguments (full, ...)
|
// TODO: accept arguments (full, ...)
|
||||||
// TODO: endpoints
|
// TODO: endpoints
|
||||||
export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> {
|
export async function getAllStudentsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
const studentRepository = getStudentRepository();
|
const students = await getAllStudents(full);
|
||||||
|
|
||||||
const students: StudentDTO[] | string[] = full ? await getAllStudents() : await getAllStudents();
|
|
||||||
|
|
||||||
if (!students) {
|
if (!students) {
|
||||||
res.status(404).json({ error: `Student not found.` });
|
res.status(404).json({ error: `Student not found.` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json(students);
|
res.json({ students: students });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStudentHandler(req: Request, res: Response): Promise<void> {
|
export async function getStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
@ -51,7 +43,7 @@ export async function getStudentHandler(req: Request, res: Response): Promise<vo
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json(user);
|
res.json(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createStudentHandler(req: Request, res: Response) {
|
export async function createStudentHandler(req: Request, res: Response) {
|
||||||
|
@ -65,6 +57,14 @@ export async function createStudentHandler(req: Request, res: Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = await createStudent(userData);
|
const newUser = await createStudent(userData);
|
||||||
|
|
||||||
|
if (!newUser) {
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Something went wrong while creating student',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
res.status(201).json(newUser);
|
res.status(201).json(newUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,25 +88,14 @@ export async function deleteStudentHandler(req: Request, res: Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> {
|
export async function getStudentClassesHandler(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
const full = req.query.full === 'true';
|
||||||
const full = req.query.full === 'true';
|
const username = req.params.id;
|
||||||
const username = req.params.id;
|
|
||||||
|
|
||||||
const classes = await getStudentClasses(username, full);
|
const classes = await getStudentClasses(username, full);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
classes: classes,
|
classes: classes,
|
||||||
endpoints: {
|
});
|
||||||
self: `${req.baseUrl}/${req.params.id}`,
|
|
||||||
classes: `${req.baseUrl}/${req.params.id}/invitations`,
|
|
||||||
questions: `${req.baseUrl}/${req.params.id}/assignments`,
|
|
||||||
students: `${req.baseUrl}/${req.params.id}/students`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching learning objects:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -137,8 +126,9 @@ export async function getStudentGroupsHandler(req: Request, res: Response): Prom
|
||||||
|
|
||||||
export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const username = req.params.id;
|
const username = req.params.id;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
const submissions = await getStudentSubmissions(username);
|
const submissions = await getStudentSubmissions(username, full);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
submissions: submissions,
|
submissions: submissions,
|
||||||
|
|
|
@ -36,10 +36,11 @@ export async function createSubmissionHandler(req: Request, res: Response) {
|
||||||
const submission = await createSubmission(submissionDTO);
|
const submission = await createSubmission(submissionDTO);
|
||||||
|
|
||||||
if (!submission) {
|
if (!submission) {
|
||||||
res.status(404).json({ error: 'Submission not added' });
|
res.status(400).json({ error: 'Failed to create submission' });
|
||||||
} else {
|
return;
|
||||||
res.json(submission);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.json(submission);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSubmissionHandler(req: Request, res: Response) {
|
export async function deleteSubmissionHandler(req: Request, res: Response) {
|
||||||
|
@ -53,7 +54,8 @@ export async function deleteSubmissionHandler(req: Request, res: Response) {
|
||||||
|
|
||||||
if (!submission) {
|
if (!submission) {
|
||||||
res.status(404).json({ error: 'Submission not found' });
|
res.status(404).json({ error: 'Submission not found' });
|
||||||
} else {
|
return;
|
||||||
res.json(submission);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.json(submission);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,33 +4,23 @@ import {
|
||||||
deleteTeacher,
|
deleteTeacher,
|
||||||
getAllTeachers,
|
getAllTeachers,
|
||||||
getClassesByTeacher,
|
getClassesByTeacher,
|
||||||
getClassIdsByTeacher,
|
|
||||||
getQuestionIdsByTeacher,
|
|
||||||
getQuestionsByTeacher,
|
getQuestionsByTeacher,
|
||||||
getStudentIdsByTeacher,
|
|
||||||
getStudentsByTeacher,
|
getStudentsByTeacher,
|
||||||
getTeacher,
|
getTeacher,
|
||||||
} from '../services/teachers.js';
|
} from '../services/teachers.js';
|
||||||
import { ClassDTO } from '../interfaces/class.js';
|
|
||||||
import { StudentDTO } from '../interfaces/student.js';
|
|
||||||
import { QuestionDTO, QuestionId } from '../interfaces/question.js';
|
|
||||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
|
||||||
import { TeacherDTO } from '../interfaces/teacher.js';
|
import { TeacherDTO } from '../interfaces/teacher.js';
|
||||||
import { getTeacherRepository } from '../data/repositories.js';
|
|
||||||
|
|
||||||
export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> {
|
export async function getAllTeachersHandler(req: Request, res: Response): Promise<void> {
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
const teacherRepository = getTeacherRepository();
|
const teachers = await getAllTeachers(full);
|
||||||
|
|
||||||
const teachers: TeacherDTO[] | string[] = full ? await getAllTeachers() : await getAllTeachers();
|
|
||||||
|
|
||||||
if (!teachers) {
|
if (!teachers) {
|
||||||
res.status(404).json({ error: `Teacher not found.` });
|
res.status(404).json({ error: `Teacher not found.` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json(teachers);
|
res.json({ teachers: teachers });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
|
export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
@ -45,12 +35,12 @@ export async function getTeacherHandler(req: Request, res: Response): Promise<vo
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
error: `User with username '${username}' not found.`,
|
error: `Teacher '${username}' not found.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json(user);
|
res.json(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTeacherHandler(req: Request, res: Response) {
|
export async function createTeacherHandler(req: Request, res: Response) {
|
||||||
|
@ -64,6 +54,12 @@ export async function createTeacherHandler(req: Request, res: Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = await createTeacher(userData);
|
const newUser = await createTeacher(userData);
|
||||||
|
|
||||||
|
if (!newUser) {
|
||||||
|
res.status(400).json({ error: 'Failed to create teacher' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
res.status(201).json(newUser);
|
res.status(201).json(newUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +74,7 @@ export async function deleteTeacherHandler(req: Request, res: Response) {
|
||||||
const deletedUser = await deleteTeacher(username);
|
const deletedUser = await deleteTeacher(username);
|
||||||
if (!deletedUser) {
|
if (!deletedUser) {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
error: `User with username '${username}' not found.`,
|
error: `User '${username}' not found.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -87,58 +83,58 @@ export async function deleteTeacherHandler(req: Request, res: Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
|
export async function getTeacherClassHandler(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
const username = req.params.username as string;
|
||||||
const username = req.params.username as string;
|
const full = req.query.full === 'true';
|
||||||
const full = req.query.full === 'true';
|
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const classes: ClassDTO[] | string[] = full ? await getClassesByTeacher(username) : await getClassIdsByTeacher(username);
|
|
||||||
|
|
||||||
res.status(201).json(classes);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching classes by teacher:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const classes = await getClassesByTeacher(username, full);
|
||||||
|
|
||||||
|
if (!classes) {
|
||||||
|
res.status(404).json({ error: 'Teacher not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ classes: classes });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
|
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
const username = req.params.username as string;
|
||||||
const username = req.params.username as string;
|
const full = req.query.full === 'true';
|
||||||
const full = req.query.full === 'true';
|
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const students: StudentDTO[] | string[] = full ? await getStudentsByTeacher(username) : await getStudentIdsByTeacher(username);
|
|
||||||
|
|
||||||
res.status(201).json(students);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching students by teacher:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const students = await getStudentsByTeacher(username, full);
|
||||||
|
|
||||||
|
if (!students) {
|
||||||
|
res.status(404).json({ error: 'Teacher not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ students: students });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
|
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
const username = req.params.username as string;
|
||||||
const username = req.params.username as string;
|
const full = req.query.full === 'true';
|
||||||
const full = req.query.full === 'true';
|
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
res.status(400).json({ error: 'Missing required field: username' });
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const questions: QuestionDTO[] | QuestionId[] = full ? await getQuestionsByTeacher(username) : await getQuestionIdsByTeacher(username);
|
|
||||||
|
|
||||||
res.status(201).json(questions);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching questions by teacher:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const questions = await getQuestionsByTeacher(username, full);
|
||||||
|
|
||||||
|
if (!questions) {
|
||||||
|
res.status(404).json({ error: 'Teacher not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ questions: questions });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
import { Request, Response } from 'express';
|
|
||||||
import { UserService } from '../services/users.js';
|
|
||||||
import { UserDTO } from '../interfaces/user.js';
|
|
||||||
import { User } from '../entities/users/user.entity.js';
|
|
||||||
|
|
||||||
export async function getAllUsersHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> {
|
|
||||||
try {
|
|
||||||
const full = req.query.full === 'true';
|
|
||||||
|
|
||||||
const users: UserDTO[] | string[] = full ? await service.getAllUsers() : await service.getAllUserIds();
|
|
||||||
|
|
||||||
if (!users) {
|
|
||||||
res.status(404).json({ error: `Users not found.` });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json(users);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error fetching users:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>): Promise<void> {
|
|
||||||
try {
|
|
||||||
const username = req.params.username as string;
|
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await service.getUserByUsername(username);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
res.status(404).json({
|
|
||||||
error: `User with username '${username}' not found.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json(user);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error fetching users:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>, UserClass: new () => T) {
|
|
||||||
try {
|
|
||||||
console.log('req', req);
|
|
||||||
const userData = req.body as UserDTO;
|
|
||||||
|
|
||||||
if (!userData.username || !userData.firstName || !userData.lastName) {
|
|
||||||
res.status(400).json({
|
|
||||||
error: 'Missing required fields: username, firstName, lastName',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = await service.createUser(userData, UserClass);
|
|
||||||
res.status(201).json(newUser);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error creating user:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteUserHandler<T extends User>(req: Request, res: Response, service: UserService<T>) {
|
|
||||||
try {
|
|
||||||
const username = req.params.username;
|
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletedUser = await service.deleteUser(username);
|
|
||||||
if (!deletedUser) {
|
|
||||||
res.status(404).json({
|
|
||||||
error: `User with username '${username}' not found.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json(deletedUser);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error deleting user:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,8 +2,6 @@ import { AnyEntity, EntityManager, EntityName, EntityRepository } from '@mikro-o
|
||||||
import { forkEntityManager } from '../orm.js';
|
import { forkEntityManager } from '../orm.js';
|
||||||
import { StudentRepository } from './users/student-repository.js';
|
import { StudentRepository } from './users/student-repository.js';
|
||||||
import { Student } from '../entities/users/student.entity.js';
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
import { User } from '../entities/users/user.entity.js';
|
|
||||||
import { UserRepository } from './users/user-repository.js';
|
|
||||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||||
import { TeacherRepository } from './users/teacher-repository.js';
|
import { TeacherRepository } from './users/teacher-repository.js';
|
||||||
import { Class } from '../entities/classes/class.entity.js';
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
import { User } from '../../entities/users/user.entity.js';
|
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
// Import { UserRepository } from './user-repository.js';
|
// Import { UserRepository } from './user-repository.js';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { UserRepository } from './user-repository.js';
|
|
||||||
|
|
||||||
export class TeacherRepository extends DwengoEntityRepository<Teacher> {
|
export class TeacherRepository extends DwengoEntityRepository<Teacher> {
|
||||||
public findByUsername(username: string): Promise<Teacher | null> {
|
public findByUsername(username: string): Promise<Teacher | null> {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||||
import { Class } from '../classes/class.entity.js';
|
import { Class } from '../classes/class.entity.js';
|
||||||
import { Group } from './group.entity.js';
|
import { Group } from './group.entity.js';
|
||||||
import { Language } from '../content/language.js';
|
import { Language } from '../content/language.js';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Collection, Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
|
import { Entity, ManyToMany, ManyToOne, PrimaryKey } from '@mikro-orm/core';
|
||||||
import { Assignment } from './assignment.entity.js';
|
import { Assignment } from './assignment.entity.js';
|
||||||
import { Student } from '../users/student.entity.js';
|
import { Student } from '../users/student.entity.js';
|
||||||
import { GroupRepository } from '../../data/assignments/group-repository.js';
|
import { GroupRepository } from '../../data/assignments/group-repository.js';
|
||||||
|
|
|
@ -3,6 +3,12 @@ import { Student } from '../users/student.entity.js';
|
||||||
import { Class } from './class.entity.js';
|
import { Class } from './class.entity.js';
|
||||||
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
|
import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js';
|
||||||
|
|
||||||
|
export enum ClassJoinRequestStatus {
|
||||||
|
Open = 'open',
|
||||||
|
Accepted = 'accepted',
|
||||||
|
Declined = 'declined',
|
||||||
|
}
|
||||||
|
|
||||||
@Entity({
|
@Entity({
|
||||||
repository: () => ClassJoinRequestRepository,
|
repository: () => ClassJoinRequestRepository,
|
||||||
})
|
})
|
||||||
|
@ -22,9 +28,3 @@ export class ClassJoinRequest {
|
||||||
@Enum(() => ClassJoinRequestStatus)
|
@Enum(() => ClassJoinRequestStatus)
|
||||||
status!: ClassJoinRequestStatus;
|
status!: ClassJoinRequestStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ClassJoinRequestStatus {
|
|
||||||
Open = 'open',
|
|
||||||
Accepted = 'accepted',
|
|
||||||
Declined = 'declined',
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { FALLBACK_LANG } from '../config.js';
|
||||||
import { Assignment } from '../entities/assignments/assignment.entity.js';
|
import { Assignment } from '../entities/assignments/assignment.entity.js';
|
||||||
import { Class } from '../entities/classes/class.entity.js';
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
import { languageMap } from '../entities/content/language.js';
|
import { languageMap } from '../entities/content/language.js';
|
||||||
import { GroupDTO, mapToGroupDTO } from './group.js';
|
import { GroupDTO } from './group.js';
|
||||||
|
|
||||||
export interface AssignmentDTO {
|
export interface AssignmentDTO {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -46,7 +46,5 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
|
||||||
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
|
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
|
||||||
assignment.within = cls;
|
assignment.within = cls;
|
||||||
|
|
||||||
console.log(assignment);
|
|
||||||
|
|
||||||
return assignment;
|
return assignment;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,6 @@ export interface ClassDTO {
|
||||||
teachers: string[];
|
teachers: string[];
|
||||||
students: string[];
|
students: string[];
|
||||||
joinRequests: string[];
|
joinRequests: string[];
|
||||||
endpoints?: {
|
|
||||||
self: string;
|
|
||||||
invitations: string;
|
|
||||||
assignments: string;
|
|
||||||
students: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToClassDTO(cls: Class): ClassDTO {
|
export function mapToClassDTO(cls: Class): ClassDTO {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { Question } from '../entities/questions/question.entity.js';
|
import { Question } from '../entities/questions/question.entity.js';
|
||||||
import { UserDTO } from './user.js';
|
|
||||||
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||||
import { mapToStudentDTO, StudentDTO } from './student.js';
|
import { mapToStudentDTO, StudentDTO } from './student.js';
|
||||||
import { TeacherDTO } from './teacher.js';
|
|
||||||
|
|
||||||
export interface QuestionDTO {
|
export interface QuestionDTO {
|
||||||
learningObjectIdentifier: LearningObjectIdentifier;
|
learningObjectIdentifier: LearningObjectIdentifier;
|
||||||
|
|
|
@ -2,13 +2,10 @@ import { Submission } from '../entities/assignments/submission.entity.js';
|
||||||
import { Language } from '../entities/content/language.js';
|
import { Language } from '../entities/content/language.js';
|
||||||
import { GroupDTO, mapToGroupDTO } from './group.js';
|
import { GroupDTO, mapToGroupDTO } from './group.js';
|
||||||
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js';
|
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js';
|
||||||
import { mapToUser } from './user';
|
import { LearningObjectIdentifier } from './learning-content.js';
|
||||||
import { Student } from '../entities/users/student.entity';
|
|
||||||
|
|
||||||
export interface SubmissionDTO {
|
export interface SubmissionDTO {
|
||||||
learningObjectHruid: string;
|
learningObjectIdentifier: LearningObjectIdentifier;
|
||||||
learningObjectLanguage: Language;
|
|
||||||
learningObjectVersion: number;
|
|
||||||
|
|
||||||
submissionNumber?: number;
|
submissionNumber?: number;
|
||||||
submitter: StudentDTO;
|
submitter: StudentDTO;
|
||||||
|
@ -17,11 +14,21 @@ export interface SubmissionDTO {
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SubmissionDTOId {
|
||||||
|
learningObjectHruid: string;
|
||||||
|
learningObjectLanguage: Language;
|
||||||
|
learningObjectVersion: number;
|
||||||
|
|
||||||
|
submissionNumber?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
||||||
return {
|
return {
|
||||||
learningObjectHruid: submission.learningObjectHruid,
|
learningObjectIdentifier: {
|
||||||
learningObjectLanguage: submission.learningObjectLanguage,
|
hruid: submission.learningObjectHruid,
|
||||||
learningObjectVersion: submission.learningObjectVersion,
|
language: submission.learningObjectLanguage,
|
||||||
|
version: submission.learningObjectVersion,
|
||||||
|
},
|
||||||
|
|
||||||
submissionNumber: submission.submissionNumber,
|
submissionNumber: submission.submissionNumber,
|
||||||
submitter: mapToStudentDTO(submission.submitter),
|
submitter: mapToStudentDTO(submission.submitter),
|
||||||
|
@ -31,11 +38,21 @@ export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId {
|
||||||
|
return {
|
||||||
|
learningObjectHruid: submission.learningObjectHruid,
|
||||||
|
learningObjectLanguage: submission.learningObjectLanguage,
|
||||||
|
learningObjectVersion: submission.learningObjectVersion,
|
||||||
|
|
||||||
|
submissionNumber: submission.submissionNumber,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission {
|
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission {
|
||||||
const submission = new Submission();
|
const submission = new Submission();
|
||||||
submission.learningObjectHruid = submissionDTO.learningObjectHruid;
|
submission.learningObjectHruid = submissionDTO.learningObjectIdentifier.hruid;
|
||||||
submission.learningObjectLanguage = submissionDTO.learningObjectLanguage;
|
submission.learningObjectLanguage = submissionDTO.learningObjectIdentifier.language;
|
||||||
submission.learningObjectVersion = submissionDTO.learningObjectVersion;
|
submission.learningObjectVersion = submissionDTO.learningObjectIdentifier.version!;
|
||||||
// Submission.submissionNumber = submissionDTO.submissionNumber;
|
// Submission.submissionNumber = submissionDTO.submissionNumber;
|
||||||
submission.submitter = mapToStudent(submissionDTO.submitter);
|
submission.submitter = mapToStudent(submissionDTO.submitter);
|
||||||
// Submission.submissionTime = submissionDTO.time;
|
// Submission.submissionTime = submissionDTO.time;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createLogger, format, Logger as WinstonLogger, transports } from 'winston';
|
import { createLogger, format, Logger as WinstonLogger, transports } from 'winston';
|
||||||
import LokiTransport from 'winston-loki';
|
import LokiTransport from 'winston-loki';
|
||||||
import { LokiLabels } from 'loki-logger-ts';
|
import { LokiLabels } from 'loki-logger-ts';
|
||||||
import { LOG_LEVEL, LOKI_HOST } from '../config.js';
|
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
||||||
|
|
||||||
export class Logger extends WinstonLogger {
|
export class Logger extends WinstonLogger {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -22,10 +22,25 @@ function initializeLogger(): Logger {
|
||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logLevel = getEnvVar(EnvVars.LogLevel);
|
||||||
|
|
||||||
|
const consoleTransport = new transports.Console({
|
||||||
|
level: getEnvVar(EnvVars.LogLevel),
|
||||||
|
format: format.combine(format.cli(), format.colorize()),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getEnvVar(EnvVars.RunMode) === 'dev') {
|
||||||
|
return createLogger({
|
||||||
|
transports: [consoleTransport],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const lokiHost = getEnvVar(EnvVars.LokiHost);
|
||||||
|
|
||||||
const lokiTransport: LokiTransport = new LokiTransport({
|
const lokiTransport: LokiTransport = new LokiTransport({
|
||||||
host: LOKI_HOST,
|
host: lokiHost,
|
||||||
labels: Labels,
|
labels: Labels,
|
||||||
level: LOG_LEVEL,
|
level: logLevel,
|
||||||
json: true,
|
json: true,
|
||||||
format: format.combine(format.timestamp(), format.json()),
|
format: format.combine(format.timestamp(), format.json()),
|
||||||
onConnectionError: (err) => {
|
onConnectionError: (err) => {
|
||||||
|
@ -34,16 +49,11 @@ function initializeLogger(): Logger {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const consoleTransport = new transports.Console({
|
|
||||||
level: LOG_LEVEL,
|
|
||||||
format: format.combine(format.cli(), format.colorize()),
|
|
||||||
});
|
|
||||||
|
|
||||||
logger = createLogger({
|
logger = createLogger({
|
||||||
transports: [lokiTransport, consoleTransport],
|
transports: [lokiTransport, consoleTransport],
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug(`Logger initialized with level ${LOG_LEVEL}, Loki host ${LOKI_HOST}`);
|
logger.debug(`Logger initialized with level ${logLevel} to Loki host ${lokiHost}`);
|
||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { PostgreSqlDriver } from '@mikro-orm/postgresql';
|
||||||
import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js';
|
import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js';
|
||||||
import { SqliteDriver } from '@mikro-orm/sqlite';
|
import { SqliteDriver } from '@mikro-orm/sqlite';
|
||||||
import { MikroOrmLogger } from './logging/mikroOrmLogger.js';
|
import { MikroOrmLogger } from './logging/mikroOrmLogger.js';
|
||||||
import { LOG_LEVEL } from './config.js';
|
|
||||||
|
|
||||||
// Import alle entity-bestanden handmatig
|
// Import alle entity-bestanden handmatig
|
||||||
import { User } from './entities/users/user.entity.js';
|
import { User } from './entities/users/user.entity.js';
|
||||||
|
@ -69,7 +68,7 @@ function config(testingMode: boolean = false): Options {
|
||||||
// EntitiesTs: entitiesTs,
|
// EntitiesTs: entitiesTs,
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
debug: LOG_LEVEL === 'debug',
|
debug: getEnvVar(EnvVars.LogLevel) === 'debug',
|
||||||
loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options),
|
loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { Response, Router } from 'express';
|
import { Response, Router } from 'express';
|
||||||
import studentRouter from './students.js';
|
import studentRouter from './students.js';
|
||||||
import groupRouter from './groups.js';
|
import teacherRouter from './teachers.js';
|
||||||
import assignmentRouter from './assignments.js';
|
|
||||||
import submissionRouter from './submissions.js';
|
|
||||||
import classRouter from './classes.js';
|
import classRouter from './classes.js';
|
||||||
import questionRouter from './questions.js';
|
|
||||||
import authRouter from './auth.js';
|
import authRouter from './auth.js';
|
||||||
import themeRoutes from './themes.js';
|
import themeRoutes from './themes.js';
|
||||||
import learningPathRoutes from './learning-paths.js';
|
import learningPathRoutes from './learning-paths.js';
|
||||||
|
@ -22,11 +19,8 @@ router.get('/', (_, res: Response) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
|
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
|
||||||
router.use('/group', groupRouter /* #swagger.tags = ['Group'] */);
|
router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */);
|
||||||
router.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */);
|
|
||||||
router.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */);
|
|
||||||
router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
|
router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
|
||||||
router.use('/question', questionRouter /* #swagger.tags = ['Question'] */);
|
|
||||||
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
|
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
|
||||||
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
|
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
|
||||||
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);
|
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
getStudentHandler,
|
getStudentHandler,
|
||||||
getStudentSubmissionsHandler,
|
getStudentSubmissionsHandler,
|
||||||
} from '../controllers/students.js';
|
} from '../controllers/students.js';
|
||||||
import { getStudentGroups } from '../services/students.js';
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
// Root endpoint used to search objects
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
|
import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
|
||||||
import { Assignment } from '../entities/assignments/assignment.entity.js';
|
|
||||||
import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
|
import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
|
||||||
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
|
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
|
||||||
|
|
||||||
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
|
export async function getAllAssignments(classid: string, full: boolean): Promise<AssignmentDTO[]> {
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
|
@ -21,7 +20,7 @@ export async function getAllAssignments(classid: string, full: boolean): Promise
|
||||||
return assignments.map(mapToAssignmentDTOId);
|
return assignments.map(mapToAssignmentDTOId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<Assignment | null> {
|
export async function createAssignment(classid: string, assignmentData: AssignmentDTO): Promise<AssignmentDTO | null> {
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
const cls = await classRepository.findById(classid);
|
const cls = await classRepository.findById(classid);
|
||||||
|
|
||||||
|
@ -36,8 +35,9 @@ export async function createAssignment(classid: string, assignmentData: Assignme
|
||||||
const newAssignment = assignmentRepository.create(assignment);
|
const newAssignment = assignmentRepository.create(assignment);
|
||||||
await assignmentRepository.save(newAssignment);
|
await assignmentRepository.save(newAssignment);
|
||||||
|
|
||||||
return newAssignment;
|
return mapToAssignmentDTO(newAssignment);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,11 @@ export async function getAssignment(classid: string, id: number): Promise<Assign
|
||||||
return mapToAssignmentDTO(assignment);
|
return mapToAssignmentDTO(assignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAssignmentsSubmissions(classid: string, assignmentNumber: number): Promise<SubmissionDTO[]> {
|
export async function getAssignmentsSubmissions(
|
||||||
|
classid: string,
|
||||||
|
assignmentNumber: number,
|
||||||
|
full: boolean
|
||||||
|
): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
const cls = await classRepository.findById(classid);
|
const cls = await classRepository.findById(classid);
|
||||||
|
|
||||||
|
@ -81,5 +85,9 @@ export async function getAssignmentsSubmissions(classid: string, assignmentNumbe
|
||||||
const submissionRepository = getSubmissionRepository();
|
const submissionRepository = getSubmissionRepository();
|
||||||
const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
|
const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat();
|
||||||
|
|
||||||
return submissions.map(mapToSubmissionDTO);
|
if (full) {
|
||||||
|
return submissions.map(mapToSubmissionDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return submissions.map(mapToSubmissionDTOId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js';
|
import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js';
|
||||||
import { Class } from '../entities/classes/class.entity.js';
|
|
||||||
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
|
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
|
||||||
import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
|
import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
|
||||||
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js';
|
import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js';
|
||||||
|
@ -21,16 +20,14 @@ export async function getAllClasses(full: boolean): Promise<ClassDTO[] | string[
|
||||||
return classes.map((cls) => cls.classId!);
|
return classes.map((cls) => cls.classId!);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createClass(classData: ClassDTO): Promise<Class | null> {
|
export async function createClass(classData: ClassDTO): Promise<ClassDTO | null> {
|
||||||
const teacherRepository = getTeacherRepository();
|
const teacherRepository = getTeacherRepository();
|
||||||
const teacherUsernames = classData.teachers || [];
|
const teacherUsernames = classData.teachers || [];
|
||||||
const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher != null);
|
const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher !== null);
|
||||||
|
|
||||||
const studentRepository = getStudentRepository();
|
const studentRepository = getStudentRepository();
|
||||||
const studentUsernames = classData.students || [];
|
const studentUsernames = classData.students || [];
|
||||||
const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null);
|
const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null);
|
||||||
|
|
||||||
//Const cls = mapToClass(classData, teachers, students);
|
|
||||||
|
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
|
|
||||||
|
@ -42,7 +39,7 @@ export async function createClass(classData: ClassDTO): Promise<Class | null> {
|
||||||
});
|
});
|
||||||
await classRepository.save(newClass);
|
await classRepository.save(newClass);
|
||||||
|
|
||||||
return newClass;
|
return mapToClassDTO(newClass);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
return null;
|
return null;
|
|
@ -1,4 +1,3 @@
|
||||||
import { GroupRepository } from '../data/assignments/group-repository.js';
|
|
||||||
import {
|
import {
|
||||||
getAssignmentRepository,
|
getAssignmentRepository,
|
||||||
getClassRepository,
|
getClassRepository,
|
||||||
|
@ -8,7 +7,7 @@ import {
|
||||||
} from '../data/repositories.js';
|
} from '../data/repositories.js';
|
||||||
import { Group } from '../entities/assignments/group.entity.js';
|
import { Group } from '../entities/assignments/group.entity.js';
|
||||||
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
|
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
|
||||||
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
|
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
|
||||||
|
|
||||||
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> {
|
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise<GroupDTO | null> {
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
|
@ -43,7 +42,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme
|
||||||
const studentRepository = getStudentRepository();
|
const studentRepository = getStudentRepository();
|
||||||
|
|
||||||
const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
|
const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list
|
||||||
const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student != null);
|
const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null);
|
||||||
|
|
||||||
console.log(members);
|
console.log(members);
|
||||||
|
|
||||||
|
@ -103,7 +102,12 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu
|
||||||
return groups.map(mapToGroupDTOId);
|
return groups.map(mapToGroupDTOId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGroupSubmissions(classId: string, assignmentNumber: number, groupNumber: number): Promise<SubmissionDTO[]> {
|
export async function getGroupSubmissions(
|
||||||
|
classId: string,
|
||||||
|
assignmentNumber: number,
|
||||||
|
groupNumber: number,
|
||||||
|
full: boolean
|
||||||
|
): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
const cls = await classRepository.findById(classId);
|
const cls = await classRepository.findById(classId);
|
||||||
|
|
||||||
|
@ -128,5 +132,9 @@ export async function getGroupSubmissions(classId: string, assignmentNumber: num
|
||||||
const submissionRepository = getSubmissionRepository();
|
const submissionRepository = getSubmissionRepository();
|
||||||
const submissions = await submissionRepository.findAllSubmissionsForGroup(group);
|
const submissions = await submissionRepository.findAllSubmissionsForGroup(group);
|
||||||
|
|
||||||
return submissions.map(mapToSubmissionDTO);
|
if (full) {
|
||||||
|
return submissions.map(mapToSubmissionDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return submissions.map(mapToSubmissionDTOId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,13 @@ export async function getLearningObjectById(hruid: string, language: string): Pr
|
||||||
return filterData(metadata, htmlUrl);
|
return filterData(metadata, htmlUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic function to fetch learning paths
|
||||||
|
*/
|
||||||
|
function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic function to fetch learning objects (full data or just HRUIDs)
|
* Generic function to fetch learning objects (full data or just HRUIDs)
|
||||||
*/
|
*/
|
||||||
|
@ -85,6 +92,3 @@ export async function getLearningObjectsFromPath(hruid: string, language: string
|
||||||
export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> {
|
export async function getLearningObjectIdsFromPath(hruid: string, language: string): Promise<string[]> {
|
||||||
return (await fetchLearningObjects(hruid, false, language)) as string[];
|
return (await fetchLearningObjects(hruid, false, language)) as string[];
|
||||||
}
|
}
|
||||||
function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike<LearningPathResponse> {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
}
|
|
||||||
|
|
|
@ -103,5 +103,5 @@ export async function deleteQuestion(questionId: QuestionId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return question;
|
return mapToQuestionDTO(question);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,18 @@ import { AssignmentDTO } from '../interfaces/assignment.js';
|
||||||
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
|
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
|
||||||
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
|
import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
|
||||||
import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
|
import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js';
|
||||||
import { mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js';
|
import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js';
|
||||||
import { getAllAssignments } from './assignments.js';
|
import { getAllAssignments } from './assignments.js';
|
||||||
import { UserService } from './users.js';
|
|
||||||
|
|
||||||
export async function getAllStudents(): Promise<StudentDTO[]> {
|
export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
|
||||||
const studentRepository = getStudentRepository();
|
const studentRepository = getStudentRepository();
|
||||||
const users = await studentRepository.findAll();
|
const students = await studentRepository.findAll();
|
||||||
return users.map(mapToStudentDTO);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllStudentIds(): Promise<string[]> {
|
if (full) {
|
||||||
const users = await getAllStudents();
|
return students.map(mapToStudentDTO);
|
||||||
return users.map((user) => user.username);
|
}
|
||||||
|
|
||||||
|
return students.map((student) => student.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStudent(username: string): Promise<StudentDTO | null> {
|
export async function getStudent(username: string): Promise<StudentDTO | null> {
|
||||||
|
@ -111,7 +110,7 @@ export async function getStudentGroups(username: string, full: boolean): Promise
|
||||||
return groups.map(mapToGroupDTOId);
|
return groups.map(mapToGroupDTOId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStudentSubmissions(username: string): Promise<SubmissionDTO[]> {
|
export async function getStudentSubmissions(username: string, full: boolean): Promise<SubmissionDTO[] | SubmissionDTOId[]> {
|
||||||
const studentRepository = getStudentRepository();
|
const studentRepository = getStudentRepository();
|
||||||
const student = await studentRepository.findByUsername(username);
|
const student = await studentRepository.findByUsername(username);
|
||||||
|
|
||||||
|
@ -122,5 +121,9 @@ export async function getStudentSubmissions(username: string): Promise<Submissio
|
||||||
const submissionRepository = getSubmissionRepository();
|
const submissionRepository = getSubmissionRepository();
|
||||||
const submissions = await submissionRepository.findAllSubmissionsForStudent(student);
|
const submissions = await submissionRepository.findAllSubmissionsForStudent(student);
|
||||||
|
|
||||||
return submissions.map(mapToSubmissionDTO);
|
if (full) {
|
||||||
|
return submissions.map(mapToSubmissionDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return submissions.map(mapToSubmissionDTOId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export async function createSubmission(submissionDTO: SubmissionDTO) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return submission;
|
return mapToSubmissionDTO(submission);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) {
|
export async function deleteSubmission(learningObjectHruid: string, language: Language, version: number, submissionNumber: number) {
|
||||||
|
|
|
@ -7,22 +7,22 @@ import {
|
||||||
} from '../data/repositories.js';
|
} from '../data/repositories.js';
|
||||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||||
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
|
import { ClassDTO, mapToClassDTO } from '../interfaces/class.js';
|
||||||
import { getClassStudents } from './class.js';
|
import { getClassStudents } from './classes.js';
|
||||||
import { StudentDTO } from '../interfaces/student.js';
|
import { StudentDTO } from '../interfaces/student.js';
|
||||||
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
|
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js';
|
||||||
import { UserService } from './users.js';
|
|
||||||
import { mapToUser } from '../interfaces/user.js';
|
import { mapToUser } from '../interfaces/user.js';
|
||||||
import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js';
|
import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js';
|
||||||
|
import { teachersOnly } from '../middleware/auth/auth.js';
|
||||||
|
|
||||||
export async function getAllTeachers(): Promise<TeacherDTO[]> {
|
export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> {
|
||||||
const teacherRepository = getTeacherRepository();
|
const teacherRepository = getTeacherRepository();
|
||||||
const users = await teacherRepository.findAll();
|
const teachers = await teacherRepository.findAll();
|
||||||
return users.map(mapToTeacherDTO);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllTeacherIds(): Promise<string[]> {
|
if (full) {
|
||||||
const users = await getAllTeachers();
|
return teachers.map(mapToTeacherDTO);
|
||||||
return users.map((user) => user.username);
|
}
|
||||||
|
|
||||||
|
return teachers.map((teacher) => teacher.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacher(username: string): Promise<TeacherDTO | null> {
|
export async function getTeacher(username: string): Promise<TeacherDTO | null> {
|
||||||
|
@ -64,11 +64,11 @@ export async function deleteTeacher(username: string): Promise<TeacherDTO | null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[]> {
|
export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[] | null> {
|
||||||
const teacherRepository = getTeacherRepository();
|
const teacherRepository = getTeacherRepository();
|
||||||
const teacher = await teacherRepository.findByUsername(username);
|
const teacher = await teacherRepository.findByUsername(username);
|
||||||
if (!teacher) {
|
if (!teacher) {
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const classRepository = getClassRepository();
|
const classRepository = getClassRepository();
|
||||||
|
@ -76,35 +76,49 @@ export async function fetchClassesByTeacher(username: string): Promise<ClassDTO[
|
||||||
return classes.map(mapToClassDTO);
|
return classes.map(mapToClassDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getClassesByTeacher(username: string): Promise<ClassDTO[]> {
|
export async function getClassesByTeacher(username: string, full: boolean): Promise<ClassDTO[] | string[] | null> {
|
||||||
return await fetchClassesByTeacher(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getClassIdsByTeacher(username: string): Promise<string[]> {
|
|
||||||
const classes = await fetchClassesByTeacher(username);
|
const classes = await fetchClassesByTeacher(username);
|
||||||
|
|
||||||
|
if (!classes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
|
||||||
return classes.map((cls) => cls.id);
|
return classes.map((cls) => cls.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchStudentsByTeacher(username: string) {
|
export async function fetchStudentsByTeacher(username: string): Promise<StudentDTO[] | null> {
|
||||||
const classes = await getClassIdsByTeacher(username);
|
const classes = (await getClassesByTeacher(username, false)) as string[];
|
||||||
|
|
||||||
|
if (!classes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat();
|
return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStudentsByTeacher(username: string): Promise<StudentDTO[]> {
|
export async function getStudentsByTeacher(username: string, full: boolean): Promise<StudentDTO[] | string[] | null> {
|
||||||
return await fetchStudentsByTeacher(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStudentIdsByTeacher(username: string): Promise<string[]> {
|
|
||||||
const students = await fetchStudentsByTeacher(username);
|
const students = await fetchStudentsByTeacher(username);
|
||||||
|
|
||||||
|
if (!students) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
return students;
|
||||||
|
}
|
||||||
|
|
||||||
return students.map((student) => student.username);
|
return students.map((student) => student.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTeacherQuestions(username: string): Promise<QuestionDTO[]> {
|
export async function fetchTeacherQuestions(username: string): Promise<QuestionDTO[] | null> {
|
||||||
const teacherRepository = getTeacherRepository();
|
const teacherRepository = getTeacherRepository();
|
||||||
const teacher = await teacherRepository.findByUsername(username);
|
const teacher = await teacherRepository.findByUsername(username);
|
||||||
if (!teacher) {
|
if (!teacher) {
|
||||||
throw new Error(`Teacher with username '${username}' not found.`);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all learning objects that this teacher manages
|
// Find all learning objects that this teacher manages
|
||||||
|
@ -118,12 +132,16 @@ export async function fetchTeacherQuestions(username: string): Promise<QuestionD
|
||||||
return questions.map(mapToQuestionDTO);
|
return questions.map(mapToQuestionDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getQuestionsByTeacher(username: string): Promise<QuestionDTO[]> {
|
export async function getQuestionsByTeacher(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[] | null> {
|
||||||
return await fetchTeacherQuestions(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getQuestionIdsByTeacher(username: string): Promise<QuestionId[]> {
|
|
||||||
const questions = await fetchTeacherQuestions(username);
|
const questions = await fetchTeacherQuestions(username);
|
||||||
|
|
||||||
|
if (!questions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (full) {
|
||||||
|
return questions;
|
||||||
|
}
|
||||||
|
|
||||||
return questions.map(mapToQuestionId);
|
return questions.map(mapToQuestionId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_';
|
||||||
const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_';
|
const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_';
|
||||||
const CORS_PREFIX = PREFIX + 'CORS_';
|
const CORS_PREFIX = PREFIX + 'CORS_';
|
||||||
|
const LOGGING_PREFIX = PREFIX + 'LOGGING_';
|
||||||
|
|
||||||
type EnvVar = { key: string; required?: boolean; defaultValue?: any };
|
type EnvVar = { key: string; required?: boolean; defaultValue?: any };
|
||||||
|
|
||||||
export const EnvVars: { [key: string]: EnvVar } = {
|
export const EnvVars: { [key: string]: EnvVar } = {
|
||||||
Port: { key: PREFIX + 'PORT', defaultValue: 3000 },
|
Port: { key: PREFIX + 'PORT', defaultValue: 3000 },
|
||||||
|
LearningContentRepoApiBaseUrl: { key: PREFIX + 'LEARNING_CONTENT_REPO_API_BASE_URL', defaultValue: 'https://dwengo.org/backend/api' },
|
||||||
|
FallbackLanguage: { key: PREFIX + 'FALLBACK_LANGUAGE', defaultValue: 'nl' },
|
||||||
|
RunMode: { key: PREFIX + 'RUN_MODE', defaultValue: 'dev' },
|
||||||
|
|
||||||
DbHost: { key: DB_PREFIX + 'HOST', required: true },
|
DbHost: { key: DB_PREFIX + 'HOST', required: true },
|
||||||
DbPort: { key: DB_PREFIX + 'PORT', defaultValue: 5432 },
|
DbPort: { key: DB_PREFIX + 'PORT', defaultValue: 5432 },
|
||||||
DbName: { key: DB_PREFIX + 'NAME', defaultValue: 'dwengo' },
|
DbName: { key: DB_PREFIX + 'NAME', defaultValue: 'dwengo' },
|
||||||
DbUsername: { key: DB_PREFIX + 'USERNAME', required: true },
|
DbUsername: { key: DB_PREFIX + 'USERNAME', required: true },
|
||||||
DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true },
|
DbPassword: { key: DB_PREFIX + 'PASSWORD', required: true },
|
||||||
DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false },
|
DbUpdate: { key: DB_PREFIX + 'UPDATE', defaultValue: false },
|
||||||
LearningContentRepoApiBaseUrl: { key: PREFIX + 'LEARNING_CONTENT_REPO_API_BASE_URL', defaultValue: 'https://dwengo.org/backend/api' },
|
|
||||||
FallbackLanguage: { key: PREFIX + 'FALLBACK_LANGUAGE', defaultValue: 'nl' },
|
|
||||||
UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: 'u_' },
|
UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: 'u_' },
|
||||||
|
|
||||||
IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true },
|
IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true },
|
||||||
IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', required: true },
|
IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', required: true },
|
||||||
IdpStudentJwksEndpoint: { key: STUDENT_IDP_PREFIX + 'JWKS_ENDPOINT', required: true },
|
IdpStudentJwksEndpoint: { key: STUDENT_IDP_PREFIX + 'JWKS_ENDPOINT', required: true },
|
||||||
|
@ -25,8 +29,12 @@ export const EnvVars: { [key: string]: EnvVar } = {
|
||||||
IdpTeacherClientId: { key: TEACHER_IDP_PREFIX + 'CLIENT_ID', required: true },
|
IdpTeacherClientId: { key: TEACHER_IDP_PREFIX + 'CLIENT_ID', required: true },
|
||||||
IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true },
|
IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true },
|
||||||
IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' },
|
IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' },
|
||||||
|
|
||||||
CorsAllowedOrigins: { key: CORS_PREFIX + 'ALLOWED_ORIGINS', defaultValue: '' },
|
CorsAllowedOrigins: { key: CORS_PREFIX + 'ALLOWED_ORIGINS', defaultValue: '' },
|
||||||
CorsAllowedHeaders: { key: CORS_PREFIX + 'ALLOWED_HEADERS', defaultValue: 'Authorization,Content-Type' },
|
CorsAllowedHeaders: { key: CORS_PREFIX + 'ALLOWED_HEADERS', defaultValue: 'Authorization,Content-Type' },
|
||||||
|
|
||||||
|
LogLevel: { key: LOGGING_PREFIX + 'LEVEL', defaultValue: 'info' },
|
||||||
|
LokiHost: { key: LOGGING_PREFIX + 'LOKI_HOST', defaultValue: 'http://localhost:3102' },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,12 +8,12 @@ const logger: Logger = getLogger();
|
||||||
|
|
||||||
export function loadTranslations<T>(language: string): T {
|
export function loadTranslations<T>(language: string): T {
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(process.cwd(), '_i18n', `${language}.yml`);
|
const filePath = path.join(process.cwd(), 'i18n', `${language}.yml`);
|
||||||
const yamlFile = fs.readFileSync(filePath, 'utf8');
|
const yamlFile = fs.readFileSync(filePath, 'utf8');
|
||||||
return yaml.load(yamlFile) as T;
|
return yaml.load(yamlFile) as T;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`Cannot load translation for ${language}, fallen back to dutch`, error);
|
logger.warn(`Cannot load translation for ${language}, fallen back to dutch`, error);
|
||||||
const fallbackPath = path.join(process.cwd(), '_i18n', `${FALLBACK_LANG}.yml`);
|
const fallbackPath = path.join(process.cwd(), 'i18n', `${FALLBACK_LANG}.yml`);
|
||||||
return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T;
|
return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
#
|
#
|
||||||
# This file is used to define the production environment for the project.
|
# Use this configuration to deploy the project on a server.
|
||||||
# It is used to deploy the project on a server.
|
#
|
||||||
# Should not be used for local development.
|
# This configuration builds the frontend and backend services as Docker images,
|
||||||
|
# and uses the paths for the services, instead of ports, and enables SSL.
|
||||||
#
|
#
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
|
@ -35,12 +36,16 @@ services:
|
||||||
- 'traefik.http.services.api.loadbalancer.server.port=3000'
|
- 'traefik.http.services.api.loadbalancer.server.port=3000'
|
||||||
|
|
||||||
db:
|
db:
|
||||||
# Also see compose.yml
|
extends:
|
||||||
|
file: ./compose.yml
|
||||||
|
service: db
|
||||||
networks:
|
networks:
|
||||||
- dwengo-1
|
- dwengo-1
|
||||||
|
|
||||||
idp:
|
idp:
|
||||||
# Also see compose.yml
|
extends:
|
||||||
|
file: ./compose.yml
|
||||||
|
service: idp
|
||||||
# TODO Replace with proper production command
|
# TODO Replace with proper production command
|
||||||
command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm']
|
command: ['start-dev', '--http-port', '7080', '--https-port', '7443', '--import-realm']
|
||||||
networks:
|
networks:
|
||||||
|
@ -92,7 +97,15 @@ services:
|
||||||
- dwengo-1
|
- dwengo-1
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
# Also see compose.yml
|
image: grafana/loki:latest
|
||||||
|
ports:
|
||||||
|
- '9001:3102'
|
||||||
|
- '9095:9095'
|
||||||
|
command: -config.file=/etc/loki/config.yaml
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./config/loki/config.yml:/etc/loki/config.yaml
|
||||||
|
- dwengo_loki_data:/loki
|
||||||
networks:
|
networks:
|
||||||
- dwengo-1
|
- dwengo-1
|
||||||
|
|
||||||
|
@ -107,6 +120,7 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
dwengo_grafana_data:
|
dwengo_grafana_data:
|
||||||
dwengo_letsencrypt:
|
dwengo_letsencrypt:
|
||||||
|
dwengo_loki_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
dwengo-1:
|
dwengo-1:
|
|
@ -32,8 +32,15 @@ services:
|
||||||
- 'traefik.http.routers.api.rule=PathPrefix(`/api`)'
|
- 'traefik.http.routers.api.rule=PathPrefix(`/api`)'
|
||||||
- 'traefik.http.services.api.loadbalancer.server.port=3000'
|
- 'traefik.http.services.api.loadbalancer.server.port=3000'
|
||||||
|
|
||||||
|
db:
|
||||||
|
extends:
|
||||||
|
file: ./compose.yml
|
||||||
|
service: db
|
||||||
|
|
||||||
idp:
|
idp:
|
||||||
# Also see compose.yml
|
extends:
|
||||||
|
file: ./compose.yml
|
||||||
|
service: idp
|
||||||
labels:
|
labels:
|
||||||
- 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)'
|
- 'traefik.http.routers.idp.rule=PathPrefix(`/idp`)'
|
||||||
- 'traefik.http.services.idp.loadbalancer.server.port=7080'
|
- 'traefik.http.services.idp.loadbalancer.server.port=7080'
|
||||||
|
@ -60,6 +67,17 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
|
||||||
|
logging:
|
||||||
|
image: grafana/loki:latest
|
||||||
|
ports:
|
||||||
|
- '9001:3102'
|
||||||
|
- '9095:9095'
|
||||||
|
command: -config.file=/etc/loki/config.yaml
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./config/loki/config.yml:/etc/loki/config.yaml
|
||||||
|
- dwengo_loki_data:/loki
|
||||||
|
|
||||||
dashboards:
|
dashboards:
|
||||||
image: grafana/grafana:latest
|
image: grafana/grafana:latest
|
||||||
ports:
|
ports:
|
||||||
|
@ -70,3 +88,5 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dwengo_grafana_data:
|
dwengo_grafana_data:
|
||||||
|
dwengo_loki_data:
|
||||||
|
dwengo_postgres_data:
|
12
compose.yml
12
compose.yml
|
@ -36,17 +36,5 @@ services:
|
||||||
KC_HEALTH_ENABLED: 'true'
|
KC_HEALTH_ENABLED: 'true'
|
||||||
KC_LOG_LEVEL: info
|
KC_LOG_LEVEL: info
|
||||||
|
|
||||||
logging:
|
|
||||||
image: grafana/loki:latest
|
|
||||||
ports:
|
|
||||||
- '9001:3102'
|
|
||||||
- '9095:9095'
|
|
||||||
command: -config.file=/etc/loki/config.yaml
|
|
||||||
restart: unless-stopped
|
|
||||||
volumes:
|
|
||||||
- ./config/loki/config.yml:/etc/loki/config.yaml
|
|
||||||
- dwengo_loki_data:/loki
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dwengo_loki_data:
|
|
||||||
dwengo_postgres_data:
|
dwengo_postgres_data:
|
||||||
|
|
|
@ -19,7 +19,16 @@ See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
## Project Setup
|
## Project Setup
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
# Install dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
|
# Start necessary services for development
|
||||||
|
cd ../ # Go to the root of the repository
|
||||||
|
docker compose up -d
|
||||||
|
# Start the backend
|
||||||
|
cd backend
|
||||||
|
cp .env.development.example .env.development.local
|
||||||
|
npm run dev # or npm run build && npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
### Compile and Hot-Reload for Development
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"npm-run-all2": "^7.0.2",
|
"npm-run-all2": "^7.0.2",
|
||||||
"typescript": "~5.7.3",
|
"typescript": "~5.7.3",
|
||||||
"vite": "^6.1.0",
|
"vite": "^6.1.2",
|
||||||
"vite-plugin-vue-devtools": "^7.7.2",
|
"vite-plugin-vue-devtools": "^7.7.2",
|
||||||
"vitest": "^3.0.5",
|
"vitest": "^3.0.5",
|
||||||
"vue-tsc": "^2.2.2"
|
"vue-tsc": "^2.2.2"
|
||||||
|
|
|
@ -1,11 +1,25 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import auth from "@/services/auth/auth-service.ts";
|
import auth from "@/services/auth/auth-service.ts";
|
||||||
|
import MenuBar from "@/components/MenuBar.vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
auth.loadUser();
|
auth.loadUser();
|
||||||
|
|
||||||
|
interface RouteMeta {
|
||||||
|
requiresAuth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showMenuBar = computed(() => (route.meta as RouteMeta).requiresAuth && auth.authState.user);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<router-view />
|
<menu-bar v-if="showMenuBar"></menu-bar>
|
||||||
|
<v-main>
|
||||||
|
<router-view />
|
||||||
|
</v-main>
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,47 +1,56 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ThemeCard from "@/components/ThemeCard.vue";
|
import ThemeCard from "@/components/ThemeCard.vue";
|
||||||
import { ref, watchEffect, computed } from "vue";
|
import { ref, watchEffect, computed } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { AGE_TO_THEMES, THEMESITEMS } from "@/utils/constants.ts";
|
import { AGE_TO_THEMES, THEMESITEMS } from "@/utils/constants.ts";
|
||||||
import { useThemeQuery } from "@/queries/themes.ts";
|
import { useThemeQuery } from "@/queries/themes.ts";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
selectedTheme: { type: String, required: true },
|
selectedTheme: { type: String, required: true },
|
||||||
selectedAge: { type: String, required: true }
|
selectedAge: { type: String, required: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const language = computed(() => locale.value);
|
const language = computed(() => locale.value);
|
||||||
|
|
||||||
const { data: allThemes, isLoading, error } = useThemeQuery(language);
|
const { data: allThemes, isLoading, error } = useThemeQuery(language);
|
||||||
|
|
||||||
const allCards = ref([]);
|
const allCards = ref([]);
|
||||||
const cards = ref([]);
|
const cards = ref([]);
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
const themes = allThemes.value ?? [];
|
const themes = allThemes.value ?? [];
|
||||||
allCards.value = themes;
|
allCards.value = themes;
|
||||||
|
|
||||||
if (props.selectedTheme) {
|
if (props.selectedTheme) {
|
||||||
cards.value = themes.filter((theme) =>
|
cards.value = themes.filter(
|
||||||
THEMESITEMS[props.selectedTheme]?.includes(theme.key) &&
|
(theme) =>
|
||||||
AGE_TO_THEMES[props.selectedAge]?.includes(theme.key)
|
THEMESITEMS[props.selectedTheme]?.includes(theme.key) &&
|
||||||
);
|
AGE_TO_THEMES[props.selectedAge]?.includes(theme.key),
|
||||||
} else {
|
);
|
||||||
cards.value = themes;
|
} else {
|
||||||
}
|
cards.value = themes;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<div v-if="isLoading" class="text-center py-10">
|
<div
|
||||||
<v-progress-circular indeterminate color="primary" />
|
v-if="isLoading"
|
||||||
|
class="text-center py-10"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</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>
|
<v-icon large>mdi-alert-circle</v-icon>
|
||||||
<p>Error loading: {{ error.message }}</p>
|
<p>Error loading: {{ error.message }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
{ name: "English", code: "en" },
|
{ name: "English", code: "en" },
|
||||||
{ name: "Nederlands", code: "nl" },
|
{ name: "Nederlands", code: "nl" },
|
||||||
{ name: "Français", code: "fr" },
|
{ 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
|
// Logic to change the language of the website to the selected language
|
||||||
|
@ -31,79 +31,95 @@
|
||||||
localStorage.setItem("user-lang", langCode);
|
localStorage.setItem("user-lang", langCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
// contains functionality to let the collapsed menu appear and disappear
|
// Contains functionality to let the collapsed menu appear and disappear
|
||||||
// when the screen size varies
|
// When the screen size varies
|
||||||
const drawer = ref(false);
|
const drawer = ref(false);
|
||||||
|
|
||||||
// when the user wants to logout, a popup is shown to verify this
|
// When the user wants to logout, a popup is shown to verify this
|
||||||
// if verified, the user should be logged out
|
// If verified, the user should be logged out
|
||||||
const performLogout = () => {
|
const performLogout = () => {
|
||||||
auth.logout();
|
auth.logout();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<v-app-bar
|
||||||
<v-app class="menu_collapsed">
|
class="app-bar"
|
||||||
<v-app-bar
|
app
|
||||||
app
|
>
|
||||||
style="background-color: #f6faf2"
|
<v-app-bar-nav-icon
|
||||||
|
class="menu_collapsed"
|
||||||
|
@click="drawer = !drawer"
|
||||||
|
/>
|
||||||
|
<router-link
|
||||||
|
to="/user"
|
||||||
|
class="dwengo_home"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
class="dwengo_logo"
|
||||||
|
alt="Dwengo logo"
|
||||||
|
:src="dwengoLogo"
|
||||||
|
/>
|
||||||
|
<p class="caption">
|
||||||
|
{{ t(`${role}`) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
<v-toolbar-items class="menu">
|
||||||
|
<v-btn
|
||||||
|
class="menu_item"
|
||||||
|
variant="text"
|
||||||
|
to="/user/assignment"
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
{{ t("assignments") }}
|
||||||
<v-app-bar-nav-icon @click="drawer = !drawer" />
|
</v-btn>
|
||||||
</template>
|
<v-btn
|
||||||
|
class="menu_item"
|
||||||
<v-app-bar-title>
|
variant="text"
|
||||||
<router-link
|
to="/user/class"
|
||||||
to="/user"
|
>
|
||||||
class="dwengo_home"
|
{{ t("classes") }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
class="menu_item"
|
||||||
|
variant="text"
|
||||||
|
to="/user/discussion"
|
||||||
|
>
|
||||||
|
{{ t("discussions") }}
|
||||||
|
</v-btn>
|
||||||
|
<v-menu open-on-hover>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
>
|
>
|
||||||
<div>
|
<v-icon
|
||||||
<img
|
icon="mdi-translate"
|
||||||
class="dwengo_logo"
|
size="small"
|
||||||
:src="dwengoLogo"
|
color="#0e6942"
|
||||||
style="width: 100px"
|
></v-icon>
|
||||||
/>
|
</v-btn>
|
||||||
<p
|
</template>
|
||||||
class="caption"
|
<v-list>
|
||||||
style="font-size: smaller"
|
<v-list-item
|
||||||
>
|
v-for="(language, index) in languages"
|
||||||
{{ t(`${role}`) }}
|
:key="index"
|
||||||
</p>
|
@click="changeLanguage(language.code)"
|
||||||
</div>
|
>
|
||||||
</router-link>
|
<v-list-item-title>{{ language.name }}</v-list-item-title>
|
||||||
</v-app-bar-title>
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
<v-spacer></v-spacer>
|
</v-menu>
|
||||||
|
</v-toolbar-items>
|
||||||
<v-menu open-on-hover>
|
<v-spacer></v-spacer>
|
||||||
<template v-slot:activator="{ props }">
|
<v-dialog max-width="500">
|
||||||
<v-btn
|
<template v-slot:activator="{ props: activatorProps }">
|
||||||
v-bind="props"
|
|
||||||
icon
|
|
||||||
variant="text"
|
|
||||||
>
|
|
||||||
<v-icon
|
|
||||||
icon="mdi-translate"
|
|
||||||
size="small"
|
|
||||||
color="#0e6942"
|
|
||||||
></v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item
|
|
||||||
v-for="(language, index) in languages"
|
|
||||||
:key="index"
|
|
||||||
@click="changeLanguage(language.code)"
|
|
||||||
>
|
|
||||||
<v-list-item-title>{{ language.name }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
@click="performLogout"
|
v-bind="activatorProps"
|
||||||
text
|
:rounded="true"
|
||||||
|
variant="text"
|
||||||
>
|
>
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
:text="t('logout')"
|
:text="t('logout')"
|
||||||
|
@ -115,200 +131,81 @@
|
||||||
icon="mdi-logout"
|
icon="mdi-logout"
|
||||||
size="x-large"
|
size="x-large"
|
||||||
color="#0e6942"
|
color="#0e6942"
|
||||||
/>
|
>
|
||||||
|
</v-icon>
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-app-bar>
|
</template>
|
||||||
|
|
||||||
<v-navigation-drawer
|
<template v-slot:default="{ isActive }">
|
||||||
v-model="drawer"
|
<v-card :title="t('logoutVerification')">
|
||||||
app
|
<v-card-actions>
|
||||||
>
|
<v-spacer></v-spacer>
|
||||||
<v-list>
|
|
||||||
<v-list-item
|
|
||||||
to="/user/assignment"
|
|
||||||
link
|
|
||||||
>
|
|
||||||
<v-list-item-content>
|
|
||||||
<v-list-item-title class="menu_item">{{ t("assignments") }}</v-list-item-title>
|
|
||||||
</v-list-item-content>
|
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-list-item
|
<v-btn
|
||||||
to="/user/class"
|
:text="t('cancel')"
|
||||||
link
|
@click="isActive.value = false"
|
||||||
>
|
></v-btn>
|
||||||
<v-list-item-content>
|
<v-btn
|
||||||
<v-list-item-title class="menu_item">{{ t("classes") }}</v-list-item-title>
|
|
||||||
</v-list-item-content>
|
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-list-item
|
|
||||||
to="/user/discussion"
|
|
||||||
link
|
|
||||||
>
|
|
||||||
<v-list-item-content>
|
|
||||||
<v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title>
|
|
||||||
</v-list-item-content>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-navigation-drawer>
|
|
||||||
</v-app>
|
|
||||||
|
|
||||||
<nav class="menu">
|
|
||||||
<div class="left">
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<router-link
|
|
||||||
to="/user"
|
|
||||||
class="dwengo_home"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="dwengo_logo"
|
|
||||||
:src="dwengoLogo"
|
|
||||||
/>
|
|
||||||
<p class="caption">
|
|
||||||
{{ t(`${role}`) }}
|
|
||||||
</p>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link
|
|
||||||
:to="`/user/assignment`"
|
|
||||||
class="menu_item"
|
|
||||||
>
|
|
||||||
{{ t("assignments") }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link
|
|
||||||
to="/user/class"
|
|
||||||
class="menu_item"
|
|
||||||
>{{ t("classes") }}</router-link
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<router-link
|
|
||||||
to="/user/discussion"
|
|
||||||
class="menu_item"
|
|
||||||
>{{ t("discussions") }}
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<v-menu open-on-hover>
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-btn
|
|
||||||
v-bind="props"
|
|
||||||
icon
|
|
||||||
variant="text"
|
|
||||||
>
|
|
||||||
<v-icon
|
|
||||||
icon="mdi-translate"
|
|
||||||
size="small"
|
|
||||||
color="#0e6942"
|
|
||||||
></v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item
|
|
||||||
v-for="(language, index) in languages"
|
|
||||||
:key="index"
|
|
||||||
@click="changeLanguage(language.code)"
|
|
||||||
>
|
|
||||||
<v-list-item-title>{{ language.name }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="right">
|
|
||||||
<li>
|
|
||||||
<!-- <v-btn
|
|
||||||
@click="performLogout"
|
|
||||||
to="/login"
|
|
||||||
style="background-color: transparent; box-shadow: none !important"
|
|
||||||
>
|
|
||||||
<v-tooltip
|
|
||||||
:text="t('logout')"
|
:text="t('logout')"
|
||||||
location="bottom"
|
@click="performLogout"
|
||||||
>
|
to="/login"
|
||||||
<template v-slot:activator="{ props }">
|
></v-btn>
|
||||||
<v-icon
|
</v-card-actions>
|
||||||
v-bind="props"
|
</v-card>
|
||||||
icon="mdi-logout"
|
</template>
|
||||||
size="x-large"
|
</v-dialog>
|
||||||
color="#0e6942"
|
<v-avatar
|
||||||
></v-icon>
|
size="large"
|
||||||
</template>
|
color="#0e6942"
|
||||||
</v-tooltip>
|
class="user-button"
|
||||||
</v-btn> -->
|
>{{ initials }}</v-avatar
|
||||||
<v-dialog max-width="500">
|
>
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
</v-app-bar>
|
||||||
<v-btn
|
<v-navigation-drawer
|
||||||
v-bind="activatorProps"
|
v-model="drawer"
|
||||||
style="background-color: transparent; box-shadow: none !important"
|
temporary
|
||||||
>
|
app
|
||||||
<v-tooltip
|
>
|
||||||
:text="t('logout')"
|
<v-list>
|
||||||
location="bottom"
|
<v-list-item
|
||||||
>
|
to="/user/assignment"
|
||||||
<template v-slot:activator="{ props }">
|
link
|
||||||
<v-icon
|
>
|
||||||
v-bind="props"
|
<v-list-item-title class="menu_item">{{ t("assignments") }}</v-list-item-title>
|
||||||
icon="mdi-logout"
|
</v-list-item>
|
||||||
size="x-large"
|
|
||||||
color="#0e6942"
|
|
||||||
>
|
|
||||||
</v-icon>
|
|
||||||
</template>
|
|
||||||
</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-slot:default="{ isActive }">
|
<v-list-item
|
||||||
<v-card :title="t('logoutVerification')">
|
to="/user/class"
|
||||||
<v-card-actions>
|
link
|
||||||
<v-spacer></v-spacer>
|
>
|
||||||
|
<v-list-item-title class="menu_item">{{ t("classes") }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
<v-btn
|
<v-list-item
|
||||||
:text="t('cancel')"
|
to="/user/discussion"
|
||||||
@click="isActive.value = false"
|
link
|
||||||
></v-btn>
|
>
|
||||||
<v-btn
|
<v-list-item-title class="menu_item">{{ t("discussions") }}</v-list-item-title>
|
||||||
:text="t('logout')"
|
</v-list-item>
|
||||||
@click="performLogout"
|
</v-list>
|
||||||
to="/login"
|
</v-navigation-drawer>
|
||||||
></v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
</v-dialog>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<v-avatar
|
|
||||||
size="large"
|
|
||||||
color="#0e6942"
|
|
||||||
style="font-size: large; font-weight: bold"
|
|
||||||
>{{ initials }}</v-avatar
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</main>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.app-bar {
|
||||||
|
background-color: #f6faf2;
|
||||||
|
}
|
||||||
.menu {
|
.menu {
|
||||||
background-color: #f6faf2;
|
background-color: #f6faf2;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
.user-button {
|
||||||
.right {
|
margin-right: 10px;
|
||||||
align-items: center;
|
font-size: large;
|
||||||
padding: 10px;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right li {
|
.right li {
|
||||||
|
@ -346,9 +243,24 @@
|
||||||
color: #0e6942;
|
color: #0e6942;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: large;
|
font-size: large;
|
||||||
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a.router-link-active {
|
@media (max-width: 700px) {
|
||||||
font-weight: bold;
|
.menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.caption {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
.dwengo_logo {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 701px) {
|
||||||
|
.menu_collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
path: string;
|
path: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
image: string;
|
image: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -31,7 +31,10 @@ defineProps<{
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text class="description flex-grow-1">{{ description }}</v-card-text>
|
<v-card-text class="description flex-grow-1">{{ description }}</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-btn :to="`theme/${path}`" variant="text">
|
<v-btn
|
||||||
|
:to="`theme/${path}`"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
{{ t("read-more") }}
|
{{ t("read-more") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
@ -39,36 +42,36 @@ defineProps<{
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.theme-card {
|
.theme-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-card:hover {
|
.theme-card:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.03);
|
background-color: rgba(0, 0, 0, 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-container {
|
.title-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-image {
|
.title-image {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {apiConfig} from "@/config.ts";
|
import { apiConfig } from "@/config.ts";
|
||||||
|
|
||||||
export class BaseController {
|
export class BaseController {
|
||||||
protected baseUrl: string;
|
protected baseUrl: string;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {ThemeController} from "@/controllers/themes.ts";
|
import { ThemeController } from "@/controllers/themes.ts";
|
||||||
|
|
||||||
export function controllerGetter<T>(Factory: new () => T): () => T {
|
export function controllerGetter<T>(Factory: new () => T): () => T {
|
||||||
let instance: T | undefined;
|
let instance: T | undefined;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {BaseController} from "@/controllers/base-controller.ts";
|
import { BaseController } from "@/controllers/base-controller.ts";
|
||||||
|
|
||||||
export class ThemeController extends BaseController {
|
export class ThemeController extends BaseController {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -1,25 +1,22 @@
|
||||||
import { useQuery } from '@tanstack/vue-query';
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import { getThemeController } from '@/controllers/controllers';
|
import { getThemeController } from "@/controllers/controllers";
|
||||||
import {type MaybeRefOrGetter, toValue} from "vue";
|
import { type MaybeRefOrGetter, toValue } from "vue";
|
||||||
|
|
||||||
const themeController = getThemeController();
|
const themeController = getThemeController();
|
||||||
|
|
||||||
export const useThemeQuery = (language: MaybeRefOrGetter<string>) => {
|
export const useThemeQuery = (language: MaybeRefOrGetter<string>) =>
|
||||||
return useQuery({
|
useQuery({
|
||||||
queryKey: ['themes', language],
|
queryKey: ["themes", language],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const lang = toValue(language);
|
const lang = toValue(language);
|
||||||
return themeController.getAll(lang);
|
return themeController.getAll(lang);
|
||||||
},
|
},
|
||||||
enabled: () => !!toValue(language),
|
enabled: () => Boolean(toValue(language)),
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export const useThemeHruidsQuery = (themeKey: string | null) => {
|
export const useThemeHruidsQuery = (themeKey: string | null) =>
|
||||||
return useQuery({
|
useQuery({
|
||||||
queryKey: ['theme-hruids', themeKey],
|
queryKey: ["theme-hruids", themeKey],
|
||||||
queryFn: () => themeController.getHruidsByKey(themeKey!),
|
queryFn: () => themeController.getHruidsByKey(themeKey!),
|
||||||
enabled: !!themeKey,
|
enabled: Boolean(themeKey),
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import MenuBar from "@/components/MenuBar.vue";
|
|
||||||
import SingleAssignment from "@/views/assignments/SingleAssignment.vue";
|
import SingleAssignment from "@/views/assignments/SingleAssignment.vue";
|
||||||
import SingleClass from "@/views/classes/SingleClass.vue";
|
import SingleClass from "@/views/classes/SingleClass.vue";
|
||||||
import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue";
|
import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue";
|
||||||
|
@ -41,7 +40,6 @@ const router = createRouter({
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/user",
|
path: "/user",
|
||||||
component: MenuBar,
|
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import apiClient from "@/services/api-client/api-client.ts";
|
import apiClient from "@/services/api-client/api-client.ts";
|
||||||
import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts";
|
import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts";
|
||||||
|
|
||||||
|
export const AUTH_CONFIG_ENDPOINT = "auth/config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the authentication configuration from the backend.
|
* Fetch the authentication configuration from the backend.
|
||||||
*/
|
*/
|
||||||
export async function loadAuthConfig() {
|
export async function loadAuthConfig() {
|
||||||
const authConfig = (await apiClient.get<FrontendAuthConfig>("auth/config")).data;
|
const authConfigResponse = await apiClient.get<FrontendAuthConfig>(AUTH_CONFIG_ENDPOINT);
|
||||||
|
const authConfig = authConfigResponse.data;
|
||||||
return {
|
return {
|
||||||
student: {
|
student: {
|
||||||
authority: authConfig.student.authority,
|
authority: authConfig.student.authority,
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import { computed, reactive } from "vue";
|
import { computed, reactive } from "vue";
|
||||||
import type { AuthState, Role, UserManagersForRoles } from "@/services/auth/auth.d.ts";
|
import type { AuthState, Role, UserManagersForRoles } from "@/services/auth/auth.d.ts";
|
||||||
import { User, UserManager } from "oidc-client-ts";
|
import { User, UserManager } from "oidc-client-ts";
|
||||||
import { loadAuthConfig } from "@/services/auth/auth-config-loader.ts";
|
import { AUTH_CONFIG_ENDPOINT, loadAuthConfig } from "@/services/auth/auth-config-loader.ts";
|
||||||
import authStorage from "./auth-storage.ts";
|
import authStorage from "./auth-storage.ts";
|
||||||
import { loginRoute } from "@/config.ts";
|
import { loginRoute } from "@/config.ts";
|
||||||
import apiClient from "@/services/api-client/api-client.ts";
|
import apiClient from "@/services/api-client/api-client.ts";
|
||||||
|
@ -108,7 +108,7 @@ async function logout(): Promise<void> {
|
||||||
apiClient.interceptors.request.use(
|
apiClient.interceptors.request.use(
|
||||||
async (reqConfig) => {
|
async (reqConfig) => {
|
||||||
const token = authState?.user?.access_token;
|
const token = authState?.user?.access_token;
|
||||||
if (token) {
|
if (token && reqConfig.url !== AUTH_CONFIG_ENDPOINT) {
|
||||||
reqConfig.headers.Authorization = `Bearer ${token}`;
|
reqConfig.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return reqConfig;
|
return reqConfig;
|
||||||
|
|
|
@ -1,37 +1,64 @@
|
||||||
export const THEMES_KEYS = [
|
export const THEMES_KEYS = [
|
||||||
"kiks", "art", "socialrobot", "agriculture", "wegostem",
|
"kiks",
|
||||||
"computational_thinking", "math_with_python", "python_programming",
|
"art",
|
||||||
"stem", "care", "chatbot", "physical_computing", "algorithms", "basics_ai"
|
"socialrobot",
|
||||||
|
"agriculture",
|
||||||
|
"wegostem",
|
||||||
|
"computational_thinking",
|
||||||
|
"math_with_python",
|
||||||
|
"python_programming",
|
||||||
|
"stem",
|
||||||
|
"care",
|
||||||
|
"chatbot",
|
||||||
|
"physical_computing",
|
||||||
|
"algorithms",
|
||||||
|
"basics_ai",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const THEMESITEMS: Record<string, string[]> = {
|
export const THEMESITEMS: Record<string, string[]> = {
|
||||||
"all": THEMES_KEYS,
|
all: THEMES_KEYS,
|
||||||
"culture": ["art", "wegostem", "chatbot"],
|
culture: ["art", "wegostem", "chatbot"],
|
||||||
"electricity-and-mechanics": ["socialrobot", "wegostem", "stem", "physical_computing"],
|
"electricity-and-mechanics": ["socialrobot", "wegostem", "stem", "physical_computing"],
|
||||||
"nature-and-climate": ["kiks", "agriculture"],
|
"nature-and-climate": ["kiks", "agriculture"],
|
||||||
"agriculture": ["agriculture"],
|
agriculture: ["agriculture"],
|
||||||
"society": ["kiks", "socialrobot", "care", "chatbot"],
|
society: ["kiks", "socialrobot", "care", "chatbot"],
|
||||||
"math": ["kiks", "math_with_python", "python_programming", "stem", "care", "basics_ai"],
|
math: ["kiks", "math_with_python", "python_programming", "stem", "care", "basics_ai"],
|
||||||
"technology": ["socialrobot", "wegostem", "computational_thinking", "stem", "physical_computing", "basics_ai"],
|
technology: ["socialrobot", "wegostem", "computational_thinking", "stem", "physical_computing", "basics_ai"],
|
||||||
"algorithms": ["math_with_python", "python_programming", "stem", "algorithms", "basics_ai"],
|
algorithms: ["math_with_python", "python_programming", "stem", "algorithms", "basics_ai"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AGEITEMS = [
|
export const AGEITEMS = ["all", "primary-school", "lower-secondary", "upper-secondary", "high-school", "older"];
|
||||||
"all", "primary-school", "lower-secondary", "upper-secondary", "high-school", "older"
|
|
||||||
];
|
|
||||||
|
|
||||||
export const AGE_TO_THEMES: Record<string, string[]> = {
|
export const AGE_TO_THEMES: Record<string, string[]> = {
|
||||||
"all": THEMES_KEYS,
|
all: THEMES_KEYS,
|
||||||
"primary-school": ["wegostem", "computational_thinking", "physical_computing"],
|
"primary-school": ["wegostem", "computational_thinking", "physical_computing"],
|
||||||
"lower-secondary": ["socialrobot", "art", "wegostem", "computational_thinking", "physical_computing"],
|
"lower-secondary": ["socialrobot", "art", "wegostem", "computational_thinking", "physical_computing"],
|
||||||
"upper-secondary": ["kiks", "art", "socialrobot", "agriculture",
|
"upper-secondary": [
|
||||||
"computational_thinking", "math_with_python", "python_programming",
|
"kiks",
|
||||||
"stem", "care", "chatbot", "algorithms", "basics_ai"],
|
"art",
|
||||||
"high-school": [
|
"socialrobot",
|
||||||
"kiks", "art", "agriculture", "computational_thinking", "math_with_python", "python_programming",
|
"agriculture",
|
||||||
"stem", "care", "chatbot", "algorithms", "basics_ai"
|
"computational_thinking",
|
||||||
|
"math_with_python",
|
||||||
|
"python_programming",
|
||||||
|
"stem",
|
||||||
|
"care",
|
||||||
|
"chatbot",
|
||||||
|
"algorithms",
|
||||||
|
"basics_ai",
|
||||||
],
|
],
|
||||||
"older": [
|
"high-school": [
|
||||||
"kiks", "computational_thinking", "algorithms", "basics_ai"
|
"kiks",
|
||||||
]
|
"art",
|
||||||
|
"agriculture",
|
||||||
|
"computational_thinking",
|
||||||
|
"math_with_python",
|
||||||
|
"python_programming",
|
||||||
|
"stem",
|
||||||
|
"care",
|
||||||
|
"chatbot",
|
||||||
|
"algorithms",
|
||||||
|
"basics_ai",
|
||||||
|
],
|
||||||
|
older: ["kiks", "computational_thinking", "algorithms", "basics_ai"],
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await auth.handleLoginCallback();
|
await auth.handleLoginCallback();
|
||||||
await router.replace("/"); // Redirect to home (or dashboard)
|
await router.replace("/user"); // Redirect to theme page
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("OIDC callback error:", error);
|
console.error("OIDC callback error:", error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,9 +27,10 @@
|
||||||
<div class="container_left">
|
<div class="container_left">
|
||||||
<img
|
<img
|
||||||
:src="dwengoLogo"
|
:src="dwengoLogo"
|
||||||
|
alt="Dwengo logo"
|
||||||
style="align-self: center"
|
style="align-self: center"
|
||||||
/>
|
/>
|
||||||
<h> {{ t("homeTitle") }}</h>
|
<h1>{{ t("homeTitle") }}</h1>
|
||||||
<p class="info">
|
<p class="info">
|
||||||
{{ t("homeIntroduction1") }}
|
{{ t("homeIntroduction1") }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -57,7 +58,7 @@
|
||||||
width="125"
|
width="125"
|
||||||
src="/assets/home/innovative.png"
|
src="/assets/home/innovative.png"
|
||||||
></v-img>
|
></v-img>
|
||||||
<h class="big">{{ t("innovative") }}</h>
|
<h2 class="big">{{ t("innovative") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="img_small">
|
<div class="img_small">
|
||||||
<v-img
|
<v-img
|
||||||
|
@ -65,7 +66,7 @@
|
||||||
width="125"
|
width="125"
|
||||||
src="/assets/home/research_based.png"
|
src="/assets/home/research_based.png"
|
||||||
></v-img>
|
></v-img>
|
||||||
<h class="big">{{ t("researchBased") }}</h>
|
<h2 class="big">{{ t("researchBased") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="img_small">
|
<div class="img_small">
|
||||||
<v-img
|
<v-img
|
||||||
|
@ -73,7 +74,7 @@
|
||||||
width="125"
|
width="125"
|
||||||
src="/assets/home/inclusive.png"
|
src="/assets/home/inclusive.png"
|
||||||
></v-img>
|
></v-img>
|
||||||
<h class="big">{{ t("sociallyRelevant") }}</h>
|
<h2 class="big">{{ t("sociallyRelevant") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="img_small">
|
<div class="img_small">
|
||||||
<v-img
|
<v-img
|
||||||
|
@ -81,7 +82,7 @@
|
||||||
width="125"
|
width="125"
|
||||||
src="/assets/home/socially_relevant.png"
|
src="/assets/home/socially_relevant.png"
|
||||||
></v-img>
|
></v-img>
|
||||||
<h class="big">{{ t("inclusive") }}</h>
|
<h2 class="big">{{ t("inclusive") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container_right">
|
<div class="container_right">
|
||||||
|
@ -160,7 +161,7 @@
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h {
|
h2 {
|
||||||
font-size: large;
|
font-size: large;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main></main>
|
<main></main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, watch} from "vue";
|
import { ref, watch } from "vue";
|
||||||
import {useI18n} from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import {THEMESITEMS, AGE_TO_THEMES} from "@/utils/constants.ts";
|
import { THEMESITEMS, AGE_TO_THEMES } from "@/utils/constants.ts";
|
||||||
import BrowseThemes from "@/components/BrowseThemes.vue";
|
import BrowseThemes from "@/components/BrowseThemes.vue";
|
||||||
|
|
||||||
const {t, locale} = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
const selectedThemeKey = ref<string>('all');
|
const selectedThemeKey = ref<string>("all");
|
||||||
const selectedAgeKey = ref<string>('all');
|
const selectedAgeKey = ref<string>("all");
|
||||||
|
|
||||||
const allThemes = ref(Object.keys(THEMESITEMS));
|
const allThemes = ref(Object.keys(THEMESITEMS));
|
||||||
const availableThemes = ref([...allThemes.value]);
|
const availableThemes = ref([...allThemes.value]);
|
||||||
|
@ -17,18 +17,17 @@ import {ref, watch} from "vue";
|
||||||
|
|
||||||
// Reset selection when language changes
|
// Reset selection when language changes
|
||||||
watch(locale, () => {
|
watch(locale, () => {
|
||||||
selectedThemeKey.value = 'all';
|
selectedThemeKey.value = "all";
|
||||||
selectedAgeKey.value = 'all';
|
selectedAgeKey.value = "all";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
watch(selectedThemeKey, () => {
|
watch(selectedThemeKey, () => {
|
||||||
if (selectedThemeKey.value === "all") {
|
if (selectedThemeKey.value === "all") {
|
||||||
availableAges.value = [...allAges.value]; // Reset to all ages
|
availableAges.value = [...allAges.value]; // Reset to all ages
|
||||||
} else {
|
} else {
|
||||||
const themes = THEMESITEMS[selectedThemeKey.value];
|
const themes = THEMESITEMS[selectedThemeKey.value];
|
||||||
availableAges.value = allAges.value.filter(age =>
|
availableAges.value = allAges.value.filter((age) =>
|
||||||
AGE_TO_THEMES[age]?.some(theme => themes.includes(theme))
|
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
|
availableThemes.value = [...allThemes.value]; // Reset to all themes
|
||||||
} else {
|
} else {
|
||||||
const themes = AGE_TO_THEMES[selectedAgeKey.value];
|
const themes = AGE_TO_THEMES[selectedAgeKey.value];
|
||||||
availableThemes.value = allThemes.value.filter(theme =>
|
availableThemes.value = allThemes.value.filter((theme) =>
|
||||||
THEMESITEMS[theme]?.some(theme => themes.includes(theme))
|
THEMESITEMS[theme]?.some((theme) => themes.includes(theme)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<h1 class="title">{{ t("themes") }}</h1>
|
<h1 class="title">{{ t("themes") }}</h1>
|
||||||
<v-container class="dropdowns">
|
<v-container class="dropdowns">
|
||||||
<v-select class="v-select"
|
<v-select
|
||||||
:label="t('choose-theme')"
|
class="v-select"
|
||||||
:items="availableThemes.map(theme => ({ title: t(`theme-options.${theme}`), value: theme }))"
|
:label="t('choose-theme')"
|
||||||
v-model="selectedThemeKey"
|
:items="availableThemes.map((theme) => ({ title: t(`theme-options.${theme}`), value: theme }))"
|
||||||
item-title="title"
|
v-model="selectedThemeKey"
|
||||||
item-value="value"
|
item-title="title"
|
||||||
variant="outlined"
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<v-select
|
<v-select
|
||||||
class="v-select"
|
class="v-select"
|
||||||
:label="t('choose-age')"
|
: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"
|
v-model="selectedAgeKey"
|
||||||
item-title="label"
|
item-title="label"
|
||||||
item-value="key"
|
item-value="key"
|
||||||
|
@ -71,55 +69,55 @@ import {ref, watch} from "vue";
|
||||||
></v-select>
|
></v-select>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<BrowseThemes :selectedTheme="selectedThemeKey ?? ''" :selectedAge="selectedAgeKey ?? ''"/>
|
<BrowseThemes
|
||||||
|
:selectedTheme="selectedThemeKey ?? ''"
|
||||||
|
:selectedAge="selectedAgeKey ?? ''"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
min-width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
max-width: 50rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.dropdowns {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 5rem;
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-select {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.main-container {
|
.main-container {
|
||||||
padding: 1rem;
|
min-height: 100vh;
|
||||||
}
|
min-width: 100vw;
|
||||||
}
|
display: flex;
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.dropdowns {
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
max-width: 50rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdowns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 5rem;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
.v-select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.dropdowns {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -118,7 +118,7 @@
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"npm-run-all2": "^7.0.2",
|
"npm-run-all2": "^7.0.2",
|
||||||
"typescript": "~5.7.3",
|
"typescript": "~5.7.3",
|
||||||
"vite": "^6.1.0",
|
"vite": "^6.1.2",
|
||||||
"vite-plugin-vue-devtools": "^7.7.2",
|
"vite-plugin-vue-devtools": "^7.7.2",
|
||||||
"vitest": "^3.0.5",
|
"vitest": "^3.0.5",
|
||||||
"vue-tsc": "^2.2.2"
|
"vue-tsc": "^2.2.2"
|
||||||
|
@ -9821,7 +9821,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.1.1",
|
"version": "6.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.1.2.tgz",
|
||||||
|
"integrity": "sha512-EiXfDyO/uNKhYOSlZ6+9qBz4H46A8Lr07pyjmb88KTbJ+xkXvnqtxvgtg2VxPU6Kfj8Ep0un9JLqdrCWLqIanw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue