Merge pull request #219 from SELab-2/release/0.2.0
release 0.2.0: Milestone 2
This commit is contained in:
commit
ed5b49e5cf
390 changed files with 16470 additions and 7215 deletions
63
.github/workflows/backend-testing.yml
vendored
Normal file
63
.github/workflows/backend-testing.yml
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# This workflow will do a clean installation of node dependencies, cache/restore them, run backend tests across different versions of node (here 22.x)
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||||
|
|
||||||
|
name: Backend Testing
|
||||||
|
|
||||||
|
# Workflow runs when:
|
||||||
|
# - a backend js/ts file on "dev" changes
|
||||||
|
# - a non-draft PR to "dev" with backend js/ts files is opened, is reopened, or changes
|
||||||
|
# - a draft PR to "dev" with backend js/ts files is marked as ready for review
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "dev", "main" ]
|
||||||
|
paths:
|
||||||
|
- 'backend/src/**.[jt]s'
|
||||||
|
- 'backend/tests/**.[jt]s'
|
||||||
|
- 'backend/vitest.config.ts'
|
||||||
|
pull_request:
|
||||||
|
branches: [ "dev", "main" ]
|
||||||
|
types: ["synchronize", "ready_for_review", "opened", "reopened"]
|
||||||
|
paths:
|
||||||
|
- 'backend/src/**.[jt]s'
|
||||||
|
- 'backend/tests/**.[jt]s'
|
||||||
|
- 'backend/vitest.config.ts'
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Run backend unit tests
|
||||||
|
if: '! github.event.pull_request.draft'
|
||||||
|
runs-on: [self-hosted, Linux, X64]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
# Required to checkout the code
|
||||||
|
contents: read
|
||||||
|
# Required to put a comment into the pull-request
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [22.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
- run: npm run test:coverage -w backend
|
||||||
|
- name: 'Report Backend Coverage'
|
||||||
|
# Set if: always() to also generate the report if tests are failing
|
||||||
|
# Only works if you set `reportOnFailure: true` in your vite config as specified above
|
||||||
|
if: always()
|
||||||
|
uses: davelosert/vitest-coverage-report-action@v2
|
||||||
|
with:
|
||||||
|
name: 'Backend'
|
||||||
|
json-summary-path: './backend/coverage/coverage-summary.json'
|
||||||
|
json-final-path: './backend/coverage/coverage-final.json'
|
||||||
|
vite-config-path: './backend/vitest.config.ts'
|
||||||
|
file-coverage-mode: all
|
||||||
22
.github/workflows/deployment.yml
vendored
Normal file
22
.github/workflows/deployment.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: Deployment
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
name: Deploy with docker
|
||||||
|
runs-on: [self-hosted, Linux, X64]
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Copy environment variables to correct file
|
||||||
|
run: cp /home/dev/.backend.env backend/.env
|
||||||
|
-
|
||||||
|
name: Start docker
|
||||||
|
run: docker compose -f compose.production.yml up --build -d
|
||||||
|
|
||||||
72
.github/workflows/frontend-testing.yml
vendored
Normal file
72
.github/workflows/frontend-testing.yml
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# This workflow will do a clean installation of node dependencies, cache/restore them, run frontend tests across different versions of node (here 22.x)
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||||
|
|
||||||
|
name: Frontend Testing
|
||||||
|
|
||||||
|
# Workflow runs when:
|
||||||
|
# - a frontend js/ts/vue/css file on "dev" changes
|
||||||
|
# - a non-draft PR to "dev" with frontend js/ts/vue/css files is opened, is reopened, or changes
|
||||||
|
# - a draft PR to "dev" with frontend js/ts/vue/css files is marked as ready for review
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "dev", "main" ]
|
||||||
|
paths:
|
||||||
|
- 'frontend/src/**.[jt]s'
|
||||||
|
- 'frontend/src/**.vue'
|
||||||
|
- 'frontend/src/**.css'
|
||||||
|
- 'frontend/tests/**.[jt]s'
|
||||||
|
- 'frontend/tests/**.vue'
|
||||||
|
- 'frontend/tests/**.css'
|
||||||
|
- 'frontend/vitest.config.ts'
|
||||||
|
- 'frontend/playwright.config.ts'
|
||||||
|
pull_request:
|
||||||
|
branches: [ "dev", "main" ]
|
||||||
|
types: ["synchronize", "ready_for_review", "opened", "reopened"]
|
||||||
|
paths:
|
||||||
|
- 'frontend/src/**.[jt]s'
|
||||||
|
- 'frontend/src/**.vue'
|
||||||
|
- 'frontend/src/**.css'
|
||||||
|
- 'frontend/tests/**.[jt]s'
|
||||||
|
- 'frontend/tests/**.vue'
|
||||||
|
- 'frontend/tests/**.css'
|
||||||
|
- 'frontend/vitest.config.ts'
|
||||||
|
- 'frontend/playwright.config.ts'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Run frontend unit tests
|
||||||
|
if: '! github.event.pull_request.draft'
|
||||||
|
runs-on: [self-hosted, Linux, X64]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
# Required to checkout the code
|
||||||
|
contents: read
|
||||||
|
# Required to put a comment into the pull-request
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [22.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
- run: npm run test:coverage -w frontend
|
||||||
|
- name: 'Report Frontend Coverage'
|
||||||
|
# Set if: always() to also generate the report if tests are failing
|
||||||
|
# Only works if you set `reportOnFailure: true` in your vite config as specified above
|
||||||
|
if: always()
|
||||||
|
uses: davelosert/vitest-coverage-report-action@v2
|
||||||
|
with:
|
||||||
|
name: 'Frontend'
|
||||||
|
json-summary-path: './frontend/coverage/coverage-summary.json'
|
||||||
|
json-final-path: './frontend/coverage/coverage-final.json'
|
||||||
|
vite-config-path: './frontend/vitest.config.ts'
|
||||||
|
file-coverage-mode: all
|
||||||
11
.github/workflows/lint-action.yml
vendored
11
.github/workflows/lint-action.yml
vendored
|
|
@ -4,13 +4,13 @@ on:
|
||||||
# Trigger the workflow on push or pull request,
|
# Trigger the workflow on push or pull request,
|
||||||
# but only for the main branch
|
# but only for the main branch
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [ "dev", "main" ]
|
||||||
- dev
|
|
||||||
# Replace pull_request with pull_request_target if you
|
# Replace pull_request with pull_request_target if you
|
||||||
# plan to use this action with forks, see the Limitations section
|
# plan to use this action with forks, see the Limitations section
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches: [ "dev", "main" ]
|
||||||
- dev
|
types: ["synchronize", "ready_for_review", "opened", "reopened"]
|
||||||
|
|
||||||
|
|
||||||
# Down scope as necessary via https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
|
# Down scope as necessary via https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -20,6 +20,7 @@ permissions:
|
||||||
jobs:
|
jobs:
|
||||||
run-linters:
|
run-linters:
|
||||||
name: Run linters
|
name: Run linters
|
||||||
|
if: '! github.event.pull_request.draft'
|
||||||
runs-on: [self-hosted, Linux, X64]
|
runs-on: [self-hosted, Linux, X64]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -40,6 +41,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
auto_fix: true
|
auto_fix: true
|
||||||
eslint: true
|
eslint: true
|
||||||
eslint_args: '--config eslint.config.ts'
|
eslint_args: "--config eslint.config.ts --ignore-pattern '**/prettier.config.js'"
|
||||||
prettier: true
|
prettier: true
|
||||||
commit_message: 'style: fix linting issues met ${linter}'
|
commit_message: 'style: fix linting issues met ${linter}'
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -737,4 +737,6 @@ flycheck_*.el
|
||||||
# network security
|
# network security
|
||||||
/network-security.data
|
/network-security.data
|
||||||
|
|
||||||
|
docs/.venv
|
||||||
|
idp_data/h2/keycloakdb.mv.db
|
||||||
|
idp_data/h2/keycloakdb.trace.db
|
||||||
|
|
|
||||||
21
README.md
21
README.md
|
|
@ -21,31 +21,28 @@ 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
|
||||||
git clone https://github.com/SELab-2/Dwengo-1.git
|
git clone https://github.com/SELab-2/Dwengo-1.git
|
||||||
cd Dwengo-1/backend
|
docker compose -f compose.staging.yml up --build
|
||||||
cp .env.example .env
|
# Gebruikt backend/.env.staging
|
||||||
# Pas .env aan
|
|
||||||
nano .env
|
|
||||||
cd ..
|
|
||||||
docker compose up
|
|
||||||
# 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
|
||||||
|
|
|
||||||
21
backend/.env.staging
Normal file
21
backend/.env.staging
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
PORT=3000
|
||||||
|
DWENGO_DB_HOST=db
|
||||||
|
DWENGO_DB_PORT=5432
|
||||||
|
DWENGO_DB_USERNAME=postgres
|
||||||
|
DWENGO_DB_PASSWORD=postgres
|
||||||
|
DWENGO_DB_UPDATE=false
|
||||||
|
|
||||||
|
DWENGO_AUTH_STUDENT_URL=http://localhost/idp/realms/student
|
||||||
|
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||||
|
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://idp:7080/idp/realms/student/protocol/openid-connect/certs
|
||||||
|
DWENGO_AUTH_TEACHER_URL=http://localhost/idp/realms/teacher
|
||||||
|
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
||||||
|
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://idp:7080/idp/realms/teacher/protocol/openid-connect/certs
|
||||||
|
|
||||||
|
# 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/,127.0.0.1:80,http://127.0.0.1,http://localhost:80,http://127.0.0.1:80,localhost
|
||||||
|
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost/*,http://idp:7080,https://idp:7080
|
||||||
|
|
||||||
|
# Logging and monitoring
|
||||||
|
|
||||||
|
LOKI_HOST=http://logging:3102
|
||||||
22
backend/.env.test
Normal file
22
backend/.env.test
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#
|
||||||
|
# Test environment configuration
|
||||||
|
#
|
||||||
|
# Should not need to be modified.
|
||||||
|
# See .env.example for more information.
|
||||||
|
#
|
||||||
|
|
||||||
|
### Dwengo ###
|
||||||
|
|
||||||
|
DWENGO_PORT=3000
|
||||||
|
|
||||||
|
DWENGO_DB_NAME=":memory:"
|
||||||
|
DWENGO_DB_UPDATE=true
|
||||||
|
|
||||||
|
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
|
||||||
|
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||||
|
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs
|
||||||
|
DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
|
||||||
|
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
||||||
|
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
|
||||||
|
|
||||||
|
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:9876,*
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
PORT=3000
|
|
||||||
DWENGO_DB_UPDATE=true
|
|
||||||
DWENGO_DB_NAME=":memory:"
|
|
||||||
|
|
@ -1,37 +1,51 @@
|
||||||
FROM node:22 AS build-stage
|
FROM node:22 AS build-stage
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app/dwengo
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
COPY backend/package.json ./backend/
|
COPY backend/package.json ./backend/
|
||||||
|
# Backend depends on common and docs
|
||||||
|
COPY common/package.json ./common/
|
||||||
|
COPY docs/package.json ./docs/
|
||||||
|
|
||||||
RUN npm install --silent
|
RUN npm install --silent
|
||||||
|
|
||||||
# Build the backend
|
# Build the backend
|
||||||
|
|
||||||
# Root tsconfig.json
|
# Root tsconfig.json
|
||||||
COPY tsconfig.json ./
|
COPY tsconfig.json tsconfig.build.json ./
|
||||||
|
|
||||||
WORKDIR /app/backend
|
COPY backend ./backend
|
||||||
|
COPY common ./common
|
||||||
COPY backend ./
|
COPY docs ./docs
|
||||||
COPY docs /app/docs
|
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:22 AS production-stage
|
FROM node:22 AS production-stage
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app/dwengo
|
||||||
|
|
||||||
COPY package-lock.json backend/package.json ./
|
# Copy static files
|
||||||
|
|
||||||
|
COPY ./backend/i18n ./i18n
|
||||||
|
|
||||||
|
# Copy built files
|
||||||
|
|
||||||
|
COPY --from=build-stage /app/dwengo/common/dist ./common/dist
|
||||||
|
COPY --from=build-stage /app/dwengo/backend/dist ./backend/dist
|
||||||
|
COPY --from=build-stage /app/dwengo/docs/api/swagger.json ./docs/api/swagger.json
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY backend/package.json ./backend/
|
||||||
|
# Backend depends on common
|
||||||
|
COPY common/package.json ./common/
|
||||||
|
|
||||||
RUN npm install --silent --only=production
|
RUN npm install --silent --only=production
|
||||||
|
|
||||||
COPY ./docs /docs
|
COPY ./backend/i18n ./backend/i18n
|
||||||
COPY --from=build-stage /app/backend/dist ./dist/
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["node", "--env-file=.env", "dist/app.js"]
|
CMD ["node", "--env-file=/app/dwengo/backend/.env", "/app/dwengo/backend/dist/app.js"]
|
||||||
|
|
|
||||||
|
|
@ -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,20 @@ Voer volgend commando uit om de unit tests uit te voeren:
|
||||||
npm run test:unit
|
npm run test:unit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Productie
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Omgevingsvariabelen
|
||||||
|
cp .env.example .env
|
||||||
|
# Configureer de .env file met de juiste waarden!
|
||||||
|
nano .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,7 +0,0 @@
|
||||||
// Can be placed in dotenv but found it redundant
|
|
||||||
// Import dotenv from "dotenv";
|
|
||||||
// Load .env file
|
|
||||||
// Dotenv.config();
|
|
||||||
export const DWENGO_API_BASE = 'https://dwengo.org/backend/api';
|
|
||||||
export const FALLBACK_LANG = 'nl';
|
|
||||||
export const FALLBACK_SEQ_NUM = 1;
|
|
||||||
|
|
@ -8,14 +8,4 @@ export default [
|
||||||
globals: globals.node,
|
globals: globals.node,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
files: ['tests/**/*.ts'],
|
|
||||||
languageOptions: {
|
|
||||||
globals: globals.node,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'no-console': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@ curricula_page:
|
||||||
contact: ''
|
contact: ''
|
||||||
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
|
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
|
||||||
basics_ai:
|
basics_ai:
|
||||||
title: Basisprincipes van AI
|
title: Grundlagen der KI
|
||||||
sub_title: Basisprincipes van AI
|
sub_title: Grundlagen der KI
|
||||||
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.'
|
description: 'Dieses Thema bündelt verschiedene Aktivitäten, in denen die grundlegenden Prinzipien der künstlichen Intelligenz (KI) behandelt werden. Die Schüler lernen, was KI ist, wie sie funktioniert und wie sie in verschiedenen Bereichen angewendet werden kann.'
|
||||||
contact: ''
|
contact: ''
|
||||||
kiks:
|
kiks:
|
||||||
title: KI und Klima
|
title: KI und Klima
|
||||||
|
|
@ -28,10 +28,11 @@ curricula_page:
|
||||||
contact: ''
|
contact: ''
|
||||||
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
|
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
|
||||||
basics_ai:
|
basics_ai:
|
||||||
title: Basisprincipes van AI
|
title: Basics of AI
|
||||||
sub_title: Basisprincipes van AI
|
sub_title: Basics of AI
|
||||||
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.'
|
description: 'This theme brings together various activities covering the basic principles of Artificial Intelligence (AI). Students learn what AI is, how it works, and how it can be applied in different domains.'
|
||||||
contact: ''
|
contact: ''
|
||||||
|
|
||||||
kiks:
|
kiks:
|
||||||
title: AI and Climate
|
title: AI and Climate
|
||||||
sub_title: KIKS
|
sub_title: KIKS
|
||||||
|
|
@ -28,9 +28,9 @@ curricula_page:
|
||||||
contact: ''
|
contact: ''
|
||||||
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
|
teaser: https://www.youtube.com/embed/2B6gZ9HdQ1Y
|
||||||
basics_ai:
|
basics_ai:
|
||||||
title: Basisprincipes van AI
|
title: Principes de base de l’IA
|
||||||
sub_title: Basisprincipes van AI
|
sub_title: Principes de base de l’IA
|
||||||
description: 'Onder dit lesthema bundelen we verschillende activiteiten waarin de basisprincipes van artificiële intelligentie (AI) aan bod komen. Leerlingen leren wat AI is, hoe het werkt en hoe het kan worden toegepast in verschillende domeinen.'
|
description: 'Ce thème rassemble différentes activités portant sur les principes fondamentaux de l’intelligence artificielle (IA). Les élèves apprennent ce qu’est l’IA, comment elle fonctionne et comment elle peut être appliquée dans divers domaines.'
|
||||||
contact: ''
|
contact: ''
|
||||||
kiks:
|
kiks:
|
||||||
title: 'IA et changement climatique'
|
title: 'IA et changement climatique'
|
||||||
|
|
@ -1,24 +1,28 @@
|
||||||
{
|
{
|
||||||
"name": "dwengo-1-backend",
|
"name": "@dwengo-1/backend",
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"description": "Backend for Dwengo-1",
|
"description": "Backend for Dwengo-1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "dist/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env NODE_ENV=production tsc --project tsconfig.json",
|
"build": "cross-env NODE_ENV=production tsc --build",
|
||||||
"dev": "cross-env NODE_ENV=development tsx watch --env-file=.env.development.local src/app.ts",
|
"predev": "tsc --build ../common/tsconfig.json",
|
||||||
|
"dev": "cross-env NODE_ENV=development tsx tool/seed.ts && tsx watch --env-file=.env.development.local src/app.ts",
|
||||||
"start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js",
|
"start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js",
|
||||||
"format": "prettier --write src/",
|
"format": "prettier --write src/",
|
||||||
"format-check": "prettier --check src/",
|
"format-check": "prettier --check src/",
|
||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
"test:unit": "vitest"
|
"pretest:unit": "tsx ../docs/api/generate.ts && npm run build",
|
||||||
|
"test:unit": "vitest --run",
|
||||||
|
"test:coverage": "vitest --run --coverage.enabled true"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mikro-orm/core": "6.4.9",
|
"@mikro-orm/core": "6.4.12",
|
||||||
"@mikro-orm/knex": "6.4.9",
|
"@mikro-orm/knex": "6.4.12",
|
||||||
"@mikro-orm/postgresql": "6.4.9",
|
"@mikro-orm/postgresql": "6.4.12",
|
||||||
"@mikro-orm/reflection": "6.4.9",
|
"@mikro-orm/reflection": "6.4.12",
|
||||||
"@mikro-orm/sqlite": "6.4.9",
|
"@mikro-orm/sqlite": "6.4.12",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross": "^1.0.0",
|
"cross": "^1.0.0",
|
||||||
|
|
@ -40,7 +44,7 @@
|
||||||
"winston-loki": "^6.1.3"
|
"winston-loki": "^6.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mikro-orm/cli": "6.4.9",
|
"@mikro-orm/cli": "6.4.12",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,16 @@ import cors from './middleware/cors.js';
|
||||||
import { getLogger, Logger } from './logging/initalize.js';
|
import { getLogger, Logger } from './logging/initalize.js';
|
||||||
import { responseTimeLogger } from './logging/responseTimeLogger.js';
|
import { responseTimeLogger } from './logging/responseTimeLogger.js';
|
||||||
import responseTime from 'response-time';
|
import responseTime from 'response-time';
|
||||||
import { EnvVars, getNumericEnvVar } from './util/envvars.js';
|
import { envVars, getNumericEnvVar } from './util/envVars.js';
|
||||||
import apiRouter from './routes/router.js';
|
import apiRouter from './routes/router.js';
|
||||||
import swaggerMiddleware from './swagger.js';
|
import swaggerMiddleware from './swagger.js';
|
||||||
import swaggerUi from 'swagger-ui-express';
|
import swaggerUi from 'swagger-ui-express';
|
||||||
|
import { errorHandler } from './middleware/error-handling/error-handler.js';
|
||||||
|
|
||||||
const logger: Logger = getLogger();
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
const app: Express = express();
|
const app: Express = express();
|
||||||
const port: string | number = getNumericEnvVar(EnvVars.Port);
|
const port: string | number = getNumericEnvVar(envVars.Port);
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cors);
|
app.use(cors);
|
||||||
|
|
@ -26,7 +27,9 @@ app.use('/api', apiRouter);
|
||||||
// Swagger
|
// Swagger
|
||||||
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
|
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
|
||||||
|
|
||||||
async function startServer() {
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
async function startServer(): Promise<void> {
|
||||||
await initORM();
|
await initORM();
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
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;
|
||||||
|
export const FALLBACK_VERSION_NUM = 1;
|
||||||
|
|
|
||||||
99
backend/src/controllers/answers.ts
Normal file
99
backend/src/controllers/answers.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { requireFields } from './error-helper.js';
|
||||||
|
import { getLearningObjectId, getQuestionId } from './questions.js';
|
||||||
|
import { createAnswer, deleteAnswer, getAnswer, getAnswersByQuestion, updateAnswer } from '../services/answers.js';
|
||||||
|
import { FALLBACK_SEQ_NUM } from '../config.js';
|
||||||
|
import { AnswerData } from '@dwengo-1/common/interfaces/answer';
|
||||||
|
|
||||||
|
export async function getAllAnswersHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
const seq = req.params.seq;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
|
const questionId = getQuestionId(learningObjectId, seq);
|
||||||
|
|
||||||
|
const answers = await getAnswersByQuestion(questionId, full);
|
||||||
|
|
||||||
|
res.json({ answers });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAnswerHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
const seq = req.params.seq;
|
||||||
|
const seqAnswer = req.params.seqAnswer;
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
|
const questionId = getQuestionId(learningObjectId, seq);
|
||||||
|
|
||||||
|
const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM;
|
||||||
|
const answer = await getAnswer(questionId, sequenceNumber);
|
||||||
|
|
||||||
|
res.json({ answer });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAnswerHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
const seq = req.params.seq;
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
|
const questionId = getQuestionId(learningObjectId, seq);
|
||||||
|
|
||||||
|
const author = req.body.author as string;
|
||||||
|
const content = req.body.content as string;
|
||||||
|
requireFields({ author, content });
|
||||||
|
|
||||||
|
const answerData = req.body as AnswerData;
|
||||||
|
|
||||||
|
const answer = await createAnswer(questionId, answerData);
|
||||||
|
|
||||||
|
res.json({ answer });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAnswerHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
const seq = req.params.seq;
|
||||||
|
const seqAnswer = req.params.seqAnswer;
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
|
const questionId = getQuestionId(learningObjectId, seq);
|
||||||
|
|
||||||
|
const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM;
|
||||||
|
const answer = await deleteAnswer(questionId, sequenceNumber);
|
||||||
|
|
||||||
|
res.json({ answer });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAnswerHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
const seq = req.params.seq;
|
||||||
|
const seqAnswer = req.params.seqAnswer;
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
|
const questionId = getQuestionId(learningObjectId, seq);
|
||||||
|
|
||||||
|
const content = req.body.content as string;
|
||||||
|
requireFields({ content });
|
||||||
|
|
||||||
|
const answerData = req.body as AnswerData;
|
||||||
|
|
||||||
|
const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM;
|
||||||
|
const answer = await updateAnswer(questionId, sequenceNumber, answerData);
|
||||||
|
|
||||||
|
res.json({ answer });
|
||||||
|
}
|
||||||
|
|
@ -1,76 +1,93 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js';
|
import {
|
||||||
import { AssignmentDTO } from '../interfaces/assignment.js';
|
createAssignment,
|
||||||
|
deleteAssignment,
|
||||||
|
getAllAssignments,
|
||||||
|
getAssignment,
|
||||||
|
getAssignmentsQuestions,
|
||||||
|
getAssignmentsSubmissions,
|
||||||
|
putAssignment,
|
||||||
|
} from '../services/assignments.js';
|
||||||
|
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
|
||||||
|
import { requireFields } from './error-helper.js';
|
||||||
|
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||||
|
import { Assignment } from '../entities/assignments/assignment.entity.js';
|
||||||
|
import { EntityDTO } from '@mikro-orm/core';
|
||||||
|
|
||||||
// Typescript is annoy with with parameter forwarding from class.ts
|
function getAssignmentParams(req: Request): { classid: string; assignmentNumber: number; full: boolean } {
|
||||||
interface AssignmentParams {
|
|
||||||
classid: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllAssignmentsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
|
||||||
const classid = req.params.classid;
|
const classid = req.params.classid;
|
||||||
|
const assignmentNumber = Number(req.params.id);
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
|
requireFields({ assignmentNumber, classid });
|
||||||
const assignments = await getAllAssignments(classid, full);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
assignments: assignments,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
|
||||||
const classid = req.params.classid;
|
|
||||||
const assignmentData = req.body as AssignmentDTO;
|
|
||||||
|
|
||||||
if (!assignmentData.description || !assignmentData.language || !assignmentData.learningPath || !assignmentData.title) {
|
|
||||||
res.status(400).json({
|
|
||||||
error: 'Missing one or more required fields: title, description, learningPath, language',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assignment = await createAssignment(classid, assignmentData);
|
|
||||||
|
|
||||||
if (!assignment) {
|
|
||||||
res.status(500).json({ error: 'Could not create assignment ' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json({ assignment: assignment });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAssignmentHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
|
||||||
const id = +req.params.id;
|
|
||||||
const classid = req.params.classid;
|
|
||||||
|
|
||||||
if (isNaN(id)) {
|
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assignment = await getAssignment(classid, id);
|
|
||||||
|
|
||||||
if (!assignment) {
|
|
||||||
res.status(404).json({ error: 'Assignment not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(assignment);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAssignmentsSubmissionsHandler(req: Request<AssignmentParams>, res: Response): Promise<void> {
|
|
||||||
const classid = req.params.classid;
|
|
||||||
const assignmentNumber = +req.params.id;
|
|
||||||
|
|
||||||
if (isNaN(assignmentNumber)) {
|
if (isNaN(assignmentNumber)) {
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
throw new BadRequestException('Assignment id should be a number');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber);
|
return { classid, assignmentNumber, full };
|
||||||
|
}
|
||||||
res.json({
|
|
||||||
submissions: submissions,
|
export async function getAllAssignmentsHandler(req: Request, res: Response): Promise<void> {
|
||||||
});
|
const classId = req.params.classid;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
|
const assignments = await getAllAssignments(classId, full);
|
||||||
|
|
||||||
|
res.json({ assignments });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classid = req.params.classid;
|
||||||
|
const description = req.body.description;
|
||||||
|
const language = req.body.language;
|
||||||
|
const learningPath = req.body.learningPath;
|
||||||
|
const title = req.body.title;
|
||||||
|
|
||||||
|
requireFields({ description, language, learningPath, title });
|
||||||
|
|
||||||
|
const assignmentData = req.body as AssignmentDTO;
|
||||||
|
const assignment = await createAssignment(classid, assignmentData);
|
||||||
|
|
||||||
|
res.json({ assignment });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const { classid, assignmentNumber } = getAssignmentParams(req);
|
||||||
|
|
||||||
|
const assignment = await getAssignment(classid, assignmentNumber);
|
||||||
|
|
||||||
|
res.json({ assignment });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const { classid, assignmentNumber } = getAssignmentParams(req);
|
||||||
|
|
||||||
|
const assignmentData = req.body as Partial<EntityDTO<Assignment>>;
|
||||||
|
const assignment = await putAssignment(classid, assignmentNumber, assignmentData);
|
||||||
|
|
||||||
|
res.json({ assignment });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const { classid, assignmentNumber } = getAssignmentParams(req);
|
||||||
|
|
||||||
|
const assignment = await deleteAssignment(classid, assignmentNumber);
|
||||||
|
|
||||||
|
res.json({ assignment });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const { classid, assignmentNumber, full } = getAssignmentParams(req);
|
||||||
|
|
||||||
|
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full);
|
||||||
|
|
||||||
|
res.json({ submissions });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAssignmentQuestionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const { classid, assignmentNumber, full } = getAssignmentParams(req);
|
||||||
|
|
||||||
|
const questions = await getAssignmentsQuestions(classid, assignmentNumber, full);
|
||||||
|
|
||||||
|
res.json({ questions });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,62 @@
|
||||||
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
import { UnauthorizedException } from '../exceptions/unauthorized-exception.js';
|
||||||
|
import { getLogger } from '../logging/initalize.js';
|
||||||
|
import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js';
|
||||||
|
import { createOrUpdateStudent } from '../services/students.js';
|
||||||
|
import { createOrUpdateTeacher } from '../services/teachers.js';
|
||||||
|
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
type FrontendIdpConfig = {
|
interface FrontendIdpConfig {
|
||||||
authority: string;
|
authority: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
responseType: string;
|
responseType: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
type FrontendAuthConfig = {
|
interface FrontendAuthConfig {
|
||||||
student: FrontendIdpConfig;
|
student: FrontendIdpConfig;
|
||||||
teacher: FrontendIdpConfig;
|
teacher: FrontendIdpConfig;
|
||||||
};
|
}
|
||||||
|
|
||||||
const SCOPE = 'openid profile email';
|
const SCOPE = 'openid profile email';
|
||||||
const RESPONSE_TYPE = 'code';
|
const RESPONSE_TYPE = 'code';
|
||||||
|
|
||||||
|
const logger = getLogger();
|
||||||
|
|
||||||
export function getFrontendAuthConfig(): FrontendAuthConfig {
|
export function getFrontendAuthConfig(): FrontendAuthConfig {
|
||||||
return {
|
return {
|
||||||
student: {
|
student: {
|
||||||
authority: getEnvVar(EnvVars.IdpStudentUrl),
|
authority: getEnvVar(envVars.IdpStudentUrl),
|
||||||
clientId: getEnvVar(EnvVars.IdpStudentClientId),
|
clientId: getEnvVar(envVars.IdpStudentClientId),
|
||||||
scope: SCOPE,
|
scope: SCOPE,
|
||||||
responseType: RESPONSE_TYPE,
|
responseType: RESPONSE_TYPE,
|
||||||
},
|
},
|
||||||
teacher: {
|
teacher: {
|
||||||
authority: getEnvVar(EnvVars.IdpTeacherUrl),
|
authority: getEnvVar(envVars.IdpTeacherUrl),
|
||||||
clientId: getEnvVar(EnvVars.IdpTeacherClientId),
|
clientId: getEnvVar(envVars.IdpTeacherClientId),
|
||||||
scope: SCOPE,
|
scope: SCOPE,
|
||||||
responseType: RESPONSE_TYPE,
|
responseType: RESPONSE_TYPE,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||||
|
const auth = req.auth;
|
||||||
|
if (!auth) {
|
||||||
|
throw new UnauthorizedException('Cannot say hello when not authenticated.');
|
||||||
|
}
|
||||||
|
const userData = {
|
||||||
|
id: auth.username,
|
||||||
|
username: auth.username,
|
||||||
|
firstName: auth.firstName ?? '',
|
||||||
|
lastName: auth.lastName ?? '',
|
||||||
|
};
|
||||||
|
if (auth.accountType === 'student') {
|
||||||
|
await createOrUpdateStudent(userData);
|
||||||
|
logger.debug(`Synchronized student ${userData.username} with IDP`);
|
||||||
|
} else {
|
||||||
|
await createOrUpdateTeacher(userData);
|
||||||
|
logger.debug(`Synchronized teacher ${userData.username} with IDP`);
|
||||||
|
}
|
||||||
|
res.status(200).send({ message: 'Welcome!' });
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,132 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/class.js';
|
import {
|
||||||
import { ClassDTO } from '../interfaces/class.js';
|
addClassStudent,
|
||||||
|
addClassTeacher,
|
||||||
|
createClass,
|
||||||
|
deleteClass,
|
||||||
|
deleteClassStudent,
|
||||||
|
deleteClassTeacher,
|
||||||
|
getAllClasses,
|
||||||
|
getClass,
|
||||||
|
getClassStudents,
|
||||||
|
getClassTeacherInvitations,
|
||||||
|
getClassTeachers,
|
||||||
|
putClass,
|
||||||
|
} from '../services/classes.js';
|
||||||
|
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
|
||||||
|
import { requireFields } from './error-helper.js';
|
||||||
|
import { EntityDTO } from '@mikro-orm/core';
|
||||||
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
|
|
||||||
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
|
export async function getAllClassesHandler(req: Request, res: Response): Promise<void> {
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
const classes = await getAllClasses(full);
|
const classes = await getAllClasses(full);
|
||||||
|
|
||||||
res.json({
|
res.json({ classes });
|
||||||
classes: classes,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createClassHandler(req: Request, res: Response): Promise<void> {
|
export async function createClassHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const displayName = req.body.displayName;
|
||||||
|
requireFields({ displayName });
|
||||||
|
|
||||||
const classData = req.body as ClassDTO;
|
const classData = req.body as ClassDTO;
|
||||||
|
|
||||||
if (!classData.displayName) {
|
|
||||||
res.status(400).json({
|
|
||||||
error: 'Missing one or more required fields: displayName',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cls = await createClass(classData);
|
const cls = await createClass(classData);
|
||||||
|
|
||||||
if (!cls) {
|
res.json({ class: cls });
|
||||||
res.status(500).json({ error: 'Something went wrong while creating class' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json({ class: 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;
|
requireFields({ classId });
|
||||||
const cls = await getClass(classId);
|
|
||||||
|
|
||||||
if (!cls) {
|
const cls = await getClass(classId);
|
||||||
res.status(404).json({ error: 'Class not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cls.endpoints = {
|
|
||||||
self: `${req.baseUrl}/${req.params.id}`,
|
|
||||||
invitations: `${req.baseUrl}/${req.params.id}/invitations`,
|
|
||||||
assignments: `${req.baseUrl}/${req.params.id}/assignments`,
|
|
||||||
students: `${req.baseUrl}/${req.params.id}/students`,
|
|
||||||
};
|
|
||||||
|
|
||||||
res.json(cls);
|
res.json({ class: cls });
|
||||||
} catch (error) {
|
}
|
||||||
console.error('Error fetching learning objects:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
export async function putClassHandler(req: Request, res: Response): Promise<void> {
|
||||||
}
|
const classId = req.params.id;
|
||||||
|
requireFields({ classId });
|
||||||
|
|
||||||
|
const newData = req.body as Partial<EntityDTO<Class>>;
|
||||||
|
const cls = await putClass(classId, newData);
|
||||||
|
|
||||||
|
res.json({ class: cls });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteClassHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classId = req.params.id;
|
||||||
|
const cls = await deleteClass(classId);
|
||||||
|
|
||||||
|
res.json({ class: cls });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
|
export async function getClassStudentsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const classId = req.params.id;
|
const classId = req.params.id;
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
|
requireFields({ classId });
|
||||||
|
|
||||||
const students = full ? await getClassStudents(classId) : await getClassStudentsIds(classId);
|
const students = await getClassStudents(classId, full);
|
||||||
|
|
||||||
res.json({
|
res.json({ students });
|
||||||
students: students,
|
}
|
||||||
});
|
|
||||||
|
export async function getClassTeachersHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classId = req.params.id;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
requireFields({ classId });
|
||||||
|
|
||||||
|
const teachers = await getClassTeachers(classId, full);
|
||||||
|
|
||||||
|
res.json({ teachers });
|
||||||
}
|
}
|
||||||
|
|
||||||
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';
|
||||||
|
requireFields({ classId });
|
||||||
|
|
||||||
const invitations = await getClassTeacherInvitations(classId, full);
|
const invitations = await getClassTeacherInvitations(classId, full);
|
||||||
|
|
||||||
res.json({
|
res.json({ invitations });
|
||||||
invitations: invitations,
|
}
|
||||||
});
|
|
||||||
|
export async function deleteClassStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classId = req.params.id;
|
||||||
|
const username = req.params.username;
|
||||||
|
requireFields({ classId, username });
|
||||||
|
|
||||||
|
const cls = await deleteClassStudent(classId, username);
|
||||||
|
|
||||||
|
res.json({ class: cls });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteClassTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classId = req.params.id;
|
||||||
|
const username = req.params.username;
|
||||||
|
requireFields({ classId, username });
|
||||||
|
|
||||||
|
const cls = await deleteClassTeacher(classId, username);
|
||||||
|
|
||||||
|
res.json({ class: cls });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addClassStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classId = req.params.id;
|
||||||
|
const username = req.body.username;
|
||||||
|
requireFields({ classId, username });
|
||||||
|
|
||||||
|
const cls = await addClassStudent(classId, username);
|
||||||
|
|
||||||
|
res.json({ class: cls });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addClassTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classId = req.params.id;
|
||||||
|
const username = req.body.username;
|
||||||
|
requireFields({ classId, username });
|
||||||
|
|
||||||
|
const cls = await addClassTeacher(classId, username);
|
||||||
|
|
||||||
|
res.json({ class: cls });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
backend/src/controllers/error-helper.ts
Normal file
18
backend/src/controllers/error-helper.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for the presence of required fields and throws a BadRequestException
|
||||||
|
* if any are missing.
|
||||||
|
*
|
||||||
|
* @param fields - An object with key-value pairs to validate.
|
||||||
|
*/
|
||||||
|
export function requireFields(fields: Record<string, unknown>): void {
|
||||||
|
const missing = Object.entries(fields)
|
||||||
|
.filter(([_, value]) => value === undefined || value === null || value === '')
|
||||||
|
.map(([key]) => key);
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
const message = `Missing required field${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`;
|
||||||
|
throw new BadRequestException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,95 +1,120 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js';
|
import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupQuestions, getGroupSubmissions, putGroup } from '../services/groups.js';
|
||||||
import { GroupDTO } from '../interfaces/group.js';
|
import { GroupDTO } from '@dwengo-1/common/interfaces/group';
|
||||||
|
import { requireFields } from './error-helper.js';
|
||||||
|
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||||
|
|
||||||
// Typescript is annoywith with parameter forwarding from class.ts
|
function checkGroupFields(classId: string, assignmentId: number, groupId: number): void {
|
||||||
interface GroupParams {
|
requireFields({ classId, assignmentId, groupId });
|
||||||
classid: string;
|
|
||||||
assignmentid: string;
|
|
||||||
groupid?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getGroupHandler(req: Request<GroupParams>, res: Response): Promise<void> {
|
|
||||||
const classId = req.params.classid;
|
|
||||||
const full = req.query.full === 'true';
|
|
||||||
const assignmentId = +req.params.assignmentid;
|
|
||||||
|
|
||||||
if (isNaN(assignmentId)) {
|
if (isNaN(assignmentId)) {
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
throw new BadRequestException('Assignment id must be a number');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupId = +req.params.groupid!; // Can't be undefined
|
|
||||||
|
|
||||||
if (isNaN(groupId)) {
|
if (isNaN(groupId)) {
|
||||||
res.status(400).json({ error: 'Group id must be a number' });
|
throw new BadRequestException('Group id must be a number');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const group = await getGroup(classId, assignmentId, groupId, full);
|
export async function getGroupHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classId = req.params.classid;
|
||||||
|
const assignmentId = parseInt(req.params.assignmentid);
|
||||||
|
const groupId = parseInt(req.params.groupid);
|
||||||
|
checkGroupFields(classId, assignmentId, groupId);
|
||||||
|
|
||||||
res.json(group);
|
const group = await getGroup(classId, assignmentId, groupId);
|
||||||
|
|
||||||
|
res.json({ group });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putGroupHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classId = req.params.classid;
|
||||||
|
const assignmentId = parseInt(req.params.assignmentid);
|
||||||
|
const groupId = parseInt(req.params.groupid);
|
||||||
|
checkGroupFields(classId, assignmentId, groupId);
|
||||||
|
|
||||||
|
// Only members field can be changed
|
||||||
|
const members = req.body.members;
|
||||||
|
requireFields({ members });
|
||||||
|
|
||||||
|
const group = await putGroup(classId, assignmentId, groupId, { members } as Partial<GroupDTO>);
|
||||||
|
|
||||||
|
res.json({ group });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteGroupHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const classId = req.params.classid;
|
||||||
|
const assignmentId = parseInt(req.params.assignmentid);
|
||||||
|
const groupId = parseInt(req.params.groupid);
|
||||||
|
checkGroupFields(classId, assignmentId, groupId);
|
||||||
|
|
||||||
|
const group = await deleteGroup(classId, assignmentId, groupId);
|
||||||
|
|
||||||
|
res.json({ group });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllGroupsHandler(req: Request, res: Response): Promise<void> {
|
export async function getAllGroupsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const classId = req.params.classid;
|
const classId = req.params.classid;
|
||||||
|
const assignmentId = Number(req.params.assignmentid);
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
|
requireFields({ classId, assignmentId });
|
||||||
const assignmentId = +req.params.assignmentid;
|
|
||||||
|
|
||||||
if (isNaN(assignmentId)) {
|
if (isNaN(assignmentId)) {
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
throw new BadRequestException('Assignment id must be a number');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = await getAllGroups(classId, assignmentId, full);
|
const groups = await getAllGroups(classId, assignmentId, full);
|
||||||
|
|
||||||
res.json({
|
res.json({ groups });
|
||||||
groups: groups,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createGroupHandler(req: Request, res: Response): Promise<void> {
|
export async function createGroupHandler(req: Request, res: Response): Promise<void> {
|
||||||
const classid = req.params.classid;
|
const classid = req.params.classid;
|
||||||
const assignmentId = +req.params.assignmentid;
|
const assignmentId = Number(req.params.assignmentid);
|
||||||
|
const members = req.body.members;
|
||||||
|
requireFields({ classid, assignmentId, members });
|
||||||
|
|
||||||
if (isNaN(assignmentId)) {
|
if (isNaN(assignmentId)) {
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
throw new BadRequestException('Assignment id must be a number');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupData = req.body as GroupDTO;
|
const groupData = req.body as GroupDTO;
|
||||||
const group = await createGroup(groupData, classid, assignmentId);
|
const group = await createGroup(groupData, classid, assignmentId);
|
||||||
|
|
||||||
if (!group) {
|
res.status(201).json({ group });
|
||||||
res.status(500).json({ error: 'Something went wrong while creating group' });
|
}
|
||||||
return;
|
|
||||||
|
function getGroupParams(req: Request): { classId: string; assignmentId: number; groupId: number; full: boolean } {
|
||||||
|
const classId = req.params.classid;
|
||||||
|
const assignmentId = Number(req.params.assignmentid);
|
||||||
|
const groupId = Number(req.params.groupid);
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
|
||||||
|
requireFields({ classId, assignmentId, groupId });
|
||||||
|
|
||||||
|
if (isNaN(assignmentId)) {
|
||||||
|
throw new BadRequestException('Assignment id must be a number');
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ group: group });
|
if (isNaN(groupId)) {
|
||||||
|
throw new BadRequestException('Group id must be a number');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { classId, assignmentId, groupId, full };
|
||||||
}
|
}
|
||||||
|
|
||||||
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, assignmentId, groupId, full } = getGroupParams(req);
|
||||||
// Const full = req.query.full === 'true';
|
|
||||||
|
|
||||||
const assignmentId = +req.params.assignmentid;
|
const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full);
|
||||||
|
|
||||||
if (isNaN(assignmentId)) {
|
res.json({ submissions });
|
||||||
res.status(400).json({ error: 'Assignment id must be a number' });
|
}
|
||||||
return;
|
|
||||||
}
|
export async function getGroupQuestionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const { classId, assignmentId, groupId, full } = getGroupParams(req);
|
||||||
const groupId = +req.params.groupid!; // Can't be undefined
|
|
||||||
|
const questions = await getGroupQuestions(classId, assignmentId, groupId, full);
|
||||||
if (isNaN(groupId)) {
|
|
||||||
res.status(400).json({ error: 'Group id must be a number' });
|
res.json({ questions });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const submissions = await getGroupSubmissions(classId, assignmentId, groupId);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
submissions: submissions,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { FALLBACK_LANG } from '../config.js';
|
import { FALLBACK_LANG } from '../config.js';
|
||||||
import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js';
|
|
||||||
import learningObjectService from '../services/learning-objects/learning-object-service.js';
|
import learningObjectService from '../services/learning-objects/learning-object-service.js';
|
||||||
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
import { Language } from '../entities/content/language.js';
|
|
||||||
import { BadRequestException } from '../exceptions.js';
|
|
||||||
import attachmentService from '../services/learning-objects/attachment-service.js';
|
import attachmentService from '../services/learning-objects/attachment-service.js';
|
||||||
import { NotFoundError } from '@mikro-orm/core';
|
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||||
|
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||||
|
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||||
|
import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content';
|
||||||
|
|
||||||
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier {
|
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO {
|
||||||
if (!req.params.hruid) {
|
if (!req.params.hruid) {
|
||||||
throw new BadRequestException('HRUID is required.');
|
throw new BadRequestException('HRUID is required.');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
hruid: req.params.hruid as string,
|
hruid: req.params.hruid,
|
||||||
language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language,
|
language: (req.query.language || getEnvVar(envVars.FallbackLanguage)) as Language,
|
||||||
version: parseInt(req.query.version as string),
|
version: parseInt(req.query.version as string),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif
|
||||||
throw new BadRequestException('HRUID is required.');
|
throw new BadRequestException('HRUID is required.');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
hruid: req.params.hruid as string,
|
hruid: req.params.hruid,
|
||||||
language: (req.query.language as Language) || FALLBACK_LANG,
|
language: (req.query.language as Language) || FALLBACK_LANG,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -40,13 +40,18 @@ 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> {
|
||||||
const learningObjectId = getLearningObjectIdentifierFromRequest(req);
|
const learningObjectId = getLearningObjectIdentifierFromRequest(req);
|
||||||
|
|
||||||
const learningObject = await learningObjectService.getLearningObjectById(learningObjectId);
|
const learningObject = await learningObjectService.getLearningObjectById(learningObjectId);
|
||||||
|
|
||||||
|
if (!learningObject) {
|
||||||
|
throw new NotFoundException('Learning object not found');
|
||||||
|
}
|
||||||
|
|
||||||
res.json(learningObject);
|
res.json(learningObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,7 +68,7 @@ export async function getAttachment(req: Request, res: Response): Promise<void>
|
||||||
const attachment = await attachmentService.getAttachment(learningObjectId, name);
|
const attachment = await attachmentService.getAttachment(learningObjectId, name);
|
||||||
|
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
throw new NotFoundError(`Attachment ${name} not found`);
|
throw new NotFoundException(`Attachment ${name} not found`);
|
||||||
}
|
}
|
||||||
res.setHeader('Content-Type', attachment.mimeType).send(attachment.content);
|
res.setHeader('Content-Type', attachment.mimeType).send(attachment.content);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@ import { Request, Response } from 'express';
|
||||||
import { themes } from '../data/themes.js';
|
import { themes } from '../data/themes.js';
|
||||||
import { FALLBACK_LANG } from '../config.js';
|
import { FALLBACK_LANG } from '../config.js';
|
||||||
import learningPathService from '../services/learning-paths/learning-path-service.js';
|
import learningPathService from '../services/learning-paths/learning-path-service.js';
|
||||||
import { BadRequestException, NotFoundException } from '../exceptions.js';
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
import { Language } from '../entities/content/language.js';
|
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||||
import {
|
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||||
PersonalizationTarget,
|
import { Group } from '../entities/assignments/group.entity.js';
|
||||||
personalizedForGroup,
|
import { getAssignmentRepository, getGroupRepository } from '../data/repositories.js';
|
||||||
personalizedForStudent,
|
|
||||||
} from '../services/learning-paths/learning-path-personalization-util.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch learning paths based on query parameters.
|
* Fetch learning paths based on query parameters.
|
||||||
|
|
@ -19,20 +17,20 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
|
||||||
const searchQuery = req.query.search as string;
|
const searchQuery = req.query.search as string;
|
||||||
const language = (req.query.language as string) || FALLBACK_LANG;
|
const language = (req.query.language as string) || FALLBACK_LANG;
|
||||||
|
|
||||||
const forStudent = req.query.forStudent as string;
|
|
||||||
const forGroupNo = req.query.forGroup as string;
|
const forGroupNo = req.query.forGroup as string;
|
||||||
const assignmentNo = req.query.assignmentNo as string;
|
const assignmentNo = req.query.assignmentNo as string;
|
||||||
const classId = req.query.classId as string;
|
const classId = req.query.classId as string;
|
||||||
|
|
||||||
let personalizationTarget: PersonalizationTarget | undefined;
|
let forGroup: Group | undefined;
|
||||||
|
|
||||||
if (forStudent) {
|
if (forGroupNo) {
|
||||||
personalizationTarget = await personalizedForStudent(forStudent);
|
|
||||||
} else if (forGroupNo) {
|
|
||||||
if (!assignmentNo || !classId) {
|
if (!assignmentNo || !classId) {
|
||||||
throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.');
|
throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.');
|
||||||
}
|
}
|
||||||
personalizationTarget = await personalizedForGroup(classId, parseInt(assignmentNo), parseInt(forGroupNo));
|
const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo));
|
||||||
|
if (assignment) {
|
||||||
|
forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hruidList;
|
let hruidList;
|
||||||
|
|
@ -47,18 +45,13 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
|
||||||
throw new NotFoundException(`Theme "${themeKey}" not found.`);
|
throw new NotFoundException(`Theme "${themeKey}" not found.`);
|
||||||
}
|
}
|
||||||
} else if (searchQuery) {
|
} else if (searchQuery) {
|
||||||
const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, personalizationTarget);
|
const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, forGroup);
|
||||||
res.json(searchResults);
|
res.json(searchResults);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
hruidList = themes.flatMap((theme) => theme.hruids);
|
hruidList = themes.flatMap((theme) => theme.hruids);
|
||||||
}
|
}
|
||||||
|
|
||||||
const learningPaths = await learningPathService.fetchLearningPaths(
|
const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup);
|
||||||
hruidList,
|
|
||||||
language as Language,
|
|
||||||
`HRUIDs: ${hruidList.join(', ')}`,
|
|
||||||
personalizationTarget
|
|
||||||
);
|
|
||||||
res.json(learningPaths.data);
|
res.json(learningPaths.data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,27 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js';
|
import {
|
||||||
import { QuestionDTO, QuestionId } from '../interfaces/question.js';
|
createQuestion,
|
||||||
import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js';
|
deleteQuestion,
|
||||||
|
getAllQuestions,
|
||||||
|
getQuestion,
|
||||||
|
getQuestionsAboutLearningObjectInAssignment,
|
||||||
|
updateQuestion,
|
||||||
|
} from '../services/questions.js';
|
||||||
|
import { FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_NUM } from '../config.js';
|
||||||
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||||
import { Language } from '../entities/content/language.js';
|
import { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
||||||
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null {
|
import { requireFields } from './error-helper.js';
|
||||||
const { hruid, version } = req.params;
|
|
||||||
const lang = req.query.lang;
|
|
||||||
|
|
||||||
if (!hruid || !version) {
|
|
||||||
res.status(400).json({ error: 'Missing required parameters.' });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier {
|
||||||
return {
|
return {
|
||||||
hruid,
|
hruid,
|
||||||
language: (lang as Language) || FALLBACK_LANG,
|
language: (lang || FALLBACK_LANG) as Language,
|
||||||
version: +version,
|
version: Number(version) || FALLBACK_VERSION_NUM,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQuestionId(req: Request, res: Response): QuestionId | null {
|
export function getQuestionId(learningObjectIdentifier: LearningObjectIdentifier, seq: string): QuestionId {
|
||||||
const seq = req.params.seq;
|
|
||||||
const learningObjectIdentifier = getObjectId(req, res);
|
|
||||||
|
|
||||||
if (!learningObjectIdentifier) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
learningObjectIdentifier,
|
learningObjectIdentifier,
|
||||||
sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM,
|
sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM,
|
||||||
|
|
@ -36,84 +29,96 @@ function getQuestionId(req: Request, res: Response): QuestionId | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> {
|
export async function getAllQuestionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const objectId = getObjectId(req, res);
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = (req.query.lang ? req.query.lang : FALLBACK_LANG) as string;
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
if (!objectId) {
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const questions = await getAllQuestions(objectId, full);
|
let questions: QuestionDTO[] | QuestionId[];
|
||||||
|
if (req.query.classId && req.query.assignmentId) {
|
||||||
if (!questions) {
|
questions = await getQuestionsAboutLearningObjectInAssignment(
|
||||||
res.status(404).json({ error: `Questions not found.` });
|
learningObjectId,
|
||||||
|
req.query.classId as string,
|
||||||
|
parseInt(req.query.assignmentId as string),
|
||||||
|
full ?? false,
|
||||||
|
req.query.forStudent as string | undefined
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
res.json(questions);
|
questions = await getAllQuestions(learningObjectId, full ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.json({ questions });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getQuestionHandler(req: Request, res: Response): Promise<void> {
|
export async function getQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||||
const questionId = getQuestionId(req, res);
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
const seq = req.params.seq;
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
if (!questionId) {
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
return;
|
const questionId = getQuestionId(learningObjectId, seq);
|
||||||
}
|
|
||||||
|
|
||||||
const question = await getQuestion(questionId);
|
const question = await getQuestion(questionId);
|
||||||
|
|
||||||
if (!question) {
|
res.json({ question });
|
||||||
res.status(404).json({ error: `Question not found.` });
|
|
||||||
} else {
|
|
||||||
res.json(question);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getQuestionAnswersHandler(req: Request, res: Response): Promise<void> {
|
|
||||||
const questionId = getQuestionId(req, res);
|
|
||||||
const full = req.query.full === 'true';
|
|
||||||
|
|
||||||
if (!questionId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const answers = getAnswersByQuestion(questionId, full);
|
|
||||||
|
|
||||||
if (!answers) {
|
|
||||||
res.status(404).json({ error: `Questions not found.` });
|
|
||||||
} else {
|
|
||||||
res.json(answers);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createQuestionHandler(req: Request, res: Response): Promise<void> {
|
export async function createQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||||
const questionDTO = req.body as QuestionDTO;
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
if (!questionDTO.learningObjectIdentifier || !questionDTO.author || !questionDTO.content) {
|
const loId = getLearningObjectId(hruid, version, language);
|
||||||
res.status(400).json({ error: 'Missing required fields: identifier and content' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const question = await createQuestion(questionDTO);
|
const author = req.body.author as string;
|
||||||
|
const content = req.body.content as string;
|
||||||
|
const inGroup = req.body.inGroup;
|
||||||
|
requireFields({ author, content, inGroup });
|
||||||
|
|
||||||
if (!question) {
|
const questionData = req.body as QuestionData;
|
||||||
res.status(400).json({ error: 'Could not add question' });
|
|
||||||
} else {
|
const question = await createQuestion(loId, questionData);
|
||||||
res.json(question);
|
|
||||||
}
|
res.json({ question });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> {
|
export async function deleteQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||||
const questionId = getQuestionId(req, res);
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
const seq = req.params.seq;
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
if (!questionId) {
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
return;
|
const questionId = getQuestionId(learningObjectId, seq);
|
||||||
}
|
|
||||||
|
|
||||||
const question = await deleteQuestion(questionId);
|
const question = await deleteQuestion(questionId);
|
||||||
|
|
||||||
if (!question) {
|
res.json({ question });
|
||||||
res.status(400).json({ error: 'Could not find nor delete question' });
|
}
|
||||||
} else {
|
|
||||||
res.json(question);
|
export async function updateQuestionHandler(req: Request, res: Response): Promise<void> {
|
||||||
}
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
const seq = req.params.seq;
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
|
const questionId = getQuestionId(learningObjectId, seq);
|
||||||
|
|
||||||
|
const content = req.body.content as string;
|
||||||
|
requireFields({ content });
|
||||||
|
|
||||||
|
const questionData = req.body as QuestionData;
|
||||||
|
|
||||||
|
const question = await updateQuestion(questionId, questionData);
|
||||||
|
|
||||||
|
res.json({ question });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,112 +1,67 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import {
|
import {
|
||||||
|
createClassJoinRequest,
|
||||||
createStudent,
|
createStudent,
|
||||||
|
deleteClassJoinRequest,
|
||||||
deleteStudent,
|
deleteStudent,
|
||||||
getAllStudents,
|
getAllStudents,
|
||||||
|
getJoinRequestByStudentClass,
|
||||||
|
getJoinRequestsByStudent,
|
||||||
getStudent,
|
getStudent,
|
||||||
getStudentAssignments,
|
getStudentAssignments,
|
||||||
getStudentClasses,
|
getStudentClasses,
|
||||||
getStudentGroups,
|
getStudentGroups,
|
||||||
|
getStudentQuestions,
|
||||||
getStudentSubmissions,
|
getStudentSubmissions,
|
||||||
} from '../services/students.js';
|
} from '../services/students.js';
|
||||||
import { ClassDTO } from '../interfaces/class.js';
|
import { requireFields } from './error-helper.js';
|
||||||
import { getAllAssignments } from '../services/assignments.js';
|
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
||||||
import { getUserHandler } from './users.js';
|
|
||||||
import { Student } from '../entities/users/student.entity.js';
|
|
||||||
import { StudentDTO } from '../interfaces/student.js';
|
|
||||||
import { getStudentRepository } from '../data/repositories.js';
|
|
||||||
import { UserDTO } from '../interfaces/user.js';
|
|
||||||
|
|
||||||
// TODO: accept arguments (full, ...)
|
|
||||||
// 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: StudentDTO[] | string[] = await getAllStudents(full);
|
||||||
|
|
||||||
const students: StudentDTO[] | string[] = full ? await getAllStudents() : await getAllStudents();
|
res.json({ students });
|
||||||
|
|
||||||
if (!students) {
|
|
||||||
res.status(404).json({ error: `Student not found.` });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json(students);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStudentHandler(req: Request, res: Response): Promise<void> {
|
export async function getStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
const username = req.params.username;
|
const username = req.params.username;
|
||||||
|
requireFields({ username });
|
||||||
|
|
||||||
if (!username) {
|
const student = await getStudent(username);
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await getStudent(username);
|
res.json({ student });
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
res.status(404).json({
|
|
||||||
error: `User with username '${username}' not found.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createStudentHandler(req: Request, res: Response) {
|
export async function createStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const username = req.body.username;
|
||||||
|
const firstName = req.body.firstName;
|
||||||
|
const lastName = req.body.lastName;
|
||||||
|
requireFields({ username, firstName, lastName });
|
||||||
|
|
||||||
const userData = req.body as StudentDTO;
|
const userData = req.body as StudentDTO;
|
||||||
|
|
||||||
if (!userData.username || !userData.firstName || !userData.lastName) {
|
const student = await createStudent(userData);
|
||||||
res.status(400).json({
|
res.json({ student });
|
||||||
error: 'Missing required fields: username, firstName, lastName',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = await createStudent(userData);
|
|
||||||
res.status(201).json(newUser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteStudentHandler(req: Request, res: Response) {
|
export async function deleteStudentHandler(req: Request, res: Response): Promise<void> {
|
||||||
const username = req.params.username;
|
const username = req.params.username;
|
||||||
|
requireFields({ username });
|
||||||
|
|
||||||
if (!username) {
|
const student = await deleteStudent(username);
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
res.json({ student });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletedUser = await deleteStudent(username);
|
|
||||||
if (!deletedUser) {
|
|
||||||
res.status(404).json({
|
|
||||||
error: `User with username '${username}' not found.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json(deletedUser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.username;
|
||||||
const username = req.params.id;
|
requireFields({ username });
|
||||||
|
|
||||||
const classes = await getStudentClasses(username, full);
|
const classes = await getStudentClasses(username, full);
|
||||||
|
|
||||||
res.json({
|
res.json({ 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
|
||||||
|
|
@ -115,32 +70,75 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro
|
||||||
// Have this assignment.
|
// Have this assignment.
|
||||||
export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> {
|
export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
const username = req.params.id;
|
const username = req.params.username;
|
||||||
|
requireFields({ username });
|
||||||
|
|
||||||
const assignments = getStudentAssignments(username, full);
|
const assignments = await getStudentAssignments(username, full);
|
||||||
|
|
||||||
res.json({
|
res.json({ assignments });
|
||||||
assignments: assignments,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> {
|
export async function getStudentGroupsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const full = req.query.full === 'true';
|
const full = req.query.full === 'true';
|
||||||
const username = req.params.id;
|
const username = req.params.username;
|
||||||
|
requireFields({ username });
|
||||||
|
|
||||||
const groups = await getStudentGroups(username, full);
|
const groups = await getStudentGroups(username, full);
|
||||||
|
|
||||||
res.json({
|
res.json({ groups });
|
||||||
groups: groups,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.username;
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
requireFields({ username });
|
||||||
|
|
||||||
const submissions = await getStudentSubmissions(username);
|
const submissions = await getStudentSubmissions(username, full);
|
||||||
|
|
||||||
res.json({
|
res.json({ submissions });
|
||||||
submissions: submissions,
|
}
|
||||||
});
|
|
||||||
|
export async function getStudentQuestionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const full = req.query.full === 'true';
|
||||||
|
const username = req.params.username;
|
||||||
|
requireFields({ username });
|
||||||
|
|
||||||
|
const questions = await getStudentQuestions(username, full);
|
||||||
|
|
||||||
|
res.json({ questions });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStudentRequestHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const username = req.params.username;
|
||||||
|
const classId = req.body.classId;
|
||||||
|
requireFields({ username, classId });
|
||||||
|
|
||||||
|
const request = await createClassJoinRequest(username, classId);
|
||||||
|
res.json({ request });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentRequestsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const username = req.params.username;
|
||||||
|
requireFields({ username });
|
||||||
|
|
||||||
|
const requests = await getJoinRequestsByStudent(username);
|
||||||
|
res.json({ requests });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStudentRequestHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const username = req.params.username;
|
||||||
|
const classId = req.params.classId;
|
||||||
|
requireFields({ username, classId });
|
||||||
|
|
||||||
|
const request = await getJoinRequestByStudentClass(username, classId);
|
||||||
|
res.json({ request });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteClassJoinRequestHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const username = req.params.username;
|
||||||
|
const classId = req.params.classId;
|
||||||
|
requireFields({ username, classId });
|
||||||
|
|
||||||
|
const request = await deleteClassJoinRequest(username, classId);
|
||||||
|
res.json({ request });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,86 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js';
|
import {
|
||||||
import { Language, languageMap } from '../entities/content/language.js';
|
createSubmission,
|
||||||
import { SubmissionDTO } from '../interfaces/submission';
|
deleteSubmission,
|
||||||
|
getAllSubmissions,
|
||||||
|
getSubmission,
|
||||||
|
getSubmissionsForLearningObjectAndAssignment,
|
||||||
|
} from '../services/submissions.js';
|
||||||
|
import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission';
|
||||||
|
import { Language, languageMap } from '@dwengo-1/common/util/language';
|
||||||
|
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||||
|
import { requireFields } from './error-helper.js';
|
||||||
|
import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js';
|
||||||
|
|
||||||
interface SubmissionParams {
|
export async function getSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
hruid: string;
|
const loHruid = req.params.hruid;
|
||||||
id: number;
|
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
||||||
|
const version = parseInt(req.query.version as string) ?? 1;
|
||||||
|
|
||||||
|
const forGroup = req.query.forGroup as string | undefined;
|
||||||
|
|
||||||
|
const submissions: SubmissionDTO[] = await getSubmissionsForLearningObjectAndAssignment(
|
||||||
|
loHruid,
|
||||||
|
lang,
|
||||||
|
version,
|
||||||
|
req.query.classId as string,
|
||||||
|
parseInt(req.query.assignmentId as string),
|
||||||
|
forGroup ? parseInt(forGroup) : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ submissions });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSubmissionHandler(req: Request<SubmissionParams>, res: Response): Promise<void> {
|
export async function getSubmissionHandler(req: Request, res: Response): Promise<void> {
|
||||||
const lohruid = req.params.hruid;
|
const lohruid = req.params.hruid;
|
||||||
const submissionNumber = +req.params.id;
|
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
||||||
|
const version = (req.query.version || 1) as number;
|
||||||
|
const submissionNumber = Number(req.params.id);
|
||||||
|
requireFields({ lohruid, submissionNumber });
|
||||||
|
|
||||||
if (isNaN(submissionNumber)) {
|
if (isNaN(submissionNumber)) {
|
||||||
res.status(400).json({ error: 'Submission number is not a number' });
|
throw new BadRequestException('Submission number must be a number');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
const loId = new LearningObjectIdentifier(lohruid, lang, version);
|
||||||
const version = (req.query.version || 1) as number;
|
const submission = await getSubmission(loId, submissionNumber);
|
||||||
|
|
||||||
const submission = await getSubmission(lohruid, lang, version, submissionNumber);
|
res.json({ submission });
|
||||||
|
|
||||||
if (!submission) {
|
|
||||||
res.status(404).json({ error: 'Submission not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(submission);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSubmissionHandler(req: Request, res: Response) {
|
export async function getAllSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const submissionDTO = req.body as SubmissionDTO;
|
const lohruid = req.params.hruid;
|
||||||
|
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
||||||
|
const version = (req.query.version || 1) as number;
|
||||||
|
requireFields({ lohruid });
|
||||||
|
|
||||||
|
const loId = new LearningObjectIdentifier(lohruid, lang, version);
|
||||||
|
const submissions = await getAllSubmissions(loId);
|
||||||
|
|
||||||
|
res.json({ submissions });
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: gerald moet nog dingen toevoegen aan de databank voor dat dit gefinaliseerd kan worden
|
||||||
|
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const submissionDTO = req.body as SubmissionDTO;
|
||||||
const submission = await createSubmission(submissionDTO);
|
const submission = await createSubmission(submissionDTO);
|
||||||
|
|
||||||
if (!submission) {
|
res.json({ submission });
|
||||||
res.status(404).json({ error: 'Submission not added' });
|
|
||||||
} else {
|
|
||||||
res.json(submission);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSubmissionHandler(req: Request, res: Response) {
|
export async function deleteSubmissionHandler(req: Request, res: Response): Promise<void> {
|
||||||
const hruid = req.params.hruid;
|
const hruid = req.params.hruid;
|
||||||
const submissionNumber = +req.params.id;
|
|
||||||
|
|
||||||
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
const lang = languageMap[req.query.language as string] || Language.Dutch;
|
||||||
const version = (req.query.version || 1) as number;
|
const version = (req.query.version || 1) as number;
|
||||||
|
const submissionNumber = Number(req.params.id);
|
||||||
|
requireFields({ hruid, submissionNumber });
|
||||||
|
|
||||||
const submission = await deleteSubmission(hruid, lang, version, submissionNumber);
|
if (isNaN(submissionNumber)) {
|
||||||
|
throw new BadRequestException('Submission number must be a number');
|
||||||
if (!submission) {
|
|
||||||
res.status(404).json({ error: 'Submission not found' });
|
|
||||||
} else {
|
|
||||||
res.json(submission);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loId = new LearningObjectIdentifier(hruid, lang, version);
|
||||||
|
const submission = await deleteSubmission(loId, submissionNumber);
|
||||||
|
|
||||||
|
res.json({ submission });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
backend/src/controllers/teacher-invitations.ts
Normal file
66
backend/src/controllers/teacher-invitations.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { requireFields } from './error-helper.js';
|
||||||
|
import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations.js';
|
||||||
|
import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation';
|
||||||
|
|
||||||
|
export async function getAllInvitationsHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const username = req.params.username;
|
||||||
|
const by = req.query.sent === 'true';
|
||||||
|
requireFields({ username });
|
||||||
|
|
||||||
|
const invitations = await getAllInvitations(username, by);
|
||||||
|
|
||||||
|
res.json({ invitations });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getInvitationHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const sender = req.params.sender;
|
||||||
|
const receiver = req.params.receiver;
|
||||||
|
const classId = req.params.classId;
|
||||||
|
requireFields({ sender, receiver, classId });
|
||||||
|
|
||||||
|
const invitation = await getInvitation(sender, receiver, classId);
|
||||||
|
|
||||||
|
res.json({ invitation });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createInvitationHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const sender = req.body.sender;
|
||||||
|
const receiver = req.body.receiver;
|
||||||
|
const classId = req.body.class;
|
||||||
|
requireFields({ sender, receiver, classId });
|
||||||
|
|
||||||
|
const data = req.body as TeacherInvitationData;
|
||||||
|
const invitation = await createInvitation(data);
|
||||||
|
|
||||||
|
res.json({ invitation });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateInvitationHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const sender = req.body.sender;
|
||||||
|
const receiver = req.body.receiver;
|
||||||
|
const classId = req.body.class;
|
||||||
|
req.body.accepted = req.body.accepted !== false;
|
||||||
|
requireFields({ sender, receiver, classId });
|
||||||
|
|
||||||
|
const data = req.body as TeacherInvitationData;
|
||||||
|
const invitation = await updateInvitation(data);
|
||||||
|
|
||||||
|
res.json({ invitation });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteInvitationHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const sender = req.params.sender;
|
||||||
|
const receiver = req.params.receiver;
|
||||||
|
const classId = req.params.classId;
|
||||||
|
requireFields({ sender, receiver, classId });
|
||||||
|
|
||||||
|
const data: TeacherInvitationData = {
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
class: classId,
|
||||||
|
};
|
||||||
|
const invitation = await deleteInvitation(data);
|
||||||
|
|
||||||
|
res.json({ invitation });
|
||||||
|
}
|
||||||
|
|
@ -4,141 +4,96 @@ import {
|
||||||
deleteTeacher,
|
deleteTeacher,
|
||||||
getAllTeachers,
|
getAllTeachers,
|
||||||
getClassesByTeacher,
|
getClassesByTeacher,
|
||||||
getClassIdsByTeacher,
|
getJoinRequestsByClass,
|
||||||
getQuestionIdsByTeacher,
|
|
||||||
getQuestionsByTeacher,
|
|
||||||
getStudentIdsByTeacher,
|
|
||||||
getStudentsByTeacher,
|
getStudentsByTeacher,
|
||||||
getTeacher,
|
getTeacher,
|
||||||
|
getTeacherQuestions,
|
||||||
|
updateClassJoinRequestStatus,
|
||||||
} from '../services/teachers.js';
|
} from '../services/teachers.js';
|
||||||
import { ClassDTO } from '../interfaces/class.js';
|
import { requireFields } from './error-helper.js';
|
||||||
import { StudentDTO } from '../interfaces/student.js';
|
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
||||||
import { QuestionDTO, QuestionId } from '../interfaces/question.js';
|
|
||||||
import { Teacher } from '../entities/users/teacher.entity.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: TeacherDTO[] | string[] = await getAllTeachers(full);
|
||||||
|
|
||||||
const teachers: TeacherDTO[] | string[] = full ? await getAllTeachers() : await getAllTeachers();
|
res.json({ teachers });
|
||||||
|
|
||||||
if (!teachers) {
|
|
||||||
res.status(404).json({ error: `Teacher not found.` });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json(teachers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
|
export async function getTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||||
const username = req.params.username;
|
const username = req.params.username;
|
||||||
|
requireFields({ username });
|
||||||
|
|
||||||
if (!username) {
|
const teacher = await getTeacher(username);
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await getTeacher(username);
|
res.json({ teacher });
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
res.status(404).json({
|
|
||||||
error: `User with username '${username}' not found.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTeacherHandler(req: Request, res: Response) {
|
export async function createTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const username = req.body.username;
|
||||||
|
const firstName = req.body.firstName;
|
||||||
|
const lastName = req.body.lastName;
|
||||||
|
requireFields({ username, firstName, lastName });
|
||||||
|
|
||||||
const userData = req.body as TeacherDTO;
|
const userData = req.body as TeacherDTO;
|
||||||
|
|
||||||
if (!userData.username || !userData.firstName || !userData.lastName) {
|
const teacher = await createTeacher(userData);
|
||||||
res.status(400).json({
|
res.json({ teacher });
|
||||||
error: 'Missing required fields: username, firstName, lastName',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = await createTeacher(userData);
|
|
||||||
res.status(201).json(newUser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTeacherHandler(req: Request, res: Response) {
|
export async function deleteTeacherHandler(req: Request, res: Response): Promise<void> {
|
||||||
const username = req.params.username;
|
const username = req.params.username;
|
||||||
|
requireFields({ username });
|
||||||
|
|
||||||
if (!username) {
|
const teacher = await deleteTeacher(username);
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
res.json({ teacher });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletedUser = await deleteTeacher(username);
|
|
||||||
if (!deletedUser) {
|
|
||||||
res.status(404).json({
|
|
||||||
error: `User with username '${username}' not found.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json(deletedUser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
const username = req.params.username as string;
|
const full = req.query.full === 'true';
|
||||||
const full = req.query.full === 'true';
|
requireFields({ username });
|
||||||
|
|
||||||
if (!username) {
|
const classes = await getClassesByTeacher(username, full);
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes: ClassDTO[] | string[] = full ? await getClassesByTeacher(username) : await getClassIdsByTeacher(username);
|
res.json({ classes });
|
||||||
|
|
||||||
res.status(201).json(classes);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching classes by teacher:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
const username = req.params.username as string;
|
const full = req.query.full === 'true';
|
||||||
const full = req.query.full === 'true';
|
requireFields({ username });
|
||||||
|
|
||||||
if (!username) {
|
const students = await getStudentsByTeacher(username, full);
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const students: StudentDTO[] | string[] = full ? await getStudentsByTeacher(username) : await getStudentIdsByTeacher(username);
|
res.json({ students });
|
||||||
|
|
||||||
res.status(201).json(students);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching students by teacher:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
const username = req.params.username as string;
|
const full = req.query.full === 'true';
|
||||||
const full = req.query.full === 'true';
|
requireFields({ username });
|
||||||
|
|
||||||
if (!username) {
|
const questions = await getTeacherQuestions(username, full);
|
||||||
res.status(400).json({ error: 'Missing required field: username' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const questions: QuestionDTO[] | QuestionId[] = full ? await getQuestionsByTeacher(username) : await getQuestionIdsByTeacher(username);
|
res.json({ questions });
|
||||||
|
}
|
||||||
res.status(201).json(questions);
|
|
||||||
} catch (error) {
|
export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
|
||||||
console.error('Error fetching questions by teacher:', error);
|
const classId = req.params.classId;
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
requireFields({ classId });
|
||||||
}
|
|
||||||
|
const joinRequests = await getJoinRequestsByClass(classId);
|
||||||
|
res.json({ joinRequests });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const studentUsername = req.params.studentUsername;
|
||||||
|
const classId = req.params.classId;
|
||||||
|
const accepted = req.body.accepted !== 'false'; // Default = true
|
||||||
|
requireFields({ studentUsername, classId });
|
||||||
|
|
||||||
|
const request = await updateClassJoinRequestStatus(studentUsername, classId, accepted);
|
||||||
|
res.json({ request });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,30 @@ import { themes } from '../data/themes.js';
|
||||||
import { loadTranslations } from '../util/translation-helper.js';
|
import { loadTranslations } from '../util/translation-helper.js';
|
||||||
|
|
||||||
interface Translations {
|
interface Translations {
|
||||||
curricula_page: {
|
curricula_page: Record<string, { title: string; description?: string }>;
|
||||||
[key: string]: { title: string; description?: string };
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getThemes(req: Request, res: Response) {
|
export function getThemesHandler(req: Request, res: Response): void {
|
||||||
const language = (req.query.language as string)?.toLowerCase() || 'nl';
|
const language = ((req.query.language as string) || 'nl').toLowerCase();
|
||||||
const translations = loadTranslations<Translations>(language);
|
const translations = loadTranslations<Translations>(language);
|
||||||
const themeList = themes.map((theme) => ({
|
const themeList = themes.map((theme) => ({
|
||||||
key: theme.title,
|
key: theme.title,
|
||||||
title: translations.curricula_page[theme.title]?.title || theme.title,
|
title: translations.curricula_page[theme.title].title || theme.title,
|
||||||
description: translations.curricula_page[theme.title]?.description,
|
description: translations.curricula_page[theme.title].description,
|
||||||
image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
|
image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json(themeList);
|
res.json(themeList);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getThemeByTitle(req: Request, res: Response) {
|
export function getHruidsByThemeHandler(req: Request, res: Response): void {
|
||||||
const themeKey = req.params.theme;
|
const themeKey = req.params.theme;
|
||||||
|
|
||||||
|
if (!themeKey) {
|
||||||
|
res.status(400).json({ error: 'Missing required field: theme' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const theme = themes.find((t) => t.title === themeKey);
|
const theme = themes.find((t) => t.title === themeKey);
|
||||||
|
|
||||||
if (theme) {
|
if (theme) {
|
||||||
|
|
|
||||||
|
|
@ -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' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,13 +3,29 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
||||||
import { Class } from '../../entities/classes/class.entity.js';
|
import { Class } from '../../entities/classes/class.entity.js';
|
||||||
|
|
||||||
export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
|
export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
|
||||||
public findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
|
public async findByClassAndId(within: Class, id: number): Promise<Assignment | null> {
|
||||||
return this.findOne({ within: within, id: id });
|
return this.findOne({ within: within, id: id }, { populate: ['groups', 'groups.members'] });
|
||||||
}
|
}
|
||||||
public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
|
public async findByClassIdAndAssignmentId(withinClass: string, id: number): Promise<Assignment | null> {
|
||||||
return this.findAll({ where: { within: within } });
|
return this.findOne({ within: { classId: withinClass }, id: id });
|
||||||
}
|
}
|
||||||
public deleteByClassAndId(within: Class, id: number): Promise<void> {
|
public async findAllByResponsibleTeacher(teacherUsername: string): Promise<Assignment[]> {
|
||||||
|
return this.findAll({
|
||||||
|
where: {
|
||||||
|
within: {
|
||||||
|
teachers: {
|
||||||
|
$some: {
|
||||||
|
username: teacherUsername,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
|
||||||
|
return this.findAll({ where: { within: within }, populate: ['groups', 'groups.members'] });
|
||||||
|
}
|
||||||
|
public async deleteByClassAndId(within: Class, id: number): Promise<void> {
|
||||||
return this.deleteWhere({ within: within, id: id });
|
return this.deleteWhere({ within: within, id: id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
|
|
||||||
export class GroupRepository extends DwengoEntityRepository<Group> {
|
export class GroupRepository extends DwengoEntityRepository<Group> {
|
||||||
public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
|
public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
assignment: assignment,
|
assignment: assignment,
|
||||||
|
|
@ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository<Group> {
|
||||||
{ populate: ['members'] }
|
{ populate: ['members'] }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
|
public async findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> {
|
||||||
return this.findAll({
|
return this.findAll({
|
||||||
where: { assignment: assignment },
|
where: { assignment: assignment },
|
||||||
populate: ['members'],
|
populate: ['members'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public findAllGroupsWithStudent(student: Student): Promise<Group[]> {
|
public async findAllGroupsWithStudent(student: Student): Promise<Group[]> {
|
||||||
return this.find({ members: student }, { populate: ['members'] });
|
return this.find({ members: student }, { populate: ['members'] });
|
||||||
}
|
}
|
||||||
public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) {
|
public async deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<void> {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
assignment: assignment,
|
assignment: assignment,
|
||||||
groupNumber: groupNumber,
|
groupNumber: groupNumber,
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,13 @@ import { Group } from '../../entities/assignments/group.entity.js';
|
||||||
import { Submission } from '../../entities/assignments/submission.entity.js';
|
import { Submission } from '../../entities/assignments/submission.entity.js';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
|
import { Assignment } from '../../entities/assignments/assignment.entity';
|
||||||
|
|
||||||
export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
||||||
public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission | null> {
|
public async findSubmissionByLearningObjectAndSubmissionNumber(
|
||||||
|
loId: LearningObjectIdentifier,
|
||||||
|
submissionNumber: number
|
||||||
|
): Promise<Submission | null> {
|
||||||
return this.findOne({
|
return this.findOne({
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
learningObjectLanguage: loId.language,
|
learningObjectLanguage: loId.language,
|
||||||
|
|
@ -14,7 +18,15 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> {
|
public async findByLearningObject(loId: LearningObjectIdentifier): Promise<Submission[]> {
|
||||||
|
return this.find({
|
||||||
|
learningObjectHruid: loId.hruid,
|
||||||
|
learningObjectLanguage: loId.language,
|
||||||
|
learningObjectVersion: loId.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
|
|
@ -26,7 +38,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> {
|
public async findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
|
|
@ -38,15 +50,60 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
|
public async findAllSubmissionsForGroup(group: Group): Promise<Submission[]> {
|
||||||
return this.find({ onBehalfOf: group });
|
return this.find(
|
||||||
|
{ onBehalfOf: group },
|
||||||
|
{
|
||||||
|
populate: ['onBehalfOf.members'],
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
|
/**
|
||||||
return this.find({ submitter: student });
|
* Looks up all submissions for the given learning object which were submitted as part of the given assignment.
|
||||||
|
*/
|
||||||
|
public async findAllSubmissionsForLearningObjectAndAssignment(loId: LearningObjectIdentifier, assignment: Assignment): Promise<Submission[]> {
|
||||||
|
return this.findAll({
|
||||||
|
where: {
|
||||||
|
learningObjectHruid: loId.hruid,
|
||||||
|
learningObjectLanguage: loId.language,
|
||||||
|
learningObjectVersion: loId.version,
|
||||||
|
onBehalfOf: {
|
||||||
|
assignment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
|
/**
|
||||||
|
* Looks up all submissions for the given learning object which were submitted by the given group
|
||||||
|
*/
|
||||||
|
public async findAllSubmissionsForLearningObjectAndGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission[]> {
|
||||||
|
return this.findAll({
|
||||||
|
where: {
|
||||||
|
learningObjectHruid: loId.hruid,
|
||||||
|
learningObjectLanguage: loId.language,
|
||||||
|
learningObjectVersion: loId.version,
|
||||||
|
onBehalfOf: group,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findAllSubmissionsForStudent(student: Student): Promise<Submission[]> {
|
||||||
|
const result = await this.find(
|
||||||
|
{ submitter: student },
|
||||||
|
{
|
||||||
|
populate: ['onBehalfOf.members'],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Workaround: For some reason, without this MikroORM generates an UPDATE query with a syntax error in some tests
|
||||||
|
this.em.clear();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
learningObjectLanguage: loId.language,
|
learningObjectLanguage: loId.language,
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,19 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { Class } from '../../entities/classes/class.entity.js';
|
import { Class } from '../../entities/classes/class.entity.js';
|
||||||
import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js';
|
import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js';
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
|
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||||
|
|
||||||
export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> {
|
export class ClassJoinRequestRepository extends DwengoEntityRepository<ClassJoinRequest> {
|
||||||
public findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> {
|
public async findAllRequestsBy(requester: Student): Promise<ClassJoinRequest[]> {
|
||||||
return this.findAll({ where: { requester: requester } });
|
return this.findAll({ where: { requester: requester } });
|
||||||
}
|
}
|
||||||
public findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
|
public async findAllOpenRequestsTo(clazz: Class): Promise<ClassJoinRequest[]> {
|
||||||
return this.findAll({ where: { class: clazz } });
|
return this.findAll({ where: { class: clazz, status: ClassStatus.Open } }); // TODO check if works like this
|
||||||
}
|
}
|
||||||
public deleteBy(requester: Student, clazz: Class): Promise<void> {
|
public async findByStudentAndClass(requester: Student, clazz: Class): Promise<ClassJoinRequest | null> {
|
||||||
|
return this.findOne({ requester, class: clazz });
|
||||||
|
}
|
||||||
|
public async deleteBy(requester: Student, clazz: Class): Promise<void> {
|
||||||
return this.deleteWhere({ requester: requester, class: clazz });
|
return this.deleteWhere({ requester: requester, class: clazz });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,20 @@ import { Student } from '../../entities/users/student.entity.js';
|
||||||
import { Teacher } from '../../entities/users/teacher.entity';
|
import { Teacher } from '../../entities/users/teacher.entity';
|
||||||
|
|
||||||
export class ClassRepository extends DwengoEntityRepository<Class> {
|
export class ClassRepository extends DwengoEntityRepository<Class> {
|
||||||
public findById(id: string): Promise<Class | null> {
|
public async findById(id: string): Promise<Class | null> {
|
||||||
return this.findOne({ classId: id }, { populate: ['students', 'teachers'] });
|
return this.findOne({ classId: id }, { populate: ['students', 'teachers'] });
|
||||||
}
|
}
|
||||||
public deleteById(id: string): Promise<void> {
|
public async deleteById(id: string): Promise<void> {
|
||||||
return this.deleteWhere({ classId: id });
|
return this.deleteWhere({ classId: id });
|
||||||
}
|
}
|
||||||
public findByStudent(student: Student): Promise<Class[]> {
|
public async findByStudent(student: Student): Promise<Class[]> {
|
||||||
return this.find(
|
return this.find(
|
||||||
{ students: student },
|
{ students: student },
|
||||||
{ populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
|
{ populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findByTeacher(teacher: Teacher): Promise<Class[]> {
|
public async findByTeacher(teacher: Teacher): Promise<Class[]> {
|
||||||
return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] });
|
return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,30 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { Class } from '../../entities/classes/class.entity.js';
|
import { Class } from '../../entities/classes/class.entity.js';
|
||||||
import { TeacherInvitation } from '../../entities/classes/teacher-invitation.entity.js';
|
import { TeacherInvitation } from '../../entities/classes/teacher-invitation.entity.js';
|
||||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||||
|
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||||
|
|
||||||
export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> {
|
export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> {
|
||||||
public findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> {
|
public async findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> {
|
||||||
return this.findAll({ where: { class: clazz } });
|
return this.findAll({ where: { class: clazz } });
|
||||||
}
|
}
|
||||||
public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
|
public async findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> {
|
||||||
return this.findAll({ where: { sender: sender } });
|
return this.findAll({ where: { sender: sender } });
|
||||||
}
|
}
|
||||||
public findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> {
|
public async findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> {
|
||||||
return this.findAll({ where: { receiver: receiver } });
|
return this.findAll({ where: { receiver: receiver, status: ClassStatus.Open } });
|
||||||
}
|
}
|
||||||
public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
|
public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
sender: sender,
|
sender: sender,
|
||||||
receiver: receiver,
|
receiver: receiver,
|
||||||
class: clazz,
|
class: clazz,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
public async findBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<TeacherInvitation | null> {
|
||||||
|
return this.findOne({
|
||||||
|
sender: sender,
|
||||||
|
receiver: receiver,
|
||||||
|
class: clazz,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { Attachment } from '../../entities/content/attachment.entity.js';
|
import { Attachment } from '../../entities/content/attachment.entity.js';
|
||||||
import { Language } from '../../entities/content/language';
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier';
|
||||||
|
|
||||||
export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
|
export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
|
||||||
public findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> {
|
public async findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise<Attachment | null> {
|
||||||
return this.findOne({
|
return this.findOne({
|
||||||
learningObject: {
|
learningObject: {
|
||||||
hruid: learningObjectId.hruid,
|
hruid: learningObjectId.hruid,
|
||||||
|
|
@ -15,7 +15,11 @@ export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public findByMostRecentVersionOfLearningObjectAndName(hruid: string, language: Language, attachmentName: string): Promise<Attachment | null> {
|
public async findByMostRecentVersionOfLearningObjectAndName(
|
||||||
|
hruid: string,
|
||||||
|
language: Language,
|
||||||
|
attachmentName: string
|
||||||
|
): Promise<Attachment | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
learningObject: {
|
learningObject: {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||||
import { Language } from '../../entities/content/language.js';
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||||
|
|
||||||
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
||||||
public findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
hruid: identifier.hruid,
|
hruid: identifier.hruid,
|
||||||
|
|
@ -18,7 +18,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findLatestByHruidAndLanguage(hruid: string, language: Language) {
|
public async findLatestByHruidAndLanguage(hruid: string, language: Language): Promise<LearningObject | null> {
|
||||||
return this.findOne(
|
return this.findOne(
|
||||||
{
|
{
|
||||||
hruid: hruid,
|
hruid: hruid,
|
||||||
|
|
@ -33,7 +33,7 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
|
public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
|
||||||
return this.find(
|
return this.find(
|
||||||
{ admins: teacher },
|
{ admins: teacher },
|
||||||
{ populate: ['admins'] } // Make sure to load admin relations
|
{ populate: ['admins'] } // Make sure to load admin relations
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { LearningPath } from '../../entities/content/learning-path.entity.js';
|
import { LearningPath } from '../../entities/content/learning-path.entity.js';
|
||||||
import { Language } from '../../entities/content/language.js';
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
|
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
|
||||||
|
import { RequiredEntityData } from '@mikro-orm/core';
|
||||||
|
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
||||||
|
import { EntityAlreadyExistsException } from '../../exceptions/entity-already-exists-exception.js';
|
||||||
|
|
||||||
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
|
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
|
||||||
public findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
|
public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
|
||||||
return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] });
|
return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,4 +27,27 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
|
||||||
populate: ['nodes', 'nodes.transitions'],
|
populate: ['nodes', 'nodes.transitions'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public createNode(nodeData: RequiredEntityData<LearningPathNode>): LearningPathNode {
|
||||||
|
return this.em.create(LearningPathNode, nodeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public createTransition(transitionData: RequiredEntityData<LearningPathTransition>): LearningPathTransition {
|
||||||
|
return this.em.create(LearningPathTransition, transitionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async saveLearningPathNodesAndTransitions(
|
||||||
|
path: LearningPath,
|
||||||
|
nodes: LearningPathNode[],
|
||||||
|
transitions: LearningPathTransition[],
|
||||||
|
options?: { preventOverwrite?: boolean }
|
||||||
|
): Promise<void> {
|
||||||
|
if (options?.preventOverwrite && (await this.findOne(path))) {
|
||||||
|
throw new EntityAlreadyExistsException('A learning path with this hruid/language combination already exists.');
|
||||||
|
}
|
||||||
|
const em = this.getEntityManager();
|
||||||
|
await em.persistAndFlush(path);
|
||||||
|
await Promise.all(nodes.map(async (it) => em.persistAndFlush(it)));
|
||||||
|
await Promise.all(transitions.map(async (it) => em.persistAndFlush(it)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
|
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
|
||||||
|
import { EntityAlreadyExistsException } from '../exceptions/entity-already-exists-exception.js';
|
||||||
|
|
||||||
export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> {
|
export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> {
|
||||||
public async save(entity: T) {
|
public async save(entity: T, options?: { preventOverwrite?: boolean }): Promise<void> {
|
||||||
const em = this.getEntityManager();
|
if (options?.preventOverwrite && (await this.findOne(entity))) {
|
||||||
em.persist(entity);
|
throw new EntityAlreadyExistsException(`A ${this.getEntityName()} with this identifier already exists.`);
|
||||||
await em.flush();
|
}
|
||||||
|
await this.getEntityManager().persistAndFlush(entity);
|
||||||
}
|
}
|
||||||
public async deleteWhere(query: FilterQuery<T>) {
|
public async deleteWhere(query: FilterQuery<T>): Promise<void> {
|
||||||
const toDelete = await this.findOne(query);
|
const toDelete = await this.findOne(query);
|
||||||
const em = this.getEntityManager();
|
const em = this.getEntityManager();
|
||||||
if (toDelete) {
|
if (toDelete) {
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,43 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { Answer } from '../../entities/questions/answer.entity.js';
|
import { Answer } from '../../entities/questions/answer.entity.js';
|
||||||
import { Question } from '../../entities/questions/question.entity.js';
|
import { Question } from '../../entities/questions/question.entity.js';
|
||||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
import { Teacher } from '../../entities/users/teacher.entity.js';
|
||||||
|
import { Loaded } from '@mikro-orm/core';
|
||||||
|
|
||||||
export class AnswerRepository extends DwengoEntityRepository<Answer> {
|
export class AnswerRepository extends DwengoEntityRepository<Answer> {
|
||||||
public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
|
public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> {
|
||||||
const answerEntity = this.create({
|
const answerEntity = this.create({
|
||||||
toQuestion: answer.toQuestion,
|
toQuestion: answer.toQuestion,
|
||||||
author: answer.author,
|
author: answer.author,
|
||||||
content: answer.content,
|
content: answer.content,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
return this.insert(answerEntity);
|
await this.insert(answerEntity);
|
||||||
|
answerEntity.toQuestion = answer.toQuestion;
|
||||||
|
answerEntity.author = answer.author;
|
||||||
|
answerEntity.content = answer.content;
|
||||||
|
return answerEntity;
|
||||||
}
|
}
|
||||||
public findAllAnswersToQuestion(question: Question): Promise<Answer[]> {
|
public async findAllAnswersToQuestion(question: Question): Promise<Answer[]> {
|
||||||
return this.findAll({
|
return this.findAll({
|
||||||
where: { toQuestion: question },
|
where: { toQuestion: question },
|
||||||
orderBy: { sequenceNumber: 'ASC' },
|
orderBy: { sequenceNumber: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
|
public async findAnswer(question: Question, sequenceNumber: number): Promise<Loaded<Answer> | null> {
|
||||||
|
return this.findOne({
|
||||||
|
toQuestion: question,
|
||||||
|
sequenceNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
toQuestion: question,
|
toQuestion: question,
|
||||||
sequenceNumber: sequenceNumber,
|
sequenceNumber: sequenceNumber,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
public async updateContent(answer: Answer, newContent: string): Promise<Answer> {
|
||||||
|
answer.content = newContent;
|
||||||
|
await this.save(answer);
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,18 @@ import { Question } from '../../entities/questions/question.entity.js';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||||
|
import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
||||||
|
import { Loaded } from '@mikro-orm/core';
|
||||||
|
import { Group } from '../../entities/assignments/group.entity';
|
||||||
|
|
||||||
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||||
public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> {
|
public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise<Question> {
|
||||||
const questionEntity = this.create({
|
const questionEntity = this.create({
|
||||||
learningObjectHruid: question.loId.hruid,
|
learningObjectHruid: question.loId.hruid,
|
||||||
learningObjectLanguage: question.loId.language,
|
learningObjectLanguage: question.loId.language,
|
||||||
learningObjectVersion: question.loId.version,
|
learningObjectVersion: question.loId.version,
|
||||||
author: question.author,
|
author: question.author,
|
||||||
|
inGroup: question.inGroup,
|
||||||
content: question.content,
|
content: question.content,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
|
|
@ -18,10 +22,11 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||||
questionEntity.learningObjectLanguage = question.loId.language;
|
questionEntity.learningObjectLanguage = question.loId.language;
|
||||||
questionEntity.learningObjectVersion = question.loId.version;
|
questionEntity.learningObjectVersion = question.loId.version;
|
||||||
questionEntity.author = question.author;
|
questionEntity.author = question.author;
|
||||||
|
questionEntity.inGroup = question.inGroup;
|
||||||
questionEntity.content = question.content;
|
questionEntity.content = question.content;
|
||||||
return this.insert(questionEntity);
|
return await this.insert(questionEntity);
|
||||||
}
|
}
|
||||||
public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
|
public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> {
|
||||||
return this.findAll({
|
return this.findAll({
|
||||||
where: {
|
where: {
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
|
|
@ -33,7 +38,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> {
|
public async removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> {
|
||||||
return this.deleteWhere({
|
return this.deleteWhere({
|
||||||
learningObjectHruid: loId.hruid,
|
learningObjectHruid: loId.hruid,
|
||||||
learningObjectLanguage: loId.language,
|
learningObjectLanguage: loId.language,
|
||||||
|
|
@ -54,4 +59,73 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||||
orderBy: { timestamp: 'ASC' },
|
orderBy: { timestamp: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async findAllByAssignment(assignment: Assignment): Promise<Question[]> {
|
||||||
|
return this.find({
|
||||||
|
inGroup: assignment.groups.getItems(),
|
||||||
|
learningObjectHruid: assignment.learningPathHruid,
|
||||||
|
learningObjectLanguage: assignment.learningPathLanguage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findAllByAuthor(author: Student): Promise<Question[]> {
|
||||||
|
return this.findAll({
|
||||||
|
where: { author },
|
||||||
|
orderBy: { timestamp: 'DESC' }, // New to old
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findAllByGroup(inGroup: Group): Promise<Question[]> {
|
||||||
|
return this.findAll({
|
||||||
|
where: { inGroup },
|
||||||
|
orderBy: { timestamp: 'DESC' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up all questions for the given learning object which were asked as part of the given assignment.
|
||||||
|
* When forStudentUsername is set, only the questions within the given user's group are shown.
|
||||||
|
*/
|
||||||
|
public async findAllQuestionsAboutLearningObjectInAssignment(
|
||||||
|
loId: LearningObjectIdentifier,
|
||||||
|
assignment: Assignment,
|
||||||
|
forStudentUsername?: string
|
||||||
|
): Promise<Question[]> {
|
||||||
|
const inGroup = forStudentUsername
|
||||||
|
? {
|
||||||
|
assignment,
|
||||||
|
members: {
|
||||||
|
$some: {
|
||||||
|
username: forStudentUsername,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
assignment,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.findAll({
|
||||||
|
where: {
|
||||||
|
learningObjectHruid: loId.hruid,
|
||||||
|
learningObjectLanguage: loId.language,
|
||||||
|
learningObjectVersion: loId.version,
|
||||||
|
inGroup,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<Loaded<Question> | null> {
|
||||||
|
return this.findOne({
|
||||||
|
learningObjectHruid: loId.hruid,
|
||||||
|
learningObjectLanguage: loId.language,
|
||||||
|
learningObjectVersion: loId.version,
|
||||||
|
sequenceNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateContent(question: Question, newContent: string): Promise<Question> {
|
||||||
|
question.content = newContent;
|
||||||
|
await this.save(question);
|
||||||
|
return question;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -36,8 +34,8 @@ let entityManager: EntityManager | undefined;
|
||||||
/**
|
/**
|
||||||
* Execute all the database operations within the function f in a single transaction.
|
* Execute all the database operations within the function f in a single transaction.
|
||||||
*/
|
*/
|
||||||
export function transactional<T>(f: () => Promise<T>) {
|
export async function transactional<T>(f: () => Promise<T>): Promise<void> {
|
||||||
entityManager?.transactional(f);
|
await entityManager?.transactional(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R {
|
function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
export interface Theme {
|
import { Theme } from '@dwengo-1/common/interfaces/theme';
|
||||||
title: string;
|
|
||||||
hruids: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const themes: Theme[] = [
|
export const themes: Theme[] = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
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';
|
|
||||||
|
|
||||||
// Export class StudentRepository extends UserRepository<Student> {}
|
|
||||||
|
|
||||||
export class StudentRepository extends DwengoEntityRepository<Student> {
|
export class StudentRepository extends DwengoEntityRepository<Student> {
|
||||||
public findByUsername(username: string): Promise<Student | null> {
|
public async findByUsername(username: string): Promise<Student | null> {
|
||||||
return this.findOne({ username: username });
|
return this.findOne({ username: username });
|
||||||
}
|
}
|
||||||
public deleteByUsername(username: string): Promise<void> {
|
public async deleteByUsername(username: string): Promise<void> {
|
||||||
return this.deleteWhere({ username: username });
|
return this.deleteWhere({ username: username });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
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 async findByUsername(username: string): Promise<Teacher | null> {
|
||||||
return this.findOne({ username: username });
|
return this.findOne({ username: username });
|
||||||
}
|
}
|
||||||
public deleteByUsername(username: string): Promise<void> {
|
public async deleteByUsername(username: string): Promise<void> {
|
||||||
return this.deleteWhere({ username: username });
|
return this.deleteWhere({ username: username });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { User } from '../../entities/users/user.entity.js';
|
import { User } from '../../entities/users/user.entity.js';
|
||||||
|
|
||||||
export class UserRepository<T extends User> extends DwengoEntityRepository<T> {
|
export class UserRepository<T extends User> extends DwengoEntityRepository<T> {
|
||||||
public findByUsername(username: string): Promise<T | null> {
|
public async findByUsername(username: string): Promise<T | null> {
|
||||||
return this.findOne({ username } as Partial<T>);
|
return this.findOne({ username } as Partial<T>);
|
||||||
}
|
}
|
||||||
public deleteByUsername(username: string): Promise<void> {
|
public async deleteByUsername(username: string): Promise<void> {
|
||||||
return this.deleteWhere({ username } as Partial<T>);
|
return this.deleteWhere({ username } as Partial<T>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
import { Cascade, Collection, 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 '@dwengo-1/common/util/language';
|
||||||
import { AssignmentRepository } from '../../data/assignments/assignment-repository.js';
|
import { AssignmentRepository } from '../../data/assignments/assignment-repository.js';
|
||||||
|
|
||||||
@Entity({
|
@Entity({
|
||||||
|
|
@ -14,7 +14,7 @@ export class Assignment {
|
||||||
})
|
})
|
||||||
within!: Class;
|
within!: Class;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'number', autoincrement: true })
|
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
|
|
@ -34,6 +34,7 @@ export class Assignment {
|
||||||
@OneToMany({
|
@OneToMany({
|
||||||
entity: () => Group,
|
entity: () => Group,
|
||||||
mappedBy: 'assignment',
|
mappedBy: 'assignment',
|
||||||
|
cascade: [Cascade.ALL],
|
||||||
})
|
})
|
||||||
groups!: Group[];
|
groups: Collection<Group> = new Collection<Group>(this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,23 @@ import { GroupRepository } from '../../data/assignments/group-repository.js';
|
||||||
repository: () => GroupRepository,
|
repository: () => GroupRepository,
|
||||||
})
|
})
|
||||||
export class Group {
|
export class Group {
|
||||||
|
/*
|
||||||
|
WARNING: Don't move the definition of groupNumber! If it does not come before the definition of assignment,
|
||||||
|
creating groups fails because of a MikroORM bug!
|
||||||
|
*/
|
||||||
|
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||||
|
groupNumber?: number;
|
||||||
|
|
||||||
@ManyToOne({
|
@ManyToOne({
|
||||||
entity: () => Assignment,
|
entity: () => Assignment,
|
||||||
primary: true,
|
primary: true,
|
||||||
})
|
})
|
||||||
assignment!: Assignment;
|
assignment!: Assignment;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
|
||||||
groupNumber?: number;
|
|
||||||
|
|
||||||
@ManyToMany({
|
@ManyToMany({
|
||||||
entity: () => Student,
|
entity: () => Student,
|
||||||
|
owner: true,
|
||||||
|
inversedBy: 'groups',
|
||||||
})
|
})
|
||||||
members!: Student[];
|
members: Collection<Student> = new Collection<Student>(this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import { Student } from '../users/student.entity.js';
|
import { Student } from '../users/student.entity.js';
|
||||||
import { Group } from './group.entity.js';
|
import { Group } from './group.entity.js';
|
||||||
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
import { Entity, Enum, ManyToOne, PrimaryKey, Property, Cascade } from '@mikro-orm/core';
|
||||||
import { Language } from '../content/language.js';
|
|
||||||
import { SubmissionRepository } from '../../data/assignments/submission-repository.js';
|
import { SubmissionRepository } from '../../data/assignments/submission-repository.js';
|
||||||
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
|
|
||||||
@Entity({ repository: () => SubmissionRepository })
|
@Entity({ repository: () => SubmissionRepository })
|
||||||
export class Submission {
|
export class Submission {
|
||||||
|
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||||
|
submissionNumber?: number;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'string' })
|
@PrimaryKey({ type: 'string' })
|
||||||
learningObjectHruid!: string;
|
learningObjectHruid!: string;
|
||||||
|
|
||||||
|
|
@ -15,11 +18,13 @@ export class Submission {
|
||||||
})
|
})
|
||||||
learningObjectLanguage!: Language;
|
learningObjectLanguage!: Language;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'numeric' })
|
@PrimaryKey({ type: 'numeric', autoincrement: false })
|
||||||
learningObjectVersion: number = 1;
|
learningObjectVersion = 1;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
@ManyToOne(() => Group, {
|
||||||
submissionNumber!: number;
|
cascade: [Cascade.REMOVE],
|
||||||
|
})
|
||||||
|
onBehalfOf!: Group;
|
||||||
|
|
||||||
@ManyToOne({
|
@ManyToOne({
|
||||||
entity: () => Student,
|
entity: () => Student,
|
||||||
|
|
@ -29,12 +34,6 @@ export class Submission {
|
||||||
@Property({ type: 'datetime' })
|
@Property({ type: 'datetime' })
|
||||||
submissionTime!: Date;
|
submissionTime!: Date;
|
||||||
|
|
||||||
@ManyToOne({
|
|
||||||
entity: () => Group,
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
onBehalfOf?: Group;
|
|
||||||
|
|
||||||
@Property({ type: 'json' })
|
@Property({ type: 'json' })
|
||||||
content!: string;
|
content!: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Entity, Enum, ManyToOne } from '@mikro-orm/core';
|
||||||
import { Student } from '../users/student.entity.js';
|
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';
|
||||||
|
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||||
|
|
||||||
@Entity({
|
@Entity({
|
||||||
repository: () => ClassJoinRequestRepository,
|
repository: () => ClassJoinRequestRepository,
|
||||||
|
|
@ -19,12 +20,6 @@ export class ClassJoinRequest {
|
||||||
})
|
})
|
||||||
class!: Class;
|
class!: Class;
|
||||||
|
|
||||||
@Enum(() => ClassJoinRequestStatus)
|
@Enum(() => ClassStatus)
|
||||||
status!: ClassJoinRequestStatus;
|
status!: ClassStatus;
|
||||||
}
|
|
||||||
|
|
||||||
export enum ClassJoinRequestStatus {
|
|
||||||
Open = 'open',
|
|
||||||
Accepted = 'accepted',
|
|
||||||
Declined = 'declined',
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ export class Class {
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
displayName!: string;
|
displayName!: string;
|
||||||
|
|
||||||
@ManyToMany(() => Teacher)
|
@ManyToMany({ entity: () => Teacher, owner: true, inversedBy: 'classes' })
|
||||||
teachers!: Collection<Teacher>;
|
teachers!: Collection<Teacher>;
|
||||||
|
|
||||||
@ManyToMany(() => Student)
|
@ManyToMany({ entity: () => Student, owner: true, inversedBy: 'classes' })
|
||||||
students!: Collection<Student>;
|
students!: Collection<Student>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { Entity, ManyToOne } from '@mikro-orm/core';
|
import { Entity, Enum, ManyToOne } from '@mikro-orm/core';
|
||||||
import { Teacher } from '../users/teacher.entity.js';
|
import { Teacher } from '../users/teacher.entity.js';
|
||||||
import { Class } from './class.entity.js';
|
import { Class } from './class.entity.js';
|
||||||
import { TeacherInvitationRepository } from '../../data/classes/teacher-invitation-repository.js';
|
import { TeacherInvitationRepository } from '../../data/classes/teacher-invitation-repository.js';
|
||||||
|
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invitation of a teacher into a class (in order to teach it).
|
* Invitation of a teacher into a class (in order to teach it).
|
||||||
|
|
@ -25,4 +26,7 @@ export class TeacherInvitation {
|
||||||
primary: true,
|
primary: true,
|
||||||
})
|
})
|
||||||
class!: Class;
|
class!: Class;
|
||||||
|
|
||||||
|
@Enum(() => ClassStatus)
|
||||||
|
status!: ClassStatus;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
backend/src/entities/content/educational-goal.entity.ts
Normal file
10
backend/src/entities/content/educational-goal.entity.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Embeddable, Property } from '@mikro-orm/core';
|
||||||
|
|
||||||
|
@Embeddable()
|
||||||
|
export class EducationalGoal {
|
||||||
|
@Property({ type: 'string' })
|
||||||
|
source!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'string' })
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { Language } from './language.js';
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
|
|
||||||
export class LearningObjectIdentifier {
|
export class LearningObjectIdentifier {
|
||||||
constructor(
|
constructor(
|
||||||
public hruid: string,
|
public hruid: string,
|
||||||
public language: Language,
|
public language: Language,
|
||||||
public version: number
|
public version: number
|
||||||
) {}
|
) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,12 @@
|
||||||
import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
import { ArrayType, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||||
import { Language } from './language.js';
|
|
||||||
import { Attachment } from './attachment.entity.js';
|
import { Attachment } from './attachment.entity.js';
|
||||||
import { Teacher } from '../users/teacher.entity.js';
|
import { Teacher } from '../users/teacher.entity.js';
|
||||||
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
|
import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { LearningObjectRepository } from '../../data/content/learning-object-repository.js';
|
import { LearningObjectRepository } from '../../data/content/learning-object-repository.js';
|
||||||
|
import { EducationalGoal } from './educational-goal.entity.js';
|
||||||
@Embeddable()
|
import { ReturnValue } from './return-value.entity.js';
|
||||||
export class EducationalGoal {
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
@Property({ type: 'string' })
|
|
||||||
source!: string;
|
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
|
||||||
id!: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Embeddable()
|
|
||||||
export class ReturnValue {
|
|
||||||
@Property({ type: 'string' })
|
|
||||||
callbackUrl!: string;
|
|
||||||
|
|
||||||
@Property({ type: 'json' })
|
|
||||||
callbackSchema!: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Entity({ repository: () => LearningObjectRepository })
|
@Entity({ repository: () => LearningObjectRepository })
|
||||||
export class LearningObject {
|
export class LearningObject {
|
||||||
|
|
@ -36,7 +20,7 @@ export class LearningObject {
|
||||||
language!: Language;
|
language!: Language;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'number' })
|
@PrimaryKey({ type: 'number' })
|
||||||
version: number = 1;
|
version = 1;
|
||||||
|
|
||||||
@Property({ type: 'uuid', unique: true })
|
@Property({ type: 'uuid', unique: true })
|
||||||
uuid = v4();
|
uuid = v4();
|
||||||
|
|
@ -58,11 +42,11 @@ export class LearningObject {
|
||||||
@Property({ type: 'array' })
|
@Property({ type: 'array' })
|
||||||
keywords: string[] = [];
|
keywords: string[] = [];
|
||||||
|
|
||||||
@Property({ type: 'array', nullable: true })
|
@Property({ type: new ArrayType((i) => Number(i)), nullable: true })
|
||||||
targetAges?: number[] = [];
|
targetAges?: number[] = [];
|
||||||
|
|
||||||
@Property({ type: 'bool' })
|
@Property({ type: 'bool' })
|
||||||
teacherExclusive: boolean = false;
|
teacherExclusive = false;
|
||||||
|
|
||||||
@Property({ type: 'array' })
|
@Property({ type: 'array' })
|
||||||
skosConcepts: string[] = [];
|
skosConcepts: string[] = [];
|
||||||
|
|
@ -74,10 +58,10 @@ export class LearningObject {
|
||||||
educationalGoals: EducationalGoal[] = [];
|
educationalGoals: EducationalGoal[] = [];
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
copyright: string = '';
|
copyright = '';
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
license: string = '';
|
license = '';
|
||||||
|
|
||||||
@Property({ type: 'smallint', nullable: true })
|
@Property({ type: 'smallint', nullable: true })
|
||||||
difficulty?: number;
|
difficulty?: number;
|
||||||
|
|
@ -91,7 +75,7 @@ export class LearningObject {
|
||||||
returnValue!: ReturnValue;
|
returnValue!: ReturnValue;
|
||||||
|
|
||||||
@Property({ type: 'bool' })
|
@Property({ type: 'bool' })
|
||||||
available: boolean = true;
|
available = true;
|
||||||
|
|
||||||
@Property({ type: 'string', nullable: true })
|
@Property({ type: 'string', nullable: true })
|
||||||
contentLocation?: string;
|
contentLocation?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core';
|
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core';
|
||||||
import { Language } from './language.js';
|
|
||||||
import { LearningPath } from './learning-path.entity.js';
|
import { LearningPath } from './learning-path.entity.js';
|
||||||
import { LearningPathTransition } from './learning-path-transition.entity.js';
|
import { LearningPathTransition } from './learning-path-transition.entity.js';
|
||||||
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class LearningPathNode {
|
export class LearningPathNode {
|
||||||
|
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||||
|
nodeNumber?: number;
|
||||||
|
|
||||||
@ManyToOne({ entity: () => LearningPath, primary: true })
|
@ManyToOne({ entity: () => LearningPath, primary: true })
|
||||||
learningPath!: Rel<LearningPath>;
|
learningPath!: Rel<LearningPath>;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
|
||||||
nodeNumber!: number;
|
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
learningObjectHruid!: string;
|
learningObjectHruid!: string;
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@ export class LearningPathNode {
|
||||||
startNode!: boolean;
|
startNode!: boolean;
|
||||||
|
|
||||||
@OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' })
|
@OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' })
|
||||||
transitions: LearningPathTransition[] = [];
|
transitions!: Collection<LearningPathTransition>;
|
||||||
|
|
||||||
@Property({ length: 3 })
|
@Property({ length: 3 })
|
||||||
createdAt: Date = new Date();
|
createdAt: Date = new Date();
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ import { LearningPathNode } from './learning-path-node.entity.js';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class LearningPathTransition {
|
export class LearningPathTransition {
|
||||||
@ManyToOne({ entity: () => LearningPathNode, primary: true })
|
|
||||||
node!: Rel<LearningPathNode>;
|
|
||||||
|
|
||||||
@PrimaryKey({ type: 'numeric' })
|
@PrimaryKey({ type: 'numeric' })
|
||||||
transitionNumber!: number;
|
transitionNumber!: number;
|
||||||
|
|
||||||
|
@ManyToOne({ entity: () => LearningPathNode, primary: true })
|
||||||
|
node!: Rel<LearningPathNode>;
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
condition!: string;
|
condition!: string;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
import { Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||||
import { Language } from './language.js';
|
|
||||||
import { Teacher } from '../users/teacher.entity.js';
|
import { Teacher } from '../users/teacher.entity.js';
|
||||||
import { LearningPathRepository } from '../../data/content/learning-path-repository.js';
|
import { LearningPathRepository } from '../../data/content/learning-path-repository.js';
|
||||||
import { LearningPathNode } from './learning-path-node.entity.js';
|
import { LearningPathNode } from './learning-path-node.entity.js';
|
||||||
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
|
|
||||||
@Entity({ repository: () => LearningPathRepository })
|
@Entity({ repository: () => LearningPathRepository })
|
||||||
export class LearningPath {
|
export class LearningPath {
|
||||||
|
|
@ -25,5 +25,5 @@ export class LearningPath {
|
||||||
image: Buffer | null = null;
|
image: Buffer | null = null;
|
||||||
|
|
||||||
@OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' })
|
@OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' })
|
||||||
nodes: LearningPathNode[] = [];
|
nodes: Collection<LearningPathNode> = new Collection<LearningPathNode>(this);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
backend/src/entities/content/return-value.entity.ts
Normal file
10
backend/src/entities/content/return-value.entity.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Embeddable, Property } from '@mikro-orm/core';
|
||||||
|
|
||||||
|
@Embeddable()
|
||||||
|
export class ReturnValue {
|
||||||
|
@Property({ type: 'string' })
|
||||||
|
callbackUrl!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'json' })
|
||||||
|
callbackSchema!: string;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
||||||
import { Language } from '../content/language.js';
|
|
||||||
import { Student } from '../users/student.entity.js';
|
import { Student } from '../users/student.entity.js';
|
||||||
import { QuestionRepository } from '../../data/questions/question-repository.js';
|
import { QuestionRepository } from '../../data/questions/question-repository.js';
|
||||||
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
|
import { Group } from '../assignments/group.entity.js';
|
||||||
|
|
||||||
@Entity({ repository: () => QuestionRepository })
|
@Entity({ repository: () => QuestionRepository })
|
||||||
export class Question {
|
export class Question {
|
||||||
|
|
@ -15,11 +16,14 @@ export class Question {
|
||||||
learningObjectLanguage!: Language;
|
learningObjectLanguage!: Language;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'number' })
|
@PrimaryKey({ type: 'number' })
|
||||||
learningObjectVersion: number = 1;
|
learningObjectVersion = 1;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'integer', autoincrement: true })
|
@PrimaryKey({ type: 'integer', autoincrement: true })
|
||||||
sequenceNumber?: number;
|
sequenceNumber?: number;
|
||||||
|
|
||||||
|
@ManyToOne({ entity: () => Group })
|
||||||
|
inGroup!: Group;
|
||||||
|
|
||||||
@ManyToOne({
|
@ManyToOne({
|
||||||
entity: () => Student,
|
entity: () => Student,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,9 @@ import { StudentRepository } from '../../data/users/student-repository.js';
|
||||||
repository: () => StudentRepository,
|
repository: () => StudentRepository,
|
||||||
})
|
})
|
||||||
export class Student extends User {
|
export class Student extends User {
|
||||||
@ManyToMany(() => Class)
|
@ManyToMany({ entity: () => Class, mappedBy: 'students' })
|
||||||
classes!: Collection<Class>;
|
classes!: Collection<Class>;
|
||||||
|
|
||||||
@ManyToMany(() => Group)
|
@ManyToMany({ entity: () => Group, mappedBy: 'members' })
|
||||||
groups!: Collection<Group>;
|
groups: Collection<Group> = new Collection<Group>(this);
|
||||||
|
|
||||||
constructor(
|
|
||||||
public username: string,
|
|
||||||
public firstName: string,
|
|
||||||
public lastName: string
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,6 @@ import { TeacherRepository } from '../../data/users/teacher-repository.js';
|
||||||
|
|
||||||
@Entity({ repository: () => TeacherRepository })
|
@Entity({ repository: () => TeacherRepository })
|
||||||
export class Teacher extends User {
|
export class Teacher extends User {
|
||||||
@ManyToMany(() => Class)
|
@ManyToMany({ entity: () => Class, mappedBy: 'teachers' })
|
||||||
classes!: Collection<Class>;
|
classes!: Collection<Class>;
|
||||||
|
|
||||||
constructor(
|
|
||||||
public username: string,
|
|
||||||
public firstName: string,
|
|
||||||
public lastName: string
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ export abstract class User {
|
||||||
username!: string;
|
username!: string;
|
||||||
|
|
||||||
@Property()
|
@Property()
|
||||||
firstName: string = '';
|
firstName = '';
|
||||||
|
|
||||||
@Property()
|
@Property()
|
||||||
lastName: string = '';
|
lastName = '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
/**
|
|
||||||
* Exception for HTTP 400 Bad Request
|
|
||||||
*/
|
|
||||||
export class BadRequestException extends Error {
|
|
||||||
public status = 400;
|
|
||||||
|
|
||||||
constructor(error: string) {
|
|
||||||
super(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception for HTTP 401 Unauthorized
|
|
||||||
*/
|
|
||||||
export class UnauthorizedException extends Error {
|
|
||||||
status = 401;
|
|
||||||
constructor(message: string = 'Unauthorized') {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception for HTTP 403 Forbidden
|
|
||||||
*/
|
|
||||||
export class ForbiddenException extends Error {
|
|
||||||
status = 403;
|
|
||||||
|
|
||||||
constructor(message: string = 'Forbidden') {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception for HTTP 404 Not Found
|
|
||||||
*/
|
|
||||||
export class NotFoundException extends Error {
|
|
||||||
public status = 404;
|
|
||||||
|
|
||||||
constructor(error: string) {
|
|
||||||
super(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
backend/src/exceptions/bad-request-exception.ts
Normal file
10
backend/src/exceptions/bad-request-exception.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for HTTP 400 Bad Request
|
||||||
|
*/
|
||||||
|
export class BadRequestException extends ExceptionWithHttpState {
|
||||||
|
constructor(error: string) {
|
||||||
|
super(400, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/src/exceptions/conflict-exception.ts
Normal file
12
backend/src/exceptions/conflict-exception.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for HTTP 409 Conflict
|
||||||
|
*/
|
||||||
|
export class ConflictException extends ExceptionWithHttpState {
|
||||||
|
public status = 409;
|
||||||
|
|
||||||
|
constructor(error: string) {
|
||||||
|
super(409, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ConflictException } from './conflict-exception.js';
|
||||||
|
|
||||||
|
export class EntityAlreadyExistsException extends ConflictException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
backend/src/exceptions/exception-with-http-state.ts
Normal file
13
backend/src/exceptions/exception-with-http-state.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { HasStatusCode } from './has-status-code';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exceptions which are associated with a HTTP error code.
|
||||||
|
*/
|
||||||
|
export abstract class ExceptionWithHttpState extends Error implements HasStatusCode {
|
||||||
|
constructor(
|
||||||
|
public status: number,
|
||||||
|
public error: string
|
||||||
|
) {
|
||||||
|
super(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/src/exceptions/forbidden-exception.ts
Normal file
12
backend/src/exceptions/forbidden-exception.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for HTTP 403 Forbidden
|
||||||
|
*/
|
||||||
|
export class ForbiddenException extends ExceptionWithHttpState {
|
||||||
|
status = 403;
|
||||||
|
|
||||||
|
constructor(message = 'Forbidden') {
|
||||||
|
super(403, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
backend/src/exceptions/has-status-code.ts
Normal file
6
backend/src/exceptions/has-status-code.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface HasStatusCode {
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
export function hasStatusCode(err: unknown): err is HasStatusCode {
|
||||||
|
return typeof err === 'object' && err !== null && 'status' in err && typeof (err as HasStatusCode)?.status === 'number';
|
||||||
|
}
|
||||||
12
backend/src/exceptions/not-found-exception.ts
Normal file
12
backend/src/exceptions/not-found-exception.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for HTTP 404 Not Found
|
||||||
|
*/
|
||||||
|
export class NotFoundException extends ExceptionWithHttpState {
|
||||||
|
public status = 404;
|
||||||
|
|
||||||
|
constructor(error: string) {
|
||||||
|
super(404, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/src/exceptions/server-error-exception.ts
Normal file
12
backend/src/exceptions/server-error-exception.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for HTTP 500 Internal Server Error
|
||||||
|
*/
|
||||||
|
export class ServerErrorException extends ExceptionWithHttpState {
|
||||||
|
status = 500;
|
||||||
|
|
||||||
|
constructor(message = 'Internal server error, something went wrong') {
|
||||||
|
super(500, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
backend/src/exceptions/unauthorized-exception.ts
Normal file
10
backend/src/exceptions/unauthorized-exception.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { ExceptionWithHttpState } from './exception-with-http-state.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for HTTP 401 Unauthorized
|
||||||
|
*/
|
||||||
|
export class UnauthorizedException extends ExceptionWithHttpState {
|
||||||
|
constructor(message = 'Unauthorized') {
|
||||||
|
super(401, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,14 @@
|
||||||
import { mapToUserDTO, UserDTO } from './user.js';
|
import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js';
|
||||||
import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from './question.js';
|
|
||||||
import { Answer } from '../entities/questions/answer.entity.js';
|
import { Answer } from '../entities/questions/answer.entity.js';
|
||||||
|
import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
|
||||||
export interface AnswerDTO {
|
import { mapToTeacherDTO } from './teacher.js';
|
||||||
author: UserDTO;
|
|
||||||
toQuestion: QuestionDTO;
|
|
||||||
sequenceNumber: number;
|
|
||||||
timestamp: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a Question entity to a DTO format.
|
* Convert a Question entity to a DTO format.
|
||||||
*/
|
*/
|
||||||
export function mapToAnswerDTO(answer: Answer): AnswerDTO {
|
export function mapToAnswerDTO(answer: Answer): AnswerDTO {
|
||||||
return {
|
return {
|
||||||
author: mapToUserDTO(answer.author),
|
author: mapToTeacherDTO(answer.author),
|
||||||
toQuestion: mapToQuestionDTO(answer.toQuestion),
|
toQuestion: mapToQuestionDTO(answer.toQuestion),
|
||||||
sequenceNumber: answer.sequenceNumber!,
|
sequenceNumber: answer.sequenceNumber!,
|
||||||
timestamp: answer.timestamp.toISOString(),
|
timestamp: answer.timestamp.toISOString(),
|
||||||
|
|
@ -23,16 +16,10 @@ export function mapToAnswerDTO(answer: Answer): AnswerDTO {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnswerId {
|
export function mapToAnswerDTOId(answer: Answer): AnswerId {
|
||||||
author: string;
|
|
||||||
toQuestion: QuestionId;
|
|
||||||
sequenceNumber: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapToAnswerId(answer: AnswerDTO): AnswerId {
|
|
||||||
return {
|
return {
|
||||||
author: answer.author.username,
|
author: answer.author.username,
|
||||||
toQuestion: mapToQuestionId(answer.toQuestion),
|
toQuestion: mapToQuestionDTOId(answer.toQuestion),
|
||||||
sequenceNumber: answer.sequenceNumber,
|
sequenceNumber: answer.sequenceNumber!,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,36 @@
|
||||||
import { FALLBACK_LANG } from '../config.js';
|
import { languageMap } from '@dwengo-1/common/util/language';
|
||||||
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 { AssignmentDTO, AssignmentDTOId } from '@dwengo-1/common/interfaces/assignment';
|
||||||
import { GroupDTO, mapToGroupDTO } from './group.js';
|
import { mapToGroupDTO } from './group.js';
|
||||||
|
import { getAssignmentRepository } from '../data/repositories.js';
|
||||||
|
|
||||||
export interface AssignmentDTO {
|
export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTOId {
|
||||||
id: number;
|
|
||||||
class: string; // Id of class 'within'
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
learningPath: string;
|
|
||||||
language: string;
|
|
||||||
groups?: GroupDTO[] | string[]; // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO {
|
|
||||||
return {
|
return {
|
||||||
id: assignment.id!,
|
id: assignment.id!,
|
||||||
class: assignment.within.classId!,
|
within: assignment.within.classId!,
|
||||||
title: assignment.title,
|
|
||||||
description: assignment.description,
|
|
||||||
learningPath: assignment.learningPathHruid,
|
|
||||||
language: assignment.learningPathLanguage,
|
|
||||||
// Groups: assignment.groups.map(group => group.groupNumber),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
|
export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
|
||||||
return {
|
return {
|
||||||
id: assignment.id!,
|
id: assignment.id!,
|
||||||
class: assignment.within.classId!,
|
within: assignment.within.classId!,
|
||||||
title: assignment.title,
|
title: assignment.title,
|
||||||
description: assignment.description,
|
description: assignment.description,
|
||||||
learningPath: assignment.learningPathHruid,
|
learningPath: assignment.learningPathHruid,
|
||||||
language: assignment.learningPathLanguage,
|
language: assignment.learningPathLanguage,
|
||||||
// Groups: assignment.groups.map(mapToGroupDTO),
|
groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assignment {
|
export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assignment {
|
||||||
const assignment = new Assignment();
|
return getAssignmentRepository().create({
|
||||||
assignment.title = assignmentData.title;
|
within: cls,
|
||||||
assignment.description = assignmentData.description;
|
title: assignmentData.title,
|
||||||
assignment.learningPathHruid = assignmentData.learningPath;
|
description: assignmentData.description,
|
||||||
assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG;
|
learningPathHruid: assignmentData.learningPath,
|
||||||
assignment.within = cls;
|
learningPathLanguage: languageMap[assignmentData.language],
|
||||||
|
groups: [],
|
||||||
console.log(assignment);
|
});
|
||||||
|
|
||||||
return assignment;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,7 @@ import { Collection } from '@mikro-orm/core';
|
||||||
import { Class } from '../entities/classes/class.entity.js';
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
import { Student } from '../entities/users/student.entity.js';
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||||
|
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
|
||||||
export interface ClassDTO {
|
|
||||||
id: string;
|
|
||||||
displayName: string;
|
|
||||||
teachers: string[];
|
|
||||||
students: string[];
|
|
||||||
joinRequests: string[];
|
|
||||||
endpoints?: {
|
|
||||||
self: string;
|
|
||||||
invitations: string;
|
|
||||||
assignments: string;
|
|
||||||
students: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapToClassDTO(cls: Class): ClassDTO {
|
export function mapToClassDTO(cls: Class): ClassDTO {
|
||||||
return {
|
return {
|
||||||
|
|
@ -23,7 +10,6 @@ export function mapToClassDTO(cls: Class): ClassDTO {
|
||||||
displayName: cls.displayName,
|
displayName: cls.displayName,
|
||||||
teachers: cls.teachers.map((teacher) => teacher.username),
|
teachers: cls.teachers.map((teacher) => teacher.username),
|
||||||
students: cls.students.map((student) => student.username),
|
students: cls.students.map((student) => student.username),
|
||||||
joinRequests: [], // TODO
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,46 @@
|
||||||
import { Group } from '../entities/assignments/group.entity.js';
|
import { Group } from '../entities/assignments/group.entity.js';
|
||||||
import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js';
|
import { mapToAssignment } from './assignment.js';
|
||||||
import { mapToStudentDTO, StudentDTO } from './student.js';
|
import { mapToStudent } from './student.js';
|
||||||
|
import { mapToStudentDTO } from './student.js';
|
||||||
|
import { GroupDTO, GroupDTOId } from '@dwengo-1/common/interfaces/group';
|
||||||
|
import { getGroupRepository } from '../data/repositories.js';
|
||||||
|
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
|
||||||
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
|
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
||||||
|
|
||||||
export interface GroupDTO {
|
export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group {
|
||||||
assignment: number | AssignmentDTO;
|
const assignmentDto = groupDto.assignment as AssignmentDTO;
|
||||||
groupNumber: number;
|
|
||||||
members: string[] | StudentDTO[];
|
return getGroupRepository().create({
|
||||||
|
groupNumber: groupDto.groupNumber,
|
||||||
|
assignment: mapToAssignment(assignmentDto, clazz),
|
||||||
|
members: groupDto.members!.map((studentDto) => mapToStudent(studentDto as StudentDTO)),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToGroupDTO(group: Group): GroupDTO {
|
export function mapToGroupDTO(group: Group, cls: Class): GroupDTO {
|
||||||
return {
|
return {
|
||||||
assignment: mapToAssignmentDTO(group.assignment), // ERROR: , group.assignment.within),
|
class: cls.classId!,
|
||||||
|
assignment: group.assignment.id!,
|
||||||
groupNumber: group.groupNumber!,
|
groupNumber: group.groupNumber!,
|
||||||
members: group.members.map(mapToStudentDTO),
|
members: group.members.map(mapToStudentDTO),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToGroupDTOId(group: Group): GroupDTO {
|
export function mapToGroupDTOId(group: Group, cls: Class): GroupDTOId {
|
||||||
return {
|
return {
|
||||||
|
class: cls.classId!,
|
||||||
|
assignment: group.assignment.id!,
|
||||||
|
groupNumber: group.groupNumber!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map to group DTO where other objects are only referenced by their id.
|
||||||
|
*/
|
||||||
|
export function mapToShallowGroupDTO(group: Group): GroupDTO {
|
||||||
|
return {
|
||||||
|
class: group.assignment.within.classId!,
|
||||||
assignment: group.assignment.id!,
|
assignment: group.assignment.id!,
|
||||||
groupNumber: group.groupNumber!,
|
groupNumber: group.groupNumber!,
|
||||||
members: group.members.map((member) => member.username),
|
members: group.members.map((member) => member.username),
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,47 @@
|
||||||
import { Question } from '../entities/questions/question.entity.js';
|
import { Question } from '../entities/questions/question.entity.js';
|
||||||
import { UserDTO } from './user.js';
|
import { mapToStudentDTO } from './student.js';
|
||||||
|
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
||||||
|
import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content';
|
||||||
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 { mapToGroupDTOId } from './group.js';
|
||||||
import { TeacherDTO } from './teacher.js';
|
|
||||||
|
|
||||||
export interface QuestionDTO {
|
function getLearningObjectIdentifier(question: Question): LearningObjectIdentifierDTO {
|
||||||
learningObjectIdentifier: LearningObjectIdentifier;
|
return {
|
||||||
sequenceNumber?: number;
|
hruid: question.learningObjectHruid,
|
||||||
author: StudentDTO;
|
language: question.learningObjectLanguage,
|
||||||
timestamp?: string;
|
version: question.learningObjectVersion,
|
||||||
content: string;
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToLearningObjectID(loID: LearningObjectIdentifierDTO): LearningObjectIdentifier {
|
||||||
|
return {
|
||||||
|
hruid: loID.hruid,
|
||||||
|
language: loID.language,
|
||||||
|
version: loID.version ?? 1,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a Question entity to a DTO format.
|
* Convert a Question entity to a DTO format.
|
||||||
*/
|
*/
|
||||||
export function mapToQuestionDTO(question: Question): QuestionDTO {
|
export function mapToQuestionDTO(question: Question): QuestionDTO {
|
||||||
const learningObjectIdentifier = {
|
const learningObjectIdentifier = getLearningObjectIdentifier(question);
|
||||||
hruid: question.learningObjectHruid,
|
|
||||||
language: question.learningObjectLanguage,
|
|
||||||
version: question.learningObjectVersion,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
learningObjectIdentifier,
|
learningObjectIdentifier,
|
||||||
sequenceNumber: question.sequenceNumber!,
|
sequenceNumber: question.sequenceNumber!,
|
||||||
author: mapToStudentDTO(question.author),
|
author: mapToStudentDTO(question.author),
|
||||||
|
inGroup: mapToGroupDTOId(question.inGroup, question.inGroup.assignment?.within),
|
||||||
timestamp: question.timestamp.toISOString(),
|
timestamp: question.timestamp.toISOString(),
|
||||||
content: question.content,
|
content: question.content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuestionId {
|
export function mapToQuestionDTOId(question: Question): QuestionId {
|
||||||
learningObjectIdentifier: LearningObjectIdentifier;
|
const learningObjectIdentifier = getLearningObjectIdentifier(question);
|
||||||
sequenceNumber: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapToQuestionId(question: QuestionDTO): QuestionId {
|
|
||||||
return {
|
return {
|
||||||
learningObjectIdentifier: question.learningObjectIdentifier,
|
learningObjectIdentifier,
|
||||||
sequenceNumber: question.sequenceNumber!,
|
sequenceNumber: question.sequenceNumber!,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
backend/src/interfaces/student-request.ts
Normal file
23
backend/src/interfaces/student-request.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { mapToStudentDTO } from './student.js';
|
||||||
|
import { ClassJoinRequest } from '../entities/classes/class-join-request.entity.js';
|
||||||
|
import { getClassJoinRequestRepository } from '../data/repositories.js';
|
||||||
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
|
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
|
||||||
|
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||||
|
|
||||||
|
export function mapToStudentRequestDTO(request: ClassJoinRequest): ClassJoinRequestDTO {
|
||||||
|
return {
|
||||||
|
requester: mapToStudentDTO(request.requester),
|
||||||
|
class: request.class.classId!,
|
||||||
|
status: request.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToStudentRequest(student: Student, cls: Class): ClassJoinRequest {
|
||||||
|
return getClassJoinRequestRepository().create({
|
||||||
|
requester: student,
|
||||||
|
class: cls,
|
||||||
|
status: ClassStatus.Open,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,6 @@
|
||||||
import { Student } from '../entities/users/student.entity.js';
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
|
import { getStudentRepository } from '../data/repositories.js';
|
||||||
export interface StudentDTO {
|
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
endpoints?: {
|
|
||||||
classes: string;
|
|
||||||
questions: string;
|
|
||||||
invitations: string;
|
|
||||||
groups: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapToStudentDTO(student: Student): StudentDTO {
|
export function mapToStudentDTO(student: Student): StudentDTO {
|
||||||
return {
|
return {
|
||||||
|
|
@ -23,7 +12,9 @@ export function mapToStudentDTO(student: Student): StudentDTO {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToStudent(studentData: StudentDTO): Student {
|
export function mapToStudent(studentData: StudentDTO): Student {
|
||||||
const student = new Student(studentData.username, studentData.firstName, studentData.lastName);
|
return getStudentRepository().create({
|
||||||
|
username: studentData.username,
|
||||||
return student;
|
firstName: studentData.firstName,
|
||||||
|
lastName: studentData.lastName,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,44 @@
|
||||||
import { Submission } from '../entities/assignments/submission.entity.js';
|
import { Submission } from '../entities/assignments/submission.entity.js';
|
||||||
import { Language } from '../entities/content/language.js';
|
import { mapToGroupDTOId } from './group.js';
|
||||||
import { GroupDTO, mapToGroupDTO } from './group.js';
|
import { mapToStudentDTO } from './student.js';
|
||||||
import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js';
|
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
|
||||||
import { mapToUser } from './user';
|
import { getSubmissionRepository } from '../data/repositories.js';
|
||||||
import { Student } from '../entities/users/student.entity';
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
|
import { Group } from '../entities/assignments/group.entity.js';
|
||||||
export interface SubmissionDTO {
|
|
||||||
learningObjectHruid: string;
|
|
||||||
learningObjectLanguage: Language;
|
|
||||||
learningObjectVersion: number;
|
|
||||||
|
|
||||||
submissionNumber?: number;
|
|
||||||
submitter: StudentDTO;
|
|
||||||
time?: Date;
|
|
||||||
group?: GroupDTO;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
||||||
|
return {
|
||||||
|
learningObjectIdentifier: {
|
||||||
|
hruid: submission.learningObjectHruid,
|
||||||
|
language: submission.learningObjectLanguage,
|
||||||
|
version: submission.learningObjectVersion,
|
||||||
|
},
|
||||||
|
submissionNumber: submission.submissionNumber,
|
||||||
|
submitter: mapToStudentDTO(submission.submitter),
|
||||||
|
time: submission.submissionTime,
|
||||||
|
group: submission.onBehalfOf ? mapToGroupDTOId(submission.onBehalfOf, submission.onBehalfOf.assignment.within) : undefined,
|
||||||
|
content: submission.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToSubmissionDTOId(submission: Submission): SubmissionDTOId {
|
||||||
return {
|
return {
|
||||||
learningObjectHruid: submission.learningObjectHruid,
|
learningObjectHruid: submission.learningObjectHruid,
|
||||||
learningObjectLanguage: submission.learningObjectLanguage,
|
learningObjectLanguage: submission.learningObjectLanguage,
|
||||||
learningObjectVersion: submission.learningObjectVersion,
|
learningObjectVersion: submission.learningObjectVersion,
|
||||||
|
|
||||||
submissionNumber: submission.submissionNumber,
|
submissionNumber: submission.submissionNumber,
|
||||||
submitter: mapToStudentDTO(submission.submitter),
|
|
||||||
time: submission.submissionTime,
|
|
||||||
group: submission.onBehalfOf ? mapToGroupDTO(submission.onBehalfOf) : undefined,
|
|
||||||
content: submission.content,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToSubmission(submissionDTO: SubmissionDTO): Submission {
|
export function mapToSubmission(submissionDTO: SubmissionDTO, submitter: Student, onBehalfOf: Group): Submission {
|
||||||
const submission = new Submission();
|
return getSubmissionRepository().create({
|
||||||
submission.learningObjectHruid = submissionDTO.learningObjectHruid;
|
learningObjectHruid: submissionDTO.learningObjectIdentifier.hruid,
|
||||||
submission.learningObjectLanguage = submissionDTO.learningObjectLanguage;
|
learningObjectLanguage: submissionDTO.learningObjectIdentifier.language,
|
||||||
submission.learningObjectVersion = submissionDTO.learningObjectVersion;
|
learningObjectVersion: submissionDTO.learningObjectIdentifier.version || 1,
|
||||||
// Submission.submissionNumber = submissionDTO.submissionNumber;
|
submitter: submitter,
|
||||||
submission.submitter = mapToStudent(submissionDTO.submitter);
|
submissionTime: new Date(),
|
||||||
// Submission.submissionTime = submissionDTO.time;
|
content: submissionDTO.content,
|
||||||
// Submission.onBehalfOf = submissionDTO.group!;
|
onBehalfOf: onBehalfOf,
|
||||||
// TODO fix group
|
});
|
||||||
submission.content = submissionDTO.content;
|
|
||||||
|
|
||||||
return submission;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,17 @@
|
||||||
import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js';
|
import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js';
|
||||||
import { ClassDTO, mapToClassDTO } from './class.js';
|
import { mapToUserDTO } from './user.js';
|
||||||
import { mapToUserDTO, UserDTO } from './user.js';
|
import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation';
|
||||||
|
import { getTeacherInvitationRepository } from '../data/repositories.js';
|
||||||
export interface TeacherInvitationDTO {
|
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||||
sender: string | UserDTO;
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
receiver: string | UserDTO;
|
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||||
class: string | ClassDTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO {
|
export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO {
|
||||||
return {
|
return {
|
||||||
sender: mapToUserDTO(invitation.sender),
|
sender: mapToUserDTO(invitation.sender),
|
||||||
receiver: mapToUserDTO(invitation.receiver),
|
receiver: mapToUserDTO(invitation.receiver),
|
||||||
class: mapToClassDTO(invitation.class),
|
classId: invitation.class.classId!,
|
||||||
|
status: invitation.status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,6 +19,16 @@ export function mapToTeacherInvitationDTOIds(invitation: TeacherInvitation): Tea
|
||||||
return {
|
return {
|
||||||
sender: invitation.sender.username,
|
sender: invitation.sender.username,
|
||||||
receiver: invitation.receiver.username,
|
receiver: invitation.receiver.username,
|
||||||
class: invitation.class.classId!,
|
classId: invitation.class.classId!,
|
||||||
|
status: invitation.status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapToInvitation(sender: Teacher, receiver: Teacher, cls: Class): TeacherInvitation {
|
||||||
|
return getTeacherInvitationRepository().create({
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
class: cls,
|
||||||
|
status: ClassStatus.Open,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,6 @@
|
||||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||||
|
import { getTeacherRepository } from '../data/repositories.js';
|
||||||
export interface TeacherDTO {
|
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
endpoints?: {
|
|
||||||
classes: string;
|
|
||||||
questions: string;
|
|
||||||
invitations: string;
|
|
||||||
groups: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
|
export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
|
||||||
return {
|
return {
|
||||||
|
|
@ -22,8 +11,10 @@ export function mapToTeacherDTO(teacher: Teacher): TeacherDTO {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToTeacher(TeacherData: TeacherDTO): Teacher {
|
export function mapToTeacher(teacherData: TeacherDTO): Teacher {
|
||||||
const teacher = new Teacher(TeacherData.username, TeacherData.firstName, TeacherData.lastName);
|
return getTeacherRepository().create({
|
||||||
|
username: teacherData.username,
|
||||||
return teacher;
|
firstName: teacherData.firstName,
|
||||||
|
lastName: teacherData.lastName,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,5 @@
|
||||||
import { User } from '../entities/users/user.entity.js';
|
import { User } from '../entities/users/user.entity.js';
|
||||||
|
import { UserDTO } from '@dwengo-1/common/interfaces/user';
|
||||||
export interface UserDTO {
|
|
||||||
id?: string;
|
|
||||||
username: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
endpoints?: {
|
|
||||||
self: string;
|
|
||||||
classes: string;
|
|
||||||
questions: string;
|
|
||||||
invitations: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapToUserDTO(user: User): UserDTO {
|
export function mapToUserDTO(user: User): UserDTO {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
@ -9,7 +9,7 @@ export class Logger extends WinstonLogger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Labels: LokiLabels = {
|
const lokiLabels: LokiLabels = {
|
||||||
source: 'Dwengo-Backend',
|
source: 'Dwengo-Backend',
|
||||||
service: 'API',
|
service: 'API',
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
|
|
@ -22,28 +22,40 @@ 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.simple()),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getEnvVar(envVars.RunMode) === 'dev') {
|
||||||
|
logger = createLogger({
|
||||||
|
transports: [consoleTransport],
|
||||||
|
});
|
||||||
|
logger.debug(`Logger initialized with level ${logLevel} to console`);
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lokiHost = getEnvVar(envVars.LokiHost);
|
||||||
|
|
||||||
const lokiTransport: LokiTransport = new LokiTransport({
|
const lokiTransport: LokiTransport = new LokiTransport({
|
||||||
host: LOKI_HOST,
|
host: lokiHost,
|
||||||
labels: Labels,
|
labels: lokiLabels,
|
||||||
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): void => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(`Connection error: ${err}`);
|
console.error(`Connection error: ${err}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,35 +5,54 @@ import { LokiLabels } from 'loki-logger-ts';
|
||||||
export class MikroOrmLogger extends DefaultLogger {
|
export class MikroOrmLogger extends DefaultLogger {
|
||||||
private logger: Logger = getLogger();
|
private logger: Logger = getLogger();
|
||||||
|
|
||||||
log(namespace: LoggerNamespace, message: string, context?: LogContext) {
|
static createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext): unknown {
|
||||||
|
const labels: LokiLabels = {
|
||||||
|
service: 'ORM',
|
||||||
|
};
|
||||||
|
|
||||||
|
let message: string;
|
||||||
|
if (context !== undefined && context.labels !== undefined) {
|
||||||
|
message = `[${namespace}] (${context.label}) ${messageArg}`;
|
||||||
|
} else {
|
||||||
|
message = `[${namespace}] ${messageArg}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: message,
|
||||||
|
labels: labels,
|
||||||
|
context: context,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
log(namespace: LoggerNamespace, message: string, context?: LogContext): void {
|
||||||
if (!this.isEnabled(namespace, context)) {
|
if (!this.isEnabled(namespace, context)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (namespace) {
|
switch (namespace) {
|
||||||
case 'query':
|
case 'query':
|
||||||
this.logger.debug(this.createMessage(namespace, message, context));
|
this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'query-params':
|
case 'query-params':
|
||||||
// TODO Which log level should this be?
|
// TODO Which log level should this be?
|
||||||
this.logger.info(this.createMessage(namespace, message, context));
|
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'schema':
|
case 'schema':
|
||||||
this.logger.info(this.createMessage(namespace, message, context));
|
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'discovery':
|
case 'discovery':
|
||||||
this.logger.debug(this.createMessage(namespace, message, context));
|
this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'info':
|
case 'info':
|
||||||
this.logger.info(this.createMessage(namespace, message, context));
|
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'deprecated':
|
case 'deprecated':
|
||||||
this.logger.warn(this.createMessage(namespace, message, context));
|
this.logger.warn(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
switch (context?.level) {
|
switch (context?.level) {
|
||||||
case 'info':
|
case 'info':
|
||||||
this.logger.info(this.createMessage(namespace, message, context));
|
this.logger.info(MikroOrmLogger.createMessage(namespace, message, context));
|
||||||
break;
|
break;
|
||||||
case 'warning':
|
case 'warning':
|
||||||
this.logger.warn(message);
|
this.logger.warn(message);
|
||||||
|
|
@ -47,23 +66,4 @@ export class MikroOrmLogger extends DefaultLogger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) {
|
|
||||||
const labels: LokiLabels = {
|
|
||||||
service: 'ORM',
|
|
||||||
};
|
|
||||||
|
|
||||||
let message: string;
|
|
||||||
if (context?.label) {
|
|
||||||
message = `[${namespace}] (${context?.label}) ${messageArg}`;
|
|
||||||
} else {
|
|
||||||
message = `[${namespace}] ${messageArg}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: message,
|
|
||||||
labels: labels,
|
|
||||||
context: context,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { getLogger, Logger } from './initalize.js';
|
import { getLogger, Logger } from './initalize.js';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
export function responseTimeLogger(req: Request, res: Response, time: number) {
|
export function responseTimeLogger(req: Request, res: Response, time: number): void {
|
||||||
const logger: Logger = getLogger();
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
const method = req.method;
|
const method = req.method;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { EnvVars, getEnvVar } from '../../util/envvars.js';
|
import { envVars, getEnvVar } from '../../util/envVars.js';
|
||||||
import { expressjwt } from 'express-jwt';
|
import { expressjwt } from 'express-jwt';
|
||||||
|
import * as jwt from 'jsonwebtoken';
|
||||||
import { JwtPayload } from 'jsonwebtoken';
|
import { JwtPayload } from 'jsonwebtoken';
|
||||||
import jwksClient from 'jwks-rsa';
|
import jwksClient from 'jwks-rsa';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as jwt from 'jsonwebtoken';
|
|
||||||
import { AuthenticatedRequest } from './authenticated-request.js';
|
import { AuthenticatedRequest } from './authenticated-request.js';
|
||||||
import { AuthenticationInfo } from './authentication-info.js';
|
import { AuthenticationInfo } from './authentication-info.js';
|
||||||
import { ForbiddenException, UnauthorizedException } from '../../exceptions.js';
|
import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js';
|
||||||
|
import { ForbiddenException } from '../../exceptions/forbidden-exception.js';
|
||||||
|
|
||||||
const JWKS_CACHE = true;
|
const JWKS_CACHE = true;
|
||||||
const JWKS_RATE_LIMIT = true;
|
const JWKS_RATE_LIMIT = true;
|
||||||
|
|
@ -32,12 +33,12 @@ function createJwksClient(uri: string): jwksClient.JwksClient {
|
||||||
|
|
||||||
const idpConfigs = {
|
const idpConfigs = {
|
||||||
student: {
|
student: {
|
||||||
issuer: getEnvVar(EnvVars.IdpStudentUrl),
|
issuer: getEnvVar(envVars.IdpStudentUrl),
|
||||||
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)),
|
jwksClient: createJwksClient(getEnvVar(envVars.IdpStudentJwksEndpoint)),
|
||||||
},
|
},
|
||||||
teacher: {
|
teacher: {
|
||||||
issuer: getEnvVar(EnvVars.IdpTeacherUrl),
|
issuer: getEnvVar(envVars.IdpTeacherUrl),
|
||||||
jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)),
|
jwksClient: createJwksClient(getEnvVar(envVars.IdpTeacherJwksEndpoint)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -47,14 +48,14 @@ const idpConfigs = {
|
||||||
const verifyJwtToken = expressjwt({
|
const verifyJwtToken = expressjwt({
|
||||||
secret: async (_: express.Request, token: jwt.Jwt | undefined) => {
|
secret: async (_: express.Request, token: jwt.Jwt | undefined) => {
|
||||||
if (!token?.payload || !(token.payload as JwtPayload).iss) {
|
if (!token?.payload || !(token.payload as JwtPayload).iss) {
|
||||||
throw new Error('Invalid token');
|
throw new UnauthorizedException('Invalid token.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const issuer = (token.payload as JwtPayload).iss;
|
const issuer = (token.payload as JwtPayload).iss;
|
||||||
|
|
||||||
const idpConfig = Object.values(idpConfigs).find((config) => config.issuer === issuer);
|
const idpConfig = Object.values(idpConfigs).find((config) => config.issuer === issuer);
|
||||||
if (!idpConfig) {
|
if (!idpConfig) {
|
||||||
throw new Error('Issuer not accepted.');
|
throw new UnauthorizedException('Issuer not accepted.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const signingKey = await idpConfig.jwksClient.getSigningKey(token.header.kid);
|
const signingKey = await idpConfig.jwksClient.getSigningKey(token.header.kid);
|
||||||
|
|
@ -63,7 +64,7 @@ const verifyJwtToken = expressjwt({
|
||||||
}
|
}
|
||||||
return signingKey.getPublicKey();
|
return signingKey.getPublicKey();
|
||||||
},
|
},
|
||||||
audience: getEnvVar(EnvVars.IdpAudience),
|
audience: getEnvVar(envVars.IdpAudience),
|
||||||
algorithms: [JWT_ALGORITHM],
|
algorithms: [JWT_ALGORITHM],
|
||||||
credentialsRequired: false,
|
credentialsRequired: false,
|
||||||
requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD,
|
requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD,
|
||||||
|
|
@ -74,7 +75,7 @@ const verifyJwtToken = expressjwt({
|
||||||
*/
|
*/
|
||||||
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
|
function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined {
|
||||||
if (!req.jwtPayload) {
|
if (!req.jwtPayload) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
const issuer = req.jwtPayload.iss;
|
const issuer = req.jwtPayload.iss;
|
||||||
let accountType: 'student' | 'teacher';
|
let accountType: 'student' | 'teacher';
|
||||||
|
|
@ -84,8 +85,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
|
||||||
} else if (issuer === idpConfigs.teacher.issuer) {
|
} else if (issuer === idpConfigs.teacher.issuer) {
|
||||||
accountType = 'teacher';
|
accountType = 'teacher';
|
||||||
} else {
|
} else {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accountType: accountType,
|
accountType: accountType,
|
||||||
username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!,
|
username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!,
|
||||||
|
|
@ -100,10 +102,10 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo |
|
||||||
* Add the AuthenticationInfo object with the information about the current authentication to the request in order
|
* Add the AuthenticationInfo object with the information about the current authentication to the request in order
|
||||||
* to avoid that the routers have to deal with the JWT token.
|
* to avoid that the routers have to deal with the JWT token.
|
||||||
*/
|
*/
|
||||||
const addAuthenticationInfo = (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => {
|
function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void {
|
||||||
req.auth = getAuthenticationInfo(req);
|
req.auth = getAuthenticationInfo(req);
|
||||||
next();
|
next();
|
||||||
};
|
}
|
||||||
|
|
||||||
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
|
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
|
||||||
|
|
||||||
|
|
@ -113,9 +115,8 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
|
||||||
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
|
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
|
||||||
* to true.
|
* to true.
|
||||||
*/
|
*/
|
||||||
export const authorize =
|
export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) {
|
||||||
(accessCondition: (auth: AuthenticationInfo) => boolean) =>
|
return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
|
||||||
(req: AuthenticatedRequest, res: express.Response, next: express.NextFunction): void => {
|
|
||||||
if (!req.auth) {
|
if (!req.auth) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
} else if (!accessCondition(req.auth)) {
|
} else if (!accessCondition(req.auth)) {
|
||||||
|
|
@ -124,6 +125,7 @@ export const authorize =
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware which rejects all unauthenticated users, but accepts all authenticated users.
|
* Middleware which rejects all unauthenticated users, but accepts all authenticated users.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
/**
|
/**
|
||||||
* Object with information about the user who is currently logged in.
|
* Object with information about the user who is currently logged in.
|
||||||
*/
|
*/
|
||||||
export type AuthenticationInfo = {
|
export interface AuthenticationInfo {
|
||||||
accountType: 'student' | 'teacher';
|
accountType: 'student' | 'teacher';
|
||||||
username: string;
|
username: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { EnvVars, getEnvVar } from '../util/envvars.js';
|
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||||
|
|
||||||
export default cors({
|
export default cors({
|
||||||
origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','),
|
origin: getEnvVar(envVars.CorsAllowedOrigins).split(','),
|
||||||
allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','),
|
allowedHeaders: getEnvVar(envVars.CorsAllowedHeaders).split(','),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
15
backend/src/middleware/error-handling/error-handler.ts
Normal file
15
backend/src/middleware/error-handling/error-handler.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
import { getLogger, Logger } from '../../logging/initalize.js';
|
||||||
|
import { hasStatusCode } from '../../exceptions/has-status-code.js';
|
||||||
|
|
||||||
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
|
export function errorHandler(err: unknown, _req: Request, res: Response, _: NextFunction): void {
|
||||||
|
if (hasStatusCode(err)) {
|
||||||
|
logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`);
|
||||||
|
res.status(err.status).json(err);
|
||||||
|
} else {
|
||||||
|
logger.error(`Unexpected error occurred while handing a request: ${(err as { stack: string })?.stack ?? JSON.stringify(err)}`);
|
||||||
|
res.status(500).json(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue