Merge branch 'dev' into test/e2e
This commit is contained in:
commit
e12c057ebb
112 changed files with 5304 additions and 1422 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
### Dwengo ###
|
### Dwengo ###
|
||||||
|
|
||||||
DWENGO_PORT=3000
|
DWENGO_PORT=3000
|
||||||
|
DWENGO_RUN_MODE=test
|
||||||
|
|
||||||
DWENGO_DB_NAME=":memory:"
|
DWENGO_DB_NAME=":memory:"
|
||||||
DWENGO_DB_UPDATE=true
|
DWENGO_DB_UPDATE=true
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
"jwks-rsa": "^3.1.0",
|
"jwks-rsa": "^3.1.0",
|
||||||
"loki-logger-ts": "^1.0.2",
|
"loki-logger-ts": "^1.0.2",
|
||||||
"marked": "^15.0.7",
|
"marked": "^15.0.7",
|
||||||
|
"nanoid": "^5.1.5",
|
||||||
"response-time": "^2.3.3",
|
"response-time": "^2.3.3",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { UnauthorizedException } from '../exceptions/unauthorized-exception.js';
|
import { UnauthorizedException } from '../exceptions/unauthorized-exception.js';
|
||||||
import { getLogger } from '../logging/initalize.js';
|
import { getLogger } from '../logging/initalize.js';
|
||||||
import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js';
|
import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js';
|
||||||
import { createOrUpdateStudent } from '../services/students.js';
|
|
||||||
import { createOrUpdateTeacher } from '../services/teachers.js';
|
|
||||||
import { envVars, getEnvVar } from '../util/envVars.js';
|
import { envVars, getEnvVar } from '../util/envVars.js';
|
||||||
import { Response } from 'express';
|
import { createOrUpdateStudent } from '../services/students.js';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { createOrUpdateTeacher } from '../services/teachers.js';
|
||||||
|
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||||
|
|
||||||
interface FrontendIdpConfig {
|
interface FrontendIdpConfig {
|
||||||
authority: string;
|
authority: string;
|
||||||
|
|
@ -40,6 +41,10 @@ export function getFrontendAuthConfig(): FrontendAuthConfig {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleGetFrontendAuthConfig(_req: Request, res: Response): void {
|
||||||
|
res.json(getFrontendAuthConfig());
|
||||||
|
}
|
||||||
|
|
||||||
export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise<void> {
|
export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||||
const auth = req.auth;
|
const auth = req.auth;
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
|
|
@ -51,7 +56,7 @@ export async function postHelloHandler(req: AuthenticatedRequest, res: Response)
|
||||||
firstName: auth.firstName ?? '',
|
firstName: auth.firstName ?? '',
|
||||||
lastName: auth.lastName ?? '',
|
lastName: auth.lastName ?? '',
|
||||||
};
|
};
|
||||||
if (auth.accountType === 'student') {
|
if (auth.accountType === AccountType.Student) {
|
||||||
await createOrUpdateStudent(userData);
|
await createOrUpdateStudent(userData);
|
||||||
logger.debug(`Synchronized student ${userData.username} with IDP`);
|
logger.debug(`Synchronized student ${userData.username} with IDP`);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ export async function createStudentRequestHandler(req: Request, res: Response):
|
||||||
const classId = req.body.classId;
|
const classId = req.body.classId;
|
||||||
requireFields({ username, classId });
|
requireFields({ username, classId });
|
||||||
|
|
||||||
const request = await createClassJoinRequest(username, classId);
|
const request = await createClassJoinRequest(username, classId.toUpperCase());
|
||||||
res.json({ request });
|
res.json({ request });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,11 @@ export async function getAllSubmissionsHandler(req: Request, res: Response): Pro
|
||||||
|
|
||||||
// TODO: gerald moet nog dingen toevoegen aan de databank voor dat dit gefinaliseerd kan worden
|
// TODO: gerald moet nog dingen toevoegen aan de databank voor dat dit gefinaliseerd kan worden
|
||||||
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
|
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const submitter = req.body.submitter;
|
||||||
|
const usernameSubmitter = req.body.submitter.username;
|
||||||
|
const group = req.body.group;
|
||||||
|
requireFields({ group, submitter, usernameSubmitter });
|
||||||
|
|
||||||
const submissionDTO = req.body as SubmissionDTO;
|
const submissionDTO = req.body as SubmissionDTO;
|
||||||
const submission = await createSubmission(submissionDTO);
|
const submission = await createSubmission(submissionDTO);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Request, Response } from 'express';
|
||||||
import { requireFields } from './error-helper.js';
|
import { requireFields } from './error-helper.js';
|
||||||
import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations.js';
|
import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations.js';
|
||||||
import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation';
|
import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation';
|
||||||
|
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||||
|
|
||||||
export async function getAllInvitationsHandler(req: Request, res: Response): Promise<void> {
|
export async function getAllInvitationsHandler(req: Request, res: Response): Promise<void> {
|
||||||
const username = req.params.username;
|
const username = req.params.username;
|
||||||
|
|
@ -30,6 +31,10 @@ export async function createInvitationHandler(req: Request, res: Response): Prom
|
||||||
const classId = req.body.class;
|
const classId = req.body.class;
|
||||||
requireFields({ sender, receiver, classId });
|
requireFields({ sender, receiver, classId });
|
||||||
|
|
||||||
|
if (sender === receiver) {
|
||||||
|
throw new ConflictException('Cannot send an invitation to yourself');
|
||||||
|
}
|
||||||
|
|
||||||
const data = req.body as TeacherInvitationData;
|
const data = req.body as TeacherInvitationData;
|
||||||
const invitation = await createInvitation(data);
|
const invitation = await createInvitation(data);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
getJoinRequestsByClass,
|
getJoinRequestsByClass,
|
||||||
getStudentsByTeacher,
|
getStudentsByTeacher,
|
||||||
getTeacher,
|
getTeacher,
|
||||||
getTeacherQuestions,
|
|
||||||
updateClassJoinRequestStatus,
|
updateClassJoinRequestStatus,
|
||||||
} from '../services/teachers.js';
|
} from '../services/teachers.js';
|
||||||
import { requireFields } from './error-helper.js';
|
import { requireFields } from './error-helper.js';
|
||||||
|
|
@ -70,16 +69,6 @@ export async function getTeacherStudentHandler(req: Request, res: Response): Pro
|
||||||
res.json({ students });
|
res.json({ students });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
|
|
||||||
const username = req.params.username;
|
|
||||||
const full = req.query.full === 'true';
|
|
||||||
requireFields({ username });
|
|
||||||
|
|
||||||
const questions = await getTeacherQuestions(username, full);
|
|
||||||
|
|
||||||
res.json({ questions });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
|
export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
|
||||||
const classId = req.params.classId;
|
const classId = req.params.classId;
|
||||||
requireFields({ classId });
|
requireFields({ classId });
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||||
import { Language } from '@dwengo-1/common/util/language';
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
|
||||||
|
|
||||||
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
||||||
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
||||||
|
|
@ -32,11 +31,4 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
|
|
||||||
return this.find(
|
|
||||||
{ admins: teacher },
|
|
||||||
{ populate: ['admins'] } // Make sure to load admin relations
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import { Question } from '../../entities/questions/question.entity.js';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||||
import { Student } from '../../entities/users/student.entity.js';
|
import { Student } from '../../entities/users/student.entity.js';
|
||||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||||
|
import { Group } from '../../entities/assignments/group.entity.js';
|
||||||
import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
import { Assignment } from '../../entities/assignments/assignment.entity.js';
|
||||||
import { Loaded } from '@mikro-orm/core';
|
import { Loaded } from '@mikro-orm/core';
|
||||||
import { Group } from '../../entities/assignments/group.entity';
|
|
||||||
|
|
||||||
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
export class QuestionRepository extends DwengoEntityRepository<Question> {
|
||||||
public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise<Question> {
|
public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise<Question> {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ export class Assignment {
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
learningPathHruid!: string;
|
learningPathHruid!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'datetime', nullable: true })
|
||||||
|
deadline?: Date;
|
||||||
|
|
||||||
@Enum({
|
@Enum({
|
||||||
items: () => Language,
|
items: () => Language,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core';
|
||||||
import { v4 } from 'uuid';
|
|
||||||
import { Teacher } from '../users/teacher.entity.js';
|
import { Teacher } from '../users/teacher.entity.js';
|
||||||
import { Student } from '../users/student.entity.js';
|
import { Student } from '../users/student.entity.js';
|
||||||
import { ClassRepository } from '../../data/classes/class-repository.js';
|
import { ClassRepository } from '../../data/classes/class-repository.js';
|
||||||
|
import { customAlphabet } from 'nanoid';
|
||||||
|
|
||||||
|
const generateClassId = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 6);
|
||||||
|
|
||||||
@Entity({
|
@Entity({
|
||||||
repository: () => ClassRepository,
|
repository: () => ClassRepository,
|
||||||
})
|
})
|
||||||
export class Class {
|
export class Class {
|
||||||
@PrimaryKey()
|
@PrimaryKey()
|
||||||
classId? = v4();
|
classId? = generateClassId();
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
displayName!: string;
|
displayName!: string;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
|
||||||
description: assignment.description,
|
description: assignment.description,
|
||||||
learningPath: assignment.learningPathHruid,
|
learningPath: assignment.learningPathHruid,
|
||||||
language: assignment.learningPathLanguage,
|
language: assignment.learningPathLanguage,
|
||||||
|
deadline: assignment.deadline ?? new Date(),
|
||||||
groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
|
groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +32,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
|
||||||
description: assignmentData.description,
|
description: assignmentData.description,
|
||||||
learningPathHruid: assignmentData.learningPath,
|
learningPathHruid: assignmentData.learningPath,
|
||||||
learningPathLanguage: languageMap[assignmentData.language],
|
learningPathLanguage: languageMap[assignmentData.language],
|
||||||
|
deadline: assignmentData.deadline,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ export function mapToUserDTO(user: User): UserDTO {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapToUsername(user: { username: string }): string {
|
||||||
|
return user.username;
|
||||||
|
}
|
||||||
|
|
||||||
export function mapToUser<T extends User>(userData: UserDTO, userInstance: T): T {
|
export function mapToUser<T extends User>(userData: UserDTO, userInstance: T): T {
|
||||||
userInstance.username = userData.username;
|
userInstance.username = userData.username;
|
||||||
userInstance.firstName = userData.firstName;
|
userInstance.firstName = userData.firstName;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import * as express from 'express';
|
||||||
import { AuthenticatedRequest } from './authenticated-request.js';
|
import { AuthenticatedRequest } from './authenticated-request.js';
|
||||||
import { AuthenticationInfo } from './authentication-info.js';
|
import { AuthenticationInfo } from './authentication-info.js';
|
||||||
import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js';
|
import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js';
|
||||||
import { ForbiddenException } from '../../exceptions/forbidden-exception.js';
|
|
||||||
|
|
||||||
const JWKS_CACHE = true;
|
const JWKS_CACHE = true;
|
||||||
const JWKS_RATE_LIMIT = true;
|
const JWKS_RATE_LIMIT = true;
|
||||||
|
|
@ -108,36 +107,3 @@ function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
|
export const authenticateUser = [verifyJwtToken, addAuthenticationInfo];
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill
|
|
||||||
* the given access condition.
|
|
||||||
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
|
|
||||||
* to true.
|
|
||||||
*/
|
|
||||||
export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) {
|
|
||||||
return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => {
|
|
||||||
if (!req.auth) {
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
} else if (!accessCondition(req.auth)) {
|
|
||||||
throw new ForbiddenException();
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware which rejects all unauthenticated users, but accepts all authenticated users.
|
|
||||||
*/
|
|
||||||
export const authenticatedOnly = authorize((_) => true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware which rejects requests from unauthenticated users or users that aren't students.
|
|
||||||
*/
|
|
||||||
export const studentsOnly = authorize((auth) => auth.accountType === 'student');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware which rejects requests from unauthenticated users or users that aren't teachers.
|
|
||||||
*/
|
|
||||||
export const teachersOnly = authorize((auth) => auth.accountType === 'teacher');
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { JwtPayload } from 'jsonwebtoken';
|
import { JwtPayload } from 'jsonwebtoken';
|
||||||
import { AuthenticationInfo } from './authentication-info.js';
|
import { AuthenticationInfo } from './authentication-info.js';
|
||||||
|
import * as core from 'express-serve-static-core';
|
||||||
|
|
||||||
export interface AuthenticatedRequest extends Request {
|
export interface AuthenticatedRequest<
|
||||||
|
P = core.ParamsDictionary,
|
||||||
|
ResBody = unknown,
|
||||||
|
ReqBody = unknown,
|
||||||
|
ReqQuery = core.Query,
|
||||||
|
Locals extends Record<string, unknown> = Record<string, unknown>,
|
||||||
|
> extends Request<P, ResBody, ReqBody, ReqQuery, Locals> {
|
||||||
// Properties are optional since the user is not necessarily authenticated.
|
// Properties are optional since the user is not necessarily authenticated.
|
||||||
jwtPayload?: JwtPayload;
|
jwtPayload?: JwtPayload;
|
||||||
auth?: AuthenticationInfo;
|
auth?: AuthenticationInfo;
|
||||||
|
|
|
||||||
21
backend/src/middleware/auth/checks/assignment-auth-checks.ts
Normal file
21
backend/src/middleware/auth/checks/assignment-auth-checks.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { authorize } from './auth-checks.js';
|
||||||
|
import { fetchClass } from '../../../services/classes.js';
|
||||||
|
import { fetchAllGroups } from '../../../services/groups.js';
|
||||||
|
import { mapToUsername } from '../../../interfaces/user.js';
|
||||||
|
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects the path to contain the path parameters 'classId' and 'id' (meaning the ID of the assignment).
|
||||||
|
* Only allows requests from users who are
|
||||||
|
* - either teachers of the class the assignment was posted in,
|
||||||
|
* - or students in a group of the assignment.
|
||||||
|
*/
|
||||||
|
export const onlyAllowIfHasAccessToAssignment = authorize(async (auth, req) => {
|
||||||
|
const { classid: classId, id: assignmentId } = req.params as { classid: string; id: number };
|
||||||
|
if (auth.accountType === AccountType.Teacher) {
|
||||||
|
const clazz = await fetchClass(classId);
|
||||||
|
return clazz.teachers.map(mapToUsername).includes(auth.username);
|
||||||
|
}
|
||||||
|
const groups = await fetchAllGroups(classId, assignmentId);
|
||||||
|
return groups.some((group) => group.members.map((member) => member.username).includes(auth.username));
|
||||||
|
});
|
||||||
61
backend/src/middleware/auth/checks/auth-checks.ts
Normal file
61
backend/src/middleware/auth/checks/auth-checks.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { AuthenticationInfo } from '../authentication-info.js';
|
||||||
|
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||||
|
import * as express from 'express';
|
||||||
|
import { RequestHandler } from 'express';
|
||||||
|
import { UnauthorizedException } from '../../../exceptions/unauthorized-exception.js';
|
||||||
|
import { ForbiddenException } from '../../../exceptions/forbidden-exception.js';
|
||||||
|
import { envVars, getEnvVar } from '../../../util/envVars.js';
|
||||||
|
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill
|
||||||
|
* the given access condition.
|
||||||
|
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
|
||||||
|
* to true.
|
||||||
|
*/
|
||||||
|
export function authorize<P, ResBody, ReqBody, ReqQuery, Locals extends Record<string, unknown>>(
|
||||||
|
accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>) => boolean | Promise<boolean>
|
||||||
|
): RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals> {
|
||||||
|
// Bypass authentication during testing
|
||||||
|
if (getEnvVar(envVars.RunMode) === 'test') {
|
||||||
|
return async (
|
||||||
|
_req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>,
|
||||||
|
_res: express.Response,
|
||||||
|
next: express.NextFunction
|
||||||
|
): Promise<void> => {
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return async (
|
||||||
|
req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>,
|
||||||
|
_res: express.Response,
|
||||||
|
next: express.NextFunction
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!req.auth) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
} else if (!(await accessCondition(req.auth, req))) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware which rejects all unauthenticated users, but accepts all authenticated users.
|
||||||
|
*/
|
||||||
|
export const authenticatedOnly = authorize((_) => true);
|
||||||
|
/**
|
||||||
|
* Middleware which rejects requests from unauthenticated users or users that aren't students.
|
||||||
|
*/
|
||||||
|
export const studentsOnly = authorize((auth) => auth.accountType === AccountType.Student);
|
||||||
|
/**
|
||||||
|
* Middleware which rejects requests from unauthenticated users or users that aren't teachers.
|
||||||
|
*/
|
||||||
|
export const teachersOnly = authorize((auth) => auth.accountType === AccountType.Teacher);
|
||||||
|
/**
|
||||||
|
* Middleware which is to be used on requests no normal user should be able to execute.
|
||||||
|
* Since there is no concept of administrator accounts yet, currently, those requests will always be blocked.
|
||||||
|
*/
|
||||||
|
export const adminOnly = authorize(() => false);
|
||||||
70
backend/src/middleware/auth/checks/class-auth-checks.ts
Normal file
70
backend/src/middleware/auth/checks/class-auth-checks.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { authorize } from './auth-checks.js';
|
||||||
|
import { AuthenticationInfo } from '../authentication-info.js';
|
||||||
|
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||||
|
import { fetchClass } from '../../../services/classes.js';
|
||||||
|
import { mapToUsername } from '../../../interfaces/user.js';
|
||||||
|
import { getAllInvitations } from '../../../services/teacher-invitations.js';
|
||||||
|
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||||
|
|
||||||
|
async function teaches(teacherUsername: string, classId: string): Promise<boolean> {
|
||||||
|
const clazz = await fetchClass(classId);
|
||||||
|
return clazz.teachers.map(mapToUsername).includes(teacherUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be used on a request with path parameters username and classId.
|
||||||
|
* Only allows requests whose username parameter is equal to the username of the user who is logged in and requests
|
||||||
|
* whose classId parameter references a class the logged-in user is a teacher of.
|
||||||
|
*/
|
||||||
|
export const onlyAllowStudentHimselfAndTeachersOfClass = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||||
|
if (req.params.username === auth.username) {
|
||||||
|
return true;
|
||||||
|
} else if (auth.accountType === AccountType.Teacher) {
|
||||||
|
return teaches(auth.username, req.params.classId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only let the request pass through if its path parameter "username" is the username of the currently logged-in
|
||||||
|
* teacher and the path parameter "classId" refers to a class the teacher teaches.
|
||||||
|
*/
|
||||||
|
export const onlyAllowTeacherOfClass = authorize(
|
||||||
|
async (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username && teaches(auth.username, req.params.classId)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only let the request pass through if the class id in it refers to a class the current user is in (as a student
|
||||||
|
* or teacher)
|
||||||
|
*/
|
||||||
|
export const onlyAllowIfInClass = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||||
|
const classId = req.params.classId ?? req.params.classid ?? req.params.id;
|
||||||
|
const clazz = await fetchClass(classId);
|
||||||
|
if (auth.accountType === AccountType.Teacher) {
|
||||||
|
return clazz.teachers.map(mapToUsername).includes(auth.username);
|
||||||
|
}
|
||||||
|
return clazz.students.map(mapToUsername).includes(auth.username);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const onlyAllowIfInClassOrInvited = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||||
|
const classId = req.params.classId ?? req.params.classid ?? req.params.id;
|
||||||
|
const clazz = await fetchClass(classId);
|
||||||
|
if (auth.accountType === AccountType.Teacher) {
|
||||||
|
const invitations = await getAllInvitations(auth.username, false);
|
||||||
|
return clazz.teachers.map(mapToUsername).includes(auth.username) || invitations.some((invitation) => invitation.classId === classId);
|
||||||
|
}
|
||||||
|
return clazz.students.map(mapToUsername).includes(auth.username);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only allows the request to pass if the 'class' property in its body is a class the current user is a member of.
|
||||||
|
*/
|
||||||
|
export const onlyAllowOwnClassInBody = authorize(async (auth, req) => {
|
||||||
|
const classId = (req.body as { class: string })?.class;
|
||||||
|
const clazz = await fetchClass(classId);
|
||||||
|
|
||||||
|
if (auth.accountType === AccountType.Teacher) {
|
||||||
|
return clazz.teachers.map(mapToUsername).includes(auth.username);
|
||||||
|
}
|
||||||
|
return clazz.students.map(mapToUsername).includes(auth.username);
|
||||||
|
});
|
||||||
26
backend/src/middleware/auth/checks/group-auth-checker.ts
Normal file
26
backend/src/middleware/auth/checks/group-auth-checker.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { authorize } from './auth-checks.js';
|
||||||
|
import { fetchClass } from '../../../services/classes.js';
|
||||||
|
import { fetchGroup } from '../../../services/groups.js';
|
||||||
|
import { mapToUsername } from '../../../interfaces/user.js';
|
||||||
|
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects the path to contain the path parameters 'classid', 'assignmentid' and 'groupid'.
|
||||||
|
* Only allows requests from users who are
|
||||||
|
* - either teachers of the class the assignment for the group was posted in,
|
||||||
|
* - or students in the group
|
||||||
|
*/
|
||||||
|
export const onlyAllowIfHasAccessToGroup = authorize(async (auth, req) => {
|
||||||
|
const {
|
||||||
|
classid: classId,
|
||||||
|
assignmentid: assignmentId,
|
||||||
|
groupid: groupId,
|
||||||
|
} = req.params as { classid: string; assignmentid: number; groupid: number };
|
||||||
|
|
||||||
|
if (auth.accountType === AccountType.Teacher) {
|
||||||
|
const clazz = await fetchClass(classId);
|
||||||
|
return clazz.teachers.map(mapToUsername).includes(auth.username);
|
||||||
|
} // User is student
|
||||||
|
const group = await fetchGroup(classId, assignmentId, groupId);
|
||||||
|
return group.members.map(mapToUsername).includes(auth.username);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { authorize } from './auth-checks';
|
||||||
|
import { AuthenticationInfo } from '../authentication-info';
|
||||||
|
import { AuthenticatedRequest } from '../authenticated-request';
|
||||||
|
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only allows requests whose learning path personalization query parameters ('forGroup' / 'assignmentNo' / 'classId')
|
||||||
|
* are
|
||||||
|
* - either not set
|
||||||
|
* - or set to a group the user is in,
|
||||||
|
* - or set to anything if the user is a teacher.
|
||||||
|
*/
|
||||||
|
export const onlyAllowPersonalizationForOwnGroup = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||||
|
const { forGroup, assignmentNo, classId } = req.params;
|
||||||
|
if (auth.accountType === AccountType.Student && forGroup && assignmentNo && classId) {
|
||||||
|
// TODO: groupNumber?
|
||||||
|
// Const group = await fetchGroup(Number(classId), Number(assignmentNo), )
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
66
backend/src/middleware/auth/checks/question-checks.ts
Normal file
66
backend/src/middleware/auth/checks/question-checks.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { authorize } from './auth-checks.js';
|
||||||
|
import { AuthenticationInfo } from '../authentication-info.js';
|
||||||
|
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||||
|
import { requireFields } from '../../../controllers/error-helper.js';
|
||||||
|
import { getLearningObjectId, getQuestionId } from '../../../controllers/questions.js';
|
||||||
|
import { fetchQuestion } from '../../../services/questions.js';
|
||||||
|
import { FALLBACK_SEQ_NUM } from '../../../config.js';
|
||||||
|
import { fetchAnswer } from '../../../services/answers.js';
|
||||||
|
import { mapToUsername } from '../../../interfaces/user.js';
|
||||||
|
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||||
|
|
||||||
|
export const onlyAllowAuthor = authorize(
|
||||||
|
(auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { author: string }).author === auth.username
|
||||||
|
);
|
||||||
|
|
||||||
|
export const onlyAllowAuthorRequest = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||||
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
const seq = req.params.seq;
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
|
const questionId = getQuestionId(learningObjectId, seq);
|
||||||
|
|
||||||
|
const question = await fetchQuestion(questionId);
|
||||||
|
|
||||||
|
return question.author.username === auth.username;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const onlyAllowAuthorRequestAnswer = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||||
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
const seq = req.params.seq;
|
||||||
|
const seqAnswer = req.params.seqAnswer;
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
|
const questionId = getQuestionId(learningObjectId, seq);
|
||||||
|
|
||||||
|
const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM;
|
||||||
|
const answer = await fetchAnswer(questionId, sequenceNumber);
|
||||||
|
|
||||||
|
return answer.author.username === auth.username;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const onlyAllowIfHasAccessToQuestion = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||||
|
const hruid = req.params.hruid;
|
||||||
|
const version = req.params.version;
|
||||||
|
const language = req.query.lang as string;
|
||||||
|
const seq = req.params.seq;
|
||||||
|
requireFields({ hruid });
|
||||||
|
|
||||||
|
const learningObjectId = getLearningObjectId(hruid, version, language);
|
||||||
|
const questionId = getQuestionId(learningObjectId, seq);
|
||||||
|
|
||||||
|
const question = await fetchQuestion(questionId);
|
||||||
|
const group = question.inGroup;
|
||||||
|
|
||||||
|
if (auth.accountType === AccountType.Teacher) {
|
||||||
|
const cls = group.assignment.within; // TODO check if contains full objects
|
||||||
|
return cls.teachers.map(mapToUsername).includes(auth.username);
|
||||||
|
} // User is student
|
||||||
|
return group.members.map(mapToUsername).includes(auth.username);
|
||||||
|
});
|
||||||
28
backend/src/middleware/auth/checks/submission-checks.ts
Normal file
28
backend/src/middleware/auth/checks/submission-checks.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { languageMap } from '@dwengo-1/common/util/language';
|
||||||
|
import { LearningObjectIdentifier } from '../../../entities/content/learning-object-identifier.js';
|
||||||
|
import { fetchSubmission } from '../../../services/submissions.js';
|
||||||
|
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||||
|
import { AuthenticationInfo } from '../authentication-info.js';
|
||||||
|
import { authorize } from './auth-checks.js';
|
||||||
|
import { FALLBACK_LANG } from '../../../config.js';
|
||||||
|
import { mapToUsername } from '../../../interfaces/user.js';
|
||||||
|
import { AccountType } from '@dwengo-1/common/util/account-types';
|
||||||
|
|
||||||
|
export const onlyAllowSubmitter = authorize(
|
||||||
|
(auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { submitter: string }).submitter === auth.username
|
||||||
|
);
|
||||||
|
|
||||||
|
export const onlyAllowIfHasAccessToSubmission = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||||
|
const { hruid: lohruid, id: submissionNumber } = req.params;
|
||||||
|
const { language: lang, version: version } = req.query;
|
||||||
|
|
||||||
|
const loId = new LearningObjectIdentifier(lohruid, languageMap[lang as string] ?? FALLBACK_LANG, Number(version));
|
||||||
|
const submission = await fetchSubmission(loId, Number(submissionNumber));
|
||||||
|
|
||||||
|
if (auth.accountType === AccountType.Teacher) {
|
||||||
|
// Dit kan niet werken om dat al deze objecten niet gepopulate zijn.
|
||||||
|
return submission.onBehalfOf.assignment.within.teachers.map(mapToUsername).includes(auth.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
return submission.onBehalfOf.members.map(mapToUsername).includes(auth.username);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { authorize } from './auth-checks.js';
|
||||||
|
import { AuthenticationInfo } from '../authentication-info.js';
|
||||||
|
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||||
|
|
||||||
|
export const onlyAllowSenderOrReceiver = authorize(
|
||||||
|
(auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.sender === auth.username || req.params.receiver === auth.username
|
||||||
|
);
|
||||||
|
|
||||||
|
export const onlyAllowSender = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.sender === auth.username);
|
||||||
|
|
||||||
|
export const onlyAllowSenderBody = authorize(
|
||||||
|
(auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { sender: string }).sender === auth.username
|
||||||
|
);
|
||||||
|
|
||||||
|
export const onlyAllowReceiverBody = authorize(
|
||||||
|
(auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { receiver: string }).receiver === auth.username
|
||||||
|
);
|
||||||
8
backend/src/middleware/auth/checks/user-auth-checks.ts
Normal file
8
backend/src/middleware/auth/checks/user-auth-checks.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { authorize } from './auth-checks.js';
|
||||||
|
import { AuthenticationInfo } from '../authentication-info.js';
|
||||||
|
import { AuthenticatedRequest } from '../authenticated-request.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only allow the user whose username is in the path parameter "username" to access the endpoint.
|
||||||
|
*/
|
||||||
|
export const preventImpersonation = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username);
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js';
|
import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js';
|
||||||
|
import { authenticatedOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
|
import { onlyAllowAuthor, onlyAllowAuthorRequestAnswer, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js';
|
||||||
|
|
||||||
const router = express.Router({ mergeParams: true });
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.get('/', getAllAnswersHandler);
|
router.get('/', authenticatedOnly, getAllAnswersHandler);
|
||||||
|
|
||||||
router.post('/', createAnswerHandler);
|
router.post('/', teachersOnly, onlyAllowAuthor, createAnswerHandler);
|
||||||
|
|
||||||
router.get('/:seqAnswer', getAnswerHandler);
|
router.get('/:seqAnswer', onlyAllowIfHasAccessToQuestion, getAnswerHandler);
|
||||||
|
|
||||||
router.delete('/:seqAnswer', deleteAnswerHandler);
|
router.delete('/:seqAnswer', teachersOnly, onlyAllowAuthorRequestAnswer, deleteAnswerHandler);
|
||||||
|
|
||||||
router.put('/:seqAnswer', updateAnswerHandler);
|
router.put('/:seqAnswer', teachersOnly, onlyAllowAuthorRequestAnswer, updateAnswerHandler);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,25 @@ import {
|
||||||
putAssignmentHandler,
|
putAssignmentHandler,
|
||||||
} from '../controllers/assignments.js';
|
} from '../controllers/assignments.js';
|
||||||
import groupRouter from './groups.js';
|
import groupRouter from './groups.js';
|
||||||
|
import { teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
|
import { onlyAllowIfInClass } from '../middleware/auth/checks/class-auth-checks.js';
|
||||||
|
import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks.js';
|
||||||
|
|
||||||
const router = express.Router({ mergeParams: true });
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.get('/', getAllAssignmentsHandler);
|
router.get('/', teachersOnly, onlyAllowIfInClass, getAllAssignmentsHandler);
|
||||||
|
|
||||||
router.post('/', createAssignmentHandler);
|
router.post('/', teachersOnly, onlyAllowIfInClass, createAssignmentHandler);
|
||||||
|
|
||||||
router.get('/:id', getAssignmentHandler);
|
router.get('/:id', onlyAllowIfHasAccessToAssignment, getAssignmentHandler);
|
||||||
|
|
||||||
router.put('/:id', putAssignmentHandler);
|
router.put('/:id', teachersOnly, onlyAllowIfHasAccessToAssignment, putAssignmentHandler);
|
||||||
|
|
||||||
router.delete('/:id', deleteAssignmentHandler);
|
router.delete('/:id', teachersOnly, onlyAllowIfHasAccessToAssignment, deleteAssignmentHandler);
|
||||||
|
|
||||||
router.get('/:id/submissions', getAssignmentsSubmissionsHandler);
|
router.get('/:id/submissions', teachersOnly, onlyAllowIfHasAccessToAssignment, getAssignmentsSubmissionsHandler);
|
||||||
|
|
||||||
router.get('/:id/questions', getAssignmentQuestionsHandler);
|
router.get('/:id/questions', teachersOnly, onlyAllowIfHasAccessToAssignment, getAssignmentQuestionsHandler);
|
||||||
|
|
||||||
router.use('/:assignmentid/groups', groupRouter);
|
router.use('/:assignmentid/groups', groupRouter);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,35 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { getFrontendAuthConfig, postHelloHandler } from '../controllers/auth.js';
|
import { handleGetFrontendAuthConfig, postHelloHandler } from '../controllers/auth.js';
|
||||||
import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/auth.js';
|
import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Returns auth configuration for frontend
|
// Returns auth configuration for frontend
|
||||||
router.get('/config', (_req, res) => {
|
router.get('/config', handleGetFrontendAuthConfig);
|
||||||
res.json(getFrontendAuthConfig());
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => {
|
router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => {
|
||||||
/* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */
|
/* #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||||
res.json({ message: 'If you see this, you should be authenticated!' });
|
res.json({ message: 'If you see this, you should be authenticated!' });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/testStudentsOnly', studentsOnly, (_req, res) => {
|
router.get('/testStudentsOnly', studentsOnly, (_req, res) => {
|
||||||
/* #swagger.security = [{ "student": [ ] }] */
|
/* #swagger.security = [{ "studentProduction": [ ] }, { "studentStaging": [ ] }, { "studentDev": [ ] }] */
|
||||||
res.json({ message: 'If you see this, you should be a student!' });
|
res.json({ message: 'If you see this, you should be a student!' });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/testTeachersOnly', teachersOnly, (_req, res) => {
|
router.get('/testTeachersOnly', teachersOnly, (_req, res) => {
|
||||||
/* #swagger.security = [{ "teacher": [ ] }] */
|
/* #swagger.security = [{ "teacherProduction": [ ] }, { "teacherStaging": [ ] }, { "teacherDev": [ ] }] */
|
||||||
res.json({ message: 'If you see this, you should be a teacher!' });
|
res.json({ message: 'If you see this, you should be a teacher!' });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/hello', authenticatedOnly, postHelloHandler);
|
// This endpoint is called by the client when the user has just logged in.
|
||||||
|
// It creates or updates the user entity based on the authentication data the endpoint was called with.
|
||||||
|
router.post(
|
||||||
|
'/hello',
|
||||||
|
authenticatedOnly,
|
||||||
|
/*
|
||||||
|
#swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }]
|
||||||
|
*/ postHelloHandler
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -14,33 +14,35 @@ import {
|
||||||
putClassHandler,
|
putClassHandler,
|
||||||
} from '../controllers/classes.js';
|
} from '../controllers/classes.js';
|
||||||
import assignmentRouter from './assignments.js';
|
import assignmentRouter from './assignments.js';
|
||||||
|
import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
|
import { onlyAllowIfInClass, onlyAllowIfInClassOrInvited } from '../middleware/auth/checks/class-auth-checks.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
router.get('/', adminOnly, getAllClassesHandler);
|
||||||
router.get('/', getAllClassesHandler);
|
|
||||||
|
|
||||||
router.post('/', createClassHandler);
|
router.post('/', teachersOnly, createClassHandler);
|
||||||
|
|
||||||
router.get('/:id', getClassHandler);
|
router.get('/:id', onlyAllowIfInClassOrInvited, getClassHandler);
|
||||||
|
|
||||||
router.put('/:id', putClassHandler);
|
router.put('/:id', teachersOnly, onlyAllowIfInClass, putClassHandler);
|
||||||
|
|
||||||
router.delete('/:id', deleteClassHandler);
|
router.delete('/:id', teachersOnly, onlyAllowIfInClass, deleteClassHandler);
|
||||||
|
|
||||||
router.get('/:id/teacher-invitations', getTeacherInvitationsHandler);
|
router.get('/:id/teacher-invitations', teachersOnly, onlyAllowIfInClass, getTeacherInvitationsHandler);
|
||||||
|
|
||||||
router.get('/:id/students', getClassStudentsHandler);
|
router.get('/:id/students', onlyAllowIfInClass, getClassStudentsHandler);
|
||||||
|
|
||||||
router.post('/:id/students', addClassStudentHandler);
|
router.post('/:id/students', teachersOnly, onlyAllowIfInClass, addClassStudentHandler);
|
||||||
|
|
||||||
router.delete('/:id/students/:username', deleteClassStudentHandler);
|
router.delete('/:id/students/:username', teachersOnly, onlyAllowIfInClass, deleteClassStudentHandler);
|
||||||
|
|
||||||
router.get('/:id/teachers', getClassTeachersHandler);
|
router.get('/:id/teachers', onlyAllowIfInClass, getClassTeachersHandler);
|
||||||
|
|
||||||
router.post('/:id/teachers', addClassTeacherHandler);
|
// De combinatie van deze POST en DELETE endpoints kan lethal zijn
|
||||||
|
router.post('/:id/teachers', teachersOnly, onlyAllowIfInClass, addClassTeacherHandler);
|
||||||
|
|
||||||
router.delete('/:id/teachers/:username', deleteClassTeacherHandler);
|
router.delete('/:id/teachers/:username', teachersOnly, onlyAllowIfInClass, deleteClassTeacherHandler);
|
||||||
|
|
||||||
router.use('/:classid/assignments', assignmentRouter);
|
router.use('/:classid/assignments', assignmentRouter);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,22 +8,24 @@ import {
|
||||||
getGroupSubmissionsHandler,
|
getGroupSubmissionsHandler,
|
||||||
putGroupHandler,
|
putGroupHandler,
|
||||||
} from '../controllers/groups.js';
|
} from '../controllers/groups.js';
|
||||||
|
import { onlyAllowIfHasAccessToGroup } from '../middleware/auth/checks/group-auth-checker.js';
|
||||||
|
import { teachersOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
|
import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks.js';
|
||||||
|
|
||||||
const router = express.Router({ mergeParams: true });
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
router.get('/', onlyAllowIfHasAccessToAssignment, getAllGroupsHandler);
|
||||||
router.get('/', getAllGroupsHandler);
|
|
||||||
|
|
||||||
router.post('/', createGroupHandler);
|
router.post('/', teachersOnly, onlyAllowIfHasAccessToAssignment, createGroupHandler);
|
||||||
|
|
||||||
router.get('/:groupid', getGroupHandler);
|
router.get('/:groupid', onlyAllowIfHasAccessToAssignment, getGroupHandler);
|
||||||
|
|
||||||
router.put('/:groupid', putGroupHandler);
|
router.put('/:groupid', teachersOnly, onlyAllowIfHasAccessToAssignment, putGroupHandler);
|
||||||
|
|
||||||
router.delete('/:groupid', deleteGroupHandler);
|
router.delete('/:groupid', teachersOnly, onlyAllowIfHasAccessToAssignment, deleteGroupHandler);
|
||||||
|
|
||||||
router.get('/:groupid/submissions', getGroupSubmissionsHandler);
|
router.get('/:groupid/submissions', onlyAllowIfHasAccessToGroup, getGroupSubmissionsHandler);
|
||||||
|
|
||||||
router.get('/:groupid/questions', getGroupQuestionsHandler);
|
router.get('/:groupid/questions', onlyAllowIfHasAccessToGroup, getGroupQuestionsHandler);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js';
|
import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js';
|
||||||
|
|
||||||
import submissionRoutes from './submissions.js';
|
import submissionRoutes from './submissions.js';
|
||||||
import questionRoutes from './questions.js';
|
import questionRoutes from './questions.js';
|
||||||
|
import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
@ -16,13 +16,13 @@ const router = express.Router();
|
||||||
|
|
||||||
// Route 2: list of object data
|
// Route 2: list of object data
|
||||||
// Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie
|
// Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie
|
||||||
router.get('/', getAllLearningObjects);
|
router.get('/', authenticatedOnly, getAllLearningObjects);
|
||||||
|
|
||||||
// Parameter: hruid of learning object
|
// Parameter: hruid of learning object
|
||||||
// Query: language
|
// Query: language
|
||||||
// Route to fetch data of one learning object based on its hruid
|
// Route to fetch data of one learning object based on its hruid
|
||||||
// Example: http://localhost:3000/learningObject/un_ai7
|
// Example: http://localhost:3000/learningObject/un_ai7
|
||||||
router.get('/:hruid', getLearningObject);
|
router.get('/:hruid', authenticatedOnly, getLearningObject);
|
||||||
|
|
||||||
router.use('/:hruid/submissions', submissionRoutes);
|
router.use('/:hruid/submissions', submissionRoutes);
|
||||||
|
|
||||||
|
|
@ -32,12 +32,12 @@ router.use('/:hruid/:version/questions', questionRoutes);
|
||||||
// Query: language, version (optional)
|
// Query: language, version (optional)
|
||||||
// Route to fetch the HTML rendering of one learning object based on its hruid.
|
// Route to fetch the HTML rendering of one learning object based on its hruid.
|
||||||
// Example: http://localhost:3000/learningObject/un_ai7/html
|
// Example: http://localhost:3000/learningObject/un_ai7/html
|
||||||
router.get('/:hruid/html', getLearningObjectHTML);
|
router.get('/:hruid/html', authenticatedOnly, getLearningObjectHTML);
|
||||||
|
|
||||||
// Parameter: hruid of learning object, name of attachment.
|
// Parameter: hruid of learning object, name of attachment.
|
||||||
// Query: language, version (optional).
|
// Query: language, version (optional).
|
||||||
// Route to get the raw data of the attachment for one learning object based on its hruid.
|
// Route to get the raw data of the attachment for one learning object based on its hruid.
|
||||||
// Example: http://localhost:3000/learningObject/u_test/attachment/testimage.png
|
// Example: http://localhost:3000/learningObject/u_test/attachment/testimage.png
|
||||||
router.get('/:hruid/html/:attachmentName', getAttachment);
|
router.get('/:hruid/html/:attachmentName', authenticatedOnly, getAttachment);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { getLearningPaths } from '../controllers/learning-paths.js';
|
import { getLearningPaths } from '../controllers/learning-paths.js';
|
||||||
|
import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
@ -22,6 +23,6 @@ const router = express.Router();
|
||||||
// Route to fetch learning paths based on a theme
|
// Route to fetch learning paths based on a theme
|
||||||
// Example: http://localhost:3000/learningPath?theme=kiks
|
// Example: http://localhost:3000/learningPath?theme=kiks
|
||||||
|
|
||||||
router.get('/', getLearningPaths);
|
router.get('/', authenticatedOnly, getLearningPaths);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,25 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js';
|
import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js';
|
||||||
import answerRoutes from './answers.js';
|
import answerRoutes from './answers.js';
|
||||||
|
import { authenticatedOnly, studentsOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
|
import { updateAnswerHandler } from '../controllers/answers.js';
|
||||||
|
import { onlyAllowAuthor, onlyAllowAuthorRequest, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js';
|
||||||
|
|
||||||
const router = express.Router({ mergeParams: true });
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
// Query language
|
// Query language
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
// Root endpoint used to search objects
|
||||||
router.get('/', getAllQuestionsHandler);
|
router.get('/', authenticatedOnly, getAllQuestionsHandler);
|
||||||
|
|
||||||
router.post('/', createQuestionHandler);
|
router.post('/', studentsOnly, onlyAllowAuthor, createQuestionHandler);
|
||||||
|
|
||||||
router.delete('/:seq', deleteQuestionHandler);
|
|
||||||
|
|
||||||
// Information about a question with id
|
// Information about a question with id
|
||||||
router.get('/:seq', getQuestionHandler);
|
router.get('/:seq', onlyAllowIfHasAccessToQuestion, getQuestionHandler);
|
||||||
|
|
||||||
|
router.delete('/:seq', studentsOnly, onlyAllowAuthorRequest, deleteQuestionHandler);
|
||||||
|
|
||||||
|
router.put('/:seq', studentsOnly, onlyAllowAuthorRequest, updateAnswerHandler);
|
||||||
|
|
||||||
router.use('/:seq/answers', answerRoutes);
|
router.use('/:seq/answers', answerRoutes);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,30 @@ router.get('/', (_, res: Response) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
|
|
||||||
router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */);
|
|
||||||
router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
|
|
||||||
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
|
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
|
||||||
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
|
router.use(
|
||||||
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);
|
'/class',
|
||||||
router.use('/learningObject', learningObjectRoutes /* #swagger.tags = ['Learning Object'] */);
|
classRouter /* #swagger.tags = ['Class'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||||
|
);
|
||||||
|
router.use(
|
||||||
|
'/learningObject',
|
||||||
|
learningObjectRoutes /* #swagger.tags = ['Learning Object'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||||
|
);
|
||||||
|
router.use(
|
||||||
|
'/learningPath',
|
||||||
|
learningPathRoutes /* #swagger.tags = ['Learning Path'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||||
|
);
|
||||||
|
router.use(
|
||||||
|
'/student',
|
||||||
|
studentRouter /* #swagger.tags = ['Student'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||||
|
);
|
||||||
|
router.use(
|
||||||
|
'/teacher',
|
||||||
|
teacherRouter /* #swagger.tags = ['Teacher'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||||
|
);
|
||||||
|
router.use(
|
||||||
|
'/theme',
|
||||||
|
themeRoutes /* #swagger.tags = ['Theme'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,19 @@ import {
|
||||||
getStudentRequestHandler,
|
getStudentRequestHandler,
|
||||||
getStudentRequestsHandler,
|
getStudentRequestsHandler,
|
||||||
} from '../controllers/students.js';
|
} from '../controllers/students.js';
|
||||||
|
import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js';
|
||||||
|
import { onlyAllowStudentHimselfAndTeachersOfClass } from '../middleware/auth/checks/class-auth-checks.js';
|
||||||
|
|
||||||
|
// Under /:username/joinRequests/
|
||||||
|
|
||||||
const router = express.Router({ mergeParams: true });
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.get('/', getStudentRequestsHandler);
|
router.get('/', preventImpersonation, getStudentRequestsHandler);
|
||||||
|
|
||||||
router.post('/', createStudentRequestHandler);
|
router.post('/', preventImpersonation, createStudentRequestHandler);
|
||||||
|
|
||||||
router.get('/:classId', getStudentRequestHandler);
|
router.get('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, getStudentRequestHandler);
|
||||||
|
|
||||||
router.delete('/:classId', deleteClassJoinRequestHandler);
|
router.delete('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, deleteClassJoinRequestHandler);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -11,33 +11,37 @@ import {
|
||||||
getStudentSubmissionsHandler,
|
getStudentSubmissionsHandler,
|
||||||
} from '../controllers/students.js';
|
} from '../controllers/students.js';
|
||||||
import joinRequestRouter from './student-join-requests.js';
|
import joinRequestRouter from './student-join-requests.js';
|
||||||
|
import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js';
|
||||||
|
import { adminOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
// Root endpoint used to search objects
|
||||||
router.get('/', getAllStudentsHandler);
|
router.get('/', adminOnly, getAllStudentsHandler);
|
||||||
|
|
||||||
router.post('/', createStudentHandler);
|
// Users will be created automatically when some resource is created for them. Therefore, this endpoint
|
||||||
|
// Can only be used by an administrator.
|
||||||
|
router.post('/', adminOnly, createStudentHandler);
|
||||||
|
|
||||||
router.delete('/:username', deleteStudentHandler);
|
router.delete('/:username', preventImpersonation, deleteStudentHandler);
|
||||||
|
|
||||||
// Information about a student's profile
|
// Information about a student's profile
|
||||||
router.get('/:username', getStudentHandler);
|
router.get('/:username', preventImpersonation, getStudentHandler);
|
||||||
|
|
||||||
// The list of classes a student is in
|
// The list of classes a student is in
|
||||||
router.get('/:username/classes', getStudentClassesHandler);
|
router.get('/:username/classes', preventImpersonation, getStudentClassesHandler);
|
||||||
|
|
||||||
// The list of submissions a student has made
|
// The list of submissions a student has made
|
||||||
router.get('/:username/submissions', getStudentSubmissionsHandler);
|
router.get('/:username/submissions', preventImpersonation, getStudentSubmissionsHandler);
|
||||||
|
|
||||||
// The list of assignments a student has
|
// The list of assignments a student has
|
||||||
router.get('/:username/assignments', getStudentAssignmentsHandler);
|
router.get('/:username/assignments', preventImpersonation, getStudentAssignmentsHandler);
|
||||||
|
|
||||||
// The list of groups a student is in
|
// The list of groups a student is in
|
||||||
router.get('/:username/groups', getStudentGroupsHandler);
|
router.get('/:username/groups', preventImpersonation, getStudentGroupsHandler);
|
||||||
|
|
||||||
// A list of questions a user has created
|
// A list of questions a user has created
|
||||||
router.get('/:username/questions', getStudentQuestionsHandler);
|
router.get('/:username/questions', preventImpersonation, getStudentQuestionsHandler);
|
||||||
|
|
||||||
router.use('/:username/joinRequests', joinRequestRouter);
|
router.use('/:username/joinRequests', joinRequestRouter);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler, getSubmissionsHandler } from '../controllers/submissions.js';
|
import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler, getSubmissionsHandler } from '../controllers/submissions.js';
|
||||||
|
import { onlyAllowIfHasAccessToSubmission, onlyAllowSubmitter } from '../middleware/auth/checks/submission-checks.js';
|
||||||
|
import { adminOnly, studentsOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
const router = express.Router({ mergeParams: true });
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
router.get('/', adminOnly, getSubmissionsHandler);
|
||||||
router.get('/', getSubmissionsHandler);
|
|
||||||
|
|
||||||
router.post('/', createSubmissionHandler);
|
router.post('/', studentsOnly, onlyAllowSubmitter, createSubmissionHandler);
|
||||||
|
|
||||||
// Information about an submission with id 'id'
|
router.get('/:id', onlyAllowIfHasAccessToSubmission, getSubmissionHandler);
|
||||||
router.get('/:id', getSubmissionHandler);
|
|
||||||
|
|
||||||
router.delete('/:id', deleteSubmissionHandler);
|
router.delete('/:id', onlyAllowIfHasAccessToSubmission, deleteSubmissionHandler);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,24 @@ import {
|
||||||
getInvitationHandler,
|
getInvitationHandler,
|
||||||
updateInvitationHandler,
|
updateInvitationHandler,
|
||||||
} from '../controllers/teacher-invitations.js';
|
} from '../controllers/teacher-invitations.js';
|
||||||
|
import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js';
|
||||||
|
import {
|
||||||
|
onlyAllowReceiverBody,
|
||||||
|
onlyAllowSender,
|
||||||
|
onlyAllowSenderBody,
|
||||||
|
onlyAllowSenderOrReceiver,
|
||||||
|
} from '../middleware/auth/checks/teacher-invitation-checks.js';
|
||||||
|
|
||||||
const router = express.Router({ mergeParams: true });
|
const router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.get('/:username', getAllInvitationsHandler);
|
router.get('/:username', preventImpersonation, getAllInvitationsHandler);
|
||||||
|
|
||||||
router.get('/:sender/:receiver/:classId', getInvitationHandler);
|
router.get('/:sender/:receiver/:classId', onlyAllowSenderOrReceiver, getInvitationHandler);
|
||||||
|
|
||||||
router.post('/', createInvitationHandler);
|
router.post('/', onlyAllowSenderBody, createInvitationHandler);
|
||||||
|
|
||||||
router.put('/', updateInvitationHandler);
|
router.put('/', onlyAllowReceiverBody, updateInvitationHandler);
|
||||||
|
|
||||||
router.delete('/:sender/:receiver/:classId', deleteInvitationHandler);
|
router.delete('/:sender/:receiver/:classId', onlyAllowSender, deleteInvitationHandler);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -6,32 +6,31 @@ import {
|
||||||
getStudentJoinRequestHandler,
|
getStudentJoinRequestHandler,
|
||||||
getTeacherClassHandler,
|
getTeacherClassHandler,
|
||||||
getTeacherHandler,
|
getTeacherHandler,
|
||||||
getTeacherQuestionHandler,
|
|
||||||
getTeacherStudentHandler,
|
getTeacherStudentHandler,
|
||||||
updateStudentJoinRequestHandler,
|
updateStudentJoinRequestHandler,
|
||||||
} from '../controllers/teachers.js';
|
} from '../controllers/teachers.js';
|
||||||
import invitationRouter from './teacher-invitations.js';
|
import invitationRouter from './teacher-invitations.js';
|
||||||
|
import { adminOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
|
import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js';
|
||||||
|
import { onlyAllowTeacherOfClass } from '../middleware/auth/checks/class-auth-checks.js';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Root endpoint used to search objects
|
// Root endpoint used to search objects
|
||||||
router.get('/', getAllTeachersHandler);
|
router.get('/', adminOnly, getAllTeachersHandler);
|
||||||
|
|
||||||
router.post('/', createTeacherHandler);
|
router.post('/', adminOnly, createTeacherHandler);
|
||||||
|
|
||||||
router.get('/:username', getTeacherHandler);
|
router.get('/:username', preventImpersonation, getTeacherHandler);
|
||||||
|
|
||||||
router.delete('/:username', deleteTeacherHandler);
|
router.delete('/:username', preventImpersonation, deleteTeacherHandler);
|
||||||
|
|
||||||
router.get('/:username/classes', getTeacherClassHandler);
|
router.get('/:username/classes', preventImpersonation, getTeacherClassHandler);
|
||||||
|
|
||||||
router.get('/:username/students', getTeacherStudentHandler);
|
router.get('/:username/students', preventImpersonation, getTeacherStudentHandler);
|
||||||
|
|
||||||
router.get('/:username/questions', getTeacherQuestionHandler);
|
router.get('/:username/joinRequests/:classId', onlyAllowTeacherOfClass, getStudentJoinRequestHandler);
|
||||||
|
|
||||||
router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler);
|
router.put('/:username/joinRequests/:classId/:studentUsername', onlyAllowTeacherOfClass, updateStudentJoinRequestHandler);
|
||||||
|
|
||||||
router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler);
|
|
||||||
|
|
||||||
// Invitations to other classes a teacher received
|
// Invitations to other classes a teacher received
|
||||||
router.use('/invitations', invitationRouter);
|
router.use('/invitations', invitationRouter);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { getThemesHandler, getHruidsByThemeHandler } from '../controllers/themes.js';
|
import { getThemesHandler, getHruidsByThemeHandler } from '../controllers/themes.js';
|
||||||
|
import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Query: language
|
// Query: language
|
||||||
// Route to fetch list of {key, title, description, image} themes in their respective language
|
// Route to fetch list of {key, title, description, image} themes in their respective language
|
||||||
router.get('/', getThemesHandler);
|
router.get('/', authenticatedOnly, getThemesHandler);
|
||||||
|
|
||||||
// Arg: theme (key)
|
// Arg: theme (key)
|
||||||
// Route to fetch list of hruids based on theme
|
// Route to fetch list of hruids based on theme
|
||||||
router.get('/:theme', getHruidsByThemeHandler);
|
router.get('/:theme', authenticatedOnly, getHruidsByThemeHandler);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export async function createAnswer(questionId: QuestionId, answerData: AnswerDat
|
||||||
return mapToAnswerDTO(answer);
|
return mapToAnswerDTO(answer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise<Answer> {
|
export async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise<Answer> {
|
||||||
const answerRepository = getAnswerRepository();
|
const answerRepository = getAnswerRepository();
|
||||||
const question = await fetchQuestion(questionId);
|
const question = await fetchQuestion(questionId);
|
||||||
const answer = await answerRepository.findAnswer(question, sequenceNumber);
|
const answer = await answerRepository.findAnswer(question, sequenceNumber);
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,15 @@ export async function fetchGroup(classId: string, assignmentNumber: number, grou
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAllGroups(classId: string, assignmentNumber: number): Promise<Group[]> {
|
||||||
|
const assignment = await fetchAssignment(classId, assignmentNumber);
|
||||||
|
|
||||||
|
const groupRepository = getGroupRepository();
|
||||||
|
const groups = await groupRepository.findAllGroupsForAssignment(assignment);
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> {
|
export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise<GroupDTO> {
|
||||||
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
|
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
|
||||||
return mapToGroupDTO(group, group.assignment.within);
|
return mapToGroupDTO(group, group.assignment.within);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { getLearningPathRepository } from '../../data/repositories.js';
|
||||||
import learningObjectService from '../learning-objects/learning-object-service.js';
|
import learningObjectService from '../learning-objects/learning-object-service.js';
|
||||||
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
|
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
|
||||||
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
||||||
import { getLastSubmissionForGroup, isTransitionPossible } from './learning-path-personalization-util.js';
|
import { getLastSubmissionForGroup, idFromLearningPathNode, isTransitionPossible } from './learning-path-personalization-util.js';
|
||||||
import {
|
import {
|
||||||
FilteredLearningObject,
|
FilteredLearningObject,
|
||||||
LearningObjectNode,
|
LearningObjectNode,
|
||||||
|
|
@ -95,7 +95,7 @@ async function convertNode(
|
||||||
personalizedFor: Group | undefined,
|
personalizedFor: Group | undefined,
|
||||||
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>
|
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>
|
||||||
): Promise<LearningObjectNode> {
|
): Promise<LearningObjectNode> {
|
||||||
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(node, personalizedFor) : null;
|
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(idFromLearningPathNode(node), personalizedFor) : null;
|
||||||
const transitions = node.transitions
|
const transitions = node.transitions
|
||||||
.filter(
|
.filter(
|
||||||
(trans) =>
|
(trans) =>
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,33 @@ import { DWENGO_API_BASE } from '../../config.js';
|
||||||
import { LearningPathProvider } from './learning-path-provider.js';
|
import { LearningPathProvider } from './learning-path-provider.js';
|
||||||
import { getLogger, Logger } from '../../logging/initalize.js';
|
import { getLogger, Logger } from '../../logging/initalize.js';
|
||||||
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
|
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
|
||||||
|
import { Group } from '../../entities/assignments/group.entity.js';
|
||||||
|
import { getLastSubmissionForGroup, idFromLearningObjectNode } from './learning-path-personalization-util.js';
|
||||||
|
|
||||||
const logger: Logger = getLogger();
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds progress information to the learning path. Modifies the learning path in-place.
|
||||||
|
* @param learningPath The learning path to add progress to.
|
||||||
|
* @param personalizedFor The group whose progress should be shown.
|
||||||
|
* @returns the modified learning path.
|
||||||
|
*/
|
||||||
|
async function addProgressToLearningPath(learningPath: LearningPath, personalizedFor: Group): Promise<LearningPath> {
|
||||||
|
await Promise.all(
|
||||||
|
learningPath.nodes.map(async (node) => {
|
||||||
|
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(idFromLearningObjectNode(node), personalizedFor) : null;
|
||||||
|
node.done = Boolean(lastSubmission);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
learningPath.num_nodes = learningPath.nodes.length;
|
||||||
|
learningPath.num_nodes_left = learningPath.nodes.filter((it) => !it.done).length;
|
||||||
|
|
||||||
|
return learningPath;
|
||||||
|
}
|
||||||
|
|
||||||
const dwengoApiLearningPathProvider: LearningPathProvider = {
|
const dwengoApiLearningPathProvider: LearningPathProvider = {
|
||||||
async fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse> {
|
async fetchLearningPaths(hruids: string[], language: string, source: string, personalizedFor: Group): Promise<LearningPathResponse> {
|
||||||
if (hruids.length === 0) {
|
if (hruids.length === 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|
@ -32,17 +54,24 @@ const dwengoApiLearningPathProvider: LearningPathProvider = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all(learningPaths?.map(async (it) => addProgressToLearningPath(it, personalizedFor)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
source,
|
source,
|
||||||
data: learningPaths,
|
data: learningPaths,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async searchLearningPaths(query: string, language: string): Promise<LearningPath[]> {
|
async searchLearningPaths(query: string, language: string, personalizedFor: Group): Promise<LearningPath[]> {
|
||||||
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
|
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
|
||||||
const params = { all: query, language };
|
const params = { all: query, language };
|
||||||
|
|
||||||
const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params });
|
const searchResults = await fetchWithLogging<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params });
|
||||||
|
|
||||||
|
if (searchResults) {
|
||||||
|
await Promise.all(searchResults?.map(async (it) => addProgressToLearningPath(it, personalizedFor)));
|
||||||
|
}
|
||||||
|
|
||||||
return searchResults ?? [];
|
return searchResults ?? [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,36 @@ import { getSubmissionRepository } from '../../data/repositories.js';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||||
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
||||||
import { JSONPath } from 'jsonpath-plus';
|
import { JSONPath } from 'jsonpath-plus';
|
||||||
|
import { LearningObjectNode } from '@dwengo-1/common/interfaces/learning-content';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the last submission for the learning object associated with the given node and for the group
|
* Returns the last submission for the learning object associated with the given node and for the group
|
||||||
*/
|
*/
|
||||||
export async function getLastSubmissionForGroup(node: LearningPathNode, pathFor: Group): Promise<Submission | null> {
|
export async function getLastSubmissionForGroup(learningObjectId: LearningObjectIdentifier, pathFor: Group): Promise<Submission | null> {
|
||||||
const submissionRepo = getSubmissionRepository();
|
const submissionRepo = getSubmissionRepository();
|
||||||
const learningObjectId: LearningObjectIdentifier = {
|
return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a LearningObjectIdentifier describing the specified node.
|
||||||
|
*/
|
||||||
|
export function idFromLearningObjectNode(node: LearningObjectNode): LearningObjectIdentifier {
|
||||||
|
return {
|
||||||
|
hruid: node.learningobject_hruid,
|
||||||
|
language: node.language,
|
||||||
|
version: node.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a LearningObjectIdentifier describing the specified node.
|
||||||
|
*/
|
||||||
|
export function idFromLearningPathNode(node: LearningPathNode): LearningObjectIdentifier {
|
||||||
|
return {
|
||||||
hruid: node.learningObjectHruid,
|
hruid: node.learningObjectHruid,
|
||||||
language: node.language,
|
language: node.language,
|
||||||
version: node.version,
|
version: node.version,
|
||||||
};
|
};
|
||||||
return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { fetchStudent } from './students.js';
|
||||||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||||
import { FALLBACK_VERSION_NUM } from '../config.js';
|
import { FALLBACK_VERSION_NUM } from '../config.js';
|
||||||
import { fetchAssignment } from './assignments.js';
|
import { fetchAssignment } from './assignments.js';
|
||||||
|
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||||
|
|
||||||
export async function getQuestionsAboutLearningObjectInAssignment(
|
export async function getQuestionsAboutLearningObjectInAssignment(
|
||||||
loId: LearningObjectIdentifier,
|
loId: LearningObjectIdentifier,
|
||||||
|
|
@ -99,10 +100,18 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat
|
||||||
|
|
||||||
const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber);
|
const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber);
|
||||||
|
|
||||||
|
if (!inGroup) {
|
||||||
|
throw new NotFoundException('Group with id and assignment not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inGroup.members.contains(author)) {
|
||||||
|
throw new ConflictException('Author is not part of this group');
|
||||||
|
}
|
||||||
|
|
||||||
const question = await questionRepository.createQuestion({
|
const question = await questionRepository.createQuestion({
|
||||||
loId,
|
loId,
|
||||||
author,
|
author,
|
||||||
inGroup: inGroup!,
|
inGroup: inGroup,
|
||||||
content,
|
content,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/subm
|
||||||
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
||||||
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
|
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
|
||||||
import { ConflictException } from '../exceptions/conflict-exception.js';
|
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||||
import { Submission } from '../entities/assignments/submission.entity';
|
import { Submission } from '../entities/assignments/submission.entity.js';
|
||||||
|
import { mapToUsername } from '../interfaces/user.js';
|
||||||
|
|
||||||
export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
|
export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
|
||||||
const studentRepository = getStudentRepository();
|
const studentRepository = getStudentRepository();
|
||||||
|
|
@ -34,7 +35,7 @@ export async function getAllStudents(full: boolean): Promise<StudentDTO[] | stri
|
||||||
return users.map(mapToStudentDTO);
|
return users.map(mapToStudentDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
return users.map((user) => user.username);
|
return users.map(mapToUsername);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchStudent(username: string): Promise<Student> {
|
export async function fetchStudent(username: string): Promise<Student> {
|
||||||
|
|
@ -42,7 +43,7 @@ export async function fetchStudent(username: string): Promise<Student> {
|
||||||
const user = await studentRepository.findByUsername(username);
|
const user = await studentRepository.findByUsername(username);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('Student with username not found');
|
throw new NotFoundException(`Student with username ${username} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|
@ -64,7 +65,7 @@ export async function createStudent(userData: StudentDTO): Promise<StudentDTO> {
|
||||||
const newStudent = mapToStudent(userData);
|
const newStudent = mapToStudent(userData);
|
||||||
await studentRepository.save(newStudent, { preventOverwrite: true });
|
await studentRepository.save(newStudent, { preventOverwrite: true });
|
||||||
|
|
||||||
return userData;
|
return mapToStudentDTO(newStudent);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createOrUpdateStudent(userData: StudentDTO): Promise<StudentDTO> {
|
export async function createOrUpdateStudent(userData: StudentDTO): Promise<StudentDTO> {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ export async function createInvitation(data: TeacherInvitationData): Promise<Tea
|
||||||
throw new ConflictException('The teacher sending the invite is not part of the class');
|
throw new ConflictException('The teacher sending the invite is not part of the class');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cls.teachers.contains(receiver)) {
|
||||||
|
throw new ConflictException('The teacher receiving the invite is already part of the class');
|
||||||
|
}
|
||||||
|
|
||||||
const newInvitation = mapToInvitation(sender, receiver, cls);
|
const newInvitation = mapToInvitation(sender, receiver, cls);
|
||||||
await teacherInvitationRepository.save(newInvitation, { preventOverwrite: true });
|
await teacherInvitationRepository.save(newInvitation, { preventOverwrite: true });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,5 @@
|
||||||
import {
|
import { getClassJoinRequestRepository, getClassRepository, getTeacherRepository } from '../data/repositories.js';
|
||||||
getClassJoinRequestRepository,
|
|
||||||
getClassRepository,
|
|
||||||
getLearningObjectRepository,
|
|
||||||
getQuestionRepository,
|
|
||||||
getTeacherRepository,
|
|
||||||
} from '../data/repositories.js';
|
|
||||||
import { mapToClassDTO } from '../interfaces/class.js';
|
import { mapToClassDTO } from '../interfaces/class.js';
|
||||||
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
|
|
||||||
import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js';
|
import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js';
|
||||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||||
import { fetchStudent } from './students.js';
|
import { fetchStudent } from './students.js';
|
||||||
|
|
@ -15,10 +8,6 @@ import { mapToStudentRequestDTO } from '../interfaces/student-request.js';
|
||||||
import { TeacherRepository } from '../data/users/teacher-repository.js';
|
import { TeacherRepository } from '../data/users/teacher-repository.js';
|
||||||
import { ClassRepository } from '../data/classes/class-repository.js';
|
import { ClassRepository } from '../data/classes/class-repository.js';
|
||||||
import { Class } from '../entities/classes/class.entity.js';
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
import { LearningObjectRepository } from '../data/content/learning-object-repository.js';
|
|
||||||
import { LearningObject } from '../entities/content/learning-object.entity.js';
|
|
||||||
import { QuestionRepository } from '../data/questions/question-repository.js';
|
|
||||||
import { Question } from '../entities/questions/question.entity.js';
|
|
||||||
import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js';
|
import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js';
|
||||||
import { Student } from '../entities/users/student.entity.js';
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||||
|
|
@ -26,10 +15,10 @@ import { addClassStudent, fetchClass, getClassStudentsDTO } from './classes.js';
|
||||||
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
||||||
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
|
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
|
||||||
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
||||||
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
|
||||||
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
|
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
|
||||||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||||
import { ConflictException } from '../exceptions/conflict-exception.js';
|
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||||
|
import { mapToUsername } from '../interfaces/user.js';
|
||||||
|
|
||||||
export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> {
|
export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> {
|
||||||
const teacherRepository: TeacherRepository = getTeacherRepository();
|
const teacherRepository: TeacherRepository = getTeacherRepository();
|
||||||
|
|
@ -38,7 +27,7 @@ export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | stri
|
||||||
if (full) {
|
if (full) {
|
||||||
return users.map(mapToTeacherDTO);
|
return users.map(mapToTeacherDTO);
|
||||||
}
|
}
|
||||||
return users.map((user) => user.username);
|
return users.map(mapToUsername);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTeacher(username: string): Promise<Teacher> {
|
export async function fetchTeacher(username: string): Promise<Teacher> {
|
||||||
|
|
@ -57,7 +46,8 @@ export async function getTeacher(username: string): Promise<TeacherDTO> {
|
||||||
return mapToTeacherDTO(user);
|
return mapToTeacherDTO(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO> {
|
// TODO update parameter
|
||||||
|
export async function createTeacher(userData: TeacherDTO, _update?: boolean): Promise<TeacherDTO> {
|
||||||
const teacherRepository: TeacherRepository = getTeacherRepository();
|
const teacherRepository: TeacherRepository = getTeacherRepository();
|
||||||
|
|
||||||
const newTeacher = mapToTeacher(userData);
|
const newTeacher = mapToTeacher(userData);
|
||||||
|
|
@ -110,7 +100,9 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro
|
||||||
|
|
||||||
const classIds: string[] = classes.map((cls) => cls.id);
|
const classIds: string[] = classes.map((cls) => cls.id);
|
||||||
|
|
||||||
const students: StudentDTO[] = (await Promise.all(classIds.map(async (username) => await getClassStudentsDTO(username)))).flat();
|
const students: StudentDTO[] = (await Promise.all(classIds.map(async (classId) => await getClassStudentsDTO(classId))))
|
||||||
|
.flat()
|
||||||
|
.filter((student, index, self) => self.findIndex((s) => s.username === student.username) === index);
|
||||||
|
|
||||||
if (full) {
|
if (full) {
|
||||||
return students;
|
return students;
|
||||||
|
|
@ -119,28 +111,6 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro
|
||||||
return students.map((student) => student.username);
|
return students.map((student) => student.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
|
|
||||||
const teacher: Teacher = await fetchTeacher(username);
|
|
||||||
|
|
||||||
// Find all learning objects that this teacher manages
|
|
||||||
const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository();
|
|
||||||
const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher);
|
|
||||||
|
|
||||||
if (!learningObjects || learningObjects.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch all questions related to these learning objects
|
|
||||||
const questionRepository: QuestionRepository = getQuestionRepository();
|
|
||||||
const questions: Question[] = await questionRepository.findAllByLearningObjects(learningObjects);
|
|
||||||
|
|
||||||
if (full) {
|
|
||||||
return questions.map(mapToQuestionDTO);
|
|
||||||
}
|
|
||||||
|
|
||||||
return questions.map(mapToQuestionDTOId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getJoinRequestsByClass(classId: string): Promise<ClassJoinRequestDTO[]> {
|
export async function getJoinRequestsByClass(classId: string): Promise<ClassJoinRequestDTO[]> {
|
||||||
const classRepository: ClassRepository = getClassRepository();
|
const classRepository: ClassRepository = getClassRepository();
|
||||||
const cls: Class | null = await classRepository.findById(classId);
|
const cls: Class | null = await classRepository.findById(classId);
|
||||||
|
|
|
||||||
76
backend/tests/controllers/assignments.test.ts
Normal file
76
backend/tests/controllers/assignments.test.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { setupTestApp } from '../setup-tests.js';
|
||||||
|
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { getAssignmentHandler, getAllAssignmentsHandler, getAssignmentsSubmissionsHandler } from '../../src/controllers/assignments.js';
|
||||||
|
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||||
|
import { getClass01 } from '../test_assets/classes/classes.testdata';
|
||||||
|
import { getAssignment01 } from '../test_assets/assignments/assignments.testdata';
|
||||||
|
|
||||||
|
function createRequestObject(
|
||||||
|
classid: string,
|
||||||
|
assignmentid: string
|
||||||
|
): {
|
||||||
|
query: { full: string };
|
||||||
|
params: { classid: string; id: string };
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
classid: classid,
|
||||||
|
id: assignmentid,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
full: 'true',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Assignment controllers', () => {
|
||||||
|
let req: Partial<Request>;
|
||||||
|
let res: Partial<Response>;
|
||||||
|
|
||||||
|
let jsonMock: Mock;
|
||||||
|
let statusMock: Mock;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupTestApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jsonMock = vi.fn();
|
||||||
|
statusMock = vi.fn().mockReturnThis();
|
||||||
|
|
||||||
|
res = {
|
||||||
|
json: jsonMock,
|
||||||
|
status: statusMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return error non-existing assignment', async () => {
|
||||||
|
req = createRequestObject('doesnotexist', '43000'); // Should not exist
|
||||||
|
|
||||||
|
await expect(async () => getAssignmentHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an assignment', async () => {
|
||||||
|
const assignment = getAssignment01();
|
||||||
|
req = createRequestObject(assignment.within.classId as string, (assignment.id ?? 1).toString());
|
||||||
|
|
||||||
|
await getAssignmentHandler(req as Request, res as Response);
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ assignment: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of assignments', async () => {
|
||||||
|
req = createRequestObject(getClass01().classId as string, 'irrelevant');
|
||||||
|
|
||||||
|
await getAllAssignmentsHandler(req as Request, res as Response);
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ assignments: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of submissions for an assignment', async () => {
|
||||||
|
const assignment = getAssignment01();
|
||||||
|
req = createRequestObject(assignment.within.classId as string, (assignment.id ?? 1).toString());
|
||||||
|
|
||||||
|
await getAssignmentsSubmissionsHandler(req as Request, res as Response);
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
|
||||||
|
});
|
||||||
|
});
|
||||||
123
backend/tests/controllers/classes.test.ts
Normal file
123
backend/tests/controllers/classes.test.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { setupTestApp } from '../setup-tests.js';
|
||||||
|
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||||
|
import {
|
||||||
|
createClassHandler,
|
||||||
|
deleteClassHandler,
|
||||||
|
getAllClassesHandler,
|
||||||
|
getClassHandler,
|
||||||
|
getClassStudentsHandler,
|
||||||
|
getTeacherInvitationsHandler,
|
||||||
|
} from '../../src/controllers/classes.js';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||||
|
import { BadRequestException } from '../../src/exceptions/bad-request-exception';
|
||||||
|
import { getClass01 } from '../test_assets/classes/classes.testdata';
|
||||||
|
describe('Class controllers', () => {
|
||||||
|
let req: Partial<Request>;
|
||||||
|
let res: Partial<Response>;
|
||||||
|
|
||||||
|
let jsonMock: Mock;
|
||||||
|
let statusMock: Mock;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupTestApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jsonMock = vi.fn();
|
||||||
|
statusMock = vi.fn().mockReturnThis();
|
||||||
|
|
||||||
|
res = {
|
||||||
|
json: jsonMock,
|
||||||
|
status: statusMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create and delete class', async () => {
|
||||||
|
req = {
|
||||||
|
body: { displayName: 'coole_nieuwe_klas' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await createClassHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
const result = jsonMock.mock.lastCall?.[0];
|
||||||
|
// Console.log('class', result.class);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ class: expect.anything() }));
|
||||||
|
|
||||||
|
req = {
|
||||||
|
params: { id: result.class.id },
|
||||||
|
};
|
||||||
|
|
||||||
|
await deleteClassHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ class: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error class not found', async () => {
|
||||||
|
req = {
|
||||||
|
params: { id: 'doesnotexist' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getClassHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error create a class without name', async () => {
|
||||||
|
req = {
|
||||||
|
body: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => createClassHandler(req as Request, res as Response)).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return list of students', async () => {
|
||||||
|
req = {
|
||||||
|
params: { id: getClass01().classId as string },
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await getClassStudentsHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ students: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error students on a non-existent class', async () => {
|
||||||
|
req = {
|
||||||
|
params: { id: 'doesnotexist' },
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getClassStudentsHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 200 and a list of teacher-invitations', async () => {
|
||||||
|
const classId = getClass01().classId as string;
|
||||||
|
req = {
|
||||||
|
params: { id: classId },
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await getTeacherInvitationsHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error teacher-invitations on a non-existent class', async () => {
|
||||||
|
req = {
|
||||||
|
params: { id: 'doesnotexist' },
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getTeacherInvitationsHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of classes', async () => {
|
||||||
|
req = {
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await getAllClassesHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ classes: expect.anything() }));
|
||||||
|
});
|
||||||
|
});
|
||||||
140
backend/tests/controllers/groups.test.ts
Normal file
140
backend/tests/controllers/groups.test.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
import { setupTestApp } from '../setup-tests.js';
|
||||||
|
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import {
|
||||||
|
createGroupHandler,
|
||||||
|
deleteGroupHandler,
|
||||||
|
getAllGroupsHandler,
|
||||||
|
getGroupHandler,
|
||||||
|
getGroupSubmissionsHandler,
|
||||||
|
} from '../../src/controllers/groups.js';
|
||||||
|
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||||
|
import { getClass01 } from '../test_assets/classes/classes.testdata';
|
||||||
|
import { getAssignment01, getAssignment02 } from '../test_assets/assignments/assignments.testdata';
|
||||||
|
import { getTestGroup01 } from '../test_assets/assignments/groups.testdata';
|
||||||
|
|
||||||
|
function createRequestObject(
|
||||||
|
classid: string,
|
||||||
|
assignmentid: string,
|
||||||
|
groupNumber: string
|
||||||
|
): {
|
||||||
|
query: { full: string };
|
||||||
|
params: { classid: string; groupid: string; assignmentid: string };
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
classid: classid,
|
||||||
|
assignmentid: assignmentid,
|
||||||
|
groupid: groupNumber,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
full: 'true',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Group controllers', () => {
|
||||||
|
let req: Partial<Request>;
|
||||||
|
let res: Partial<Response>;
|
||||||
|
|
||||||
|
let jsonMock: Mock;
|
||||||
|
let statusMock: Mock;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupTestApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jsonMock = vi.fn();
|
||||||
|
statusMock = vi.fn().mockReturnThis();
|
||||||
|
|
||||||
|
res = {
|
||||||
|
json: jsonMock,
|
||||||
|
status: statusMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error not found on a non-existing group', async () => {
|
||||||
|
req = {
|
||||||
|
params: {
|
||||||
|
classid: 'id01',
|
||||||
|
assignmentid: '1',
|
||||||
|
groupid: '154981', // Should not exist
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 404 not found on a non-existing assignment', async () => {
|
||||||
|
req = {
|
||||||
|
params: {
|
||||||
|
classid: 'id01',
|
||||||
|
assignmentid: '1000', // Should not exist
|
||||||
|
groupid: '42000', // Should not exist
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 404 not found ont a non-existing class', async () => {
|
||||||
|
req = {
|
||||||
|
params: {
|
||||||
|
classid: 'doesnotexist', // Should not exist
|
||||||
|
assignmentid: '1000', // Should not exist
|
||||||
|
groupid: '42000', // Should not exist
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an existing group', async () => {
|
||||||
|
const group = getTestGroup01();
|
||||||
|
const classId = getClass01().classId as string;
|
||||||
|
req = createRequestObject(classId, (group.assignment.id ?? 1).toString(), (group.groupNumber ?? 1).toString());
|
||||||
|
|
||||||
|
await getGroupHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ group: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Create and delete', async () => {
|
||||||
|
const assignment = getAssignment02();
|
||||||
|
const classId = assignment.within.classId as string;
|
||||||
|
req = createRequestObject(classId, (assignment.id ?? 1).toString(), '1');
|
||||||
|
req.body = {
|
||||||
|
members: ['Noordkaap', 'DireStraits'],
|
||||||
|
};
|
||||||
|
|
||||||
|
await createGroupHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
await deleteGroupHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ group: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the submissions for a group', async () => {
|
||||||
|
const group = getTestGroup01();
|
||||||
|
const classId = getClass01().classId as string;
|
||||||
|
req = createRequestObject(classId, (group.assignment.id ?? 1).toString(), (group.groupNumber ?? 1).toString());
|
||||||
|
|
||||||
|
await getGroupSubmissionsHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of groups for an assignment', async () => {
|
||||||
|
const assignment = getAssignment01();
|
||||||
|
const classId = assignment.within.classId as string;
|
||||||
|
req = createRequestObject(classId, (assignment.id ?? 1).toString(), '1');
|
||||||
|
|
||||||
|
await getAllGroupsHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ groups: expect.anything() }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -21,6 +21,7 @@ import { BadRequestException } from '../../src/exceptions/bad-request-exception.
|
||||||
import { ConflictException } from '../../src/exceptions/conflict-exception.js';
|
import { ConflictException } from '../../src/exceptions/conflict-exception.js';
|
||||||
import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js';
|
import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js';
|
||||||
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
||||||
|
import { getClass02 } from '../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
describe('Student controllers', () => {
|
describe('Student controllers', () => {
|
||||||
let req: Partial<Request>;
|
let req: Partial<Request>;
|
||||||
|
|
@ -186,7 +187,7 @@ describe('Student controllers', () => {
|
||||||
|
|
||||||
it('Get join request by student and class', async () => {
|
it('Get join request by student and class', async () => {
|
||||||
req = {
|
req = {
|
||||||
params: { username: 'PinkFloyd', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
params: { username: 'PinkFloyd', classId: getClass02().classId },
|
||||||
};
|
};
|
||||||
|
|
||||||
await getStudentRequestHandler(req as Request, res as Response);
|
await getStudentRequestHandler(req as Request, res as Response);
|
||||||
|
|
@ -201,7 +202,7 @@ describe('Student controllers', () => {
|
||||||
it('Create and delete join request', async () => {
|
it('Create and delete join request', async () => {
|
||||||
req = {
|
req = {
|
||||||
params: { username: 'TheDoors' },
|
params: { username: 'TheDoors' },
|
||||||
body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
body: { classId: getClass02().classId },
|
||||||
};
|
};
|
||||||
|
|
||||||
await createStudentRequestHandler(req as Request, res as Response);
|
await createStudentRequestHandler(req as Request, res as Response);
|
||||||
|
|
@ -209,7 +210,7 @@ describe('Student controllers', () => {
|
||||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() }));
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() }));
|
||||||
|
|
||||||
req = {
|
req = {
|
||||||
params: { username: 'TheDoors', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
params: { username: 'TheDoors', classId: getClass02().classId },
|
||||||
};
|
};
|
||||||
|
|
||||||
await deleteClassJoinRequestHandler(req as Request, res as Response);
|
await deleteClassJoinRequestHandler(req as Request, res as Response);
|
||||||
|
|
@ -222,7 +223,7 @@ describe('Student controllers', () => {
|
||||||
it('Create join request student already in class error', async () => {
|
it('Create join request student already in class error', async () => {
|
||||||
req = {
|
req = {
|
||||||
params: { username: 'Noordkaap' },
|
params: { username: 'Noordkaap' },
|
||||||
body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
body: { classId: getClass02().classId },
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException);
|
await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException);
|
||||||
|
|
@ -231,7 +232,7 @@ describe('Student controllers', () => {
|
||||||
it('Create join request duplicate', async () => {
|
it('Create join request duplicate', async () => {
|
||||||
req = {
|
req = {
|
||||||
params: { username: 'Tool' },
|
params: { username: 'Tool' },
|
||||||
body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
body: { classId: getClass02().classId },
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException);
|
await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException);
|
||||||
|
|
|
||||||
61
backend/tests/controllers/submissions.test.ts
Normal file
61
backend/tests/controllers/submissions.test.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { setupTestApp } from '../setup-tests.js';
|
||||||
|
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||||
|
import { getSubmissionHandler, getAllSubmissionsHandler } from '../../src/controllers/submissions.js';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||||
|
import { getClass02 } from '../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
|
function createRequestObject(
|
||||||
|
hruid: string,
|
||||||
|
submissionNumber: string
|
||||||
|
): {
|
||||||
|
query: { language: string; version: string };
|
||||||
|
params: { hruid: string; id: string };
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
hruid: hruid,
|
||||||
|
id: submissionNumber,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
language: 'en',
|
||||||
|
version: '1',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Submission controllers', () => {
|
||||||
|
let req: Partial<Request>;
|
||||||
|
let res: Partial<Response>;
|
||||||
|
|
||||||
|
let jsonMock: Mock;
|
||||||
|
let statusMock: Mock;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupTestApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jsonMock = vi.fn();
|
||||||
|
statusMock = vi.fn().mockReturnThis();
|
||||||
|
|
||||||
|
res = {
|
||||||
|
json: jsonMock,
|
||||||
|
status: statusMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('error submission is not found', async () => {
|
||||||
|
req = createRequestObject('id01', '1000000');
|
||||||
|
|
||||||
|
await expect(async () => getSubmissionHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of submissions for a learning object', async () => {
|
||||||
|
req = createRequestObject(getClass02().classId as string, 'irrelevant');
|
||||||
|
|
||||||
|
await getAllSubmissionsHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -12,6 +12,7 @@ import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invit
|
||||||
import { getClassHandler } from '../../src/controllers/classes';
|
import { getClassHandler } from '../../src/controllers/classes';
|
||||||
import { BadRequestException } from '../../src/exceptions/bad-request-exception';
|
import { BadRequestException } from '../../src/exceptions/bad-request-exception';
|
||||||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||||
|
import { getClass02 } from '../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
describe('Teacher controllers', () => {
|
describe('Teacher controllers', () => {
|
||||||
let req: Partial<Request>;
|
let req: Partial<Request>;
|
||||||
|
|
@ -57,7 +58,7 @@ describe('Teacher controllers', () => {
|
||||||
const body = {
|
const body = {
|
||||||
sender: 'LimpBizkit',
|
sender: 'LimpBizkit',
|
||||||
receiver: 'testleerkracht1',
|
receiver: 'testleerkracht1',
|
||||||
class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89',
|
class: getClass02().classId,
|
||||||
} as TeacherInvitationData;
|
} as TeacherInvitationData;
|
||||||
req = { body };
|
req = { body };
|
||||||
|
|
||||||
|
|
@ -67,7 +68,7 @@ describe('Teacher controllers', () => {
|
||||||
params: {
|
params: {
|
||||||
sender: 'LimpBizkit',
|
sender: 'LimpBizkit',
|
||||||
receiver: 'testleerkracht1',
|
receiver: 'testleerkracht1',
|
||||||
classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89',
|
classId: getClass02().classId,
|
||||||
},
|
},
|
||||||
body: { accepted: 'false' },
|
body: { accepted: 'false' },
|
||||||
};
|
};
|
||||||
|
|
@ -80,7 +81,7 @@ describe('Teacher controllers', () => {
|
||||||
params: {
|
params: {
|
||||||
sender: 'LimpBizkit',
|
sender: 'LimpBizkit',
|
||||||
receiver: 'FooFighters',
|
receiver: 'FooFighters',
|
||||||
classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89',
|
classId: getClass02().classId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await getInvitationHandler(req as Request, res as Response);
|
await getInvitationHandler(req as Request, res as Response);
|
||||||
|
|
@ -100,7 +101,7 @@ describe('Teacher controllers', () => {
|
||||||
const body = {
|
const body = {
|
||||||
sender: 'LimpBizkit',
|
sender: 'LimpBizkit',
|
||||||
receiver: 'FooFighters',
|
receiver: 'FooFighters',
|
||||||
class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89',
|
class: getClass02().classId,
|
||||||
} as TeacherInvitationData;
|
} as TeacherInvitationData;
|
||||||
req = { body };
|
req = { body };
|
||||||
|
|
||||||
|
|
@ -111,7 +112,7 @@ describe('Teacher controllers', () => {
|
||||||
|
|
||||||
req = {
|
req = {
|
||||||
params: {
|
params: {
|
||||||
id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89',
|
id: getClass02().classId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ import {
|
||||||
import { BadRequestException } from '../../src/exceptions/bad-request-exception.js';
|
import { BadRequestException } from '../../src/exceptions/bad-request-exception.js';
|
||||||
import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js';
|
import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js';
|
||||||
import { getStudentRequestsHandler } from '../../src/controllers/students.js';
|
import { getStudentRequestsHandler } from '../../src/controllers/students.js';
|
||||||
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
|
||||||
import { getClassHandler } from '../../src/controllers/classes';
|
import { getClassHandler } from '../../src/controllers/classes';
|
||||||
|
import { getClass02 } from '../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
describe('Teacher controllers', () => {
|
describe('Teacher controllers', () => {
|
||||||
let req: Partial<Request>;
|
let req: Partial<Request>;
|
||||||
|
|
@ -96,7 +96,7 @@ describe('Teacher controllers', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Teacher list', async () => {
|
it('Teacher list', async () => {
|
||||||
req = { query: { full: 'true' } };
|
req = { query: { full: 'false' } };
|
||||||
|
|
||||||
await getAllTeachersHandler(req as Request, res as Response);
|
await getAllTeachersHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
|
@ -104,8 +104,7 @@ describe('Teacher controllers', () => {
|
||||||
|
|
||||||
const result = jsonMock.mock.lastCall?.[0];
|
const result = jsonMock.mock.lastCall?.[0];
|
||||||
|
|
||||||
const teacherUsernames = result.teachers.map((s: TeacherDTO) => s.username);
|
expect(result.teachers).toContain('testleerkracht1');
|
||||||
expect(teacherUsernames).toContain('testleerkracht1');
|
|
||||||
|
|
||||||
expect(result.teachers).toHaveLength(5);
|
expect(result.teachers).toHaveLength(5);
|
||||||
});
|
});
|
||||||
|
|
@ -169,7 +168,7 @@ describe('Teacher controllers', () => {
|
||||||
|
|
||||||
it('Get join requests by class', async () => {
|
it('Get join requests by class', async () => {
|
||||||
req = {
|
req = {
|
||||||
params: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
params: { classId: getClass02().classId },
|
||||||
};
|
};
|
||||||
|
|
||||||
await getStudentJoinRequestHandler(req as Request, res as Response);
|
await getStudentJoinRequestHandler(req as Request, res as Response);
|
||||||
|
|
@ -183,7 +182,7 @@ describe('Teacher controllers', () => {
|
||||||
|
|
||||||
it('Update join request status', async () => {
|
it('Update join request status', async () => {
|
||||||
req = {
|
req = {
|
||||||
params: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', studentUsername: 'PinkFloyd' },
|
params: { classId: getClass02().classId, studentUsername: 'PinkFloyd' },
|
||||||
body: { accepted: 'true' },
|
body: { accepted: 'true' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -201,7 +200,7 @@ describe('Teacher controllers', () => {
|
||||||
expect(status).toBeTruthy();
|
expect(status).toBeTruthy();
|
||||||
|
|
||||||
req = {
|
req = {
|
||||||
params: { id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' },
|
params: { id: getClass02().classId },
|
||||||
};
|
};
|
||||||
|
|
||||||
await getClassHandler(req as Request, res as Response);
|
await getClassHandler(req as Request, res as Response);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { setupTestApp } from '../../setup-tests';
|
||||||
import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository';
|
import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository';
|
||||||
import { getAssignmentRepository, getClassRepository } from '../../../src/data/repositories';
|
import { getAssignmentRepository, getClassRepository } from '../../../src/data/repositories';
|
||||||
import { ClassRepository } from '../../../src/data/classes/class-repository';
|
import { ClassRepository } from '../../../src/data/classes/class-repository';
|
||||||
|
import { getClass02 } from '../../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
describe('AssignmentRepository', () => {
|
describe('AssignmentRepository', () => {
|
||||||
let assignmentRepository: AssignmentRepository;
|
let assignmentRepository: AssignmentRepository;
|
||||||
|
|
@ -15,7 +16,7 @@ describe('AssignmentRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the requested assignment', async () => {
|
it('should return the requested assignment', async () => {
|
||||||
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
|
const class_ = await classRepository.findById(getClass02().classId);
|
||||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 21001);
|
const assignment = await assignmentRepository.findByClassAndId(class_!, 21001);
|
||||||
|
|
||||||
expect(assignment).toBeTruthy();
|
expect(assignment).toBeTruthy();
|
||||||
|
|
@ -23,7 +24,7 @@ describe('AssignmentRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return all assignments for a class', async () => {
|
it('should return all assignments for a class', async () => {
|
||||||
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
|
const class_ = await classRepository.findById(getClass02().classId);
|
||||||
const assignments = await assignmentRepository.findAllAssignmentsInClass(class_!);
|
const assignments = await assignmentRepository.findAllAssignmentsInClass(class_!);
|
||||||
|
|
||||||
expect(assignments).toBeTruthy();
|
expect(assignments).toBeTruthy();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { GroupRepository } from '../../../src/data/assignments/group-repository'
|
||||||
import { getAssignmentRepository, getClassRepository, getGroupRepository } from '../../../src/data/repositories';
|
import { getAssignmentRepository, getClassRepository, getGroupRepository } from '../../../src/data/repositories';
|
||||||
import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository';
|
import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository';
|
||||||
import { ClassRepository } from '../../../src/data/classes/class-repository';
|
import { ClassRepository } from '../../../src/data/classes/class-repository';
|
||||||
|
import { getClass01, getClass02 } from '../../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
describe('GroupRepository', () => {
|
describe('GroupRepository', () => {
|
||||||
let groupRepository: GroupRepository;
|
let groupRepository: GroupRepository;
|
||||||
|
|
@ -18,7 +19,8 @@ describe('GroupRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the requested group', async () => {
|
it('should return the requested group', async () => {
|
||||||
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
const id = getClass01().classId;
|
||||||
|
const class_ = await classRepository.findById(id);
|
||||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
|
const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
|
||||||
|
|
||||||
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001);
|
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001);
|
||||||
|
|
@ -27,7 +29,7 @@ describe('GroupRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return all groups for assignment', async () => {
|
it('should return all groups for assignment', async () => {
|
||||||
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
const class_ = await classRepository.findById(getClass01().classId);
|
||||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
|
const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
|
||||||
|
|
||||||
const groups = await groupRepository.findAllGroupsForAssignment(assignment!);
|
const groups = await groupRepository.findAllGroupsForAssignment(assignment!);
|
||||||
|
|
@ -37,7 +39,7 @@ describe('GroupRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not find removed group', async () => {
|
it('should not find removed group', async () => {
|
||||||
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
|
const class_ = await classRepository.findById(getClass02().classId);
|
||||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 21001);
|
const assignment = await assignmentRepository.findByClassAndId(class_!, 21001);
|
||||||
|
|
||||||
await groupRepository.deleteByAssignmentAndGroupNumber(assignment!, 21001);
|
await groupRepository.deleteByAssignmentAndGroupNumber(assignment!, 21001);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { Submission } from '../../../src/entities/assignments/submission.entity'
|
||||||
import { Class } from '../../../src/entities/classes/class.entity';
|
import { Class } from '../../../src/entities/classes/class.entity';
|
||||||
import { Assignment } from '../../../src/entities/assignments/assignment.entity';
|
import { Assignment } from '../../../src/entities/assignments/assignment.entity';
|
||||||
import { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata';
|
import { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata';
|
||||||
|
import { getClass01 } from '../../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
describe('SubmissionRepository', () => {
|
describe('SubmissionRepository', () => {
|
||||||
let submissionRepository: SubmissionRepository;
|
let submissionRepository: SubmissionRepository;
|
||||||
|
|
@ -54,7 +55,7 @@ describe('SubmissionRepository', () => {
|
||||||
|
|
||||||
it('should find the most recent submission for a group', async () => {
|
it('should find the most recent submission for a group', async () => {
|
||||||
const id = new LearningObjectIdentifier('id03', Language.English, 1);
|
const id = new LearningObjectIdentifier('id03', Language.English, 1);
|
||||||
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
const class_ = await classRepository.findById(getClass01().classId);
|
||||||
const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
|
const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
|
||||||
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001);
|
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001);
|
||||||
const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, group!);
|
const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, group!);
|
||||||
|
|
@ -67,7 +68,7 @@ describe('SubmissionRepository', () => {
|
||||||
let assignment: Assignment | null;
|
let assignment: Assignment | null;
|
||||||
let loId: LearningObjectIdentifier;
|
let loId: LearningObjectIdentifier;
|
||||||
it('should find all submissions for a certain learning object and assignment', async () => {
|
it('should find all submissions for a certain learning object and assignment', async () => {
|
||||||
clazz = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
clazz = await classRepository.findById(getClass01().classId);
|
||||||
assignment = await assignmentRepository.findByClassAndId(clazz!, 21000);
|
assignment = await assignmentRepository.findByClassAndId(clazz!, 21000);
|
||||||
loId = {
|
loId = {
|
||||||
hruid: 'id02',
|
hruid: 'id02',
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { ClassJoinRequestRepository } from '../../../src/data/classes/class-join
|
||||||
import { getClassJoinRequestRepository, getClassRepository, getStudentRepository } from '../../../src/data/repositories';
|
import { getClassJoinRequestRepository, getClassRepository, getStudentRepository } from '../../../src/data/repositories';
|
||||||
import { StudentRepository } from '../../../src/data/users/student-repository';
|
import { StudentRepository } from '../../../src/data/users/student-repository';
|
||||||
import { ClassRepository } from '../../../src/data/classes/class-repository';
|
import { ClassRepository } from '../../../src/data/classes/class-repository';
|
||||||
|
import { getClass02, getClass03 } from '../../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
describe('ClassJoinRequestRepository', () => {
|
describe('ClassJoinRequestRepository', () => {
|
||||||
let classJoinRequestRepository: ClassJoinRequestRepository;
|
let classJoinRequestRepository: ClassJoinRequestRepository;
|
||||||
|
|
@ -26,7 +27,7 @@ describe('ClassJoinRequestRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should list all requests to a single class', async () => {
|
it('should list all requests to a single class', async () => {
|
||||||
const class_ = await cassRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
|
const class_ = await cassRepository.findById(getClass02().classId);
|
||||||
const requests = await classJoinRequestRepository.findAllOpenRequestsTo(class_!);
|
const requests = await classJoinRequestRepository.findAllOpenRequestsTo(class_!);
|
||||||
|
|
||||||
expect(requests).toBeTruthy();
|
expect(requests).toBeTruthy();
|
||||||
|
|
@ -35,7 +36,7 @@ describe('ClassJoinRequestRepository', () => {
|
||||||
|
|
||||||
it('should not find a removed request', async () => {
|
it('should not find a removed request', async () => {
|
||||||
const student = await studentRepository.findByUsername('SmashingPumpkins');
|
const student = await studentRepository.findByUsername('SmashingPumpkins');
|
||||||
const class_ = await cassRepository.findById('80dcc3e0-1811-4091-9361-42c0eee91cfa');
|
const class_ = await cassRepository.findById(getClass03().classId);
|
||||||
await classJoinRequestRepository.deleteBy(student!, class_!);
|
await classJoinRequestRepository.deleteBy(student!, class_!);
|
||||||
|
|
||||||
const request = await classJoinRequestRepository.findAllRequestsBy(student!);
|
const request = await classJoinRequestRepository.findAllRequestsBy(student!);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
import { ClassRepository } from '../../../src/data/classes/class-repository';
|
import { ClassRepository } from '../../../src/data/classes/class-repository';
|
||||||
import { setupTestApp } from '../../setup-tests';
|
import { setupTestApp } from '../../setup-tests';
|
||||||
import { getClassRepository } from '../../../src/data/repositories';
|
import { getClassRepository } from '../../../src/data/repositories';
|
||||||
|
import { getClass01, getClass04 } from '../../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
describe('ClassRepository', () => {
|
describe('ClassRepository', () => {
|
||||||
let classRepository: ClassRepository;
|
let classRepository: ClassRepository;
|
||||||
|
|
@ -18,16 +19,16 @@ describe('ClassRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return requested class', async () => {
|
it('should return requested class', async () => {
|
||||||
const classVar = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
const classVar = await classRepository.findById(getClass01().classId);
|
||||||
|
|
||||||
expect(classVar).toBeTruthy();
|
expect(classVar).toBeTruthy();
|
||||||
expect(classVar?.displayName).toBe('class01');
|
expect(classVar?.displayName).toBe('class01');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('class should be gone after deletion', async () => {
|
it('class should be gone after deletion', async () => {
|
||||||
await classRepository.deleteById('33d03536-83b8-4880-9982-9bbf2f908ddf');
|
await classRepository.deleteById(getClass04().classId);
|
||||||
|
|
||||||
const classVar = await classRepository.findById('33d03536-83b8-4880-9982-9bbf2f908ddf');
|
const classVar = await classRepository.findById(getClass04().classId);
|
||||||
|
|
||||||
expect(classVar).toBeNull();
|
expect(classVar).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { getClassRepository, getTeacherInvitationRepository, getTeacherRepositor
|
||||||
import { TeacherInvitationRepository } from '../../../src/data/classes/teacher-invitation-repository';
|
import { TeacherInvitationRepository } from '../../../src/data/classes/teacher-invitation-repository';
|
||||||
import { TeacherRepository } from '../../../src/data/users/teacher-repository';
|
import { TeacherRepository } from '../../../src/data/users/teacher-repository';
|
||||||
import { ClassRepository } from '../../../src/data/classes/class-repository';
|
import { ClassRepository } from '../../../src/data/classes/class-repository';
|
||||||
|
import { getClass01, getClass02 } from '../../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
describe('ClassRepository', () => {
|
describe('ClassRepository', () => {
|
||||||
let teacherInvitationRepository: TeacherInvitationRepository;
|
let teacherInvitationRepository: TeacherInvitationRepository;
|
||||||
|
|
@ -34,7 +35,7 @@ describe('ClassRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return all invitations for a class', async () => {
|
it('should return all invitations for a class', async () => {
|
||||||
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89');
|
const class_ = await classRepository.findById(getClass02().classId);
|
||||||
const invitations = await teacherInvitationRepository.findAllInvitationsForClass(class_!);
|
const invitations = await teacherInvitationRepository.findAllInvitationsForClass(class_!);
|
||||||
|
|
||||||
expect(invitations).toBeTruthy();
|
expect(invitations).toBeTruthy();
|
||||||
|
|
@ -42,7 +43,7 @@ describe('ClassRepository', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not find a removed invitation', async () => {
|
it('should not find a removed invitation', async () => {
|
||||||
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
const class_ = await classRepository.findById(getClass01().classId);
|
||||||
const sender = await teacherRepository.findByUsername('FooFighters');
|
const sender = await teacherRepository.findByUsername('FooFighters');
|
||||||
const receiver = await teacherRepository.findByUsername('LimpBizkit');
|
const receiver = await teacherRepository.findByUsername('LimpBizkit');
|
||||||
await teacherInvitationRepository.deleteBy(class_!, sender!, receiver!);
|
await teacherInvitationRepository.deleteBy(class_!, sender!, receiver!);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { Language } from '@dwengo-1/common/util/language';
|
||||||
import { Question } from '../../../src/entities/questions/question.entity';
|
import { Question } from '../../../src/entities/questions/question.entity';
|
||||||
import { Class } from '../../../src/entities/classes/class.entity';
|
import { Class } from '../../../src/entities/classes/class.entity';
|
||||||
import { Assignment } from '../../../src/entities/assignments/assignment.entity';
|
import { Assignment } from '../../../src/entities/assignments/assignment.entity';
|
||||||
|
import { getClass01 } from '../../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
describe('QuestionRepository', () => {
|
describe('QuestionRepository', () => {
|
||||||
let questionRepository: QuestionRepository;
|
let questionRepository: QuestionRepository;
|
||||||
|
|
@ -37,7 +38,7 @@ describe('QuestionRepository', () => {
|
||||||
const id = new LearningObjectIdentifier('id03', Language.English, 1);
|
const id = new LearningObjectIdentifier('id03', Language.English, 1);
|
||||||
const student = await studentRepository.findByUsername('Noordkaap');
|
const student = await studentRepository.findByUsername('Noordkaap');
|
||||||
|
|
||||||
const clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
const clazz = await getClassRepository().findById(getClass01().classId);
|
||||||
const assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000);
|
const assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000);
|
||||||
const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, 21001);
|
const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, 21001);
|
||||||
await questionRepository.createQuestion({
|
await questionRepository.createQuestion({
|
||||||
|
|
@ -56,7 +57,7 @@ describe('QuestionRepository', () => {
|
||||||
let assignment: Assignment | null;
|
let assignment: Assignment | null;
|
||||||
let loId: LearningObjectIdentifier;
|
let loId: LearningObjectIdentifier;
|
||||||
it('should find all questions for a certain learning object and assignment', async () => {
|
it('should find all questions for a certain learning object and assignment', async () => {
|
||||||
clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9');
|
clazz = await getClassRepository().findById(getClass01().classId);
|
||||||
assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000);
|
assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000);
|
||||||
loId = {
|
loId = {
|
||||||
hruid: 'id05',
|
hruid: 'id05',
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,20 @@ import { testLearningPathWithConditions } from '../content/learning-paths.testda
|
||||||
import { getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata';
|
import { getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata';
|
||||||
|
|
||||||
export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] {
|
export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] {
|
||||||
|
const futureDate = new Date();
|
||||||
|
futureDate.setDate(futureDate.getDate() + 7);
|
||||||
|
const pastDate = new Date();
|
||||||
|
pastDate.setDate(pastDate.getDate() - 7);
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(23, 59);
|
||||||
assignment01 = em.create(Assignment, {
|
assignment01 = em.create(Assignment, {
|
||||||
id: 21000,
|
id: 21000,
|
||||||
within: classes[0],
|
within: classes[0],
|
||||||
title: 'dire straits',
|
title: 'dire straits',
|
||||||
description: 'reading',
|
description: 'reading',
|
||||||
learningPathHruid: 'id02',
|
learningPathHruid: 'un_ai',
|
||||||
learningPathLanguage: Language.English,
|
learningPathLanguage: Language.English,
|
||||||
|
deadline: today,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -23,6 +30,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
|
||||||
description: 'reading',
|
description: 'reading',
|
||||||
learningPathHruid: 'id01',
|
learningPathHruid: 'id01',
|
||||||
learningPathLanguage: Language.English,
|
learningPathLanguage: Language.English,
|
||||||
|
deadline: futureDate,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -33,6 +41,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
|
||||||
description: 'will be deleted',
|
description: 'will be deleted',
|
||||||
learningPathHruid: 'id02',
|
learningPathHruid: 'id02',
|
||||||
learningPathLanguage: Language.English,
|
learningPathLanguage: Language.English,
|
||||||
|
deadline: pastDate,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -43,6 +52,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
|
||||||
description: 'with a description',
|
description: 'with a description',
|
||||||
learningPathHruid: 'id01',
|
learningPathHruid: 'id01',
|
||||||
learningPathLanguage: Language.English,
|
learningPathLanguage: Language.English,
|
||||||
|
deadline: pastDate,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -53,6 +63,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
|
||||||
description: 'You have to do the testing learning path with a condition.',
|
description: 'You have to do the testing learning path with a condition.',
|
||||||
learningPathHruid: testLearningPathWithConditions.hruid,
|
learningPathHruid: testLearningPathWithConditions.hruid,
|
||||||
learningPathLanguage: testLearningPathWithConditions.language as Language,
|
learningPathLanguage: testLearningPathWithConditions.language as Language,
|
||||||
|
deadline: futureDate,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
|
||||||
const teacherClass01: Teacher[] = teachers.slice(4, 5);
|
const teacherClass01: Teacher[] = teachers.slice(4, 5);
|
||||||
|
|
||||||
class01 = em.create(Class, {
|
class01 = em.create(Class, {
|
||||||
classId: '8764b861-90a6-42e5-9732-c0d9eb2f55f9',
|
classId: 'X2J9QT', // 8764b861-90a6-42e5-9732-c0d9eb2f55f9
|
||||||
displayName: 'class01',
|
displayName: 'class01',
|
||||||
teachers: teacherClass01,
|
teachers: teacherClass01,
|
||||||
students: studentsClass01,
|
students: studentsClass01,
|
||||||
|
|
@ -20,7 +20,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
|
||||||
const teacherClass02: Teacher[] = teachers.slice(1, 2);
|
const teacherClass02: Teacher[] = teachers.slice(1, 2);
|
||||||
|
|
||||||
class02 = em.create(Class, {
|
class02 = em.create(Class, {
|
||||||
classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89',
|
classId: '7KLPMA', // 34d484a1-295f-4e9f-bfdc-3e7a23d86a89
|
||||||
displayName: 'class02',
|
displayName: 'class02',
|
||||||
teachers: teacherClass02,
|
teachers: teacherClass02,
|
||||||
students: studentsClass02,
|
students: studentsClass02,
|
||||||
|
|
@ -30,7 +30,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
|
||||||
const teacherClass03: Teacher[] = teachers.slice(2, 3);
|
const teacherClass03: Teacher[] = teachers.slice(2, 3);
|
||||||
|
|
||||||
class03 = em.create(Class, {
|
class03 = em.create(Class, {
|
||||||
classId: '80dcc3e0-1811-4091-9361-42c0eee91cfa',
|
classId: 'R0D3UZ', // 80dcc3e0-1811-4091-9361-42c0eee91cfa
|
||||||
displayName: 'class03',
|
displayName: 'class03',
|
||||||
teachers: teacherClass03,
|
teachers: teacherClass03,
|
||||||
students: studentsClass03,
|
students: studentsClass03,
|
||||||
|
|
@ -40,14 +40,14 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
|
||||||
const teacherClass04: Teacher[] = teachers.slice(2, 3);
|
const teacherClass04: Teacher[] = teachers.slice(2, 3);
|
||||||
|
|
||||||
class04 = em.create(Class, {
|
class04 = em.create(Class, {
|
||||||
classId: '33d03536-83b8-4880-9982-9bbf2f908ddf',
|
classId: 'Q8N5YC', // 33d03536-83b8-4880-9982-9bbf2f908ddf
|
||||||
displayName: 'class04',
|
displayName: 'class04',
|
||||||
teachers: teacherClass04,
|
teachers: teacherClass04,
|
||||||
students: studentsClass04,
|
students: studentsClass04,
|
||||||
});
|
});
|
||||||
|
|
||||||
classWithTestleerlingAndTestleerkracht = em.create(Class, {
|
classWithTestleerlingAndTestleerkracht = em.create(Class, {
|
||||||
classId: 'a75298b5-18aa-471d-8eeb-5d77eb989393',
|
classId: 'ZAV71B', // Was a75298b5-18aa-471d-8eeb-5d77eb989393
|
||||||
displayName: 'Testklasse',
|
displayName: 'Testklasse',
|
||||||
teachers: [getTestleerkracht1()],
|
teachers: [getTestleerkracht1()],
|
||||||
students: [getTestleerling1()],
|
students: [getTestleerling1()],
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export interface AssignmentDTO {
|
||||||
description: string;
|
description: string;
|
||||||
learningPath: string;
|
learningPath: string;
|
||||||
language: string;
|
language: string;
|
||||||
|
deadline: Date;
|
||||||
groups: GroupDTO[] | string[][];
|
groups: GroupDTO[] | string[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ export interface QuestionDTO {
|
||||||
|
|
||||||
export interface QuestionData {
|
export interface QuestionData {
|
||||||
author?: string;
|
author?: string;
|
||||||
content: string;
|
|
||||||
inGroup: GroupDTO;
|
inGroup: GroupDTO;
|
||||||
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuestionId {
|
export interface QuestionId {
|
||||||
|
|
|
||||||
4
common/src/util/account-types.ts
Normal file
4
common/src/util/account-types.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum AccountType {
|
||||||
|
Student = 'student',
|
||||||
|
Teacher = 'teacher',
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,59 @@ const doc = {
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
student: {
|
studentDev: {
|
||||||
|
type: 'oauth2',
|
||||||
|
flows: {
|
||||||
|
implicit: {
|
||||||
|
authorizationUrl: 'http://localhost:7080/realms/student/protocol/openid-connect/auth',
|
||||||
|
scopes: {
|
||||||
|
openid: 'openid',
|
||||||
|
profile: 'profile',
|
||||||
|
email: 'email',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
teacherDev: {
|
||||||
|
type: 'oauth2',
|
||||||
|
flows: {
|
||||||
|
implicit: {
|
||||||
|
authorizationUrl: 'http://localhost:7080/realms/teacher/protocol/openid-connect/auth',
|
||||||
|
scopes: {
|
||||||
|
openid: 'openid',
|
||||||
|
profile: 'profile',
|
||||||
|
email: 'email',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
studentStaging: {
|
||||||
|
type: 'oauth2',
|
||||||
|
flows: {
|
||||||
|
implicit: {
|
||||||
|
authorizationUrl: 'http://localhost/idp/realms/student/protocol/openid-connect/auth',
|
||||||
|
scopes: {
|
||||||
|
openid: 'openid',
|
||||||
|
profile: 'profile',
|
||||||
|
email: 'email',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
teacherStaging: {
|
||||||
|
type: 'oauth2',
|
||||||
|
flows: {
|
||||||
|
implicit: {
|
||||||
|
authorizationUrl: 'http://localhost/idp/realms/teacher/protocol/openid-connect/auth',
|
||||||
|
scopes: {
|
||||||
|
openid: 'openid',
|
||||||
|
profile: 'profile',
|
||||||
|
email: 'email',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
studentProduction: {
|
||||||
type: 'oauth2',
|
type: 'oauth2',
|
||||||
flows: {
|
flows: {
|
||||||
implicit: {
|
implicit: {
|
||||||
|
|
@ -39,7 +91,7 @@ const doc = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
teacher: {
|
teacherProduction: {
|
||||||
type: 'oauth2',
|
type: 'oauth2',
|
||||||
flows: {
|
flows: {
|
||||||
implicit: {
|
implicit: {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dwengo-1/common": "^0.2.0",
|
||||||
"@tanstack/react-query": "^5.69.0",
|
"@tanstack/react-query": "^5.69.0",
|
||||||
"@tanstack/vue-query": "^5.69.0",
|
"@tanstack/vue-query": "^5.69.0",
|
||||||
"@vueuse/core": "^13.1.0",
|
"@vueuse/core": "^13.1.0",
|
||||||
|
|
|
||||||
54
frontend/src/assets/common.css
Normal file
54
frontend/src/assets/common.css
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
.h1 {
|
||||||
|
color: #0e6942;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bolder;
|
||||||
|
font-size: 50px;
|
||||||
|
padding-left: 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-message {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-weight: bold !important;
|
||||||
|
background-color: #0e6942;
|
||||||
|
color: white;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th:first-child {
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th:last-child {
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:nth-child(odd) {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:nth-child(even) {
|
||||||
|
background-color: #f6faf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td,
|
||||||
|
.table th {
|
||||||
|
border-bottom: 1px solid #0e6942;
|
||||||
|
border-top: 1px solid #0e6942;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 90%;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 850px) {
|
||||||
|
.h1 {
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,22 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-row v-else>
|
<v-row v-else>
|
||||||
|
<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 grey-bg-card"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
<v-col
|
<v-col
|
||||||
v-for="card in cards"
|
v-for="card in cards"
|
||||||
:key="card.key"
|
:key="card.key"
|
||||||
|
|
@ -74,24 +90,13 @@
|
||||||
class="fill-height"
|
class="fill-height"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</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-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.grey-bg-card {
|
||||||
|
background-color: #f6faf2;
|
||||||
|
border: 2px solid #0e6942;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,9 @@
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.search-field {
|
||||||
|
width: 25%;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,9 @@
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
.results-grid {
|
.results-grid {
|
||||||
margin: 20px;
|
margin: 20px auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
justify-content: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@
|
||||||
const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable
|
const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable
|
||||||
|
|
||||||
const name: string = auth.authState.user!.profile.name!;
|
const name: string = auth.authState.user!.profile.name!;
|
||||||
|
const username = auth.authState.user!.profile.preferred_username!;
|
||||||
|
const email = auth.authState.user!.profile.email;
|
||||||
const initials: string = name
|
const initials: string = name
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((n) => n[0])
|
.map((n) => n[0])
|
||||||
|
|
@ -90,7 +92,11 @@
|
||||||
<!-- >-->
|
<!-- >-->
|
||||||
<!-- {{ t("discussions") }}-->
|
<!-- {{ t("discussions") }}-->
|
||||||
<!-- </v-btn>-->
|
<!-- </v-btn>-->
|
||||||
<v-menu open-on-hover>
|
</v-toolbar-items>
|
||||||
|
<v-menu
|
||||||
|
open-on-hover
|
||||||
|
open-on-click
|
||||||
|
>
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
|
|
@ -114,7 +120,6 @@
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</v-toolbar-items>
|
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-dialog max-width="500">
|
<v-dialog max-width="500">
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
<template v-slot:activator="{ props: activatorProps }">
|
||||||
|
|
@ -158,12 +163,48 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
<v-avatar
|
<v-menu min-width="200px">
|
||||||
size="large"
|
<template v-slot:activator="{ props }">
|
||||||
color="#0e6942"
|
<v-btn
|
||||||
class="user-button"
|
icon
|
||||||
>{{ initials }}</v-avatar
|
v-bind="props"
|
||||||
>
|
>
|
||||||
|
<v-avatar
|
||||||
|
color="#0e6942"
|
||||||
|
size="large"
|
||||||
|
class="user-button"
|
||||||
|
>
|
||||||
|
<span>{{ initials }}</span>
|
||||||
|
</v-avatar>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-card>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="mx-auto text-center">
|
||||||
|
<v-avatar
|
||||||
|
color="#0e6942"
|
||||||
|
size="large"
|
||||||
|
class="user-button mb-3"
|
||||||
|
>
|
||||||
|
<span>{{ initials }}</span>
|
||||||
|
</v-avatar>
|
||||||
|
<h3>{{ name }}</h3>
|
||||||
|
<p class="text-caption mt-1">{{ username }}</p>
|
||||||
|
<p class="text-caption mt-1">{{ email }}</p>
|
||||||
|
<v-divider class="my-3"></v-divider>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
rounded
|
||||||
|
append-icon="mdi-logout"
|
||||||
|
@click="performLogout"
|
||||||
|
to="/login"
|
||||||
|
>{{ t("logout") }}</v-btn
|
||||||
|
>
|
||||||
|
<v-divider class="my-3"></v-divider>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-navigation-drawer
|
<v-navigation-drawer
|
||||||
v-model="drawer"
|
v-model="drawer"
|
||||||
|
|
@ -248,6 +289,12 @@
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.translate-button {
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.menu {
|
.menu {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import type { AnswersResponse } from "@/controllers/answers";
|
import type { AnswersResponse } from "@/controllers/answers";
|
||||||
import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer";
|
import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer";
|
||||||
import authService from "@/services/auth/auth-service";
|
import authService from "@/services/auth/auth-service";
|
||||||
|
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
question: QuestionDTO;
|
question: QuestionDTO;
|
||||||
|
|
@ -80,7 +81,7 @@
|
||||||
{{ question.content }}
|
{{ question.content }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="authService.authState.activeRole === 'teacher'"
|
v-if="authService.authState.activeRole === AccountType.Teacher"
|
||||||
class="answer-input-container"
|
class="answer-input-container"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,30 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { deadlineRules } from "@/utils/assignment-rules.ts";
|
import { deadlineRules } from "@/utils/assignment-rules.ts";
|
||||||
|
|
||||||
const date = ref("");
|
const emit = defineEmits<(e: "update:deadline", value: Date) => void>();
|
||||||
const time = ref("23:59");
|
|
||||||
const emit = defineEmits(["update:deadline"]);
|
|
||||||
|
|
||||||
const formattedDeadline = computed(() => {
|
const datetime = ref("");
|
||||||
if (!date.value || !time.value) return "";
|
|
||||||
return `${date.value} ${time.value}`;
|
// Watch the datetime value and emit the update
|
||||||
|
watch(datetime, (val) => {
|
||||||
|
const newDate = new Date(val);
|
||||||
|
if (!isNaN(newDate.getTime())) {
|
||||||
|
emit("update:deadline", newDate);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateDeadline(): void {
|
|
||||||
if (date.value && time.value) {
|
|
||||||
emit("update:deadline", formattedDeadline.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="date"
|
v-model="datetime"
|
||||||
label="Select Deadline Date"
|
type="datetime-local"
|
||||||
type="date"
|
label="Select Deadline"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
:rules="deadlineRules"
|
:rules="deadlineRules"
|
||||||
required
|
required
|
||||||
@update:modelValue="updateDeadline"
|
/>
|
||||||
></v-text-field>
|
|
||||||
</v-card-text>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ export class LearningPathController extends BaseController {
|
||||||
});
|
});
|
||||||
return LearningPath.fromDTO(single(dtos));
|
return LearningPath.fromDTO(single(dtos));
|
||||||
}
|
}
|
||||||
async getAllByTheme(theme: string): Promise<LearningPath[]> {
|
async getAllByThemeAndLanguage(theme: string, language: Language): Promise<LearningPath[]> {
|
||||||
const dtos = await this.get<LearningPathDTO[]>("/", { theme });
|
const dtos = await this.get<LearningPathDTO[]>("/", { theme, language });
|
||||||
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { BaseController } from "@/controllers/base-controller.ts";
|
import { BaseController } from "@/controllers/base-controller.ts";
|
||||||
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
||||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
|
||||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||||
|
|
||||||
|
|
@ -40,10 +39,6 @@ export class TeacherController extends BaseController {
|
||||||
return this.get<StudentsResponse>(`/${username}/students`, { full });
|
return this.get<StudentsResponse>(`/${username}/students`, { full });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getQuestions(username: string, full = false): Promise<QuestionsResponse> {
|
|
||||||
return this.get<QuestionsResponse>(`/${username}/questions`, { full });
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStudentJoinRequests(username: string, classId: string): Promise<JoinRequestsResponse> {
|
async getStudentJoinRequests(username: string, classId: string): Promise<JoinRequestsResponse> {
|
||||||
return this.get<JoinRequestsResponse>(`/${username}/joinRequests/${classId}`);
|
return this.get<JoinRequestsResponse>(`/${username}/joinRequests/${classId}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.",
|
"JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.",
|
||||||
"invalidFormat": "Ungültiges Format",
|
"invalidFormat": "Ungültiges Format",
|
||||||
"submitCode": "senden",
|
"submitCode": "senden",
|
||||||
|
"submit": "senden",
|
||||||
"members": "Mitglieder",
|
"members": "Mitglieder",
|
||||||
"themes": "Themen",
|
"themes": "Themen",
|
||||||
"choose-theme": "Wählen Sie ein Thema",
|
"choose-theme": "Wählen Sie ein Thema",
|
||||||
|
|
@ -68,10 +69,10 @@
|
||||||
"pick-class": "Wählen Sie eine klasse",
|
"pick-class": "Wählen Sie eine klasse",
|
||||||
"choose-students": "Studenten auswählen",
|
"choose-students": "Studenten auswählen",
|
||||||
"create-group": "Gruppe erstellen",
|
"create-group": "Gruppe erstellen",
|
||||||
"class": "klasse",
|
"class": "Klasse",
|
||||||
"delete": "löschen",
|
"delete": "löschen",
|
||||||
"view-assignment": "Auftrag anzeigen",
|
"view-assignment": "Auftrag anzeigen",
|
||||||
"code": "code",
|
"code": "Code",
|
||||||
"invitations": "Einladungen",
|
"invitations": "Einladungen",
|
||||||
"createClass": "Klasse erstellen",
|
"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.",
|
"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.",
|
||||||
|
|
@ -83,7 +84,7 @@
|
||||||
"onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
|
"onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
|
||||||
"close": "schließen",
|
"close": "schließen",
|
||||||
"copied": "kopiert!",
|
"copied": "kopiert!",
|
||||||
"accept": "akzeptieren",
|
"accept": "Akzeptieren",
|
||||||
"deny": "ablehnen",
|
"deny": "ablehnen",
|
||||||
"sent": "sent",
|
"sent": "sent",
|
||||||
"failed": "fehlgeschlagen",
|
"failed": "fehlgeschlagen",
|
||||||
|
|
@ -110,7 +111,7 @@
|
||||||
"remove": "entfernen",
|
"remove": "entfernen",
|
||||||
"students": "Studenten",
|
"students": "Studenten",
|
||||||
"classJoinRequests": "Beitrittsanfragen",
|
"classJoinRequests": "Beitrittsanfragen",
|
||||||
"reject": "ablehnen",
|
"reject": "Ablehnen",
|
||||||
"areusure": "Sind Sie sicher?",
|
"areusure": "Sind Sie sicher?",
|
||||||
"yes": "ja",
|
"yes": "ja",
|
||||||
"teachers": "Lehrer",
|
"teachers": "Lehrer",
|
||||||
|
|
@ -121,5 +122,18 @@
|
||||||
"invite": "einladen",
|
"invite": "einladen",
|
||||||
"assignmentIndicator": "AUFGABE",
|
"assignmentIndicator": "AUFGABE",
|
||||||
"searchAllLearningPathsTitle": "Alle Lernpfade durchsuchen",
|
"searchAllLearningPathsTitle": "Alle Lernpfade durchsuchen",
|
||||||
"searchAllLearningPathsDescription": "Nicht gefunden, was Sie gesucht haben? Klicken Sie hier, um unsere gesamte Lernpfad-Datenbank zu durchsuchen."
|
"searchAllLearningPathsDescription": "Nicht gefunden, was Sie gesucht haben? Klicken Sie hier, um unsere gesamte Lernpfad-Datenbank zu durchsuchen.",
|
||||||
|
"no-students-found": "Diese Klasse hat keine Schüler.",
|
||||||
|
"no-invitations-found": "Sie haben keine ausstehenden Einladungen.",
|
||||||
|
"no-join-requests-found": "Es gibt keine ausstehenden Beitrittsanfragen für diese Klasse.",
|
||||||
|
"no-classes-found": "Sie sind noch keinem Kurs beigetreten.",
|
||||||
|
"classCreated": "Klasse erstellt!",
|
||||||
|
"success": "Erfolg",
|
||||||
|
"submitted": "eingereicht",
|
||||||
|
"see-submission": "Einsendung anzeigen",
|
||||||
|
"view-submissions": "Einsendungen anzeigen",
|
||||||
|
"valid-username": "Bitte geben Sie einen gültigen Benutzernamen ein",
|
||||||
|
"creationFailed": "Erstellung fehlgeschlagen, bitte versuchen Sie es erneut",
|
||||||
|
"no-assignments": "Derzeit gibt es keine Zuweisungen.",
|
||||||
|
"deadline": "deadline"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
|
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
|
||||||
"invalidFormat": "Invalid format.",
|
"invalidFormat": "Invalid format.",
|
||||||
"submitCode": "submit",
|
"submitCode": "submit",
|
||||||
|
"submit": "submit",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"themes": "Themes",
|
"themes": "Themes",
|
||||||
"choose-theme": "Select a theme",
|
"choose-theme": "Select a theme",
|
||||||
|
|
@ -68,21 +69,21 @@
|
||||||
"pick-class": "Pick a class",
|
"pick-class": "Pick a class",
|
||||||
"choose-students": "Select students",
|
"choose-students": "Select students",
|
||||||
"create-group": "Create group",
|
"create-group": "Create group",
|
||||||
"class": "class",
|
"class": "Class",
|
||||||
"delete": "delete",
|
"delete": "delete",
|
||||||
"view-assignment": "View assignment",
|
"view-assignment": "View assignment",
|
||||||
"code": "code",
|
"code": "Code",
|
||||||
"invitations": "invitations",
|
"invitations": "Invitations",
|
||||||
"createClass": "create class",
|
"createClass": "Create class",
|
||||||
"classname": "classname",
|
"classname": "classname",
|
||||||
"EnterNameOfClass": "Enter a classname.",
|
"EnterNameOfClass": "Enter a classname.",
|
||||||
"create": "create",
|
"create": "create",
|
||||||
"sender": "sender",
|
"sender": "Sender",
|
||||||
"nameIsMandatory": "classname is mandatory",
|
"nameIsMandatory": "classname is mandatory",
|
||||||
"onlyUse": "only use letters, numbers, dashes (-) and underscores (_)",
|
"onlyUse": "only use letters, numbers, dashes (-) and underscores (_)",
|
||||||
"close": "close",
|
"close": "close",
|
||||||
"copied": "copied!",
|
"copied": "copied!",
|
||||||
"accept": "accept",
|
"accept": "Accept",
|
||||||
"deny": "deny",
|
"deny": "deny",
|
||||||
"createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join.",
|
"createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join.",
|
||||||
"sent": "sent",
|
"sent": "sent",
|
||||||
|
|
@ -108,12 +109,12 @@
|
||||||
"progress": "Progress",
|
"progress": "Progress",
|
||||||
"created": "created",
|
"created": "created",
|
||||||
"remove": "remove",
|
"remove": "remove",
|
||||||
"students": "students",
|
"students": "Students",
|
||||||
"classJoinRequests": "join requests",
|
"classJoinRequests": "Join requests",
|
||||||
"reject": "reject",
|
"reject": "Reject",
|
||||||
"areusure": "Are you sure?",
|
"areusure": "Are you sure?",
|
||||||
"yes": "yes",
|
"yes": "yes",
|
||||||
"teachers": "teachers",
|
"teachers": "Teachers",
|
||||||
"accepted": "accepted",
|
"accepted": "accepted",
|
||||||
"rejected": "rejected",
|
"rejected": "rejected",
|
||||||
"enterUsername": "enter the username of the teacher you would like to invite",
|
"enterUsername": "enter the username of the teacher you would like to invite",
|
||||||
|
|
@ -121,5 +122,18 @@
|
||||||
"invite": "invite",
|
"invite": "invite",
|
||||||
"assignmentIndicator": "ASSIGNMENT",
|
"assignmentIndicator": "ASSIGNMENT",
|
||||||
"searchAllLearningPathsTitle": "Search all learning paths",
|
"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."
|
"searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths.",
|
||||||
|
"no-students-found": "This class has no students.",
|
||||||
|
"no-invitations-found": "You have no pending invitations.",
|
||||||
|
"no-join-requests-found": "There are no pending join requests for this class.",
|
||||||
|
"no-classes-found": "You are not yet part of a class.",
|
||||||
|
"classCreated": "class created!",
|
||||||
|
"success": "success",
|
||||||
|
"submitted": "submitted",
|
||||||
|
"see-submission": "view submission",
|
||||||
|
"view-submissions": "view submissions",
|
||||||
|
"valid-username": "please enter a valid username",
|
||||||
|
"creationFailed": "creation failed, please try again",
|
||||||
|
"no-assignments": "There are currently no assignments.",
|
||||||
|
"deadline": "deadline"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
|
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
|
||||||
"invalidFormat": "Format non valide.",
|
"invalidFormat": "Format non valide.",
|
||||||
"submitCode": "envoyer",
|
"submitCode": "envoyer",
|
||||||
|
"submit": "envoyer",
|
||||||
"members": "Membres",
|
"members": "Membres",
|
||||||
"themes": "Thèmes",
|
"themes": "Thèmes",
|
||||||
"choose-theme": "Choisis un thème",
|
"choose-theme": "Choisis un thème",
|
||||||
|
|
@ -68,22 +69,22 @@
|
||||||
"pick-class": "Choisissez une classe",
|
"pick-class": "Choisissez une classe",
|
||||||
"choose-students": "Sélectionnez des élèves",
|
"choose-students": "Sélectionnez des élèves",
|
||||||
"create-group": "Créer un groupe",
|
"create-group": "Créer un groupe",
|
||||||
"class": "classe",
|
"class": "Classe",
|
||||||
"delete": "supprimer",
|
"delete": "supprimer",
|
||||||
"view-assignment": "Voir le travail",
|
"view-assignment": "Voir le travail",
|
||||||
"code": "code",
|
"code": "Code",
|
||||||
"invitations": "invitations",
|
"invitations": "Invitations",
|
||||||
"createClass": "créer une classe",
|
"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.",
|
"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.",
|
||||||
"classname": "nom de classe",
|
"classname": "nom de classe",
|
||||||
"EnterNameOfClass": "saisir un nom de classe.",
|
"EnterNameOfClass": "saisir un nom de classe.",
|
||||||
"create": "créer",
|
"create": "créer",
|
||||||
"sender": "expéditeur",
|
"sender": "Expéditeur",
|
||||||
"nameIsMandatory": "le nom de classe est obligatoire",
|
"nameIsMandatory": "le nom de classe est obligatoire",
|
||||||
"onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
|
"onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
|
||||||
"close": "fermer",
|
"close": "fermer",
|
||||||
"copied": "copié!",
|
"copied": "copié!",
|
||||||
"accept": "accepter",
|
"accept": "Accepter",
|
||||||
"deny": "refuser",
|
"deny": "refuser",
|
||||||
"sent": "envoyé",
|
"sent": "envoyé",
|
||||||
"failed": "échoué",
|
"failed": "échoué",
|
||||||
|
|
@ -108,12 +109,13 @@
|
||||||
"submission": "Soumission",
|
"submission": "Soumission",
|
||||||
"progress": "Progrès",
|
"progress": "Progrès",
|
||||||
"remove": "supprimer",
|
"remove": "supprimer",
|
||||||
"students": "étudiants",
|
"students": "Étudiants",
|
||||||
"classJoinRequests": "demandes d'adhésion",
|
|
||||||
"reject": "rejeter",
|
"classJoinRequests": "Demandes d'adhésion",
|
||||||
|
"reject": "Rejeter",
|
||||||
"areusure": "Êtes-vous sûr?",
|
"areusure": "Êtes-vous sûr?",
|
||||||
"yes": "oui",
|
"yes": "oui",
|
||||||
"teachers": "enseignants",
|
"teachers": "Enseignants",
|
||||||
"accepted": "acceptée",
|
"accepted": "acceptée",
|
||||||
"rejected": "rejetée",
|
"rejected": "rejetée",
|
||||||
"enterUsername": "entrez le nom d'utilisateur de l'enseignant que vous souhaitez inviter",
|
"enterUsername": "entrez le nom d'utilisateur de l'enseignant que vous souhaitez inviter",
|
||||||
|
|
@ -121,5 +123,18 @@
|
||||||
"invite": "inviter",
|
"invite": "inviter",
|
||||||
"assignmentIndicator": "DEVOIR",
|
"assignmentIndicator": "DEVOIR",
|
||||||
"searchAllLearningPathsTitle": "Rechercher tous les parcours d'apprentissage",
|
"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."
|
"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.",
|
||||||
|
"no-students-found": "Cette classe n'a pas d'élèves.",
|
||||||
|
"no-invitations-found": "Vous n'avez aucune invitation en attente.",
|
||||||
|
"no-join-requests-found": "Il n'y a aucune demande d'adhésion en attente pour cette classe.",
|
||||||
|
"no-classes-found": "Vous ne faites pas encore partie d'une classe.",
|
||||||
|
"classCreated": "Classe créée !",
|
||||||
|
"success": "succès",
|
||||||
|
"submitted": "soumis",
|
||||||
|
"see-submission": "voir la soumission",
|
||||||
|
"view-submissions": "voir les soumissions",
|
||||||
|
"valid-username": "veuillez entrer un nom d'utilisateur valide",
|
||||||
|
"creationFailed": "échec de la création, veuillez réessayer",
|
||||||
|
"no-assignments": "Il n'y a actuellement aucun travail.",
|
||||||
|
"deadline": "délai"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
|
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
|
||||||
"invalidFormat": "Ongeldig formaat.",
|
"invalidFormat": "Ongeldig formaat.",
|
||||||
"submitCode": "verzenden",
|
"submitCode": "verzenden",
|
||||||
|
"submit": "verzenden",
|
||||||
"members": "Leden",
|
"members": "Leden",
|
||||||
"themes": "Lesthema's",
|
"themes": "Lesthema's",
|
||||||
"choose-theme": "Kies een thema",
|
"choose-theme": "Kies een thema",
|
||||||
|
|
@ -68,22 +69,22 @@
|
||||||
"pick-class": "Kies een klas",
|
"pick-class": "Kies een klas",
|
||||||
"choose-students": "Studenten selecteren",
|
"choose-students": "Studenten selecteren",
|
||||||
"create-group": "Groep aanmaken",
|
"create-group": "Groep aanmaken",
|
||||||
"class": "klas",
|
"class": "Klas",
|
||||||
"delete": "verwijderen",
|
"delete": "verwijderen",
|
||||||
"view-assignment": "Opdracht bekijken",
|
"view-assignment": "Opdracht bekijken",
|
||||||
"code": "code",
|
"code": "Code",
|
||||||
"invitations": "uitnodigingen",
|
"invitations": "Uitnodigingen",
|
||||||
"createClass": "klas aanmaken",
|
"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.",
|
"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.",
|
||||||
"classname": "klasnaam",
|
"classname": "klasnaam",
|
||||||
"EnterNameOfClass": "Geef een klasnaam op.",
|
"EnterNameOfClass": "Geef een klasnaam op.",
|
||||||
"create": "aanmaken",
|
"create": "aanmaken",
|
||||||
"sender": "afzender",
|
"sender": "Afzender",
|
||||||
"nameIsMandatory": "klasnaam is verplicht",
|
"nameIsMandatory": "klasnaam is verplicht",
|
||||||
"onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
|
"onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
|
||||||
"close": "sluiten",
|
"close": "sluiten",
|
||||||
"copied": "gekopieerd!",
|
"copied": "gekopieerd!",
|
||||||
"accept": "accepteren",
|
"accept": "Accepteren",
|
||||||
"deny": "weigeren",
|
"deny": "weigeren",
|
||||||
"sent": "verzonden",
|
"sent": "verzonden",
|
||||||
"failed": "mislukt",
|
"failed": "mislukt",
|
||||||
|
|
@ -108,12 +109,12 @@
|
||||||
"submission": "Indiening",
|
"submission": "Indiening",
|
||||||
"progress": "Vooruitgang",
|
"progress": "Vooruitgang",
|
||||||
"remove": "verwijder",
|
"remove": "verwijder",
|
||||||
"students": "studenten",
|
"students": "Studenten",
|
||||||
"classJoinRequests": "deelname verzoeken",
|
"classJoinRequests": "Deelname verzoeken",
|
||||||
"reject": "weiger",
|
"reject": "Weiger",
|
||||||
"areusure": "Bent u zeker?",
|
"areusure": "Bent u zeker?",
|
||||||
"yes": "ja",
|
"yes": "ja",
|
||||||
"teachers": "leerkrachten",
|
"teachers": "Leerkrachten",
|
||||||
"accepted": "geaccepteerd",
|
"accepted": "geaccepteerd",
|
||||||
"rejected": "geweigerd",
|
"rejected": "geweigerd",
|
||||||
"enterUsername": "vul de gebruikersnaam van de leerkracht die je wilt uitnodigen in",
|
"enterUsername": "vul de gebruikersnaam van de leerkracht die je wilt uitnodigen in",
|
||||||
|
|
@ -121,5 +122,18 @@
|
||||||
"invite": "uitnodigen",
|
"invite": "uitnodigen",
|
||||||
"assignmentIndicator": "OPDRACHT",
|
"assignmentIndicator": "OPDRACHT",
|
||||||
"searchAllLearningPathsTitle": "Alle leerpaden doorzoeken",
|
"searchAllLearningPathsTitle": "Alle leerpaden doorzoeken",
|
||||||
"searchAllLearningPathsDescription": "Niet gevonden waar je naar op zoek was? Klik hier om onze volledige databank van beschikbare leerpaden te doorzoeken."
|
"searchAllLearningPathsDescription": "Niet gevonden waar je naar op zoek was? Klik hier om onze volledige databank van beschikbare leerpaden te doorzoeken.",
|
||||||
|
"no-students-found": "Deze klas heeft geen leerlingen.",
|
||||||
|
"no-invitations-found": "U heeft geen openstaande uitnodigingen.",
|
||||||
|
"no-join-requests-found": "Er zijn geen openstaande verzoeken om lid te worden van deze klas.",
|
||||||
|
"no-classes-found": "U maakt nog geen deel uit van een klas.",
|
||||||
|
"classCreated": "Klas aangemaakt!",
|
||||||
|
"success": "succes",
|
||||||
|
"submitted": "ingediend",
|
||||||
|
"see-submission": "inzending bekijken",
|
||||||
|
"view-submissions": "inzendingen bekijken",
|
||||||
|
"valid-username": "voer een geldige gebruikersnaam in",
|
||||||
|
"creationFailed": "aanmaak mislukt, probeer het opnieuw",
|
||||||
|
"no-assignments": "Er zijn momenteel geen opdrachten.",
|
||||||
|
"deadline": "deadline"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { invalidateAllGroupKeys } from "./groups";
|
||||||
import { invalidateAllSubmissionKeys } from "./submissions";
|
import { invalidateAllSubmissionKeys } from "./submissions";
|
||||||
import type { TeachersResponse } from "@/controllers/teachers";
|
import type { TeachersResponse } from "@/controllers/teachers";
|
||||||
import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations";
|
import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations";
|
||||||
|
import { studentClassesQueryKey } from "@/queries/students.ts";
|
||||||
|
|
||||||
const classController = new ClassController();
|
const classController = new ClassController();
|
||||||
|
|
||||||
|
|
@ -171,6 +172,8 @@ export function useClassDeleteStudentMutation(): UseMutationReturnType<
|
||||||
await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) });
|
await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) });
|
||||||
await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, true) });
|
await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, true) });
|
||||||
await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, false) });
|
await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, false) });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, false) });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, true) });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,13 @@ export function useGetLearningPathQuery(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGetAllLearningPathsByThemeQuery(
|
export function useGetAllLearningPathsByThemeAndLanguageQuery(
|
||||||
theme: MaybeRefOrGetter<string>,
|
theme: MaybeRefOrGetter<string>,
|
||||||
|
language: MaybeRefOrGetter<Language>,
|
||||||
): UseQueryReturnType<LearningPath[], Error> {
|
): UseQueryReturnType<LearningPath[], Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme],
|
queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme, language],
|
||||||
queryFn: async () => learningPathController.getAllByTheme(toValue(theme)),
|
queryFn: async () => learningPathController.getAllByThemeAndLanguage(toValue(theme), toValue(language)),
|
||||||
enabled: () => Boolean(toValue(theme)),
|
enabled: () => Boolean(toValue(theme)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ function studentsQueryKey(full: boolean): [string, boolean] {
|
||||||
function studentQueryKey(username: string): [string, string] {
|
function studentQueryKey(username: string): [string, string] {
|
||||||
return ["student", username];
|
return ["student", username];
|
||||||
}
|
}
|
||||||
function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] {
|
export function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] {
|
||||||
return ["student-classes", username, full];
|
return ["student-classes", username, full];
|
||||||
}
|
}
|
||||||
function studentAssignmentsQueryKey(username: string, full: boolean): [string, string, boolean] {
|
function studentAssignmentsQueryKey(username: string, full: boolean): [string, string, boolean] {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import {
|
||||||
import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts";
|
import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts";
|
||||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||||
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
||||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
|
||||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||||
import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts";
|
import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts";
|
||||||
|
|
||||||
|
|
@ -33,10 +32,6 @@ function teacherStudentsQueryKey(username: string, full: boolean): [string, stri
|
||||||
return ["teacher-students", username, full];
|
return ["teacher-students", username, full];
|
||||||
}
|
}
|
||||||
|
|
||||||
function teacherQuestionsQueryKey(username: string, full: boolean): [string, string, boolean] {
|
|
||||||
return ["teacher-questions", username, full];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function teacherClassJoinRequests(classId: string): [string, string] {
|
export function teacherClassJoinRequests(classId: string): [string, string] {
|
||||||
return ["teacher-class-join-requests", classId];
|
return ["teacher-class-join-requests", classId];
|
||||||
}
|
}
|
||||||
|
|
@ -80,17 +75,6 @@ export function useTeacherStudentsQuery(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTeacherQuestionsQuery(
|
|
||||||
username: MaybeRefOrGetter<string | undefined>,
|
|
||||||
full: MaybeRefOrGetter<boolean> = false,
|
|
||||||
): UseQueryReturnType<QuestionsResponse, Error> {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: computed(() => teacherQuestionsQueryKey(toValue(username)!, toValue(full))),
|
|
||||||
queryFn: async () => teacherController.getQuestions(toValue(username)!, toValue(full)),
|
|
||||||
enabled: () => Boolean(toValue(username)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTeacherJoinRequestsQuery(
|
export function useTeacherJoinRequestsQuery(
|
||||||
username: MaybeRefOrGetter<string | undefined>,
|
username: MaybeRefOrGetter<string | undefined>,
|
||||||
classId: MaybeRefOrGetter<string | undefined>,
|
classId: MaybeRefOrGetter<string | undefined>,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import UserHomePage from "@/views/homepage/UserHomePage.vue";
|
||||||
import SingleTheme from "@/views/SingleTheme.vue";
|
import SingleTheme from "@/views/SingleTheme.vue";
|
||||||
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
|
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
|
||||||
import authService from "@/services/auth/auth-service";
|
import authService from "@/services/auth/auth-service";
|
||||||
|
import { allowRedirect, Redirect } from "@/utils/redirect.ts";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
|
@ -143,7 +144,11 @@ router.beforeEach(async (to, _from, next) => {
|
||||||
// Verify if user is logged in before accessing certain routes
|
// Verify if user is logged in before accessing certain routes
|
||||||
if (to.meta.requiresAuth) {
|
if (to.meta.requiresAuth) {
|
||||||
if (!authService.isLoggedIn.value && !(await authService.loadUser())) {
|
if (!authService.isLoggedIn.value && !(await authService.loadUser())) {
|
||||||
next("/login");
|
const path = to.fullPath;
|
||||||
|
if (allowRedirect(path)) {
|
||||||
|
localStorage.setItem(Redirect.AFTER_LOGIN_KEY, path);
|
||||||
|
}
|
||||||
|
next(Redirect.LOGIN);
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
frontend/src/utils/redirect.ts
Normal file
12
frontend/src/utils/redirect.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export enum Redirect {
|
||||||
|
AFTER_LOGIN_KEY = "redirectAfterLogin",
|
||||||
|
HOME = "/user",
|
||||||
|
LOGIN = "/login",
|
||||||
|
ROOT = "/",
|
||||||
|
}
|
||||||
|
|
||||||
|
const NOT_ALLOWED_REDIRECTS = new Set<Redirect>([Redirect.HOME, Redirect.ROOT, Redirect.LOGIN]);
|
||||||
|
|
||||||
|
export function allowRedirect(path: string): boolean {
|
||||||
|
return !NOT_ALLOWED_REDIRECTS.has(path as Redirect);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { onMounted, ref, type Ref } from "vue";
|
import { onMounted, ref, type Ref } from "vue";
|
||||||
import auth from "../services/auth/auth-service.ts";
|
import auth from "../services/auth/auth-service.ts";
|
||||||
|
import { Redirect } from "@/utils/redirect.ts";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
@ -10,10 +11,20 @@
|
||||||
|
|
||||||
const errorMessage: Ref<string | null> = ref(null);
|
const errorMessage: Ref<string | null> = ref(null);
|
||||||
|
|
||||||
|
async function redirectPage(): Promise<void> {
|
||||||
|
const redirectUrl = localStorage.getItem(Redirect.AFTER_LOGIN_KEY);
|
||||||
|
if (redirectUrl) {
|
||||||
|
localStorage.removeItem(Redirect.AFTER_LOGIN_KEY);
|
||||||
|
await router.replace(redirectUrl);
|
||||||
|
} else {
|
||||||
|
await router.replace(Redirect.HOME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await auth.handleLoginCallback();
|
await auth.handleLoginCallback();
|
||||||
await router.replace("/user"); // Redirect to theme page
|
await redirectPage();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = `${t("loginUnexpectedError")}: ${error}`;
|
errorMessage.value = `${t("loginUnexpectedError")}: ${error}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
alt="Dwengo logo"
|
alt="Dwengo logo"
|
||||||
style="align-self: center"
|
style="align-self: center"
|
||||||
/>
|
/>
|
||||||
<h1>{{ t("homeTitle") }}</h1>
|
<h1 class="h1">{{ t("homeTitle") }}</h1>
|
||||||
<p class="info">
|
<p class="info">
|
||||||
{{ t("homeIntroduction1") }}
|
{{ t("homeIntroduction1") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -84,7 +84,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container_right">
|
<div class="container_right">
|
||||||
<v-menu open-on-hover>
|
<v-menu
|
||||||
|
open-on-hover
|
||||||
|
open-on-click
|
||||||
|
>
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,28 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg";
|
import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg";
|
||||||
import auth from "@/services/auth/auth-service.ts";
|
import auth from "@/services/auth/auth-service.ts";
|
||||||
|
import { watch } from "vue";
|
||||||
|
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => auth.isLoggedIn.value,
|
||||||
|
async (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
await router.push("/user");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
async function loginAsStudent(): Promise<void> {
|
async function loginAsStudent(): Promise<void> {
|
||||||
await auth.loginAs("student");
|
await auth.loginAs(AccountType.Student);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loginAsTeacher(): Promise<void> {
|
async function loginAsTeacher(): Promise<void> {
|
||||||
await auth.loginAs("teacher");
|
await auth.loginAs(AccountType.Teacher);
|
||||||
}
|
|
||||||
|
|
||||||
async function performLogout(): Promise<void> {
|
|
||||||
await auth.logout();
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -65,13 +76,6 @@
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.isLoggedIn.value">
|
|
||||||
<p>
|
|
||||||
You are currently logged in as {{ auth.authState.user!.profile.name }} ({{ auth.authState.activeRole }})
|
|
||||||
</p>
|
|
||||||
<v-btn @click="performLogout">Logout</v-btn>
|
|
||||||
<v-btn to="/user">home</v-btn>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||||
import LearningPathsGrid from "@/components/LearningPathsGrid.vue";
|
import LearningPathsGrid from "@/components/LearningPathsGrid.vue";
|
||||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||||
import { useGetAllLearningPathsByThemeQuery } from "@/queries/learning-paths.ts";
|
import { useGetAllLearningPathsByThemeAndLanguageQuery } from "@/queries/learning-paths.ts";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useThemeQuery } from "@/queries/themes.ts";
|
import { useThemeQuery } from "@/queries/themes.ts";
|
||||||
|
import type { Language } from "@/data-objects/language";
|
||||||
|
|
||||||
const props = defineProps<{ theme: string }>();
|
const props = defineProps<{ theme: string }>();
|
||||||
|
|
||||||
|
|
@ -16,7 +17,10 @@
|
||||||
|
|
||||||
const currentThemeInfo = computed(() => themeQueryResult.data.value?.find((it) => it.key === props.theme));
|
const currentThemeInfo = computed(() => themeQueryResult.data.value?.find((it) => it.key === props.theme));
|
||||||
|
|
||||||
const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeQuery(() => props.theme);
|
const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeAndLanguageQuery(
|
||||||
|
() => props.theme,
|
||||||
|
() => locale.value as Language,
|
||||||
|
);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const searchFilter = ref("");
|
const searchFilter = ref("");
|
||||||
|
|
@ -31,13 +35,14 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container d-flex flex-column align-items-center justify-center">
|
||||||
<using-query-result :query-result="themeQueryResult">
|
<using-query-result :query-result="themeQueryResult">
|
||||||
<h1>{{ currentThemeInfo!!.title }}</h1>
|
<h1>{{ currentThemeInfo!!.title }}</h1>
|
||||||
<p>{{ currentThemeInfo!!.description }}</p>
|
<p>{{ currentThemeInfo!!.description }}</p>
|
||||||
<div class="search-field-container">
|
<br />
|
||||||
|
<div class="search-field-container mt-sm-6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
class="search-field"
|
class="search-field mx-auto"
|
||||||
:label="t('search')"
|
:label="t('search')"
|
||||||
append-inner-icon="mdi-magnify"
|
append-inner-icon="mdi-magnify"
|
||||||
v-model="searchFilter"
|
v-model="searchFilter"
|
||||||
|
|
@ -56,13 +61,15 @@
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.search-field-container {
|
.search-field-container {
|
||||||
display: block;
|
justify-content: center !important;
|
||||||
margin: 20px;
|
|
||||||
}
|
}
|
||||||
.search-field {
|
.search-field {
|
||||||
max-width: 300px;
|
width: 25%;
|
||||||
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
justify-items: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
|
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
|
||||||
import { useCreateAssignmentMutation } from "@/queries/assignments.ts";
|
import { useCreateAssignmentMutation } from "@/queries/assignments.ts";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -23,7 +24,7 @@
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// Redirect student
|
// Redirect student
|
||||||
if (role.value === "student") {
|
if (role.value === AccountType.Student) {
|
||||||
await router.push("/user");
|
await router.push("/user");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,7 +49,7 @@
|
||||||
|
|
||||||
// Disable combobox when learningPath prop is passed
|
// Disable combobox when learningPath prop is passed
|
||||||
const lpIsSelected = route.query.hruid !== undefined;
|
const lpIsSelected = route.query.hruid !== undefined;
|
||||||
const deadline = ref(null);
|
const deadline = ref(new Date());
|
||||||
const description = ref("");
|
const description = ref("");
|
||||||
const groups = ref<string[][]>([]);
|
const groups = ref<string[][]>([]);
|
||||||
|
|
||||||
|
|
@ -86,6 +87,7 @@
|
||||||
title: assignmentTitle.value,
|
title: assignmentTitle.value,
|
||||||
description: description.value,
|
description: description.value,
|
||||||
learningPath: lp || "",
|
learningPath: lp || "",
|
||||||
|
deadline: deadline.value,
|
||||||
language: language.value,
|
language: language.value,
|
||||||
groups: groups.value,
|
groups: groups.value,
|
||||||
};
|
};
|
||||||
|
|
@ -96,7 +98,7 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<h1 class="title">{{ t("new-assignment") }}</h1>
|
<h1 class="h1">{{ t("new-assignment") }}</h1>
|
||||||
<v-card class="form-card">
|
<v-card class="form-card">
|
||||||
<v-form
|
<v-form
|
||||||
ref="form"
|
ref="form"
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@
|
||||||
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
|
||||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||||
|
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||||
|
|
||||||
const role = auth.authState.activeRole;
|
const role = auth.authState.activeRole;
|
||||||
const isTeacher = computed(() => role === "teacher");
|
const isTeacher = computed(() => role === AccountType.Teacher);
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const classId = ref<string>(route.params.classId as string);
|
const classId = ref<string>(route.params.classId as string);
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,7 @@
|
||||||
) => { groupProgressMap: Map<number, number> };
|
) => { groupProgressMap: Map<number, number> };
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t } = useI18n();
|
||||||
const language = ref<Language>(locale.value as Language);
|
|
||||||
const learningPath = ref();
|
const learningPath = ref();
|
||||||
// Get the user's username/id
|
// Get the user's username/id
|
||||||
const username = asyncComputed(async () => {
|
const username = asyncComputed(async () => {
|
||||||
|
|
@ -38,7 +37,7 @@
|
||||||
|
|
||||||
const lpQueryResult = useGetLearningPathQuery(
|
const lpQueryResult = useGetLearningPathQuery(
|
||||||
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
|
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
|
||||||
computed(() => language.value),
|
computed(() => assignmentQueryResult.data.value?.assignment.language as Language),
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true);
|
const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true);
|
||||||
|
|
@ -100,7 +99,7 @@ language
|
||||||
>
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="lpData"
|
v-if="lpData"
|
||||||
:to="`/learningPath/${lpData.hruid}/${language}/${lpData.startNode.learningobjectHruid}?forGroup=${group?.groupNumber}&assignmentNo=${assignmentId}&classId=${classId}`"
|
:to="`/learningPath/${lpData.hruid}/${assignmentQueryResult.data.value?.assignment.language}/${lpData.startNode.learningobjectHruid}?forGroup=${group?.groupNumber}&assignmentNo=${assignmentId}&classId=${classId}`"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@
|
||||||
) => { groupProgressMap: Map<number, number> };
|
) => { groupProgressMap: Map<number, number> };
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t } = useI18n();
|
||||||
const language = computed(() => locale.value);
|
|
||||||
const groups = ref();
|
const groups = ref();
|
||||||
const learningPath = ref();
|
const learningPath = ref();
|
||||||
|
|
||||||
|
|
@ -29,7 +28,7 @@
|
||||||
// Get learning path object
|
// Get learning path object
|
||||||
const lpQueryResult = useGetLearningPathQuery(
|
const lpQueryResult = useGetLearningPathQuery(
|
||||||
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
|
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
|
||||||
computed(() => language.value as Language),
|
computed(() => assignmentQueryResult.data.value?.assignment.language as Language),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get all the groups withing the assignment
|
// Get all the groups withing the assignment
|
||||||
|
|
@ -121,7 +120,7 @@ Const {groupProgressMap} = props.useGroupsWithProgress(
|
||||||
>
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="lpData"
|
v-if="lpData"
|
||||||
:to="`/learningPath/${lpData.hruid}/${language}/${lpData.startNode.learningobjectHruid}?assignmentNo=${assignmentId}&classId=${classId}`"
|
:to="`/learningPath/${lpData.hruid}/${assignmentQueryResult.data.value?.assignment.language}/${lpData.startNode.learningobjectHruid}?assignmentNo=${assignmentId}&classId=${classId}`"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
|
|
@ -203,8 +202,8 @@ Const {groupProgressMap} = props.useGroupsWithProgress(
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="dialog = false"
|
@click="dialog = false"
|
||||||
>Close</v-btn
|
>Close
|
||||||
>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,16 @@
|
||||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||||
import { asyncComputed } from "@vueuse/core";
|
import { asyncComputed } from "@vueuse/core";
|
||||||
import { useDeleteAssignmentMutation } from "@/queries/assignments.ts";
|
import { useDeleteAssignmentMutation } from "@/queries/assignments.ts";
|
||||||
|
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||||
|
import "../../assets/common.css";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const role = ref(auth.authState.activeRole);
|
const role = ref(auth.authState.activeRole);
|
||||||
const username = ref<string>("");
|
const username = ref<string>("");
|
||||||
|
|
||||||
const isTeacher = computed(() => role.value === "teacher");
|
const isTeacher = computed(() => role.value === AccountType.Teacher);
|
||||||
|
|
||||||
// Fetch and store all the teacher's classes
|
// Fetch and store all the teacher's classes
|
||||||
let classesQueryResults = undefined;
|
let classesQueryResults = undefined;
|
||||||
|
|
@ -27,13 +29,13 @@
|
||||||
classesQueryResults = useStudentClassesQuery(username, true);
|
classesQueryResults = useStudentClassesQuery(username, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: remove later
|
|
||||||
const classController = new ClassController();
|
const classController = new ClassController();
|
||||||
|
|
||||||
//TODO: replace by query that fetches all user's assignment
|
const assignments = asyncComputed(
|
||||||
const assignments = asyncComputed(async () => {
|
async () => {
|
||||||
const classes = classesQueryResults?.data?.value?.classes;
|
const classes = classesQueryResults?.data?.value?.classes;
|
||||||
if (!classes) return [];
|
if (!classes) return [];
|
||||||
|
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
(classes as ClassDTO[]).map(async (cls) => {
|
(classes as ClassDTO[]).map(async (cls) => {
|
||||||
const { assignments } = await classController.getAssignments(cls.id);
|
const { assignments } = await classController.getAssignments(cls.id);
|
||||||
|
|
@ -44,13 +46,30 @@
|
||||||
description: a.description,
|
description: a.description,
|
||||||
learningPath: a.learningPath,
|
learningPath: a.learningPath,
|
||||||
language: a.language,
|
language: a.language,
|
||||||
|
deadline: a.deadline,
|
||||||
groups: a.groups,
|
groups: a.groups,
|
||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.flat();
|
// Order the assignments by deadline
|
||||||
}, []);
|
return result.flat().sort((a, b) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const aTime = new Date(a.deadline).getTime();
|
||||||
|
const bTime = new Date(b.deadline).getTime();
|
||||||
|
|
||||||
|
const aIsPast = aTime < now;
|
||||||
|
const bIsPast = bTime < now;
|
||||||
|
|
||||||
|
if (aIsPast && !bIsPast) return 1;
|
||||||
|
if (!aIsPast && bIsPast) return -1;
|
||||||
|
|
||||||
|
return aTime - bTime;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
{ evaluating: true },
|
||||||
|
);
|
||||||
|
|
||||||
async function goToCreateAssignment(): Promise<void> {
|
async function goToCreateAssignment(): Promise<void> {
|
||||||
await router.push("/assignment/create");
|
await router.push("/assignment/create");
|
||||||
|
|
@ -72,6 +91,35 @@
|
||||||
mutate({ cid: clsId, an: num });
|
mutate({ cid: clsId, an: num });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDate(date?: string | Date): string {
|
||||||
|
if (!date) return "–";
|
||||||
|
const d = new Date(date);
|
||||||
|
|
||||||
|
// Choose locale based on selected language
|
||||||
|
const currentLocale = locale.value;
|
||||||
|
|
||||||
|
return d.toLocaleDateString(currentLocale, {
|
||||||
|
weekday: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeadlineClass(deadline?: string | Date): string {
|
||||||
|
if (!deadline) return "";
|
||||||
|
|
||||||
|
const date = new Date(deadline);
|
||||||
|
const now = new Date();
|
||||||
|
const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
if (date.getTime() < now.getTime()) return "deadline-passed";
|
||||||
|
if (date.getTime() <= in24Hours.getTime()) return "deadline-in24hours";
|
||||||
|
return "deadline-upcoming";
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const user = await auth.loadUser();
|
const user = await auth.loadUser();
|
||||||
username.value = user?.profile?.preferred_username ?? "";
|
username.value = user?.profile?.preferred_username ?? "";
|
||||||
|
|
@ -80,7 +128,7 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="assignments-container">
|
<div class="assignments-container">
|
||||||
<h1>{{ t("assignments") }}</h1>
|
<h1 class="h1">{{ t("assignments") }}</h1>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="isTeacher"
|
v-if="isTeacher"
|
||||||
|
|
@ -107,6 +155,13 @@
|
||||||
{{ assignment.class.displayName }}
|
{{ assignment.class.displayName }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="assignment-deadline"
|
||||||
|
:class="getDeadlineClass(assignment.deadline)"
|
||||||
|
>
|
||||||
|
{{ t("deadline") }}:
|
||||||
|
<span>{{ formatDate(assignment.deadline) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
|
@ -131,6 +186,13 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
<v-row v-if="assignments.length === 0">
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="no-assignments">
|
||||||
|
{{ t("no-assignments") }}
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -139,18 +201,32 @@
|
||||||
.assignments-container {
|
.assignments-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2% 4%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.center-btn {
|
.center-btn {
|
||||||
display: block;
|
display: block;
|
||||||
margin-left: auto;
|
margin: 0 auto 2rem auto;
|
||||||
margin-right: auto;
|
font-weight: 600;
|
||||||
|
background-color: #10ad61;
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.center-btn:hover {
|
||||||
|
background-color: #0e6942;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assignment-card {
|
.assignment-card {
|
||||||
padding: 1rem;
|
padding: 1.25rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
background-color: white;
|
||||||
|
transition:
|
||||||
|
transform 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.assignment-card:hover {
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-content {
|
.top-content {
|
||||||
|
|
@ -158,6 +234,35 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assignment-title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #0e6942;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-class,
|
||||||
|
.assignment-deadline {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #444;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #097180;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-deadline.deadline-passed {
|
||||||
|
color: #d32f2f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-deadline.deadline-in24hours {
|
||||||
|
color: #f57c00;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
@ -165,24 +270,14 @@
|
||||||
.button-row {
|
.button-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.5rem;
|
gap: 0.75rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assignment-title {
|
.no-assignments {
|
||||||
font-weight: bold;
|
text-align: center;
|
||||||
font-size: 1.5rem;
|
font-size: 1.2rem;
|
||||||
margin-bottom: 0.1rem;
|
color: #777;
|
||||||
word-break: break-word;
|
padding: 3rem 0;
|
||||||
}
|
|
||||||
|
|
||||||
.assignment-class {
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.class-name {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
20
frontend/src/views/classes/ClassDisplay.vue
Normal file
20
frontend/src/views/classes/ClassDisplay.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useClassQuery } from "@/queries/classes";
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
classId: String,
|
||||||
|
});
|
||||||
|
|
||||||
|
const classQuery = useClassQuery(props.classId);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<using-query-result
|
||||||
|
:query-result="classQuery"
|
||||||
|
v-slot="{ data: classResponse }"
|
||||||
|
>
|
||||||
|
<span>{{ classResponse?.class.displayName }}</span>
|
||||||
|
</using-query-result>
|
||||||
|
</template>
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
import { useCreateTeacherInvitationMutation } from "@/queries/teacher-invitations";
|
import { useCreateTeacherInvitationMutation } from "@/queries/teacher-invitations";
|
||||||
import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation";
|
import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation";
|
||||||
import { useDisplay } from "vuetify";
|
import { useDisplay } from "vuetify";
|
||||||
|
import "../../assets/common.css";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|
@ -76,7 +77,7 @@
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
dialog.value = false;
|
dialog.value = false;
|
||||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -104,7 +105,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -112,7 +113,7 @@
|
||||||
|
|
||||||
function sentInvite(): void {
|
function sentInvite(): void {
|
||||||
if (!usernameTeacher.value) {
|
if (!usernameTeacher.value) {
|
||||||
showSnackbar(t("please enter a valid username"), "error");
|
showSnackbar(t("valid-username"), "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data: TeacherInvitationData = {
|
const data: TeacherInvitationData = {
|
||||||
|
|
@ -125,7 +126,7 @@
|
||||||
usernameTeacher.value = "";
|
usernameTeacher.value = "";
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +187,7 @@
|
||||||
v-slot="classResponse: { data: ClassResponse }"
|
v-slot="classResponse: { data: ClassResponse }"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="title">{{ classResponse.data.class.displayName }}</h1>
|
<h1 class="h1">{{ classResponse.data.class.displayName }}</h1>
|
||||||
<using-query-result
|
<using-query-result
|
||||||
:query-result="getStudents"
|
:query-result="getStudents"
|
||||||
v-slot="studentsResponse: { data: StudentsResponse }"
|
v-slot="studentsResponse: { data: StudentsResponse }"
|
||||||
|
|
@ -211,19 +212,34 @@
|
||||||
<th class="header"></th>
|
<th class="header"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
|
||||||
|
<tbody v-if="studentsResponse.data.students.length">
|
||||||
<tr
|
<tr
|
||||||
v-for="s in studentsResponse.data.students as StudentDTO[]"
|
v-for="s in studentsResponse.data.students as StudentDTO[]"
|
||||||
:key="s.id"
|
:key="s.id"
|
||||||
>
|
>
|
||||||
<td>
|
<td>{{ s.firstName + " " + s.lastName }}</td>
|
||||||
{{ s.firstName + " " + s.lastName }}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<v-btn @click="showPopup(s)">{{ t("remove") }}</v-btn>
|
<v-btn @click="showPopup(s)">{{ t("remove") }}</v-btn>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
|
<tbody v-else>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colspan="2"
|
||||||
|
class="empty-message"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-information-outline"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
</v-icon>
|
||||||
|
{{ t("no-students-found") }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</v-table>
|
</v-table>
|
||||||
</v-col>
|
</v-col>
|
||||||
<using-query-result
|
<using-query-result
|
||||||
|
|
@ -242,7 +258,7 @@
|
||||||
<th class="header">{{ t("accept") + "/" + t("reject") }}</th>
|
<th class="header">{{ t("accept") + "/" + t("reject") }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody v-if="joinRequests.data.joinRequests.length">
|
||||||
<tr
|
<tr
|
||||||
v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]"
|
v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]"
|
||||||
:key="(jr.class, jr.requester, jr.status)"
|
:key="(jr.class, jr.requester, jr.status)"
|
||||||
|
|
@ -287,6 +303,21 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tbody v-else>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colspan="2"
|
||||||
|
class="empty-message"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-information-outline"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
</v-icon>
|
||||||
|
{{ t("no-join-requests-found") }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</v-table>
|
</v-table>
|
||||||
</v-col>
|
</v-col>
|
||||||
</using-query-result>
|
</using-query-result>
|
||||||
|
|
@ -356,49 +387,6 @@
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
|
||||||
font-weight: bold !important;
|
|
||||||
background-color: #0e6942;
|
|
||||||
color: white;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table thead th:first-child {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table thead th:last-child {
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody tr:nth-child(odd) {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody tr:nth-child(even) {
|
|
||||||
background-color: #f6faf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
border-bottom: 1px solid #0e6942;
|
|
||||||
border-top: 1px solid #0e6942;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
width: 90%;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #0e6942;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bolder;
|
|
||||||
padding-top: 2%;
|
|
||||||
font-size: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: #0e6942;
|
color: #0e6942;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
|
|
@ -407,6 +395,7 @@
|
||||||
.join {
|
.join {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
margin-left: 1%;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
@ -416,16 +405,7 @@
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join {
|
.join {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import authState from "@/services/auth/auth-service.ts";
|
import authState from "@/services/auth/auth-service.ts";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { validate, version } from "uuid";
|
import { useRoute } from "vue-router";
|
||||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||||
import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students";
|
import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students";
|
||||||
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
||||||
|
|
@ -12,8 +12,10 @@
|
||||||
import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes";
|
import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes";
|
||||||
import type { StudentsResponse } from "@/controllers/students";
|
import type { StudentsResponse } from "@/controllers/students";
|
||||||
import type { TeachersResponse } from "@/controllers/teachers";
|
import type { TeachersResponse } from "@/controllers/teachers";
|
||||||
|
import "../../assets/common.css";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
// Username of logged in student
|
// Username of logged in student
|
||||||
const username = ref<string | undefined>(undefined);
|
const username = ref<string | undefined>(undefined);
|
||||||
|
|
@ -37,6 +39,11 @@
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queryCode = route.query.code as string | undefined;
|
||||||
|
if (queryCode) {
|
||||||
|
code.value = queryCode;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch all classes of the logged in student
|
// Fetch all classes of the logged in student
|
||||||
|
|
@ -74,11 +81,15 @@
|
||||||
|
|
||||||
// The code a student sends in to join a class needs to be formatted as v4 to be valid
|
// The code a student sends in to join a class needs to be formatted as v4 to be valid
|
||||||
// These rules are used to display a message to the user if they use a code that has an invalid format
|
// These rules are used to display a message to the user if they use a code that has an invalid format
|
||||||
|
function codeRegex(value: string): boolean {
|
||||||
|
return /^[a-zA-Z0-9]{6}$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
const codeRules = [
|
const codeRules = [
|
||||||
(value: string | undefined): string | boolean => {
|
(value: string | undefined): string | boolean => {
|
||||||
if (value === undefined || value === "") {
|
if (value === undefined || value === "") {
|
||||||
return true;
|
return true;
|
||||||
} else if (value !== undefined && validate(value) && version(value) === 4) {
|
} else if (codeRegex(value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return t("invalidFormat");
|
return t("invalidFormat");
|
||||||
|
|
@ -91,7 +102,7 @@
|
||||||
// Function called when a student submits a code to join a class
|
// Function called when a student submits a code to join a class
|
||||||
function submitCode(): void {
|
function submitCode(): void {
|
||||||
// Check if the code is valid
|
// Check if the code is valid
|
||||||
if (code.value !== undefined && validate(code.value) && version(code.value) === 4) {
|
if (code.value !== undefined && codeRegex(code.value)) {
|
||||||
mutate(
|
mutate(
|
||||||
{ username: username.value!, classId: code.value },
|
{ username: username.value!, classId: code.value },
|
||||||
{
|
{
|
||||||
|
|
@ -99,7 +110,7 @@
|
||||||
showSnackbar(t("sent"), "success");
|
showSnackbar(t("sent"), "success");
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -135,7 +146,7 @@
|
||||||
></v-empty-state>
|
></v-empty-state>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h1 class="title">{{ t("classes") }}</h1>
|
<h1 class="h1">{{ t("classes") }}</h1>
|
||||||
<using-query-result
|
<using-query-result
|
||||||
:query-result="classesQuery"
|
:query-result="classesQuery"
|
||||||
v-slot="classResponse: { data: ClassesResponse }"
|
v-slot="classResponse: { data: ClassesResponse }"
|
||||||
|
|
@ -161,7 +172,7 @@
|
||||||
<th class="header">{{ t("members") }}</th>
|
<th class="header">{{ t("members") }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody v-if="classResponse.data.classes.length">
|
||||||
<tr
|
<tr
|
||||||
v-for="c in classResponse.data.classes as ClassDTO[]"
|
v-for="c in classResponse.data.classes as ClassDTO[]"
|
||||||
:key="c.id"
|
:key="c.id"
|
||||||
|
|
@ -181,6 +192,21 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tbody v-else>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colspan="3"
|
||||||
|
class="empty-message"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-information-outline"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
</v-icon>
|
||||||
|
{{ t("no-classes-found") }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</v-table>
|
</v-table>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
@ -244,7 +270,7 @@
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="CODE"
|
label="CODE"
|
||||||
v-model="code"
|
v-model="code"
|
||||||
placeholder="XXXXXXXX-XXXX-4XXX-XXXX-XXXXXXXXXXXX"
|
placeholder="XXXXXX"
|
||||||
:rules="codeRules"
|
:rules="codeRules"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
@ -271,49 +297,6 @@
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
|
||||||
font-weight: bold !important;
|
|
||||||
background-color: #0e6942;
|
|
||||||
color: white;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table thead th:first-child {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table thead th:last-child {
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody tr:nth-child(odd) {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody tr:nth-child(even) {
|
|
||||||
background-color: #f6faf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
border-bottom: 1px solid #0e6942;
|
|
||||||
border-top: 1px solid #0e6942;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
width: 90%;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #0e6942;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bolder;
|
|
||||||
padding-top: 2%;
|
|
||||||
font-size: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: #0e6942;
|
color: #0e6942;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
|
|
@ -321,6 +304,7 @@
|
||||||
|
|
||||||
.join {
|
.join {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-left: 1%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
|
|
@ -331,16 +315,7 @@
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join {
|
.join {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue