From a12e072ead90226404c006b313294f86a28c40fe Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 18 Jan 2020 15:05:41 +0100 Subject: [PATCH 01/12] Add first part of implementation to generate results of a scheinexam. --- server/config/html/scheinexam.html | 30 +++++++++++ .../src/model/documents/ScheinexamDocument.ts | 13 ++++- .../criterias/ScheinexamCriteria.ts | 1 + .../services/pdf-service/PdfService.class.ts | 54 ++++++++++++++++++- .../services/pdf-service/PdfService.routes.ts | 8 +++ .../ScheinexamService.class.ts | 2 +- .../student-service/StudentService.class.ts | 6 ++- 7 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 server/config/html/scheinexam.html diff --git a/server/config/html/scheinexam.html b/server/config/html/scheinexam.html new file mode 100644 index 000000000..808ac0712 --- /dev/null +++ b/server/config/html/scheinexam.html @@ -0,0 +1,30 @@ + + +

Scheinklausur {{ scheinExamNo }}

+ + + + + + + + + + + + {{ statuses, { passed: "Bestanden", notPassed: "Nicht bestanden" } }} + +
MatrikelnummerBestanden / Nicht bestanden
+ \ No newline at end of file diff --git a/server/src/model/documents/ScheinexamDocument.ts b/server/src/model/documents/ScheinexamDocument.ts index 97c96f6e8..b5c602812 100644 --- a/server/src/model/documents/ScheinexamDocument.ts +++ b/server/src/model/documents/ScheinexamDocument.ts @@ -1,8 +1,10 @@ +import { arrayProp, instanceMethod, InstanceType, prop, Typegoose } from '@typegoose/typegoose'; import { Document, Model } from 'mongoose'; import { ScheinExam } from 'shared/dist/model/Scheinexam'; -import { arrayProp, prop, Typegoose } from '@typegoose/typegoose'; +import { getPointsOfAllExercises, PointMap } from 'shared/src/model/Points'; import { CollectionName } from '../CollectionName'; import { ExerciseDocument, ExerciseSchema } from './ExerciseDocument'; +import { StudentDocument } from './StudentDocument'; export class ScheinexamSchema extends Typegoose implements Omit { @prop({ required: true }) @@ -16,6 +18,15 @@ export class ScheinexamSchema extends Typegoose implements Omit, student: StudentDocument): boolean { + const points = new PointMap(student.scheinExamResults); + const achieved: number = points.getSumOfPoints(this); + const { must: total } = getPointsOfAllExercises(this); + + return achieved / total > this.percentageNeeded; + } } export interface ScheinexamDocument extends ScheinexamSchema, Document {} diff --git a/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts b/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts index 4454a3587..e7bc57190 100644 --- a/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts +++ b/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts @@ -49,6 +49,7 @@ export class ScheinexamCriteria extends Scheincriteria { student: Student, infos: StatusCheckResponse['infos'] ): { examsPassed: number; pointsAchieved: number; pointsTotal: number } { + // FIXME: DOES NOT WORK!!! let pointsAchieved = 0; let pointsTotal = 0; let examsPassed = 0; diff --git a/server/src/services/pdf-service/PdfService.class.ts b/server/src/services/pdf-service/PdfService.class.ts index b5cd9676b..f4a9a07bf 100644 --- a/server/src/services/pdf-service/PdfService.class.ts +++ b/server/src/services/pdf-service/PdfService.class.ts @@ -13,7 +13,10 @@ import Logger from '../../helpers/Logger'; import { StudentDocument } from '../../model/documents/StudentDocument'; import { TutorialDocument } from '../../model/documents/TutorialDocument'; import { BadRequestError, TemplatesNotFoundError } from '../../model/Errors'; -import markdownService, { TeamCommentData } from '../markdown-service/MarkdownService.class'; +import markdownService, { + TeamCommentData, + PointInformation, +} from '../markdown-service/MarkdownService.class'; import scheincriteriaService from '../scheincriteria-service/ScheincriteriaService.class'; import sheetService from '../sheet-service/SheetService.class'; import studentService from '../student-service/StudentService.class'; @@ -21,6 +24,14 @@ import teamService from '../team-service/TeamService.class'; import tutorialService from '../tutorial-service/TutorialService.class'; import userService from '../user-service/UserService.class'; import githubMarkdownCSS from './css/githubMarkdown'; +import scheinexamService from '../scheinexam-service/ScheinexamService.class'; +import { PointMap, getPointsOfAllExercises, ExercisePointInfo } from 'shared/src/model/Points'; + +enum ExamPassedState { + PASSED = 'PASSED', + NOT_PASSED = 'NOT_PASSED', + NOT_ATTENDED = 'NOT_ATTENDED', +} interface StudentData { matriculationNo: string; @@ -89,6 +100,42 @@ class PdfService { return this.generatePDFFromMarkdown(markdown); } + public async generateScheinexamResultPDF(examId: string): Promise { + const exam = await scheinexamService.getDocumentWithId(examId); + const students: StudentDocument[] = await studentService.getAllStudentsAsDocuments(); + const results: { + shortenedMatrNo: string; + passedState: ExamPassedState; + }[] = []; + + students.forEach(student => { + // FIXME: Use me as the code for the general schein exam result calculation + const scheinExamResults = new PointMap(student.scheinExamResults); + const hasAttended = scheinExamResults.hasPointEntry(exam.id); + const shortenedMatrNo = this.getShortenedMatrNo(student, students); + let result: ExamPassedState = ExamPassedState.NOT_PASSED; + + if (hasAttended) { + result = exam.hasPassed(student) ? ExamPassedState.PASSED : ExamPassedState.NOT_PASSED; + } else { + result = ExamPassedState.NOT_ATTENDED; + } + + results.push({ + shortenedMatrNo, + passedState: result, + }); + }); + + const rows: string[] = []; + results + .sort((a, b) => a.shortenedMatrNo.localeCompare(b.shortenedMatrNo)) + .forEach(({ shortenedMatrNo, passedState }) => { + rows.push(`${shortenedMatrNo}{{${passedState}}}`); + }); + + } + public async generateZIPFromComments( tutorialId: string, sheetId: string @@ -322,7 +369,10 @@ class PdfService { return studentDataToPrint; } - private getShortenedMatrNo(student: Student, students: Student[]): string { + private getShortenedMatrNo( + student: Student | StudentDocument, + students: (Student | StudentDocument)[] + ): string { if (!student.matriculationNo) { throw new Error(`Student ${student.id} does not have a matriculation number.`); } diff --git a/server/src/services/pdf-service/PdfService.routes.ts b/server/src/services/pdf-service/PdfService.routes.ts index 8ba57ab2e..bddec27aa 100644 --- a/server/src/services/pdf-service/PdfService.routes.ts +++ b/server/src/services/pdf-service/PdfService.routes.ts @@ -41,6 +41,14 @@ pdfRouter.get('/scheinstatus', ...checkRoleAccess(Role.ADMIN), async (_, res) => res.send(pdfBuffer); }); +pdfRouter.get('/scheinexam/:id/result', ...checkRoleAccess(Role.ADMIN), async (req, res) => { + const id = req.params.id; + const pdfBuffer = await pdfService.generateScheinexamResultPDF(id); + + res.contentType('pdf'); + res.send(pdfBuffer); +}) + pdfRouter.get('/credentials', ...checkRoleAccess(Role.ADMIN), async (_, res) => { const pdfBuffer = await pdfService.generateCredentialsPDF(); diff --git a/server/src/services/scheinexam-service/ScheinexamService.class.ts b/server/src/services/scheinexam-service/ScheinexamService.class.ts index 7f362c71c..e208c80a2 100644 --- a/server/src/services/scheinexam-service/ScheinexamService.class.ts +++ b/server/src/services/scheinexam-service/ScheinexamService.class.ts @@ -83,7 +83,7 @@ class ScheinExamService { } public getScheinExamResult(student: Student, exam: ScheinExam): number { - const pointsOfStudent = new PointMap(student.points); + const pointsOfStudent = new PointMap(student.scheinExamResults); let result = 0; exam.exercises.forEach(exercise => { diff --git a/server/src/services/student-service/StudentService.class.ts b/server/src/services/student-service/StudentService.class.ts index ae340ecd8..eea9d0a37 100644 --- a/server/src/services/student-service/StudentService.class.ts +++ b/server/src/services/student-service/StudentService.class.ts @@ -28,7 +28,7 @@ import tutorialService from '../tutorial-service/TutorialService.class'; class StudentService { public async getAllStudents(): Promise { - const studentDocs: StudentDocument[] = await StudentModel.find(); + const studentDocs: StudentDocument[] = await this.getAllStudentsAsDocuments(); const students: Student[] = []; for (const doc of studentDocs) { @@ -38,6 +38,10 @@ class StudentService { return students; } + public async getAllStudentsAsDocuments(): Promise { + return StudentModel.find(); + } + public async createStudent({ tutorial: tutorialId, ...dto }: StudentDTO): Promise { const tutorial = await tutorialService.getDocumentWithID(tutorialId); From 6fbf089d00952d94efd4002e41263742d2a468ed Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 18 Jan 2020 15:05:58 +0100 Subject: [PATCH 02/12] Remove error message from ErrorResponse if it's an internal one. --- server/src/model/Errors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/model/Errors.ts b/server/src/model/Errors.ts index 3057b5a1d..068457e14 100644 --- a/server/src/model/Errors.ts +++ b/server/src/model/Errors.ts @@ -107,5 +107,5 @@ export function handleError(err: any, req: Request, res: Response, next: NextFun Logger.error(err.stack); } - return res.status(500).send(new ErrorResponse(500, err.message || 'Internal server error.')); + return res.status(500).send(new ErrorResponse(500, 'Internal server error.')); } From 14b347daf37acc11507dbc492b4a0fe3759edf4e Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 18 Jan 2020 15:49:34 +0100 Subject: [PATCH 03/12] Add missing logic to generate PDF afor the result of a schein exam. --- server/config/html/scheinexam.html | 8 +-- .../services/pdf-service/PdfService.class.ts | 54 ++++++++++++++++--- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/server/config/html/scheinexam.html b/server/config/html/scheinexam.html index 808ac0712..40090ab89 100644 --- a/server/config/html/scheinexam.html +++ b/server/config/html/scheinexam.html @@ -4,10 +4,12 @@ The head & body tags will be inserted by the service and must NOT be in this file. The following replacement options are available (one can inlucde spaces after '{{' and before '}}'): - - {{ statuses, { passed: "Passed", notPassed: "Not passed" } }}: Generated table rows (2 columns: 'matriculation no', 'passed' / not passed') for all students. The inner object is optional but if provided one can customize the text for 'passed'/'not passed' ('passed' for 'passed', 'notPassed' for 'Not passed'). If it's not provided the literal strings 'passed'/'Not passed' will be used. + - {{ statuses, { "passed": "Passed", "notPassed": "Not passed", "notAttended": "Not attended" } }}: + Generated table rows (2 columns: 'matriculation no', 'passed' / not passed') for all students. The inner object is optional and must be valid JSON. If provided one can customize the text for 'passed'/'not passed'/'not attended' ('passed' for 'passed', 'notPassed' for 'Not passed', 'notAttended' for 'Not attended'). If it's not provided the literal strings 'passed'/'Not passed'/'Not attended' will be used. + --> -

Scheinklausur {{ scheinExamNo }}

+

Scheinklausur Nr. {{ scheinExamNo }}

${body}`; + } + + /** + * Checks if the template file associated with this class exists. If it does NOT exist an error gets thrown else nothing happens. + * + * @throws `BadRequestError`: If the HTML template file could not be loaded. + */ + private checkIfTemplateFileExists() { + this.getTemplate(); + } + + /** + * Prepares the given html template string to be used in further operations. + * + * This operations removes all HTML comments as well as spaced after `{{` and before `}}`. + * + * @param template Template string to adjust + * + * @returns Cleaned up HTML template file. + */ + private prepareTemplate(template: string): string { + return template + .replace(/{{\s+/g, '{{') + .replace(/\s+}}/g, '}}') + .replace(/(?=/gim, ''); + } + + /** + * @returns The GitHub markdown CSS. + */ + private getGithubMarkdownCSS(): string { + return githubMarkdownCSS; + } + + /** + * @returns Some small customizations to the GitHub markdown CSS. + */ + private getCustomCSS(): string { + return '.markdown-body table { display: table; width: 100%; }'; + } +} diff --git a/server/src/services/pdf-service/modules/PDFWithStudentsModule.ts b/server/src/services/pdf-service/modules/PDFWithStudentsModule.ts new file mode 100644 index 000000000..973083ab0 --- /dev/null +++ b/server/src/services/pdf-service/modules/PDFWithStudentsModule.ts @@ -0,0 +1,76 @@ +import { PDFModule } from './PDFModule'; +import { StudentDocument } from '../../../model/documents/StudentDocument'; + +interface ShortenedMatriculationNumbers { + [studentId: string]: string; +} + +export abstract class PDFWithStudentsModule extends PDFModule { + /** + * Returns the shortened number for all students as a "Map" in the form `studentId` -> `short matriculation no.`. + * + * Those shortened numbers are still enough to identify a student. However, this is only true if one only consideres the given students. If one extends that array without re-running this function the identifying feature may get lost. + * + * @param students All students to get the shortened number from. + * + * @returns The shortened but still identifying matriculation numbers of all given students. + */ + protected getShortenedMatriculationNumbers( + students: StudentDocument[] + ): ShortenedMatriculationNumbers { + const result: ShortenedMatriculationNumbers = {}; + + for (const student of students) { + result[student.id] = this.shortOneMatriculationNumber(student, students); + } + + return result; + } + + /** + * Generates a matriculation number with the form "***123" where the first digits get replaced with "*". All students in the `students` array get a unique matriculation number. Therefore these "shortened" numbers are still enough to identify a student. + * + * @param student Student to generate the short matriculation number for. + * @param students All students to consider. + * + * @returns The shortened but still identifying number of the given student. + */ + private shortOneMatriculationNumber( + student: StudentDocument, + students: StudentDocument[] + ): string { + if (!student.matriculationNo) { + throw new Error(`Student ${student.id} does not have a matriculation number.`); + } + + const otherStudents = students.filter(s => s.id !== student.id); + const lengthOfNo = student.matriculationNo.length; + + for (let iteration = 1; iteration < lengthOfNo; iteration++) { + const shortStudent = student.matriculationNo.substr(lengthOfNo - iteration, iteration); + let isOkay = true; + + for (const otherStudent of otherStudents) { + if (!otherStudent.matriculationNo) { + continue; + } + + const shortOtherStudent = otherStudent.matriculationNo.substr( + lengthOfNo - iteration, + iteration + ); + + if (shortStudent === shortOtherStudent) { + isOkay = false; + break; + } + } + + if (isOkay) { + return shortStudent.padStart(7, '*'); + } + } + + return student.matriculationNo; + } +} diff --git a/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts b/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts new file mode 100644 index 000000000..b02c88177 --- /dev/null +++ b/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts @@ -0,0 +1,123 @@ +import { ScheinexamDocument } from '../../../model/documents/ScheinexamDocument'; +import { StudentDocument } from '../../../model/documents/StudentDocument'; +import { PDFWithStudentsModule } from './PDFWithStudentsModule'; +import { PointMap } from 'shared/src/model/Points'; +import { StudentStatus } from 'shared/src/model/Student'; +import Logger from '../../../helpers/Logger'; + +enum ExamPassedState { + PASSED = 'PASSED', + NOT_PASSED = 'NOT_PASSED', + NOT_ATTENDED = 'NOT_ATTENDED', +} + +interface PDFGeneratorOptions { + exam: ScheinexamDocument; + students: StudentDocument[]; +} + +interface ExamResultsByStudents { + [studentId: string]: ExamPassedState; +} + +export class ScheinexamResultPDFModule extends PDFWithStudentsModule { + constructor() { + super('scheinexam.html'); + } + + /** + * Generates a PDF from the given students with their results of the given Scheinexam. Students which are INACTIVE are getting ignored aswell as students which don't have a matriculation number (due to not being able to put those students in the PDF in an "anonymous" way). + * + * @param options Must contain a ScheinexamDocument and an array of StudentDocument. + * + * @returns Buffer containing a PDF which itself contains the results of the given students of the given exam. + */ + public async generatePDF({ + exam, + students: givenStudents, + }: PDFGeneratorOptions): Promise { + const students = givenStudents + .filter(student => !!student.matriculationNo) + .filter(student => student.status !== StudentStatus.INACTIVE); + const shortenedMatriculationNumbers = this.getShortenedMatriculationNumbers(students); + const results = this.getResultsOfAllStudents({ exam, students }); + + const rows: string[] = []; + Object.entries(shortenedMatriculationNumbers) + .sort(([, matrA], [, matrB]) => matrA.localeCompare(matrB)) + .forEach(([id, shortenedMatrNo]) => { + const passedState: ExamPassedState = results[id] ?? ExamPassedState.NOT_ATTENDED; + rows.push(``); + }); + + const body = this.replacePlaceholdersInTemplate(rows, exam); + + return this.generatePDFFromBody(body); + } + + /** + * Replaces the placeholdes `{{statuses}}` and `{{scheinExamNo}}` in the template by their actual values. The adjusted template gets returned. + * + * @param tableRows Rows to add to the template instead of `{{statuses}}` + * @param exam Exam to get the `{{scheinExamNo}}` of. + * + * @returns String containing the template but with the actual information. + */ + private replacePlaceholdersInTemplate(tableRows: string[], exam: ScheinexamDocument): string { + const template = this.getTemplate(); + + return template + .replace(/{{scheinExamNo}}/g, exam.scheinExamNo.toString()) + .replace(/{{statuses(?:,\s*(.*))?}}/g, (_, option) => { + let replacements = { + passed: 'Passed', + notPassed: 'Not passed', + notAttended: 'Not attended', + }; + + try { + replacements = { ...replacements, ...JSON.parse(option) }; + } catch (err) { + Logger.warn( + `Could not parse option argument in schein exam html template. Falling back to defaults instead.` + ); + Logger.warn(`\tProvided option: ${option}`); + } + + return tableRows + .join('') + .replace(new RegExp(`{{${ExamPassedState.PASSED}}}`, 'g'), replacements.passed) + .replace(new RegExp(`{{${ExamPassedState.NOT_PASSED}}}`, 'g'), replacements.notPassed) + .replace( + new RegExp(`{{${ExamPassedState.NOT_ATTENDED}}}`, 'g'), + replacements.notAttended + ); + }); + } + + /** + * Returns the results of all given students for the given exam. Those results are mapped with the student's id as a key and the result as value. + * + * @param Options Options containing the exam and all students to get the results from. + * + * @returns The results of all students mapped by their ID. + */ + private getResultsOfAllStudents({ exam, students }: PDFGeneratorOptions): ExamResultsByStudents { + const results: ExamResultsByStudents = {}; + + students.forEach(student => { + const examResults = new PointMap(student.scheinExamResults); + const hasAttended = examResults.has(exam.id); + + if (hasAttended) { + results[student.id] = exam.hasPassed(student) + ? ExamPassedState.PASSED + : ExamPassedState.NOT_PASSED; + } else { + results[student.id] = ExamPassedState.NOT_ATTENDED; + } + }); + + return results; + } +} From 2da02138dc507ccda326ea7d849b4f93c04b6541 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 18 Jan 2020 18:28:20 +0100 Subject: [PATCH 05/12] Extract AttendancePDFModule. --- .../services/pdf-service/PdfService.class.ts | 86 ++------------- .../modules/AttendancePDFModule.ts | 102 ++++++++++++++++++ 2 files changed, 108 insertions(+), 80 deletions(-) create mode 100644 server/src/services/pdf-service/modules/AttendancePDFModule.ts diff --git a/server/src/services/pdf-service/PdfService.class.ts b/server/src/services/pdf-service/PdfService.class.ts index 4a1d25b4b..472765cfa 100644 --- a/server/src/services/pdf-service/PdfService.class.ts +++ b/server/src/services/pdf-service/PdfService.class.ts @@ -1,4 +1,3 @@ -import { format } from 'date-fns'; import fs from 'fs'; import JSZip from 'jszip'; import MarkdownIt from 'markdown-it'; @@ -7,11 +6,8 @@ import puppeteer from 'puppeteer'; import { ScheincriteriaSummaryByStudents } from 'shared/dist/model/ScheinCriteria'; import { Student } from 'shared/dist/model/Student'; import { User } from 'shared/dist/model/User'; -import { getNameOfEntity, sortByName } from 'shared/dist/util/helpers'; -import { getIdOfDocumentRef } from '../../helpers/documentHelpers'; import Logger from '../../helpers/Logger'; import { StudentDocument } from '../../model/documents/StudentDocument'; -import { TutorialDocument } from '../../model/documents/TutorialDocument'; import { BadRequestError, TemplatesNotFoundError } from '../../model/Errors'; import markdownService, { TeamCommentData } from '../markdown-service/MarkdownService.class'; import scheincriteriaService from '../scheincriteria-service/ScheincriteriaService.class'; @@ -22,6 +18,7 @@ import teamService from '../team-service/TeamService.class'; import tutorialService from '../tutorial-service/TutorialService.class'; import userService from '../user-service/UserService.class'; import githubMarkdownCSS from './css/githubMarkdown'; +import { AttendancePDFModule } from './modules/AttendancePDFModule'; import { ScheinexamResultPDFModule } from './modules/ScheinexamResultPDFModule'; interface StudentData { @@ -30,27 +27,18 @@ interface StudentData { } class PdfService { + private readonly attendancePDFModule: AttendancePDFModule; private readonly scheinexamResultsPDFModule: ScheinexamResultPDFModule; constructor() { + this.attendancePDFModule = new AttendancePDFModule(); this.scheinexamResultsPDFModule = new ScheinexamResultPDFModule(); } - public generateAttendancePDF(tutorialId: string, date: Date): Promise { - return new Promise(async (resolve, reject) => { - try { - const tutorial = await tutorialService.getDocumentWithID(tutorialId); - - const body: string = await this.generateAttendanceHTML(tutorial, date); - const html = this.putBodyInHtml(body); - - const buffer = await this.getPDFFromHTML(html); + public async generateAttendancePDF(tutorialId: string, date: Date): Promise { + const tutorial = await tutorialService.getDocumentWithID(tutorialId); - resolve(buffer); - } catch (err) { - reject(err); - } - }); + return this.attendancePDFModule.generatePDF({ tutorial, date }); } public generateStudentScheinOverviewPDF(): Promise { @@ -144,7 +132,6 @@ class PdfService { public checkIfAllTemplatesArePresent() { const notFound: string[] = []; const templatesToCheck: { getTemplate: () => string; name: string }[] = [ - { name: 'Attendance', getTemplate: this.getAttendanceTemplate.bind(this) }, { name: 'Schein status', getTemplate: this.getScheinStatusTemplate.bind(this) }, { name: 'Credentials', getTemplate: this.getCredentialsTemplate.bind(this) }, ]; @@ -175,10 +162,6 @@ class PdfService { return this.putBodyInHtml(body); } - private getAttendanceTemplate(): string { - return this.getTemplate('attendance.html'); - } - private getScheinStatusTemplate(): string { return this.getTemplate('scheinstatus.html'); } @@ -199,37 +182,6 @@ class PdfService { } } - private async generateAttendanceHTML(tutorial: TutorialDocument, date: Date): Promise { - if (!tutorial.tutor) { - throw new BadRequestError( - 'Tutorial which attendance list should be generated does NOT have a tutor assigned.' - ); - } - - const template = this.getAttendanceTemplate(); - - const tutor = await userService.getUserWithId(getIdOfDocumentRef(tutorial.tutor)); - const students: StudentDocument[] = await tutorial.getStudents(); - - students.sort(sortByName); - // const substitutePart = isSubstituteTutor(tutorial, userData) - // ? `, Ersatztutor: ${getNameOfEntity(userData)}` - // : ''; - - const tutorName = `${tutor.lastname}, ${tutor.firstname}`; - - const rows: string = students - .map( - student => - `` - ) - .join(''); - - return this.fillAttendanceTemplate(template, tutorial.slot, tutorName, rows, date); - } - private async generateScheinStatusHTML( students: Student[], summaries: ScheincriteriaSummaryByStudents @@ -260,32 +212,6 @@ class PdfService { return this.fillCredentialsTemplate(template, rows.join('')); } - private fillAttendanceTemplate( - template: string, - slot: string, - tutorName: string, - students: string, - date: Date - ): string { - return this.prepareTemplate(template) - .replace(/{{tutorialSlot}}/g, slot) - .replace(/{{tutorName}}/g, tutorName) - .replace(/{{students}}/g, students) - .replace(/{{date.*}}/g, substring => { - const dateFormat = substring.split(',').map(s => s.replace(/{{|}}/, ''))[1]; - - try { - if (dateFormat) { - return format(date, dateFormat); - } else { - return date.toDateString(); - } - } catch { - return date.toDateString(); - } - }); - } - private fillScheinStatusTemplate(template: string, statuses: string): string { return this.prepareTemplate(template).replace(/{{statuses.*}}/g, substring => { const wordArray = substring.match(/(\[(\w|\s)*,(\w|\s)*\])/g); diff --git a/server/src/services/pdf-service/modules/AttendancePDFModule.ts b/server/src/services/pdf-service/modules/AttendancePDFModule.ts new file mode 100644 index 000000000..1c3986114 --- /dev/null +++ b/server/src/services/pdf-service/modules/AttendancePDFModule.ts @@ -0,0 +1,102 @@ +import { format } from 'date-fns'; +import { getNameOfEntity, sortByName } from 'shared/src/util/helpers'; +import { getIdOfDocumentRef } from '../../../helpers/documentHelpers'; +import { TutorialDocument } from '../../../model/documents/TutorialDocument'; +import { BadRequestError } from '../../../model/Errors'; +import userService from '../../user-service/UserService.class'; +import { PDFModule } from './PDFModule'; + +interface GeneratorOptions { + tutorial: TutorialDocument; + date: Date; +} + +interface PlaceholderOptions { + tutorialSlot: string; + tutorName: string; + tableRows: string[]; + date: Date; +} + +export class AttendancePDFModule extends PDFModule { + constructor() { + super('attendance.html'); + } + + /** + * Generates a PDF which contains a list with all students of the given tutorial. This list contains one empty column for signings. + * + * @param options Must contain the tutorial and the date. + * + * @returns Buffer containing a PDF with a list for attendances. + */ + public async generatePDF({ tutorial, date }: GeneratorOptions): Promise { + if (!tutorial.tutor) { + throw new BadRequestError( + 'Tutorial which attendance list should be generated does NOT have a tutor assigned.' + ); + } + + const [tutor, students] = await Promise.all([ + userService.getUserWithId(getIdOfDocumentRef(tutorial.tutor)), + tutorial.getStudents(), + ]); + + students.sort(sortByName); + + const tutorName = getNameOfEntity(tutor, { lastNameFirst: true }); + const tableRows: string[] = students.map( + student => + `` + ); + + const body = this.replacePlaceholdersInTemplate({ + tutorialSlot: tutorial.slot, + tutorName, + tableRows, + date, + }); + + return this.generatePDFFromBody(body); + } + + /** + * Replaces the following placeholders in the template with the corresponding information. The adjusted template gets returned. + * - `{{tutorialSlot}}`: Slot of the tutorial. + * - `{{tutorName}}`: Name of the tutor of the tutorial. + * - `{{statuses}}`: The table rows will be put in here. + * - `{{date, format}}`: The given date but with the specified format inside the template. + * + * @param options Containing the information which will get replaced in the template. + * + * @returns String containing the template but with the actual information. + */ + private replacePlaceholdersInTemplate({ + tutorialSlot, + tutorName, + tableRows, + date, + }: PlaceholderOptions): string { + const template = this.getTemplate(); + + return template + .replace(/{{tutorialSlot}}/g, tutorialSlot) + .replace(/{{tutorName}}/g, tutorName) + .replace(/{{students}}/g, tableRows.join('')) + .replace(/{{date.*}}/g, substring => { + const dateFormat = substring.split(',').map(s => s.replace(/{{|}}/, ''))[1]; + + try { + if (dateFormat) { + return format(date, dateFormat); + } else { + return date.toDateString(); + } + } catch { + return date.toDateString(); + } + }); + } +} From 08c2b991251aa9fb8cdddbc9142d7be502d85123 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 18 Jan 2020 18:41:10 +0100 Subject: [PATCH 06/12] Fix wrong imports to files in shared package. --- server/src/model/documents/ScheinexamDocument.ts | 2 +- server/src/services/pdf-service/PdfService.routes.ts | 2 +- .../services/pdf-service/modules/AttendancePDFModule.ts | 2 +- .../pdf-service/modules/ScheinexamResultPDFModule.ts | 4 ++-- server/src/services/team-service/TeamService.class.ts | 9 +-------- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/server/src/model/documents/ScheinexamDocument.ts b/server/src/model/documents/ScheinexamDocument.ts index b5c602812..77d0b2814 100644 --- a/server/src/model/documents/ScheinexamDocument.ts +++ b/server/src/model/documents/ScheinexamDocument.ts @@ -1,7 +1,7 @@ import { arrayProp, instanceMethod, InstanceType, prop, Typegoose } from '@typegoose/typegoose'; import { Document, Model } from 'mongoose'; import { ScheinExam } from 'shared/dist/model/Scheinexam'; -import { getPointsOfAllExercises, PointMap } from 'shared/src/model/Points'; +import { getPointsOfAllExercises, PointMap } from 'shared/dist/model/Points'; import { CollectionName } from '../CollectionName'; import { ExerciseDocument, ExerciseSchema } from './ExerciseDocument'; import { StudentDocument } from './StudentDocument'; diff --git a/server/src/services/pdf-service/PdfService.routes.ts b/server/src/services/pdf-service/PdfService.routes.ts index bddec27aa..f8ba07536 100644 --- a/server/src/services/pdf-service/PdfService.routes.ts +++ b/server/src/services/pdf-service/PdfService.routes.ts @@ -47,7 +47,7 @@ pdfRouter.get('/scheinexam/:id/result', ...checkRoleAccess(Role.ADMIN), async (r res.contentType('pdf'); res.send(pdfBuffer); -}) +}); pdfRouter.get('/credentials', ...checkRoleAccess(Role.ADMIN), async (_, res) => { const pdfBuffer = await pdfService.generateCredentialsPDF(); diff --git a/server/src/services/pdf-service/modules/AttendancePDFModule.ts b/server/src/services/pdf-service/modules/AttendancePDFModule.ts index 1c3986114..2074204a7 100644 --- a/server/src/services/pdf-service/modules/AttendancePDFModule.ts +++ b/server/src/services/pdf-service/modules/AttendancePDFModule.ts @@ -1,5 +1,5 @@ import { format } from 'date-fns'; -import { getNameOfEntity, sortByName } from 'shared/src/util/helpers'; +import { getNameOfEntity, sortByName } from 'shared/dist/util/helpers'; import { getIdOfDocumentRef } from '../../../helpers/documentHelpers'; import { TutorialDocument } from '../../../model/documents/TutorialDocument'; import { BadRequestError } from '../../../model/Errors'; diff --git a/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts b/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts index b02c88177..3c1fd5c36 100644 --- a/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts +++ b/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts @@ -1,8 +1,8 @@ import { ScheinexamDocument } from '../../../model/documents/ScheinexamDocument'; import { StudentDocument } from '../../../model/documents/StudentDocument'; import { PDFWithStudentsModule } from './PDFWithStudentsModule'; -import { PointMap } from 'shared/src/model/Points'; -import { StudentStatus } from 'shared/src/model/Student'; +import { PointMap } from 'shared/dist/model/Points'; +import { StudentStatus } from 'shared/dist/model/Student'; import Logger from '../../../helpers/Logger'; enum ExamPassedState { diff --git a/server/src/services/team-service/TeamService.class.ts b/server/src/services/team-service/TeamService.class.ts index f9ed14ca5..b5cde79f4 100644 --- a/server/src/services/team-service/TeamService.class.ts +++ b/server/src/services/team-service/TeamService.class.ts @@ -1,13 +1,6 @@ import { isDocument } from '@typegoose/typegoose'; import _ from 'lodash'; -import { - ExercisePointInfo, - getPointsOfExercise, - PointId, - PointMap, - PointMapEntry, - UpdatePointsDTO, -} from 'shared/dist/model/Points'; +import { PointMap, UpdatePointsDTO } from 'shared/dist/model/Points'; import { Team, TeamDTO } from 'shared/dist/model/Team'; import { getIdOfDocumentRef } from '../../helpers/documentHelpers'; import Logger from '../../helpers/Logger'; From d2ab42f5325b9f64db0847b7a7a3b2f42daa07c1 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 18 Jan 2020 19:41:10 +0100 Subject: [PATCH 07/12] Extract the PDF generation for the schein results PDF. --- .../services/pdf-service/PdfService.class.ts | 135 ++---------------- .../modules/PDFWithStudentsModule.ts | 6 + .../modules/ScheinResultsPDFModule.ts | 72 ++++++++++ .../modules/ScheinexamResultPDFModule.ts | 8 +- 4 files changed, 91 insertions(+), 130 deletions(-) create mode 100644 server/src/services/pdf-service/modules/ScheinResultsPDFModule.ts diff --git a/server/src/services/pdf-service/PdfService.class.ts b/server/src/services/pdf-service/PdfService.class.ts index 472765cfa..491edd103 100644 --- a/server/src/services/pdf-service/PdfService.class.ts +++ b/server/src/services/pdf-service/PdfService.class.ts @@ -3,8 +3,6 @@ import JSZip from 'jszip'; import MarkdownIt from 'markdown-it'; import path from 'path'; import puppeteer from 'puppeteer'; -import { ScheincriteriaSummaryByStudents } from 'shared/dist/model/ScheinCriteria'; -import { Student } from 'shared/dist/model/Student'; import { User } from 'shared/dist/model/User'; import Logger from '../../helpers/Logger'; import { StudentDocument } from '../../model/documents/StudentDocument'; @@ -20,6 +18,7 @@ import userService from '../user-service/UserService.class'; import githubMarkdownCSS from './css/githubMarkdown'; import { AttendancePDFModule } from './modules/AttendancePDFModule'; import { ScheinexamResultPDFModule } from './modules/ScheinexamResultPDFModule'; +import { ScheinResultsPDFModule } from './modules/ScheinResultsPDFModule'; interface StudentData { matriculationNo: string; @@ -28,10 +27,12 @@ interface StudentData { class PdfService { private readonly attendancePDFModule: AttendancePDFModule; + private readonly scheinResultsPDFModule: ScheinResultsPDFModule; private readonly scheinexamResultsPDFModule: ScheinexamResultPDFModule; constructor() { this.attendancePDFModule = new AttendancePDFModule(); + this.scheinResultsPDFModule = new ScheinResultsPDFModule(); this.scheinexamResultsPDFModule = new ScheinexamResultPDFModule(); } @@ -41,24 +42,13 @@ class PdfService { return this.attendancePDFModule.generatePDF({ tutorial, date }); } - public generateStudentScheinOverviewPDF(): Promise { - return new Promise(async (resolve, reject) => { - try { - const [students, summaries] = await Promise.all([ - studentService.getAllStudents(), - scheincriteriaService.getCriteriaResultsOfAllStudents(), - ]); - - const body = await this.generateScheinStatusHTML(students, summaries); - const html = this.putBodyInHtml(body); - - const buffer = await this.getPDFFromHTML(html); + public async generateStudentScheinOverviewPDF(): Promise { + const [students, summaries] = await Promise.all([ + studentService.getAllStudentsAsDocuments(), + scheincriteriaService.getCriteriaResultsOfAllStudents(), + ]); - resolve(buffer); - } catch (err) { - reject(err); - } - }); + return this.scheinResultsPDFModule.generatePDF({ students, summaries }); } public async generateCredentialsPDF(): Promise { @@ -132,7 +122,6 @@ class PdfService { public checkIfAllTemplatesArePresent() { const notFound: string[] = []; const templatesToCheck: { getTemplate: () => string; name: string }[] = [ - { name: 'Schein status', getTemplate: this.getScheinStatusTemplate.bind(this) }, { name: 'Credentials', getTemplate: this.getCredentialsTemplate.bind(this) }, ]; @@ -162,10 +151,6 @@ class PdfService { return this.putBodyInHtml(body); } - private getScheinStatusTemplate(): string { - return this.getTemplate('scheinstatus.html'); - } - private getCredentialsTemplate(): string { return this.getTemplate('credentials.html'); } @@ -182,22 +167,6 @@ class PdfService { } } - private async generateScheinStatusHTML( - students: Student[], - summaries: ScheincriteriaSummaryByStudents - ): Promise { - const template = this.getScheinStatusTemplate(); - const studentDataToPrint: StudentData[] = this.getStudentDataToPrint(students, summaries); - - const rows: string[] = []; - - studentDataToPrint.forEach(data => { - rows.push(``); - }); - - return this.fillScheinStatusTemplate(template, rows.join('')); - } - private async generateCredentialsHTML(users: User[]): Promise { const template = this.getCredentialsTemplate(); const rows: string[] = []; @@ -212,96 +181,10 @@ class PdfService { return this.fillCredentialsTemplate(template, rows.join('')); } - private fillScheinStatusTemplate(template: string, statuses: string): string { - return this.prepareTemplate(template).replace(/{{statuses.*}}/g, substring => { - const wordArray = substring.match(/(\[(\w|\s)*,(\w|\s)*\])/g); - const replacements = { yes: 'yes', no: 'no' }; - - if (wordArray && wordArray[0]) { - const [yes, no] = wordArray[0] - .replace(/\[|\]|/g, '') - .replace(/,\s*/g, ',') - .split(','); - - replacements.yes = yes || replacements.yes; - replacements.no = no || replacements.no; - } - - return this.prepareTemplate(statuses) - .replace(/{{yes}}/g, replacements.yes) - .replace(/{{no}}/g, replacements.no); - }); - } - private fillCredentialsTemplate(template: string, credentials: string): string { return this.prepareTemplate(template).replace(/{{credentials}}/g, credentials); } - private getStudentDataToPrint( - students: Student[], - summaries: ScheincriteriaSummaryByStudents - ): StudentData[] { - const studentDataToPrint: { matriculationNo: string; schein: string }[] = []; - - students.forEach(student => { - try { - const matriculationNo = this.getShortenedMatrNo(student, students); - - studentDataToPrint.push({ - matriculationNo, - schein: summaries[student.id].passed ? '{{yes}}' : '{{no}}', - }); - } catch { - Logger.warn( - `Student ${student.id} does NOT have a matriculation number. Therefore it can not be added to the list` - ); - } - }); - - studentDataToPrint.sort((a, b) => a.matriculationNo.localeCompare(b.matriculationNo)); - - return studentDataToPrint; - } - - private getShortenedMatrNo( - student: Student | StudentDocument, - students: (Student | StudentDocument)[] - ): string { - if (!student.matriculationNo) { - throw new Error(`Student ${student.id} does not have a matriculation number.`); - } - - const otherStudents = students.filter(s => s.id !== student.id); - const lengthOfNo = student.matriculationNo.length; - - for (let iteration = 1; iteration < lengthOfNo; iteration++) { - const shortStudent = student.matriculationNo.substr(lengthOfNo - iteration, iteration); - let isOkay = true; - - for (const otherStudent of otherStudents) { - if (!otherStudent.matriculationNo) { - continue; - } - - const shortOtherStudent = otherStudent.matriculationNo.substr( - lengthOfNo - iteration, - iteration - ); - - if (shortStudent === shortOtherStudent) { - isOkay = false; - break; - } - } - - if (isOkay) { - return shortStudent.padStart(7, '*'); - } - } - - return student.matriculationNo; - } - private prepareTemplate(template: string): string { return template .replace(/{{\s+/g, '{{') diff --git a/server/src/services/pdf-service/modules/PDFWithStudentsModule.ts b/server/src/services/pdf-service/modules/PDFWithStudentsModule.ts index 973083ab0..05d51299b 100644 --- a/server/src/services/pdf-service/modules/PDFWithStudentsModule.ts +++ b/server/src/services/pdf-service/modules/PDFWithStudentsModule.ts @@ -27,6 +27,12 @@ export abstract class PDFWithStudentsModule extends PDFModule { return result; } + protected sortShortenedMatriculationNumbers( + numbers: ShortenedMatriculationNumbers + ): [string, string][] { + return Object.entries(numbers).sort(([, matrA], [, matrB]) => matrA.localeCompare(matrB)); + } + /** * Generates a matriculation number with the form "***123" where the first digits get replaced with "*". All students in the `students` array get a unique matriculation number. Therefore these "shortened" numbers are still enough to identify a student. * diff --git a/server/src/services/pdf-service/modules/ScheinResultsPDFModule.ts b/server/src/services/pdf-service/modules/ScheinResultsPDFModule.ts new file mode 100644 index 000000000..6fc491ffe --- /dev/null +++ b/server/src/services/pdf-service/modules/ScheinResultsPDFModule.ts @@ -0,0 +1,72 @@ +import { PDFWithStudentsModule } from './PDFWithStudentsModule'; +import { StudentDocument } from '../../../model/documents/StudentDocument'; +import { ScheincriteriaSummaryByStudents } from 'shared/src/model/ScheinCriteria'; + +interface GeneratorOptions { + students: StudentDocument[]; + summaries: ScheincriteriaSummaryByStudents; +} + +export class ScheinResultsPDFModule extends PDFWithStudentsModule { + constructor() { + super('scheinstatus.html'); + } + + /** + * Generates a PDF which shows a list of all students and their schein status. + * + * @param options Must contain all students which schein status should be added to the list. Furthermore it needs to contain the scheincriteria summaries for at least all of those students. + * + * @returns Buffer of a PDF containing the list with the schein status of all the given students. + */ + public async generatePDF({ + students: givenStudents, + summaries, + }: GeneratorOptions): Promise { + const students = givenStudents.filter(student => !!student.matriculationNo); + const shortenedMatriculationNumbers = this.getShortenedMatriculationNumbers(students); + + const tableRows: string[] = []; + + this.sortShortenedMatriculationNumbers(shortenedMatriculationNumbers).forEach( + ([id, shortenedMatrNo]) => { + const passedString = summaries[id].passed ? '{{yes}}' : '{{no}}'; + tableRows.push(``); + } + ); + + const body = this.replacePlaceholdersInTemplate(tableRows); + return this.generatePDFFromBody(body); + } + + /** + * Replaces the placeholder in the HTML template by the actual table rows. The rows get put into the spot of the `{{statuses, [yes, no]}}` placeholder. + * + * @param tableRows Rows to place into `{{statuses}}`. + * + * @returns The prepared template with filled in information. + */ + private replacePlaceholdersInTemplate(tableRows: string[]): string { + const template = this.getTemplate(); + + return template.replace(/{{statuses.*}}/g, substring => { + const wordArray = substring.match(/(\[(\w|\s)*,(\w|\s)*\])/g); + const replacements = { yes: 'yes', no: 'no' }; + + if (wordArray && wordArray[0]) { + const [yes, no] = wordArray[0] + .replace(/\[|\]|/g, '') + .replace(/,\s*/g, ',') + .split(','); + + replacements.yes = yes || replacements.yes; + replacements.no = no || replacements.no; + } + + return tableRows + .join('') + .replace(/{{yes}}/g, replacements.yes) + .replace(/{{no}}/g, replacements.no); + }); + } +} diff --git a/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts b/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts index 3c1fd5c36..d43c4b171 100644 --- a/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts +++ b/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts @@ -43,12 +43,12 @@ export class ScheinexamResultPDFModule extends PDFWithStudentsModule matrA.localeCompare(matrB)) - .forEach(([id, shortenedMatrNo]) => { + this.sortShortenedMatriculationNumbers(shortenedMatriculationNumbers).forEach( + ([id, shortenedMatrNo]) => { const passedState: ExamPassedState = results[id] ?? ExamPassedState.NOT_ATTENDED; rows.push(``); - }); + } + ); const body = this.replacePlaceholdersInTemplate(rows, exam); From 34d50b5698a399cab7f947500fb6f930992c20cf Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 18 Jan 2020 19:51:44 +0100 Subject: [PATCH 08/12] Extract the generation of the credentials PDF. --- server/src/server.ts | 13 --- .../services/pdf-service/PdfService.class.ts | 82 +------------------ .../modules/CredentialsPDFModule.ts | 45 ++++++++++ 3 files changed, 49 insertions(+), 91 deletions(-) create mode 100644 server/src/services/pdf-service/modules/CredentialsPDFModule.ts diff --git a/server/src/server.ts b/server/src/server.ts index 3d67df07a..c2a178a7f 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -7,17 +7,6 @@ import Logger from './helpers/Logger'; import { StartUpError } from './model/Errors'; import { initScheincriteriaBlueprints } from './model/scheincriteria/Scheincriteria'; import userService from './services/user-service/UserService.class'; -import pdfService from './services/pdf-service/PdfService.class'; - -/** - * Perfoms several sanity checks on startup. If a check fails a corresponding error is thrown. - * - * Checks performed: - * - Can all template files required by the PDF-service be found and loaded. - */ -async function performStartupChecks() { - pdfService.checkIfAllTemplatesArePresent(); -} /** * Tries to establish a conection to the database. @@ -78,8 +67,6 @@ async function startServer() { try { Logger.info(`Starting server with version ${pkgInfo.version}...`); - await performStartupChecks(); - await connectToDB(); initScheincriteriaBlueprints(); diff --git a/server/src/services/pdf-service/PdfService.class.ts b/server/src/services/pdf-service/PdfService.class.ts index 491edd103..398717f62 100644 --- a/server/src/services/pdf-service/PdfService.class.ts +++ b/server/src/services/pdf-service/PdfService.class.ts @@ -1,12 +1,9 @@ -import fs from 'fs'; import JSZip from 'jszip'; import MarkdownIt from 'markdown-it'; -import path from 'path'; import puppeteer from 'puppeteer'; import { User } from 'shared/dist/model/User'; import Logger from '../../helpers/Logger'; import { StudentDocument } from '../../model/documents/StudentDocument'; -import { BadRequestError, TemplatesNotFoundError } from '../../model/Errors'; import markdownService, { TeamCommentData } from '../markdown-service/MarkdownService.class'; import scheincriteriaService from '../scheincriteria-service/ScheincriteriaService.class'; import scheinexamService from '../scheinexam-service/ScheinexamService.class'; @@ -17,22 +14,20 @@ import tutorialService from '../tutorial-service/TutorialService.class'; import userService from '../user-service/UserService.class'; import githubMarkdownCSS from './css/githubMarkdown'; import { AttendancePDFModule } from './modules/AttendancePDFModule'; +import { CredentialsPDFModule } from './modules/CredentialsPDFModule'; import { ScheinexamResultPDFModule } from './modules/ScheinexamResultPDFModule'; import { ScheinResultsPDFModule } from './modules/ScheinResultsPDFModule'; -interface StudentData { - matriculationNo: string; - schein: string; -} - class PdfService { private readonly attendancePDFModule: AttendancePDFModule; private readonly scheinResultsPDFModule: ScheinResultsPDFModule; + private readonly credentialsPDFModule: CredentialsPDFModule; private readonly scheinexamResultsPDFModule: ScheinexamResultPDFModule; constructor() { this.attendancePDFModule = new AttendancePDFModule(); this.scheinResultsPDFModule = new ScheinResultsPDFModule(); + this.credentialsPDFModule = new CredentialsPDFModule(); this.scheinexamResultsPDFModule = new ScheinexamResultPDFModule(); } @@ -53,12 +48,8 @@ class PdfService { public async generateCredentialsPDF(): Promise { const users: User[] = await userService.getAllUsers(); - const body = await this.generateCredentialsHTML(users); - const html = this.putBodyInHtml(body); - const buffer = await this.getPDFFromHTML(html); - - return buffer; + return this.credentialsPDFModule.generatePDF({ users }); } public async generatePDFFromSingleComment( @@ -114,30 +105,6 @@ class PdfService { return zip.generateNodeStream({ type: 'nodebuffer' }); } - /** - * Checks if all required templates can be found. - * - * If at least one template file cannnot be found a corresponding error is thrown listing all missing template files. If _all_ template files could be found the function ends without an error. - */ - public checkIfAllTemplatesArePresent() { - const notFound: string[] = []; - const templatesToCheck: { getTemplate: () => string; name: string }[] = [ - { name: 'Credentials', getTemplate: this.getCredentialsTemplate.bind(this) }, - ]; - - for (const template of templatesToCheck) { - try { - template.getTemplate(); - } catch (err) { - notFound.push(template.name); - } - } - - if (notFound.length > 0) { - throw new TemplatesNotFoundError(notFound); - } - } - private async generatePDFFromMarkdown(markdown: string): Promise { const html = this.generateHTMLFromMarkdown(markdown); @@ -151,47 +118,6 @@ class PdfService { return this.putBodyInHtml(body); } - private getCredentialsTemplate(): string { - return this.getTemplate('credentials.html'); - } - - private getTemplate(filename: string): string { - try { - const filePath = path.join(process.cwd(), 'config', 'html', filename); - - return fs.readFileSync(filePath).toString(); - } catch { - throw new BadRequestError( - `No template file present for filename '${filename}' in ./config/tms folder` - ); - } - } - - private async generateCredentialsHTML(users: User[]): Promise { - const template = this.getCredentialsTemplate(); - const rows: string[] = []; - - users.forEach(user => { - const tempPwd = user.temporaryPassword || 'NO TMP PASSWORD'; - const nameOfUser = `${user.lastname}, ${user.firstname}`; - - rows.push(``); - }); - - return this.fillCredentialsTemplate(template, rows.join('')); - } - - private fillCredentialsTemplate(template: string, credentials: string): string { - return this.prepareTemplate(template).replace(/{{credentials}}/g, credentials); - } - - private prepareTemplate(template: string): string { - return template - .replace(/{{\s+/g, '{{') - .replace(/\s+}}/g, '}}') - .replace(/(?=/gim, ''); - } - private putBodyInHtml(body: string): string { return `${body}`; } diff --git a/server/src/services/pdf-service/modules/CredentialsPDFModule.ts b/server/src/services/pdf-service/modules/CredentialsPDFModule.ts new file mode 100644 index 000000000..171679f9d --- /dev/null +++ b/server/src/services/pdf-service/modules/CredentialsPDFModule.ts @@ -0,0 +1,45 @@ +import { User } from 'shared/src/model/User'; +import { PDFModule } from './PDFModule'; +import { getNameOfEntity } from 'shared/src/util/helpers'; + +interface GeneratorOptions { + users: User[]; +} + +export class CredentialsPDFModule extends PDFModule { + constructor() { + super('credentials.html'); + } + + /** + * Generates a PDF containing a list with all the given users and their temporary passwords. + * + * @param options Must contain an array of users which credentials should be generated. + * + * @returns Buffer containing the PDF with the temporary passwords of the given users. + */ + public generatePDF({ users }: GeneratorOptions): Promise { + const tableRows: string[] = users.map(user => { + const tempPwd = user.temporaryPassword || 'NO TMP PASSWORD'; + const nameOfUser = getNameOfEntity(user, { lastNameFirst: true }); + + return ``; + }); + + const body = this.replacePlaceholdersInTemplate(tableRows); + return this.generatePDFFromBody(body); + } + + /** + * Prepares the HTML template to contain the actual credentials inplace of the `{{credentials}}`. + * + * @param tableRows Table rows which get placed in the placeholder `{{credentials}}`. + * + * @returns Prepared template with the corresponding information. + */ + private replacePlaceholdersInTemplate(tableRows: string[]): string { + const template = this.getTemplate(); + + return template.replace(/{{credentials}}/g, tableRows.join('')); + } +} From dd10c696e79c95ce8e52ac790948fc32a128f49b Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 18 Jan 2020 19:56:00 +0100 Subject: [PATCH 09/12] Fix edit schein exam dialog being to small in width. --- client/src/view/scheinexam-management/ScheinExamManagement.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/view/scheinexam-management/ScheinExamManagement.tsx b/client/src/view/scheinexam-management/ScheinExamManagement.tsx index 9c6d6cc04..26e4c0c1d 100644 --- a/client/src/view/scheinexam-management/ScheinExamManagement.tsx +++ b/client/src/view/scheinexam-management/ScheinExamManagement.tsx @@ -123,6 +123,9 @@ function ScheinExamManagement({ enqueueSnackbar }: Props): JSX.Element { content: ( ), + DialogProps: { + maxWidth: 'lg', + }, }); } From 1d93f6c90f76f4ac527cd723d6c8b51c9149795a Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 18 Jan 2020 20:30:30 +0100 Subject: [PATCH 10/12] Add option for admin to generate the result of a schein exam. The results are served as printable PDF. --- .../src/components/loading/LoadingModal.tsx | 70 +++++++++++++++++++ client/src/hooks/fetching/Files.ts | 15 ++++ .../ScheinExamManagement.tsx | 63 +++++++++++------ .../components/ScheinExamRow.tsx | 10 +++ 4 files changed, 137 insertions(+), 21 deletions(-) create mode 100644 client/src/components/loading/LoadingModal.tsx diff --git a/client/src/components/loading/LoadingModal.tsx b/client/src/components/loading/LoadingModal.tsx new file mode 100644 index 000000000..ef20e14dd --- /dev/null +++ b/client/src/components/loading/LoadingModal.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { makeStyles, createStyles } from '@material-ui/core/styles'; +import { + Modal, + CircularProgress, + Typography, + ModalProps, + CircularProgressProps, +} from '@material-ui/core'; +import clsx from 'clsx'; + +const useStyles = makeStyles(theme => + createStyles({ + spinner: { + marginRight: theme.spacing(1), + }, + modal: { + color: theme.palette.common.white, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + modalContent: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + outline: 'none', + }, + modalText: { + marginTop: theme.spacing(2), + }, + }) +); + +interface Props extends Omit { + modalText: string; + open: boolean; + CircularProgressProps?: CircularProgressProps; +} + +function LoadingModal({ + modalText, + CircularProgressProps, + className, + ...props +}: Props): JSX.Element { + const classes = useStyles(); + + return ( + +
+ + + + {modalText} + +
+
+ ); +} + +export default LoadingModal; diff --git a/client/src/hooks/fetching/Files.ts b/client/src/hooks/fetching/Files.ts index f74f58e9e..af1aa9b88 100644 --- a/client/src/hooks/fetching/Files.ts +++ b/client/src/hooks/fetching/Files.ts @@ -30,6 +30,21 @@ export async function getScheinStatusPDF(): Promise { return Promise.reject(`Wrong response code (${response.status})`); } +export async function getScheinexamResultPDF(examId: string): Promise { + const response = await axios.get(`/pdf/scheinexam/${examId}/result`, { + responseType: 'arraybuffer', + headers: { + Accept: 'application/pdf', + }, + }); + + if (response.status === 200) { + return new Blob([response.data], { type: 'application/pdf' }); + } + + return Promise.reject(`Wrong response code (${response.status})`); +} + export async function getCredentialsPDF(): Promise { const response = await axios.get('/pdf/credentials/', { responseType: 'arraybuffer', diff --git a/client/src/view/scheinexam-management/ScheinExamManagement.tsx b/client/src/view/scheinexam-management/ScheinExamManagement.tsx index 26e4c0c1d..e7165c5fa 100644 --- a/client/src/view/scheinexam-management/ScheinExamManagement.tsx +++ b/client/src/view/scheinexam-management/ScheinExamManagement.tsx @@ -7,14 +7,21 @@ import ScheinExamForm, { ScheinExamFormState, ScheinExamFormSubmitCallback, } from '../../components/forms/ScheinExamForm'; -import LoadingSpinner from '../../components/LoadingSpinner'; +import { convertFormExercisesToDTOs } from '../../components/forms/SheetForm'; +import LoadingModal from '../../components/loading/LoadingModal'; +import LoadingSpinner from '../../components/loading/LoadingSpinner'; import TableWithForm from '../../components/TableWithForm'; import { useDialog } from '../../hooks/DialogService'; -import { useAxios } from '../../hooks/FetchingService'; -import { getDisplayStringOfScheinExam } from '../../util/helperFunctions'; -import ScheinExamRow from './components/ScheinExamRow'; +import { getScheinexamResultPDF } from '../../hooks/fetching/Files'; +import { + createScheinExam, + deleteScheinExam, + editScheinExam, + getAllScheinExams, +} from '../../hooks/fetching/ScheinExam'; +import { getDisplayStringOfScheinExam, saveBlob } from '../../util/helperFunctions'; import { getDuplicateExerciseName } from '../points-sheet/util/helper'; -import { convertFormExercisesToDTOs } from '../../components/forms/SheetForm'; +import ScheinExamRow from './components/ScheinExamRow'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -44,9 +51,9 @@ function ScheinExamManagement({ enqueueSnackbar }: Props): JSX.Element { const classes = useStyles(); const [isLoading, setIsLoading] = useState(false); + const [isGeneratingResults, setGeneratingResults] = useState(false); const [exams, setExams] = useState([]); const dialog = useDialog(); - const { getAllScheinExams, createScheinExam, editScheinExam, deleteScheinExam } = useAxios(); useEffect(() => { setIsLoading(true); @@ -54,7 +61,7 @@ function ScheinExamManagement({ enqueueSnackbar }: Props): JSX.Element { setExams(exams); setIsLoading(false); }); - }, [getAllScheinExams]); + }, []); const handleSubmit: ScheinExamFormSubmitCallback = async ( values, @@ -87,6 +94,15 @@ function ScheinExamManagement({ enqueueSnackbar }: Props): JSX.Element { } }; + const handleGenerateResultPDF: (exam: ScheinExam) => void = async exam => { + setGeneratingResults(true); + + const blob = await getScheinexamResultPDF(exam.id); + saveBlob(blob, `Scheinklausur_${exam.scheinExamNo}_Ergebnis`); + + setGeneratingResults(false); + }; + const editExam: (exam: ScheinExam) => ScheinExamFormSubmitCallback = exam => async ( values, { setSubmitting } @@ -156,20 +172,25 @@ function ScheinExamManagement({ enqueueSnackbar }: Props): JSX.Element { {isLoading ? ( ) : ( - } - items={exams} - createRowFromItem={exam => ( - - )} - placeholder='Keine Scheinklausuren vorhanden.' - /> + <> + } + items={exams} + createRowFromItem={exam => ( + + )} + placeholder='Keine Scheinklausuren vorhanden.' + /> + + + )} ); diff --git a/client/src/view/scheinexam-management/components/ScheinExamRow.tsx b/client/src/view/scheinexam-management/components/ScheinExamRow.tsx index 862c3215b..65398b88b 100644 --- a/client/src/view/scheinexam-management/components/ScheinExamRow.tsx +++ b/client/src/view/scheinexam-management/components/ScheinExamRow.tsx @@ -5,16 +5,19 @@ import EntityListItemMenu from '../../../components/list-item-menu/EntityListIte import PaperTableRow, { PaperTableRowProps } from '../../../components/PaperTableRow'; import { getDisplayStringOfScheinExam } from '../../../util/helperFunctions'; import { getPointsOfEntityAsString } from '../../points-sheet/util/helper'; +import { PdfBox as PDFGenerationIcon } from 'mdi-material-ui'; interface Props extends PaperTableRowProps { exam: ScheinExam; onEditExamClicked: (exam: ScheinExam) => void; + onHandleGenerateResultPDFClicked: (exam: ScheinExam) => void; onDeleteExamClicked: (exam: ScheinExam) => void; } function ScheinExamRow({ exam, onEditExamClicked, + onHandleGenerateResultPDFClicked, onDeleteExamClicked, ...other }: Props): JSX.Element { @@ -25,6 +28,13 @@ function ScheinExamRow({ onEditExamClicked(exam)} onDeleteClicked={() => onDeleteExamClicked(exam)} + additionalItems={[ + { + primary: 'Ergebnisse', + Icon: PDFGenerationIcon, + onClick: () => onHandleGenerateResultPDFClicked(exam), + }, + ]} /> } {...other} From 6c965542692f72088884e847359043d1d1435662 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 18 Jan 2020 20:30:49 +0100 Subject: [PATCH 11/12] Move all loading related components into /components/loading. --- client/src/components/Placeholder.tsx | 2 +- .../components/AttendanceNotePopper.tsx | 2 +- .../components/forms/ChangePasswordForm.tsx | 2 +- .../src/components/forms/FormikBaseForm.tsx | 2 +- client/src/components/forms/LoginForm.tsx | 2 +- .../{ => loading}/LoadingSpinner.tsx | 0 .../components => loading}/SubmitButton.tsx | 26 +++++++------------ client/src/view/AppBar.tsx | 2 +- .../src/view/attendance/AttendanceManager.tsx | 4 +-- client/src/view/attendance/AttendanceView.tsx | 2 +- client/src/view/dashboard/Dashboard.tsx | 2 +- .../components/ScheinexamPointsForm.tsx | 2 +- .../enter-form/components/EnterPointsForm.tsx | 2 +- .../points-sheet/overview/PointsOverview.tsx | 2 +- .../ScheinCriteriaManagement.tsx | 2 +- .../view/sheetmanagement/SheetManagement.tsx | 2 +- .../AllStudentsAdminView.tsx | 2 +- .../components/EvaluationInformation.tsx | 2 +- .../student-overview/Studentoverview.tsx | 2 +- client/src/view/teamoverview/Teamoverview.tsx | 2 +- .../tutorialmanagement/TutorialManagement.tsx | 2 +- .../TutorialSubstituteManagement.tsx | 2 +- .../view/usermanagement/UserManagement.tsx | 4 +-- 23 files changed, 32 insertions(+), 40 deletions(-) rename client/src/components/{ => loading}/LoadingSpinner.tsx (100%) rename client/src/components/{forms/components => loading}/SubmitButton.tsx (74%) diff --git a/client/src/components/Placeholder.tsx b/client/src/components/Placeholder.tsx index f97867bc4..c0ee7dfd3 100644 --- a/client/src/components/Placeholder.tsx +++ b/client/src/components/Placeholder.tsx @@ -1,7 +1,7 @@ import { Typography } from '@material-ui/core'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import React from 'react'; -import LoadingSpinner from './LoadingSpinner'; +import LoadingSpinner from './loading/LoadingSpinner'; import clsx from 'clsx'; const useStyles = makeStyles((theme: Theme) => diff --git a/client/src/components/attendance-controls/components/AttendanceNotePopper.tsx b/client/src/components/attendance-controls/components/AttendanceNotePopper.tsx index 60f146a56..7fa38d6d1 100644 --- a/client/src/components/attendance-controls/components/AttendanceNotePopper.tsx +++ b/client/src/components/attendance-controls/components/AttendanceNotePopper.tsx @@ -18,7 +18,7 @@ import { import React, { useState, useRef } from 'react'; import { FormikSubmitCallback } from '../../../types'; import FormikTextField from '../../forms/components/FormikTextField'; -import SubmitButton from '../../forms/components/SubmitButton'; +import SubmitButton from '../../loading/SubmitButton'; import clsx from 'clsx'; const useStyles = makeStyles(theme => diff --git a/client/src/components/forms/ChangePasswordForm.tsx b/client/src/components/forms/ChangePasswordForm.tsx index 75cebd5cd..f56a921c6 100644 --- a/client/src/components/forms/ChangePasswordForm.tsx +++ b/client/src/components/forms/ChangePasswordForm.tsx @@ -3,7 +3,7 @@ import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; import { Formik } from 'formik'; import { Paper, Typography, Button } from '@material-ui/core'; import FormikTextField from './components/FormikTextField'; -import SubmitButton from './components/SubmitButton'; +import SubmitButton from '../loading/SubmitButton'; import * as Yup from 'yup'; import { FormikSubmitCallback } from '../../types'; import clsx from 'clsx'; diff --git a/client/src/components/forms/FormikBaseForm.tsx b/client/src/components/forms/FormikBaseForm.tsx index b4ede5ad8..57a29906d 100644 --- a/client/src/components/forms/FormikBaseForm.tsx +++ b/client/src/components/forms/FormikBaseForm.tsx @@ -4,7 +4,7 @@ import clsx from 'clsx'; import { Formik, FormikConfig } from 'formik'; import React from 'react'; import FormikDebugDisplay from './components/FormikDebugDisplay'; -import SubmitButton from './components/SubmitButton'; +import SubmitButton from '../loading/SubmitButton'; const useStyles = makeStyles((theme: Theme) => createStyles({ diff --git a/client/src/components/forms/LoginForm.tsx b/client/src/components/forms/LoginForm.tsx index ad4e9953d..cfb782505 100644 --- a/client/src/components/forms/LoginForm.tsx +++ b/client/src/components/forms/LoginForm.tsx @@ -6,7 +6,7 @@ import React from 'react'; import * as Yup from 'yup'; import { FormikSubmitCallback } from '../../types'; import FormikTextField from './components/FormikTextField'; -import SubmitButton from './components/SubmitButton'; +import SubmitButton from '../loading/SubmitButton'; const useStyles = makeStyles((theme: Theme) => createStyles({ diff --git a/client/src/components/LoadingSpinner.tsx b/client/src/components/loading/LoadingSpinner.tsx similarity index 100% rename from client/src/components/LoadingSpinner.tsx rename to client/src/components/loading/LoadingSpinner.tsx diff --git a/client/src/components/forms/components/SubmitButton.tsx b/client/src/components/loading/SubmitButton.tsx similarity index 74% rename from client/src/components/forms/components/SubmitButton.tsx rename to client/src/components/loading/SubmitButton.tsx index 42ed33aef..006bec456 100644 --- a/client/src/components/forms/components/SubmitButton.tsx +++ b/client/src/components/loading/SubmitButton.tsx @@ -1,9 +1,10 @@ -import { CircularProgress, Modal, Tooltip, Typography } from '@material-ui/core'; +import { CircularProgress, Tooltip } from '@material-ui/core'; import Button, { ButtonProps } from '@material-ui/core/Button'; import { CircularProgressProps } from '@material-ui/core/CircularProgress'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import clsx from 'clsx'; import React from 'react'; +import LoadingModal from './LoadingModal'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -72,22 +73,13 @@ function SubmitButton({ ButtomComp )} - -
- - - - {modalText} - -
-
+ {modalText && ( + + )} ); } diff --git a/client/src/view/AppBar.tsx b/client/src/view/AppBar.tsx index 93e03e158..b5a5a6910 100644 --- a/client/src/view/AppBar.tsx +++ b/client/src/view/AppBar.tsx @@ -21,7 +21,7 @@ import React, { useState } from 'react'; import { matchPath, useLocation } from 'react-router'; import { LoggedInUserTutorial } from 'shared/dist/model/Tutorial'; import { useChangeTheme } from '../components/ContextWrapper'; -import SubmitButton from '../components/forms/components/SubmitButton'; +import SubmitButton from '../components/loading/SubmitButton'; import { getTutorialXLSX } from '../hooks/fetching/Files'; import { useLogin } from '../hooks/LoginService'; import { getDisplayStringForTutorial, saveBlob } from '../util/helperFunctions'; diff --git a/client/src/view/attendance/AttendanceManager.tsx b/client/src/view/attendance/AttendanceManager.tsx index dacb1f666..ca03c89cd 100644 --- a/client/src/view/attendance/AttendanceManager.tsx +++ b/client/src/view/attendance/AttendanceManager.tsx @@ -9,8 +9,8 @@ import { Student, StudentStatus } from 'shared/dist/model/Student'; import { LoggedInUser, TutorInfo } from 'shared/dist/model/User'; import CustomSelect from '../../components/CustomSelect'; import DateOfTutorialSelection from '../../components/DateOfTutorialSelection'; -import SubmitButton from '../../components/forms/components/SubmitButton'; -import LoadingSpinner from '../../components/LoadingSpinner'; +import SubmitButton from '../../components/loading/SubmitButton'; +import LoadingSpinner from '../../components/loading/LoadingSpinner'; import TableWithPadding from '../../components/TableWithPadding'; import { useAxios } from '../../hooks/FetchingService'; import { useLogin } from '../../hooks/LoginService'; diff --git a/client/src/view/attendance/AttendanceView.tsx b/client/src/view/attendance/AttendanceView.tsx index de1e9f0c0..e1f0594c7 100644 --- a/client/src/view/attendance/AttendanceView.tsx +++ b/client/src/view/attendance/AttendanceView.tsx @@ -5,7 +5,7 @@ import { RouteComponentProps, withRouter } from 'react-router'; import { useAxios } from '../../hooks/FetchingService'; import { TutorialWithFetchedStudents } from '../../typings/types'; import AttendanceManager from './AttendanceManager'; -import LoadingSpinner from '../../components/LoadingSpinner'; +import LoadingSpinner from '../../components/loading/LoadingSpinner'; const useStyles = makeStyles(() => createStyles({ diff --git a/client/src/view/dashboard/Dashboard.tsx b/client/src/view/dashboard/Dashboard.tsx index 51f4f2994..b0ee81c4a 100644 --- a/client/src/view/dashboard/Dashboard.tsx +++ b/client/src/view/dashboard/Dashboard.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Role } from 'shared/dist/model/Role'; import { Tutorial } from 'shared/dist/model/Tutorial'; import { LoggedInUser } from 'shared/dist/model/User'; -import LoadingSpinner from '../../components/LoadingSpinner'; +import LoadingSpinner from '../../components/loading/LoadingSpinner'; import { getTutorial } from '../../hooks/fetching/Tutorial'; import { useAxios } from '../../hooks/FetchingService'; import { useLogin } from '../../hooks/LoginService'; diff --git a/client/src/view/points-scheinexam/enter-form/components/ScheinexamPointsForm.tsx b/client/src/view/points-scheinexam/enter-form/components/ScheinexamPointsForm.tsx index 51f350134..351020330 100644 --- a/client/src/view/points-scheinexam/enter-form/components/ScheinexamPointsForm.tsx +++ b/client/src/view/points-scheinexam/enter-form/components/ScheinexamPointsForm.tsx @@ -18,7 +18,7 @@ import { getPointsOfAllExercises, convertExercisePointInfoToString, } from 'shared/dist/model/Points'; -import SubmitButton from '../../../../components/forms/components/SubmitButton'; +import SubmitButton from '../../../../components/loading/SubmitButton'; import { useDialog } from '../../../../hooks/DialogService'; import FormikDebugDisplay from '../../../../components/forms/components/FormikDebugDisplay'; import { useKeyboardShortcut } from '../../../../hooks/useKeyboardShortcut'; diff --git a/client/src/view/points-sheet/enter-form/components/EnterPointsForm.tsx b/client/src/view/points-sheet/enter-form/components/EnterPointsForm.tsx index bcbf5baf9..6e6ec0456 100644 --- a/client/src/view/points-sheet/enter-form/components/EnterPointsForm.tsx +++ b/client/src/view/points-sheet/enter-form/components/EnterPointsForm.tsx @@ -10,7 +10,7 @@ import { } from 'shared/dist/model/Points'; import { Exercise, Sheet } from 'shared/dist/model/Sheet'; import FormikDebugDisplay from '../../../../components/forms/components/FormikDebugDisplay'; -import SubmitButton from '../../../../components/forms/components/SubmitButton'; +import SubmitButton from '../../../../components/loading/SubmitButton'; import { useDialog } from '../../../../hooks/DialogService'; import { HasPoints } from '../../../../typings/types'; import { getPointsFromState as getAchievedPointsFromState } from '../EnterPoints.helpers'; diff --git a/client/src/view/points-sheet/overview/PointsOverview.tsx b/client/src/view/points-sheet/overview/PointsOverview.tsx index 1faa90e97..b5a330617 100644 --- a/client/src/view/points-sheet/overview/PointsOverview.tsx +++ b/client/src/view/points-sheet/overview/PointsOverview.tsx @@ -5,7 +5,7 @@ import { useParams, useHistory } from 'react-router'; import { Sheet } from 'shared/dist/model/Sheet'; import { Team } from 'shared/dist/model/Team'; import CustomSelect from '../../../components/CustomSelect'; -import SubmitButton from '../../../components/forms/components/SubmitButton'; +import SubmitButton from '../../../components/loading/SubmitButton'; import { getAllSheets } from '../../../hooks/fetching/Sheet'; import { getTeamsOfTutorial } from '../../../hooks/fetching/Team'; import { useErrorSnackbar } from '../../../hooks/useErrorSnackbar'; diff --git a/client/src/view/scheincriteriamanagement/ScheinCriteriaManagement.tsx b/client/src/view/scheincriteriamanagement/ScheinCriteriaManagement.tsx index 1ce85e96c..d8fd38b8b 100644 --- a/client/src/view/scheincriteriamanagement/ScheinCriteriaManagement.tsx +++ b/client/src/view/scheincriteriamanagement/ScheinCriteriaManagement.tsx @@ -9,7 +9,7 @@ import ScheinCriteriaForm, { ScheinCriteriaFormCallback, } from '../../components/forms/ScheinCriteriaForm'; import { FormDataResponse } from '../../components/generatedForm/types/FieldData'; -import LoadingSpinner from '../../components/LoadingSpinner'; +import LoadingSpinner from '../../components/loading/LoadingSpinner'; import TableWithForm from '../../components/TableWithForm'; import { useDialog } from '../../hooks/DialogService'; import { useAxios } from '../../hooks/FetchingService'; diff --git a/client/src/view/sheetmanagement/SheetManagement.tsx b/client/src/view/sheetmanagement/SheetManagement.tsx index e429a0825..351906e9b 100644 --- a/client/src/view/sheetmanagement/SheetManagement.tsx +++ b/client/src/view/sheetmanagement/SheetManagement.tsx @@ -7,7 +7,7 @@ import SheetForm, { getInitialSheetFormState, SheetFormSubmitCallback, } from '../../components/forms/SheetForm'; -import LoadingSpinner from '../../components/LoadingSpinner'; +import LoadingSpinner from '../../components/loading/LoadingSpinner'; import TableWithForm from '../../components/TableWithForm'; import { useDialog } from '../../hooks/DialogService'; import { useAxios } from '../../hooks/FetchingService'; diff --git a/client/src/view/studentmanagement/AllStudentsAdminView.tsx b/client/src/view/studentmanagement/AllStudentsAdminView.tsx index 034a8930e..84c4ed2bf 100644 --- a/client/src/view/studentmanagement/AllStudentsAdminView.tsx +++ b/client/src/view/studentmanagement/AllStudentsAdminView.tsx @@ -10,7 +10,7 @@ import { Attendance } from 'shared/dist/model/Attendance'; import { PointMap } from 'shared/dist/model/Points'; import { ScheinCriteriaSummary } from 'shared/dist/model/ScheinCriteria'; import { Tutorial } from 'shared/dist/model/Tutorial'; -import SubmitButton from '../../components/forms/components/SubmitButton'; +import SubmitButton from '../../components/loading/SubmitButton'; import { getScheinStatusPDF } from '../../hooks/fetching/Files'; import { getAllScheinExams } from '../../hooks/fetching/ScheinExam'; import { getAllSheets } from '../../hooks/fetching/Sheet'; diff --git a/client/src/view/studentmanagement/student-info/components/EvaluationInformation.tsx b/client/src/view/studentmanagement/student-info/components/EvaluationInformation.tsx index 97f0e7db7..9f6411818 100644 --- a/client/src/view/studentmanagement/student-info/components/EvaluationInformation.tsx +++ b/client/src/view/studentmanagement/student-info/components/EvaluationInformation.tsx @@ -6,7 +6,7 @@ import { PointMap } from 'shared/dist/model/Points'; import { Sheet } from 'shared/dist/model/Sheet'; import { Student } from 'shared/dist/model/Student'; import CustomSelect, { OnChangeHandler } from '../../../../components/CustomSelect'; -import LoadingSpinner from '../../../../components/LoadingSpinner'; +import LoadingSpinner from '../../../../components/loading/LoadingSpinner'; import Markdown from '../../../../components/Markdown'; import Placeholder from '../../../../components/Placeholder'; import PointsTable from '../../../../components/points-table/PointsTable'; diff --git a/client/src/view/studentmanagement/student-overview/Studentoverview.tsx b/client/src/view/studentmanagement/student-overview/Studentoverview.tsx index c124c6541..fba59bb32 100644 --- a/client/src/view/studentmanagement/student-overview/Studentoverview.tsx +++ b/client/src/view/studentmanagement/student-overview/Studentoverview.tsx @@ -10,7 +10,7 @@ import { getNameOfEntity } from 'shared/dist/util/helpers'; import CustomSelect from '../../../components/CustomSelect'; import StudentForm from '../../../components/forms/StudentForm'; import TutorialChangeForm from '../../../components/forms/TutorialChangeForm'; -import LoadingSpinner from '../../../components/LoadingSpinner'; +import LoadingSpinner from '../../../components/loading/LoadingSpinner'; import TableWithForm from '../../../components/TableWithForm'; import TableWithPadding from '../../../components/TableWithPadding'; import { useDialog } from '../../../hooks/DialogService'; diff --git a/client/src/view/teamoverview/Teamoverview.tsx b/client/src/view/teamoverview/Teamoverview.tsx index e8500367d..3726dab19 100644 --- a/client/src/view/teamoverview/Teamoverview.tsx +++ b/client/src/view/teamoverview/Teamoverview.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { Team, TeamDTO } from 'shared/dist/model/Team'; import TeamForm, { TeamFormSubmitCallback } from '../../components/forms/TeamForm'; -import LoadingSpinner from '../../components/LoadingSpinner'; +import LoadingSpinner from '../../components/loading/LoadingSpinner'; import TableWithForm from '../../components/TableWithForm'; import TeamTableRow from '../../components/TeamTableRow'; import { useDialog } from '../../hooks/DialogService'; diff --git a/client/src/view/tutorialmanagement/TutorialManagement.tsx b/client/src/view/tutorialmanagement/TutorialManagement.tsx index 6675d5eb3..827e2f93a 100644 --- a/client/src/view/tutorialmanagement/TutorialManagement.tsx +++ b/client/src/view/tutorialmanagement/TutorialManagement.tsx @@ -12,7 +12,7 @@ import TutorialForm, { TutorialFormState, TutorialFormSubmitCallback, } from '../../components/forms/TutorialForm'; -import LoadingSpinner from '../../components/LoadingSpinner'; +import LoadingSpinner from '../../components/loading/LoadingSpinner'; import TableWithForm from '../../components/TableWithForm'; import { useDialog } from '../../hooks/DialogService'; import { useAxios } from '../../hooks/FetchingService'; diff --git a/client/src/view/tutorialmanagement/TutorialSubstituteManagement.tsx b/client/src/view/tutorialmanagement/TutorialSubstituteManagement.tsx index a54250d88..0c1a64fe8 100644 --- a/client/src/view/tutorialmanagement/TutorialSubstituteManagement.tsx +++ b/client/src/view/tutorialmanagement/TutorialSubstituteManagement.tsx @@ -15,7 +15,7 @@ import FormikMultipleDatesPicker, { getDateString, } from '../../components/forms/components/FormikMultipleDatesPicker'; import FormikSelect from '../../components/forms/components/FormikSelect'; -import SubmitButton from '../../components/forms/components/SubmitButton'; +import SubmitButton from '../../components/loading/SubmitButton'; import { useAxios } from '../../hooks/FetchingService'; import { FormikSubmitCallback } from '../../types'; import { getDisplayStringForTutorial, parseDateToMapKey } from '../../util/helperFunctions'; diff --git a/client/src/view/usermanagement/UserManagement.tsx b/client/src/view/usermanagement/UserManagement.tsx index 7098eaca8..520eba8a6 100644 --- a/client/src/view/usermanagement/UserManagement.tsx +++ b/client/src/view/usermanagement/UserManagement.tsx @@ -6,9 +6,9 @@ import { Role } from 'shared/dist/model/Role'; import { Tutorial } from 'shared/dist/model/Tutorial'; import { CreateUserDTO, UserDTO } from 'shared/dist/model/User'; import { getNameOfEntity } from 'shared/dist/util/helpers'; -import SubmitButton from '../../components/forms/components/SubmitButton'; +import SubmitButton from '../../components/loading/SubmitButton'; import UserForm, { UserFormState, UserFormSubmitCallback } from '../../components/forms/UserForm'; -import LoadingSpinner from '../../components/LoadingSpinner'; +import LoadingSpinner from '../../components/loading/LoadingSpinner'; import SnackbarWithList from '../../components/SnackbarWithList'; import TableWithForm from '../../components/TableWithForm'; import { useDialog } from '../../hooks/DialogService'; From 3f1c1b2b1bbd4b57aa9a00932177ff65c48a9127 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 18 Jan 2020 20:35:28 +0100 Subject: [PATCH 12/12] Fix wrong imports from shared package. --- .../src/services/pdf-service/modules/CredentialsPDFModule.ts | 4 ++-- .../services/pdf-service/modules/ScheinResultsPDFModule.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/services/pdf-service/modules/CredentialsPDFModule.ts b/server/src/services/pdf-service/modules/CredentialsPDFModule.ts index 171679f9d..c5b85b036 100644 --- a/server/src/services/pdf-service/modules/CredentialsPDFModule.ts +++ b/server/src/services/pdf-service/modules/CredentialsPDFModule.ts @@ -1,6 +1,6 @@ -import { User } from 'shared/src/model/User'; +import { User } from 'shared/dist/model/User'; import { PDFModule } from './PDFModule'; -import { getNameOfEntity } from 'shared/src/util/helpers'; +import { getNameOfEntity } from 'shared/dist/util/helpers'; interface GeneratorOptions { users: User[]; diff --git a/server/src/services/pdf-service/modules/ScheinResultsPDFModule.ts b/server/src/services/pdf-service/modules/ScheinResultsPDFModule.ts index 6fc491ffe..ffcd582eb 100644 --- a/server/src/services/pdf-service/modules/ScheinResultsPDFModule.ts +++ b/server/src/services/pdf-service/modules/ScheinResultsPDFModule.ts @@ -1,6 +1,6 @@ import { PDFWithStudentsModule } from './PDFWithStudentsModule'; import { StudentDocument } from '../../../model/documents/StudentDocument'; -import { ScheincriteriaSummaryByStudents } from 'shared/src/model/ScheinCriteria'; +import { ScheincriteriaSummaryByStudents } from 'shared/dist/model/ScheinCriteria'; interface GeneratorOptions { students: StudentDocument[];
${shortenedMatrNo}{{${passedState}}}
${getNameOfEntity(student, { - lastNameFirst: true, - })}
${getNameOfEntity(student, { + lastNameFirst: true, + })}
${data.matriculationNo}${data.schein}
${shortenedMatrNo}${passedString}
${shortenedMatrNo}{{${passedState}}}
${nameOfUser}${user.username}${tempPwd}
${nameOfUser}${user.username}${tempPwd}