Commit 619844ce authored by Pierre Dittgen's avatar Pierre Dittgen

PDF report using chromium headless

parent 9a0854c2
...@@ -23,14 +23,15 @@ setup( ...@@ -23,14 +23,15 @@ setup(
packages=['validata_ui'], packages=['validata_ui'],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
'flask',
'goodtables',
'requests',
'validata_core >= 0.1, < 0.2',
'commonmark', 'commonmark',
'ezodf', 'ezodf',
'flask',
'lxml', 'lxml',
'requests',
'goodtables',
'tabulator', 'tabulator',
'validata_core >= 0.1, < 0.2',
] ]
) )
#!/usr/bin/env python3 #!/usr/bin/env python3
from pathlib import Path from pathlib import Path
from urllib.parse import quote_plus
from flask import Flask import flask
import validata_core import jinja2
import validata_core
from validata_ui.validate_helper import ValidatorHelper from validata_ui.validate_helper import ValidatorHelper
# Schemas settings # Schemas settings
...@@ -11,7 +13,18 @@ schemas_config = validata_core.get_schemas_config() ...@@ -11,7 +13,18 @@ schemas_config = validata_core.get_schemas_config()
ValidatorHelper.init(schemas_config) ValidatorHelper.init(schemas_config)
# Flask things # Flask things
app = Flask(__name__) app = flask.Flask(__name__)
app.secret_key = 'MyPr3ci0u5$€cr€t' app.secret_key = 'MyPr3ci0u5$€cr€t'
# Jinja2 url_quote_plus custom filter
# https://stackoverflow.com/questions/12288454/how-to-import-custom-jinja2-filters-from-another-file-and-using-flask
blueprint = flask.Blueprint('filters', __name__)
@jinja2.contextfilter
@blueprint.app_template_filter()
def urlencode(context, value):
return quote_plus(value)
# Let this import after app initialisation
import validata_ui.views import validata_ui.views
...@@ -69,4 +69,4 @@ ...@@ -69,4 +69,4 @@
</footer> </footer>
</body> </body>
</html> </html>
\ No newline at end of file
...@@ -7,142 +7,167 @@ ...@@ -7,142 +7,167 @@
font-size: 1.2em; font-size: 1.2em;
font-weight: bold; font-weight: bold;
} }
@media print {
.hidden-print {
display: none;
}
}
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>{{ title }}</h1> {% if print_mode %}
<p class="text">Validation effectuée {{ validation_date }}</p>
<p class="text alert alert-info">
Pour relancer la validation, <a href="#" onclick="location.reload();">rechargez</a> la page.
</p>
{# Schema info #}
<div class="row"> <div class="row">
<div class="col-md-5 my-4"> <div class="col-md-5">
<div class="card bg-faded"> {% endif %}
<div class="card-body"> <h1>{{ title }}</h1>
<h5 class="card-title"> <p class="text">Validation effectuée {{ validation_date }}</p>
Schéma {{ val_info.code }} <p class="text alert alert-info hidden-print">
<span class="badge badge-primary">{{ val_info.version }}</span> Pour relancer la validation, <a href="#" onclick="location.reload();">rechargez</a> la page.
</h5> </p>
<h6 class="card-subtitle mb-2 text-muted">{{ val_info.description }}</h6> {% if print_mode %}
{% if val_info.author or val_info.contributor %} </div>
<p class="text"> <div class="col-md-7">
{% if val_info.author %} <img src="{{ url_for('static', filename='img/logo-horizontal.png') }}" height="80" alt="Validata" />
Auteur : {{ val_info.author }} </div>
{% endif %} </div>
{% if val_info.contributor %} {% endif %}
<br />Contributeur(s) : {{ val_info.contributor }} {% if source.type == 'url' %}
{% endif %} <p class="text hidden-print">Obtenir le rapport au format <a href="{{ url_for('pdf_report', val_code=val_info.code) }}?url={{source.name|urlencode}}">PDF</a>
</p> {% endif %}
{% endif %} {# Schema info #}
<p> <div class="row">
<a href="{{ val_info.docurl }}" target="_blank" class="card-link">Documentation</a> <div class="col-md-{% if print_mode %}12{% else %}5{% endif %} my-4">
</p> <div class="card bg-faded">
<hr /> <div class="card-body">
<p class="text alert alert-secondary" style="margin-bottom: 0;"> <h5 class="card-title">
Source Schéma {{ val_info.code }}
{% if source_type == 'file' %} <span class="badge badge-primary">{{ val_info.version }}</span>
(fichier) : <em>{{ source.name }}</em> </h5>
{% endif %} <h6 class="card-subtitle mb-2 text-muted">{{ val_info.description }}</h6>
{% if source_type == 'url' %} {% if val_info.author or val_info.contributor %}
(URL) : <em><a href="{{source.name}}" target="_blank" title="{{source.name}}">{{source.name | <p class="text">
truncate(60)}}</a></em> {% if val_info.author %}
Auteur : {{ val_info.author }}
{% endif %}
{% if val_info.contributor %}
<br />Contributeur(s) : {{ val_info.contributor }}
{% endif %}
</p>
{% endif %} {% endif %}
<br /> <p>
Dimensions : <em>{{ report.table.col_count }} colonnes et {{ report.table.row_count }} lignes</em> <a href="{{ val_info.docurl }}" target="_blank" class="card-link">Documentation</a>
</p> </p>
<hr />
<p class="text alert alert-secondary" style="margin-bottom: 0;">
Source
{% if source_type == 'file' %}
(fichier) : <em>{{ source.name }}</em>
{% endif %}
{% if source_type == 'url' %}
(URL) : <em><a href="{{source.name}}" target="_blank" title="{{source.name}}">
{% if print_mode %}
{{source.name}}
{% else %}
{{source.name | truncate(60)}}
{% endif %}
</a></em>
{% endif %}
<br />
Dimensions : <em>{{ report.table.col_count }} colonnes et {{ report.table.row_count }}
lignes</em>
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
{% import 'table_macros.html' as tables %} {% import 'table_macros.html' as tables %}
{% if report.error_count == 0 %} {% if report.error_count == 0 %}
<h2>La table est valide</h2> <h2>La table est valide</h2>
<p>Aucune erreur détectée</p> <p>Aucune erreur détectée</p>
{{ tables.preview(source_data) }} {{ tables.preview(source_data) }}
{% else %} {% else %}
<h2>La table est invalide</h2> <h2>La table est invalide</h2>
{# table checks #} {# table checks #}
{% if report.table.errors.structure %} {% if report.table.errors.structure %}
<div> <div>
<h3>Problèmes de structure</h3> <h3>Problèmes de structure</h3>
{% for err in report.table.errors.structure %} {% for err in report.table.errors.structure %}
<div class="alert alert-danger"> <div class="alert alert-danger">
{{ err.message | safe}} {{ err.message | safe}}
</div>
{% endfor %}
</div> </div>
{% endfor %} {% endif %}
</div>
{% endif %}
<!-- row checks -->
{% if not report.table.errors.body %} <!-- row checks -->
<h3>Problèmes de contenu</h3>
<p class="text">
Aucune erreur de contenu
</p>
{{ tables.preview(source_data) }} {% if not report.table.errors.body %}
<h3>Problèmes de contenu</h3>
<p class="text">
Aucune erreur de contenu
</p>
{% endif %} {{ tables.preview(source_data) }}
{% if report.table.errors.body %}
{% if not report.table.display_body_errors %} {% endif %}
<p class="text">
Veuillez corriger ce(s) erreur(s) pour visualiser les problèmes de contenu.
</p>
<p class="text"> {% if report.table.errors.body %}
<a href="#" id="show_body_errors" title="">Affichez
tout de même les problèmes de contenu</a>
</p>
<div class="alert alert-warning">
Attention, en cas de problèmes de structure signalés ci-dessus, certains messages d'erreurs ne seront pas
pertinents
</div>
<div id="body_errors" style="display: none"> {% if not report.table.display_body_errors %}
{{ tables.body_errors(report, source_data) }} <p class="text">
</div> Veuillez corriger ce(s) erreur(s) pour visualiser les problèmes de contenu.
{% else %} </p>
<div>
{{ tables.body_errors(report, source_data) }}
</div>
{% endif %}
{% endif %}
<p class="text">
<a href="#" id="show_body_errors">Affichez
tout de même les problèmes de contenu</a>
</p>
<div class="alert alert-warning">
Attention, en cas de problèmes de structure signalés ci-dessus, certains messages d'erreurs ne seront pas
pertinents
</div>
{% endif %} <div id="body_errors" style="display: none">
{% endblock %} {{ tables.body_errors(report, source_data) }}
{% block footer %} </div>
<script> {% else %}
$(function () { <div>
// Errors tooltip activate {{ tables.body_errors(report, source_data) }}
$('[data-toggle="popover"]').popover({ </div>
html: true, {% endif %}
placement: 'auto', {% endif %}
trigger: 'hover'
});
{% endif %}
// Show body errors {% endblock %}
$('#show_body_errors').on('click', function () { {% block footer %}
$('#body_errors').show(); <script>
$(this).remove(); $(function () {
// Errors tooltip activate
$('[data-toggle="popover"]').popover({
html: true,
placement: 'auto',
trigger: 'hover'
});
// Show body errors
$('#show_body_errors').on('click', function () {
$('#body_errors').show();
$(this).remove();
})
}) })
}) </script>
</script>
{# {#
{{ report.table.errors.structure|length }} erreur(s) détectée(s) {{ report.table.errors.structure|length }} erreur(s) détectée(s)
{{ report.table.errors.body|length }} erreur(s) détectée(s) {{ report.table.errors.body|length }} erreur(s) détectée(s)
#} #}
{% endblock %} {% endblock %}
\ No newline at end of file
...@@ -38,7 +38,7 @@ def bytes_data(f): ...@@ -38,7 +38,7 @@ def bytes_data(f):
return iob.getvalue() return iob.getvalue()
def validate(schema_code, **args): def core_validate(schema_code, **args):
""" Validate source against schema using custom-checks and pre-checks""" """ Validate source against schema using custom-checks and pre-checks"""
return validata_core.validate( return validata_core.validate(
......
...@@ -4,19 +4,22 @@ ...@@ -4,19 +4,22 @@
""" """
import copy import copy
import json import json
import subprocess
import time
from datetime import datetime from datetime import datetime
from io import BytesIO from io import BytesIO
from pathlib import Path
from urllib.parse import quote_plus
from commonmark import commonmark from commonmark import commonmark
from flask import redirect, render_template, request, url_for from flask import make_response, redirect, render_template, request, url_for
import tabulator import tabulator
import validata_ui.validata_util as vu
from validata_core import csv_helpers from validata_core import csv_helpers
from validata_core.loaders import custom_loaders from validata_core.loaders import custom_loaders
from validata_ui import app from validata_ui import app
from validata_ui.ui_util import flash_error, flash_warning from validata_ui.ui_util import flash_error, flash_warning
from validata_ui.validata_util import ValidataSource from validata_ui.validata_util import ValidataSource, core_validate
from validata_ui.validate_helper import ValidatorHelper from validata_ui.validate_helper import ValidatorHelper
...@@ -167,7 +170,7 @@ def validate(schema_code, source: ValidataSource): ...@@ -167,7 +170,7 @@ def validate(schema_code, source: ValidataSource):
""" Validate source and display report """ """ Validate source and display report """
try: try:
goodtables_report = vu.validate(schema_code, **source.get_tabulator_params()) goodtables_report = core_validate(schema_code, **source.get_tabulator_params())
except tabulator.exceptions.FormatError: except tabulator.exceptions.FormatError:
flash_error('Erreur : format de fichier non supporté') flash_error('Erreur : format de fichier non supporté')
return redirect(url_for('scdl_validator', val_code=schema_code)) return redirect(url_for('scdl_validator', val_code=schema_code))
...@@ -187,6 +190,7 @@ def validate(schema_code, source: ValidataSource): ...@@ -187,6 +190,7 @@ def validate(schema_code, source: ValidataSource):
val_info=ValidatorHelper.schema_info(schema_code), report=validata_report, val_info=ValidatorHelper.schema_info(schema_code), report=validata_report,
validation_date=report_date.strftime('le %d/%m/%Y à %Hh%M'), validation_date=report_date.strftime('le %d/%m/%Y à %Hh%M'),
source=source, source_type=source.type, source_data=source_data, source=source, source_type=source.type, source_data=source_data,
print_mode=request.args.get('print', 'false') == 'true',
report_str=json.dumps(validata_report, sort_keys=True, indent=2), report_str=json.dumps(validata_report, sort_keys=True, indent=2),
breadcrumbs=[{'url': url_for('home'), 'title': 'Accueil'}, breadcrumbs=[{'url': url_for('home'), 'title': 'Accueil'},
{'url': url_for('scdl_validator', val_code=schema_code), {'url': url_for('scdl_validator', val_code=schema_code),
...@@ -218,6 +222,45 @@ def validators(): ...@@ -218,6 +222,45 @@ def validators():
return redirect(url_for('home')) return redirect(url_for('home'))
@app.route('/pdf/<val_code>')
def pdf_report(val_code):
"""PDF report generation"""
err_prefix = 'Erreur de génération du rapport PDF: '
if not ValidatorHelper.schema_exist(val_code):
flash_error(err_prefix + 'schéma inconnu')
return redirect(url_for('home'))
url_param = request.args.get('url')
if not url_param:
flash_error(err_prefix+'url non fournie')
return redirect(url_for('home'))
validation_url = '{}?input=url&print=true&url={}'.format(url_for('scdl_validator', val_code=val_code, _external=True),
quote_plus(url_param))
# temporary pdf_report.pdf filename
tmp_pdf_report = Path('/tmp') / 'validata_{}_{}_pdf_report.pdf'.format(val_code, str(time.time()))
cmd = ['chromium', '--headless', '--disable-gpu',
'--print-to-pdf={}'.format(str(tmp_pdf_report)), validation_url]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if result.returncode != 0:
flash_error(err_prefix + result.stdout)
if tmp_pdf_report.exists():
tmp_pdf_report.unlink()
return redirect(url_for('home'))
response = make_response(tmp_pdf_report.open('rb').read())
response.headers.set('Content-disposition', 'attachment', filename='report.pdf')
response.headers.set('Content-type', 'application/pdf')
response.headers.set('Content-length', tmp_pdf_report.stat().st_size)
tmp_pdf_report.unlink()
return response
@app.route('/validators/<val_code>', methods=['GET', 'POST']) @app.route('/validators/<val_code>', methods=['GET', 'POST'])
def scdl_validator(val_code): def scdl_validator(val_code):
""" Validator page """ """ Validator page """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment