Merge branch 'dev' into feat/leerpad-vragen
This commit is contained in:
commit
8240059c2c
60 changed files with 10729 additions and 1042 deletions
20
.github/workflows/backend-testing.yml
vendored
20
.github/workflows/backend-testing.yml
vendored
|
@ -29,6 +29,12 @@ jobs:
|
|||
if: '! github.event.pull_request.draft'
|
||||
runs-on: [self-hosted, Linux, X64]
|
||||
|
||||
permissions:
|
||||
# Required to checkout the code
|
||||
contents: read
|
||||
# Required to put a comment into the pull-request
|
||||
pull-requests: write
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
|
@ -42,4 +48,16 @@ jobs:
|
|||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- run: npm run test:unit -w backend
|
||||
- run: npm run build
|
||||
- run: npm run test:coverage -w backend
|
||||
- name: 'Report Backend Coverage'
|
||||
# Set if: always() to also generate the report if tests are failing
|
||||
# Only works if you set `reportOnFailure: true` in your vite config as specified above
|
||||
if: always()
|
||||
uses: davelosert/vitest-coverage-report-action@v2
|
||||
with:
|
||||
name: 'Backend'
|
||||
json-summary-path: './backend/coverage/coverage-summary.json'
|
||||
json-final-path: './backend/coverage/coverage-final.json'
|
||||
vite-config-path: './backend/vitest.config.ts'
|
||||
file-coverage-mode: all
|
||||
|
|
20
.github/workflows/frontend-testing.yml
vendored
20
.github/workflows/frontend-testing.yml
vendored
|
@ -38,6 +38,12 @@ jobs:
|
|||
if: '! github.event.pull_request.draft'
|
||||
runs-on: [self-hosted, Linux, X64]
|
||||
|
||||
permissions:
|
||||
# Required to checkout the code
|
||||
contents: read
|
||||
# Required to put a comment into the pull-request
|
||||
pull-requests: write
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.x]
|
||||
|
@ -51,4 +57,16 @@ jobs:
|
|||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- run: npm run test:unit -w frontend
|
||||
- run: npm run build
|
||||
- run: npm run test:coverage -w frontend
|
||||
- name: 'Report Frontend Coverage'
|
||||
# Set if: always() to also generate the report if tests are failing
|
||||
# Only works if you set `reportOnFailure: true` in your vite config as specified above
|
||||
if: always()
|
||||
uses: davelosert/vitest-coverage-report-action@v2
|
||||
with:
|
||||
name: 'Frontend'
|
||||
json-summary-path: './frontend/coverage/coverage-summary.json'
|
||||
json-final-path: './frontend/coverage/coverage-final.json'
|
||||
vite-config-path: './frontend/vitest.config.ts'
|
||||
file-coverage-mode: all
|
||||
|
|
|
@ -13,8 +13,10 @@ DWENGO_DB_NAME=":memory:"
|
|||
DWENGO_DB_UPDATE=true
|
||||
|
||||
DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student
|
||||
DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo
|
||||
DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs
|
||||
DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
|
||||
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
|
||||
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
|
||||
|
||||
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
|
||||
DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:9876,*
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
"format-check": "prettier --check src/",
|
||||
"lint": "eslint . --fix",
|
||||
"pretest:unit": "tsx ../docs/api/generate.ts && npm run build",
|
||||
"test:unit": "vitest --run"
|
||||
"test:unit": "vitest --run",
|
||||
"test:coverage": "vitest --run --coverage.enabled true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mikro-orm/core": "6.4.12",
|
||||
|
@ -27,7 +28,6 @@
|
|||
"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",
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
deleteAssignment,
|
||||
getAllAssignments,
|
||||
getAssignment,
|
||||
getAssignmentsQuestions,
|
||||
getAssignmentsSubmissions,
|
||||
putAssignment,
|
||||
} from '../services/assignments.js';
|
||||
|
@ -13,6 +14,19 @@ import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
|||
import { Assignment } from '../entities/assignments/assignment.entity.js';
|
||||
import { EntityDTO } from '@mikro-orm/core';
|
||||
|
||||
function getAssignmentParams(req: Request): { classid: string; assignmentNumber: number; full: boolean } {
|
||||
const classid = req.params.classid;
|
||||
const assignmentNumber = Number(req.params.id);
|
||||
const full = req.query.full === 'true';
|
||||
requireFields({ assignmentNumber, classid });
|
||||
|
||||
if (isNaN(assignmentNumber)) {
|
||||
throw new BadRequestException('Assignment id should be a number');
|
||||
}
|
||||
|
||||
return { classid, assignmentNumber, full };
|
||||
}
|
||||
|
||||
export async function getAllAssignmentsHandler(req: Request, res: Response): Promise<void> {
|
||||
const classId = req.params.classid;
|
||||
const full = req.query.full === 'true';
|
||||
|
@ -38,57 +52,42 @@ export async function createAssignmentHandler(req: Request, res: Response): Prom
|
|||
}
|
||||
|
||||
export async function getAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||
const id = Number(req.params.id);
|
||||
const classid = req.params.classid;
|
||||
requireFields({ id, classid });
|
||||
const { classid, assignmentNumber } = getAssignmentParams(req);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new BadRequestException('Assignment id should be a number');
|
||||
}
|
||||
|
||||
const assignment = await getAssignment(classid, id);
|
||||
const assignment = await getAssignment(classid, assignmentNumber);
|
||||
|
||||
res.json({ assignment });
|
||||
}
|
||||
|
||||
export async function putAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||
const id = Number(req.params.id);
|
||||
const classid = req.params.classid;
|
||||
requireFields({ id, classid });
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new BadRequestException('Assignment id should be a number');
|
||||
}
|
||||
const { classid, assignmentNumber } = getAssignmentParams(req);
|
||||
|
||||
const assignmentData = req.body as Partial<EntityDTO<Assignment>>;
|
||||
const assignment = await putAssignment(classid, id, assignmentData);
|
||||
const assignment = await putAssignment(classid, assignmentNumber, assignmentData);
|
||||
|
||||
res.json({ assignment });
|
||||
}
|
||||
|
||||
export async function deleteAssignmentHandler(req: Request, _res: Response): Promise<void> {
|
||||
const id = Number(req.params.id);
|
||||
const classid = req.params.classid;
|
||||
requireFields({ id, classid });
|
||||
export async function deleteAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||
const { classid, assignmentNumber } = getAssignmentParams(req);
|
||||
|
||||
if (isNaN(id)) {
|
||||
throw new BadRequestException('Assignment id should be a number');
|
||||
}
|
||||
const assignment = await deleteAssignment(classid, assignmentNumber);
|
||||
|
||||
await deleteAssignment(classid, id);
|
||||
res.json({ assignment });
|
||||
}
|
||||
|
||||
export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const classid = req.params.classid;
|
||||
const assignmentNumber = Number(req.params.id);
|
||||
const full = req.query.full === 'true';
|
||||
requireFields({ assignmentNumber, classid });
|
||||
|
||||
if (isNaN(assignmentNumber)) {
|
||||
throw new BadRequestException('Assignment id should be a number');
|
||||
}
|
||||
const { classid, assignmentNumber, full } = getAssignmentParams(req);
|
||||
|
||||
const submissions = await getAssignmentsSubmissions(classid, assignmentNumber, full);
|
||||
|
||||
res.json({ submissions });
|
||||
}
|
||||
|
||||
export async function getAssignmentQuestionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const { classid, assignmentNumber, full } = getAssignmentParams(req);
|
||||
|
||||
const questions = await getAssignmentsQuestions(classid, assignmentNumber, full);
|
||||
|
||||
res.json({ questions });
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupSubmissions, putGroup } from '../services/groups.js';
|
||||
import { createGroup, deleteGroup, getAllGroups, getGroup, getGroupQuestions, getGroupSubmissions, putGroup } from '../services/groups.js';
|
||||
import { GroupDTO } from '@dwengo-1/common/interfaces/group';
|
||||
import { requireFields } from './error-helper.js';
|
||||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
|
@ -84,7 +84,7 @@ export async function createGroupHandler(req: Request, res: Response): Promise<v
|
|||
res.status(201).json({ group });
|
||||
}
|
||||
|
||||
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||
function getGroupParams(req: Request): { classId: string; assignmentId: number; groupId: number; full: boolean } {
|
||||
const classId = req.params.classid;
|
||||
const assignmentId = Number(req.params.assignmentid);
|
||||
const groupId = Number(req.params.groupid);
|
||||
|
@ -100,7 +100,21 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P
|
|||
throw new BadRequestException('Group id must be a number');
|
||||
}
|
||||
|
||||
return { classId, assignmentId, groupId, full };
|
||||
}
|
||||
|
||||
export async function getGroupSubmissionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const { classId, assignmentId, groupId, full } = getGroupParams(req);
|
||||
|
||||
const submissions = await getGroupSubmissions(classId, assignmentId, groupId, full);
|
||||
|
||||
res.json({ submissions });
|
||||
}
|
||||
|
||||
export async function getGroupQuestionsHandler(req: Request, res: Response): Promise<void> {
|
||||
const { classId, assignmentId, groupId, full } = getGroupParams(req);
|
||||
|
||||
const questions = await getGroupQuestions(classId, assignmentId, groupId, full);
|
||||
|
||||
res.json({ questions });
|
||||
}
|
||||
|
|
|
@ -63,9 +63,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
|||
|
||||
public async findAllByAssignment(assignment: Assignment): Promise<Question[]> {
|
||||
return this.find({
|
||||
inGroup: {
|
||||
$contained: assignment.groups,
|
||||
},
|
||||
inGroup: assignment.groups.getItems(),
|
||||
learningObjectHruid: assignment.learningPathHruid,
|
||||
learningObjectLanguage: assignment.learningPathLanguage,
|
||||
});
|
||||
|
@ -78,6 +76,13 @@ export class QuestionRepository extends DwengoEntityRepository<Question> {
|
|||
});
|
||||
}
|
||||
|
||||
public async findAllByGroup(inGroup: Group): Promise<Question[]> {
|
||||
return this.findAll({
|
||||
where: { inGroup },
|
||||
orderBy: { timestamp: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up all questions for the given learning object which were asked as part of the given assignment.
|
||||
* When forStudentUsername is set, only the questions within the given user's group are shown.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Cascade, Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Class } from '../classes/class.entity.js';
|
||||
import { Group } from './group.entity.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
|
@ -34,6 +34,7 @@ export class Assignment {
|
|||
@OneToMany({
|
||||
entity: () => Group,
|
||||
mappedBy: 'assignment',
|
||||
cascade: [Cascade.ALL],
|
||||
})
|
||||
groups: Collection<Group> = new Collection<Group>(this);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Student } from '../users/student.entity.js';
|
||||
import { Group } from './group.entity.js';
|
||||
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
||||
import { Entity, Enum, ManyToOne, PrimaryKey, Property, Cascade } from '@mikro-orm/core';
|
||||
import { SubmissionRepository } from '../../data/assignments/submission-repository.js';
|
||||
import { Language } from '@dwengo-1/common/util/language';
|
||||
|
||||
|
@ -21,8 +21,8 @@ export class Submission {
|
|||
@PrimaryKey({ type: 'numeric', autoincrement: false })
|
||||
learningObjectVersion = 1;
|
||||
|
||||
@ManyToOne({
|
||||
entity: () => Group,
|
||||
@ManyToOne(() => Group, {
|
||||
cascade: [Cascade.REMOVE],
|
||||
})
|
||||
onBehalfOf!: Group;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
deleteAssignmentHandler,
|
||||
getAllAssignmentsHandler,
|
||||
getAssignmentHandler,
|
||||
getAssignmentQuestionsHandler,
|
||||
getAssignmentsSubmissionsHandler,
|
||||
putAssignmentHandler,
|
||||
} from '../controllers/assignments.js';
|
||||
|
@ -23,6 +24,8 @@ router.delete('/:id', deleteAssignmentHandler);
|
|||
|
||||
router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
|
||||
|
||||
router.get('/:id/questions', getAssignmentQuestionsHandler);
|
||||
|
||||
router.use('/:assignmentid/groups', groupRouter);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
deleteGroupHandler,
|
||||
getAllGroupsHandler,
|
||||
getGroupHandler,
|
||||
getGroupQuestionsHandler,
|
||||
getGroupSubmissionsHandler,
|
||||
putGroupHandler,
|
||||
} from '../controllers/groups.js';
|
||||
|
@ -23,4 +24,6 @@ router.delete('/:groupid', deleteGroupHandler);
|
|||
|
||||
router.get('/:groupid/submissions', getGroupSubmissionsHandler);
|
||||
|
||||
router.get('/:groupid/questions', getGroupQuestionsHandler);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { EntityDTO } from '@mikro-orm/core';
|
||||
import { getGroupRepository, getSubmissionRepository } from '../data/repositories.js';
|
||||
import { getGroupRepository, getQuestionRepository, getSubmissionRepository } from '../data/repositories.js';
|
||||
import { Group } from '../entities/assignments/group.entity.js';
|
||||
import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
|
||||
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
|
||||
|
@ -12,6 +12,8 @@ import { fetchClass } from './classes.js';
|
|||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
import { Student } from '../entities/users/student.entity.js';
|
||||
import { Class } from '../entities/classes/class.entity.js';
|
||||
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
||||
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
|
||||
|
||||
async function assertMembersInClass(members: Student[], cls: Class): Promise<void> {
|
||||
if (!members.every((student) => cls.students.contains(student))) {
|
||||
|
@ -121,3 +123,21 @@ export async function getGroupSubmissions(
|
|||
|
||||
return submissions.map(mapToSubmissionDTOId);
|
||||
}
|
||||
|
||||
export async function getGroupQuestions(
|
||||
classId: string,
|
||||
assignmentNumber: number,
|
||||
groupNumber: number,
|
||||
full: boolean
|
||||
): Promise<QuestionDTO[] | QuestionId[]> {
|
||||
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
|
||||
|
||||
const questionRepository = getQuestionRepository();
|
||||
const questions = await questionRepository.findAllByGroup(group);
|
||||
|
||||
if (full) {
|
||||
return questions.map(mapToQuestionDTO);
|
||||
}
|
||||
|
||||
return questions.map(mapToQuestionDTOId);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { ClassRepository } from '../../../src/data/classes/class-repository';
|
|||
import { Submission } from '../../../src/entities/assignments/submission.entity';
|
||||
import { Class } from '../../../src/entities/classes/class.entity';
|
||||
import { Assignment } from '../../../src/entities/assignments/assignment.entity';
|
||||
import { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata';
|
||||
|
||||
describe('SubmissionRepository', () => {
|
||||
let submissionRepository: SubmissionRepository;
|
||||
|
@ -106,7 +107,7 @@ describe('SubmissionRepository', () => {
|
|||
});
|
||||
|
||||
it('should not find a deleted submission', async () => {
|
||||
const id = new LearningObjectIdentifier('id01', Language.English, 1);
|
||||
const id = new LearningObjectIdentifier(testLearningObject01.hruid, testLearningObject01.language, testLearningObject01.version);
|
||||
await submissionRepository.deleteSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
||||
|
||||
const submission = await submissionRepository.findSubmissionByLearningObjectAndSubmissionNumber(id, 1);
|
||||
|
|
|
@ -5,5 +5,17 @@ export default defineConfig({
|
|||
environment: 'node',
|
||||
globals: true,
|
||||
testTimeout: 100000,
|
||||
coverage: {
|
||||
reporter: ['text', 'json-summary', 'json'],
|
||||
// If you want a coverage reports even if your tests are failing, include the reportOnFailure option
|
||||
reportOnFailure: true,
|
||||
exclude: ['**/*config*', '**/tests/**', 'src/*.ts', '**/dist/**', '**/node_modules/**', 'src/logging/**', 'src/routes/**'],
|
||||
thresholds: {
|
||||
lines: 50,
|
||||
branches: 50,
|
||||
functions: 50,
|
||||
statements: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"oauth2DevicePollingInterval": 5,
|
||||
"enabled": true,
|
||||
"sslRequired": "external",
|
||||
"registrationAllowed": false,
|
||||
"registrationAllowed": true,
|
||||
"registrationEmailAsUsername": false,
|
||||
"rememberMe": false,
|
||||
"verifyEmail": false,
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"oauth2DevicePollingInterval": 5,
|
||||
"enabled": true,
|
||||
"sslRequired": "external",
|
||||
"registrationAllowed": false,
|
||||
"registrationAllowed": true,
|
||||
"registrationEmailAsUsername": false,
|
||||
"rememberMe": false,
|
||||
"verifyEmail": false,
|
||||
|
|
|
@ -52,11 +52,18 @@ npm run test:unit
|
|||
### Run End-to-End Tests with [Playwright](https://playwright.dev)
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
|
||||
# Install browsers for the first run
|
||||
npx playwright install
|
||||
# On Ubuntu, you can also use
|
||||
npx playwright install --with-deps
|
||||
# to additionally install the dependencies.
|
||||
|
||||
# When testing on CI, must build the project first
|
||||
cd ..
|
||||
npm run build
|
||||
cd frontend
|
||||
|
||||
# Runs the end-to-end tests
|
||||
npm run test:e2e
|
||||
|
|
86
frontend/e2e/basic-homepage.spec.ts
Normal file
86
frontend/e2e/basic-homepage.spec.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("User can pick their language", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByRole("button", { name: "translate" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "translate" }).click();
|
||||
await page.getByText("Nederlands").click();
|
||||
await expect(page.locator("h1")).toContainText("Onze sterke punten");
|
||||
await expect(page.getByRole("heading", { name: "Innovatief" })).toBeVisible();
|
||||
|
||||
await page.getByRole("heading", { name: "Innovatief" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "vertalen" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "vertalen" }).click();
|
||||
await page.getByText("English").click();
|
||||
await expect(page.locator("h1")).toContainText("Our strengths");
|
||||
await expect(page.getByRole("heading", { name: "Innovative" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Teacher can sign in", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("link", { name: "log in" })).toBeVisible();
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "teacher" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "teacher" }).click();
|
||||
|
||||
await expect(page.getByText("teacher")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Sign In" })).toBeVisible();
|
||||
|
||||
await expect(page).toHaveURL(/\/realms\/teacher\//);
|
||||
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerkracht1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
await expect(page.getByRole("link", { name: "Dwengo logo teacher" })).toBeVisible();
|
||||
await expect(page.getByRole("button").nth(1)).toBeVisible();
|
||||
});
|
||||
|
||||
test("Student can sign in", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("link", { name: "log in" })).toBeVisible();
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "student" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "student" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/realms\/student\//);
|
||||
|
||||
await expect(page.getByText("student")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Sign In" })).toBeVisible();
|
||||
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerling1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
await expect(page.getByRole("link", { name: "Dwengo logo student" })).toBeVisible();
|
||||
await expect(page.getByRole("button").nth(1)).toBeVisible();
|
||||
});
|
||||
|
||||
test("Cannot sign in with invalid credentials", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "teacher" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("doesnotexist");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("wrong");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
await expect(page.getByText("Invalid username or password.")).toBeVisible();
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "student" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("doesnotexist");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("wrong");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
await expect(page.getByText("Invalid username or password.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Cannot skip login", async ({ page }) => {
|
||||
await page.goto("/user");
|
||||
// Should redirect to login
|
||||
await expect(page.getByText("login")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "teacher" })).toBeVisible();
|
||||
});
|
12
frontend/e2e/basic-learning.spec.ts
Normal file
12
frontend/e2e/basic-learning.spec.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { test, expect } from "./fixtures.js";
|
||||
|
||||
test("Users can filter", async ({ page }) => {
|
||||
await page.goto("/user");
|
||||
|
||||
await page.getByRole("combobox").filter({ hasText: "Select a themeAll" }).locator("i").click();
|
||||
await page.getByText("Nature and climate").click();
|
||||
await page.getByRole("combobox").filter({ hasText: "Select ageAll agesSelect age" }).locator("i").click();
|
||||
await page.getByText("and older").click();
|
||||
|
||||
await expect(page.getByRole("link", { name: "AI and Climate Students in" })).toBeVisible();
|
||||
});
|
5
frontend/e2e/basic-learning.ts
Normal file
5
frontend/e2e/basic-learning.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { test, expect } from "./fixtures.js";
|
||||
|
||||
test("myTest", async ({ page }) => {
|
||||
await expect(page).toHaveURL("/");
|
||||
});
|
116
frontend/e2e/fixtures.ts
Normal file
116
frontend/e2e/fixtures.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
/* eslint-disable no-await-in-loop */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { test as baseTest, expect } from "@playwright/test";
|
||||
import type { Browser } from "playwright-core";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
/* Based on https://playwright.dev/docs/auth#moderate-one-account-per-parallel-worker */
|
||||
|
||||
export * from "@playwright/test";
|
||||
export const ROOT_URL = "http://localhost:5173";
|
||||
|
||||
interface Account {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire an account by logging in or creating a new one.
|
||||
* @param id
|
||||
* @param browser
|
||||
*/
|
||||
async function acquireAccount(id: number, browser: Browser): Promise<Account> {
|
||||
const account = {
|
||||
username: `worker${id}`,
|
||||
password: "password",
|
||||
};
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.goto(ROOT_URL);
|
||||
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "student" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(account.username);
|
||||
await page.getByRole("textbox", { name: "Password", exact: true }).fill(account.password);
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
let failed = await page.getByText("Invalid username or password.").isVisible();
|
||||
|
||||
if (failed) {
|
||||
await page.getByRole("link", { name: "Register" }).click();
|
||||
}
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
let retries = 0;
|
||||
while (failed && retries < MAX_RETRIES) {
|
||||
// Retry with a different username, based on Unix timestamp.
|
||||
account.username = `worker${id}-${Date.now()}`;
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(account.username);
|
||||
await page.getByRole("textbox", { name: "Password", exact: true }).fill(account.password);
|
||||
await page.getByRole("textbox", { name: "Confirm password" }).fill(account.password);
|
||||
await page.getByRole("textbox", { name: "Email" }).fill(`${account.username}@dwengo.org`);
|
||||
await page.getByRole("textbox", { name: "First name" }).fill("Worker");
|
||||
await page.getByRole("textbox", { name: "Last name" }).fill(id.toString());
|
||||
await page.getByRole("button", { name: "Register" }).click();
|
||||
|
||||
await page.waitForURL(/localhost/);
|
||||
|
||||
failed = await page.getByText("Username already exists.").isVisible();
|
||||
retries += failed ? 1 : 0;
|
||||
}
|
||||
|
||||
await page.waitForURL(/localhost/);
|
||||
await page.close();
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
export const test = baseTest.extend<object, { workerStorageState: string }>({
|
||||
// Use the same storage state for all tests in this worker.
|
||||
storageState: async ({ workerStorageState }, use) => use(workerStorageState),
|
||||
|
||||
// Authenticate once per worker with a worker-scoped fixture.
|
||||
workerStorageState: [
|
||||
async ({ browser }, use): Promise<void> => {
|
||||
// Use parallelIndex as a unique identifier for each worker.
|
||||
const id = test.info().parallelIndex;
|
||||
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);
|
||||
|
||||
if (fs.existsSync(fileName)) {
|
||||
// Reuse existing authentication state if any.
|
||||
await use(fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Important: make sure we authenticate in a clean environment by unsetting storage state.
|
||||
const page = await browser.newPage({ storageState: undefined });
|
||||
|
||||
// Acquire a unique account by creating a new one.
|
||||
const account = await acquireAccount(id, browser);
|
||||
|
||||
// Perform authentication steps. Replace these actions with your own.
|
||||
await page.goto(ROOT_URL);
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "student" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill(account.username);
|
||||
await page.getByRole("textbox", { name: "Password" }).fill(account.password);
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
// Wait until the page receives the cookies.
|
||||
//
|
||||
// Sometimes login flow sets cookies in the process of several redirects.
|
||||
// Wait for the final URL to ensure that the cookies are actually set.
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
// Alternatively, you can wait until the page reaches a state where all cookies are set.
|
||||
|
||||
// End of authentication steps.
|
||||
|
||||
await page.context().storageState({ path: fileName });
|
||||
await page.close();
|
||||
await use(fileName);
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// See here how to get started:
|
||||
// https://playwright.dev/docs/intro
|
||||
test("visits the app root url", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.locator("h1")).toHaveText("You did it!");
|
||||
});
|
|
@ -13,11 +13,13 @@
|
|||
"format-check": "prettier --check src/",
|
||||
"lint": "eslint . --fix",
|
||||
"test:unit": "vitest --run",
|
||||
"test:coverage": "vitest --run --coverage.enabled true",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@tanstack/vue-query": "^5.69.0",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"axios": "^1.8.2",
|
||||
"oidc-client-ts": "^3.1.0",
|
||||
"rollup": "^4.40.0",
|
||||
|
@ -25,10 +27,11 @@
|
|||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"vue-router": "^4.5.0",
|
||||
"vuetify": "^3.7.12"
|
||||
"vuetify": "^3.7.12",
|
||||
"wait-on": "^8.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.13.4",
|
||||
|
|
|
@ -24,7 +24,7 @@ export default defineConfig({
|
|||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
retries: process.env.CI ? 2 : 1,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
|
@ -65,18 +65,18 @@ export default defineConfig({
|
|||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// Name: 'Mobile Chrome',
|
||||
// Use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// Name: 'Mobile Safari',
|
||||
// Use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
{
|
||||
name: "Mobile Chrome",
|
||||
use: {
|
||||
...devices["Pixel 5"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Mobile Safari",
|
||||
use: {
|
||||
...devices["iPhone 12"],
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
|
@ -97,14 +97,25 @@ export default defineConfig({
|
|||
// OutputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
/**
|
||||
* Use the dev server by default for faster feedback loop.
|
||||
* Use the preview server on CI for more realistic testing.
|
||||
* Playwright will re-use the local server if there is already a dev-server running.
|
||||
*/
|
||||
command: process.env.CI ? "npm run preview" : "npm run dev",
|
||||
port: process.env.CI ? 4173 : 5173,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
webServer: [
|
||||
// Assuming the idp is already running (because it is slow)
|
||||
{
|
||||
/* Frontend */
|
||||
command: `VITE_API_BASE_URL='http://localhost:9876/api' ${process.env.CI ? "npm run preview" : "npm run dev"}`,
|
||||
port: process.env.CI ? 4173 : 5173,
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
{
|
||||
/* Backend */
|
||||
command: `
|
||||
cd .. \
|
||||
&& npx tsc --build common/tsconfig.json \
|
||||
&& cd backend \
|
||||
&& npx tsx --env-file=./.env.test ./tool/startTestApp.ts
|
||||
`,
|
||||
port: 9876,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
49
frontend/src/assets/assignment.css
Normal file
49
frontend/src/assets/assignment.css
Normal file
|
@ -0,0 +1,49 @@
|
|||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.assignment-card {
|
||||
width: 80%;
|
||||
padding: 2%;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 2%;
|
||||
line-height: 1.6;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.top-right-btn {
|
||||
position: absolute;
|
||||
right: 2%;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.group-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.group-section h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.group-section ul {
|
||||
padding-left: 1.2rem;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.subtitle-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.assignmentTopTitle {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
|
@ -11,7 +11,7 @@
|
|||
selectedAge: { type: String, required: true },
|
||||
});
|
||||
|
||||
const { locale } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const language = computed(() => locale.value);
|
||||
|
||||
const { data: allThemes, isLoading, error } = useThemeQuery(language);
|
||||
|
@ -74,6 +74,22 @@
|
|||
class="fill-height"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="4"
|
||||
class="d-flex"
|
||||
>
|
||||
<ThemeCard
|
||||
path="/learningPath/search"
|
||||
:is-absolute-path="true"
|
||||
:title="t('searchAllLearningPathsTitle')"
|
||||
:description="t('searchAllLearningPathsDescription')"
|
||||
icon="mdi-magnify"
|
||||
class="fill-height"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
path: string;
|
||||
isAbsolutePath?: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
image?: string;
|
||||
icon?: string;
|
||||
}>();
|
||||
|
||||
const routerLink = computed(() => (props.isAbsolutePath ? props.path : `/theme/${props.path}`));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
class="theme-card d-flex flex-column"
|
||||
:to="`theme/${path}`"
|
||||
:to="routerLink"
|
||||
link
|
||||
>
|
||||
<v-card-title class="title-container">
|
||||
|
@ -27,12 +32,18 @@
|
|||
contain
|
||||
class="title-image"
|
||||
></v-img>
|
||||
<v-icon
|
||||
v-if="icon"
|
||||
class="title-image"
|
||||
>{{ icon }}</v-icon
|
||||
>
|
||||
|
||||
<span class="title">{{ title }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text class="description flex-grow-1">{{ description }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
:to="`theme/${path}`"
|
||||
:to="routerLink"
|
||||
variant="text"
|
||||
>
|
||||
{{ t("read-more") }}
|
||||
|
|
49
frontend/src/components/assignments/DeadlineSelector.vue
Normal file
49
frontend/src/components/assignments/DeadlineSelector.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { deadlineRules } from "@/utils/assignment-rules.ts";
|
||||
|
||||
const date = ref("");
|
||||
const time = ref("23:59");
|
||||
const emit = defineEmits(["update:deadline"]);
|
||||
|
||||
const formattedDeadline = computed(() => {
|
||||
if (!date.value || !time.value) return "";
|
||||
return `${date.value} ${time.value}`;
|
||||
});
|
||||
|
||||
function updateDeadline(): void {
|
||||
if (date.value && time.value) {
|
||||
emit("update:deadline", formattedDeadline.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="date"
|
||||
label="Select Deadline Date"
|
||||
type="date"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="deadlineRules"
|
||||
required
|
||||
@update:modelValue="updateDeadline"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="time"
|
||||
label="Select Deadline Time"
|
||||
type="time"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
@update:modelValue="updateDeadline"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
75
frontend/src/components/assignments/GroupSelector.vue
Normal file
75
frontend/src/components/assignments/GroupSelector.vue
Normal file
|
@ -0,0 +1,75 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import type { StudentsResponse } from "@/controllers/students.ts";
|
||||
import { useClassStudentsQuery } from "@/queries/classes.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
classId: string | undefined;
|
||||
groups: string[][];
|
||||
}>();
|
||||
const emit = defineEmits(["groupCreated"]);
|
||||
const { t } = useI18n();
|
||||
|
||||
const selectedStudents = ref([]);
|
||||
|
||||
const studentQueryResult = useClassStudentsQuery(() => props.classId, true);
|
||||
|
||||
function filterStudents(data: StudentsResponse): { title: string; value: string }[] {
|
||||
const students = data.students;
|
||||
const studentsInGroups = props.groups.flat();
|
||||
|
||||
return students
|
||||
?.map((st) => ({
|
||||
title: `${st.firstName} ${st.lastName}`,
|
||||
value: st.username,
|
||||
}))
|
||||
.filter((student) => !studentsInGroups.includes(student.value));
|
||||
}
|
||||
|
||||
function createGroup(): void {
|
||||
if (selectedStudents.value.length) {
|
||||
// Extract only usernames (student.value)
|
||||
const usernames = selectedStudents.value.map((student) => student.value);
|
||||
emit("groupCreated", usernames);
|
||||
selectedStudents.value = []; // Reset selection after creating group
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<using-query-result
|
||||
:query-result="studentQueryResult"
|
||||
v-slot="{ data }: { data: StudentsResponse }"
|
||||
>
|
||||
<h3>{{ t("create-groups") }}</h3>
|
||||
<v-card-text>
|
||||
<v-combobox
|
||||
v-model="selectedStudents"
|
||||
:items="filterStudents(data)"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
:label="t('choose-students')"
|
||||
variant="outlined"
|
||||
clearable
|
||||
multiple
|
||||
hide-details
|
||||
density="compact"
|
||||
chips
|
||||
append-inner-icon="mdi-magnify"
|
||||
></v-combobox>
|
||||
|
||||
<v-btn
|
||||
@click="createGroup"
|
||||
color="primary"
|
||||
class="mt-2"
|
||||
size="small"
|
||||
>
|
||||
{{ t("create-group") }}
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</using-query-result>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -30,4 +30,10 @@ export class LearningPathController extends BaseController {
|
|||
const dtos = await this.get<LearningPathDTO[]>("/", { theme });
|
||||
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
||||
}
|
||||
|
||||
async getAllLearningPaths(language: string | null = null): Promise<LearningPath[]> {
|
||||
const query = language ? { language } : undefined;
|
||||
const dtos = await this.get<LearningPathDTO[]>("/", query);
|
||||
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,8 +57,21 @@
|
|||
"legendNotCompletedYet": "Noch nicht fertig",
|
||||
"legendCompleted": "Fertig",
|
||||
"legendTeacherExclusive": "Information für Lehrkräfte",
|
||||
"new-assignment": "Neue Aufgabe",
|
||||
"edit-assignment": "Zuordnung bearbeiten",
|
||||
"groups": "Gruppen",
|
||||
"learning-path": "Lernpfad",
|
||||
"choose-lp": "Einen lernpfad auswählen",
|
||||
"choose-classes": "Klassen wählen",
|
||||
"create-groups": "Gruppen erstellen",
|
||||
"title": "Titel",
|
||||
"pick-class": "Wählen Sie eine klasse",
|
||||
"choose-students": "Studenten auswählen",
|
||||
"create-group": "Gruppe erstellen",
|
||||
"class": "klasse",
|
||||
"delete": "löschen",
|
||||
"view-assignment": "Auftrag anzeigen",
|
||||
"code": "code",
|
||||
"class": "Klasse",
|
||||
"invitations": "Einladungen",
|
||||
"createClass": "Klasse erstellen",
|
||||
"createClassInstructions": "Geben Sie einen Namen für Ihre Klasse ein und klicken Sie auf „Erstellen“. Es erscheint ein Fenster mit einem Code, den Sie kopieren können. Geben Sie diesen Code an Ihre Schüler weiter und sie können Ihrer Klasse beitreten.",
|
||||
|
@ -89,6 +102,11 @@
|
|||
"noSubmissionsYet": "Noch keine Lösungen eingereicht.",
|
||||
"viewAsGroup": "Fortschritt ansehen von Gruppe...",
|
||||
"assignLearningPath": "Als Aufgabe geben",
|
||||
"group": "Gruppe",
|
||||
"description": "Beschreibung",
|
||||
"no-submission": "keine vorlage",
|
||||
"submission": "Einreichung",
|
||||
"progress": "Fortschritte",
|
||||
"remove": "entfernen",
|
||||
"students": "Studenten",
|
||||
"classJoinRequests": "Beitrittsanfragen",
|
||||
|
@ -102,4 +120,6 @@
|
|||
"username": "Nutzername",
|
||||
"invite": "einladen",
|
||||
"assignmentIndicator": "AUFGABE"
|
||||
"searchAllLearningPathsTitle": "Alle Lernpfade durchsuchen",
|
||||
"searchAllLearningPathsDescription": "Nicht gefunden, was Sie gesucht haben? Klicken Sie hier, um unsere gesamte Lernpfad-Datenbank zu durchsuchen."
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"welcome": "Welcome",
|
||||
"student": "student",
|
||||
"teacher": "teacher",
|
||||
"assignments": "assignments",
|
||||
"classes": "classes",
|
||||
"assignments": "Assignments",
|
||||
"classes": "Classes",
|
||||
"discussions": "discussions",
|
||||
"logout": "log out",
|
||||
"error_title": "Error",
|
||||
|
@ -33,7 +33,7 @@
|
|||
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
|
||||
"invalidFormat": "Invalid format.",
|
||||
"submitCode": "submit",
|
||||
"members": "members",
|
||||
"members": "Members",
|
||||
"themes": "Themes",
|
||||
"choose-theme": "Select a theme",
|
||||
"choose-age": "Select age",
|
||||
|
@ -57,8 +57,21 @@
|
|||
"older": "18 and older"
|
||||
},
|
||||
"read-more": "Read more",
|
||||
"code": "code",
|
||||
"new-assignment": "New Assignment",
|
||||
"edit-assignment": "Edit Assignment",
|
||||
"groups": "Groups",
|
||||
"learning-path": "Learning path",
|
||||
"choose-lp": "Select a learning path",
|
||||
"choose-classes": "Select classes",
|
||||
"create-groups": "Create groups",
|
||||
"title": "Title",
|
||||
"pick-class": "Pick a class",
|
||||
"choose-students": "Select students",
|
||||
"create-group": "Create group",
|
||||
"class": "class",
|
||||
"delete": "delete",
|
||||
"view-assignment": "View assignment",
|
||||
"code": "code",
|
||||
"invitations": "invitations",
|
||||
"createClass": "create class",
|
||||
"classname": "classname",
|
||||
|
@ -75,7 +88,6 @@
|
|||
"sent": "sent",
|
||||
"failed": "failed",
|
||||
"wrong": "something went wrong",
|
||||
"created": "created",
|
||||
"callbackLoading": "You are being logged in...",
|
||||
"loginUnexpectedError": "Login failed",
|
||||
"submitSolution": "Submit solution",
|
||||
|
@ -89,6 +101,12 @@
|
|||
"noSubmissionsYet": "No submissions yet.",
|
||||
"viewAsGroup": "View progress of group...",
|
||||
"assignLearningPath": "assign",
|
||||
"group": "Group",
|
||||
"description": "Description",
|
||||
"no-submission": "no submission",
|
||||
"submission": "Submission",
|
||||
"progress": "Progress",
|
||||
"created": "created",
|
||||
"remove": "remove",
|
||||
"students": "students",
|
||||
"classJoinRequests": "join requests",
|
||||
|
@ -102,4 +120,6 @@
|
|||
"username": "username",
|
||||
"invite": "invite",
|
||||
"assignmentIndicator": "ASSIGNMENT"
|
||||
"searchAllLearningPathsTitle": "Search all learning paths",
|
||||
"searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths."
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
|
||||
"invalidFormat": "Format non valide.",
|
||||
"submitCode": "envoyer",
|
||||
"members": "membres",
|
||||
"members": "Membres",
|
||||
"themes": "Thèmes",
|
||||
"choose-theme": "Choisis un thème",
|
||||
"choose-age": "Choisis un âge",
|
||||
|
@ -57,8 +57,21 @@
|
|||
"older": "18 et plus"
|
||||
},
|
||||
"read-more": "En savoir plus",
|
||||
"code": "code",
|
||||
"new-assignment": "Nouveau travail",
|
||||
"edit-assignment": "Modifier le travail",
|
||||
"groups": "Groupes",
|
||||
"learning-path": "Parcours d'apprentissage",
|
||||
"choose-lp": "Choisissez un parcours d'apprentissage",
|
||||
"choose-classes": "Choisissez des classes",
|
||||
"create-groups": "Créer des groupes",
|
||||
"title": "Titre",
|
||||
"pick-class": "Choisissez une classe",
|
||||
"choose-students": "Sélectionnez des élèves",
|
||||
"create-group": "Créer un groupe",
|
||||
"class": "classe",
|
||||
"delete": "supprimer",
|
||||
"view-assignment": "Voir le travail",
|
||||
"code": "code",
|
||||
"invitations": "invitations",
|
||||
"createClass": "créer une classe",
|
||||
"createClassInstructions": "Entrez un nom pour votre classe et cliquez sur créer. Une fenêtre apparaît avec un code que vous pouvez copier. Donnez ce code à vos élèves et ils pourront rejoindre votre classe.",
|
||||
|
@ -89,6 +102,11 @@
|
|||
"noSubmissionsYet": "Pas encore de soumissions.",
|
||||
"viewAsGroup": "Voir la progression du groupe...",
|
||||
"assignLearningPath": "donner comme tâche",
|
||||
"group": "Groupe",
|
||||
"description": "Description",
|
||||
"no-submission": "aucune soumission",
|
||||
"submission": "Soumission",
|
||||
"progress": "Progrès",
|
||||
"remove": "supprimer",
|
||||
"students": "étudiants",
|
||||
"classJoinRequests": "demandes d'adhésion",
|
||||
|
@ -102,4 +120,6 @@
|
|||
"username": "Nom d'utilisateur",
|
||||
"invite": "inviter",
|
||||
"assignmentIndicator": "DEVOIR"
|
||||
"searchAllLearningPathsTitle": "Rechercher tous les parcours d'apprentissage",
|
||||
"searchAllLearningPathsDescription": "Vous n'avez pas trouvé ce que vous cherchiez ? Cliquez ici pour rechercher dans toute notre base de données de parcours d'apprentissage disponibles."
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
"welcome": "Welkom",
|
||||
"student": "leerling",
|
||||
"teacher": "leerkracht",
|
||||
"assignments": "opdrachten",
|
||||
"classes": "klassen",
|
||||
"assignments": "Opdrachten",
|
||||
"classes": "Klassen",
|
||||
"discussions": "discussies",
|
||||
"logout": "log uit",
|
||||
"error_title": "Fout",
|
||||
|
@ -33,7 +33,7 @@
|
|||
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
|
||||
"invalidFormat": "Ongeldig formaat.",
|
||||
"submitCode": "verzenden",
|
||||
"members": "leden",
|
||||
"members": "Leden",
|
||||
"themes": "Lesthema's",
|
||||
"choose-theme": "Kies een thema",
|
||||
"choose-age": "Kies een leeftijd",
|
||||
|
@ -57,8 +57,21 @@
|
|||
"older": "Hoger onderwijs"
|
||||
},
|
||||
"read-more": "Lees meer",
|
||||
"code": "code",
|
||||
"new-assignment": "Nieuwe opdracht",
|
||||
"edit-assignment": "Opdracht bewerken",
|
||||
"groups": "Groepen",
|
||||
"learning-path": "Leerpad",
|
||||
"choose-lp": "Kies een leerpad",
|
||||
"choose-classes": "Kies klassen",
|
||||
"create-groups": "Groepen maken",
|
||||
"title": "Titel",
|
||||
"pick-class": "Kies een klas",
|
||||
"choose-students": "Studenten selecteren",
|
||||
"create-group": "Groep aanmaken",
|
||||
"class": "klas",
|
||||
"delete": "verwijderen",
|
||||
"view-assignment": "Opdracht bekijken",
|
||||
"code": "code",
|
||||
"invitations": "uitnodigingen",
|
||||
"createClass": "klas aanmaken",
|
||||
"createClassInstructions": "Voer een naam in voor je klas en klik op create. Er verschijnt een venster met een code die je kunt kopiëren. Geef deze code aan je leerlingen en ze kunnen deelnemen aan je klas.",
|
||||
|
@ -89,6 +102,11 @@
|
|||
"noSubmissionsYet": "Nog geen indieningen.",
|
||||
"viewAsGroup": "Vooruitgang bekijken van groep...",
|
||||
"assignLearningPath": "Als opdracht geven",
|
||||
"group": "Groep",
|
||||
"description": "Beschrijving",
|
||||
"no-submission": "geen indiening",
|
||||
"submission": "Indiening",
|
||||
"progress": "Vooruitgang",
|
||||
"remove": "verwijder",
|
||||
"students": "studenten",
|
||||
"classJoinRequests": "deelname verzoeken",
|
||||
|
@ -102,4 +120,6 @@
|
|||
"username": "gebruikersnaam",
|
||||
"invite": "uitnodigen",
|
||||
"assignmentIndicator": "OPDRACHT"
|
||||
"searchAllLearningPathsTitle": "Alle leerpaden doorzoeken",
|
||||
"searchAllLearningPathsDescription": "Niet gevonden waar je naar op zoek was? Klik hier om onze volledige databank van beschikbare leerpaden te doorzoeken."
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ export function useAssignmentsQuery(
|
|||
export function useAssignmentQuery(
|
||||
classid: MaybeRefOrGetter<string | undefined>,
|
||||
assignmentNumber: MaybeRefOrGetter<number | undefined>,
|
||||
): UseQueryReturnType<AssignmentsResponse, Error> {
|
||||
): UseQueryReturnType<AssignmentResponse, Error> {
|
||||
const { cid, an } = toValues(classid, assignmentNumber, 1, true);
|
||||
|
||||
return useQuery({
|
||||
|
@ -146,7 +146,7 @@ export function useDeleteAssignmentMutation(): UseMutationReturnType<
|
|||
|
||||
await invalidateAllAssignmentKeys(queryClient, cid, an);
|
||||
await invalidateAllGroupKeys(queryClient, cid, an);
|
||||
await invalidateAllSubmissionKeys(queryClient, cid, an);
|
||||
await invalidateAllSubmissionKeys(queryClient, undefined, undefined, undefined, cid, an);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -46,3 +46,16 @@ export function useSearchLearningPathQuery(
|
|||
enabled: () => Boolean(toValue(query)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetAllLearningPaths(
|
||||
language: MaybeRefOrGetter<string | undefined>,
|
||||
): UseQueryReturnType<LearningPath[], Error> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_PATH_KEY, "getAllLearningPaths", language],
|
||||
queryFn: async () => {
|
||||
const lang = toValue(language);
|
||||
return learningPathController.getAllLearningPaths(lang);
|
||||
},
|
||||
enabled: () => Boolean(toValue(language)),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { computed, toValue } from "vue";
|
||||
import { computed, type Ref, toValue } from "vue";
|
||||
import type { MaybeRefOrGetter } from "vue";
|
||||
import {
|
||||
type QueryObserverResult,
|
||||
useMutation,
|
||||
type UseMutationReturnType,
|
||||
useQueries,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseQueryReturnType,
|
||||
|
@ -70,6 +72,20 @@ export function useStudentQuery(
|
|||
});
|
||||
}
|
||||
|
||||
export function useStudentsByUsernamesQuery(
|
||||
usernames: MaybeRefOrGetter<string[] | undefined>,
|
||||
): Ref<QueryObserverResult<StudentResponse>[]> {
|
||||
const resolvedUsernames = toValue(usernames) ?? [];
|
||||
|
||||
return useQueries({
|
||||
queries: resolvedUsernames?.map((username) => ({
|
||||
queryKey: computed(() => studentQueryKey(toValue(username))),
|
||||
queryFn: async () => studentController.getByUsername(toValue(username)),
|
||||
enabled: Boolean(toValue(username)),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export function useStudentClassesQuery(
|
||||
username: MaybeRefOrGetter<string | undefined>,
|
||||
full: MaybeRefOrGetter<boolean> = true,
|
||||
|
|
|
@ -7,13 +7,13 @@ import CreateAssignment from "@/views/assignments/CreateAssignment.vue";
|
|||
import CreateDiscussion from "@/views/discussions/CreateDiscussion.vue";
|
||||
import CallbackPage from "@/views/CallbackPage.vue";
|
||||
import UserClasses from "@/views/classes/UserClasses.vue";
|
||||
import UserAssignments from "@/views/classes/UserAssignments.vue";
|
||||
import authService from "@/services/auth/auth-service.ts";
|
||||
import UserAssignments from "@/views/assignments/UserAssignments.vue";
|
||||
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/learning-object/LearningObjectView.vue";
|
||||
import authService from "@/services/auth/auth-service";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
@ -72,16 +72,20 @@ const router = createRouter({
|
|||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/assignment/create",
|
||||
name: "CreateAssigment",
|
||||
component: CreateAssignment,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/assignment/:id",
|
||||
name: "SingleAssigment",
|
||||
component: SingleAssignment,
|
||||
path: "/assignment",
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: "create",
|
||||
name: "CreateAssigment",
|
||||
component: CreateAssignment,
|
||||
},
|
||||
{
|
||||
path: ":classId/:id",
|
||||
name: "SingleAssigment",
|
||||
component: SingleAssignment,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/class/:id",
|
||||
|
|
76
frontend/src/utils/assignment-rules.ts
Normal file
76
frontend/src/utils/assignment-rules.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Validation rule for the assignment title.
|
||||
*
|
||||
* Ensures that the title is not empty.
|
||||
*/
|
||||
export const assignmentTitleRules = [
|
||||
(value: string): string | boolean => {
|
||||
if (value?.length >= 1) {
|
||||
return true;
|
||||
} // Title must not be empty
|
||||
return "Title cannot be empty.";
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Validation rule for the learning path selection.
|
||||
*
|
||||
* Ensures that a valid learning path is selected.
|
||||
*/
|
||||
export const learningPathRules = [
|
||||
(value: { hruid: string; title: string }): string | boolean => {
|
||||
if (value && value.hruid) {
|
||||
return true; // Valid if hruid is present
|
||||
}
|
||||
return "You must select a learning path.";
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Validation rule for the classes selection.
|
||||
*
|
||||
* Ensures that at least one class is selected.
|
||||
*/
|
||||
export const classRules = [
|
||||
(value: string): string | boolean => {
|
||||
if (value) {
|
||||
return true;
|
||||
}
|
||||
return "You must select at least one class.";
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Validation rule for the deadline field.
|
||||
*
|
||||
* Ensures that a valid deadline is selected and is in the future.
|
||||
*/
|
||||
export const deadlineRules = [
|
||||
(value: string): string | boolean => {
|
||||
if (!value) {
|
||||
return "You must set a deadline.";
|
||||
}
|
||||
|
||||
const selectedDateTime = new Date(value);
|
||||
const now = new Date();
|
||||
|
||||
if (isNaN(selectedDateTime.getTime())) {
|
||||
return "Invalid date or time.";
|
||||
}
|
||||
|
||||
if (selectedDateTime <= now) {
|
||||
return "The deadline must be in the future.";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
];
|
||||
|
||||
export const descriptionRules = [
|
||||
(value: string): string | boolean => {
|
||||
if (!value || value.trim() === "") {
|
||||
return "Description cannot be empty.";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
];
|
|
@ -1,14 +1,258 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import GroupSelector from "@/components/assignments/GroupSelector.vue";
|
||||
import { assignmentTitleRules, classRules, descriptionRules, learningPathRules } from "@/utils/assignment-rules.ts";
|
||||
import DeadlineSelector from "@/components/assignments/DeadlineSelector.vue";
|
||||
import auth from "@/services/auth/auth-service.ts";
|
||||
import { useTeacherClassesQuery } from "@/queries/teachers.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useGetAllLearningPaths } from "@/queries/learning-paths.ts";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
|
||||
import { useCreateAssignmentMutation } from "@/queries/assignments.ts";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const role = ref(auth.authState.activeRole);
|
||||
const username = ref<string>("");
|
||||
|
||||
onMounted(async () => {
|
||||
// Redirect student
|
||||
if (role.value === "student") {
|
||||
await router.push("/user");
|
||||
}
|
||||
|
||||
// Get the user's username
|
||||
const user = await auth.loadUser();
|
||||
username.value = user?.profile?.preferred_username ?? "";
|
||||
});
|
||||
|
||||
const language = computed(() => locale.value);
|
||||
const form = ref();
|
||||
|
||||
//Fetch all learning paths
|
||||
const learningPathsQueryResults = useGetAllLearningPaths(language);
|
||||
|
||||
// Fetch and store all the teacher's classes
|
||||
const classesQueryResults = useTeacherClassesQuery(username, true);
|
||||
|
||||
const selectedClass = ref(undefined);
|
||||
|
||||
const assignmentTitle = ref("");
|
||||
const selectedLearningPath = ref(route.query.hruid || undefined);
|
||||
|
||||
// Disable combobox when learningPath prop is passed
|
||||
const lpIsSelected = route.query.hruid !== undefined;
|
||||
const deadline = ref(null);
|
||||
const description = ref("");
|
||||
const groups = ref<string[][]>([]);
|
||||
|
||||
// New group is added to the list
|
||||
function addGroupToList(students: string[]): void {
|
||||
if (students.length) {
|
||||
groups.value = [...groups.value, students];
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedClass, () => {
|
||||
groups.value = [];
|
||||
});
|
||||
|
||||
const { mutate, data, isSuccess } = useCreateAssignmentMutation();
|
||||
|
||||
watch([isSuccess, data], async ([success, newData]) => {
|
||||
if (success && newData?.assignment) {
|
||||
await router.push(`/assignment/${newData.assignment.within}/${newData.assignment.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
async function submitFormHandler(): Promise<void> {
|
||||
const { valid } = await form.value.validate();
|
||||
if (!valid) return;
|
||||
|
||||
let lp = selectedLearningPath.value;
|
||||
if (!lpIsSelected) {
|
||||
lp = selectedLearningPath.value?.hruid;
|
||||
}
|
||||
|
||||
const assignmentDTO: AssignmentDTO = {
|
||||
id: 0,
|
||||
within: selectedClass.value?.id || "",
|
||||
title: assignmentTitle.value,
|
||||
description: description.value,
|
||||
learningPath: lp || "",
|
||||
language: language.value,
|
||||
groups: groups.value,
|
||||
};
|
||||
|
||||
mutate({ cid: assignmentDTO.within, data: assignmentDTO });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
Hier zou de pagina staan om een assignment aan te maken voor de leerpad met hruid {{ route.query.hruid }} en
|
||||
language {{ route.query.language }}. (Overschrijf dit)
|
||||
</main>
|
||||
<div class="main-container">
|
||||
<h1 class="title">{{ t("new-assignment") }}</h1>
|
||||
<v-card class="form-card">
|
||||
<v-form
|
||||
ref="form"
|
||||
class="form-container"
|
||||
validate-on="submit lazy"
|
||||
@submit.prevent="submitFormHandler"
|
||||
>
|
||||
<v-container class="step-container">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="assignmentTitle"
|
||||
:label="t('title')"
|
||||
:rules="assignmentTitleRules"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
clearable
|
||||
required
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
|
||||
<using-query-result
|
||||
:query-result="learningPathsQueryResults"
|
||||
v-slot="{ data }: { data: LearningPath[] }"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-combobox
|
||||
v-model="selectedLearningPath"
|
||||
:items="data"
|
||||
:label="t('choose-lp')"
|
||||
:rules="learningPathRules"
|
||||
variant="outlined"
|
||||
clearable
|
||||
hide-details
|
||||
density="compact"
|
||||
append-inner-icon="mdi-magnify"
|
||||
item-title="title"
|
||||
item-value="hruid"
|
||||
required
|
||||
:disabled="lpIsSelected"
|
||||
:filter="
|
||||
(item, query: string) => item.title.toLowerCase().includes(query.toLowerCase())
|
||||
"
|
||||
></v-combobox>
|
||||
</v-card-text>
|
||||
</using-query-result>
|
||||
|
||||
<using-query-result
|
||||
:query-result="classesQueryResults"
|
||||
v-slot="{ data }: { data: ClassesResponse }"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-combobox
|
||||
v-model="selectedClass"
|
||||
:items="data?.classes ?? []"
|
||||
:label="t('pick-class')"
|
||||
:rules="classRules"
|
||||
variant="outlined"
|
||||
clearable
|
||||
hide-details
|
||||
density="compact"
|
||||
append-inner-icon="mdi-magnify"
|
||||
item-title="displayName"
|
||||
item-value="id"
|
||||
required
|
||||
></v-combobox>
|
||||
</v-card-text>
|
||||
</using-query-result>
|
||||
|
||||
<GroupSelector
|
||||
:classId="selectedClass?.id"
|
||||
:groups="groups"
|
||||
@groupCreated="addGroupToList"
|
||||
/>
|
||||
|
||||
<!-- Counter for created groups -->
|
||||
<v-card-text v-if="groups.length">
|
||||
<strong>Created Groups: {{ groups.length }}</strong>
|
||||
</v-card-text>
|
||||
<DeadlineSelector v-model:deadline="deadline" />
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
v-model="description"
|
||||
:label="t('description')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
auto-grow
|
||||
rows="3"
|
||||
:rules="descriptionRules"
|
||||
></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-btn
|
||||
class="mt-2"
|
||||
color="secondary"
|
||||
type="submit"
|
||||
block
|
||||
>{{ t("submit") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
to="/user/assignment"
|
||||
color="grey"
|
||||
block
|
||||
>{{ t("cancel") }}
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 55%;
|
||||
/*padding: 1%;*/
|
||||
}
|
||||
|
||||
.form-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.step-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.form-card {
|
||||
width: 70%;
|
||||
padding: 1%;
|
||||
}
|
||||
|
||||
.step-container {
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
.form-card {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,75 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import auth from "@/services/auth/auth-service.ts";
|
||||
import { computed, type Ref, ref, watchEffect } from "vue";
|
||||
import StudentAssignment from "@/views/assignments/StudentAssignment.vue";
|
||||
import TeacherAssignment from "@/views/assignments/TeacherAssignment.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
|
||||
const role = auth.authState.activeRole;
|
||||
const isTeacher = computed(() => role === "teacher");
|
||||
|
||||
const route = useRoute();
|
||||
const classId = ref<string>(route.params.classId as string);
|
||||
const assignmentId = ref(Number(route.params.id));
|
||||
|
||||
function useGroupsWithProgress(
|
||||
groups: Ref<GroupDTO[]>,
|
||||
hruid: Ref<string>,
|
||||
language: Ref<string>,
|
||||
): { groupProgressMap: Map<number, number> } {
|
||||
const groupProgressMap: Map<number, number> = new Map<number, number>();
|
||||
|
||||
watchEffect(() => {
|
||||
// Clear existing entries to avoid stale data
|
||||
groupProgressMap.clear();
|
||||
|
||||
const lang = ref(language.value as Language);
|
||||
|
||||
groups.value.forEach((group) => {
|
||||
const groupKey = group.groupNumber;
|
||||
const forGroup = ref({
|
||||
forGroup: groupKey,
|
||||
assignmentNo: assignmentId,
|
||||
classId: classId,
|
||||
});
|
||||
|
||||
const query = useGetLearningPathQuery(hruid.value, lang, forGroup);
|
||||
|
||||
const data = query.data.value;
|
||||
|
||||
groupProgressMap.set(groupKey, data ? calculateProgress(data) : 0);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
groupProgressMap,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateProgress(lp: LearningPath): number {
|
||||
return ((lp.amountOfNodes - lp.amountOfNodesLeft) / lp.amountOfNodes) * 100;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
<TeacherAssignment
|
||||
:class-id="classId"
|
||||
:assignment-id="assignmentId"
|
||||
:use-groups-with-progress="useGroupsWithProgress"
|
||||
v-if="isTeacher"
|
||||
>
|
||||
</TeacherAssignment>
|
||||
<StudentAssignment
|
||||
:class-id="classId"
|
||||
:assignment-id="assignmentId"
|
||||
:use-groups-with-progress="useGroupsWithProgress"
|
||||
v-else
|
||||
>
|
||||
</StudentAssignment>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
167
frontend/src/views/assignments/StudentAssignment.vue
Normal file
167
frontend/src/views/assignments/StudentAssignment.vue
Normal file
|
@ -0,0 +1,167 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, type Ref } from "vue";
|
||||
import auth from "@/services/auth/auth-service.ts";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useAssignmentQuery } from "@/queries/assignments.ts";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import type { AssignmentResponse } from "@/controllers/assignments.ts";
|
||||
import { asyncComputed } from "@vueuse/core";
|
||||
import { useStudentsByUsernamesQuery } from "@/queries/students.ts";
|
||||
import { useGroupsQuery } from "@/queries/groups.ts";
|
||||
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
|
||||
const props = defineProps<{
|
||||
classId: string;
|
||||
assignmentId: number;
|
||||
useGroupsWithProgress: (
|
||||
groups: Ref<GroupDTO[]>,
|
||||
hruid: Ref<string>,
|
||||
language: Ref<Language>,
|
||||
) => { groupProgressMap: Map<number, number> };
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const language = ref<Language>(locale.value as Language);
|
||||
const learningPath = ref();
|
||||
// Get the user's username/id
|
||||
const username = asyncComputed(async () => {
|
||||
const user = await auth.loadUser();
|
||||
return user?.profile?.preferred_username ?? undefined;
|
||||
});
|
||||
|
||||
const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId);
|
||||
learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath;
|
||||
|
||||
const submitted = ref(false); //TODO: update by fetching submissions and check if group submitted
|
||||
|
||||
const lpQueryResult = useGetLearningPathQuery(
|
||||
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
|
||||
computed(() => language.value),
|
||||
);
|
||||
|
||||
const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true);
|
||||
const group = computed(() =>
|
||||
groupsQueryResult?.data.value?.groups.find((group) =>
|
||||
group.members?.some((m) => m.username === username.value),
|
||||
),
|
||||
);
|
||||
|
||||
const _groupArray = computed(() => (group.value ? [group.value] : []));
|
||||
const progressValue = ref(0);
|
||||
/* Crashes right now cause api data has inexistent hruid TODO: uncomment later and use it in progress bar
|
||||
Const {groupProgressMap} = props.useGroupsWithProgress(
|
||||
groupArray,
|
||||
learningPath,
|
||||
language
|
||||
);
|
||||
*/
|
||||
|
||||
// Assuming group.value.members is a list of usernames TODO: case when it's StudentDTO's
|
||||
const studentQueries = useStudentsByUsernamesQuery(() => group.value?.members as string[]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<using-query-result
|
||||
:query-result="assignmentQueryResult"
|
||||
v-slot="{ data }: { data: AssignmentResponse }"
|
||||
>
|
||||
<v-card
|
||||
v-if="data"
|
||||
class="assignment-card"
|
||||
>
|
||||
<div class="top-buttons">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
class="back-btn"
|
||||
to="/user/assignment"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-chip
|
||||
v-if="submitted"
|
||||
class="ma-2 top-right-btn"
|
||||
label
|
||||
color="success"
|
||||
>
|
||||
{{ t("submitted") }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-card-title class="text-h4 assignmentTopTitle">{{ data.assignment.title }}</v-card-title>
|
||||
|
||||
<v-card-subtitle class="subtitle-section">
|
||||
<using-query-result
|
||||
:query-result="lpQueryResult"
|
||||
v-slot="{ data: lpData }"
|
||||
>
|
||||
<v-btn
|
||||
v-if="lpData"
|
||||
:to="`/learningPath/${lpData.hruid}/${language}/${lpData.startNode.learningobjectHruid}?forGroup=${group?.groupNumber}&assignmentNo=${assignmentId}&classId=${classId}`"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
>
|
||||
{{ t("learning-path") }}
|
||||
</v-btn>
|
||||
</using-query-result>
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text class="description">
|
||||
{{ data.assignment.description }}
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row
|
||||
align="center"
|
||||
no-gutters
|
||||
>
|
||||
<v-col cols="auto">
|
||||
<span class="progress-label">{{ t("progress") + ": " }}</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-progress-linear
|
||||
:model-value="progressValue"
|
||||
color="primary"
|
||||
height="20"
|
||||
class="progress-bar"
|
||||
>
|
||||
<template v-slot:default="{ value }">
|
||||
<strong>{{ Math.ceil(value) }}%</strong>
|
||||
</template>
|
||||
</v-progress-linear>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text class="group-section">
|
||||
<h3>{{ t("group") }}</h3>
|
||||
<div v-if="studentQueries">
|
||||
<ul>
|
||||
<li
|
||||
v-for="student in group?.members"
|
||||
:key="student.username"
|
||||
>
|
||||
{{ student.firstName + " " + student.lastName }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</using-query-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import "@/assets/assignment.css";
|
||||
|
||||
.progress-label {
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 40%;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +0,0 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
234
frontend/src/views/assignments/TeacherAssignment.vue
Normal file
234
frontend/src/views/assignments/TeacherAssignment.vue
Normal file
|
@ -0,0 +1,234 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, type Ref, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useAssignmentQuery, useDeleteAssignmentMutation } from "@/queries/assignments.ts";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import { useGroupsQuery } from "@/queries/groups.ts";
|
||||
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import type { AssignmentResponse } from "@/controllers/assignments.ts";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
|
||||
const props = defineProps<{
|
||||
classId: string;
|
||||
assignmentId: number;
|
||||
useGroupsWithProgress: (
|
||||
groups: Ref<GroupDTO[]>,
|
||||
hruid: Ref<string>,
|
||||
language: Ref<Language>,
|
||||
) => { groupProgressMap: Map<number, number> };
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const language = computed(() => locale.value);
|
||||
const groups = ref();
|
||||
const learningPath = ref();
|
||||
|
||||
const assignmentQueryResult = useAssignmentQuery(() => props.classId, props.assignmentId);
|
||||
learningPath.value = assignmentQueryResult.data.value?.assignment?.learningPath;
|
||||
// Get learning path object
|
||||
const lpQueryResult = useGetLearningPathQuery(
|
||||
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
|
||||
computed(() => language.value as Language),
|
||||
);
|
||||
|
||||
// Get all the groups withing the assignment
|
||||
const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true);
|
||||
groups.value = groupsQueryResult.data.value?.groups;
|
||||
|
||||
/* Crashes right now cause api data has inexistent hruid TODO: uncomment later and use it in progress bar
|
||||
Const {groupProgressMap} = props.useGroupsWithProgress(
|
||||
groups,
|
||||
learningPath,
|
||||
language
|
||||
);
|
||||
*/
|
||||
|
||||
const allGroups = computed(() => {
|
||||
const groups = groupsQueryResult.data.value?.groups;
|
||||
if (!groups) return [];
|
||||
|
||||
return groups.map((group) => ({
|
||||
name: `${t("group")} ${group.groupNumber}`,
|
||||
progress: 0, //GroupProgressMap[group.groupNumber],
|
||||
members: group.members,
|
||||
submitted: false, //TODO: fetch from submission
|
||||
}));
|
||||
});
|
||||
|
||||
const dialog = ref(false);
|
||||
const selectedGroup = ref({});
|
||||
|
||||
function openGroupDetails(group): void {
|
||||
selectedGroup.value = group;
|
||||
dialog.value = true;
|
||||
}
|
||||
|
||||
const headers = computed(() => [
|
||||
{ title: t("group"), align: "start", key: "name" },
|
||||
{ title: t("progress"), align: "center", key: "progress" },
|
||||
{ title: t("submission"), align: "center", key: "submission" },
|
||||
]);
|
||||
|
||||
const { mutate } = useDeleteAssignmentMutation();
|
||||
|
||||
async function deleteAssignment(num: number, clsId: string): Promise<void> {
|
||||
mutate(
|
||||
{ cid: clsId, an: num },
|
||||
{
|
||||
onSuccess: () => {
|
||||
window.location.href = "/user/assignment";
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<using-query-result
|
||||
:query-result="assignmentQueryResult"
|
||||
v-slot="{ data }: { data: AssignmentResponse }"
|
||||
>
|
||||
<v-card
|
||||
v-if="data"
|
||||
class="assignment-card"
|
||||
>
|
||||
<div class="top-buttons">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
class="back-btn"
|
||||
to="/user/assignment"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
class="top-right-btn"
|
||||
@click="deleteAssignment(data.assignment.id, data.assignment.within)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-card-title class="text-h4 assignmentTopTitle">{{ data.assignment.title }}</v-card-title>
|
||||
<v-card-subtitle class="subtitle-section">
|
||||
<using-query-result
|
||||
:query-result="lpQueryResult"
|
||||
v-slot="{ data: lpData }"
|
||||
>
|
||||
<v-btn
|
||||
v-if="lpData"
|
||||
:to="`/learningPath/${lpData.hruid}/${language}/${lpData.startNode.learningobjectHruid}?assignmentNo=${assignmentId}&classId=${classId}`"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
>
|
||||
{{ t("learning-path") }}
|
||||
</v-btn>
|
||||
</using-query-result>
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text class="description">
|
||||
{{ data.assignment.description }}
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text class="group-section">
|
||||
<h3>{{ t("groups") }}</h3>
|
||||
<div class="table-scroll">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="allGroups"
|
||||
item-key="id"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template #[`item.name`]="{ item }">
|
||||
<v-btn
|
||||
@click="openGroupDetails(item)"
|
||||
variant="text"
|
||||
color="primary"
|
||||
>
|
||||
{{ item.name }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template #[`item.progress`]="{ item }">
|
||||
<v-progress-linear
|
||||
:model-value="item.progress"
|
||||
color="blue-grey"
|
||||
height="25"
|
||||
>
|
||||
<template v-slot:default="{ value }">
|
||||
<strong>{{ Math.ceil(value) }}%</strong>
|
||||
</template>
|
||||
</v-progress-linear>
|
||||
</template>
|
||||
|
||||
<template #[`item.submission`]="{ item }">
|
||||
<v-btn
|
||||
:to="item.submitted ? `${props.assignmentId}/submissions/` : undefined"
|
||||
:color="item.submitted ? 'green' : 'red'"
|
||||
variant="text"
|
||||
class="text-capitalize"
|
||||
>
|
||||
{{ item.submitted ? t("see-submission") : t("no-submission") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="50%"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">{{ t("members") }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(member, index) in selectedGroup.members"
|
||||
:key="index"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title
|
||||
>{{ member.firstName + " " + member.lastName }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="dialog = false"
|
||||
>Close</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<!--
|
||||
<v-card-actions class="justify-end">
|
||||
<v-btn
|
||||
size="large"
|
||||
color="success"
|
||||
variant="text"
|
||||
>
|
||||
{{ t("view-submissions") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
-->
|
||||
</v-card>
|
||||
</using-query-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import "@/assets/assignment.css";
|
||||
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +0,0 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
188
frontend/src/views/assignments/UserAssignments.vue
Normal file
188
frontend/src/views/assignments/UserAssignments.vue
Normal file
|
@ -0,0 +1,188 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import auth from "@/services/auth/auth-service.ts";
|
||||
import { useTeacherClassesQuery } from "@/queries/teachers.ts";
|
||||
import { useStudentClassesQuery } from "@/queries/students.ts";
|
||||
import { ClassController } from "@/controllers/classes.ts";
|
||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import { asyncComputed } from "@vueuse/core";
|
||||
import { useDeleteAssignmentMutation } from "@/queries/assignments.ts";
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const role = ref(auth.authState.activeRole);
|
||||
const username = ref<string>("");
|
||||
|
||||
const isTeacher = computed(() => role.value === "teacher");
|
||||
|
||||
// Fetch and store all the teacher's classes
|
||||
let classesQueryResults = undefined;
|
||||
|
||||
if (isTeacher.value) {
|
||||
classesQueryResults = useTeacherClassesQuery(username, true);
|
||||
} else {
|
||||
classesQueryResults = useStudentClassesQuery(username, true);
|
||||
}
|
||||
|
||||
//TODO: remove later
|
||||
const classController = new ClassController();
|
||||
|
||||
//TODO: replace by query that fetches all user's assignment
|
||||
const assignments = asyncComputed(async () => {
|
||||
const classes = classesQueryResults?.data?.value?.classes;
|
||||
if (!classes) return [];
|
||||
const result = await Promise.all(
|
||||
(classes as ClassDTO[]).map(async (cls) => {
|
||||
const { assignments } = await classController.getAssignments(cls.id);
|
||||
return assignments.map((a) => ({
|
||||
id: a.id,
|
||||
class: cls,
|
||||
title: a.title,
|
||||
description: a.description,
|
||||
learningPath: a.learningPath,
|
||||
language: a.language,
|
||||
groups: a.groups,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
return result.flat();
|
||||
}, []);
|
||||
|
||||
async function goToCreateAssignment(): Promise<void> {
|
||||
await router.push("/assignment/create");
|
||||
}
|
||||
|
||||
async function goToAssignmentDetails(id: number, clsId: string): Promise<void> {
|
||||
await router.push(`/assignment/${clsId}/${id}`);
|
||||
}
|
||||
|
||||
const { mutate, data, isSuccess } = useDeleteAssignmentMutation();
|
||||
|
||||
watch([isSuccess, data], async ([success, oldData]) => {
|
||||
if (success && oldData?.assignment) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
async function goToDeleteAssignment(num: number, clsId: string): Promise<void> {
|
||||
mutate({ cid: clsId, an: num });
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const user = await auth.loadUser();
|
||||
username.value = user?.profile?.preferred_username ?? "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="assignments-container">
|
||||
<h1>{{ t("assignments") }}</h1>
|
||||
|
||||
<v-btn
|
||||
v-if="isTeacher"
|
||||
color="primary"
|
||||
class="mb-4 center-btn"
|
||||
@click="goToCreateAssignment"
|
||||
>
|
||||
{{ t("new-assignment") }}
|
||||
</v-btn>
|
||||
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="assignment in assignments"
|
||||
:key="assignment.id"
|
||||
cols="12"
|
||||
>
|
||||
<v-card class="assignment-card">
|
||||
<div class="top-content">
|
||||
<div class="assignment-title">{{ assignment.title }}</div>
|
||||
<div class="assignment-class">
|
||||
{{ t("class") }}:
|
||||
<span class="class-name">
|
||||
{{ assignment.class.displayName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="button-row">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="goToAssignmentDetails(assignment.id, assignment.class.id)"
|
||||
>
|
||||
{{ t("view-assignment") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="isTeacher"
|
||||
color="red"
|
||||
variant="text"
|
||||
@click="goToDeleteAssignment(assignment.id, assignment.class.id)"
|
||||
>
|
||||
{{ t("delete") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.assignments-container {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 2% 4%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.center-btn {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.assignment-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.top-content {
|
||||
margin-bottom: 1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.assignment-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.assignment-class {
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.class-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +0,0 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
64
frontend/tests/controllers/assignments-controller.test.ts
Normal file
64
frontend/tests/controllers/assignments-controller.test.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { AssignmentController } from "../../src/controllers/assignments";
|
||||
|
||||
describe("AssignmentController Tests", () => {
|
||||
let controller: AssignmentController;
|
||||
|
||||
beforeEach(() => {
|
||||
controller = new AssignmentController("8764b861-90a6-42e5-9732-c0d9eb2f55f9"); // Example class ID
|
||||
});
|
||||
|
||||
it("should fetch all assignments", async () => {
|
||||
const result = await controller.getAll(true);
|
||||
expect(result).toHaveProperty("assignments");
|
||||
expect(Array.isArray(result.assignments)).toBe(true);
|
||||
expect(result.assignments.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should fetch an assignment by number", async () => {
|
||||
const assignmentNumber = 21000; // Example assignment ID
|
||||
const result = await controller.getByNumber(assignmentNumber);
|
||||
expect(result).toHaveProperty("assignment");
|
||||
expect(result.assignment).toHaveProperty("id", assignmentNumber);
|
||||
});
|
||||
|
||||
it("should update an existing assignment", async () => {
|
||||
const assignmentNumber = 21000;
|
||||
const updatedData = { title: "Updated Assignment Title" };
|
||||
const result = await controller.updateAssignment(assignmentNumber, updatedData);
|
||||
expect(result).toHaveProperty("assignment");
|
||||
expect(result.assignment).toHaveProperty("id", assignmentNumber);
|
||||
expect(result.assignment).toHaveProperty("title", updatedData.title);
|
||||
});
|
||||
|
||||
it("should fetch submissions for an assignment", async () => {
|
||||
const assignmentNumber = 21000;
|
||||
const result = await controller.getSubmissions(assignmentNumber, true);
|
||||
expect(result).toHaveProperty("submissions");
|
||||
expect(Array.isArray(result.submissions)).toBe(true);
|
||||
});
|
||||
|
||||
it("should fetch questions for an assignment", async () => {
|
||||
const assignmentNumber = 21000;
|
||||
const result = await controller.getQuestions(assignmentNumber, true);
|
||||
expect(result).toHaveProperty("questions");
|
||||
expect(Array.isArray(result.questions)).toBe(true);
|
||||
});
|
||||
|
||||
it("should fetch groups for an assignment", async () => {
|
||||
const assignmentNumber = 21000;
|
||||
const result = await controller.getGroups(assignmentNumber, true);
|
||||
expect(result).toHaveProperty("groups");
|
||||
expect(Array.isArray(result.groups)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle fetching a non-existent assignment", async () => {
|
||||
const assignmentNumber = 99999; // Non-existent assignment ID
|
||||
await expect(controller.getByNumber(assignmentNumber)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should handle deleting a non-existent assignment", async () => {
|
||||
const assignmentNumber = 99999; // Non-existent assignment ID
|
||||
await expect(controller.deleteAssignment(assignmentNumber)).rejects.toThrow();
|
||||
});
|
||||
});
|
10
frontend/tests/controllers/classes-controller.test.ts
Normal file
10
frontend/tests/controllers/classes-controller.test.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { ClassController } from "../../src/controllers/classes";
|
||||
|
||||
describe("Test controller classes", () => {
|
||||
it("Get classes", async () => {
|
||||
const controller = new ClassController();
|
||||
const data = await controller.getAll(true);
|
||||
expect(data.classes).to.have.length.greaterThan(0);
|
||||
});
|
||||
});
|
13
frontend/tests/controllers/groups.controller.test.ts
Normal file
13
frontend/tests/controllers/groups.controller.test.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { GroupController } from "../../src/controllers/groups";
|
||||
|
||||
describe("Test controller groups", () => {
|
||||
it("Get groups", async () => {
|
||||
const classId = "8764b861-90a6-42e5-9732-c0d9eb2f55f9";
|
||||
const assignmentNumber = 21000;
|
||||
|
||||
const controller = new GroupController(classId, assignmentNumber);
|
||||
const data = await controller.getAll(true);
|
||||
expect(data.groups).to.have.length.greaterThan(0);
|
||||
});
|
||||
});
|
21
frontend/tests/controllers/learning-paths-controller.test.ts
Normal file
21
frontend/tests/controllers/learning-paths-controller.test.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { LearningPathController } from "../../src/controllers/learning-paths";
|
||||
import { Language } from "../../src/data-objects/language";
|
||||
|
||||
describe("Test controller learning paths", () => {
|
||||
let controller: LearningPathController;
|
||||
|
||||
beforeEach(async () => {
|
||||
controller = new LearningPathController();
|
||||
});
|
||||
|
||||
it("Can search for learning paths", async () => {
|
||||
const data = await controller.search("kiks", Language.Dutch);
|
||||
expect(data).to.have.length.greaterThan(0);
|
||||
});
|
||||
|
||||
it("Can get learning path by id", async () => {
|
||||
const data = await controller.getAllByTheme("kiks");
|
||||
expect(data).to.have.length.greaterThan(0);
|
||||
});
|
||||
});
|
|
@ -1,19 +1,39 @@
|
|||
import { StudentController } from "../../src/controllers/students";
|
||||
import { expect, it, describe, afterAll, beforeAll } from "vitest";
|
||||
import { setup, teardown } from "../setup-backend.js";
|
||||
import { beforeEach, describe, expect, it, test } from "vitest";
|
||||
|
||||
describe("Test controller students", () => {
|
||||
beforeAll(async () => {
|
||||
await setup();
|
||||
});
|
||||
let controller: StudentController;
|
||||
|
||||
afterAll(async () => {
|
||||
await teardown();
|
||||
beforeEach(async () => {
|
||||
controller = new StudentController();
|
||||
});
|
||||
|
||||
it("Get students", async () => {
|
||||
const controller = new StudentController();
|
||||
const data = await controller.getAll(true);
|
||||
expect(data.students).to.have.length.greaterThan(0);
|
||||
});
|
||||
|
||||
it("Get student by username", async () => {
|
||||
const username = "testleerling1";
|
||||
const data = await controller.getByUsername(username);
|
||||
expect(data.student.username).to.equal(username);
|
||||
});
|
||||
});
|
||||
|
||||
const controller = new StudentController();
|
||||
|
||||
test.each([
|
||||
{ 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" },
|
||||
// Makes sure when logged in as leerling1, there exists a corresponding user
|
||||
{ username: "testleerling1", firstName: "Gerald", lastName: "Schmittinger" },
|
||||
])("Get classes of student", async (student) => {
|
||||
const data = await controller.getClasses(student.username, true);
|
||||
expect(data.classes).to.have.length.greaterThan(0);
|
||||
});
|
||||
|
|
15
frontend/tests/controllers/submissions-controller.test.ts
Normal file
15
frontend/tests/controllers/submissions-controller.test.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { SubmissionController } from "../../src/controllers/submissions";
|
||||
import { Language } from "../../src/data-objects/language";
|
||||
|
||||
describe("Test controller submissions", () => {
|
||||
it("Get submission by number", async () => {
|
||||
const hruid = "id03";
|
||||
const classId = "8764b861-90a6-42e5-9732-c0d9eb2f55f9";
|
||||
const controller = new SubmissionController(hruid);
|
||||
|
||||
const data = await controller.getByNumber(Language.English, 1, classId, 1, 1, 1);
|
||||
|
||||
expect(data.submission).to.have.property("submissionNumber");
|
||||
});
|
||||
});
|
53
frontend/tests/controllers/teacher-controller.test.ts
Normal file
53
frontend/tests/controllers/teacher-controller.test.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { TeacherController } from "../../src/controllers/teachers";
|
||||
|
||||
describe("Test controller teachers", () => {
|
||||
let controller: TeacherController;
|
||||
|
||||
beforeEach(async () => {
|
||||
controller = new TeacherController();
|
||||
});
|
||||
|
||||
it("Get all teachers", async () => {
|
||||
const data = await controller.getAll(true);
|
||||
expect(data.teachers).to.have.length.greaterThan(0);
|
||||
expect(data.teachers[0]).to.have.property("username");
|
||||
expect(data.teachers[0]).to.have.property("firstName");
|
||||
expect(data.teachers[0]).to.have.property("lastName");
|
||||
});
|
||||
|
||||
it("Get teacher by username", async () => {
|
||||
const username = "testleerkracht1";
|
||||
const data = await controller.getByUsername(username);
|
||||
expect(data.teacher.username).to.equal(username);
|
||||
expect(data.teacher).to.have.property("firstName");
|
||||
expect(data.teacher).to.have.property("lastName");
|
||||
});
|
||||
|
||||
it("Get teacher by non-existent username", async () => {
|
||||
const username = "nonexistentuser";
|
||||
await expect(controller.getByUsername(username)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("Handle deletion of non-existent teacher", async () => {
|
||||
const username = "nonexistentuser";
|
||||
await expect(controller.deleteTeacher(username)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("Get classes for a teacher", async () => {
|
||||
const username = "testleerkracht1";
|
||||
const data = await controller.getClasses(username, true);
|
||||
expect(data.classes).to.have.length.greaterThan(0);
|
||||
expect(data.classes[0]).to.have.property("id");
|
||||
expect(data.classes[0]).to.have.property("displayName");
|
||||
});
|
||||
|
||||
it("Get students for a teacher", async () => {
|
||||
const username = "testleerkracht1";
|
||||
const data = await controller.getStudents(username, true);
|
||||
expect(data.students).to.have.length.greaterThan(0);
|
||||
expect(data.students[0]).to.have.property("username");
|
||||
expect(data.students[0]).to.have.property("firstName");
|
||||
expect(data.students[0]).to.have.property("lastName");
|
||||
});
|
||||
});
|
48
frontend/tests/controllers/theme-controller.test.ts
Normal file
48
frontend/tests/controllers/theme-controller.test.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { ThemeController } from "../../src/controllers/themes";
|
||||
|
||||
describe("ThemeController Tests", () => {
|
||||
let controller: ThemeController;
|
||||
|
||||
beforeEach(() => {
|
||||
controller = new ThemeController();
|
||||
});
|
||||
|
||||
it("should fetch all themes", async () => {
|
||||
const result = await controller.getAll();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
expect(result[0]).toHaveProperty("key");
|
||||
expect(result[0]).toHaveProperty("title");
|
||||
expect(result[0]).toHaveProperty("description");
|
||||
expect(result[0]).toHaveProperty("image");
|
||||
});
|
||||
|
||||
it("should fetch all themes filtered by language", async () => {
|
||||
const language = "en";
|
||||
const result = await controller.getAll(language);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
result.forEach((theme) => {
|
||||
expect(theme).toHaveProperty("key");
|
||||
expect(theme).toHaveProperty("title");
|
||||
expect(theme).toHaveProperty("description");
|
||||
expect(theme).toHaveProperty("image");
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch HRUIDs by theme key", async () => {
|
||||
const themeKey = "kiks";
|
||||
const result = await controller.getHruidsByKey(themeKey);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
result.forEach((hruid) => {
|
||||
expect(typeof hruid).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle fetching HRUIDs for a non-existent theme key", async () => {
|
||||
const themeKey = "nonexistent";
|
||||
await expect(controller.getHruidsByKey(themeKey)).rejects.toThrow();
|
||||
});
|
||||
});
|
82
frontend/tests/utils/assingment-rules.test.ts
Normal file
82
frontend/tests/utils/assingment-rules.test.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assignmentTitleRules,
|
||||
classRules,
|
||||
deadlineRules,
|
||||
descriptionRules,
|
||||
learningPathRules,
|
||||
} from "../../src/utils/assignment-rules";
|
||||
|
||||
describe("Validation Rules", () => {
|
||||
describe("assignmentTitleRules", () => {
|
||||
it("should return true for a valid title", () => {
|
||||
const result = assignmentTitleRules[0]("Valid Title");
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return an error message for an empty title", () => {
|
||||
const result = assignmentTitleRules[0]("");
|
||||
expect(result).toBe("Title cannot be empty.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("learningPathRules", () => {
|
||||
it("should return true for a valid learning path", () => {
|
||||
const result = learningPathRules[0]({ hruid: "123", title: "Path Title" });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return an error message for an invalid learning path", () => {
|
||||
const result = learningPathRules[0]({ hruid: "", title: "" });
|
||||
expect(result).toBe("You must select a learning path.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("classRules", () => {
|
||||
it("should return true for a valid class", () => {
|
||||
const result = classRules[0]("Class 1");
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return an error message for an empty class", () => {
|
||||
const result = classRules[0]("");
|
||||
expect(result).toBe("You must select at least one class.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deadlineRules", () => {
|
||||
it("should return true for a valid future deadline", () => {
|
||||
const futureDate = new Date(Date.now() + 1000 * 60 * 60).toISOString();
|
||||
const result = deadlineRules[0](futureDate);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return an error message for a past deadline", () => {
|
||||
const pastDate = new Date(Date.now() - 1000 * 60 * 60).toISOString();
|
||||
const result = deadlineRules[0](pastDate);
|
||||
expect(result).toBe("The deadline must be in the future.");
|
||||
});
|
||||
|
||||
it("should return an error message for an invalid date", () => {
|
||||
const result = deadlineRules[0]("invalid-date");
|
||||
expect(result).toBe("Invalid date or time.");
|
||||
});
|
||||
|
||||
it("should return an error message for an empty deadline", () => {
|
||||
const result = deadlineRules[0]("");
|
||||
expect(result).toBe("You must set a deadline.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("descriptionRules", () => {
|
||||
it("should return true for a valid description", () => {
|
||||
const result = descriptionRules[0]("This is a valid description.");
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return an error message for an empty description", () => {
|
||||
const result = descriptionRules[0]("");
|
||||
expect(result).toBe("Description cannot be empty.");
|
||||
});
|
||||
});
|
||||
});
|
68
frontend/tests/utils/deep-equals.test.ts
Normal file
68
frontend/tests/utils/deep-equals.test.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { deepEquals } from "../../src/utils/deep-equals";
|
||||
|
||||
describe("deepEquals", () => {
|
||||
it("should return true for identical primitive values", () => {
|
||||
expect(deepEquals(1, 1)).toBe(true);
|
||||
expect(deepEquals("test", "test")).toBe(true);
|
||||
expect(deepEquals(true, true)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for different primitive values", () => {
|
||||
expect(deepEquals(1, 2)).toBe(false);
|
||||
expect(deepEquals("test", "other")).toBe(false);
|
||||
expect(deepEquals(true, false)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for identical objects", () => {
|
||||
const obj1 = { a: 1, b: { c: 2 } };
|
||||
const obj2 = { a: 1, b: { c: 2 } };
|
||||
expect(deepEquals(obj1, obj2)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for different objects", () => {
|
||||
const obj1 = { a: 1, b: { c: 2 } };
|
||||
const obj2 = { a: 1, b: { c: 3 } };
|
||||
expect(deepEquals(obj1, obj2)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for identical arrays", () => {
|
||||
const arr1 = [1, 2, [3, 4]];
|
||||
const arr2 = [1, 2, [3, 4]];
|
||||
expect(deepEquals(arr1, arr2)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for different arrays", () => {
|
||||
const arr1 = [1, 2, [3, 4]];
|
||||
const arr2 = [1, 2, [3, 5]];
|
||||
expect(deepEquals(arr1, arr2)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for objects and arrays compared", () => {
|
||||
expect(deepEquals({ a: 1 }, [1])).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for null compared to null", () => {
|
||||
expect(deepEquals(null, null)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for null compared to an object", () => {
|
||||
expect(deepEquals(null, {})).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for undefined compared to null", () => {
|
||||
expect(deepEquals(undefined, null)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for deeply nested identical structures", () => {
|
||||
const obj1 = { a: [1, { b: 2, c: [3, 4] }] };
|
||||
const obj2 = { a: [1, { b: 2, c: [3, 4] }] };
|
||||
expect(deepEquals(obj1, obj2)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for deeply nested different structures", () => {
|
||||
const obj1 = { a: [1, { b: 2, c: [3, 4] }] };
|
||||
const obj2 = { a: [1, { b: 2, c: [3, 5] }] };
|
||||
expect(deepEquals(obj1, obj2)).toBe(false);
|
||||
});
|
||||
});
|
|
@ -9,6 +9,43 @@ export default mergeConfig(
|
|||
environment: "jsdom",
|
||||
exclude: [...configDefaults.exclude, "e2e/**"],
|
||||
root: fileURLToPath(new URL("./", import.meta.url)),
|
||||
testTimeout: 100000,
|
||||
coverage: {
|
||||
reporter: ["text", "json-summary", "json"],
|
||||
// If you want a coverage reports even if your tests are failing, include the reportOnFailure option
|
||||
reportOnFailure: true,
|
||||
exclude: [
|
||||
"**/*config*",
|
||||
"**/tests/**",
|
||||
"playwright-report/**",
|
||||
"**/dist/**",
|
||||
"**/e2e/**",
|
||||
"**/*config*",
|
||||
"**/node_modules/**",
|
||||
|
||||
"src/main.ts",
|
||||
"src/router/index.ts",
|
||||
"src/utils/constants.ts",
|
||||
|
||||
"**/*.d.ts",
|
||||
|
||||
"src/**/*.vue",
|
||||
"src/assets/**",
|
||||
"src/i18n/**",
|
||||
|
||||
"src/data-objects/**",
|
||||
"src/exception/**", // TODO Might be useful to test later
|
||||
"src/queries/**", // TODO Might be useful to test later
|
||||
"src/views/learning-paths/gift-adapters/**", // TODO Might be useful to test later
|
||||
"src/services/auth/**", // TODO Might be useful to test later
|
||||
],
|
||||
thresholds: {
|
||||
lines: 50,
|
||||
branches: 50,
|
||||
functions: 50,
|
||||
statements: 50,
|
||||
},
|
||||
},
|
||||
|
||||
/*
|
||||
* The test-backend server can be started for each test-file individually using `beforeAll(() => setup())`,
|
||||
|
@ -16,6 +53,7 @@ export default mergeConfig(
|
|||
globalSetup: ["./tests/setup-backend.ts"],
|
||||
* In this project, the backend server is started for each test-file individually.
|
||||
*/
|
||||
globalSetup: ["./tests/setup-backend.ts"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
9430
package-lock.json
generated
9430
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -13,7 +13,8 @@
|
|||
"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"
|
||||
"test:unit": "npm run test:unit --workspace=backend --workspace=frontend",
|
||||
"test:coverage": "npm run test:coverage --workspace=backend --workspace=frontend"
|
||||
},
|
||||
"workspaces": [
|
||||
"backend",
|
||||
|
@ -41,5 +42,8 @@
|
|||
"eslint-config-prettier": "^10.0.1",
|
||||
"jiti": "^2.4.2",
|
||||
"typescript-eslint": "^8.24.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"swagger": "^0.7.5"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue