Merge branch 'dev' into feat/discussions
This commit is contained in:
commit
e28a57754f
44 changed files with 2270 additions and 767 deletions
|
@ -11,8 +11,7 @@ import {
|
|||
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
|
||||
import { requireFields } from './error-helper.js';
|
||||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
import { Assignment } from '../entities/assignments/assignment.entity.js';
|
||||
import { EntityDTO } from '@mikro-orm/core';
|
||||
import { FALLBACK_LANG } from '../config.js';
|
||||
|
||||
function getAssignmentParams(req: Request): { classid: string; assignmentNumber: number; full: boolean } {
|
||||
const classid = req.params.classid;
|
||||
|
@ -38,14 +37,19 @@ export async function getAllAssignmentsHandler(req: Request, res: Response): Pro
|
|||
|
||||
export async function createAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||
const classid = req.params.classid;
|
||||
const description = req.body.description;
|
||||
const language = req.body.language;
|
||||
const learningPath = req.body.learningPath;
|
||||
const description = req.body.description || '';
|
||||
const language = req.body.language || FALLBACK_LANG;
|
||||
const learningPath = req.body.learningPath || '';
|
||||
const title = req.body.title;
|
||||
|
||||
requireFields({ description, language, learningPath, title });
|
||||
requireFields({ title });
|
||||
|
||||
const assignmentData = req.body as AssignmentDTO;
|
||||
const assignmentData = {
|
||||
description: description,
|
||||
language: language,
|
||||
learningPath: learningPath,
|
||||
title: title,
|
||||
} as AssignmentDTO;
|
||||
const assignment = await createAssignment(classid, assignmentData);
|
||||
|
||||
res.json({ assignment });
|
||||
|
@ -62,7 +66,7 @@ export async function getAssignmentHandler(req: Request, res: Response): Promise
|
|||
export async function putAssignmentHandler(req: Request, res: Response): Promise<void> {
|
||||
const { classid, assignmentNumber } = getAssignmentParams(req);
|
||||
|
||||
const assignmentData = req.body as Partial<EntityDTO<Assignment>>;
|
||||
const assignmentData = req.body as Partial<AssignmentDTO>;
|
||||
const assignment = await putAssignment(classid, assignmentNumber, assignmentData);
|
||||
|
||||
res.json({ assignment });
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
getJoinRequestsByClass,
|
||||
getStudentsByTeacher,
|
||||
getTeacher,
|
||||
getTeacherAssignments,
|
||||
updateClassJoinRequestStatus,
|
||||
} from '../services/teachers.js';
|
||||
import { requireFields } from './error-helper.js';
|
||||
|
@ -59,6 +60,16 @@ export async function getTeacherClassHandler(req: Request, res: Response): Promi
|
|||
res.json({ classes });
|
||||
}
|
||||
|
||||
export async function getTeacherAssignmentsHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
const full = req.query.full === 'true';
|
||||
requireFields({ username });
|
||||
|
||||
const assignments = await getTeacherAssignments(username, full);
|
||||
|
||||
res.json({ assignments });
|
||||
}
|
||||
|
||||
export async function getTeacherStudentHandler(req: Request, res: Response): Promise<void> {
|
||||
const username = req.params.username;
|
||||
const full = req.query.full === 'true';
|
||||
|
|
|
@ -7,7 +7,7 @@ export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
|
|||
return this.findOne({ within: within, id: id }, { populate: ['groups', 'groups.members'] });
|
||||
}
|
||||
public async findByClassIdAndAssignmentId(withinClass: string, id: number): Promise<Assignment | null> {
|
||||
return this.findOne({ within: { classId: withinClass }, id: id });
|
||||
return this.findOne({ within: { classId: withinClass }, id: id }, { populate: ['groups', 'groups.members'] });
|
||||
}
|
||||
public async findAllByResponsibleTeacher(teacherUsername: string): Promise<Assignment[]> {
|
||||
return this.findAll({
|
||||
|
@ -20,6 +20,7 @@ export class AssignmentRepository extends DwengoEntityRepository<Assignment> {
|
|||
},
|
||||
},
|
||||
},
|
||||
populate: ['groups', 'groups.members'],
|
||||
});
|
||||
}
|
||||
public async findAllAssignmentsInClass(within: Class): Promise<Assignment[]> {
|
||||
|
|
|
@ -28,4 +28,9 @@ export class GroupRepository extends DwengoEntityRepository<Group> {
|
|||
groupNumber: groupNumber,
|
||||
});
|
||||
}
|
||||
public async deleteAllByAssignment(assignment: Assignment): Promise<void> {
|
||||
return this.deleteAllWhere({
|
||||
assignment: assignment,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,4 +16,13 @@ export abstract class DwengoEntityRepository<T extends object> extends EntityRep
|
|||
await em.flush();
|
||||
}
|
||||
}
|
||||
public async deleteAllWhere(query: FilterQuery<T>): Promise<void> {
|
||||
const toDelete = await this.find(query);
|
||||
const em = this.getEntityManager();
|
||||
|
||||
if (toDelete) {
|
||||
em.remove(toDelete);
|
||||
await em.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
|
|||
description: assignment.description,
|
||||
learningPath: assignment.learningPathHruid,
|
||||
language: assignment.learningPathLanguage,
|
||||
deadline: assignment.deadline ?? new Date(),
|
||||
deadline: assignment.deadline ?? null,
|
||||
groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,10 +7,16 @@ 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';
|
||||
import { fetchClass } from '../../../services/classes.js';
|
||||
import { fetchGroup } from '../../../services/groups.js';
|
||||
import { requireFields } from '../../../controllers/error-helper.js';
|
||||
import { SubmissionDTO } from '@dwengo-1/common/interfaces/submission';
|
||||
|
||||
export const onlyAllowSubmitter = authorize(
|
||||
(auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { submitter: string }).submitter === auth.username
|
||||
);
|
||||
export const onlyAllowSubmitter = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const submittedFor = (req.body as SubmissionDTO).submitter.username;
|
||||
const submittedBy = auth.username;
|
||||
return submittedFor === submittedBy;
|
||||
});
|
||||
|
||||
export const onlyAllowIfHasAccessToSubmission = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const { hruid: lohruid, id: submissionNumber } = req.params;
|
||||
|
@ -26,3 +32,17 @@ export const onlyAllowIfHasAccessToSubmission = authorize(async (auth: Authentic
|
|||
|
||||
return submission.onBehalfOf.members.map(mapToUsername).includes(auth.username);
|
||||
});
|
||||
|
||||
export const onlyAllowIfHasAccessToSubmissionFromParams = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
|
||||
const { classId, assignmentId, groupId } = req.query;
|
||||
|
||||
requireFields({ classId, assignmentId, groupId });
|
||||
|
||||
if (auth.accountType === AccountType.Teacher) {
|
||||
const cls = await fetchClass(classId as string);
|
||||
return cls.teachers.map(mapToUsername).includes(auth.username);
|
||||
}
|
||||
|
||||
const group = await fetchGroup(classId as string, Number(assignmentId as string), Number(groupId as string));
|
||||
return group.members.map(mapToUsername).includes(auth.username);
|
||||
});
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import express from 'express';
|
||||
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';
|
||||
import {
|
||||
onlyAllowIfHasAccessToSubmission,
|
||||
onlyAllowIfHasAccessToSubmissionFromParams,
|
||||
onlyAllowSubmitter,
|
||||
} from '../middleware/auth/checks/submission-checks.js';
|
||||
import { studentsOnly } from '../middleware/auth/checks/auth-checks.js';
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get('/', adminOnly, getSubmissionsHandler);
|
||||
router.get('/', onlyAllowIfHasAccessToSubmissionFromParams, getSubmissionsHandler);
|
||||
|
||||
router.post('/', studentsOnly, onlyAllowSubmitter, createSubmissionHandler);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
deleteTeacherHandler,
|
||||
getAllTeachersHandler,
|
||||
getStudentJoinRequestHandler,
|
||||
getTeacherAssignmentsHandler,
|
||||
getTeacherClassHandler,
|
||||
getTeacherHandler,
|
||||
getTeacherStudentHandler,
|
||||
|
@ -28,6 +29,8 @@ router.get('/:username/classes', preventImpersonation, getTeacherClassHandler);
|
|||
|
||||
router.get('/:username/students', preventImpersonation, getTeacherStudentHandler);
|
||||
|
||||
router.get(`/:username/assignments`, getTeacherAssignmentsHandler);
|
||||
|
||||
router.get('/:username/joinRequests/:classId', onlyAllowTeacherOfClass, getStudentJoinRequestHandler);
|
||||
|
||||
router.put('/:username/joinRequests/:classId/:studentUsername', onlyAllowTeacherOfClass, updateStudentJoinRequestHandler);
|
||||
|
|
|
@ -14,10 +14,13 @@ import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submissi
|
|||
import { fetchClass } from './classes.js';
|
||||
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
||||
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
|
||||
import { EntityDTO } from '@mikro-orm/core';
|
||||
import { EntityDTO, ForeignKeyConstraintViolationException } from '@mikro-orm/core';
|
||||
import { putObject } from './service-helper.js';
|
||||
import { fetchStudents } from './students.js';
|
||||
import { ServerErrorException } from '../exceptions/server-error-exception.js';
|
||||
import { BadRequestException } from '../exceptions/bad-request-exception.js';
|
||||
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||
import { PostgreSqlExceptionConverter } from '@mikro-orm/postgresql';
|
||||
|
||||
export async function fetchAssignment(classid: string, assignmentNumber: number): Promise<Assignment> {
|
||||
const classRepository = getClassRepository();
|
||||
|
@ -59,7 +62,7 @@ export async function createAssignment(classid: string, assignmentData: Assignme
|
|||
|
||||
if (assignmentData.groups) {
|
||||
/*
|
||||
For some reason when trying to add groups, it does not work when using the original assignment variable.
|
||||
For some reason when trying to add groups, it does not work when using the original assignment variable.
|
||||
The assignment needs to be refetched in order for it to work.
|
||||
*/
|
||||
|
||||
|
@ -93,10 +96,36 @@ export async function getAssignment(classid: string, id: number): Promise<Assign
|
|||
return mapToAssignmentDTO(assignment);
|
||||
}
|
||||
|
||||
export async function putAssignment(classid: string, id: number, assignmentData: Partial<EntityDTO<Assignment>>): Promise<AssignmentDTO> {
|
||||
function hasDuplicates(arr: string[]): boolean {
|
||||
return new Set(arr).size !== arr.length;
|
||||
}
|
||||
|
||||
export async function putAssignment(classid: string, id: number, assignmentData: Partial<AssignmentDTO>): Promise<AssignmentDTO> {
|
||||
const assignment = await fetchAssignment(classid, id);
|
||||
|
||||
await putObject<Assignment>(assignment, assignmentData, getAssignmentRepository());
|
||||
if (assignmentData.groups) {
|
||||
if (hasDuplicates(assignmentData.groups.flat() as string[])) {
|
||||
throw new BadRequestException('Student can only be in one group');
|
||||
}
|
||||
|
||||
const studentLists = await Promise.all((assignmentData.groups as string[][]).map(async (group) => await fetchStudents(group)));
|
||||
|
||||
const groupRepository = getGroupRepository();
|
||||
await groupRepository.deleteAllByAssignment(assignment);
|
||||
await Promise.all(
|
||||
studentLists.map(async (students) => {
|
||||
const newGroup = groupRepository.create({
|
||||
assignment: assignment,
|
||||
members: students,
|
||||
});
|
||||
await groupRepository.save(newGroup);
|
||||
})
|
||||
);
|
||||
|
||||
delete assignmentData.groups;
|
||||
}
|
||||
|
||||
await putObject<Assignment>(assignment, assignmentData as Partial<EntityDTO<Assignment>>, getAssignmentRepository());
|
||||
|
||||
return mapToAssignmentDTO(assignment);
|
||||
}
|
||||
|
@ -106,7 +135,16 @@ export async function deleteAssignment(classid: string, id: number): Promise<Ass
|
|||
const cls = await fetchClass(classid);
|
||||
|
||||
const assignmentRepository = getAssignmentRepository();
|
||||
await assignmentRepository.deleteByClassAndId(cls, id);
|
||||
|
||||
try {
|
||||
await assignmentRepository.deleteByClassAndId(cls, id);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof ForeignKeyConstraintViolationException || e instanceof PostgreSqlExceptionConverter) {
|
||||
throw new ConflictException('Cannot delete assigment with questions or submissions');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return mapToAssignmentDTO(assignment);
|
||||
}
|
||||
|
|
|
@ -70,6 +70,8 @@ async function convertLearningPath(learningPath: LearningPathEntity, order: numb
|
|||
// Convert the learning object notes as retrieved from the database into the expected response format-
|
||||
const convertedNodes = await convertNodes(nodesToLearningObjects, personalizedFor);
|
||||
|
||||
const nodesActuallyOnPath = traverseLearningPath(convertedNodes);
|
||||
|
||||
return {
|
||||
_id: `${learningPath.hruid}/${learningPath.language}`, // For backwards compatibility with the original Dwengo API.
|
||||
__order: order,
|
||||
|
@ -79,8 +81,8 @@ async function convertLearningPath(learningPath: LearningPathEntity, order: numb
|
|||
image: image,
|
||||
title: learningPath.title,
|
||||
nodes: convertedNodes,
|
||||
num_nodes: learningPath.nodes.length,
|
||||
num_nodes_left: convertedNodes.filter((it) => !it.done).length,
|
||||
num_nodes: nodesActuallyOnPath.length,
|
||||
num_nodes_left: nodesActuallyOnPath.filter((it) => !it.done).length,
|
||||
keywords: keywords.join(' '),
|
||||
target_ages: targetAges,
|
||||
max_age: Math.max(...targetAges),
|
||||
|
@ -180,7 +182,6 @@ function convertTransition(
|
|||
return {
|
||||
_id: String(index), // Retained for backwards compatibility. The index uniquely identifies the transition within the learning path.
|
||||
default: false, // We don't work with default transitions but retain this for backwards compatibility.
|
||||
condition: transition.condition,
|
||||
next: {
|
||||
_id: nextNode._id ? nextNode._id + index : v4(), // Construct a unique ID for the transition for backwards compatibility.
|
||||
hruid: transition.next.learningObjectHruid,
|
||||
|
@ -191,6 +192,29 @@ function convertTransition(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start from the start node and then always take the first transition until there are no transitions anymore.
|
||||
* Returns the traversed nodes as an array. (This effectively filters outs nodes that cannot be reached.)
|
||||
*/
|
||||
function traverseLearningPath(nodes: LearningObjectNode[]): LearningObjectNode[] {
|
||||
const traversedNodes: LearningObjectNode[] = [];
|
||||
let currentNode = nodes.find((it) => it.start_node);
|
||||
|
||||
while (currentNode) {
|
||||
traversedNodes.push(currentNode);
|
||||
|
||||
const next = currentNode.transitions[0]?.next;
|
||||
|
||||
if (next) {
|
||||
currentNode = nodes.find((it) => it.learningobject_hruid === next.hruid && it.language === next.language && it.version === next.version);
|
||||
} else {
|
||||
currentNode = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return traversedNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service providing access to data about learning paths from the database.
|
||||
*/
|
||||
|
|
|
@ -10,7 +10,7 @@ import { mapToClassDTO } from '../interfaces/class.js';
|
|||
import { mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js';
|
||||
import { mapToStudent, mapToStudentDTO } from '../interfaces/student.js';
|
||||
import { mapToSubmissionDTO, mapToSubmissionDTOId } from '../interfaces/submission.js';
|
||||
import { getAllAssignments } from './assignments.js';
|
||||
import { fetchAssignment } from './assignments.js';
|
||||
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
|
||||
import { mapToStudentRequest, mapToStudentRequestDTO } from '../interfaces/student-request.js';
|
||||
import { Student } from '../entities/users/student.entity.js';
|
||||
|
@ -26,6 +26,7 @@ import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-requ
|
|||
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||
import { Submission } from '../entities/assignments/submission.entity.js';
|
||||
import { mapToUsername } from '../interfaces/user.js';
|
||||
import { mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
|
||||
|
||||
export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
|
||||
const studentRepository = getStudentRepository();
|
||||
|
@ -50,8 +51,7 @@ export async function fetchStudent(username: string): Promise<Student> {
|
|||
}
|
||||
|
||||
export async function fetchStudents(usernames: string[]): Promise<Student[]> {
|
||||
const members = await Promise.all(usernames.map(async (username) => await fetchStudent(username)));
|
||||
return members;
|
||||
return await Promise.all(usernames.map(async (username) => await fetchStudent(username)));
|
||||
}
|
||||
|
||||
export async function getStudent(username: string): Promise<StudentDTO> {
|
||||
|
@ -102,10 +102,14 @@ export async function getStudentClasses(username: string, full: boolean): Promis
|
|||
export async function getStudentAssignments(username: string, full: boolean): Promise<AssignmentDTO[] | AssignmentDTOId[]> {
|
||||
const student = await fetchStudent(username);
|
||||
|
||||
const classRepository = getClassRepository();
|
||||
const classes = await classRepository.findByStudent(student);
|
||||
const groupRepository = getGroupRepository();
|
||||
const groups = await groupRepository.findAllGroupsWithStudent(student);
|
||||
const assignments = await Promise.all(groups.map(async (group) => await fetchAssignment(group.assignment.within.classId!, group.assignment.id!)));
|
||||
|
||||
return (await Promise.all(classes.map(async (cls) => await getAllAssignments(cls.classId!, full)))).flat();
|
||||
if (full) {
|
||||
return assignments.map(mapToAssignmentDTO);
|
||||
}
|
||||
return assignments.map(mapToAssignmentDTOId);
|
||||
}
|
||||
|
||||
export async function getStudentGroups(username: string, full: boolean): Promise<GroupDTO[] | GroupDTOId[]> {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getClassJoinRequestRepository, getClassRepository, getTeacherRepository } from '../data/repositories.js';
|
||||
import { getAssignmentRepository, getClassJoinRequestRepository, getClassRepository, getTeacherRepository } from '../data/repositories.js';
|
||||
import { mapToClassDTO } from '../interfaces/class.js';
|
||||
import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js';
|
||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||
|
@ -18,6 +18,8 @@ import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
|||
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
|
||||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||
import { AssignmentDTO, AssignmentDTOId } from '@dwengo-1/common/interfaces/assignment';
|
||||
import { mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js';
|
||||
import { mapToUsername } from '../interfaces/user.js';
|
||||
|
||||
export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> {
|
||||
|
@ -91,6 +93,17 @@ export async function getClassesByTeacher(username: string, full: boolean): Prom
|
|||
return classes.map((cls) => cls.id);
|
||||
}
|
||||
|
||||
export async function getTeacherAssignments(username: string, full: boolean): Promise<AssignmentDTO[] | AssignmentDTOId[]> {
|
||||
const assignmentRepository = getAssignmentRepository();
|
||||
const assignments = await assignmentRepository.findAllByResponsibleTeacher(username);
|
||||
|
||||
if (full) {
|
||||
return assignments.map(mapToAssignmentDTO);
|
||||
}
|
||||
|
||||
return assignments.map(mapToAssignmentDTOId);
|
||||
}
|
||||
|
||||
export async function getStudentsByTeacher(username: string, full: boolean): Promise<StudentDTO[] | string[]> {
|
||||
const classes: ClassDTO[] = await fetchClassesByTeacher(username);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue