diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 865f4524..2eb42efa 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -13,7 +13,10 @@ jobs: - 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.yml -f compose.prod.yml up --build -d + run: docker compose -f compose.production.yml up --build -d \ No newline at end of file diff --git a/.github/workflows/lint-action.yml b/.github/workflows/lint-action.yml index 32823417..7fd77d28 100644 --- a/.github/workflows/lint-action.yml +++ b/.github/workflows/lint-action.yml @@ -43,6 +43,6 @@ jobs: with: auto_fix: true eslint: true - eslint_args: '--config eslint.config.ts' + eslint_args: "--config eslint.config.ts --ignore-pattern '**/prettier.config.js'" prettier: true - commit_message: 'style: fix linting issues met ${linter}' \ No newline at end of file + commit_message: 'style: fix linting issues met ${linter}' diff --git a/.gitignore b/.gitignore index d28e7d73..d3905d1f 100644 --- a/.gitignore +++ b/.gitignore @@ -737,4 +737,4 @@ flycheck_*.el # network security /network-security.data - +docs/.venv diff --git a/README.md b/README.md index 3526b53d..0499b037 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,8 @@ Om de applicatie lokaal te draaien als kant-en-klare Docker-containers: ```bash docker compose version git clone https://github.com/SELab-2/Dwengo-1.git -cd Dwengo-1/backend -cp .env.example .env -# Pas .env aan -nano .env -cd .. docker compose -f compose.staging.yml up --build +# Gebruikt backend/.env.staging ``` ### Handmatige installatie en ontwikkeling diff --git a/backend/.env.staging b/backend/.env.staging new file mode 100644 index 00000000..bedfb0b7 --- /dev/null +++ b/backend/.env.staging @@ -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 diff --git a/backend/.env.test b/backend/.env.test index b8a81003..535628cd 100644 --- a/backend/.env.test +++ b/backend/.env.test @@ -1,3 +1,13 @@ -PORT=3000 -DWENGO_DB_UPDATE=true +# +# Test environment configuration +# +# Should not need to be modified. +# See .env.example for more information. +# + +### Dwengo ### + +DWENGO_PORT=3000 + DWENGO_DB_NAME=":memory:" +DWENGO_DB_UPDATE=true diff --git a/backend/.env.test.example b/backend/.env.test.example deleted file mode 100644 index 535628cd..00000000 --- a/backend/.env.test.example +++ /dev/null @@ -1,13 +0,0 @@ -# -# 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 diff --git a/backend/Dockerfile b/backend/Dockerfile index 7c63c4b8..f09a89eb 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,38 +1,51 @@ FROM node:22 AS build-stage -WORKDIR /app +WORKDIR /app/dwengo # Install dependencies COPY package*.json ./ COPY backend/package.json ./backend/ +# Backend depends on common +COPY common/package.json ./common/ RUN npm install --silent # Build the backend # Root tsconfig.json -COPY tsconfig.json ./ +COPY tsconfig.json tsconfig.build.json ./ -WORKDIR /app/backend - -COPY backend ./ -COPY docs /app/docs +COPY backend ./backend +COPY common ./common +COPY docs ./docs RUN npm run build 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 package*.json ./ +COPY backend/package.json ./backend/ +# Backend depends on common +COPY common/package.json ./common/ RUN npm install --silent --only=production -COPY ./docs /docs -COPY ./backend/i18n /app/i18n -COPY --from=build-stage /app/backend/dist ./dist/ +COPY ./docs ./docs +COPY ./backend/i18n ./backend/i18n +COPY ./backend/.env ./backend/.env EXPOSE 3000 -CMD ["node", "--env-file=.env", "dist/app.js"] +CMD ["node", "--env-file=/app/dwengo/backend/.env", "/app/dwengo/backend/dist/app.js"] diff --git a/backend/README.md b/backend/README.md index 8a78ed14..ded42bd8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -34,7 +34,9 @@ npm run test:unit ```shell # Omgevingsvariabelen -cp .env.development.example .env +cp .env.example .env +# Configureer de .env file met de juiste waarden! +nano .env npm run build npm run start diff --git a/backend/config.js b/backend/config.js deleted file mode 100644 index be42027c..00000000 --- a/backend/config.js +++ /dev/null @@ -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; diff --git a/backend/package.json b/backend/package.json index c08fb1dc..83db321f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,16 +1,18 @@ { - "name": "dwengo-1-backend", + "name": "@dwengo-1/backend", "version": "0.1.1", "description": "Backend for Dwengo-1", "private": true, "type": "module", + "main": "dist/app.js", "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", "start": "cross-env NODE_ENV=production node --env-file=.env dist/app.js", "format": "prettier --write src/", "format-check": "prettier --check src/", "lint": "eslint . --fix", + "pretest:unit": "npm run build", "test:unit": "vitest --run" }, "dependencies": { @@ -24,6 +26,7 @@ "cross": "^1.0.0", "cross-env": "^7.0.3", "dotenv": "^16.4.7", + "dwengo-1-common": "^0.1.1", "express": "^5.0.1", "express-jwt": "^8.5.1", "gift-pegjs": "^1.0.2", diff --git a/backend/src/controllers/assignments.ts b/backend/src/controllers/assignments.ts index c8dd1ec8..1520fc10 100644 --- a/backend/src/controllers/assignments.ts +++ b/backend/src/controllers/assignments.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; -import { AssignmentDTO } from '../interfaces/assignment.js'; +import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; // Typescript is annoying with parameter forwarding from class.ts interface AssignmentParams { diff --git a/backend/src/controllers/classes.ts b/backend/src/controllers/classes.ts index 7526f7c4..a041bf22 100644 --- a/backend/src/controllers/classes.ts +++ b/backend/src/controllers/classes.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import { createClass, getAllClasses, getClass, getClassStudents, getClassStudentsIds, getClassTeacherInvitations } from '../services/classes.js'; -import { ClassDTO } from '../interfaces/class.js'; +import { ClassDTO } from '@dwengo-1/common/interfaces/class'; export async function getAllClassesHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; diff --git a/backend/src/controllers/error-helper.ts b/backend/src/controllers/error-helper.ts new file mode 100644 index 00000000..a902560f --- /dev/null +++ b/backend/src/controllers/error-helper.ts @@ -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): 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); + } +} diff --git a/backend/src/controllers/groups.ts b/backend/src/controllers/groups.ts index 7de3e114..989066a6 100644 --- a/backend/src/controllers/groups.ts +++ b/backend/src/controllers/groups.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import { createGroup, getAllGroups, getGroup, getGroupSubmissions } from '../services/groups.js'; -import { GroupDTO } from '../interfaces/group.js'; +import { GroupDTO } from '@dwengo-1/common/interfaces/group'; // Typescript is annoywith with parameter forwarding from class.ts interface GroupParams { diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index fc79ef0d..a2510631 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -1,12 +1,12 @@ import { Request, Response } from 'express'; 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 { envVars, getEnvVar } from '../util/envVars.js'; -import { Language } from '../entities/content/language.js'; +import { Language } from '@dwengo-1/common/util/language'; 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, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { if (!req.params.hruid) { @@ -47,6 +47,11 @@ export async function getLearningObject(req: Request, res: Response): Promise const attachment = await attachmentService.getAttachment(learningObjectId, name); 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); } diff --git a/backend/src/controllers/learning-paths.ts b/backend/src/controllers/learning-paths.ts index 04e44b59..0097d568 100644 --- a/backend/src/controllers/learning-paths.ts +++ b/backend/src/controllers/learning-paths.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import { themes } from '../data/themes.js'; import { FALLBACK_LANG } from '../config.js'; import learningPathService from '../services/learning-paths/learning-path-service.js'; -import { Language } from '../entities/content/language.js'; +import { Language } from '@dwengo-1/common/util/language'; import { PersonalizationTarget, personalizedForGroup, diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 6735c305..b5b764ac 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -1,9 +1,9 @@ import { Request, Response } from 'express'; import { createQuestion, deleteQuestion, getAllQuestions, getAnswersByQuestion, getQuestion } from '../services/questions.js'; -import { QuestionDTO, QuestionId } from '../interfaces/question.js'; import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -import { Language } from '../entities/content/language.js'; +import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; +import { Language } from '@dwengo-1/common/util/language'; function getObjectId(req: Request, res: Response): LearningObjectIdentifier | null { const { hruid, version } = req.params; diff --git a/backend/src/controllers/students.ts b/backend/src/controllers/students.ts index 7e0b5565..51488a2a 100644 --- a/backend/src/controllers/students.ts +++ b/backend/src/controllers/students.ts @@ -1,99 +1,67 @@ import { Request, Response } from 'express'; import { + createClassJoinRequest, createStudent, + deleteClassJoinRequest, deleteStudent, getAllStudents, + getJoinRequestByStudentClass, + getJoinRequestsByStudent, getStudent, getStudentAssignments, getStudentClasses, getStudentGroups, + getStudentQuestions, getStudentSubmissions, } from '../services/students.js'; -import { StudentDTO } from '../interfaces/student.js'; +import { requireFields } from './error-helper.js'; +import { StudentDTO } from '@dwengo-1/common/interfaces/student'; -// TODO: accept arguments (full, ...) -// TODO: endpoints export async function getAllStudentsHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const students = await getAllStudents(full); + const students: StudentDTO[] | string[] = await getAllStudents(full); - if (!students) { - res.status(404).json({ error: `Student not found.` }); - return; - } - - res.json({ students: students }); + res.json({ students }); } export async function getStudentHandler(req: Request, res: Response): Promise { const username = req.params.username; + requireFields({ username }); - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } + const student = await getStudent(username); - const user = await getStudent(username); - - if (!user) { - res.status(404).json({ - error: `User with username '${username}' not found.`, - }); - return; - } - - res.json(user); + res.json({ student }); } export async function createStudentHandler(req: Request, res: Response): Promise { + 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; - if (!userData.username || !userData.firstName || !userData.lastName) { - res.status(400).json({ - error: 'Missing required fields: username, firstName, lastName', - }); - return; - } - - const newUser = await createStudent(userData); - - if (!newUser) { - res.status(500).json({ - error: 'Something went wrong while creating student', - }); - return; - } - - res.status(201).json(newUser); + const student = await createStudent(userData); + res.json({ student }); } export async function deleteStudentHandler(req: Request, res: Response): Promise { const username = req.params.username; + requireFields({ username }); - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - 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); + const student = await deleteStudent(username); + res.json({ student }); } export async function getStudentClassesHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const username = req.params.id; + const username = req.params.username; + requireFields({ username }); const classes = await getStudentClasses(username, full); - res.json({ classes: classes }); + res.json({ classes }); } // TODO @@ -102,33 +70,75 @@ export async function getStudentClassesHandler(req: Request, res: Response): Pro // Have this assignment. export async function getStudentAssignmentsHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const username = req.params.id; + const username = req.params.username; + requireFields({ username }); const assignments = getStudentAssignments(username, full); - res.json({ - assignments: assignments, - }); + res.json({ assignments }); } export async function getStudentGroupsHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const username = req.params.id; + const username = req.params.username; + requireFields({ username }); const groups = await getStudentGroups(username, full); - res.json({ - groups: groups, - }); + res.json({ groups }); } export async function getStudentSubmissionsHandler(req: Request, res: Response): Promise { - const username = req.params.id; + const username = req.params.username; const full = req.query.full === 'true'; + requireFields({ username }); const submissions = await getStudentSubmissions(username, full); - res.json({ - submissions: submissions, - }); + res.json({ submissions }); +} + +export async function getStudentQuestionsHandler(req: Request, res: Response): Promise { + 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 { + 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 { + const username = req.params.username; + requireFields({ username }); + + const requests = await getJoinRequestsByStudent(username); + res.json({ requests }); +} + +export async function getStudentRequestHandler(req: Request, res: Response): Promise { + 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 { + const username = req.params.username; + const classId = req.params.classId; + requireFields({ username, classId }); + + const request = await deleteClassJoinRequest(username, classId); + res.json({ request }); } diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts index 67c1d3a9..239eb6d7 100644 --- a/backend/src/controllers/submissions.ts +++ b/backend/src/controllers/submissions.ts @@ -1,7 +1,7 @@ import { Request, Response } from 'express'; import { createSubmission, deleteSubmission, getSubmission } from '../services/submissions.js'; -import { Language, languageMap } from '../entities/content/language.js'; -import { SubmissionDTO } from '../interfaces/submission'; +import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; +import { Language, languageMap } from '@dwengo-1/common/util/language'; interface SubmissionParams { hruid: string; diff --git a/backend/src/controllers/teachers.ts b/backend/src/controllers/teachers.ts index 06316681..9275ca92 100644 --- a/backend/src/controllers/teachers.ts +++ b/backend/src/controllers/teachers.ts @@ -4,137 +4,97 @@ import { deleteTeacher, getAllTeachers, getClassesByTeacher, - getQuestionsByTeacher, + getJoinRequestsByClass, getStudentsByTeacher, getTeacher, + getTeacherQuestions, + updateClassJoinRequestStatus, } from '../services/teachers.js'; -import { TeacherDTO } from '../interfaces/teacher.js'; +import { requireFields } from './error-helper.js'; +import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; export async function getAllTeachersHandler(req: Request, res: Response): Promise { const full = req.query.full === 'true'; - const teachers = await getAllTeachers(full); + const teachers: TeacherDTO[] | string[] = await getAllTeachers(full); - if (!teachers) { - res.status(404).json({ error: `Teacher not found.` }); - return; - } - - res.json({ teachers: teachers }); + res.json({ teachers }); } export async function getTeacherHandler(req: Request, res: Response): Promise { const username = req.params.username; + requireFields({ username }); - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } + const teacher = await getTeacher(username); - const user = await getTeacher(username); - - if (!user) { - res.status(404).json({ - error: `Teacher '${username}' not found.`, - }); - return; - } - - res.json(user); + res.json({ teacher }); } export async function createTeacherHandler(req: Request, res: Response): Promise { + 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; - if (!userData.username || !userData.firstName || !userData.lastName) { - res.status(400).json({ - error: 'Missing required fields: username, firstName, lastName', - }); - return; - } - - const newUser = await createTeacher(userData); - - if (!newUser) { - res.status(400).json({ error: 'Failed to create teacher' }); - return; - } - - res.status(201).json(newUser); + const teacher = await createTeacher(userData); + res.json({ teacher }); } export async function deleteTeacherHandler(req: Request, res: Response): Promise { const username = req.params.username; + requireFields({ username }); - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } - - const deletedUser = await deleteTeacher(username); - if (!deletedUser) { - res.status(404).json({ - error: `User '${username}' not found.`, - }); - return; - } - - res.status(200).json(deletedUser); + const teacher = await deleteTeacher(username); + res.json({ teacher }); } export async function getTeacherClassHandler(req: Request, res: Response): Promise { const username = req.params.username; const full = req.query.full === 'true'; - - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } + requireFields({ username }); const classes = await getClassesByTeacher(username, full); - if (!classes) { - res.status(404).json({ error: 'Teacher not found' }); - return; - } - - res.json({ classes: classes }); + res.json({ classes }); } export async function getTeacherStudentHandler(req: Request, res: Response): Promise { const username = req.params.username; const full = req.query.full === 'true'; - - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } + requireFields({ username }); const students = await getStudentsByTeacher(username, full); - if (!students) { - res.status(404).json({ error: 'Teacher not found' }); - return; - } - - res.json({ students: students }); + res.json({ students }); } export async function getTeacherQuestionHandler(req: Request, res: Response): Promise { const username = req.params.username; const full = req.query.full === 'true'; + requireFields({ username }); - if (!username) { - res.status(400).json({ error: 'Missing required field: username' }); - return; - } + const questions = await getTeacherQuestions(username, full); - const questions = await getQuestionsByTeacher(username, full); - - if (!questions) { - res.status(404).json({ error: 'Teacher not found' }); - return; - } - - res.json({ questions: questions }); + res.json({ questions }); +} + +export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise { + const username = req.query.username as string; + const classId = req.params.classId; + requireFields({ username, classId }); + + const joinRequests = await getJoinRequestsByClass(classId); + res.json({ joinRequests }); +} + +export async function updateStudentJoinRequestHandler(req: Request, res: Response): Promise { + const studentUsername = req.query.studentUsername as string; + 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 }); } diff --git a/backend/src/data/classes/class-join-request-repository.ts b/backend/src/data/classes/class-join-request-repository.ts index 1cd0288c..0d9ab6e1 100644 --- a/backend/src/data/classes/class-join-request-repository.ts +++ b/backend/src/data/classes/class-join-request-repository.ts @@ -2,13 +2,17 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { Class } from '../../entities/classes/class.entity.js'; import { ClassJoinRequest } from '../../entities/classes/class-join-request.entity.js'; import { Student } from '../../entities/users/student.entity.js'; +import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; export class ClassJoinRequestRepository extends DwengoEntityRepository { public async findAllRequestsBy(requester: Student): Promise { return this.findAll({ where: { requester: requester } }); } public async findAllOpenRequestsTo(clazz: Class): Promise { - return this.findAll({ where: { class: clazz } }); + return this.findAll({ where: { class: clazz, status: ClassJoinRequestStatus.Open } }); // TODO check if works like this + } + public async findByStudentAndClass(requester: Student, clazz: Class): Promise { + return this.findOne({ requester, class: clazz }); } public async deleteBy(requester: Student, clazz: Class): Promise { return this.deleteWhere({ requester: requester, class: clazz }); diff --git a/backend/src/data/content/attachment-repository.ts b/backend/src/data/content/attachment-repository.ts index 73baa943..69178b1c 100644 --- a/backend/src/data/content/attachment-repository.ts +++ b/backend/src/data/content/attachment-repository.ts @@ -1,6 +1,6 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.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'; export class AttachmentRepository extends DwengoEntityRepository { diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index 4684c6cc..889a1594 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -1,7 +1,7 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { LearningObject } from '../../entities/content/learning-object.entity.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'; export class LearningObjectRepository extends DwengoEntityRepository { diff --git a/backend/src/data/content/learning-path-repository.ts b/backend/src/data/content/learning-path-repository.ts index e34508ec..87035f21 100644 --- a/backend/src/data/content/learning-path-repository.ts +++ b/backend/src/data/content/learning-path-repository.ts @@ -1,6 +1,6 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.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'; export class LearningPathRepository extends DwengoEntityRepository { public async findByHruidAndLanguage(hruid: string, language: Language): Promise { diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index 596b562c..2d165abc 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -54,4 +54,11 @@ export class QuestionRepository extends DwengoEntityRepository { orderBy: { timestamp: 'ASC' }, }); } + + public async findAllByAuthor(author: Student): Promise { + return this.findAll({ + where: { author }, + orderBy: { timestamp: 'DESC' }, // New to old + }); + } } diff --git a/backend/src/data/themes.ts b/backend/src/data/themes.ts index b0fc930c..0a2272e6 100644 --- a/backend/src/data/themes.ts +++ b/backend/src/data/themes.ts @@ -1,7 +1,4 @@ -export interface Theme { - title: string; - hruids: string[]; -} +import { Theme } from '@dwengo-1/common/interfaces/theme'; export const themes: Theme[] = [ { diff --git a/backend/src/entities/assignments/assignment.entity.ts b/backend/src/entities/assignments/assignment.entity.ts index daa71ed6..36b24344 100644 --- a/backend/src/entities/assignments/assignment.entity.ts +++ b/backend/src/entities/assignments/assignment.entity.ts @@ -1,7 +1,7 @@ import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Class } from '../classes/class.entity.js'; import { Group } from './group.entity.js'; -import { Language } from '../content/language.js'; +import { Language } from '@dwengo-1/common/util/language'; import { AssignmentRepository } from '../../data/assignments/assignment-repository.js'; @Entity({ diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index e4330e0d..80b9a8fb 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -1,8 +1,8 @@ import { Student } from '../users/student.entity.js'; import { Group } from './group.entity.js'; import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; -import { Language } from '../content/language.js'; import { SubmissionRepository } from '../../data/assignments/submission-repository.js'; +import { Language } from '@dwengo-1/common/util/language'; @Entity({ repository: () => SubmissionRepository }) export class Submission { diff --git a/backend/src/entities/classes/class-join-request.entity.ts b/backend/src/entities/classes/class-join-request.entity.ts index fdf13aa9..907c0199 100644 --- a/backend/src/entities/classes/class-join-request.entity.ts +++ b/backend/src/entities/classes/class-join-request.entity.ts @@ -2,12 +2,7 @@ import { Entity, Enum, ManyToOne } from '@mikro-orm/core'; import { Student } from '../users/student.entity.js'; import { Class } from './class.entity.js'; import { ClassJoinRequestRepository } from '../../data/classes/class-join-request-repository.js'; - -export enum ClassJoinRequestStatus { - Open = 'open', - Accepted = 'accepted', - Declined = 'declined', -} +import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; @Entity({ repository: () => ClassJoinRequestRepository, diff --git a/backend/src/entities/content/learning-object-identifier.ts b/backend/src/entities/content/learning-object-identifier.ts index 9234afa7..09a9c057 100644 --- a/backend/src/entities/content/learning-object-identifier.ts +++ b/backend/src/entities/content/learning-object-identifier.ts @@ -1,4 +1,4 @@ -import { Language } from './language.js'; +import { Language } from '@dwengo-1/common/util/language'; export class LearningObjectIdentifier { constructor( diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index e352a10a..ff858fe6 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -1,5 +1,4 @@ import { Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; -import { Language } from './language.js'; import { Attachment } from './attachment.entity.js'; import { Teacher } from '../users/teacher.entity.js'; import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; @@ -7,6 +6,7 @@ import { v4 } from 'uuid'; import { LearningObjectRepository } from '../../data/content/learning-object-repository.js'; import { EducationalGoal } from './educational-goal.entity.js'; import { ReturnValue } from './return-value.entity.js'; +import { Language } from '@dwengo-1/common/util/language'; @Entity({ repository: () => LearningObjectRepository }) export class LearningObject { diff --git a/backend/src/entities/content/learning-path-node.entity.ts b/backend/src/entities/content/learning-path-node.entity.ts index 03499270..3016c367 100644 --- a/backend/src/entities/content/learning-path-node.entity.ts +++ b/backend/src/entities/content/learning-path-node.entity.ts @@ -1,7 +1,7 @@ import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core'; -import { Language } from './language.js'; import { LearningPath } from './learning-path.entity.js'; import { LearningPathTransition } from './learning-path-transition.entity.js'; +import { Language } from '@dwengo-1/common/util/language'; @Entity() export class LearningPathNode { diff --git a/backend/src/entities/content/learning-path.entity.ts b/backend/src/entities/content/learning-path.entity.ts index 888cc0cf..203af86d 100644 --- a/backend/src/entities/content/learning-path.entity.ts +++ b/backend/src/entities/content/learning-path.entity.ts @@ -1,8 +1,8 @@ import { Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; -import { Language } from './language.js'; import { Teacher } from '../users/teacher.entity.js'; import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; import { LearningPathNode } from './learning-path-node.entity.js'; +import { Language } from '@dwengo-1/common/util/language'; @Entity({ repository: () => LearningPathRepository }) export class LearningPath { diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index 09e3cd46..5e691f70 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -1,7 +1,7 @@ import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'; -import { Language } from '../content/language.js'; import { Student } from '../users/student.entity.js'; import { QuestionRepository } from '../../data/questions/question-repository.js'; +import { Language } from '@dwengo-1/common/util/language'; @Entity({ repository: () => QuestionRepository }) export class Question { diff --git a/backend/src/interfaces/answer.ts b/backend/src/interfaces/answer.ts index 493fd3c0..1f0d0625 100644 --- a/backend/src/interfaces/answer.ts +++ b/backend/src/interfaces/answer.ts @@ -1,14 +1,7 @@ -import { mapToUserDTO, UserDTO } from './user.js'; -import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from './question.js'; +import { mapToUserDTO } from './user.js'; +import { mapToQuestionDTO, mapToQuestionDTOId } from './question.js'; import { Answer } from '../entities/questions/answer.entity.js'; - -export interface AnswerDTO { - author: UserDTO; - toQuestion: QuestionDTO; - sequenceNumber: number; - timestamp: string; - content: string; -} +import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; /** * Convert a Question entity to a DTO format. @@ -23,16 +16,10 @@ export function mapToAnswerDTO(answer: Answer): AnswerDTO { }; } -export interface AnswerId { - author: string; - toQuestion: QuestionId; - sequenceNumber: number; -} - -export function mapToAnswerId(answer: AnswerDTO): AnswerId { +export function mapToAnswerDTOId(answer: Answer): AnswerId { return { author: answer.author.username, - toQuestion: mapToQuestionId(answer.toQuestion), - sequenceNumber: answer.sequenceNumber, + toQuestion: mapToQuestionDTOId(answer.toQuestion), + sequenceNumber: answer.sequenceNumber!, }; } diff --git a/backend/src/interfaces/assignment.ts b/backend/src/interfaces/assignment.ts index 698b5b40..d48a9083 100644 --- a/backend/src/interfaces/assignment.ts +++ b/backend/src/interfaces/assignment.ts @@ -1,19 +1,9 @@ +import { languageMap } from '@dwengo-1/common/util/language'; import { FALLBACK_LANG } from '../config.js'; import { Assignment } from '../entities/assignments/assignment.entity.js'; import { Class } from '../entities/classes/class.entity.js'; -import { languageMap } from '../entities/content/language.js'; -import { GroupDTO } from './group.js'; import { getLogger } from '../logging/initalize.js'; - -export interface AssignmentDTO { - id: number; - class: string; // Id of class 'within' - title: string; - description: string; - learningPath: string; - language: string; - groups?: GroupDTO[] | string[]; // TODO -} +import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; export function mapToAssignmentDTOId(assignment: Assignment): AssignmentDTO { return { diff --git a/backend/src/interfaces/class.ts b/backend/src/interfaces/class.ts index ea1d4901..7b07fcf2 100644 --- a/backend/src/interfaces/class.ts +++ b/backend/src/interfaces/class.ts @@ -2,14 +2,7 @@ import { Collection } from '@mikro-orm/core'; import { Class } from '../entities/classes/class.entity.js'; import { Student } from '../entities/users/student.entity.js'; import { Teacher } from '../entities/users/teacher.entity.js'; - -export interface ClassDTO { - id: string; - displayName: string; - teachers: string[]; - students: string[]; - joinRequests: string[]; -} +import { ClassDTO } from '@dwengo-1/common/interfaces/class'; export function mapToClassDTO(cls: Class): ClassDTO { return { diff --git a/backend/src/interfaces/group.ts b/backend/src/interfaces/group.ts index a25c5b8e..1a169b2b 100644 --- a/backend/src/interfaces/group.ts +++ b/backend/src/interfaces/group.ts @@ -1,12 +1,7 @@ import { Group } from '../entities/assignments/group.entity.js'; -import { AssignmentDTO, mapToAssignmentDTO } from './assignment.js'; -import { mapToStudentDTO, StudentDTO } from './student.js'; - -export interface GroupDTO { - assignment: number | AssignmentDTO; - groupNumber: number; - members: string[] | StudentDTO[]; -} +import { mapToAssignmentDTO } from './assignment.js'; +import { mapToStudentDTO } from './student.js'; +import { GroupDTO } from '@dwengo-1/common/interfaces/group'; export function mapToGroupDTO(group: Group): GroupDTO { return { diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index 0da87eb7..48d64f11 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -1,24 +1,21 @@ import { Question } from '../entities/questions/question.entity.js'; -import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -import { mapToStudentDTO, StudentDTO } from './student.js'; +import { mapToStudentDTO } from './student.js'; +import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; +import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; -export interface QuestionDTO { - learningObjectIdentifier: LearningObjectIdentifier; - sequenceNumber?: number; - author: StudentDTO; - timestamp?: string; - content: string; +function getLearningObjectIdentifier(question: Question): LearningObjectIdentifier { + return { + hruid: question.learningObjectHruid, + language: question.learningObjectLanguage, + version: question.learningObjectVersion, + }; } /** * Convert a Question entity to a DTO format. */ export function mapToQuestionDTO(question: Question): QuestionDTO { - const learningObjectIdentifier = { - hruid: question.learningObjectHruid, - language: question.learningObjectLanguage, - version: question.learningObjectVersion, - }; + const learningObjectIdentifier = getLearningObjectIdentifier(question); return { learningObjectIdentifier, @@ -29,14 +26,11 @@ export function mapToQuestionDTO(question: Question): QuestionDTO { }; } -export interface QuestionId { - learningObjectIdentifier: LearningObjectIdentifier; - sequenceNumber: number; -} +export function mapToQuestionDTOId(question: Question): QuestionId { + const learningObjectIdentifier = getLearningObjectIdentifier(question); -export function mapToQuestionId(question: QuestionDTO): QuestionId { return { - learningObjectIdentifier: question.learningObjectIdentifier, + learningObjectIdentifier, sequenceNumber: question.sequenceNumber!, }; } diff --git a/backend/src/interfaces/student-request.ts b/backend/src/interfaces/student-request.ts new file mode 100644 index 00000000..d97f5eb5 --- /dev/null +++ b/backend/src/interfaces/student-request.ts @@ -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 { ClassJoinRequestStatus } 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: ClassJoinRequestStatus.Open, + }); +} diff --git a/backend/src/interfaces/student.ts b/backend/src/interfaces/student.ts index ecce8f89..06e173a1 100644 --- a/backend/src/interfaces/student.ts +++ b/backend/src/interfaces/student.ts @@ -1,18 +1,6 @@ import { Student } from '../entities/users/student.entity.js'; import { getStudentRepository } from '../data/repositories.js'; - -export interface StudentDTO { - id: string; - username: string; - firstName: string; - lastName: string; - endpoints?: { - classes: string; - questions: string; - invitations: string; - groups: string; - }; -} +import { StudentDTO } from '@dwengo-1/common/interfaces/student'; export function mapToStudentDTO(student: Student): StudentDTO { return { diff --git a/backend/src/interfaces/submission.ts b/backend/src/interfaces/submission.ts index 98cc4f22..b4ed4a2b 100644 --- a/backend/src/interfaces/submission.ts +++ b/backend/src/interfaces/submission.ts @@ -1,26 +1,7 @@ import { Submission } from '../entities/assignments/submission.entity.js'; -import { Language } from '../entities/content/language.js'; -import { GroupDTO, mapToGroupDTO } from './group.js'; -import { mapToStudent, mapToStudentDTO, StudentDTO } from './student.js'; -import { LearningObjectIdentifier } from './learning-content.js'; - -export interface SubmissionDTO { - learningObjectIdentifier: LearningObjectIdentifier; - - submissionNumber?: number; - submitter: StudentDTO; - time?: Date; - group?: GroupDTO; - content: string; -} - -export interface SubmissionDTOId { - learningObjectHruid: string; - learningObjectLanguage: Language; - learningObjectVersion: number; - - submissionNumber?: number; -} +import { mapToGroupDTO } from './group.js'; +import { mapToStudent, mapToStudentDTO } from './student.js'; +import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; export function mapToSubmissionDTO(submission: Submission): SubmissionDTO { return { diff --git a/backend/src/interfaces/teacher-invitation.ts b/backend/src/interfaces/teacher-invitation.ts index cddef566..d9cb9915 100644 --- a/backend/src/interfaces/teacher-invitation.ts +++ b/backend/src/interfaces/teacher-invitation.ts @@ -1,12 +1,7 @@ import { TeacherInvitation } from '../entities/classes/teacher-invitation.entity.js'; -import { ClassDTO, mapToClassDTO } from './class.js'; -import { mapToUserDTO, UserDTO } from './user.js'; - -export interface TeacherInvitationDTO { - sender: string | UserDTO; - receiver: string | UserDTO; - class: string | ClassDTO; -} +import { mapToClassDTO } from './class.js'; +import { mapToUserDTO } from './user.js'; +import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; export function mapToTeacherInvitationDTO(invitation: TeacherInvitation): TeacherInvitationDTO { return { diff --git a/backend/src/interfaces/teacher.ts b/backend/src/interfaces/teacher.ts index 31b4723f..f7e1745f 100644 --- a/backend/src/interfaces/teacher.ts +++ b/backend/src/interfaces/teacher.ts @@ -1,18 +1,6 @@ import { Teacher } from '../entities/users/teacher.entity.js'; import { getTeacherRepository } from '../data/repositories.js'; - -export interface TeacherDTO { - id: string; - username: string; - firstName: string; - lastName: string; - endpoints?: { - classes: string; - questions: string; - invitations: string; - groups: string; - }; -} +import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; export function mapToTeacherDTO(teacher: Teacher): TeacherDTO { return { diff --git a/backend/src/interfaces/user.ts b/backend/src/interfaces/user.ts index 58f0dd5a..f4413b5e 100644 --- a/backend/src/interfaces/user.ts +++ b/backend/src/interfaces/user.ts @@ -1,17 +1,5 @@ import { User } from '../entities/users/user.entity.js'; - -export interface UserDTO { - id?: string; - username: string; - firstName: string; - lastName: string; - endpoints?: { - self: string; - classes: string; - questions: string; - invitations: string; - }; -} +import { UserDTO } from '@dwengo-1/common/interfaces/user'; export function mapToUserDTO(user: User): UserDTO { return { diff --git a/backend/src/routes/student-join-requests.ts b/backend/src/routes/student-join-requests.ts new file mode 100644 index 00000000..daf79f09 --- /dev/null +++ b/backend/src/routes/student-join-requests.ts @@ -0,0 +1,19 @@ +import express from 'express'; +import { + createStudentRequestHandler, + deleteClassJoinRequestHandler, + getStudentRequestHandler, + getStudentRequestsHandler, +} from '../controllers/students.js'; + +const router = express.Router({ mergeParams: true }); + +router.get('/', getStudentRequestsHandler); + +router.post('/', createStudentRequestHandler); + +router.get('/:classId', getStudentRequestHandler); + +router.delete('/:classId', deleteClassJoinRequestHandler); + +export default router; diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index d58c2adc..0f5d5349 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -7,8 +7,10 @@ import { getStudentClassesHandler, getStudentGroupsHandler, getStudentHandler, + getStudentQuestionsHandler, getStudentSubmissionsHandler, } from '../controllers/students.js'; +import joinRequestRouter from './student-join-requests.js'; const router = express.Router(); @@ -17,30 +19,26 @@ router.get('/', getAllStudentsHandler); router.post('/', createStudentHandler); -router.delete('/', deleteStudentHandler); - router.delete('/:username', deleteStudentHandler); // Information about a student's profile router.get('/:username', getStudentHandler); // The list of classes a student is in -router.get('/:id/classes', getStudentClassesHandler); +router.get('/:username/classes', getStudentClassesHandler); // The list of submissions a student has made -router.get('/:id/submissions', getStudentSubmissionsHandler); +router.get('/:username/submissions', getStudentSubmissionsHandler); // The list of assignments a student has -router.get('/:id/assignments', getStudentAssignmentsHandler); +router.get('/:username/assignments', getStudentAssignmentsHandler); // The list of groups a student is in -router.get('/:id/groups', getStudentGroupsHandler); +router.get('/:username/groups', getStudentGroupsHandler); // A list of questions a user has created -router.get('/:id/questions', (_req, res) => { - res.json({ - questions: ['0'], - }); -}); +router.get('/:username/questions', getStudentQuestionsHandler); + +router.use('/:username/joinRequests', joinRequestRouter); export default router; diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index 3782a6ca..a6106a80 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -3,10 +3,12 @@ import { createTeacherHandler, deleteTeacherHandler, getAllTeachersHandler, + getStudentJoinRequestHandler, getTeacherClassHandler, getTeacherHandler, getTeacherQuestionHandler, getTeacherStudentHandler, + updateStudentJoinRequestHandler, } from '../controllers/teachers.js'; const router = express.Router(); @@ -15,8 +17,6 @@ router.get('/', getAllTeachersHandler); router.post('/', createTeacherHandler); -router.delete('/', deleteTeacherHandler); - router.get('/:username', getTeacherHandler); router.delete('/:username', deleteTeacherHandler); @@ -27,6 +27,10 @@ router.get('/:username/students', getTeacherStudentHandler); router.get('/:username/questions', getTeacherQuestionHandler); +router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler); + +router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); + // Invitations to other classes a teacher received router.get('/:id/invitations', (_req, res) => { res.json({ diff --git a/backend/src/services/assignments.ts b/backend/src/services/assignments.ts index 22c5ce9e..e86b69b2 100644 --- a/backend/src/services/assignments.ts +++ b/backend/src/services/assignments.ts @@ -1,6 +1,8 @@ import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; -import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; -import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; +import { mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; +import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; +import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; +import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { getLogger } from '../logging/initalize.js'; export async function getAllAssignments(classid: string, full: boolean): Promise { diff --git a/backend/src/services/classes.ts b/backend/src/services/classes.ts index dc3ac8a0..754277cf 100644 --- a/backend/src/services/classes.ts +++ b/backend/src/services/classes.ts @@ -1,11 +1,27 @@ import { getClassRepository, getStudentRepository, getTeacherInvitationRepository, getTeacherRepository } from '../data/repositories.js'; -import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; -import { mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; -import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds, TeacherInvitationDTO } from '../interfaces/teacher-invitation.js'; +import { mapToClassDTO } from '../interfaces/class.js'; +import { mapToStudentDTO } from '../interfaces/student.js'; +import { mapToTeacherInvitationDTO, mapToTeacherInvitationDTOIds } from '../interfaces/teacher-invitation.js'; import { getLogger } from '../logging/initalize.js'; +import { NotFoundException } from '../exceptions/not-found-exception.js'; +import { Class } from '../entities/classes/class.entity.js'; +import { ClassDTO } from '@dwengo-1/common/interfaces/class'; +import { TeacherInvitationDTO } from '@dwengo-1/common/interfaces/teacher-invitation'; +import { StudentDTO } from '@dwengo-1/common/interfaces/student'; const logger = getLogger(); +export async function fetchClass(classId: string): Promise { + const classRepository = getClassRepository(); + const cls = await classRepository.findById(classId); + + if (!cls) { + throw new NotFoundException('Class with id not found'); + } + + return cls; +} + export async function getAllClasses(full: boolean): Promise { const classRepository = getClassRepository(); const classes = await classRepository.find({}, { populate: ['students', 'teachers'] }); diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 16895e0a..346c1ee1 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -6,8 +6,10 @@ import { getSubmissionRepository, } from '../data/repositories.js'; import { Group } from '../entities/assignments/group.entity.js'; -import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; -import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; +import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; +import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js'; +import { GroupDTO } from '@dwengo-1/common/interfaces/group'; +import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission'; import { getLogger } from '../logging/initalize.js'; export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise { diff --git a/backend/src/services/learning-objects.ts b/backend/src/services/learning-objects.ts index 43af1aca..436c4a08 100644 --- a/backend/src/services/learning-objects.ts +++ b/backend/src/services/learning-objects.ts @@ -1,6 +1,12 @@ import { DWENGO_API_BASE } from '../config.js'; import { fetchWithLogging } from '../util/api-helper.js'; -import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js'; + +import { + FilteredLearningObject, + LearningObjectMetadata, + LearningObjectNode, + LearningPathResponse, +} from '@dwengo-1/common/interfaces/learning-content'; import { getLogger } from '../logging/initalize.js'; function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { diff --git a/backend/src/services/learning-objects/attachment-service.ts b/backend/src/services/learning-objects/attachment-service.ts index 4ff4ec47..46fc5e03 100644 --- a/backend/src/services/learning-objects/attachment-service.ts +++ b/backend/src/services/learning-objects/attachment-service.ts @@ -1,6 +1,7 @@ import { getAttachmentRepository } from '../../data/repositories.js'; import { Attachment } from '../../entities/content/attachment.entity.js'; -import { LearningObjectIdentifier } from '../../interfaces/learning-content.js'; + +import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; const attachmentService = { async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise { diff --git a/backend/src/services/learning-objects/database-learning-object-provider.ts b/backend/src/services/learning-objects/database-learning-object-provider.ts index a8055f2c..361153f5 100644 --- a/backend/src/services/learning-objects/database-learning-object-provider.ts +++ b/backend/src/services/learning-objects/database-learning-object-provider.ts @@ -1,5 +1,4 @@ import { LearningObjectProvider } from './learning-object-provider.js'; -import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { getUrlStringForLearningObject } from '../../util/links.js'; @@ -7,6 +6,7 @@ import processingService from './processing/processing-service.js'; import { NotFoundError } from '@mikro-orm/core'; import learningObjectService from './learning-object-service.js'; import { getLogger, Logger } from '../../logging/initalize.js'; +import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; const logger: Logger = getLogger(); diff --git a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts index dfee329d..d67b69ae 100644 --- a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts +++ b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts @@ -1,5 +1,8 @@ import { DWENGO_API_BASE } from '../../config.js'; import { fetchWithLogging } from '../../util/api-helper.js'; +import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; +import { LearningObjectProvider } from './learning-object-provider.js'; +import { getLogger, Logger } from '../../logging/initalize.js'; import { FilteredLearningObject, LearningObjectIdentifier, @@ -7,10 +10,7 @@ import { LearningObjectNode, LearningPathIdentifier, LearningPathResponse, -} from '../../interfaces/learning-content.js'; -import dwengoApiLearningPathProvider from '../learning-paths/dwengo-api-learning-path-provider.js'; -import { LearningObjectProvider } from './learning-object-provider.js'; -import { getLogger, Logger } from '../../logging/initalize.js'; +} from '@dwengo-1/common/interfaces/learning-content'; const logger: Logger = getLogger(); @@ -66,12 +66,13 @@ async function fetchLearningObjects(learningPathId: LearningPathIdentifier, full } const objects = await Promise.all( - nodes.map(async (node) => - dwengoApiLearningObjectProvider.getLearningObjectById({ + nodes.map(async (node) => { + const learningObjectId: LearningObjectIdentifier = { hruid: node.learningobject_hruid, language: learningPathId.language, - }) - ) + }; + return dwengoApiLearningObjectProvider.getLearningObjectById(learningObjectId); + }) ); return objects.filter((obj): obj is FilteredLearningObject => obj !== null); } catch (error) { @@ -90,7 +91,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { metadataUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { - params: id, + params: { ...id }, } ); @@ -123,7 +124,7 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { const htmlUrl = `${DWENGO_API_BASE}/learningObject/getRaw`; const html = await fetchWithLogging(htmlUrl, `Metadata for Learning Object HRUID "${id.hruid}" (language ${id.language})`, { - params: id, + params: { ...id }, }); if (!html) { diff --git a/backend/src/services/learning-objects/learning-object-provider.ts b/backend/src/services/learning-objects/learning-object-provider.ts index 81b4d228..a0fcb552 100644 --- a/backend/src/services/learning-objects/learning-object-provider.ts +++ b/backend/src/services/learning-objects/learning-object-provider.ts @@ -1,4 +1,4 @@ -import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; +import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; export interface LearningObjectProvider { /** diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 59ffb643..5a06f0f2 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -1,8 +1,8 @@ -import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js'; import { LearningObjectProvider } from './learning-object-provider.js'; import { envVars, getEnvVar } from '../../util/envVars.js'; import databaseLearningObjectProvider from './database-learning-object-provider.js'; +import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { diff --git a/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts b/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts index d1c797be..87ba13b7 100644 --- a/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts +++ b/backend/src/services/learning-objects/processing/markdown/dwengo-marked-renderer.ts @@ -8,13 +8,12 @@ import InlineImageProcessor from '../image/inline-image-processor.js'; import * as marked from 'marked'; import { getUrlStringForLearningObjectHTML, isValidHttpUrl } from '../../../../util/links.js'; import { ProcessingError } from '../processing-error.js'; -import { LearningObjectIdentifier } from '../../../../interfaces/learning-content.js'; -import { Language } from '../../../../entities/content/language.js'; - import Image = marked.Tokens.Image; import Heading = marked.Tokens.Heading; import Link = marked.Tokens.Link; import RendererObject = marked.RendererObject; +import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { Language } from '@dwengo-1/common/util/language'; const prefixes = { learningObject: '@learning-object', diff --git a/backend/src/services/learning-objects/processing/processing-service.ts b/backend/src/services/learning-objects/processing/processing-service.ts index f731eb5d..e9147e31 100644 --- a/backend/src/services/learning-objects/processing/processing-service.ts +++ b/backend/src/services/learning-objects/processing/processing-service.ts @@ -13,9 +13,9 @@ import GiftProcessor from './gift/gift-processor.js'; import { LearningObject } from '../../../entities/content/learning-object.entity.js'; import Processor from './processor.js'; import { DwengoContentType } from './content-type.js'; -import { LearningObjectIdentifier } from '../../../interfaces/learning-content.js'; -import { Language } from '../../../entities/content/language.js'; import { replaceAsync } from '../../../util/async.js'; +import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { Language } from '@dwengo-1/common/util/language'; const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = //g; const LEARNING_OBJECT_DOES_NOT_EXIST = "
"; diff --git a/backend/src/services/learning-paths/database-learning-path-provider.ts b/backend/src/services/learning-paths/database-learning-path-provider.ts index 3d883ef2..ba312b08 100644 --- a/backend/src/services/learning-paths/database-learning-path-provider.ts +++ b/backend/src/services/learning-paths/database-learning-path-provider.ts @@ -1,12 +1,18 @@ import { LearningPathProvider } from './learning-path-provider.js'; -import { FilteredLearningObject, LearningObjectNode, LearningPath, LearningPathResponse, Transition } from '../../interfaces/learning-content.js'; import { LearningPath as LearningPathEntity } from '../../entities/content/learning-path.entity.js'; import { getLearningPathRepository } from '../../data/repositories.js'; -import { Language } from '../../entities/content/language.js'; import learningObjectService from '../learning-objects/learning-object-service.js'; import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js'; import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js'; import { getLastSubmissionForCustomizationTarget, isTransitionPossible, PersonalizationTarget } from './learning-path-personalization-util.js'; +import { + FilteredLearningObject, + LearningObjectNode, + LearningPath, + LearningPathResponse, + Transition, +} from '@dwengo-1/common/interfaces/learning-content'; +import { Language } from '@dwengo-1/common/util/language'; /** * Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its diff --git a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts index a6093bb4..110cd570 100644 --- a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts @@ -1,8 +1,8 @@ import { fetchWithLogging } from '../../util/api-helper.js'; import { DWENGO_API_BASE } from '../../config.js'; -import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js'; import { LearningPathProvider } from './learning-path-provider.js'; import { getLogger, Logger } from '../../logging/initalize.js'; +import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; const logger: Logger = getLogger(); diff --git a/backend/src/services/learning-paths/learning-path-provider.ts b/backend/src/services/learning-paths/learning-path-provider.ts index 5e2a09df..3bf734e7 100644 --- a/backend/src/services/learning-paths/learning-path-provider.ts +++ b/backend/src/services/learning-paths/learning-path-provider.ts @@ -1,6 +1,6 @@ -import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js'; -import { Language } from '../../entities/content/language.js'; +import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; import { PersonalizationTarget } from './learning-path-personalization-util.js'; +import { Language } from '@dwengo-1/common/util/language'; /** * Generic interface for a service which provides access to learning paths from a data source. diff --git a/backend/src/services/learning-paths/learning-path-service.ts b/backend/src/services/learning-paths/learning-path-service.ts index f8e328b2..0e4d2c5e 100644 --- a/backend/src/services/learning-paths/learning-path-service.ts +++ b/backend/src/services/learning-paths/learning-path-service.ts @@ -1,9 +1,9 @@ -import { LearningPath, LearningPathResponse } from '../../interfaces/learning-content.js'; import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; import databaseLearningPathProvider from './database-learning-path-provider.js'; import { envVars, getEnvVar } from '../../util/envVars.js'; -import { Language } from '../../entities/content/language.js'; import { PersonalizationTarget } from './learning-path-personalization-util.js'; +import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; +import { Language } from '@dwengo-1/common/util/language'; const userContentPrefix = getEnvVar(envVars.UserContentPrefix); const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider]; diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index ac1095bf..319061c5 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -1,11 +1,13 @@ import { getAnswerRepository, getQuestionRepository } from '../data/repositories.js'; -import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; +import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; import { Question } from '../entities/questions/question.entity.js'; import { Answer } from '../entities/questions/answer.entity.js'; -import { AnswerDTO, AnswerId, mapToAnswerDTO, mapToAnswerId } from '../interfaces/answer.js'; +import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js'; import { QuestionRepository } from '../data/questions/question-repository.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { mapToStudent } from '../interfaces/student.js'; +import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; +import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; export async function getAllQuestions(id: LearningObjectIdentifier, full: boolean): Promise { const questionRepository: QuestionRepository = getQuestionRepository(); @@ -15,13 +17,11 @@ export async function getAllQuestions(id: LearningObjectIdentifier, full: boolea return []; } - const questionsDTO: QuestionDTO[] = questions.map(mapToQuestionDTO); - if (full) { - return questionsDTO; + return questions.map(mapToQuestionDTO); } - return questionsDTO.map(mapToQuestionId); + return questions.map(mapToQuestionDTOId); } async function fetchQuestion(questionId: QuestionId): Promise { @@ -59,13 +59,11 @@ export async function getAnswersByQuestion(questionId: QuestionId, full: boolean return []; } - const answersDTO = answers.map(mapToAnswerDTO); - if (full) { - return answersDTO; + return answers.map(mapToAnswerDTO); } - return answersDTO.map(mapToAnswerId); + return answers.map(mapToAnswerDTOId); } export async function createQuestion(questionDTO: QuestionDTO): Promise { @@ -73,9 +71,14 @@ export async function createQuestion(questionDTO: QuestionDTO): Promise { const studentRepository = getStudentRepository(); - const students = await studentRepository.findAll(); + const users = await studentRepository.findAll(); if (full) { - return students.map(mapToStudentDTO); + return users.map(mapToStudentDTO); } - return students.map((student) => student.username); + return users.map((user) => user.username); } -export async function getStudent(username: string): Promise { +export async function fetchStudent(username: string): Promise { const studentRepository = getStudentRepository(); const user = await studentRepository.findByUsername(username); - return user ? mapToStudentDTO(user) : null; + + if (!user) { + throw new NotFoundException('Student with username not found'); + } + + return user; } -export async function createStudent(userData: StudentDTO): Promise { +export async function getStudent(username: string): Promise { + const user = await fetchStudent(username); + return mapToStudentDTO(user); +} + +export async function createStudent(userData: StudentDTO): Promise { const studentRepository = getStudentRepository(); const newStudent = mapToStudent(userData); await studentRepository.save(newStudent, { preventOverwrite: true }); - return mapToStudentDTO(newStudent); + return userData; } -export async function deleteStudent(username: string): Promise { +export async function deleteStudent(username: string): Promise { const studentRepository = getStudentRepository(); - const user = await studentRepository.findByUsername(username); + const student = await fetchStudent(username); // Throws error if it does not exist - if (!user) { - return null; - } - - try { - await studentRepository.deleteByUsername(username); - - return mapToStudentDTO(user); - } catch (e) { - getLogger().error(e); - return null; - } + await studentRepository.deleteByUsername(username); + return mapToStudentDTO(student); } export async function getStudentClasses(username: string, full: boolean): Promise { - const studentRepository = getStudentRepository(); - const student = await studentRepository.findByUsername(username); - - if (!student) { - return []; - } + const student = await fetchStudent(username); const classRepository = getClassRepository(); const classes = await classRepository.findByStudent(student); @@ -70,12 +82,7 @@ export async function getStudentClasses(username: string, full: boolean): Promis } export async function getStudentAssignments(username: string, full: boolean): Promise { - const studentRepository = getStudentRepository(); - const student = await studentRepository.findByUsername(username); - - if (!student) { - return []; - } + const student = await fetchStudent(username); const classRepository = getClassRepository(); const classes = await classRepository.findByStudent(student); @@ -84,12 +91,7 @@ export async function getStudentAssignments(username: string, full: boolean): Pr } export async function getStudentGroups(username: string, full: boolean): Promise { - const studentRepository = getStudentRepository(); - const student = await studentRepository.findByUsername(username); - - if (!student) { - return []; - } + const student = await fetchStudent(username); const groupRepository = getGroupRepository(); const groups = await groupRepository.findAllGroupsWithStudent(student); @@ -102,12 +104,7 @@ export async function getStudentGroups(username: string, full: boolean): Promise } export async function getStudentSubmissions(username: string, full: boolean): Promise { - const studentRepository = getStudentRepository(); - const student = await studentRepository.findByUsername(username); - - if (!student) { - return []; - } + const student = await fetchStudent(username); const submissionRepository = getSubmissionRepository(); const submissions = await submissionRepository.findAllSubmissionsForStudent(student); @@ -118,3 +115,66 @@ export async function getStudentSubmissions(username: string, full: boolean): Pr return submissions.map(mapToSubmissionDTOId); } + +export async function getStudentQuestions(username: string, full: boolean): Promise { + const student = await fetchStudent(username); + + const questionRepository = getQuestionRepository(); + const questions = await questionRepository.findAllByAuthor(student); + + if (full) { + return questions.map(mapToQuestionDTO); + } + + return questions.map(mapToQuestionDTOId); +} + +export async function createClassJoinRequest(username: string, classId: string): Promise { + const requestRepo = getClassJoinRequestRepository(); + + const student = await fetchStudent(username); // Throws error if student not found + const cls = await fetchClass(classId); + + const request = mapToStudentRequest(student, cls); + await requestRepo.save(request, { preventOverwrite: true }); + return mapToStudentRequestDTO(request); +} + +export async function getJoinRequestsByStudent(username: string): Promise { + const requestRepo = getClassJoinRequestRepository(); + + const student = await fetchStudent(username); + + const requests = await requestRepo.findAllRequestsBy(student); + return requests.map(mapToStudentRequestDTO); +} + +export async function getJoinRequestByStudentClass(username: string, classId: string): Promise { + const requestRepo = getClassJoinRequestRepository(); + + const student = await fetchStudent(username); + const cls = await fetchClass(classId); + + const request = await requestRepo.findByStudentAndClass(student, cls); + if (!request) { + throw new NotFoundException('Join request not found'); + } + + return mapToStudentRequestDTO(request); +} + +export async function deleteClassJoinRequest(username: string, classId: string): Promise { + const requestRepo = getClassJoinRequestRepository(); + + const student = await fetchStudent(username); + const cls = await fetchClass(classId); + + const request = await requestRepo.findByStudentAndClass(student, cls); + + if (!request) { + throw new NotFoundException('Join request not found'); + } + + await requestRepo.deleteBy(student, cls); + return mapToStudentRequestDTO(request); +} diff --git a/backend/src/services/submissions.ts b/backend/src/services/submissions.ts index a456d9d1..1d8a7874 100644 --- a/backend/src/services/submissions.ts +++ b/backend/src/services/submissions.ts @@ -1,7 +1,8 @@ import { getSubmissionRepository } from '../data/repositories.js'; -import { Language } from '../entities/content/language.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; -import { mapToSubmission, mapToSubmissionDTO, SubmissionDTO } from '../interfaces/submission.js'; +import { mapToSubmission, mapToSubmissionDTO } from '../interfaces/submission.js'; +import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission'; +import { Language } from '@dwengo-1/common/util/language'; export async function getSubmission( learningObjectHruid: string, diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index b2aefe63..1b7643fb 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -1,134 +1,165 @@ -import { getClassRepository, getLearningObjectRepository, getQuestionRepository, getTeacherRepository } from '../data/repositories.js'; -import { ClassDTO, mapToClassDTO } from '../interfaces/class.js'; +import { + getClassJoinRequestRepository, + getClassRepository, + getLearningObjectRepository, + getQuestionRepository, + getTeacherRepository, +} from '../data/repositories.js'; +import { mapToClassDTO } from '../interfaces/class.js'; +import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js'; +import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js'; +import { Teacher } from '../entities/users/teacher.entity.js'; +import { fetchStudent } from './students.js'; +import { ClassJoinRequest } from '../entities/classes/class-join-request.entity.js'; +import { mapToStudentRequestDTO } from '../interfaces/student-request.js'; +import { TeacherRepository } from '../data/users/teacher-repository.js'; +import { ClassRepository } from '../data/classes/class-repository.js'; +import { Class } from '../entities/classes/class.entity.js'; +import { LearningObjectRepository } from '../data/content/learning-object-repository.js'; +import { LearningObject } from '../entities/content/learning-object.entity.js'; +import { QuestionRepository } from '../data/questions/question-repository.js'; +import { Question } from '../entities/questions/question.entity.js'; +import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js'; +import { Student } from '../entities/users/student.entity.js'; +import { NotFoundException } from '../exceptions/not-found-exception.js'; import { getClassStudents } from './classes.js'; -import { StudentDTO } from '../interfaces/student.js'; -import { mapToQuestionDTO, mapToQuestionId, QuestionDTO, QuestionId } from '../interfaces/question.js'; -import { mapToTeacher, mapToTeacherDTO, TeacherDTO } from '../interfaces/teacher.js'; -import { getLogger } from '../logging/initalize.js'; +import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; +import { ClassDTO } from '@dwengo-1/common/interfaces/class'; +import { StudentDTO } from '@dwengo-1/common/interfaces/student'; +import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; +import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; +import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; export async function getAllTeachers(full: boolean): Promise { - const teacherRepository = getTeacherRepository(); - const teachers = await teacherRepository.findAll(); + const teacherRepository: TeacherRepository = getTeacherRepository(); + const users: Teacher[] = await teacherRepository.findAll(); if (full) { - return teachers.map(mapToTeacherDTO); + return users.map(mapToTeacherDTO); + } + return users.map((user) => user.username); +} + +export async function fetchTeacher(username: string): Promise { + const teacherRepository: TeacherRepository = getTeacherRepository(); + const user: Teacher | null = await teacherRepository.findByUsername(username); + + if (!user) { + throw new NotFoundException('Teacher with username not found'); } - return teachers.map((teacher) => teacher.username); + return user; } -export async function getTeacher(username: string): Promise { - const teacherRepository = getTeacherRepository(); - const user = await teacherRepository.findByUsername(username); - return user ? mapToTeacherDTO(user) : null; +export async function getTeacher(username: string): Promise { + const user: Teacher = await fetchTeacher(username); + return mapToTeacherDTO(user); } -export async function createTeacher(userData: TeacherDTO): Promise { - const teacherRepository = getTeacherRepository(); +export async function createTeacher(userData: TeacherDTO): Promise { + const teacherRepository: TeacherRepository = getTeacherRepository(); const newTeacher = mapToTeacher(userData); await teacherRepository.save(newTeacher, { preventOverwrite: true }); - return mapToTeacherDTO(newTeacher); } -export async function deleteTeacher(username: string): Promise { - const teacherRepository = getTeacherRepository(); +export async function deleteTeacher(username: string): Promise { + const teacherRepository: TeacherRepository = getTeacherRepository(); - const user = await teacherRepository.findByUsername(username); + const teacher = await fetchTeacher(username); // Throws error if it does not exist - if (!user) { - return null; - } - - try { - await teacherRepository.deleteByUsername(username); - - return mapToTeacherDTO(user); - } catch (e) { - getLogger().error(e); - return null; - } + await teacherRepository.deleteByUsername(username); + return mapToTeacherDTO(teacher); } -export async function fetchClassesByTeacher(username: string): Promise { - const teacherRepository = getTeacherRepository(); - const teacher = await teacherRepository.findByUsername(username); - if (!teacher) { - return null; - } +async function fetchClassesByTeacher(username: string): Promise { + const teacher: Teacher = await fetchTeacher(username); - const classRepository = getClassRepository(); - const classes = await classRepository.findByTeacher(teacher); + const classRepository: ClassRepository = getClassRepository(); + const classes: Class[] = await classRepository.findByTeacher(teacher); return classes.map(mapToClassDTO); } -export async function getClassesByTeacher(username: string, full: boolean): Promise { - const classes = await fetchClassesByTeacher(username); - - if (!classes) { - return null; - } +export async function getClassesByTeacher(username: string, full: boolean): Promise { + const classes: ClassDTO[] = await fetchClassesByTeacher(username); if (full) { return classes; } - return classes.map((cls) => cls.id); } -export async function fetchStudentsByTeacher(username: string): Promise { - const classes = (await getClassesByTeacher(username, false)) as string[]; +export async function getStudentsByTeacher(username: string, full: boolean): Promise { + const classes: ClassDTO[] = await fetchClassesByTeacher(username); - if (!classes) { - return null; + if (!classes || classes.length === 0) { + return []; } - return (await Promise.all(classes.map(async (id) => getClassStudents(id)))).flat(); -} - -export async function getStudentsByTeacher(username: string, full: boolean): Promise { - const students = await fetchStudentsByTeacher(username); - - if (!students) { - return null; - } + const classIds: string[] = classes.map((cls) => cls.id); + const students: StudentDTO[] = (await Promise.all(classIds.map(async (id) => getClassStudents(id)))).flat(); if (full) { return students; } - return students.map((student) => student.username); } -export async function fetchTeacherQuestions(username: string): Promise { - const teacherRepository = getTeacherRepository(); - const teacher = await teacherRepository.findByUsername(username); - if (!teacher) { - return null; - } +export async function getTeacherQuestions(username: string, full: boolean): Promise { + const teacher: Teacher = await fetchTeacher(username); // Find all learning objects that this teacher manages - const learningObjectRepository = getLearningObjectRepository(); - const learningObjects = await learningObjectRepository.findAllByTeacher(teacher); + const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository(); + const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher); + + if (!learningObjects || learningObjects.length === 0) { + return []; + } // Fetch all questions related to these learning objects - const questionRepository = getQuestionRepository(); - const questions = await questionRepository.findAllByLearningObjects(learningObjects); - - return questions.map(mapToQuestionDTO); -} - -export async function getQuestionsByTeacher(username: string, full: boolean): Promise { - const questions = await fetchTeacherQuestions(username); - - if (!questions) { - return null; - } + const questionRepository: QuestionRepository = getQuestionRepository(); + const questions: Question[] = await questionRepository.findAllByLearningObjects(learningObjects); if (full) { - return questions; + return questions.map(mapToQuestionDTO); } - return questions.map(mapToQuestionId); + return questions.map(mapToQuestionDTOId); +} + +export async function getJoinRequestsByClass(classId: string): Promise { + const classRepository: ClassRepository = getClassRepository(); + const cls: Class | null = await classRepository.findById(classId); + + if (!cls) { + throw new NotFoundException('Class with id not found'); + } + + const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository(); + const requests: ClassJoinRequest[] = await requestRepo.findAllOpenRequestsTo(cls); + return requests.map(mapToStudentRequestDTO); +} + +export async function updateClassJoinRequestStatus(studentUsername: string, classId: string, accepted = true): Promise { + const requestRepo: ClassJoinRequestRepository = getClassJoinRequestRepository(); + const classRepo: ClassRepository = getClassRepository(); + + const student: Student = await fetchStudent(studentUsername); + const cls: Class | null = await classRepo.findById(classId); + + if (!cls) { + throw new NotFoundException('Class not found'); + } + + const request: ClassJoinRequest | null = await requestRepo.findByStudentAndClass(student, cls); + + if (!request) { + throw new NotFoundException('Join request not found'); + } + + request.status = accepted ? ClassJoinRequestStatus.Accepted : ClassJoinRequestStatus.Declined; + + await requestRepo.save(request); + return mapToStudentRequestDTO(request); } diff --git a/backend/src/util/api-helper.ts b/backend/src/util/api-helper.ts index 9aac5bd1..af36532d 100644 --- a/backend/src/util/api-helper.ts +++ b/backend/src/util/api-helper.ts @@ -1,6 +1,6 @@ import axios, { AxiosRequestConfig } from 'axios'; import { getLogger, Logger } from '../logging/initalize.js'; -import { LearningObjectIdentifier } from '../interfaces/learning-content.js'; +import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; const logger: Logger = getLogger(); diff --git a/backend/src/util/links.ts b/backend/src/util/links.ts index ff334eb5..9ede7d12 100644 --- a/backend/src/util/links.ts +++ b/backend/src/util/links.ts @@ -1,4 +1,4 @@ -import { LearningObjectIdentifier } from '../interfaces/learning-content'; +import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; export function isValidHttpUrl(url: string): boolean { try { diff --git a/backend/tests/controllers/students.test.ts b/backend/tests/controllers/students.test.ts new file mode 100644 index 00000000..93f35c48 --- /dev/null +++ b/backend/tests/controllers/students.test.ts @@ -0,0 +1,232 @@ +import { setupTestApp } from '../setup-tests.js'; +import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest'; +import { Request, Response } from 'express'; +import { + getAllStudentsHandler, + getStudentHandler, + createStudentHandler, + deleteStudentHandler, + getStudentClassesHandler, + getStudentGroupsHandler, + getStudentSubmissionsHandler, + getStudentQuestionsHandler, + createStudentRequestHandler, + getStudentRequestsHandler, + deleteClassJoinRequestHandler, + getStudentRequestHandler, +} from '../../src/controllers/students.js'; +import { TEST_STUDENTS } from '../test_assets/users/students.testdata.js'; +import { NotFoundException } from '../../src/exceptions/not-found-exception.js'; +import { BadRequestException } from '../../src/exceptions/bad-request-exception.js'; +import { ConflictException } from '../../src/exceptions/conflict-exception.js'; +import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; +import { StudentDTO } from '@dwengo-1/common/interfaces/student'; + +describe('Student controllers', () => { + let req: Partial; + let res: Partial; + + let jsonMock: Mock; + + beforeAll(async () => { + await setupTestApp(); + }); + + beforeEach(() => { + jsonMock = vi.fn(); + res = { + json: jsonMock, + }; + }); + + it('Get student', async () => { + req = { params: { username: 'DireStraits' } }; + + await getStudentHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ student: expect.anything() })); + }); + + it('Student not found', async () => { + req = { params: { username: 'doesnotexist' } }; + + await expect(async () => getStudentHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); + }); + + it('No username', async () => { + req = { params: {} }; + + await expect(async () => getStudentHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); + }); + + it('Create and delete student', async () => { + const student = { + id: 'coolstudent', + username: 'coolstudent', + firstName: 'New', + lastName: 'Student', + } as StudentDTO; + req = { + body: student, + }; + + await createStudentHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ student: expect.objectContaining(student) })); + + req = { params: { username: 'coolstudent' } }; + + await deleteStudentHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ student: expect.objectContaining(student) })); + }); + + it('Create duplicate student', async () => { + req = { + body: { + username: 'DireStraits', + firstName: 'dupe', + lastName: 'dupe', + }, + }; + + await expect(async () => createStudentHandler(req as Request, res as Response)).rejects.toThrowError(EntityAlreadyExistsException); + }); + + it('Create student no body', async () => { + req = { body: {} }; + + await expect(async () => createStudentHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); + }); + + it('Student list', async () => { + req = { query: { full: 'true' } }; + + await getAllStudentsHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ students: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + + // Check is DireStraits is part of the student list + const studentUsernames = result.students.map((s: StudentDTO) => s.username); + expect(studentUsernames).toContain('DireStraits'); + + // Check length, +1 because of create + expect(result.students).toHaveLength(TEST_STUDENTS.length); + }); + + it('Student classes', async () => { + req = { params: { username: 'DireStraits' }, query: {} }; + + await getStudentClassesHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ classes: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + expect(result.classes).to.have.length.greaterThan(0); + }); + + it('Student groups', async () => { + req = { params: { username: 'DireStraits' }, query: {} }; + + await getStudentGroupsHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ groups: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + expect(result.groups).to.have.length.greaterThan(0); + }); + + it('Student submissions', async () => { + req = { params: { username: 'DireStraits' }, query: { full: 'true' } }; + + await getStudentSubmissionsHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + expect(result.submissions).to.have.length.greaterThan(0); + }); + + it('Student questions', async () => { + req = { params: { username: 'DireStraits' }, query: { full: 'true' } }; + + await getStudentQuestionsHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ questions: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + expect(result.questions).to.have.length.greaterThan(0); + }); + + it('Deleting non-existent student', async () => { + req = { params: { username: 'doesnotexist' } }; + + await expect(async () => deleteStudentHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); + }); + + it('Get join requests by student', async () => { + req = { + params: { username: 'PinkFloyd' }, + }; + + await getStudentRequestsHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith( + expect.objectContaining({ + requests: expect.anything(), + }) + ); + + const result = jsonMock.mock.lastCall?.[0]; + // Console.log('[JOIN REQUESTS]', result.requests); + expect(result.requests.length).toBeGreaterThan(0); + }); + + it('Get join request by student and class', async () => { + req = { + params: { username: 'PinkFloyd', classId: 'id02' }, + }; + + await getStudentRequestHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith( + expect.objectContaining({ + request: expect.anything(), + }) + ); + }); + + it('Create join request', async () => { + req = { + params: { username: 'Noordkaap' }, + body: { classId: 'id02' }, + }; + + await createStudentRequestHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); + }); + + it('Create join request duplicate', async () => { + req = { + params: { username: 'Tool' }, + body: { classId: 'id02' }, + }; + + await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException); + }); + + it('Delete join request', async () => { + req = { + params: { username: 'Noordkaap', classId: 'id02' }, + }; + + await deleteClassJoinRequestHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); + + await expect(async () => deleteClassJoinRequestHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); + }); +}); diff --git a/backend/tests/controllers/teachers.test.ts b/backend/tests/controllers/teachers.test.ts new file mode 100644 index 00000000..bee23987 --- /dev/null +++ b/backend/tests/controllers/teachers.test.ts @@ -0,0 +1,204 @@ +import { beforeAll, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { Request, Response } from 'express'; +import { setupTestApp } from '../setup-tests.js'; +import { NotFoundException } from '../../src/exceptions/not-found-exception.js'; +import { + createTeacherHandler, + deleteTeacherHandler, + getAllTeachersHandler, + getStudentJoinRequestHandler, + getTeacherClassHandler, + getTeacherHandler, + getTeacherStudentHandler, + updateStudentJoinRequestHandler, +} from '../../src/controllers/teachers.js'; +import { BadRequestException } from '../../src/exceptions/bad-request-exception.js'; +import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; +import { getStudentRequestsHandler } from '../../src/controllers/students.js'; +import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; + +describe('Teacher controllers', () => { + let req: Partial; + let res: Partial; + + let jsonMock: Mock; + + beforeAll(async () => { + await setupTestApp(); + }); + + beforeEach(() => { + jsonMock = vi.fn(); + res = { + json: jsonMock, + }; + }); + + it('Get teacher', async () => { + req = { params: { username: 'FooFighters' } }; + + await getTeacherHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ teacher: expect.anything() })); + }); + + it('Teacher not found', async () => { + req = { params: { username: 'doesnotexist' } }; + + await expect(async () => getTeacherHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); + }); + + it('No username', async () => { + req = { params: {} }; + + await expect(async () => getTeacherHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); + }); + + it('Create and delete teacher', async () => { + const teacher = { + id: 'coolteacher', + username: 'coolteacher', + firstName: 'New', + lastName: 'Teacher', + }; + req = { + body: teacher, + }; + + await createTeacherHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ teacher: expect.objectContaining(teacher) })); + + req = { params: { username: 'coolteacher' } }; + + await deleteTeacherHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ teacher: expect.objectContaining(teacher) })); + }); + + it('Create duplicate student', async () => { + req = { + body: { + username: 'FooFighters', + firstName: 'Dave', + lastName: 'Grohl', + }, + }; + + await expect(async () => createTeacherHandler(req as Request, res as Response)).rejects.toThrowError(EntityAlreadyExistsException); + }); + + it('Create teacher no body', async () => { + req = { body: {} }; + + await expect(async () => createTeacherHandler(req as Request, res as Response)).rejects.toThrowError(BadRequestException); + }); + + it('Teacher list', async () => { + req = { query: { full: 'true' } }; + + await getAllTeachersHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ teachers: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + + const teacherUsernames = result.teachers.map((s: TeacherDTO) => s.username); + expect(teacherUsernames).toContain('FooFighters'); + + expect(result.teachers).toHaveLength(4); + }); + + it('Deleting non-existent student', async () => { + req = { params: { username: 'doesnotexist' } }; + + await expect(async () => deleteTeacherHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException); + }); + + it('Get teacher classes', async () => { + req = { + params: { username: 'FooFighters' }, + query: { full: 'true' }, + }; + + await getTeacherClassHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ classes: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + // Console.log('[TEACHER CLASSES]', result); + expect(result.classes.length).toBeGreaterThan(0); + }); + + it('Get teacher students', async () => { + req = { + params: { username: 'FooFighters' }, + query: { full: 'true' }, + }; + + await getTeacherStudentHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ students: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + // Console.log('[TEACHER STUDENTS]', result.students); + expect(result.students.length).toBeGreaterThan(0); + }); + + /* + + It('Get teacher questions', async () => { + req = { + params: { username: 'FooFighters' }, + query: { full: 'true' }, + }; + + await getTeacherQuestionHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ questions: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + // console.log('[TEACHER QUESTIONS]', result.questions); + expect(result.questions.length).toBeGreaterThan(0); + + // TODO fix + }); + + */ + + it('Get join requests by class', async () => { + req = { + query: { username: 'LimpBizkit' }, + params: { classId: 'id02' }, + }; + + await getStudentJoinRequestHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ joinRequests: expect.anything() })); + + const result = jsonMock.mock.lastCall?.[0]; + // Console.log('[JOIN REQUESTS FOR CLASS]', result.joinRequests); + expect(result.joinRequests.length).toBeGreaterThan(0); + }); + + it('Update join request status', async () => { + req = { + query: { username: 'LimpBizkit', studentUsername: 'PinkFloyd' }, + params: { classId: 'id02' }, + body: { accepted: 'true' }, + }; + + await updateStudentJoinRequestHandler(req as Request, res as Response); + + expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); + + req = { + params: { username: 'PinkFloyd' }, + }; + + await getStudentRequestsHandler(req as Request, res as Response); + + const status: boolean = jsonMock.mock.lastCall?.[0].requests[0].status; + expect(status).toBeTruthy(); + }); +}); diff --git a/backend/tests/data/assignments/submissions.test.ts b/backend/tests/data/assignments/submissions.test.ts index cd212b77..85e1bc11 100644 --- a/backend/tests/data/assignments/submissions.test.ts +++ b/backend/tests/data/assignments/submissions.test.ts @@ -9,7 +9,7 @@ import { getSubmissionRepository, } from '../../../src/data/repositories'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { StudentRepository } from '../../../src/data/users/student-repository'; import { GroupRepository } from '../../../src/data/assignments/group-repository'; import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository'; diff --git a/backend/tests/data/content/attachments.test.ts b/backend/tests/data/content/attachments.test.ts index 94e132a9..4e65954e 100644 --- a/backend/tests/data/content/attachments.test.ts +++ b/backend/tests/data/content/attachments.test.ts @@ -4,7 +4,7 @@ import { getAttachmentRepository, getLearningObjectRepository } from '../../../s import { AttachmentRepository } from '../../../src/data/content/attachment-repository.js'; import { LearningObjectRepository } from '../../../src/data/content/learning-object-repository.js'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier.js'; -import { Language } from '../../../src/entities/content/language.js'; +import { Language } from '@dwengo-1/common/util/language'; describe('AttachmentRepository', () => { let attachmentRepository: AttachmentRepository; diff --git a/backend/tests/data/content/learning-objects.test.ts b/backend/tests/data/content/learning-objects.test.ts index 712f75c9..3c9d5dde 100644 --- a/backend/tests/data/content/learning-objects.test.ts +++ b/backend/tests/data/content/learning-objects.test.ts @@ -3,7 +3,7 @@ import { LearningObjectRepository } from '../../../src/data/content/learning-obj import { getLearningObjectRepository } from '../../../src/data/repositories'; import { setupTestApp } from '../../setup-tests'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; describe('LearningObjectRepository', () => { let learningObjectRepository: LearningObjectRepository; diff --git a/backend/tests/data/content/learning-path-repository.test.ts b/backend/tests/data/content/learning-path-repository.test.ts index 4424fced..bdf5377e 100644 --- a/backend/tests/data/content/learning-path-repository.test.ts +++ b/backend/tests/data/content/learning-path-repository.test.ts @@ -5,7 +5,7 @@ import { LearningPathRepository } from '../../../src/data/content/learning-path- import example from '../../test-assets/learning-paths/pn-werking-example.js'; import { LearningPath } from '../../../src/entities/content/learning-path.entity.js'; import { expectToBeCorrectEntity } from '../../test-utils/expectations.js'; -import { Language } from '../../../src/entities/content/language.js'; +import { Language } from '@dwengo-1/common/util/language'; function expectToHaveFoundPrecisely(expected: LearningPath, result: LearningPath[]): void { expect(result).toHaveProperty('length'); diff --git a/backend/tests/data/content/learning-paths.test.ts b/backend/tests/data/content/learning-paths.test.ts index 01fd20e5..683e1d40 100644 --- a/backend/tests/data/content/learning-paths.test.ts +++ b/backend/tests/data/content/learning-paths.test.ts @@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest'; import { getLearningPathRepository } from '../../../src/data/repositories'; import { LearningPathRepository } from '../../../src/data/content/learning-path-repository'; import { setupTestApp } from '../../setup-tests'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; describe('LearningPathRepository', () => { let learningPathRepository: LearningPathRepository; diff --git a/backend/tests/data/questions/answers.test.ts b/backend/tests/data/questions/answers.test.ts index 59917c3f..80cd3034 100644 --- a/backend/tests/data/questions/answers.test.ts +++ b/backend/tests/data/questions/answers.test.ts @@ -4,7 +4,7 @@ import { AnswerRepository } from '../../../src/data/questions/answer-repository' import { getAnswerRepository, getQuestionRepository, getTeacherRepository } from '../../../src/data/repositories'; import { QuestionRepository } from '../../../src/data/questions/question-repository'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { TeacherRepository } from '../../../src/data/users/teacher-repository'; describe('AnswerRepository', () => { diff --git a/backend/tests/data/questions/questions.test.ts b/backend/tests/data/questions/questions.test.ts index c1d2b483..055a9d79 100644 --- a/backend/tests/data/questions/questions.test.ts +++ b/backend/tests/data/questions/questions.test.ts @@ -4,7 +4,7 @@ import { QuestionRepository } from '../../../src/data/questions/question-reposit import { getQuestionRepository, getStudentRepository } from '../../../src/data/repositories'; import { StudentRepository } from '../../../src/data/users/student-repository'; import { LearningObjectIdentifier } from '../../../src/entities/content/learning-object-identifier'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; describe('QuestionRepository', () => { let questionRepository: QuestionRepository; diff --git a/backend/tests/services/learning-objects/database-learning-object-provider.test.ts b/backend/tests/services/learning-objects/database-learning-object-provider.test.ts index 2c3b7dfc..31899ded 100644 --- a/backend/tests/services/learning-objects/database-learning-object-provider.test.ts +++ b/backend/tests/services/learning-objects/database-learning-object-provider.test.ts @@ -5,11 +5,11 @@ import example from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-w import { LearningObject } from '../../../src/entities/content/learning-object.entity'; import databaseLearningObjectProvider from '../../../src/services/learning-objects/database-learning-object-provider'; import { expectToBeCorrectFilteredLearningObject } from '../../test-utils/expectations'; -import { FilteredLearningObject } from '../../../src/interfaces/learning-content'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example'; import learningPathExample from '../../test-assets/learning-paths/pn-werking-example'; import { LearningPath } from '../../../src/entities/content/learning-path.entity'; +import { FilteredLearningObject } from '@dwengo-1/common/interfaces/learning-content'; async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { const learningObjectRepo = getLearningObjectRepository(); @@ -37,7 +37,7 @@ describe('DatabaseLearningObjectProvider', () => { it('should return the learning object when it is queried by its id', async () => { const result: FilteredLearningObject | null = await databaseLearningObjectProvider.getLearningObjectById(exampleLearningObject); expect(result).toBeTruthy(); - expectToBeCorrectFilteredLearningObject(result!, exampleLearningObject); + expectToBeCorrectFilteredLearningObject(result, exampleLearningObject); }); it('should return the learning object when it is queried by only hruid and language (but not version)', async () => { @@ -46,7 +46,7 @@ describe('DatabaseLearningObjectProvider', () => { language: exampleLearningObject.language, }); expect(result).toBeTruthy(); - expectToBeCorrectFilteredLearningObject(result!, exampleLearningObject); + expectToBeCorrectFilteredLearningObject(result, exampleLearningObject); }); it('should return null when queried with an id that does not exist', async () => { diff --git a/backend/tests/services/learning-objects/learning-object-service.test.ts b/backend/tests/services/learning-objects/learning-object-service.test.ts index f284b939..a0fea849 100644 --- a/backend/tests/services/learning-objects/learning-object-service.test.ts +++ b/backend/tests/services/learning-objects/learning-object-service.test.ts @@ -4,11 +4,11 @@ import { LearningObject } from '../../../src/entities/content/learning-object.en import { getLearningObjectRepository, getLearningPathRepository } from '../../../src/data/repositories'; import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example'; import learningObjectService from '../../../src/services/learning-objects/learning-object-service'; -import { LearningObjectIdentifier, LearningPathIdentifier } from '../../../src/interfaces/learning-content'; -import { Language } from '../../../src/entities/content/language'; import { envVars, getEnvVar } from '../../../src/util/envVars'; import { LearningPath } from '../../../src/entities/content/learning-path.entity'; import learningPathExample from '../../test-assets/learning-paths/pn-werking-example'; +import { LearningObjectIdentifier, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { Language } from '@dwengo-1/common/util/language'; const EXPECTED_DWENGO_LEARNING_OBJECT_TITLE = 'Werken met notebooks'; const DWENGO_TEST_LEARNING_OBJECT_ID: LearningObjectIdentifier = { @@ -105,8 +105,11 @@ describe('LearningObjectService', () => { expect(new Set(result.map((it) => it.key))).toEqual(DWENGO_TEST_LEARNING_PATH_HRUIDS); }); it('returns an empty list when queried with a non-existing learning path id', async () => { - const result = await learningObjectService.getLearningObjectsFromPath({ hruid: 'non_existing', language: Language.Dutch }); - expect(result).toEqual([]); + const result = await learningObjectService.getLearningObjectsFromPath({ + hruid: 'non_existing', + language: Language.Dutch, + }); + expect(result).toStrictEqual([]); }); }); @@ -120,8 +123,11 @@ describe('LearningObjectService', () => { expect(new Set(result)).toEqual(DWENGO_TEST_LEARNING_PATH_HRUIDS); }); it('returns an empty list when queried with a non-existing learning path id', async () => { - const result = await learningObjectService.getLearningObjectIdsFromPath({ hruid: 'non_existing', language: Language.Dutch }); - expect(result).toEqual([]); + const result = await learningObjectService.getLearningObjectIdsFromPath({ + hruid: 'non_existing', + language: Language.Dutch, + }); + expect(result).toStrictEqual([]); }); }); }); diff --git a/backend/tests/services/learning-path/database-learning-path-provider.test.ts b/backend/tests/services/learning-path/database-learning-path-provider.test.ts index a5943f5f..b8a733e7 100644 --- a/backend/tests/services/learning-path/database-learning-path-provider.test.ts +++ b/backend/tests/services/learning-path/database-learning-path-provider.test.ts @@ -13,13 +13,14 @@ import learningPathExample from '../../test-assets/learning-paths/pn-werking-exa import databaseLearningPathProvider from '../../../src/services/learning-paths/database-learning-path-provider.js'; import { expectToBeCorrectLearningPath } from '../../test-utils/expectations.js'; import learningObjectService from '../../../src/services/learning-objects/learning-object-service.js'; -import { Language } from '../../../src/entities/content/language.js'; +import { Language } from '@dwengo-1/common/util/language'; import { ConditionTestLearningPathAndLearningObjects, createConditionTestLearningPathAndLearningObjects, } from '../../test-assets/learning-paths/test-conditions-example.js'; import { Student } from '../../../src/entities/users/student.entity.js'; -import { LearningObjectNode, LearningPathResponse } from '../../../src/interfaces/learning-content.js'; + +import { LearningObjectNode, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { const learningObjectRepo = getLearningObjectRepository(); diff --git a/backend/tests/services/learning-path/learning-path-service.test.ts b/backend/tests/services/learning-path/learning-path-service.test.ts index 7a45dc43..972a7fa1 100644 --- a/backend/tests/services/learning-path/learning-path-service.test.ts +++ b/backend/tests/services/learning-path/learning-path-service.test.ts @@ -5,8 +5,8 @@ import { LearningPath } from '../../../src/entities/content/learning-path.entity import { getLearningObjectRepository, getLearningPathRepository } from '../../../src/data/repositories'; import learningObjectExample from '../../test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example'; import learningPathExample from '../../test-assets/learning-paths/pn-werking-example'; -import { Language } from '../../../src/entities/content/language'; import learningPathService from '../../../src/services/learning-paths/learning-path-service'; +import { Language } from '@dwengo-1/common/util/language'; async function initExampleData(): Promise<{ learningObject: LearningObject; learningPath: LearningPath }> { const learningObjectRepo = getLearningObjectRepository(); @@ -48,8 +48,8 @@ describe('LearningPathService', () => { expect(result.data?.length).toBe(1); // Should include all the nodes, even those pointing to foreign learning objects. - expect([...result.data![0].nodes.map((it) => it.learningobject_hruid)].sort()).toEqual( - example.learningPath.nodes.map((it) => it.learningObjectHruid).sort() + expect([...result.data![0].nodes.map((it) => it.learningobject_hruid)].sort((a, b) => a.localeCompare(b))).toEqual( + example.learningPath.nodes.map((it) => it.learningObjectHruid).sort((a, b) => a.localeCompare(b)) ); }); }); diff --git a/backend/tests/test-assets/learning-objects/dummy/dummy-learning-object-example.ts b/backend/tests/test-assets/learning-objects/dummy/dummy-learning-object-example.ts index f810d57a..6889c93b 100644 --- a/backend/tests/test-assets/learning-objects/dummy/dummy-learning-object-example.ts +++ b/backend/tests/test-assets/learning-objects/dummy/dummy-learning-object-example.ts @@ -1,6 +1,6 @@ import { LearningObjectExample } from '../learning-object-example'; import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; -import { Language } from '../../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { loadTestAsset } from '../../../test-utils/load-test-asset'; import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; import { envVars, getEnvVar } from '../../../../src/util/envVars'; diff --git a/backend/tests/test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example.ts b/backend/tests/test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example.ts index 835a0ed0..ab0c8640 100644 --- a/backend/tests/test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example.ts +++ b/backend/tests/test-assets/learning-objects/pn-werkingnotebooks/pn-werkingnotebooks-example.ts @@ -1,5 +1,5 @@ import { LearningObjectExample } from '../learning-object-example'; -import { Language } from '../../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; import { loadTestAsset } from '../../../test-utils/load-test-asset'; import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; diff --git a/backend/tests/test-assets/learning-objects/test-essay/test-essay-example.ts b/backend/tests/test-assets/learning-objects/test-essay/test-essay-example.ts index 943a10c7..5a444fc0 100644 --- a/backend/tests/test-assets/learning-objects/test-essay/test-essay-example.ts +++ b/backend/tests/test-assets/learning-objects/test-essay/test-essay-example.ts @@ -2,7 +2,7 @@ import { LearningObjectExample } from '../learning-object-example'; import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; import { loadTestAsset } from '../../../test-utils/load-test-asset'; import { envVars, getEnvVar } from '../../../../src/util/envVars'; -import { Language } from '../../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; const example: LearningObjectExample = { diff --git a/backend/tests/test-assets/learning-objects/test-multiple-choice/test-multiple-choice-example.ts b/backend/tests/test-assets/learning-objects/test-multiple-choice/test-multiple-choice-example.ts index 276a41bb..129665ae 100644 --- a/backend/tests/test-assets/learning-objects/test-multiple-choice/test-multiple-choice-example.ts +++ b/backend/tests/test-assets/learning-objects/test-multiple-choice/test-multiple-choice-example.ts @@ -2,8 +2,8 @@ import { LearningObjectExample } from '../learning-object-example'; import { LearningObject } from '../../../../src/entities/content/learning-object.entity'; import { loadTestAsset } from '../../../test-utils/load-test-asset'; import { envVars, getEnvVar } from '../../../../src/util/envVars'; -import { Language } from '../../../../src/entities/content/language'; import { DwengoContentType } from '../../../../src/services/learning-objects/processing/content-type'; +import { Language } from '@dwengo-1/common/util/language'; const example: LearningObjectExample = { createLearningObject: () => { diff --git a/backend/tests/test-assets/learning-paths/learning-path-utils.ts b/backend/tests/test-assets/learning-paths/learning-path-utils.ts index eb786fe1..177d905f 100644 --- a/backend/tests/test-assets/learning-paths/learning-path-utils.ts +++ b/backend/tests/test-assets/learning-paths/learning-path-utils.ts @@ -1,4 +1,4 @@ -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { LearningPathTransition } from '../../../src/entities/content/learning-path-transition.entity'; import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity'; import { LearningPath } from '../../../src/entities/content/learning-path.entity'; diff --git a/backend/tests/test-assets/learning-paths/pn-werking-example.ts b/backend/tests/test-assets/learning-paths/pn-werking-example.ts index 06a29007..1ac1c40d 100644 --- a/backend/tests/test-assets/learning-paths/pn-werking-example.ts +++ b/backend/tests/test-assets/learning-paths/pn-werking-example.ts @@ -1,5 +1,5 @@ import { LearningPath } from '../../../src/entities/content/learning-path.entity'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { envVars, getEnvVar } from '../../../src/util/envVars'; import { createLearningPathNode, createLearningPathTransition } from './learning-path-utils'; import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity'; diff --git a/backend/tests/test-assets/learning-paths/test-conditions-example.ts b/backend/tests/test-assets/learning-paths/test-conditions-example.ts index b5e38c24..0fb7ead5 100644 --- a/backend/tests/test-assets/learning-paths/test-conditions-example.ts +++ b/backend/tests/test-assets/learning-paths/test-conditions-example.ts @@ -1,5 +1,5 @@ import { LearningPath } from '../../../src/entities/content/learning-path.entity'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import testMultipleChoiceExample from '../learning-objects/test-multiple-choice/test-multiple-choice-example'; import { dummyLearningObject } from '../learning-objects/dummy/dummy-learning-object-example'; import { createLearningPathNode, createLearningPathTransition } from './learning-path-utils'; diff --git a/backend/tests/test-utils/expectations.ts b/backend/tests/test-utils/expectations.ts index c3fa5a43..b6462702 100644 --- a/backend/tests/test-utils/expectations.ts +++ b/backend/tests/test-utils/expectations.ts @@ -1,8 +1,8 @@ import { AssertionError } from 'node:assert'; import { LearningObject } from '../../src/entities/content/learning-object.entity'; -import { FilteredLearningObject, LearningPath } from '../../src/interfaces/learning-content'; import { LearningPath as LearningPathEntity } from '../../src/entities/content/learning-path.entity'; import { expect } from 'vitest'; +import { FilteredLearningObject, LearningPath } from '@dwengo-1/common/interfaces/learning-content'; // Ignored properties because they belang for example to the class, not to the entity itself. const IGNORE_PROPERTIES = ['parent']; diff --git a/backend/tests/test_assets/assignments/assignments.testdata.ts b/backend/tests/test_assets/assignments/assignments.testdata.ts index 8753b0ce..b0da638f 100644 --- a/backend/tests/test_assets/assignments/assignments.testdata.ts +++ b/backend/tests/test_assets/assignments/assignments.testdata.ts @@ -1,7 +1,7 @@ import { EntityManager } from '@mikro-orm/core'; import { Assignment } from '../../../src/entities/assignments/assignment.entity'; import { Class } from '../../../src/entities/classes/class.entity'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] { const assignment01 = em.create(Assignment, { diff --git a/backend/tests/test_assets/assignments/submission.testdata.ts b/backend/tests/test_assets/assignments/submission.testdata.ts index 4fca5f9b..f454b133 100644 --- a/backend/tests/test_assets/assignments/submission.testdata.ts +++ b/backend/tests/test_assets/assignments/submission.testdata.ts @@ -1,6 +1,6 @@ import { EntityManager } from '@mikro-orm/core'; import { Submission } from '../../../src/entities/assignments/submission.entity'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { Student } from '../../../src/entities/users/student.entity'; import { Group } from '../../../src/entities/assignments/group.entity'; diff --git a/backend/tests/test_assets/classes/class-join-requests.testdata.ts b/backend/tests/test_assets/classes/class-join-requests.testdata.ts index 632fd392..32337b19 100644 --- a/backend/tests/test_assets/classes/class-join-requests.testdata.ts +++ b/backend/tests/test_assets/classes/class-join-requests.testdata.ts @@ -1,7 +1,8 @@ import { EntityManager } from '@mikro-orm/core'; -import { ClassJoinRequest, ClassJoinRequestStatus } from '../../../src/entities/classes/class-join-request.entity'; +import { ClassJoinRequest } from '../../../src/entities/classes/class-join-request.entity'; import { Student } from '../../../src/entities/users/student.entity'; import { Class } from '../../../src/entities/classes/class.entity'; +import { ClassJoinRequestStatus } from '@dwengo-1/common/util/class-join-request'; export function makeTestClassJoinRequests(em: EntityManager, students: Student[], classes: Class[]): ClassJoinRequest[] { const classJoinRequest01 = em.create(ClassJoinRequest, { diff --git a/backend/tests/test_assets/content/learning-objects.testdata.ts b/backend/tests/test_assets/content/learning-objects.testdata.ts index bbca8830..6e28dc16 100644 --- a/backend/tests/test_assets/content/learning-objects.testdata.ts +++ b/backend/tests/test_assets/content/learning-objects.testdata.ts @@ -1,6 +1,6 @@ import { EntityManager } from '@mikro-orm/core'; import { LearningObject } from '../../../src/entities/content/learning-object.entity'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { DwengoContentType } from '../../../src/services/learning-objects/processing/content-type'; import { ReturnValue } from '../../../src/entities/content/return-value.entity'; diff --git a/backend/tests/test_assets/content/learning-paths.testdata.ts b/backend/tests/test_assets/content/learning-paths.testdata.ts index e41773cc..72581f42 100644 --- a/backend/tests/test_assets/content/learning-paths.testdata.ts +++ b/backend/tests/test_assets/content/learning-paths.testdata.ts @@ -1,6 +1,6 @@ import { EntityManager } from '@mikro-orm/core'; import { LearningPath } from '../../../src/entities/content/learning-path.entity'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { LearningPathTransition } from '../../../src/entities/content/learning-path-transition.entity'; import { LearningPathNode } from '../../../src/entities/content/learning-path-node.entity'; diff --git a/backend/tests/test_assets/questions/questions.testdata.ts b/backend/tests/test_assets/questions/questions.testdata.ts index 8604cf07..dff742bb 100644 --- a/backend/tests/test_assets/questions/questions.testdata.ts +++ b/backend/tests/test_assets/questions/questions.testdata.ts @@ -1,6 +1,6 @@ import { EntityManager } from '@mikro-orm/core'; import { Question } from '../../../src/entities/questions/question.entity'; -import { Language } from '../../../src/entities/content/language'; +import { Language } from '@dwengo-1/common/util/language'; import { Student } from '../../../src/entities/users/student.entity'; export function makeTestQuestions(em: EntityManager, students: Student[]): Question[] { diff --git a/backend/tests/test_assets/users/students.testdata.ts b/backend/tests/test_assets/users/students.testdata.ts index 7d69db9e..5cd75787 100644 --- a/backend/tests/test_assets/users/students.testdata.ts +++ b/backend/tests/test_assets/users/students.testdata.ts @@ -1,49 +1,19 @@ import { EntityManager } from '@mikro-orm/core'; import { Student } from '../../../src/entities/users/student.entity'; +// 🔓 Ruwe testdata array — herbruikbaar in assertions +export const TEST_STUDENTS = [ + { username: 'Noordkaap', firstName: 'Stijn', lastName: 'Meuris' }, + { username: 'DireStraits', firstName: 'Mark', lastName: 'Knopfler' }, + { username: 'Tool', firstName: 'Maynard', lastName: 'Keenan' }, + { username: 'SmashingPumpkins', firstName: 'Billy', lastName: 'Corgan' }, + { username: 'PinkFloyd', firstName: 'David', lastName: 'Gilmoure' }, + { username: 'TheDoors', firstName: 'Jim', lastName: 'Morisson' }, + // ⚠️ Deze mag niet gebruikt worden in elke test! + { username: 'Nirvana', firstName: 'Kurt', lastName: 'Cobain' }, +]; + +// 🏗️ Functie die ORM entities maakt uit de data array export function makeTestStudents(em: EntityManager): Student[] { - const student01 = em.create(Student, { - username: 'Noordkaap', - firstName: 'Stijn', - lastName: 'Meuris', - }); - - const student02 = em.create(Student, { - username: 'DireStraits', - firstName: 'Mark', - lastName: 'Knopfler', - }); - - const student03 = em.create(Student, { - username: 'Tool', - firstName: 'Maynard', - lastName: 'Keenan', - }); - - const student04 = em.create(Student, { - username: 'SmashingPumpkins', - firstName: 'Billy', - lastName: 'Corgan', - }); - - const student05 = em.create(Student, { - username: 'PinkFloyd', - firstName: 'David', - lastName: 'Gilmoure', - }); - - const student06 = em.create(Student, { - username: 'TheDoors', - firstName: 'Jim', - lastName: 'Morisson', - }); - - // Do not use for any tests, gets deleted in a unit test - const student07 = em.create(Student, { - username: 'Nirvana', - firstName: 'Kurt', - lastName: 'Cobain', - }); - - return [student01, student02, student03, student04, student05, student06, student07]; + return TEST_STUDENTS.map((data) => em.create(Student, data)); } diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 2dd3998d..1ddfe1e0 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -3,7 +3,11 @@ "include": ["src/**/*.ts"], "compilerOptions": { "rootDir": "./src", - "outDir": "./dist", - "resolveJsonModule": true - } + "outDir": "./dist" + }, + "references": [ + { + "path": "../common" + } + ] } diff --git a/backend/vitest.config.ts b/backend/vitest.config.ts index 461d2018..29142c49 100644 --- a/backend/vitest.config.ts +++ b/backend/vitest.config.ts @@ -4,6 +4,6 @@ export default defineConfig({ test: { environment: 'node', globals: true, - testTimeout: 10000, + testTimeout: 100000, }, }); diff --git a/common/package.json b/common/package.json new file mode 100644 index 00000000..7eef4ded --- /dev/null +++ b/common/package.json @@ -0,0 +1,21 @@ +{ + "name": "@dwengo-1/common", + "version": "0.1.1", + "description": "Common types and utilities for Dwengo-1", + "private": true, + "type": "module", + "files": [ + "./dist" + ], + "scripts": { + "build": "tsc --build", + "clean": "tsc --build --clean", + "watch": "tsc --build --watch", + "format": "prettier --write src/", + "format-check": "prettier --check src/", + "lint": "eslint . --fix" + }, + "exports": { + "./*": "./dist/*.js" + } +} diff --git a/common/src/interfaces/answer.ts b/common/src/interfaces/answer.ts new file mode 100644 index 00000000..e9280f8a --- /dev/null +++ b/common/src/interfaces/answer.ts @@ -0,0 +1,16 @@ +import { UserDTO } from './user'; +import { QuestionDTO, QuestionId } from './question'; + +export interface AnswerDTO { + author: UserDTO; + toQuestion: QuestionDTO; + sequenceNumber: number; + timestamp: string; + content: string; +} + +export interface AnswerId { + author: string; + toQuestion: QuestionId; + sequenceNumber: number; +} diff --git a/common/src/interfaces/assignment.ts b/common/src/interfaces/assignment.ts new file mode 100644 index 00000000..8ad1649b --- /dev/null +++ b/common/src/interfaces/assignment.ts @@ -0,0 +1,11 @@ +import { GroupDTO } from './group'; + +export interface AssignmentDTO { + id: number; + class: string; // Id of class 'within' + title: string; + description: string; + learningPath: string; + language: string; + groups?: GroupDTO[] | string[]; // TODO +} diff --git a/common/src/interfaces/class-join-request.ts b/common/src/interfaces/class-join-request.ts new file mode 100644 index 00000000..6787998b --- /dev/null +++ b/common/src/interfaces/class-join-request.ts @@ -0,0 +1,8 @@ +import { StudentDTO } from './student'; +import { ClassJoinRequestStatus } from '../util/class-join-request'; + +export interface ClassJoinRequestDTO { + requester: StudentDTO; + class: string; + status: ClassJoinRequestStatus; +} diff --git a/common/src/interfaces/class.ts b/common/src/interfaces/class.ts new file mode 100644 index 00000000..c35c2dfc --- /dev/null +++ b/common/src/interfaces/class.ts @@ -0,0 +1,7 @@ +export interface ClassDTO { + id: string; + displayName: string; + teachers: string[]; + students: string[]; + joinRequests: string[]; +} diff --git a/common/src/interfaces/group.ts b/common/src/interfaces/group.ts new file mode 100644 index 00000000..ca95770a --- /dev/null +++ b/common/src/interfaces/group.ts @@ -0,0 +1,8 @@ +import { AssignmentDTO } from './assignment'; +import { StudentDTO } from './student'; + +export interface GroupDTO { + assignment: number | AssignmentDTO; + groupNumber: number; + members: string[] | StudentDTO[]; +} diff --git a/backend/src/interfaces/learning-content.ts b/common/src/interfaces/learning-content.ts similarity index 97% rename from backend/src/interfaces/learning-content.ts rename to common/src/interfaces/learning-content.ts index 693aec37..02e49648 100644 --- a/backend/src/interfaces/learning-content.ts +++ b/common/src/interfaces/learning-content.ts @@ -1,4 +1,4 @@ -import { Language } from '../entities/content/language'; +import { Language } from '../util/language'; export interface Transition { default: boolean; diff --git a/common/src/interfaces/question.ts b/common/src/interfaces/question.ts new file mode 100644 index 00000000..b80ff0af --- /dev/null +++ b/common/src/interfaces/question.ts @@ -0,0 +1,15 @@ +import { LearningObjectIdentifier } from './learning-content'; +import { StudentDTO } from './student'; + +export interface QuestionDTO { + learningObjectIdentifier: LearningObjectIdentifier; + sequenceNumber?: number; + author: StudentDTO; + timestamp?: string; + content: string; +} + +export interface QuestionId { + learningObjectIdentifier: LearningObjectIdentifier; + sequenceNumber: number; +} diff --git a/common/src/interfaces/student.ts b/common/src/interfaces/student.ts new file mode 100644 index 00000000..7ec628b4 --- /dev/null +++ b/common/src/interfaces/student.ts @@ -0,0 +1,6 @@ +export interface StudentDTO { + id: string; + username: string; + firstName: string; + lastName: string; +} diff --git a/common/src/interfaces/submission.ts b/common/src/interfaces/submission.ts new file mode 100644 index 00000000..6b250616 --- /dev/null +++ b/common/src/interfaces/submission.ts @@ -0,0 +1,22 @@ +import { GroupDTO } from './group'; +import { LearningObjectIdentifier } from './learning-content'; +import { StudentDTO } from './student'; +import { Language } from '../util/language'; + +export interface SubmissionDTO { + learningObjectIdentifier: LearningObjectIdentifier; + + submissionNumber?: number; + submitter: StudentDTO; + time?: Date; + group?: GroupDTO; + content: string; +} + +export interface SubmissionDTOId { + learningObjectHruid: string; + learningObjectLanguage: Language; + learningObjectVersion: number; + + submissionNumber?: number; +} diff --git a/common/src/interfaces/teacher-invitation.ts b/common/src/interfaces/teacher-invitation.ts new file mode 100644 index 00000000..13709322 --- /dev/null +++ b/common/src/interfaces/teacher-invitation.ts @@ -0,0 +1,8 @@ +import { UserDTO } from './user'; +import { ClassDTO } from './class'; + +export interface TeacherInvitationDTO { + sender: string | UserDTO; + receiver: string | UserDTO; + class: string | ClassDTO; +} diff --git a/common/src/interfaces/teacher.ts b/common/src/interfaces/teacher.ts new file mode 100644 index 00000000..53195c33 --- /dev/null +++ b/common/src/interfaces/teacher.ts @@ -0,0 +1,6 @@ +export interface TeacherDTO { + id: string; + username: string; + firstName: string; + lastName: string; +} diff --git a/common/src/interfaces/theme.ts b/common/src/interfaces/theme.ts new file mode 100644 index 00000000..5334b775 --- /dev/null +++ b/common/src/interfaces/theme.ts @@ -0,0 +1,4 @@ +export interface Theme { + title: string; + hruids: string[]; +} diff --git a/common/src/interfaces/user.ts b/common/src/interfaces/user.ts new file mode 100644 index 00000000..66bca296 --- /dev/null +++ b/common/src/interfaces/user.ts @@ -0,0 +1,6 @@ +export interface UserDTO { + id?: string; + username: string; + firstName: string; + lastName: string; +} diff --git a/common/src/util/class-join-request.ts b/common/src/util/class-join-request.ts new file mode 100644 index 00000000..5f9410f0 --- /dev/null +++ b/common/src/util/class-join-request.ts @@ -0,0 +1,5 @@ +export enum ClassJoinRequestStatus { + Open = 'open', + Accepted = 'accepted', + Declined = 'declined', +} diff --git a/backend/src/entities/content/language.ts b/common/src/util/language.ts similarity index 100% rename from backend/src/entities/content/language.ts rename to common/src/util/language.ts diff --git a/common/tsconfig.json b/common/tsconfig.json new file mode 100644 index 00000000..86267d25 --- /dev/null +++ b/common/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + } +} diff --git a/compose.staging.yml b/compose.staging.yml index 8357e773..16814b77 100644 --- a/compose.staging.yml +++ b/compose.staging.yml @@ -24,7 +24,7 @@ services: - '3000:3000/tcp' restart: unless-stopped volumes: - - ./backend/.env:/app/.env + - ./backend/.env.staging:/app/.env depends_on: - db - logging diff --git a/eslint.config.ts b/eslint.config.ts index 61a99fda..fb19e5c4 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -16,8 +16,17 @@ export default [ prettierConfig, includeIgnoreFile(gitignorePath), { - ignores: ['**/dist/**', '**/.node_modules/**', '**/coverage/**', '**/.github/**'], - files: ['**/*.ts', '**/*.cts', '**.*.mts', '**/*.ts'], + ignores: [ + '**/dist/**', + '**/.node_modules/**', + '**/coverage/**', + '**/.github/**', + '**/prettier.config.js', + 'docs/.venv/**', + 'prettier.config.js', + 'frontend/prettier.config.js', + ], + files: ['**/*.ts', '**/*.cts', '**.*.mts'], }, { languageOptions: { diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts index e9359af7..9ca1a08b 100644 --- a/frontend/eslint.config.ts +++ b/frontend/eslint.config.ts @@ -21,7 +21,7 @@ const vueConfig = defineConfigWithVueTs( { name: "app/files-to-ignore", - ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**"], + ignores: ["**/dist/**", "**/dist-ssr/**", "**/coverage/**", "prettier.config.js"], }, pluginVue.configs["flat/essential"], diff --git a/frontend/src/components/BrowseThemes.vue b/frontend/src/components/BrowseThemes.vue index 97ff8352..805d2720 100644 --- a/frontend/src/components/BrowseThemes.vue +++ b/frontend/src/components/BrowseThemes.vue @@ -1,9 +1,10 @@ - - - - diff --git a/frontend/src/components/LearningPathSearchField.vue b/frontend/src/components/LearningPathSearchField.vue new file mode 100644 index 00000000..b8b71960 --- /dev/null +++ b/frontend/src/components/LearningPathSearchField.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/frontend/src/components/LearningPathsGrid.vue b/frontend/src/components/LearningPathsGrid.vue new file mode 100644 index 00000000..865c7166 --- /dev/null +++ b/frontend/src/components/LearningPathsGrid.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/frontend/src/components/UsingQueryResult.vue b/frontend/src/components/UsingQueryResult.vue new file mode 100644 index 00000000..27271184 --- /dev/null +++ b/frontend/src/components/UsingQueryResult.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/frontend/src/controllers/assignments.ts b/frontend/src/controllers/assignments.ts new file mode 100644 index 00000000..5fd5090a --- /dev/null +++ b/frontend/src/controllers/assignments.ts @@ -0,0 +1,5 @@ +import type { AssignmentDTO } from "@dwengo-1/interfaces/assignment"; + +export interface AssignmentsResponse { + assignments: AssignmentDTO[]; +} // TODO ID diff --git a/frontend/src/controllers/base-controller.ts b/frontend/src/controllers/base-controller.ts index 5456c9e8..72d71819 100644 --- a/frontend/src/controllers/base-controller.ts +++ b/frontend/src/controllers/base-controller.ts @@ -1,73 +1,47 @@ -import { apiConfig } from "@/config.ts"; +import apiClient from "@/services/api-client/api-client.ts"; +import type { AxiosResponse, ResponseType } from "axios"; +import { HttpErrorResponseException } from "@/exception/http-error-response-exception.ts"; -export class BaseController { - protected baseUrl: string; +export abstract class BaseController { + protected basePath: string; - constructor(basePath: string) { - this.baseUrl = `${apiConfig.baseUrl}/${basePath}`; + protected constructor(basePath: string) { + this.basePath = basePath; } - protected async get(path: string, queryParams?: Record): Promise { - let url = `${this.baseUrl}${path}`; - if (queryParams) { - const query = new URLSearchParams(); - Object.entries(queryParams).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - query.append(key, value.toString()); - } - }); - url += `?${query.toString()}`; + private static assertSuccessResponse(response: AxiosResponse): void { + if (response.status / 100 !== 2) { + throw new HttpErrorResponseException(response); } + } - const res = await fetch(url); - if (!res.ok) { - const errorData = await res.json().catch(() => ({})); - throw new Error(errorData?.error || `Error ${res.status}: ${res.statusText}`); - } - - return res.json(); + protected async get(path: string, queryParams?: QueryParams, responseType?: ResponseType): Promise { + const response = await apiClient.get(this.absolutePathFor(path), { params: queryParams, responseType }); + BaseController.assertSuccessResponse(response); + return response.data; } protected async post(path: string, body: unknown): Promise { - const res = await fetch(`${this.baseUrl}${path}`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - - if (!res.ok) { - const errorData = await res.json().catch(() => ({})); - throw new Error(errorData?.error || `Error ${res.status}: ${res.statusText}`); - } - - return res.json(); + const response = await apiClient.post(this.absolutePathFor(path), body); + BaseController.assertSuccessResponse(response); + return response.data; } protected async delete(path: string): Promise { - const res = await fetch(`${this.baseUrl}${path}`, { - method: "DELETE", - }); - - if (!res.ok) { - const errorData = await res.json().catch(() => ({})); - throw new Error(errorData?.error || `Error ${res.status}: ${res.statusText}`); - } - - return res.json(); + const response = await apiClient.delete(this.absolutePathFor(path)); + BaseController.assertSuccessResponse(response); + return response.data; } protected async put(path: string, body: unknown): Promise { - const res = await fetch(`${this.baseUrl}${path}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); + const response = await apiClient.put(this.absolutePathFor(path), body); + BaseController.assertSuccessResponse(response); + return response.data; + } - if (!res.ok) { - const errorData = await res.json().catch(() => ({})); - throw new Error(errorData?.error || `Error ${res.status}: ${res.statusText}`); - } - - return res.json(); + private absolutePathFor(path: string): string { + return "/" + this.basePath + path; } } + +type QueryParams = Record; diff --git a/frontend/src/controllers/classes.ts b/frontend/src/controllers/classes.ts new file mode 100644 index 00000000..d2d95ed5 --- /dev/null +++ b/frontend/src/controllers/classes.ts @@ -0,0 +1,5 @@ +import type { ClassDTO } from "@dwengo-1/interfaces/class"; + +export interface ClassesResponse { + classes: ClassDTO[] | string[]; +} diff --git a/frontend/src/controllers/controllers.ts b/frontend/src/controllers/controllers.ts index 639a8f1c..39392a7d 100644 --- a/frontend/src/controllers/controllers.ts +++ b/frontend/src/controllers/controllers.ts @@ -1,4 +1,6 @@ import { ThemeController } from "@/controllers/themes.ts"; +import { LearningObjectController } from "@/controllers/learning-objects.ts"; +import { LearningPathController } from "@/controllers/learning-paths.ts"; export function controllerGetter(factory: new () => T): () => T { let instance: T | undefined; @@ -12,3 +14,5 @@ export function controllerGetter(factory: new () => T): () => T { } export const getThemeController = controllerGetter(ThemeController); +export const getLearningObjectController = controllerGetter(LearningObjectController); +export const getLearningPathController = controllerGetter(LearningPathController); diff --git a/frontend/src/controllers/groups.ts b/frontend/src/controllers/groups.ts new file mode 100644 index 00000000..d6738e04 --- /dev/null +++ b/frontend/src/controllers/groups.ts @@ -0,0 +1,5 @@ +import type { GroupDTO } from "@dwengo-1/interfaces/group"; + +export interface GroupsResponse { + groups: GroupDTO[]; +} // | TODO id diff --git a/frontend/src/controllers/learning-objects.ts b/frontend/src/controllers/learning-objects.ts new file mode 100644 index 00000000..d62ba1f4 --- /dev/null +++ b/frontend/src/controllers/learning-objects.ts @@ -0,0 +1,17 @@ +import { BaseController } from "@/controllers/base-controller.ts"; +import type { Language } from "@/data-objects/language.ts"; +import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; + +export class LearningObjectController extends BaseController { + constructor() { + super("learningObject"); + } + + async getMetadata(hruid: string, language: Language, version: number): Promise { + return this.get(`/${hruid}`, { language, version }); + } + + async getHTML(hruid: string, language: Language, version: number): Promise { + return this.get(`/${hruid}/html`, { language, version }, "document"); + } +} diff --git a/frontend/src/controllers/learning-paths.ts b/frontend/src/controllers/learning-paths.ts new file mode 100644 index 00000000..15967d28 --- /dev/null +++ b/frontend/src/controllers/learning-paths.ts @@ -0,0 +1,32 @@ +import { BaseController } from "@/controllers/base-controller.ts"; +import { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; +import type { Language } from "@/data-objects/language.ts"; +import { single } from "@/utils/response-assertions.ts"; +import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto.ts"; + +export class LearningPathController extends BaseController { + constructor() { + super("learningPath"); + } + async search(query: string): Promise { + const dtos = await this.get("/", { search: query }); + return dtos.map((dto) => LearningPath.fromDTO(dto)); + } + async getBy( + hruid: string, + language: Language, + options?: { forGroup?: string; forStudent?: string }, + ): Promise { + const dtos = await this.get("/", { + hruid, + language, + forGroup: options?.forGroup, + forStudent: options?.forStudent, + }); + return LearningPath.fromDTO(single(dtos)); + } + async getAllByTheme(theme: string): Promise { + const dtos = await this.get("/", { theme }); + return dtos.map((dto) => LearningPath.fromDTO(dto)); + } +} diff --git a/frontend/src/controllers/questions.ts b/frontend/src/controllers/questions.ts new file mode 100644 index 00000000..9b0182de --- /dev/null +++ b/frontend/src/controllers/questions.ts @@ -0,0 +1,5 @@ +import type { QuestionDTO, QuestionId } from "@dwengo-1/interfaces/question"; + +export interface QuestionsResponse { + questions: QuestionDTO[] | QuestionId[]; +} diff --git a/frontend/src/controllers/students.ts b/frontend/src/controllers/students.ts new file mode 100644 index 00000000..b36f1d5a --- /dev/null +++ b/frontend/src/controllers/students.ts @@ -0,0 +1,79 @@ +import { BaseController } from "@/controllers/base-controller.ts"; +import type { ClassesResponse } from "@/controllers/classes.ts"; +import type { AssignmentsResponse } from "@/controllers/assignments.ts"; +import type { GroupsResponse } from "@/controllers/groups.ts"; +import type { SubmissionsResponse } from "@/controllers/submissions.ts"; +import type { QuestionsResponse } from "@/controllers/questions.ts"; +import type { StudentDTO } from "@dwengo-1/interfaces/student"; +import type { ClassJoinRequestDTO } from "@dwengo-1/interfaces/class-join-request"; + +export interface StudentsResponse { + students: StudentDTO[] | string[]; +} +export interface StudentResponse { + student: StudentDTO; +} +export interface JoinRequestsResponse { + requests: ClassJoinRequestDTO[]; +} +export interface JoinRequestResponse { + request: ClassJoinRequestDTO; +} + +export class StudentController extends BaseController { + constructor() { + super("student"); + } + + async getAll(full = true): Promise { + return this.get("/", { full }); + } + + async getByUsername(username: string): Promise { + return this.get(`/${username}`); + } + + async createStudent(data: StudentDTO): Promise { + return this.post("/", data); + } + + async deleteStudent(username: string): Promise { + return this.delete(`/${username}`); + } + + async getClasses(username: string, full = true): Promise { + return this.get(`/${username}/classes`, { full }); + } + + async getAssignments(username: string, full = true): Promise { + return this.get(`/${username}/assignments`, { full }); + } + + async getGroups(username: string, full = true): Promise { + return this.get(`/${username}/groups`, { full }); + } + + async getSubmissions(username: string): Promise { + return this.get(`/${username}/submissions`); + } + + async getQuestions(username: string, full = true): Promise { + return this.get(`/${username}/questions`, { full }); + } + + async getJoinRequests(username: string): Promise { + return this.get(`/${username}/joinRequests`); + } + + async getJoinRequest(username: string, classId: string): Promise { + return this.get(`/${username}/joinRequests/${classId}`); + } + + async createJoinRequest(username: string, classId: string): Promise { + return this.post(`/${username}/joinRequests}`, classId); + } + + async deleteJoinRequest(username: string, classId: string): Promise { + return this.delete(`/${username}/joinRequests/${classId}`); + } +} diff --git a/frontend/src/controllers/submissions.ts b/frontend/src/controllers/submissions.ts new file mode 100644 index 00000000..99b6ba8d --- /dev/null +++ b/frontend/src/controllers/submissions.ts @@ -0,0 +1,5 @@ +import { type SubmissionDTO, SubmissionDTOId } from "@dwengo-1/interfaces/submission"; + +export interface SubmissionsResponse { + submissions: SubmissionDTO[] | SubmissionDTOId[]; +} diff --git a/frontend/src/controllers/teachers.ts b/frontend/src/controllers/teachers.ts new file mode 100644 index 00000000..e0d38a6c --- /dev/null +++ b/frontend/src/controllers/teachers.ts @@ -0,0 +1,64 @@ +import { BaseController } from "@/controllers/base-controller.ts"; +import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts"; +import type { QuestionsResponse } from "@/controllers/questions.ts"; +import type { ClassesResponse } from "@/controllers/classes.ts"; +import type { TeacherDTO } from "@dwengo-1/interfaces/teacher"; + +export interface TeachersResponse { + teachers: TeacherDTO[] | string[]; +} +export interface TeacherResponse { + teacher: TeacherDTO; +} + +export class TeacherController extends BaseController { + constructor() { + super("teacher"); + } + + async getAll(full = false): Promise { + return this.get("/", { full }); + } + + async getByUsername(username: string): Promise { + return this.get(`/${username}`); + } + + async createTeacher(data: TeacherDTO): Promise { + return this.post("/", data); + } + + async deleteTeacher(username: string): Promise { + return this.delete(`/${username}`); + } + + async getClasses(username: string, full = false): Promise { + return this.get(`/${username}/classes`, { full }); + } + + async getStudents(username: string, full = false): Promise { + return this.get(`/${username}/students`, { full }); + } + + async getQuestions(username: string, full = false): Promise { + return this.get(`/${username}/questions`, { full }); + } + + async getStudentJoinRequests(username: string, classId: string): Promise { + return this.get(`/${username}/joinRequests/${classId}`); + } + + async updateStudentJoinRequest( + teacherUsername: string, + classId: string, + studentUsername: string, + accepted: boolean, + ): Promise { + return this.put( + `/${teacherUsername}/joinRequests/${classId}/${studentUsername}`, + accepted, + ); + } + + // GetInvitations(id: string) {return this.get<{ invitations: string[] }>(`/${id}/invitations`);} +} diff --git a/frontend/src/controllers/themes.ts b/frontend/src/controllers/themes.ts index d6c8be98..bb76249d 100644 --- a/frontend/src/controllers/themes.ts +++ b/frontend/src/controllers/themes.ts @@ -1,11 +1,12 @@ import { BaseController } from "@/controllers/base-controller.ts"; +import type { Theme } from "@dwengo-1/interfaces/theme"; export class ThemeController extends BaseController { constructor() { super("theme"); } - async getAll(language: string | null = null): Promise { + async getAll(language: string | null = null): Promise { const query = language ? { language } : undefined; return this.get("/", query); } diff --git a/frontend/src/data-objects/language.ts b/frontend/src/data-objects/language.ts new file mode 100644 index 00000000..cb7b7fd1 --- /dev/null +++ b/frontend/src/data-objects/language.ts @@ -0,0 +1,186 @@ +export enum Language { + Afar = "aa", + Abkhazian = "ab", + Afrikaans = "af", + Akan = "ak", + Albanian = "sq", + Amharic = "am", + Arabic = "ar", + Aragonese = "an", + Armenian = "hy", + Assamese = "as", + Avaric = "av", + Avestan = "ae", + Aymara = "ay", + Azerbaijani = "az", + Bashkir = "ba", + Bambara = "bm", + Basque = "eu", + Belarusian = "be", + Bengali = "bn", + Bihari = "bh", + Bislama = "bi", + Bosnian = "bs", + Breton = "br", + Bulgarian = "bg", + Burmese = "my", + Catalan = "ca", + Chamorro = "ch", + Chechen = "ce", + Chinese = "zh", + ChurchSlavic = "cu", + Chuvash = "cv", + Cornish = "kw", + Corsican = "co", + Cree = "cr", + Czech = "cs", + Danish = "da", + Divehi = "dv", + Dutch = "nl", + Dzongkha = "dz", + English = "en", + Esperanto = "eo", + Estonian = "et", + Ewe = "ee", + Faroese = "fo", + Fijian = "fj", + Finnish = "fi", + French = "fr", + Frisian = "fy", + Fulah = "ff", + Georgian = "ka", + German = "de", + Gaelic = "gd", + Irish = "ga", + Galician = "gl", + Manx = "gv", + Greek = "el", + Guarani = "gn", + Gujarati = "gu", + Haitian = "ht", + Hausa = "ha", + Hebrew = "he", + Herero = "hz", + Hindi = "hi", + HiriMotu = "ho", + Croatian = "hr", + Hungarian = "hu", + Igbo = "ig", + Icelandic = "is", + Ido = "io", + SichuanYi = "ii", + Inuktitut = "iu", + Interlingue = "ie", + Interlingua = "ia", + Indonesian = "id", + Inupiaq = "ik", + Italian = "it", + Javanese = "jv", + Japanese = "ja", + Kalaallisut = "kl", + Kannada = "kn", + Kashmiri = "ks", + Kanuri = "kr", + Kazakh = "kk", + Khmer = "km", + Kikuyu = "ki", + Kinyarwanda = "rw", + Kirghiz = "ky", + Komi = "kv", + Kongo = "kg", + Korean = "ko", + Kuanyama = "kj", + Kurdish = "ku", + Lao = "lo", + Latin = "la", + Latvian = "lv", + Limburgan = "li", + Lingala = "ln", + Lithuanian = "lt", + Luxembourgish = "lb", + LubaKatanga = "lu", + Ganda = "lg", + Macedonian = "mk", + Marshallese = "mh", + Malayalam = "ml", + Maori = "mi", + Marathi = "mr", + Malay = "ms", + Malagasy = "mg", + Maltese = "mt", + Mongolian = "mn", + Nauru = "na", + Navajo = "nv", + SouthNdebele = "nr", + NorthNdebele = "nd", + Ndonga = "ng", + Nepali = "ne", + NorwegianNynorsk = "nn", + NorwegianBokmal = "nb", + Norwegian = "no", + Chichewa = "ny", + Occitan = "oc", + Ojibwa = "oj", + Oriya = "or", + Oromo = "om", + Ossetian = "os", + Punjabi = "pa", + Persian = "fa", + Pali = "pi", + Polish = "pl", + Portuguese = "pt", + Pashto = "ps", + Quechua = "qu", + Romansh = "rm", + Romanian = "ro", + Rundi = "rn", + Russian = "ru", + Sango = "sg", + Sanskrit = "sa", + Sinhala = "si", + Slovak = "sk", + Slovenian = "sl", + NorthernSami = "se", + Samoan = "sm", + Shona = "sn", + Sindhi = "sd", + Somali = "so", + Sotho = "st", + Spanish = "es", + Sardinian = "sc", + Serbian = "sr", + Swati = "ss", + Sundanese = "su", + Swahili = "sw", + Swedish = "sv", + Tahitian = "ty", + Tamil = "ta", + Tatar = "tt", + Telugu = "te", + Tajik = "tg", + Tagalog = "tl", + Thai = "th", + Tibetan = "bo", + Tigrinya = "ti", + Tonga = "to", + Tswana = "tn", + Tsonga = "ts", + Turkmen = "tk", + Turkish = "tr", + Twi = "tw", + Uighur = "ug", + Ukrainian = "uk", + Urdu = "ur", + Uzbek = "uz", + Venda = "ve", + Vietnamese = "vi", + Volapuk = "vo", + Welsh = "cy", + Walloon = "wa", + Wolof = "wo", + Xhosa = "xh", + Yiddish = "yi", + Yoruba = "yo", + Zhuang = "za", + Zulu = "zu", +} diff --git a/frontend/src/data-objects/learning-objects/educational-goal.ts b/frontend/src/data-objects/learning-objects/educational-goal.ts new file mode 100644 index 00000000..05c7a786 --- /dev/null +++ b/frontend/src/data-objects/learning-objects/educational-goal.ts @@ -0,0 +1,4 @@ +export interface EducationalGoal { + source: string; + id: string; +} diff --git a/frontend/src/data-objects/learning-objects/learning-object.ts b/frontend/src/data-objects/learning-objects/learning-object.ts new file mode 100644 index 00000000..1205e8fe --- /dev/null +++ b/frontend/src/data-objects/learning-objects/learning-object.ts @@ -0,0 +1,25 @@ +import type { Language } from "@/data-objects/language.ts"; +import type { ReturnValue } from "@/data-objects/learning-objects/return-value.ts"; +import type { EducationalGoal } from "@/data-objects/learning-objects/educational-goal.ts"; + +export interface LearningObject { + key: string; + _id: string; + uuid: string; + version: number; + title: string; + htmlUrl: string; + language: Language; + difficulty: number; + estimatedTime?: number; + available: boolean; + teacherExclusive: boolean; + educationalGoals: EducationalGoal[]; + keywords: string[]; + description: string; + targetAges: number[]; + contentType: string; + contentLocation?: string; + skosConcepts?: string[]; + returnValue?: ReturnValue; +} diff --git a/frontend/src/data-objects/learning-objects/return-value.ts b/frontend/src/data-objects/learning-objects/return-value.ts new file mode 100644 index 00000000..56b89380 --- /dev/null +++ b/frontend/src/data-objects/learning-objects/return-value.ts @@ -0,0 +1,4 @@ +export interface ReturnValue { + callback_url: string; + callback_schema: Record; +} diff --git a/frontend/src/data-objects/learning-paths/learning-path-dto.ts b/frontend/src/data-objects/learning-paths/learning-path-dto.ts new file mode 100644 index 00000000..60e713aa --- /dev/null +++ b/frontend/src/data-objects/learning-paths/learning-path-dto.ts @@ -0,0 +1,17 @@ +import type { LearningPathNodeDTO } from "@/data-objects/learning-paths/learning-path.ts"; + +export interface LearningPathDTO { + language: string; + hruid: string; + title: string; + description: string; + image?: string; // Image might be missing, so it's optional + num_nodes: number; + num_nodes_left: number; + nodes: LearningPathNodeDTO[]; + keywords: string; + target_ages: number[]; + min_age: number; + max_age: number; + __order: number; +} diff --git a/frontend/src/data-objects/learning-paths/learning-path-node.ts b/frontend/src/data-objects/learning-paths/learning-path-node.ts new file mode 100644 index 00000000..99bac8db --- /dev/null +++ b/frontend/src/data-objects/learning-paths/learning-path-node.ts @@ -0,0 +1,58 @@ +import type { Language } from "@/data-objects/language.ts"; +import type { LearningPathNodeDTO } from "@/data-objects/learning-paths/learning-path.ts"; + +export class LearningPathNode { + public readonly learningobjectHruid: string; + public readonly version: number; + public readonly language: Language; + public readonly transitions: { next: LearningPathNode; default: boolean }[]; + public readonly createdAt: Date; + public readonly updatedAt: Date; + public readonly done: boolean; + + constructor(options: { + learningobjectHruid: string; + version: number; + language: Language; + transitions: { next: LearningPathNode; default: boolean }[]; + createdAt: Date; + updatedAt: Date; + done?: boolean; + }) { + this.learningobjectHruid = options.learningobjectHruid; + this.version = options.version; + this.language = options.language; + this.transitions = options.transitions; + this.createdAt = options.createdAt; + this.updatedAt = options.updatedAt; + this.done = options.done || false; + } + + static fromDTOAndOtherNodes(dto: LearningPathNodeDTO, otherNodes: LearningPathNodeDTO[]): LearningPathNode { + return new LearningPathNode({ + learningobjectHruid: dto.learningobject_hruid, + version: dto.version, + language: dto.language, + transitions: dto.transitions + .map((transDto) => { + const nextNodeDto = otherNodes.find( + (it) => + it.learningobject_hruid === transDto.next.hruid && + it.language === transDto.next.language && + it.version === transDto.next.version, + ); + if (nextNodeDto) { + return { + next: LearningPathNode.fromDTOAndOtherNodes(nextNodeDto, otherNodes), + default: transDto.default, + }; + } + return undefined; + }) + .filter((it) => it !== undefined), + createdAt: new Date(dto.created_at), + updatedAt: new Date(dto.updatedAt), + done: dto.done, + }); + } +} diff --git a/frontend/src/data-objects/learning-paths/learning-path.ts b/frontend/src/data-objects/learning-paths/learning-path.ts new file mode 100644 index 00000000..d764d123 --- /dev/null +++ b/frontend/src/data-objects/learning-paths/learning-path.ts @@ -0,0 +1,97 @@ +import type { Language } from "@/data-objects/language.ts"; +import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts"; +import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto.ts"; + +export interface LearningPathNodeDTO { + _id: string; + learningobject_hruid: string; + version: number; + language: Language; + start_node?: boolean; + transitions: LearningPathTransitionDTO[]; + created_at: string; + updatedAt: string; + done?: boolean; // True if a submission exists for this node by the user for whom the learning path is customized. +} + +export interface LearningPathTransitionDTO { + default: boolean; + _id: string; + next: { + _id: string; + hruid: string; + version: number; + language: string; + }; +} + +export class LearningPath { + public readonly language: string; + public readonly hruid: string; + public readonly title: string; + public readonly description: string; + public readonly amountOfNodes: number; + public readonly amountOfNodesLeft: number; + public readonly keywords: string[]; + public readonly targetAges: { min: number; max: number }; + public readonly startNode: LearningPathNode; + public readonly image?: string; // Image might be missing, so it's optional + + constructor(options: { + language: string; + hruid: string; + title: string; + description: string; + amountOfNodes: number; + amountOfNodesLeft: number; + keywords: string[]; + targetAges: { min: number; max: number }; + startNode: LearningPathNode; + image?: string; // Image might be missing, so it's optional + }) { + this.language = options.language; + this.hruid = options.hruid; + this.title = options.title; + this.description = options.description; + this.amountOfNodes = options.amountOfNodes; + this.amountOfNodesLeft = options.amountOfNodesLeft; + this.keywords = options.keywords; + this.targetAges = options.targetAges; + this.startNode = options.startNode; + this.image = options.image; + } + + public get nodesAsList(): LearningPathNode[] { + const list: LearningPathNode[] = []; + let currentNode = this.startNode; + while (currentNode) { + list.push(currentNode); + currentNode = currentNode.transitions.find((it) => it.default)?.next || currentNode.transitions[0]?.next; + } + return list; + } + + static fromDTO(dto: LearningPathDTO): LearningPath { + return new LearningPath({ + language: dto.language, + hruid: dto.hruid, + title: dto.title, + description: dto.description, + amountOfNodes: dto.num_nodes, + amountOfNodesLeft: dto.num_nodes_left, + keywords: dto.keywords.split(" "), + targetAges: { min: dto.min_age, max: dto.max_age }, + startNode: LearningPathNode.fromDTOAndOtherNodes(LearningPath.getStartNode(dto), dto.nodes), + image: dto.image, + }); + } + + static getStartNode(dto: LearningPathDTO): LearningPathNodeDTO { + const startNodeDtos = dto.nodes.filter((it) => it.start_node === true); + if (startNodeDtos.length < 1) { + // The learning path has no starting node -> use the first node. + return dto.nodes[0]; + } // The learning path has 1 or more starting nodes -> use the first start node. + return startNodeDtos[0]; + } +} diff --git a/frontend/src/data-objects/theme.ts b/frontend/src/data-objects/theme.ts new file mode 100644 index 00000000..c54f1cec --- /dev/null +++ b/frontend/src/data-objects/theme.ts @@ -0,0 +1,8 @@ +export interface Theme { + key: string; + title: string; + description: string; + + // URL of the image + image: string; +} diff --git a/frontend/src/exception/http-error-response-exception.ts b/frontend/src/exception/http-error-response-exception.ts new file mode 100644 index 00000000..c519f4fd --- /dev/null +++ b/frontend/src/exception/http-error-response-exception.ts @@ -0,0 +1,9 @@ +import type { AxiosResponse } from "axios"; + +export class HttpErrorResponseException extends Error { + public statusCode: number; + constructor(public response: AxiosResponse) { + super((response.data as { message: string })?.message || JSON.stringify(response.data)); + this.statusCode = response.status; + } +} diff --git a/frontend/src/exception/invalid-response-exception.ts b/frontend/src/exception/invalid-response-exception.ts new file mode 100644 index 00000000..5cbd35b3 --- /dev/null +++ b/frontend/src/exception/invalid-response-exception.ts @@ -0,0 +1,5 @@ +export class InvalidResponseException extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/frontend/src/exception/not-found-exception.ts b/frontend/src/exception/not-found-exception.ts new file mode 100644 index 00000000..fd5b29a6 --- /dev/null +++ b/frontend/src/exception/not-found-exception.ts @@ -0,0 +1,5 @@ +export class NotFoundException extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/frontend/src/i18n/locale/de.json b/frontend/src/i18n/locale/de.json index 7a20e3ce..7e72d334 100644 --- a/frontend/src/i18n/locale/de.json +++ b/frontend/src/i18n/locale/de.json @@ -44,5 +44,17 @@ "high-school": "16-18 jahre alt", "older": "18 und älter" }, - "read-more": "Mehr lesen" + "read-more": "Mehr lesen", + "error_title": "Fehler", + "previous": "Zurück", + "next": "Weiter", + "search": "Suchen...", + "yearsAge": "Jahre", + "enterSearchTerm": "Lernpfade suchen", + "enterSearchTermDescription": "Bitte geben Sie einen Suchbegriff ein.", + "noLearningPathsFound": "Nichts gefunden!", + "noLearningPathsFoundDescription": "Es gibt keine Lernpfade, die zu Ihrem Suchbegriff passen.", + "legendNotCompletedYet": "Noch nicht fertig", + "legendCompleted": "Fertig", + "legendTeacherExclusive": "Information für Lehrkräfte" } diff --git a/frontend/src/i18n/locale/en.json b/frontend/src/i18n/locale/en.json index 92c8275a..4fe9dd83 100644 --- a/frontend/src/i18n/locale/en.json +++ b/frontend/src/i18n/locale/en.json @@ -2,10 +2,22 @@ "welcome": "Welcome", "student": "student", "teacher": "teacher", - "assignments": "Assignments", - "classes": "Classes", - "discussions": "Discussions", + "assignments": "assignments", + "classes": "classes", + "discussions": "discussions", "logout": "log out", + "error_title": "Error", + "previous": "Previous", + "next": "Next", + "search": "Search...", + "yearsAge": "years", + "enterSearchTerm": "Search learning paths", + "enterSearchTermDescription": "Please enter a search term.", + "noLearningPathsFound": "Nothing found!", + "noLearningPathsFoundDescription": "There are no learning paths matching your search term.", + "legendNotCompletedYet": "Not completed yet", + "legendCompleted": "Completed", + "legendTeacherExclusive": "Information for teachers", "cancel": "cancel", "logoutVerification": "Are you sure you want to log out?", "homeTitle": "Our strengths", diff --git a/frontend/src/i18n/locale/fr.json b/frontend/src/i18n/locale/fr.json index 47a0f548..744d222d 100644 --- a/frontend/src/i18n/locale/fr.json +++ b/frontend/src/i18n/locale/fr.json @@ -1,5 +1,17 @@ { "welcome": "Bienvenue", + "error_title": "Erreur", + "previous": "Précédente", + "next": "Suivante", + "search": "Réchercher...", + "yearsAge": "ans", + "enterSearchTerm": "Rechercher des parcours d'apprentissage", + "enterSearchTermDescription": "Saisissez un terme de recherche pour commencer.", + "noLearningPathsFound": "Rien trouvé !", + "noLearningPathsFoundDescription": "Aucun parcours d'apprentissage ne correspond à votre recherche.", + "legendNotCompletedYet": "Pas encore achevé", + "legendCompleted": "Achevé", + "legendTeacherExclusive": "Informations pour les enseignants", "student": "élève", "teacher": "enseignant", "assignments": "Travails", diff --git a/frontend/src/i18n/locale/nl.json b/frontend/src/i18n/locale/nl.json index f5e3d771..c6ab7576 100644 --- a/frontend/src/i18n/locale/nl.json +++ b/frontend/src/i18n/locale/nl.json @@ -2,10 +2,22 @@ "welcome": "Welkom", "student": "leerling", "teacher": "leerkracht", - "assignments": "Opdrachten", - "classes": "Klassen", - "discussions": "Discussies", + "assignments": "opdrachten", + "classes": "klassen", + "discussions": "discussies", "logout": "log uit", + "error_title": "Fout", + "previous": "Vorige", + "next": "Volgende", + "search": "Zoeken...", + "yearsAge": "jaar", + "enterSearchTerm": "Zoek naar leerpaden", + "enterSearchTermDescription": "Gelieve een zoekterm in te voeren.", + "noLearningPathsFound": "Niets gevonden!", + "noLearningPathsFoundDescription": "Er zijn geen leerpaden die overeenkomen met je zoekterm.", + "legendNotCompletedYet": "Nog niet afgewerkt", + "legendCompleted": "Afgewerkt", + "legendTeacherExclusive": "Informatie voor leerkrachten", "cancel": "annuleren", "logoutVerification": "Bent u zeker dat u wilt uitloggen?", "homeTitle": "Onze sterke punten", diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 5945a2ab..b5315634 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -10,6 +10,7 @@ import i18n from "./i18n/i18n.ts"; // Components import App from "./App.vue"; import router from "./router"; +import { aliases, mdi } from "vuetify/iconsets/mdi"; import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query"; const app = createApp(App); @@ -24,6 +25,13 @@ document.head.appendChild(link); const vuetify = createVuetify({ components, directives, + icons: { + defaultSet: "mdi", + aliases, + sets: { + mdi, + }, + }, }); const queryClient = new QueryClient({ diff --git a/frontend/src/queries/learning-objects.ts b/frontend/src/queries/learning-objects.ts new file mode 100644 index 00000000..3ff801b4 --- /dev/null +++ b/frontend/src/queries/learning-objects.ts @@ -0,0 +1,57 @@ +import { type MaybeRefOrGetter, toValue } from "vue"; +import type { Language } from "@/data-objects/language.ts"; +import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { getLearningObjectController } from "@/controllers/controllers.ts"; +import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; +import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; + +const LEARNING_OBJECT_KEY = "learningObject"; +const learningObjectController = getLearningObjectController(); + +export function useLearningObjectMetadataQuery( + hruid: MaybeRefOrGetter, + language: MaybeRefOrGetter, + version: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: [LEARNING_OBJECT_KEY, "metadata", hruid, language, version], + queryFn: async () => { + const [hruidVal, languageVal, versionVal] = [toValue(hruid), toValue(language), toValue(version)]; + return learningObjectController.getMetadata(hruidVal, languageVal, versionVal); + }, + enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)) && Boolean(toValue(version)), + }); +} + +export function useLearningObjectHTMLQuery( + hruid: MaybeRefOrGetter, + language: MaybeRefOrGetter, + version: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: [LEARNING_OBJECT_KEY, "html", hruid, language, version], + queryFn: async () => { + const [hruidVal, languageVal, versionVal] = [toValue(hruid), toValue(language), toValue(version)]; + return learningObjectController.getHTML(hruidVal, languageVal, versionVal); + }, + enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)) && Boolean(toValue(version)), + }); +} + +export function useLearningObjectListForPathQuery( + learningPath: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: [LEARNING_OBJECT_KEY, "onPath", learningPath], + queryFn: async () => { + const learningObjects: Promise[] = []; + for (const node of toValue(learningPath)!.nodesAsList) { + learningObjects.push( + learningObjectController.getMetadata(node.learningobjectHruid, node.language, node.version), + ); + } + return Promise.all(learningObjects); + }, + enabled: () => Boolean(toValue(learningPath)), + }); +} diff --git a/frontend/src/queries/learning-paths.ts b/frontend/src/queries/learning-paths.ts new file mode 100644 index 00000000..3d8e6fcf --- /dev/null +++ b/frontend/src/queries/learning-paths.ts @@ -0,0 +1,46 @@ +import { type MaybeRefOrGetter, toValue } from "vue"; +import type { Language } from "@/data-objects/language.ts"; +import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { getLearningPathController } from "@/controllers/controllers"; +import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; + +const LEARNING_PATH_KEY = "learningPath"; +const learningPathController = getLearningPathController(); + +export function useGetLearningPathQuery( + hruid: MaybeRefOrGetter, + language: MaybeRefOrGetter, + options?: MaybeRefOrGetter<{ forGroup?: string; forStudent?: string }>, +): UseQueryReturnType { + return useQuery({ + queryKey: [LEARNING_PATH_KEY, "get", hruid, language, options], + queryFn: async () => { + const [hruidVal, languageVal, optionsVal] = [toValue(hruid), toValue(language), toValue(options)]; + return learningPathController.getBy(hruidVal, languageVal, optionsVal); + }, + enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)), + }); +} + +export function useGetAllLearningPathsByThemeQuery( + theme: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme], + queryFn: async () => learningPathController.getAllByTheme(toValue(theme)), + enabled: () => Boolean(toValue(theme)), + }); +} + +export function useSearchLearningPathQuery( + query: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: [LEARNING_PATH_KEY, "search", query], + queryFn: async () => { + const queryVal = toValue(query)!; + return learningPathController.search(queryVal); + }, + enabled: () => Boolean(toValue(query)), + }); +} diff --git a/frontend/src/queries/students.ts b/frontend/src/queries/students.ts new file mode 100644 index 00000000..822083d9 --- /dev/null +++ b/frontend/src/queries/students.ts @@ -0,0 +1,205 @@ +import { computed, toValue } from "vue"; +import type { MaybeRefOrGetter } from "vue"; +import { + useMutation, + type UseMutationReturnType, + useQuery, + useQueryClient, + type UseQueryReturnType, +} from "@tanstack/vue-query"; +import { + type JoinRequestResponse, + type JoinRequestsResponse, + StudentController, + type StudentResponse, + type StudentsResponse, +} from "@/controllers/students.ts"; +import type { ClassesResponse } from "@/controllers/classes.ts"; +import type { AssignmentsResponse } from "@/controllers/assignments.ts"; +import type { GroupsResponse } from "@/controllers/groups.ts"; +import type { SubmissionsResponse } from "@/controllers/submissions.ts"; +import type { QuestionsResponse } from "@/controllers/questions.ts"; +import type { StudentDTO } from "@dwengo-1/interfaces/student"; + +const studentController = new StudentController(); + +/** 🔑 Query keys */ +function studentsQueryKey(full: boolean): [string, boolean] { + return ["students", full]; +} +function studentQueryKey(username: string): [string, string] { + return ["student", username]; +} +function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] { + return ["student-classes", username, full]; +} +function studentAssignmentsQueryKey(username: string, full: boolean): [string, string, boolean] { + return ["student-assignments", username, full]; +} +function studentGroupsQueryKeys(username: string, full: boolean): [string, string, boolean] { + return ["student-groups", username, full]; +} +function studentSubmissionsQueryKey(username: string): [string, string] { + return ["student-submissions", username]; +} +function studentQuestionsQueryKey(username: string, full: boolean): [string, string, boolean] { + return ["student-questions", username, full]; +} +export function studentJoinRequestsQueryKey(username: string): [string, string] { + return ["student-join-requests", username]; +} +export function studentJoinRequestQueryKey(username: string, classId: string): [string, string, string] { + return ["student-join-request", username, classId]; +} + +export function useStudentsQuery(full: MaybeRefOrGetter = true): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentsQueryKey(toValue(full))), + queryFn: async () => studentController.getAll(toValue(full)), + }); +} + +export function useStudentQuery( + username: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentQueryKey(toValue(username)!)), + queryFn: async () => studentController.getByUsername(toValue(username)!), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useStudentClassesQuery( + username: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentClassesQueryKey(toValue(username)!, toValue(full))), + queryFn: async () => studentController.getClasses(toValue(username)!, toValue(full)), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useStudentAssignmentsQuery( + username: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentAssignmentsQueryKey(toValue(username)!, toValue(full))), + queryFn: async () => studentController.getAssignments(toValue(username)!, toValue(full)), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useStudentGroupsQuery( + username: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentGroupsQueryKeys(toValue(username)!, toValue(full))), + queryFn: async () => studentController.getGroups(toValue(username)!, toValue(full)), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useStudentSubmissionsQuery( + username: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentSubmissionsQueryKey(toValue(username)!)), + queryFn: async () => studentController.getSubmissions(toValue(username)!), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useStudentQuestionsQuery( + username: MaybeRefOrGetter, + full: MaybeRefOrGetter = true, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentQuestionsQueryKey(toValue(username)!, toValue(full))), + queryFn: async () => studentController.getQuestions(toValue(username)!, toValue(full)), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useStudentJoinRequestsQuery( + username: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentJoinRequestsQueryKey(toValue(username)!)), + queryFn: async () => studentController.getJoinRequests(toValue(username)!), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useStudentJoinRequestQuery( + username: MaybeRefOrGetter, + classId: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => studentJoinRequestQueryKey(toValue(username)!, toValue(classId)!)), + queryFn: async () => studentController.getJoinRequest(toValue(username)!, toValue(classId)!), + enabled: () => Boolean(toValue(username)) && Boolean(toValue(classId)), + }); +} + +export function useCreateStudentMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data) => studentController.createStudent(data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["students"] }); + }, + }); +} + +export function useDeleteStudentMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (username) => studentController.deleteStudent(username), + onSuccess: async (deletedUser) => { + await queryClient.invalidateQueries({ queryKey: ["students"] }); + await queryClient.invalidateQueries({ queryKey: studentQueryKey(deletedUser.student.username) }); + }, + }); +} + +export function useCreateJoinRequestMutation(): UseMutationReturnType< + JoinRequestResponse, + Error, + { username: string; classId: string }, + unknown +> { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ username, classId }) => studentController.createJoinRequest(username, classId), + onSuccess: async (newJoinRequest) => { + await queryClient.invalidateQueries({ + queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester), + }); + }, + }); +} + +export function useDeleteJoinRequestMutation(): UseMutationReturnType< + JoinRequestResponse, + Error, + { username: string; classId: string }, + unknown +> { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ username, classId }) => studentController.deleteJoinRequest(username, classId), + onSuccess: async (deletedJoinRequest) => { + const username = deletedJoinRequest.request.requester; + const classId = deletedJoinRequest.request.class; + await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) }); + await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) }); + }, + }); +} diff --git a/frontend/src/queries/teachers.ts b/frontend/src/queries/teachers.ts new file mode 100644 index 00000000..778b2cba --- /dev/null +++ b/frontend/src/queries/teachers.ts @@ -0,0 +1,136 @@ +import { computed, toValue } from "vue"; +import type { MaybeRefOrGetter } from "vue"; +import { useMutation, useQuery, useQueryClient, UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query"; +import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts"; +import type { ClassesResponse } from "@/controllers/classes.ts"; +import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts"; +import type { QuestionsResponse } from "@/controllers/questions.ts"; +import type { TeacherDTO } from "@dwengo-1/interfaces/teacher"; +import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts"; + +const teacherController = new TeacherController(); + +/** 🔑 Query keys */ +function teachersQueryKey(full: boolean): [string, boolean] { + return ["teachers", full]; +} + +function teacherQueryKey(username: string): [string, string] { + return ["teacher", username]; +} + +function teacherClassesQueryKey(username: string, full: boolean): [string, string, boolean] { + return ["teacher-classes", username, full]; +} + +function teacherStudentsQueryKey(username: string, full: boolean): [string, string, boolean] { + return ["teacher-students", username, full]; +} + +function teacherQuestionsQueryKey(username: string, full: boolean): [string, string, boolean] { + return ["teacher-questions", username, full]; +} + +export function useTeachersQuery(full: MaybeRefOrGetter = false): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => teachersQueryKey(toValue(full))), + queryFn: async () => teacherController.getAll(toValue(full)), + }); +} + +export function useTeacherQuery( + username: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => teacherQueryKey(toValue(username)!)), + queryFn: async () => teacherController.getByUsername(toValue(username)!), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useTeacherClassesQuery( + username: MaybeRefOrGetter, + full: MaybeRefOrGetter = false, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => teacherClassesQueryKey(toValue(username)!, toValue(full))), + queryFn: async () => teacherController.getClasses(toValue(username)!, toValue(full)), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useTeacherStudentsQuery( + username: MaybeRefOrGetter, + full: MaybeRefOrGetter = false, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => teacherStudentsQueryKey(toValue(username)!, toValue(full))), + queryFn: async () => teacherController.getStudents(toValue(username)!, toValue(full)), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useTeacherQuestionsQuery( + username: MaybeRefOrGetter, + full: MaybeRefOrGetter = false, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => teacherQuestionsQueryKey(toValue(username)!, toValue(full))), + queryFn: async () => teacherController.getQuestions(toValue(username)!, toValue(full)), + enabled: () => Boolean(toValue(username)), + }); +} + +export function useTeacherJoinRequestsQuery( + username: MaybeRefOrGetter, + classId: MaybeRefOrGetter, +): UseQueryReturnType { + return useQuery({ + queryKey: computed(() => JOIN_REQUESTS_QUERY_KEY(toValue(username)!, toValue(classId)!)), + queryFn: async () => teacherController.getStudentJoinRequests(toValue(username)!, toValue(classId)!), + enabled: () => Boolean(toValue(username)) && Boolean(toValue(classId)), + }); +} + +export function useCreateTeacherMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: TeacherDTO) => teacherController.createTeacher(data), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["teachers"] }); + }, + }); +} + +export function useDeleteTeacherMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (username: string) => teacherController.deleteTeacher(username), + onSuccess: async (deletedTeacher) => { + await queryClient.invalidateQueries({ queryKey: ["teachers"] }); + await queryClient.invalidateQueries({ queryKey: teacherQueryKey(deletedTeacher.teacher.username) }); + }, + }); +} + +export function useUpdateJoinRequestMutation(): UseMutationReturnType< + JoinRequestResponse, + Error, + { teacherUsername: string; classId: string; studentUsername: string; accepted: boolean }, + unknown +> { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ teacherUsername, classId, studentUsername, accepted }) => + teacherController.updateStudentJoinRequest(teacherUsername, classId, studentUsername, accepted), + onSuccess: async (deletedJoinRequest) => { + const username = deletedJoinRequest.request.requester; + const classId = deletedJoinRequest.request.class; + await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) }); + await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) }); + }, + }); +} diff --git a/frontend/src/queries/themes.ts b/frontend/src/queries/themes.ts index a8c2a87c..c3be25ae 100644 --- a/frontend/src/queries/themes.ts +++ b/frontend/src/queries/themes.ts @@ -1,10 +1,11 @@ import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; -import { getThemeController } from "@/controllers/controllers"; import { type MaybeRefOrGetter, toValue } from "vue"; +import type { Theme } from "@dwengo-1/interfaces/theme"; +import { getThemeController } from "@/controllers/controllers.ts"; const themeController = getThemeController(); -export function useThemeQuery(language: MaybeRefOrGetter): UseQueryReturnType { +export function useThemeQuery(language: MaybeRefOrGetter): UseQueryReturnType { return useQuery({ queryKey: ["themes", language], queryFn: async () => { @@ -15,10 +16,12 @@ export function useThemeQuery(language: MaybeRefOrGetter): UseQueryRetur }); } -export function useThemeHruidsQuery(themeKey: string | null): UseQueryReturnType { +export function useThemeHruidsQuery( + themeKey: MaybeRefOrGetter, +): UseQueryReturnType { return useQuery({ queryKey: ["theme-hruids", themeKey], - queryFn: async () => themeController.getHruidsByKey(themeKey!), + queryFn: async () => themeController.getHruidsByKey(toValue(themeKey)!), enabled: Boolean(themeKey), }); } diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index dc9cd03c..23695680 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -11,8 +11,11 @@ import UserDiscussions from "@/views/discussions/UserDiscussions.vue"; import UserClasses from "@/views/classes/UserClasses.vue"; import UserAssignments from "@/views/classes/UserAssignments.vue"; import authState from "@/services/auth/auth-service.ts"; +import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue"; +import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue"; import UserHomePage from "@/views/homepage/UserHomePage.vue"; import SingleTheme from "@/views/SingleTheme.vue"; +import LearningObjectView from "@/views/learning-paths/LearningObjectView.vue"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -63,9 +66,10 @@ const router = createRouter({ }, { - path: "/theme/:id", + path: "/theme/:theme", name: "Theme", component: SingleTheme, + props: true, meta: { requiresAuth: true }, }, { @@ -104,6 +108,31 @@ const router = createRouter({ component: SingleDiscussion, meta: { requiresAuth: true }, }, + { + path: "/learningPath", + children: [ + { + path: "search", + name: "LearningPathSearchPage", + component: LearningPathSearchPage, + meta: { requiresAuth: true }, + }, + { + path: ":hruid/:language/:learningObjectHruid", + name: "LearningPath", + component: LearningPathPage, + props: true, + meta: { requiresAuth: true }, + }, + ], + }, + { + path: "/learningObject/:hruid/:language/:version/raw", + name: "LearningObjectView", + component: LearningObjectView, + props: true, + meta: { requiresAuth: true }, + }, { path: "/:catchAll(.*)", name: "NotFound", diff --git a/frontend/src/services/api-client.ts b/frontend/src/services/api-client/api-client.ts similarity index 100% rename from frontend/src/services/api-client.ts rename to frontend/src/services/api-client/api-client.ts diff --git a/frontend/src/services/auth/auth-config-loader.ts b/frontend/src/services/auth/auth-config-loader.ts index 1d1b46af..83dc7608 100644 --- a/frontend/src/services/auth/auth-config-loader.ts +++ b/frontend/src/services/auth/auth-config-loader.ts @@ -1,4 +1,4 @@ -import apiClient from "@/services/api-client.ts"; +import apiClient from "@/services/api-client/api-client.ts"; import type { FrontendAuthConfig } from "@/services/auth/auth.d.ts"; import type { UserManagerSettings } from "oidc-client-ts"; diff --git a/frontend/src/services/auth/auth-service.ts b/frontend/src/services/auth/auth-service.ts index f038f545..977e1dbf 100644 --- a/frontend/src/services/auth/auth-service.ts +++ b/frontend/src/services/auth/auth-service.ts @@ -8,7 +8,7 @@ import { User, UserManager } from "oidc-client-ts"; import { AUTH_CONFIG_ENDPOINT, loadAuthConfig } from "@/services/auth/auth-config-loader.ts"; import authStorage from "./auth-storage.ts"; import { loginRoute } from "@/config.ts"; -import apiClient from "@/services/api-client.ts"; +import apiClient from "@/services/api-client/api-client.ts"; import router from "@/router"; import type { AxiosError } from "axios"; diff --git a/frontend/src/utils/response-assertions.ts b/frontend/src/utils/response-assertions.ts new file mode 100644 index 00000000..1de05eb7 --- /dev/null +++ b/frontend/src/utils/response-assertions.ts @@ -0,0 +1,14 @@ +import { NotFoundException } from "@/exception/not-found-exception.ts"; +import { InvalidResponseException } from "@/exception/invalid-response-exception.ts"; + +export function single(list: T[]): T { + if (list.length === 1) { + return list[0]; + } else if (list.length === 0) { + throw new NotFoundException("Expected list with exactly one element, but got an empty list."); + } else { + throw new InvalidResponseException( + `Expected list with exactly one element, but got one with ${list.length} elements.`, + ); + } +} diff --git a/frontend/src/views/CallbackPage.vue b/frontend/src/views/CallbackPage.vue index 45a0134f..06f814e8 100644 --- a/frontend/src/views/CallbackPage.vue +++ b/frontend/src/views/CallbackPage.vue @@ -1,22 +1,25 @@ diff --git a/frontend/src/views/SingleTheme.vue b/frontend/src/views/SingleTheme.vue index 1a35a59f..6924cc1c 100644 --- a/frontend/src/views/SingleTheme.vue +++ b/frontend/src/views/SingleTheme.vue @@ -1,7 +1,68 @@ - + - + diff --git a/frontend/src/views/learning-paths/LearningObjectView.vue b/frontend/src/views/learning-paths/LearningObjectView.vue new file mode 100644 index 00000000..25fd5672 --- /dev/null +++ b/frontend/src/views/learning-paths/LearningObjectView.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/frontend/src/views/learning-paths/LearningPathPage.vue b/frontend/src/views/learning-paths/LearningPathPage.vue new file mode 100644 index 00000000..2a86d08d --- /dev/null +++ b/frontend/src/views/learning-paths/LearningPathPage.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/frontend/src/views/learning-paths/LearningPathSearchPage.vue b/frontend/src/views/learning-paths/LearningPathSearchPage.vue new file mode 100644 index 00000000..44bc0306 --- /dev/null +++ b/frontend/src/views/learning-paths/LearningPathSearchPage.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index e0d34b13..baf5577b 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -12,6 +12,7 @@ } ], "compilerOptions": { + "composite": true, "resolveJsonModule": true } } diff --git a/package-lock.json b/package-lock.json index a38ca88a..27d261cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { - "name": "dwengo-1-monorepo", + "name": "dwengo-1", "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "dwengo-1-monorepo", + "name": "dwengo-1", "version": "0.1.1", "license": "MIT", "workspaces": [ "backend", + "common", "frontend", "docs" ], @@ -27,7 +28,7 @@ } }, "backend": { - "name": "dwengo-1-backend", + "name": "@dwengo-1/backend", "version": "0.1.1", "dependencies": { "@mikro-orm/core": "6.4.9", @@ -40,6 +41,7 @@ "cross": "^1.0.0", "cross-env": "^7.0.3", "dotenv": "^16.4.7", + "dwengo-1-common": "^0.1.1", "express": "^5.0.1", "express-jwt": "^8.5.1", "gift-pegjs": "^1.0.2", @@ -83,6 +85,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "common": { + "name": "@dwengo-1/common", + "version": "0.1.1" + }, "docs": { "name": "dwengo-1-docs", "version": "0.0.1", @@ -843,6 +849,14 @@ "kuler": "^2.0.0" } }, + "node_modules/@dwengo-1/backend": { + "resolved": "backend", + "link": true + }, + "node_modules/@dwengo-1/common": { + "resolved": "common", + "link": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", @@ -4915,8 +4929,8 @@ "node": ">= 0.4" } }, - "node_modules/dwengo-1-backend": { - "resolved": "backend", + "node_modules/dwengo-1-common": { + "resolved": "common", "link": true }, "node_modules/dwengo-1-docs": { diff --git a/package.json b/package.json index e23a49f6..64cfd665 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,23 @@ { - "name": "dwengo-1-monorepo", + "name": "dwengo-1", "version": "0.1.1", "description": "Monorepo for Dwengo-1", "private": true, "type": "module", "scripts": { - "build": "npm run build --workspace=backend --workspace=frontend", - "format": "npm run format --workspace=backend --workspace=frontend", - "format-check": "npm run format-check --workspace=backend --workspace=frontend", - "lint": "npm run lint --workspace=backend --workspace=frontend", + "prebuild": "npm run clean", + "build": "tsc --build tsconfig.build.json", + "clean": "tsc --build tsconfig.build.json --clean", + "watch": "tsc --build tsconfig.build.json --watch", + "format": "npm run format --workspace=backend --workspace=common --workspace=frontend", + "format-check": "npm run format-check --workspace=backend --workspace=common --workspace=frontend", + "lint": "npm run lint --workspace=backend --workspace=common --workspace=frontend", + "pretest:unit": "npm run build", "test:unit": "npm run test:unit --workspace=backend --workspace=frontend" }, "workspaces": [ "backend", + "common", "frontend", "docs" ], diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..e283237e --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./common" + }, + { + "path": "./backend" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index b41449cf..1da18198 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + "composite": true /* Enable constraints that allow a TypeScript project to be used with project references. */, // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ @@ -25,7 +25,7 @@ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - "moduleDetection": "force", + // "moduleDetection": "force", /* Control what method is used to detect module-format JS files. */ /* Modules */ @@ -33,7 +33,7 @@ /* Specify what module code is generated. */ // "rootDir": "./src", /* Specify the root folder within your source files. */ - "moduleResolution": "node", + "moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -53,16 +53,16 @@ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ - "allowJs": true, + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + "declarationMap": true /* Create sourcemaps for d.ts files. */, // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */