import { EntityRepository } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/mysql';
import { InjectRepository } from '@mikro-orm/nestjs';
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
import { GradingResponseData } from 'shared/model/Gradings';
import { Grading } from '../../database/entities/grading.entity';
import { HandIn } from '../../database/entities/ratedEntity.entity';
import { Student } from '../../database/entities/student.entity';
import { Team } from '../../database/entities/team.entity';
import { GradingList, GradingListsForStudents } from '../../helpers/GradingList';
import { ScheinexamService } from '../scheinexam/scheinexam.service';
import { SheetService } from '../sheet/sheet.service';
import { ShortTestService } from '../short-test/short-test.service';
import { GradingDTO } from './student.dto';
import { StudentService } from './student.service';
@Injectable()
export class GradingService {
constructor(
@Inject(forwardRef(() => StudentService))
private readonly studentService: StudentService,
private readonly sheetService: SheetService,
private readonly scheinexamService: ScheinexamService,
private readonly shortTestService: ShortTestService,
private readonly entityManager: EntityManager,
@InjectRepository(Grading)
private readonly repository: EntityRepository<Grading>,
@Inject(EntityManager)
private readonly em: EntityManager
) {}
/**
* @param handInId ID of the hand-in to find the gradings for.
*
* @returns All gradings which belong to the hand-in with the given handInId.
*/
async findOfHandIn(handInId: string): Promise<GradingResponseData[]> {
const gradings = await this.repository.find({ handInId: handInId }, { populate: ['*'] });
const data: GradingResponseData[] = [];
gradings.forEach((grading) => {
grading.students.getItems().forEach((student) => {
data.push({ studentId: student.id, gradingData: grading.toDTO() });
});
});
return data;
}
/**
* @param studentId ID of the student to get the gradings for.
*
* @returns All gradings that this student has.
*/
async findOfStudent(studentId: string): Promise<GradingList> {
const gradings = await this.repository.find({ students: studentId }, { populate: ['*'] });
return new GradingList(gradings);
}
/**
* @param studentId ID of the student to get the grading for.
* @param handInId ID of the hand-in to get the grading of.
*
* @returns Grading which matches the parameters. If there is no such grading `undefined` is returned instead.
*/
async findOfStudentAndHandIn(
studentId: string,
handInId: string
): Promise<Grading | undefined> {
const grading = await this.repository.findOne(
{ students: studentId, handInId: handInId },
{ populate: ['*'] }
);
return grading ?? undefined;
}
/**
* @param studentIds IDs of all students to get the gradings for.
*
* @returns The gradings of the students with the given IDs.
*/
async findOfMultipleStudents(studentIds: string[]): Promise<GradingListsForStudents> {
if (studentIds.length === 0) return new GradingListsForStudents();
const gradings = await this.em.find(
Grading,
{
students: { $in: studentIds },
},
{ populate: ['students', 'handInId'] }
);
const gradingLists = new GradingListsForStudents();
const studentGradingMap = new Map<string, Grading[]>();
for (const grading of gradings) {
for (const student of grading.students) {
if (!studentGradingMap.has(student.id)) {
studentGradingMap.set(student.id, []);
}
studentGradingMap.get(student.id)!.push(grading);
}
}
for (const [studentId, studentGradings] of studentGradingMap.entries()) {
gradingLists.addGradingList(studentId, new GradingList(studentGradings));
}
return gradingLists;
}
/**
* @param tutorialId ID of the tutorial to get the gradings for.
* @param handInId ID of the hand-in to get the grading for.
*
* @returns The gradings of the students with the given IDs.
*/
async findOfTutorialAndHandIn(
tutorialId: string,
handInId: string
): Promise<GradingResponseData[]> {
const students = await this.studentService.findOfTutorial(tutorialId);
const gradings = students.map<Promise<GradingResponseData>>(async (s) => {
return {
studentId: s.id,
gradingData: (await this.findOfStudentAndHandIn(s.id, handInId))?.toDTO(),
};
});
return await Promise.all(gradings);
}
/**
* Sets the grading of the given student.
*
* If the DTO indicates an update the corresponding grading will be updated.
*
* @param student Student to set the grading for.
* @param dto DTO which resembles the grading.
*
* @see setOfMultipleStudents
*/
async setOfStudent(student: Student, dto: GradingDTO): Promise<void> {
return this.setOfMultipleStudents(new Map([[student, dto]]));
}
/**
* Sets the grading of the given students to the one from the DTO.
*
* @param dtos Maps each student to the DTO of the grading which belongs to it.
*
* @throws `BadRequestException` - If an error occurs during the setting process of _any_ student this exception is thrown.
*/
async setOfMultipleStudents(dtos: Map<Student, GradingDTO>): Promise<void> {
const handIns = await this.getMultipleHandInsFromDTO([...dtos.values()]);
for (const handIn of handIns.values()) {
await this.updateGradingsOfMultipleStudents({
dtos,
handIn,
});
}
await this.em.flush();
}
/**
* Sets the grading of all students of the given team to the one from the DTO.
*
* @param team Team which students should get the new grading.
* @param dto DTO of the new grading.
*
* @throws `BadRequestException` - If the students of the team have different gradings.
*/
async setOfTeam(team: Team, dto: GradingDTO): Promise<void> {
const handIn = await this.getHandInFromDTO(dto);
const students = team.getStudents();
if (students.length > 0) {
const oldGradings = await Promise.all(
students.map((student) => this.findOfStudentAndHandIn(student.id, handIn.id))
);
const oldGradingIds = new Set(
oldGradings.map((grading) => grading?.id).filter((gradingId) => gradingId)
);
if (oldGradingIds.size > 1) {
throw new BadRequestException('Students have different gradings.');
}
const oldGrading = oldGradings[0];
const newGrading =
!oldGrading || dto.createNewGrading ? new Grading({ handIn }) : oldGrading;
newGrading.updateFromDTO({ dto, handIn });
oldGrading?.students.remove(students);
newGrading.students.add(students);
if (!!oldGrading && oldGrading.students.length === 0) {
this.em.remove(oldGrading);
}
await this.em.persistAndFlush(newGrading);
}
}
async findAllGradingsOfStudent(student: Student): Promise<Grading[]> {
return this.repository.find({ students: student.id }, { populate: ['*'] });
}
async findAllGradingsOfMultipleStudents(students: Student[]): Promise<StudentAndGradings[]> {
const gradingsOfStudents: StudentAndGradings[] = [];
for (const student of students) {
const gradings = await this.findAllGradingsOfStudent(student);
gradingsOfStudents.push({
student,
gradingsOfStudent: new GradingList(gradings),
});
}
return gradingsOfStudents;
}
async findAllHandInGradingsOfTeam(team: Team, handIn: HandIn): Promise<Grading[]> {
const gradings = await Promise.all(
team.getStudents().map((s) =>
this.repository.find(
{
handInId: handIn.id,
students: s,
},
{ populate: ['*'] }
)
)
);
return [...new Set(gradings.flat())];
}
private async updateGradingOfStudent({
student,
dto,
handIn,
}: UpdateGradingParams): Promise<void> {
const oldGrading: Grading | undefined = await this.findOfStudentAndHandIn(
student.id,
handIn.id
);
const newGrading: Grading =
!oldGrading || dto.createNewGrading ? new Grading({ handIn }) : oldGrading;
newGrading.updateFromDTO({ dto, handIn });
oldGrading?.students.remove(student);
newGrading.students.add(student);
if (!!oldGrading && oldGrading.students.length === 0) {
this.em.remove(oldGrading);
}
this.em.persist(newGrading);
}
private async updateGradingsOfMultipleStudents({
dtos,
handIn,
}: UpdateMultipleStudentsGradingsParams): Promise<void> {
if (dtos.size === 0) return;
const studentIds = [...dtos.keys()].map((student) => student.id);
const gradingLists = await this.findOfMultipleStudents(studentIds);
dtos.forEach((dto, student) => {
const oldGrading = gradingLists.getGradingForHandIn(student.id, handIn);
const newGrading: Grading =
!oldGrading || dto.createNewGrading ? new Grading({ handIn }) : oldGrading;
newGrading.updateFromDTO({ dto, handIn });
oldGrading?.students.remove(student);
newGrading.students.add(student);
if (!!oldGrading && oldGrading.students.length === 0) {
this.em.remove(oldGrading);
}
this.em.persist(newGrading);
});
}
/**
* Returns either a ScheinexamDocument or an ScheinexamDocument associated to the given DTO.
*
* If at least two fields, `sheetId`, `examId` and `shortTestId`, are set, an exception is thrown. An exception is also thrown if none of the both fields is set.
*
* @param dto DTO to return the associated document with exercises for.
*
* @returns Associated document with exercises.
*
* @throws `BadRequestException` - If either all fields (`sheetId`, `examId` and `shortTestId`) or none of those fields are set.
*/
async getHandInFromDTO(dto: GradingDTO): Promise<HandIn> {
const { sheetId, examId, shortTestId } = dto;
const fieldsSet = [!!sheetId, !!examId, !!shortTestId].filter(Boolean).length;
if (fieldsSet !== 1) {
throw new BadRequestException(
'You must set exactly one of the three fields: sheetId, examId, or shortTestId.'
);
}
if (!!sheetId) {
return this.sheetService.findById(sheetId);
}
if (!!examId) {
return this.scheinexamService.findById(examId);
}
if (!!shortTestId) {
return this.shortTestService.findById(shortTestId);
}
throw new BadRequestException(
'You have to either set the sheetId or the examId or the shortTestId field.'
);
}
/**
* Returns a mapping of `HandIn` entities (either `Sheet`, `Scheinexam`, or `ShortTest`) associated with the provided DTOs.
*
* This method fetches all required `HandIn` records **in bulk**, avoiding multiple database queries.
*
* If at least two fields (`sheetId`, `examId`, and `shortTestId`) are set in any DTO, an exception is thrown.
* An exception is also thrown if none of these fields are set in a DTO.
*
* @param dtos - List of `GradingDTO`s containing references to `HandIn` entities.
*
* @returns A `Map` where the key is the `HandIn` ID, and the value is the corresponding `HandIn` entity.
*
* @throws `BadRequestException` - If a DTO contains at least two of the fields (`sheetId`, `examId`, `shortTestId`) or none of them.
*/
async getMultipleHandInsFromDTO(dtos: GradingDTO[]): Promise<Map<string, HandIn>> {
const sheetIds = new Set<string>();
const examIds = new Set<string>();
const shortTestIds = new Set<string>();
for (const dto of dtos) {
const { sheetId, examId, shortTestId } = dto;
const fieldsSet = [!!sheetId, !!examId, !!shortTestId].filter(Boolean).length;
if (fieldsSet > 1) {
throw new BadRequestException(
'You must set only one of the three fields: sheetId, examId, or shortTestId.'
);
}
if (fieldsSet === 0) {
throw new BadRequestException(
'You must set at least one of the fields: sheetId, examId, or shortTestId.'
);
}
if (sheetId) sheetIds.add(sheetId);
if (examId) examIds.add(examId);
if (shortTestId) shortTestIds.add(shortTestId);
}
const [sheets, exams, shortTests] = await Promise.all([
sheetIds.size ? this.sheetService.findMany([...sheetIds]) : Promise.resolve([]),
examIds.size ? this.scheinexamService.findMany([...examIds]) : Promise.resolve([]),
shortTestIds.size
? this.shortTestService.findMany([...shortTestIds])
: Promise.resolve([]),
]);
const handInMap = new Map<string, HandIn>();
for (const sheet of sheets) handInMap.set(sheet.id, sheet);
for (const exam of exams) handInMap.set(exam.id, exam);
for (const shortTest of shortTests) handInMap.set(shortTest.id, shortTest);
return handInMap;
}
}
interface UpdateGradingParams {
student: Student;
dto: GradingDTO;
handIn: HandIn;
}
interface UpdateMultipleStudentsGradingsParams {
dtos: Map<Student, GradingDTO>;
handIn: HandIn;
}
export interface StudentAndGradings {
student: Student;
gradingsOfStudent: GradingList;
}