Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"symfony/runtime": "5.4.*",
"league/flysystem-bundle": "^3.3",
"league/flysystem-async-aws-s3": "^3.29",
"league/flysystem-aws-s3-v3": "^3.29"
"league/flysystem-aws-s3-v3": "^3.29",
"knplabs/knp-snappy-bundle": "^1.10"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4",
Expand Down
36 changes: 35 additions & 1 deletion docs/specs/validator-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ info:
contact:
name: IGNF/validator
url: "https://github.com/IGNF/validator/issues"
version: "0.6.1"
version: "0.6.2"
title: "API Validator"
license:
name: "AGPL-3.0-or-later"
Expand Down Expand Up @@ -193,6 +193,40 @@ paths:
schema:
$ref: "#/components/schemas/Error"

/api/validations/{uid}/results.pdf:
get:
tags:
- validation
operationId: get_validation_pdf
summary: "Télécharger le résultat au format pdf"
description: "Télécharger le résultat au format pdf"
parameters:
- description: "Identifiant unique de la validation"
in: path
name: uid
required: true
schema:
type: string
example: g7258vq1t639uagbv8rg7b97
responses:
200:
description: Récupération réussie
content:
text/csv: {}
400:
description: "Paramètre uid manquant"
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
404:
description: "Aucune demande de validation ne correspond à l'uid"
content:

application/json:
schema:
$ref: "#/components/schemas/Error"


/api/validations/{uid}/files/source:
get:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
"webpack-cli": "^4.7.2",
"webpack-copy-plugin": "0.0.4"
}
}
}
196 changes: 98 additions & 98 deletions public/vendor/validator-api-client/validator-client.js

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions src/Controller/Api/ValidationsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Entity\Validation;
use App\Exception\ApiException;
use App\Export\CsvReportWriter;
use App\Export\PdfReportWriter;
use App\Repository\ValidationRepository;
use App\Service\MimeTypeGuesserService;
use App\Service\ValidatorArgumentsService;
Expand Down Expand Up @@ -323,6 +324,28 @@ public function downloadSourceData($uid)
return $this->getDownloadResponse($zipFilepath, $validation->getDatasetName() . '-source.zip');
}

/**
* @Route(
* "/{uid}/results.pdf",
* name="validator_api_get_validation_pdf",
* methods={"GET"}
* )
*/
public function generatePdf($uid, PdfReportWriter $writer,
): Response {
$validation = $this->repository->findOneByUid($uid);
if (!$validation) {
throw new ApiException("No record found for uid=$uid", Response::HTTP_NOT_FOUND);
}

$pdf = $writer->generate($validation);

return new Response($pdf, Response::HTTP_OK, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="' . $validation->getDatasetName() . '.pdf"',
]);
}

/**
* Returns binary response of the specified file.
*
Expand Down
50 changes: 50 additions & 0 deletions src/Export/PdfReportWriter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace App\Export;

use App\Entity\Validation;
use Knp\Snappy\Pdf;
use Twig\Environment;

class PdfReportWriter
{
public function __construct(
private readonly Pdf $snappy,
private readonly Environment $twig,
) {}

/**
* @param Validation $validation
* @return string Raw PDF binary content
*/
public function generate(Validation $validation): string
{
$entries = json_decode($validation->getResults());

$hasErrors = (bool) array_filter(
$entries,
static fn(array $e): bool => strtolower($e['level']) === 'error'
);

$order = ['error', 'warning', 'info'];
$grouped = [];

foreach ($entries as $entry) {
$grouped[$entry['level']][] = $entry;
}

$html = $this->twig->render('pdfModel.html.twig', [
'groupedEntries' => $grouped,
'hasErrors' => $hasErrors,
]);

return $this->snappy->getOutputFromHtml($html, [
'encoding' => 'UTF-8',
'enable-local-file-access' => true,
'margin-top' => '10mm',
'margin-bottom' => '10mm',
'margin-left' => '12mm',
'margin-right' => '12mm',
]);
}
}
128 changes: 128 additions & 0 deletions templates/pdfModel.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<style>
body {
font-family: DejaVu Sans, Arial, sans-serif;
font-size: 13px;
color: #1a1a2e;
margin: 40px;
}

.banner {
padding: 16px 24px;
border-radius: 6px;
font-size: 15px;
font-weight: bold;
margin-bottom: 32px;
border-left: 6px solid;
}

.banner.has-errors {
background-color: #fde8e8;
border-color: #e53e3e;
color: #c53030;
}

.banner.no-errors {
background-color: #e6f4ea;
border-color: #38a169;
color: #276749;
}

h2.group-title {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.08em;
margin: 28px 0 8px;
padding-bottom: 4px;
border-bottom: 2px solid currentColor;
}

h2.level-error { color: #e53e3e; border-color: #e53e3e; }
h2.level-warning { color: #d97706; border-color: #d97706; }
h2.level-info { color: #3182ce; border-color: #3182ce; }

table {
width: 100%;
border-collapse: collapse;
margin-bottom: 8px;
}

thead th {
background-color: #2d3748;
color: #ffffff;
text-align: left;
padding: 8px 12px;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
}

tbody tr:nth-child(even) { background-color: #f7fafc; }
tbody tr:nth-child(odd) { background-color: #ffffff; }

tbody td {
padding: 7px 12px;
border-bottom: 1px solid #e2e8f0;
vertical-align: top;
}

.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
}

.badge-error { background: #fed7d7; color: #c53030; }
.badge-warning { background: #fefcbf; color: #b7791f; }
.badge-info { background: #bee3f8; color: #2b6cb0; }

.code { font-family: monospace; font-size: 12px; color: #553c9a; }
</style>
</head>
<body>

{# ── Top banner ── #}
{% if hasErrors %}
<div class="banner has-errors">
Document Invalide
</div>
{% else %}
<div class="banner no-errors">
Document Valide
</div>
{% endif %}

{# ── One table per level ── #}
{% for level, rows in groupedEntries %}
<h2 class="group-title level-{{ level|lower }}">{{ level|upper }}</h2>

<table>
<thead>
<tr>
<th style="width: 18%">Code</th>
<th style="width: 14%">Level</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{% for entry in rows %}
<tr>
<td class="code">{{ entry.code }}</td>
<td>
<span class="badge badge-{{ entry.level|lower }}">{{ entry.level }}</span>
</td>
<td>{{ entry.message }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}

</body>
</html>
Loading