src/module/tutorial/tutorial.service.ts
Methods |
|
constructor(userService: UserService, studentService: StudentService, entityManager: EntityManager, repository: EntityRepository<Tutorial>, em: EntityManager)
|
||||||||||||||||||
Defined in src/module/tutorial/tutorial.service.ts:30
|
||||||||||||||||||
Parameters :
|
Private Async addSubstituteForDates | |||||
addSubstituteForDates(undefined: AddSubstituteForDatesParams)
|
|||||
Defined in src/module/tutorial/tutorial.service.ts:249
|
|||||
Adds the tutor with the given id as substitute of the tutorial to the given dates. If a substitute for the given tutial and date The updates are persisted in the entityManager but not flushed or committed. Therefore they must be committed manually.
Parameters :
Returns :
Promise<void>
|
Private assertAtLeastOneDate | ||||||
assertAtLeastOneDate(dates: DateTime[])
|
||||||
Defined in src/module/tutorial/tutorial.service.ts:490
|
||||||
Parameters :
Returns :
void
|
Private assertCorrectorsHaveCorrectorRole | ||||||
assertCorrectorsHaveCorrectorRole(correctors: User[])
|
||||||
Defined in src/module/tutorial/tutorial.service.ts:472
|
||||||
Parameters :
Returns :
void
|
Private assertTutorHasTutorRole | ||||||
assertTutorHasTutorRole(tutor?: User)
|
||||||
Defined in src/module/tutorial/tutorial.service.ts:456
|
||||||
Parameters :
Returns :
void
|
Private Async assertTutorialSlot | ||||||
assertTutorialSlot(slot: string)
|
||||||
Defined in src/module/tutorial/tutorial.service.ts:482
|
||||||
Parameters :
Returns :
any
|
Private assertTutorsHaveTutorRole | ||||||
assertTutorsHaveTutorRole(tutors: User[])
|
||||||
Defined in src/module/tutorial/tutorial.service.ts:462
|
||||||
Parameters :
Returns :
void
|
Async create | ||||||||
create(dto: TutorialDTO)
|
||||||||
Defined in src/module/tutorial/tutorial.service.ts:100
|
||||||||
Creates a new tutorial based on the given information.
Parameters :
Returns :
Promise<ITutorial>
Created tutorial. |
Async createMany | ||||||||
createMany(dto: TutorialGenerationDTO)
|
||||||||
Defined in src/module/tutorial/tutorial.service.ts:334
|
||||||||
Creates multiple tutorials with the given information. The created tutorials will not have any correctors or a tutor assigned but will have all dates specified in the DTO.
Parameters :
Returns :
Promise<ITutorial[]>
Array containing the response DTOs of the created tutorials. |
Private Async createTutorial | |||||
createTutorial(undefined: CreateParameters)
|
|||||
Defined in src/module/tutorial/tutorial.service.ts:379
|
|||||
Creates a new tutorial and adds it to the database. This function first checks if the given
Parameters :
Returns :
Promise<Tutorial>
Document of the created tutorial. |
Private datesInIntervalGroupedByWeekday | ||||||||
datesInIntervalGroupedByWeekday(interval: Interval)
|
||||||||
Defined in src/module/tutorial/tutorial.service.ts:408
|
||||||||
Returns all dates in the interval grouped by their weekday. Groups all dates in the interval by their weekday (1 - monday, 7 - sunday) and returns a map with those weekdays as keys. The map only contains weekdays as keys which are present in the interval (ie if only a monday and a tuesday are in the interval the map will only contain the keys
Parameters :
Returns :
Map<number, DateTime[]>
Map with weekdays as keys and all dates from the interval on the corresponding weekday. Note: Not all weekdays may be present in the returned map. |
Async delete | ||||||||
delete(id: string)
|
||||||||
Defined in src/module/tutorial/tutorial.service.ts:168
|
||||||||
Deletes the given tutorial and returns it's document. However, a tutorial which still has one or more students assigned to it can not be deleted.
Parameters :
Returns :
Promise<void>
Document of the deleted tutorial. |
Async findAll |
findAll()
|
Defined in src/module/tutorial/tutorial.service.ts:46
|
Returns :
Promise<Tutorial[]>
All tutorials saved in the database. |
Async findById | ||||||||
findById(id: string)
|
||||||||
Defined in src/module/tutorial/tutorial.service.ts:80
|
||||||||
Searches for a tutorial with the given ID and returns it.
Parameters :
Returns :
Promise<Tutorial>
TutorialDocument with the given ID. |
Async findMultiple | ||||||||
findMultiple(ids: string[])
|
||||||||
Defined in src/module/tutorial/tutorial.service.ts:57
|
||||||||
Parameters :
Returns :
Promise<Tutorial[]>
Tutorials with the given IDs. |
Async getAllStudentsOfTutorial | ||||||||
getAllStudentsOfTutorial(id: string)
|
||||||||
Defined in src/module/tutorial/tutorial.service.ts:320
|
||||||||
Returns all students in the tutorial with the given ID.
Parameters :
Returns :
Promise<Student[]>
All students in the tutorial with the given ID. |
Private Async getSubstitutesForDates | ||||||||||||
getSubstitutesForDates(tutorial: Tutorial, dates: DateTime[])
|
||||||||||||
Defined in src/module/tutorial/tutorial.service.ts:301
|
||||||||||||
Gets substitutes for a tutorial and the given dates
Parameters :
Returns :
Promise<Substitute[]>
the found substitutes |
Private removeExcludedDates | ||||||||||||
removeExcludedDates(dates: DateTime[], excludedDates: ExcludedTutorialDate[])
|
||||||||||||
Defined in src/module/tutorial/tutorial.service.ts:437
|
||||||||||||
Creates a copy of the The
Parameters :
Returns :
DateTime[]
A copy of the |
Private Async removeSubstituteForDates | |||||
removeSubstituteForDates(undefined: RemoveSubstituteForDatesParams)
|
|||||
Defined in src/module/tutorial/tutorial.service.ts:286
|
|||||
Removes all substitute from the tutorial at the given dates. The changes are persisted in the entityManager but not flushed or committed. Therefore they must be committed manually.
Parameters :
Returns :
Promise<void>
|
Async setMultipleSubstitutes | |||||||||
setMultipleSubstitutes(id: string, dtos: SubstituteDTO[])
|
|||||||||
Defined in src/module/tutorial/tutorial.service.ts:198
|
|||||||||
Sets the substitutes of a tutorial according to the given DTOs. Every DTO in the given array will be handled separately. See setTutorialSubstitute
Parameters :
Returns :
Promise<void>
|
Async setSubstitute | |||||||||
setSubstitute(id: string, dto: SubstituteDTO)
|
|||||||||
Defined in src/module/tutorial/tutorial.service.ts:186
|
|||||||||
Sets the substitute for the given dates to the given tutor. See setTutorialSubstitute
Parameters :
Returns :
Promise<void>
|
Private Async setTutorialSubstitutes | ||||||||||||
setTutorialSubstitutes(tutorial: Tutorial, dtos: SubstituteDTO[])
|
||||||||||||
Defined in src/module/tutorial/tutorial.service.ts:215
|
||||||||||||
Sets the substitutes according to the given data. If the DTO does not contain a
Parameters :
Returns :
Promise<void>
|
Async update | ||||||||||||
update(id: string, dto: TutorialDTO)
|
||||||||||||
Defined in src/module/tutorial/tutorial.service.ts:132
|
||||||||||||
Updates the tutorial with the given information and returns the updated tutorial.
Parameters :
Returns :
Promise<ITutorial>
Updated document. |
import { EntityRepository } from '@mikro-orm/core';
import { EntityManager } from '@mikro-orm/mysql';
import { InjectRepository } from '@mikro-orm/nestjs';
import {
BadRequestException,
forwardRef,
Inject,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { DateTime, Interval } from 'luxon';
import { Role } from 'shared/model/Role';
import { ITutorial } from 'shared/model/Tutorial';
import { Student } from '../../database/entities/student.entity';
import { Substitute } from '../../database/entities/substitute.entity';
import { Tutorial } from '../../database/entities/tutorial.entity';
import { User } from '../../database/entities/user.entity';
import { CRUDService } from '../../helpers/CRUDService';
import { StudentService } from '../student/student.service';
import { UserService } from '../user/user.service';
import {
ExcludedTutorialDate,
SubstituteDTO,
TutorialDTO,
TutorialGenerationDTO,
} from './tutorial.dto';
import { updateCollection } from '../../helpers/updateCollection';
@Injectable()
export class TutorialService implements CRUDService<ITutorial, TutorialDTO, Tutorial> {
constructor(
@Inject(forwardRef(() => UserService))
private readonly userService: UserService,
@Inject(forwardRef(() => StudentService))
private readonly studentService: StudentService,
private readonly entityManager: EntityManager,
@InjectRepository(Tutorial)
private readonly repository: EntityRepository<Tutorial>,
@Inject(EntityManager)
private readonly em: EntityManager
) {}
/**
* @returns All tutorials saved in the database.
*/
async findAll(): Promise<Tutorial[]> {
return this.repository.findAll({ populate: ['*'] });
}
/**
*
* @param ids IDs of the tutorials to get.
* @returns Tutorials with the given IDs.
*
* @throws {@link NotFoundException} - If at least one of the tutorials could not be found.
*/
async findMultiple(ids: string[]): Promise<Tutorial[]> {
const tutorials = await this.repository.find({ id: { $in: ids } }, { populate: ['*'] });
if (tutorials.length !== ids.length) {
const tutorialIds = tutorials.map((tutorial) => tutorial.id);
const notFound = ids.filter((id) => !tutorialIds.includes(id));
throw new NotFoundException(
`Could not find the tutorials with the following ids: [${notFound.join(', ')}]`
);
}
return tutorials;
}
/**
* Searches for a tutorial with the given ID and returns it.
*
* @param id ID to search for.
*
* @returns TutorialDocument with the given ID.
*
* @throws `NotFoundException` - If no tutorial with the given ID could be found.
*/
async findById(id: string): Promise<Tutorial> {
const tutorial = await this.repository.findOne({ id }, { populate: ['*'] });
if (!tutorial) {
throw new NotFoundException(`Tutorial with the ID ${id} could not be found.`);
}
return tutorial;
}
/**
* Creates a new tutorial based on the given information.
*
* @param dto Information about the tutorial to create.
*
* @throws `NotFoundException` - If the tutor or any of the correctors could not be found.
* @throws `BadRequestException` - If the tutor to be assigned does not have the TUTOR role or if any of the correctors to be assigned does not have the CORRECTOR role.
*
* @returns Created tutorial.
*/
async create(dto: TutorialDTO): Promise<ITutorial> {
await this.assertTutorialSlot(dto.slot);
const { slot, tutorIds, correctorIds, startTime, endTime, dates } = dto;
const [tutors, correctors] = await Promise.all([
Promise.all(tutorIds.map((id) => this.userService.findById(id))),
Promise.all(correctorIds.map((id) => this.userService.findById(id))),
]);
const created = await this.createTutorial({
slot,
tutors,
correctors,
startTime: DateTime.fromISO(startTime),
endTime: DateTime.fromISO(endTime),
dates: dates.map((date) => DateTime.fromISO(date)),
});
return created.toDTO();
}
/**
* Updates the tutorial with the given information and returns the updated tutorial.
*
* @param id ID of the Tutorial to update.
* @param dto Information to update the tutorial with.
*
* @returns Updated document.
*
* @throws `BadRequestException` - If the tutor to be assigned does not have the TUTOR role or if any of the correctors to be assigned does not have the CORRECTOR role.
* @throws `NotFoundException` - If the tutorial with the given ID or if the tutor with the ID in the DTO or if any corrector with the ID in the DTO could NOT be found.
*/
async update(id: string, dto: TutorialDTO): Promise<ITutorial> {
const tutorial = await this.findById(id);
const tutors = await Promise.all(
dto.tutorIds.map((tutorId) => this.userService.findById(tutorId))
);
const correctors = await Promise.all(
dto.correctorIds.map((corrId) => this.userService.findById(corrId))
);
this.assertTutorsHaveTutorRole(tutors);
this.assertCorrectorsHaveCorrectorRole(correctors);
tutorial.slot = dto.slot;
tutorial.dates = dto.dates.map((date) => DateTime.fromISO(date));
tutorial.startTime = DateTime.fromISO(dto.startTime);
tutorial.endTime = DateTime.fromISO(dto.endTime);
updateCollection(tutorial.tutors, tutors);
updateCollection(tutorial.correctors, correctors);
await this.em.persistAndFlush(tutorial);
return tutorial.toDTO();
}
/**
* Deletes the given tutorial and returns it's document.
*
* However, a tutorial which still has one or more students assigned to it can _not_ be deleted.
*
* @param id ID of the tutorial to delete.
*
* @returns Document of the deleted tutorial.
*
* @throws `NotFoundException` - If no tutorial with the given ID could be found.
* @throws `BadRequestException` - If the tutorial to delete still has one or more student assigned to it.
*/
async delete(id: string): Promise<void> {
const tutorial = await this.findById(id);
if (tutorial.studentCount > 0) {
throw new BadRequestException(`A tutorial with students can NOT be deleted.`);
}
this.entityManager.remove(tutorial.teams.getItems());
this.entityManager.remove(tutorial.substitutes.getItems());
await this.em.removeAndFlush(tutorial);
}
/**
* Sets the substitute for the given dates to the given tutor.
*
* @see setTutorialSubstitute
*/
async setSubstitute(id: string, dto: SubstituteDTO): Promise<void> {
const tutorial = await this.findById(id);
await this.setTutorialSubstitutes(tutorial, [dto]);
}
/**
* Sets the substitutes of a tutorial according to the given DTOs.
*
* Every DTO in the given array will be handled separately.
*
* @see setTutorialSubstitute
*/
async setMultipleSubstitutes(id: string, dtos: SubstituteDTO[]): Promise<void> {
const tutorial = await this.findById(id);
await this.setTutorialSubstitutes(tutorial, dtos);
}
/**
* Sets the substitutes according to the given data.
*
* If the DTO does not contain a `tutorId` field (ie it is `undefined`) the substitutes of the given dates will be removed. If there is already a substitute for a given date in the DTO the previous substitute gets overridden.
*
* @param tutorial Tutorial to set the substitute for.
* @param dtos DTOs containing the information of the substitutes.
*
* @throws `BadRequestException` - If the tutorial of the given `id` parameter could not be found.
* @throws `BadRequestException` - If the `tutorId` field contains a user ID which can not be found or which does not belong to a tutor.
*/
private async setTutorialSubstitutes(tutorial: Tutorial, dtos: SubstituteDTO[]): Promise<void> {
await Promise.all(
dtos.map(async (dto) => {
const dates = dto.dates.map((date) => DateTime.fromISO(date));
if (!dto.tutorId) {
await this.removeSubstituteForDates({
tutorial: tutorial,
dates: dates,
});
} else {
await this.addSubstituteForDates({
tutorId: dto.tutorId,
tutorial: tutorial,
dates: dates,
});
}
})
);
await this.entityManager.flush();
}
/**
* Adds the tutor with the given id as substitute of the tutorial to the given dates.
* If a substitute for the given tutial and date
*
* The updates are persisted in the {@link entityManager} but not flushed or committed. Therefore they must be committed manually.
*
* @param tutorId ID of the substitute tutor.
* @param tutorial Tutorial to substitute.
* @param dates Dates to substitute.
*
* @throws {@link NotFoundException} - If no tutor with the given `tutorId` could be found.
* @private
*/
private async addSubstituteForDates({
tutorId,
tutorial,
dates,
}: AddSubstituteForDatesParams): Promise<void> {
const tutor = await this.userService.findById(tutorId);
this.assertTutorHasTutorRole(tutor);
const existingSubstitutes = await this.getSubstitutesForDates(tutorial, dates);
dates.forEach((date) => {
const existingSubstitute = existingSubstitutes.find(
(substitute) => +substitute.date === +date
);
if (existingSubstitute) {
existingSubstitute.substituteTutor = tutor;
this.entityManager.persist(existingSubstitute);
} else {
this.entityManager.persist(
new Substitute({
tutorialToSubstitute: tutorial,
substituteTutor: tutor,
date,
})
);
}
});
}
/**
* Removes all substitute from the tutorial at the given dates.
*
* The changes are persisted in the {@link entityManager} but not flushed or committed. Therefore they must be committed manually.
*
* @param tutorial Tutorial to remove the substitutes.
* @param dates Dates to remove the substitutes.
* @private
*/
private async removeSubstituteForDates({
tutorial,
dates,
}: RemoveSubstituteForDatesParams): Promise<void> {
const substituteForGivenDates = await this.getSubstitutesForDates(tutorial, dates);
this.entityManager.remove(substituteForGivenDates);
}
/**
* Gets substitutes for a tutorial and the given dates
*
* @param tutorial Tutorial substitutes must be associated with.
* @param dates Dates substitutes must take place at.
* @returns the found substitutes
*/
private async getSubstitutesForDates(
tutorial: Tutorial,
dates: DateTime[]
): Promise<Substitute[]> {
return this.entityManager.find(Substitute, {
tutorialToSubstitute: tutorial,
date: { $in: dates },
});
}
/**
* Returns all students in the tutorial with the given ID.
*
* @param id ID of the tutorial to get the students of.
*
* @returns All students in the tutorial with the given ID.
*
* @throws `NotFoundException` - If no tutorial with the given ID could be found.
*/
async getAllStudentsOfTutorial(id: string): Promise<Student[]> {
const tutorial = await this.findById(id);
return tutorial.getStudents();
}
/**
* Creates multiple tutorials with the given information.
*
* The created tutorials will **not** have any correctors or a tutor assigned but will have all dates specified in the DTO.
*
* @param dto DTO with the information of the tutorials to generate.
*
* @returns Array containing the response DTOs of the created tutorials.
*/
async createMany(dto: TutorialGenerationDTO): Promise<ITutorial[]> {
const { excludedDates, generationDatas } = dto;
const createdTutorials: Tutorial[] = [];
const interval = Interval.fromDateTimes(dto.getFirstDay(), dto.getLastDay());
const daysInInterval = this.datesInIntervalGroupedByWeekday(interval);
const indexForWeekday: { [key: string]: number } = {};
for (const data of generationDatas) {
const { amount, prefix, weekday } = data;
const days = daysInInterval.get(weekday) ?? [];
const dates = this.removeExcludedDates(days, excludedDates);
const timeInterval = data.getInterval();
if (dates.length > 0) {
for (let i = 0; i < amount; i++) {
const nr = (indexForWeekday[weekday] ?? 0) + 1;
const created = await this.createTutorial({
slot: `${prefix}${nr.toString().padStart(2, '0')}`,
dates,
startTime: timeInterval.start as DateTime,
endTime: timeInterval.end as DateTime,
tutors: [],
correctors: [],
});
indexForWeekday[weekday] = nr;
createdTutorials.push(created);
}
}
}
return createdTutorials.map((t) => t.toDTO());
}
/**
* Creates a new tutorial and adds it to the database.
*
* This function first checks if the given `tutor` and `correctors` are all valid. Afterwards a new TutorialDocument is created and saved in the database. This document is returned in the end.
*
* @param params Parameters needed to create a tutorial.
*
* @returns Document of the created tutorial.
*
* @throws `BadRequestException` - If the given `tutor` is not a TUTOR or one of the given `correctors` is not a CORRECTOR.
*/
private async createTutorial({
slot,
tutors,
startTime,
endTime,
dates,
correctors,
}: CreateParameters): Promise<Tutorial> {
this.assertTutorsHaveTutorRole(tutors);
this.assertCorrectorsHaveCorrectorRole(correctors);
this.assertAtLeastOneDate(dates);
const tutorial = new Tutorial({ slot, dates, startTime, endTime });
tutorial.tutors.set(tutors);
tutorial.correctors.set(correctors);
await this.em.persistAndFlush(tutorial);
return tutorial;
}
/**
* Returns all dates in the interval grouped by their weekday.
*
* Groups all dates in the interval by their weekday (1 - monday, 7 - sunday) and returns a map with those weekdays as keys. The map only contains weekdays as keys which are present in the interval (ie if only a monday and a tuesday are in the interval the map will only contain the keys `1` and `2`).
*
* @param interval Interval to get dates from.
*
* @returns Map with weekdays as keys and all dates from the interval on the corresponding weekday. Note: Not all weekdays may be present in the returned map.
*/
private datesInIntervalGroupedByWeekday(interval: Interval): Map<number, DateTime[]> {
const datesInInterval: Map<number, DateTime[]> = new Map();
if (interval.start === null || interval.end === null) {
return datesInInterval;
}
let cursor = interval.start.startOf('day');
while (cursor <= interval.end) {
const dates = datesInInterval.get(cursor.weekday) ?? [];
dates.push(cursor);
datesInInterval.set(cursor.weekday, dates);
cursor = cursor.plus({ day: 1 });
}
return datesInInterval;
}
/**
* Creates a copy of the `dates` array without the excluded dates.
*
* The `dates` array itself will **not** be changed but copied in the process.
*
* @param dates Dates to remove the excludedDates from.
* @param excludedDates Information about the dates which should be excluded.
*
* @returns A copy of the `dates` array but without the excluded dates.
*/
private removeExcludedDates(
dates: DateTime[],
excludedDates: ExcludedTutorialDate[]
): DateTime[] {
const dateArray = [...dates];
for (const excluded of excludedDates) {
for (const excludedDate of excluded.getDates()) {
const idx = dateArray.findIndex((date) => date.hasSame(excludedDate, 'day'));
if (idx !== -1) {
dateArray.splice(idx, 1);
}
}
}
return dateArray;
}
private assertTutorHasTutorRole(tutor?: User) {
if (tutor && !tutor.roles.includes(Role.TUTOR)) {
throw new BadRequestException('The tutor of a tutorial needs to have the TUTOR role.');
}
}
private assertTutorsHaveTutorRole(tutors: User[]) {
for (const tutor of tutors) {
if (!tutor.roles.includes(Role.TUTOR)) {
throw new BadRequestException(
'The tutor of a tutorial needs to have the TUTOR role.'
);
}
}
}
private assertCorrectorsHaveCorrectorRole(correctors: User[]) {
for (const doc of correctors) {
if (!doc.roles.includes(Role.CORRECTOR)) {
throw new BadRequestException(
'The corrector of a tutorial needs to have the CORRECTOR role.'
);
}
}
}
private async assertTutorialSlot(slot: string) {
const tutorialWithSameSlot = await this.repository.findOne({ slot });
if (!!tutorialWithSameSlot) {
throw new BadRequestException(`A tutorial with the slot '${slot} already exists.`);
}
}
private assertAtLeastOneDate(dates: DateTime[]) {
if (dates.length === 0) {
throw new BadRequestException(
`A tutorial without dates should be generated. This is not allowed.`
);
}
}
}
interface CreateParameters {
slot: string;
tutors: User[];
startTime: DateTime;
endTime: DateTime;
dates: DateTime[];
correctors: User[];
}
interface AddSubstituteForDatesParams {
tutorId: string;
tutorial: Tutorial;
dates: DateTime[];
}
interface RemoveSubstituteForDatesParams {
tutorial: Tutorial;
dates: DateTime[];
}