Skip to content

Commit

Permalink
[FEATURE] Transformer la feuille d'émargement en PDF pour le non SCO (P…
Browse files Browse the repository at this point in the history
  • Loading branch information
pix-service-auto-merge authored Oct 9, 2023
2 parents 878b8b3 + 44b8786 commit 6799718
Show file tree
Hide file tree
Showing 20 changed files with 522 additions and 223 deletions.
11 changes: 5 additions & 6 deletions api/lib/application/sessions/session-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ import { fillCandidatesImportSheet } from '../../infrastructure/files/candidates
import * as supervisorKitPdf from '../../infrastructure/utils/pdf/supervisor-kit-pdf.js';
import * as certificationAttestationPdf from '../../infrastructure/utils/pdf/certification-attestation-pdf.js';
import lodash from 'lodash';

const { trim } = lodash;

import { UserLinkedToCertificationCandidate } from '../../domain/events/UserLinkedToCertificationCandidate.js';
import { logger } from '../../infrastructure/logger.js';

const { trim } = lodash;

const findPaginatedFilteredJurySessions = async function (
request,
h,
Expand Down Expand Up @@ -71,12 +70,12 @@ const getAttendanceSheet = async function (request, h, dependencies = { tokenSer
const sessionId = request.params.id;
const token = request.query.accessToken;
const userId = dependencies.tokenService.extractUserId(token);
const attendanceSheet = await usecases.getAttendanceSheet({ sessionId, userId });
const { fileExtension, contentType, attendanceSheet } = await usecases.getAttendanceSheet({ sessionId, userId });

const fileName = `feuille-emargement-session-${sessionId}.ods`;
const fileName = `feuille-emargement-session-${sessionId}.${fileExtension}`;
return h
.response(attendanceSheet)
.header('Content-Type', 'application/vnd.oasis.opendocument.spreadsheet')
.header('Content-Type', contentType)
.header('Content-Disposition', `attachment; filename=${fileName}`);
};

Expand Down
41 changes: 32 additions & 9 deletions api/lib/domain/usecases/get-attendance-sheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { UserNotAuthorizedToAccessEntityError } from '../errors.js';

import {
EXTRA_EMPTY_CANDIDATE_ROWS,
NON_SCO_ATTENDANCE_SHEET_CANDIDATE_TEMPLATE_VALUES,
SCO_ATTENDANCE_SHEET_CANDIDATE_TEMPLATE_VALUES,
ATTENDANCE_SHEET_SESSION_TEMPLATE_VALUES,
} from './../../infrastructure/files/attendance-sheet/attendance-sheet-placeholders.js';
Expand All @@ -20,13 +19,43 @@ const getAttendanceSheet = async function ({
writeOdsUtils,
readOdsUtils,
sessionXmlService,
attendanceSheetPdfUtils,
}) {
const hasMembership = await sessionRepository.doesUserHaveCertificationCenterMembershipForSession(userId, sessionId);
if (!hasMembership) {
throw new UserNotAuthorizedToAccessEntityError('User is not allowed to access session.');
}

let fileExtension;
let contentType;
let attendanceSheet;

const session = await sessionForAttendanceSheetRepository.getWithCertificationCandidates(sessionId);

if (_isScoCertificationCenterAndManagingStudentOrganization({ session })) {
attendanceSheet = await _getAttendanceSheetOds({ session, readOdsUtils, writeOdsUtils, sessionXmlService });
contentType = 'application/vnd.oasis.opendocument.spreadsheet';
fileExtension = 'ods';
} else {
attendanceSheet = await attendanceSheetPdfUtils.getAttendanceSheetPdfBuffer({ session });
contentType = 'application/pdf';
fileExtension = 'pdf';
}

return {
fileExtension,
contentType,
attendanceSheet,
};
};

export { getAttendanceSheet };

function _isScoCertificationCenterAndManagingStudentOrganization({ session }) {
return session.certificationCenterType === 'SCO' && session.isOrganizationManagingStudents;
}

async function _getAttendanceSheetOds({ session, readOdsUtils, writeOdsUtils, sessionXmlService }) {
const odsFilePath = _getAttendanceSheetTemplatePath(
session.certificationCenterType,
session.isOrganizationManagingStudents,
Expand All @@ -42,9 +71,7 @@ const getAttendanceSheet = async function ({
stringifiedXml: updatedStringifiedXml,
odsFilePath,
});
};

export { getAttendanceSheet };
}

function _updateXmlWithSession(stringifiedXml, session, sessionXmlService) {
const sessionData = _.transform(session, _transformSessionIntoAttendanceSheetSessionData);
Expand All @@ -58,11 +85,7 @@ function _updateXmlWithSession(stringifiedXml, session, sessionXmlService) {
}

function _attendanceSheetWithCertificationCandidates(stringifiedXml, session, sessionXmlService) {
let candidateTemplateValues = NON_SCO_ATTENDANCE_SHEET_CANDIDATE_TEMPLATE_VALUES;

if (session.certificationCenterType === 'SCO' && session.isOrganizationManagingStudents) {
candidateTemplateValues = SCO_ATTENDANCE_SHEET_CANDIDATE_TEMPLATE_VALUES;
}
const candidateTemplateValues = SCO_ATTENDANCE_SHEET_CANDIDATE_TEMPLATE_VALUES;

const candidatesData = _.map(session.certificationCandidates, (candidate, index) => {
const candidateData = _.transform(candidate, _transformCandidateIntoAttendanceSheetCandidateData);
Expand Down
2 changes: 2 additions & 0 deletions api/lib/domain/usecases/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ import { importNamedExportsFromDirectory } from '../../../src/shared/infrastruct
import { injectDependencies } from '../../../src/shared/infrastructure/utils/dependency-injection.js';
import { findTargetProfileOrganizations as findPaginatedFilteredTargetProfileOrganizations } from './find-paginated-filtered-target-profile-organizations.js';
import { getCampaignManagement as getCampaignDetailsManagement } from './get-campaign-details-management.js';
import * as attendanceSheetPdfUtils from '../../infrastructure/utils/pdf/attendance-sheet-pdf.js';

function requirePoleEmploiNotifier() {
if (config.poleEmploi.pushEnabled) {
Expand Down Expand Up @@ -411,6 +412,7 @@ const dependencies = {
verifyCertificateCodeService,
writeCsvUtils,
writeOdsUtils,
attendanceSheetPdfUtils,
};

const path = dirname(fileURLToPath(import.meta.url));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const getWithCertificationCandidates = async function (idSession) {
'certification-centers.type',
);
})
.leftJoin('certification-candidates', 'certification-candidates.sessionId', 'sessions.id')
.innerJoin('certification-candidates', 'certification-candidates.sessionId', 'sessions.id')
.leftJoin(
'view-active-organization-learners',
'view-active-organization-learners.id',
Expand All @@ -47,7 +47,7 @@ const getWithCertificationCandidates = async function (idSession) {
.first();

if (!results) {
throw new NotFoundError("La session n'existe pas");
throw new NotFoundError("La session n'existe pas ou aucun candidat n'est inscrit à celle-ci");
}

return _toDomain(results);
Expand Down
185 changes: 185 additions & 0 deletions api/lib/infrastructure/utils/pdf/attendance-sheet-pdf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { readFile } from 'fs/promises';
import { PDFDocument, rgb } from 'pdf-lib';

import pdfLibFontkit from '@pdf-lib/fontkit';
import * as url from 'url';
import dayjs from 'dayjs';
import _ from 'lodash';

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

const SESSION_DETAIL_FONT_SIZE = 8;
const CANDIDATES_PER_PAGE = 20;
const SESSION_DETAIL_DEFAULT_COLOR = rgb(0, 0, 0);

async function getAttendanceSheetPdfBuffer({
dirname = __dirname,
fontkit = pdfLibFontkit,
creationDate = new Date(),
session,
} = {}) {
const templatePath = `${dirname}/files/FR-non-SCO-attendance-sheet.pdf`;
const templateBuffer = await readFile(templatePath);

const pdfDoc = await PDFDocument.create();

pdfDoc.setCreationDate(creationDate);
pdfDoc.setModificationDate(creationDate);
pdfDoc.registerFontkit(fontkit);

const fontFile = await readFile(`${dirname}/files/Roboto-Regular.ttf`);
const font = await pdfDoc.embedFont(fontFile, { subset: true, customName: 'Roboto-Regular.ttf' });

const certificationCandidates = session.certificationCandidates;
const certificationCandidatesSplitByPage = _.chunk(certificationCandidates, CANDIDATES_PER_PAGE);

for (const [index, candidatesGroup] of certificationCandidatesSplitByPage.entries()) {
const page = pdfDoc.addPage();
const [templatePage] = await pdfDoc.embedPdf(templateBuffer);
page.drawPage(templatePage);
const pagesCount = certificationCandidatesSplitByPage.length;

_drawPageNumber({ pageIndex: index, pagesCount, page, font });
_drawSessionDate({ session, page, font });
_drawSessionStartTime({ session, page, font });
_drawSessionAddress({ session, page, font });
_drawSessionRoom({ session, page, font });
_drawSessionExaminer({ session, page, font });
_drawSessionId({ session, page, font });

candidatesGroup.forEach((candidate, index) => {
const gap = 30;
const initialY = 603;
const y = initialY - gap * index;
const firstName = _formatInformation(candidate.firstName);
const lastName = _formatInformation(candidate.lastName);
const externalId = _formatInformation(candidate.externalId);

const parameters = [
[30, y, firstName],
[133, y, lastName],
[238, y, _formatDate(candidate.birthdate)],
[305, y, externalId],
];

_drawCandidate({ parameters, page, font });
});
}

const pdfBytes = await pdfDoc.save();
return Buffer.from(pdfBytes);
}

function _drawPageNumber({ pageIndex, pagesCount, page, font }) {
const pageNumber = (pageIndex + 1).toString();
const pagePagination = `${pageNumber}/${pagesCount}`;
page.drawText(pagePagination, {
x: 61,
y: 807,
size: SESSION_DETAIL_FONT_SIZE,
font,
color: rgb(1, 1, 1),
});
}

function _formatInformation(information, limit = 21) {
if (information.length > limit) {
return information.slice(0, limit) + '...';
}

return information;
}

function _drawSessionDate({ session, page, font }) {
const date = new Date(session.date);
const day = date.getDate();
const year = date.getFullYear();
const options = { month: 'short' };

const month = new Intl.DateTimeFormat('fr', options).format(date);
const fullDate = `${day} ${month} ${year}`;

page.drawText(fullDate, {
x: 62,
y: 715,
size: SESSION_DETAIL_FONT_SIZE,
font,
color: SESSION_DETAIL_DEFAULT_COLOR,
});
}

function _drawSessionStartTime({ session, page, font }) {
const [hours, minutes] = session.time.split(':');
const hour = `${hours}h${minutes}`;
page.drawText(hour, {
x: 272,
y: 715,
size: SESSION_DETAIL_FONT_SIZE,
font,
color: SESSION_DETAIL_DEFAULT_COLOR,
});
}

function _drawSessionAddress({ session, page, font }) {
const address = _formatInformation(session.address, 22);
page.drawText(address, {
x: 89,
y: 691,
size: SESSION_DETAIL_FONT_SIZE,
font,
color: SESSION_DETAIL_DEFAULT_COLOR,
});
}

function _drawSessionRoom({ session, page, font }) {
const room = _formatInformation(session.room);

page.drawText(room, {
x: 256,
y: 691,
size: SESSION_DETAIL_FONT_SIZE,
font,
color: SESSION_DETAIL_DEFAULT_COLOR,
});
}

function _drawSessionExaminer({ session, page, font }) {
const examiner = _formatInformation(session.examiner, 40);

page.drawText(examiner, {
x: 410,
y: 717,
size: SESSION_DETAIL_FONT_SIZE,
font,
color: SESSION_DETAIL_DEFAULT_COLOR,
});
}

function _drawSessionId({ session, page, font }) {
const sessionId = String(session.id);
page.drawText(sessionId, {
x: 246,
y: 739,
size: 10,
font,
color: SESSION_DETAIL_DEFAULT_COLOR,
});
}

function _drawCandidate({ page, font, parameters }) {
parameters.forEach(([x, y, text]) => {
page.drawText(text, {
x,
y,
size: SESSION_DETAIL_FONT_SIZE,
font,
color: SESSION_DETAIL_DEFAULT_COLOR,
});
});
}

function _formatDate(date) {
return dayjs(date).format('DD/MM/YYYY');
}

export { getAttendanceSheetPdfBuffer };
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('Acceptance | Controller | session-controller-get-attendance-sheet', fu
});

sessionIdAllowed = databaseBuilder.factory.buildSession({ certificationCenterId }).id;
databaseBuilder.factory.buildCertificationCandidate({ sessionId: sessionIdAllowed });
sessionIdNotAllowed = databaseBuilder.factory.buildSession({
certificationCenterId: otherCertificationCenterId,
}).id;
Expand All @@ -41,13 +42,12 @@ describe('Acceptance | Controller | session-controller-get-attendance-sheet', fu
url: `/api/sessions/${sessionIdAllowed}/attendance-sheet?accessToken=${token}`,
payload: {},
};

// when
const promise = server.inject(options);
const response = await server.inject(options);

// then
return promise.then((response) => {
expect(response.statusCode).to.equal(200);
});
expect(response.statusCode).to.equal(200);
});

it('should respond with a 403 when user cant access the session', async function () {
Expand All @@ -59,13 +59,12 @@ describe('Acceptance | Controller | session-controller-get-attendance-sheet', fu
url: `/api/sessions/${sessionIdNotAllowed}/attendance-sheet?accessToken=${token}`,
payload: {},
};

// when
const promise = server.inject(options);
const response = await server.inject(options);

// then
return promise.then((response) => {
expect(response.statusCode).to.equal(403);
});
expect(response.statusCode).to.equal(403);
});
});
});
Loading

0 comments on commit 6799718

Please sign in to comment.