Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,17 @@ tasks:
create-onboarding-pdf-with-custom-png-logo:
desc: Gets the onboarding document as PDF
cmds:
- 'echo -n ''{"schoolLogo": "{{.LOGO_BASE64}}"}'' | http -b POST localhost:8099/students/{{ .STUDENT_ID }}/onboarding Accept:application/pdf ''X-API-KEY: This_is_a_test_APIKEY_with_30_chars+'' > {{ .STUDENT_ID }}_onboarding.pdf'
- 'echo -n ''{"logo": {"bytes": "{{.LOGO_BASE64}}"}}'' | http -b POST localhost:8099/students/{{ .STUDENT_ID }}/onboarding Accept:application/pdf ''X-API-KEY: This_is_a_test_APIKEY_with_30_chars+'' > {{ .STUDENT_ID }}_onboarding.pdf'
requires:
vars: [STUDENT_ID]
vars:
LOGO_BASE64:
sh: 'cat assets/Logo_GM.png | {{if eq OS "darwin"}}base64{{else}}base64 -w 0{{end}}'

create-onboarding-pdf-with-custom-values:
desc: Gets the onboarding document as PDF
cmds:
- 'echo -n ''{ "logo": { "bytes": "{{.LOGO_BASE64}}", "x": 15, "y": 15, "maxWidth": 50, "maxHeight": 50 }, "fields": { "salutation": "Moin {{"{{"}}student.givenname{{"}}"}}," } }'' | http -b POST localhost:8099/students/{{ .STUDENT_ID }}/onboarding Accept:application/pdf ''X-API-KEY: This_is_a_test_APIKEY_with_30_chars+'' > {{ .STUDENT_ID }}_onboarding.pdf'
requires:
vars: [STUDENT_ID]
vars:
Expand All @@ -61,7 +71,7 @@ tasks:
create-onboarding-pdf-with-custom-jpg-logo:
desc: Gets the onboarding document as PDF
cmds:
- 'echo -n ''{"schoolLogo": "{{.LOGO_BASE64}}"}'' | http -b POST localhost:8099/students/{{ .STUDENT_ID }}/onboarding Accept:application/pdf ''X-API-KEY: This_is_a_test_APIKEY_with_30_chars+'' > {{ .STUDENT_ID }}_onboarding.pdf'
- 'echo -n ''{"logo": {"bytes": "{{.LOGO_BASE64}}"}}'' | http -b POST localhost:8099/students/{{ .STUDENT_ID }}/onboarding Accept:application/pdf ''X-API-KEY: This_is_a_test_APIKEY_with_30_chars+'' > {{ .STUDENT_ID }}_onboarding.pdf'
requires:
vars: [STUDENT_ID]
vars:
Expand Down
Binary file modified assets/template_onboarding.pdf
Binary file not shown.
124 changes: 75 additions & 49 deletions src/StudentsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,19 @@ export class StudentsController {
return entries;
}

public async getOnboardingDataForStudent(student: Student, schoolLogo?: string): Promise<Result<{ pdf: Buffer; png: Buffer; link: string }>> {
public async getOnboardingDataForStudent(
student: Student,
pdfSettings: {
logo?: {
bytes?: string;
x?: number;
y?: number;
maxWidth?: number;
maxHeight?: number;
};
fields?: Record<string, string>;
}
): Promise<Result<{ pdf: Buffer; png: Buffer; link: string }>> {
if (!student.correspondingRelationshipTemplateId || !student.givenname || !student.surname) {
throw new ApplicationError("error.schoolModule.studentAlreadyDeleted", "The student seems to be already deleted.");
}
Expand All @@ -257,25 +269,22 @@ export class StudentsController {
const link = template.value.reference.url;
const pngAsBuffer = await qrCodeLib.toBuffer(link, { type: "png" });

const onboardingPdf = await this.createOnboardingPDF(
{
organizationDisplayName: (this.displayName.content.value as DisplayNameJSON).value,
givenname: student.givenname,
surname: student.surname,
schoolLogo: schoolLogo
},
pngAsBuffer
);
const onboardingPdf = await this.createOnboardingPDF(student, pdfSettings, pngAsBuffer);

return Result.ok({ link: link, png: pngAsBuffer, pdf: onboardingPdf });
}

private async createOnboardingPDF(
data: {
organizationDisplayName: string;
givenname: string;
surname: string;
schoolLogo?: string;
student: Student,
settings: {
logo?: {
bytes?: string;
x?: number;
y?: number;
maxWidth?: number;
maxHeight?: number;
};
fields?: Record<string, string>;
},
pngAsBuffer: Buffer
) {
Expand All @@ -295,13 +304,27 @@ export class StudentsController {
const qrImage = await pdfDoc.embedPng(pngAsBuffer);
const form = pdfDoc.getForm();

form.getTextField("Vorname_Nachname").setText(`${data.givenname} ${data.surname}`);
form.getTextField("Schulname_01").setText(data.organizationDisplayName);
form.getTextField("Schulname_02").setText(data.organizationDisplayName);
form.getTextField("Ort_Datum").setText("");
form.getTextField("QR_Code_Schueler").setImage(qrImage);
const fields = {
schoolname: "{{organization.displayName}}",
salutation: `Guten Tag {{student.givenname}} {{student.surname}},`,
greeting: "{{organization.displayName}}",
// eslint-disable-next-line @typescript-eslint/naming-convention
place_date: "",
...settings.fields
};

await this.embedImage(pdfDoc, data.schoolLogo);
const formFields = form.getFields();

for (const [key, value] of Object.entries(fields)) {
if (!formFields.some((field) => field.getName() === key)) continue;

const templatedValue = await this.fillTemplateStringWithStudentAndOrganizationData(student, value);
form.getTextField(key).setText(templatedValue);
}

form.getTextField("qr_code").setImage(qrImage);

await this.embedImage(pdfDoc, settings.logo);

try {
form.flatten();
Expand All @@ -320,26 +343,34 @@ export class StudentsController {
return Buffer.from(pdfBytes);
}

private async embedImage(pdfDoc: PDFDocument, schoolLogoBase64?: string) {
const image = await this.getImage(pdfDoc, schoolLogoBase64);
private async embedImage(pdfDoc: PDFDocument, logo: { bytes?: string; x?: number; y?: number; maxWidth?: number; maxHeight?: number } = {}) {
const image = await this.getImage(pdfDoc, logo.bytes);
if (!image) return;

const page = pdfDoc.getPage(0);

// 25.5mm / 72DPI
const pointsPerMillimeter = 0.353;

const pagePaddingInMillimeter = 15;
const pagePaddingInPoints = pagePaddingInMillimeter / pointsPerMillimeter;
const yInMillis = logo.y ?? 15;
const yInPoints = yInMillis / pointsPerMillimeter;

const xInMillis = logo.x ?? 15;
const xInPoints = xInMillis / pointsPerMillimeter;

const availableHorizontalSpace = page.getWidth() - pagePaddingInPoints * 2;
const maxWidth = (availableHorizontalSpace / 5) * 2;
const maxHeight = 80;
const userDefinedMaxWidth = logo.maxWidth !== undefined ? logo.maxWidth / pointsPerMillimeter : undefined;
const userDefinedMaxHeight = logo.maxHeight !== undefined ? logo.maxHeight / pointsPerMillimeter : undefined;

const availableHorizontalSpace = page.getWidth() - yInPoints * 2;
const calulatedMaxWidth = (availableHorizontalSpace / 5) * 2;

const maxWidth = userDefinedMaxWidth ?? calulatedMaxWidth;
const maxHeight = userDefinedMaxHeight ?? 80;
const scale = image.scaleToFit(maxWidth, maxHeight);

page.drawImage(image, {
x: pagePaddingInPoints,
y: page.getHeight() - scale.height - pagePaddingInPoints,
x: xInPoints,
y: page.getHeight() - scale.height - yInPoints,
height: scale.height,
width: scale.width
});
Expand Down Expand Up @@ -494,8 +525,12 @@ export class StudentsController {
public async sendMail(student: Student, rawSubject: string, rawBody: string, additionalData: any = {}): Promise<MessageDTO> {
if (!student.correspondingRelationshipId) throw new ApplicationError("error.schoolModule.noRelationship", "The student has no relationship.");

const subject = await this.fillMailTemplateWithStudentData(student, rawSubject, additionalData);
const body = await this.fillMailTemplateWithStudentData(student, rawBody, additionalData);
if (!student.correspondingRelationshipTemplateId) {
throw new ApplicationError("error.schoolModule.studentAlreadyDeleted", "The student seems to be already deleted.");
}

const subject = await this.fillTemplateStringWithStudentAndOrganizationData(student, rawSubject, additionalData);
const body = await this.fillTemplateStringWithStudentAndOrganizationData(student, rawBody, additionalData);

const relationship = await this.services.transportServices.relationships.getRelationship({ id: student.correspondingRelationshipId.toString() });

Expand All @@ -507,26 +542,17 @@ export class StudentsController {
return result.value;
}

private async fillMailTemplateWithStudentData(student: Student, template: string, additionalData: any = {}): Promise<string> {
if (!student.correspondingRelationshipTemplateId) {
throw new ApplicationError("error.schoolModule.studentAlreadyDeleted", "The student seems to be already deleted.");
}

if (!student.correspondingRelationshipId) {
throw new ApplicationError("error.schoolModule.noRelationship", "The student has no relationship.");
}

const relationship = await this.getRelationship(student.correspondingRelationshipId);
if (relationship.status !== RelationshipStatus.Active) {
throw new ApplicationError("error.schoolModule.noActiveRelationship", "The relationship to the student is not active, so sending a mail is not possible.");
}

const contact = await this.services.dataViewExpander.expandAddress(relationship.peer);
private async fillTemplateStringWithStudentAndOrganizationData(student: Student, template: string, additionalData: any = {}): Promise<string> {
const relationship = student.correspondingRelationshipId ? await this.getRelationship(student.correspondingRelationshipId) : undefined;
const contact = relationship && relationship.status !== RelationshipStatus.Active ? await this.services.dataViewExpander.expandAddress(relationship.peer) : undefined;

const data = {
student: {
givenname: contact.relationship?.nameMap["GivenName"] ?? student.givenname,
surname: contact.relationship?.nameMap["Surname"] ?? student.surname
givenname: contact?.relationship?.nameMap["GivenName"] ?? student.givenname,
surname: contact?.relationship?.nameMap["Surname"] ?? student.surname
},
organization: {
displayName: (this.displayName.content.value as DisplayNameJSON).value
},
requestBody: additionalData
};
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/StudentsRESTController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class StudentsRESTController extends BaseController {
const student = await this.studentsController.getStudent(id);
if (!student) throw RuntimeErrors.general.recordNotFound(Student);

const result = await this.studentsController.getOnboardingDataForStudent(student, data.schoolLogo);
const result = await this.studentsController.getOnboardingDataForStudent(student, data);
return this.file(
result,
(r) => r.value.pdf,
Expand All @@ -107,7 +107,7 @@ export class StudentsRESTController extends BaseController {
const student = await this.studentsController.getStudent(id);
if (!student) throw RuntimeErrors.general.recordNotFound(Student);

const result = await this.studentsController.getOnboardingDataForStudent(student);
const result = await this.studentsController.getOnboardingDataForStudent(student, {});

switch (accept) {
case "application/pdf":
Expand Down
11 changes: 10 additions & 1 deletion src/controllers/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@ export const createStudentRequestSchema = z.object({
});

export const createStudentOnboardingPDFSchema = z.object({
schoolLogo: z.string().base64()
logo: z
.object({
bytes: z.string().base64().optional(),
x: z.number().min(0).optional(),
y: z.number().min(0).optional(),
maxWidth: z.number().min(0).optional(),
maxHeight: z.number().min(0).optional()
})
.optional(),
fields: z.record(z.string()).optional()
});

export const sendMailRequestSchema = z.object({
Expand Down