Commit 3c01a449 authored by Christophe Benz's avatar Christophe Benz

Enhance UX

parent 2bf9a556
{
"sections": [
{
"code": "scdl",
"title": "Validateurs SCDL",
"name": "scdl",
"title": "Socle Commun des Données Locales",
"catalog": "https://git.opendatafrance.net/scdl/catalog/raw/master/catalog.json"
},
{
"code": "external",
"name": "external",
"title": "Validateurs externes",
"catalog": [
{
......
......@@ -36,11 +36,11 @@ if config.HOMEPAGE_CONFIG:
caching_session = cachecontrol.CacheControl(requests.Session())
schema_from_url = generate_schema_from_url_func(caching_session)
for section in config.HOMEPAGE_CONFIG['sections']:
code = section['code']
log.info('Initializing homepage section "{}"...'.format(code))
name = section['name']
log.info('Initializing homepage section "{}"...'.format(name))
if isinstance(section['catalog'], str) and section['catalog'].startswith('http'):
url = section['catalog']
schema_catalog_map[code] = opendataschema.SchemaCatalog(url, session=caching_session)
schema_catalog_map[name] = opendataschema.SchemaCatalog(url, session=caching_session)
log.info("...done")
# Flask things
......
......@@ -10,7 +10,7 @@
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU" crossorigin="anonymous">
<title>Validata - {% block title %}{% endblock %}</title>
<title>Validata {% block title %}{% endblock %}</title>
<style>
body {
......@@ -28,7 +28,7 @@
<body>
<!-- horizontal navigation bar -->
<nav class="nav px-3 py-2">
<nav class="nav px-3 py-2 border-bottom">
<a class="navbar-brand" href="{{ url_for('home') }}">
<img src="{{ url_for('static', filename='img/logo-horizontal.png') }}" height="15" alt="Validata" />
</a>
......@@ -41,14 +41,21 @@
</nav>
<!-- Breadcrumbs -->
{% if breadcrumbs %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{% for bc in breadcrumbs %}
<li class="breadcrumb-item"><a href="{{ bc.url }}">{{ bc.title|e }}</a></li>
<li class="breadcrumb-item">
{% if bc.url %}
<a href="{{ bc.url }}">{{ bc.title|e }}</a>
{% else %}
<span>{{ bc.title|e }}</span>
{% endif %}
</li>
{% endfor %}
<li class="breadcrumb-item active" aria-current="page">{{ title|e }}</li>
</ol>
</nav>
{% endif %}
<!-- Flashing messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
......@@ -62,8 +69,8 @@
{% endwith %}
<!-- main content -->
<div id="content" class="container-fluid">
{% block content %}{% endblock %}
<div id="content" class="container">
{% block content %}{% endblock %}
</div>
<footer class="footer hidden-print">
......
{% extends "base_template.html" %} {% block title %}{{ title }}{% endblock %} {%
block head %}
{{ super() }}
{% endblock %} {% block content %}
<h1 class="my-4">Validez vos jeux de données</h1>
{% extends "base_template.html" %}
{% block title %}Accueil{% endblock %}
{% block content %}
{% for section in config.sections %}
{% if section.name != "external" %}
<div class="mb-5">
<h2 class="my-4">{{section.title}}</h2>
<form action="{{ url_for('custom_validator') }}" method="GET">
<div class="form-row">
<div class="form-group col-lg-9">
<select required name="schema_name" class="form-control">
<option disabled selected value="">Choisissez un schéma</option>
{% for val in section.catalog | sort(attribute='title') %}
<option value="{{ val.name }}">{{ val.title }}</option>
{% endfor %}
</select>
</div>
<div class="form-group col-lg-3">
<button type="submit" class="btn btn-primary">Valider un fichier</button>
</div>
</div>
<input type="hidden" name="schema_ref" value="master" />
</form>
</div>
{% endif %}
{% endfor %}
<div class="my-5">
<h2 class="my-4">Autre schéma</h2>
<form action="{{ url_for('custom_validator') }}" method="GET">
<div class="form-row">
<div class="form-group col-lg-9">
<input
name="schema_url"
type="url"
class="form-control"
id="schema_url"
aria-describedby="urlHelpBlock"
placeholder="https://example.com/schema.json"
required
/>
<small id="urlHelpBlock" class="form-text text-muted">
Le schéma doit être au format
<a href="https://frictionlessdata.io/docs/table-schema/" target="_blank">Table Schema</a>.
</small>
</div>
<div class="form-group col-lg-3">
<button type="submit" class="btn btn-primary">Valider un fichier</button>
</div>
</div>
</form>
</div>
{% for section in config.sections %}
<h2>{{section.title}}</h2>
<div class="row my-4">
{% for val in section.catalog %}
{% if val.website %}
<div class="col-sm-4 col-md-3 mb-4">
<div class="card text-center h-100">
{% if section.name == "external" %}
<div class="my-5">
<h2 class="my-4">{{ section.title }}</h2>
<div class="row my-4">
{% for val in section.catalog %}
<div class="col-sm-4 mb-4">
<div class="card h-100">
<div class="card-body d-flex flex-column">
<h4 class="card-title">{{ val.title }}</h4>
<p class="card-text">{{ val.description }}</p>
<a
href="{{ val.website }}" target="_blank"
class="btn btn-primary mt-auto"
>Voir
class="btn btn-outline-secondary mt-auto"
>Utiliser
<i class="fas fa-external-link-alt ml-1"></i>
</a
>
</div>
</div>
</div>
{% else %}
<div class="col-sm-4 col-md-3 mb-4">
<div class="card text-center h-100">
<div class="card-body d-flex flex-column">
<h4 class="card-title">{{ val.title }}</h4>
<p class="card-text">{{ val.description }}</p>
<a
href="{{ url_for('custom_validator') }}?schema_name={{val.name}}&schema_ref=master"
class="btn btn-primary mt-auto"
>Choisir</a
>
</div>
</div>
</div>
{% endif %}
{% endfor %}
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
<h2>Validation à la carte</h2>
<p>
<form action="{{ url_for('custom_validator') }}" method="GET">
<div class="form-group">
<label for="schema_url">Indiquez ici l'URL du schéma de validation à utiliser</label>
<input
name="schema_url"
type="url"
class="form-control"
id="schema_url"
aria-describedby="urlHelp"
placeholder="https://..."
/>
</div>
<input type="submit" class="btn btn-primary" value="Continuer" />
</form>
</p>
{% endblock %}
{% macro html(schema_info, schema_versions, schema_current_version, doc_url) %}
<h5 class="card-title">
Schéma
<h2 class="card-title">
{% if schema_info.title %}
« {{ schema_info.title }} »
{{ schema_info.title }}
{% endif %}
{% if schema_info.version %}
<span class="badge badge-primary">{{ schema_info.version }}</span>
{% endif %}
</h5>
</h2>
{% if schema_versions %}
<form class="form-inline my-2">
<div class="form-group">
<label for="git_ref" class="mr-2">Version</label>
<select class="form-control" id="version_select" name="git_ref">
{% for sv in schema_versions %}
<option{% if sv.name == schema_current_version %} selected="selected"{% endif %}>{{ sv.name }}</option>
{% endfor %}
</select>
</div>
</form>
{% endif %}
{% if schema_info.description %}
<h6 class="card-subtitle mb-2 text-muted">{{ schema_info.description }}</h6>
<p>{{ schema_info.description }}</p>
{% endif %}
{% if schema_info.contributors %}
<p>
<h6>Contributeur{% if schema_info.contributors|length > 1 %}s{% endif %} :</h6>
<ul>
{% for contributor in schema_info.contributors %}
<li>
{{ contributor.title }}{% if contributor.title and contributor.organisation %}, {% endif %}
{% if contributor.organisation %}{{ contributor.organisation }}{% endif %}
{# role #}
{% if contributor.role == 'author' %}(auteur){% endif %}
{% if contributor.role == 'contributor' %}(contributeur){% endif %}
</li>
{% endfor %}
</ul>
</p>
<p>Contributeurs :</p>
<ul>
{% for contributor in schema_info.contributors %}
<li>
{{ contributor.title }}{% if contributor.title and contributor.organisation %}, {% endif %}
{% if contributor.organisation %}{{ contributor.organisation }}{% endif %}
{# role #}
{% if contributor.role == 'author' %}(auteur){% endif %}
{% if contributor.role == 'contributor' %}(contributeur){% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% if schema_info.homepage or schema_info.url %}
<p>
{% if schema_info.homepage %}
Site : <a href="{{ schema_info.homepage }}" target="_blank" title="{{ schema_info.homepage }}" class="card-link">{{ schema_info.homepage | truncate(40) }}</a>
{% endif %}
{#
{% if schema_info.homepage and schema_info.url %}<br />{% endif %}
{% if schema_info.url %}
Source : <a href="{{ schema_info.url }}" target="_blank" title="{{ schema_info.url }}" class="card-link">{{ schema_info.url | truncate(40) }}</a>
{% endif %}
#}
</p>
{% if schema_info.homepage %}
<a href="{{ schema_info.homepage }}" target="_blank" class="card-link d-block ml-0">
Page d'accueil
</a>
{% endif %}
{% if schema_versions %}
<p>
Changer de version Git :
<select id="version_select">
{% for sv in schema_versions %}
<option{% if sv.name == schema_current_version %} selected="selected"{% endif %}>{{ sv.name }}</option>
{% endfor %}
</select>
{% if schema_current_version == 'master' %}
<br />
<a href="{{ doc_url }}" target="_blank">Documentation</a>
{% endif %}
</p>
{% if schema_current_version == 'master' %}
<a href="{{ doc_url }}" target="_blank" class="card-link d-block ml-0">Documentation</a>
{% endif %}
{% endmacro %}
......
{% extends "base_template.html" %}
{% import 'schema_info_part.html' as schema_info_part %}
{% import 'schema_info_macros.html' as schema_info_macros %}
{% block title %}{{ title }}{% endblock %}
{% block title %}{{ section_title }} – {{ schema_info.title }}{% endblock %}
{% block content %}
<h1 class="my-4">{{ title }}</h1>
{% set cols_my_classes = 'my-md-0 my-4' %}
<div class="row">
<div class="col-md-4 {{ cols_my_classes }}">
<div class="card bg-faded">
<div class="row my-4">
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-body">
{{ schema_info_part.html(schema_info, schema_versions, schema_current_version, doc_url) }}
{{ schema_info_macros.html(schema_info, schema_versions, schema_current_version, doc_url) }}
</div>
</div>
</div>
<div class="col-md-8 {{ cols_my_classes }}">
<h2>Outil de validation</h2>
<!-- Tab validator -->
<p class="text">Validez ici le fichier de votre choix</p>
<div class="col-lg-6">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="file-tab" data-toggle="tab" href="#file" role="tab" aria-controls="file"
......@@ -38,51 +32,52 @@
</ul>
{% set padding_class = 'p-3' %}
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active {{ padding_class }}" id="file" role="tabpanel" aria-labelledby="file-tab">
<div class="tab-pane show active {{ padding_class }}" id="file" role="tabpanel" aria-labelledby="file-tab">
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="input" value="file" />
{% for key, value in schema_params.items() %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
<div class="form-group">
<label for="file">Choisissez un fichier tabulaire à valider (.xlsx, .xls, .ods, .csv, .tsv,
etc.)</label>
<input type="file" class="form-control-file" name="file" id="file" accept=".csv, .xls, .xlsx, .ods" />
<input required type="file" class="form-control-file" name="file" id="file" accept=".csv, .xls, .xlsx, .ods" aria-describedby="fileHelpBlock" />
<small id="fileHelpBlock" class="form-text text-muted">
Exemple : .xlsx, .xls, .ods, .csv, .tsv, etc.
</small>
</div>
<button type="submit" class="btn btn-primary">Valider</button>
<button type="submit" class="btn btn-primary">Valider le fichier</button>
</form>
</div>
<div class="tab-pane fade {{ padding_class }}" id="url" role="tabpanel" aria-labelledby="url-tab">
<div class="tab-pane {{ padding_class }}" id="url" role="tabpanel" aria-labelledby="url-tab">
<form method="GET">
<input type="hidden" name="input" value="url" />
{% for key, value in schema_params.items() %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
<div class="form-group">
<label for="url">Indiquez l'URL de la table à valider</label>
<input name="url" type="url" class="form-control" id="url" aria-describedby="urlHelp"
placeholder="https://..." />
<input required name="url" type="url" class="form-control" id="url" aria-describedby="urlHelpBlock" placeholder="https://example.com/file.csv" />
<small id="urlHelpBlock" class="form-text text-muted">
Exemple : .xlsx, .xls, .ods, .csv, .tsv, etc.
</small>
</div>
<button type="submit" class="btn btn-primary">Valider</button>
<button type="submit" class="btn btn-primary">Valider le fichier</button>
</form>
</div>
{% if schema_info.resources %}
<div class="tab-pane fade {{ padding_class }}" id="example" role="tabpanel" aria-labelledby="examples-tab">
<div class="tab-pane {{ padding_class }}" id="example" role="tabpanel" aria-labelledby="examples-tab">
<form method="GET">
<input name="input" value="example" type="hidden">
{% for key, value in schema_params.items() %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
<div class="form-group">
<label for="url">Choisissez l'exemple à valider</label>
<select name="url" id="url" class="form-control">
<option value="">...</option>
<select required name="url" id="url" class="form-control">
<option selected disabled value="">Choisissez un fichier tabulaire d'exemple</option>
{% for res in schema_info.resources %}
<option value="{{ res.path }}">{{ res.title }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">Valider</button>
<button type="submit" class="btn btn-primary">Valider le fichier</button>
</form>
</div>
{% endif %}
......@@ -93,6 +88,6 @@
{% block page_scripts %}
{% if schema_versions %}
{{ schema_info_part.script() }}
{{ schema_info_macros.script() }}
{% endif %}
{% endblock %}
\ No newline at end of file
{% macro preview(source_data) %}
<p class="text">
Affichage de {{ source_data.preview_rows_nb }} lignes sur {{ source_data.rows_nb }} au total
<p>
Prévisualisation de {{ source_data.preview_rows_nb }} ligne{% if source_data.preview_rows_nb > 1 %}s{% endif %} sur {{ source_data.rows_nb }} au total :
</p>
<div class="table-responsive">
<table class="table table-striped table-bordered table-sm table-hover">
......@@ -114,41 +114,26 @@
{% set structure_errors = report.table['error-stats']['structure-errors'] %}
{% if structure_errors['count'] != 0 %}
Erreurs de structure ({{ structure_errors['count'] }}) :
<p>Erreur de structure ({{ structure_errors['count'] }}) :</p>
<ul>
{% for elt in structure_errors['count-by-code'] %}
<li>{{ elt[0] }} : {{ elt[1] }}</li>
{% for item in structure_errors['count-by-code'] %}
<li>{{ item[0] }} ({{ item[1] }})</li>
{% endfor %}
</ul>
{% else %}
Aucune erreur de structure.<br/>
<p>Aucune erreur de structure.</p>
{% endif %}
{% if report.table.do_display_body_errors %}
{% set value_errors = report.table['error-stats']['value-errors'] %}
{% if value_errors['count'] != 0 %}
Erreurs de contenu ({{ value_errors['count'] }}) :
{% if value_errors['count'] > 0 %}
<p>Erreur de contenu ({{ value_errors['count'] }} sur {{ value_errors['rows-count'] }} ligne{% if value_errors['rows-count'] > 1 %}s{% endif %}) :</p>
<ul>
{% for elt in value_errors['count-by-code'] %}
<li>{{ elt[0] }} : {{ elt[1] }}</li>
{% for item in value_errors['count-by-code'] %}
<li>{{ item[0] }} ({{ item[1] }})</li>
{% endfor %}
</ul>
<p>
{% if value_errors['rows-count'] == 1 %}
1 ligne en erreur
{% else %}
{{ value_errors['rows-count'] }} lignes en erreur
{% endif %}
sur
{% if report.table['row-count'] == 1 %}
1 ligne
{% else %}
{{ report.table['row-count'] }} lignes
{% endif %}
au total
</p>
{% endif %}
{% endif %}
......
{% extends "base_template.html" %}
{% import 'schema_info_part.html' as schema_info_part %}
{% import 'schema_info_macros.html' as schema_info_macros %}
{% import 'validation_macros.html' as macros %}
{% block title %}{{ title }}{% endblock %}
{% block title %}{{ section_title }} – {{ schema_info.title }}{% endblock %}
{% block head %}
{{ super() }}
......@@ -25,82 +26,75 @@
{% endblock %}
{% block content %}
{% if print_mode %}
<div class="row">
<div class="col-md-5">
{% endif %}
<h1 class="my-4">{{ title }}</h1>
<p>Validation effectuée {{ validation_date }}</p>
{% if display_badge %}
<p><img src="{{ badge_url }}" alt="{{ badge_msg }}" title="{{ badge_msg }}"/></p>
{% endif %}
{% if print_mode %}
</div>
<div class="col-md-7">
<img src="{{ url_for('static', filename='img/logo-horizontal.png') }}" height="80" alt="Validata" />
</div>
</div>
{% endif %}
<p>Validation effectuée {{ validation_date }}</p>
{% if source.type == 'url' %}
<p class="hidden-print">
<a href="{{ pdf_report_url }}&url={{source.url|urlencode}}" target="_blank">
<a class="btn btn-outline-secondary" href="{{ pdf_report_url }}&url={{source.url|urlencode}}" target="_blank">
Télécharger en PDF
</a>
</p>
{% endif %}
{# Schema info #}
<div class="row">
<div class="col-md-{% if print_mode %}12{% else %}5{% endif %} my-4">
<div class="card bg-faded">
<div class="row my-4">
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-body">
{{ schema_info_macros.html(schema_info, None, schema_current_version, doc_url) }}
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card">
<div class="card-body">
{{ schema_info_part.html(schema_info, schema_versions, schema_current_version, doc_url) }}
<hr />
<div>
<h5 class="card-title">
Fichier traité
</h5>
<h2 class="card-title">
Fichier {% if report.error_count == 0 %}valide{% else %}invalide{% endif %}
</h2>
{% if display_badge %}
<p><img src="{{ badge_url }}" alt="{{ badge_msg }}" /></p>
{% endif %}
<p>
<code>
{% if source.type == 'file' %}
{{ source.filename }}
{% elif source.type == 'url' %}
<a href="{{source.url}}" target="_blank">{{source.filename}}</a>
{% endif %}
{% if source.type == 'url' %}
<a href="{{source.url}}" target="_blank">
{{source.url}}
</a>
</code>
({{ report.table.col_count }} colonnes × {{ report.table.row_count }} lignes)
</p>
{% if report.error_count == 0 %}
<p>Aucune erreur détectée</p>
{% else %}
{{ macros.error_statistics(report) }}
<p>
{% set error_count = report.table['error-stats']['count'] %}
Total :
{% if error_count == 1 %}
1 erreur détectée
{% else %}
{{ error_count }} erreurs détectées
{% endif %}
<br />
Dimensions : {{ report.table.col_count }} colonnes et {{ report.table.row_count }} lignes
</div>
</p>
{% endif %}
</div>
</div>
</div>
</div>
{% import 'validation_macros.html' as macros %}
{% if report.error_count == 0 %}
<h2>La table est valide</h2>
<p>Aucune erreur détectée</p>
{{ macros.preview(source_data) }}
{% else %}
<h2>La table est invalide</h2>
<p>
{% set error_count = report.table['error-stats']['count'] %}
{% if error_count == 1 %}
1 erreur détectée.
{% else %}
{{ error_count }} erreurs détectées.
{% endif %}
</p>
{{ macros.error_statistics(report) }}
{% if report.table.errors.structure %}
<h3>Erreurs de structure</h3>
<h3 class="my-4">Erreurs de structure</h3>
{# Non-column errors #}
{% for err in report.table.errors.structure %}
{% if not err.in_column_comp %}
......@@ -146,7 +140,7 @@
{# We do display body errors! #}
{% if report.table.do_display_body_errors %}
<h3>Erreurs de contenu</h3>
<h3 class="my-4">Erreurs de contenu</h3>
{# No errors - display preview #}
{% if not report.table.errors.body %}
......@@ -174,10 +168,6 @@
{% endblock %}
{% block page_scripts %}
{% if schema_versions %}
{{ schema_info_part.script() }}
{% endif %}
<script>
$(function() {
// Errors tooltip activate
......
""" Call validation code """
import logging
from abc import abstractmethod, ABC
from abc import ABC, abstractmethod
from io import BytesIO
from pathlib import Path