Commit 1ffa4af0 authored by Pierre Dittgen's avatar Pierre Dittgen

Improve error messages

parent a1b4ae15
......@@ -8,9 +8,10 @@ FRENCH_DATE_RE = re.compile(r'^[0-3]\d/[0-1]\d/[12]\d{3}$')
DATETIME_RE = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$')
def update_msg(err, msg):
""" Handy method to update err message and return err """
err['message'] = msg
def u_err(err, title, content):
""" Update error """
err['title'] = title
err['content'] = content
return err
......@@ -34,11 +35,14 @@ def et_join(values):
return ' et '.join([', '.join(values[:-1]), values[-1]])
# Core goodtables checks
# -> error message title is stored in 'title' attribute
# -> error message content is stored in 'content' attribute
# This is adapted to pophover display
def blank_row(err, headers, schema):
""" blank-row error """
return update_msg(err, 'La ligne est vide')
return u_err(err, 'Ligne vide', 'Les lignes vides doivent être retirées de la table')
def duplicate_row(err, headers, schema):
......@@ -46,27 +50,34 @@ def duplicate_row(err, headers, schema):
msg_prefix = 'La ligne est identique '
row_numbers = err['message-data']['row_numbers']
if not ',' in row_numbers:
msg = msg_prefix + "à la ligne {}".format(row_numbers)
msg = msg_prefix + "à la ligne {}.".format(row_numbers)
else:
msg = msg_prefix + "aux lignes {}".format(et_join(row_numbers))
return update_msg(err, msg)
msg = msg_prefix + "aux lignes {}.".format(et_join(row_numbers))
msg += "<br/>Vous pouvez la supprimer."
return u_err(err, 'Ligne dupliquée', msg)
def enumerable_constraint(err, headers, schema):
""" enumerable-constraint """
constraint_values = json.loads(err['message-data']['constraint'].replace("'", '"'))
constraint_values = eval(err['message-data']['constraint'])
ok_values = ['"{}"'.format(val) for val in constraint_values]
if len(ok_values) == 1:
return update_msg(err, 'Valeur incorrecte. La valeur autorisée est : {}'.format(ok_values[0]))
return u_err(err, 'Valeur incorrecte', 'L\'unique valeur autorisée pour cette colonne est: {}'.format(ok_values[0]))
else:
ok_values_str = et_join(ok_values)
return update_msg(err, 'Valeur incorrecte. Les valeurs autorisées sont : {}'.format(ok_values_str))
return u_err(err, 'Valeur incorrecte', 'Les valeurs autorisées pour cette colonne sont : {}'.format(ok_values_str))
def minimum_constraint(err, headers, schema):
""" minimum-constraint """
min_value = err['message-data']['constraint']
return u_err(err, 'Valeur trop petite', 'La valeur attendue doit être supérieure à {}'.format(min_value))
def maximum_constraint(err, headers, schema):
""" maximum-constraint """
max_value = err['message-data']['constraint']
return update_msg(err, 'Valeur incorrecte. La valeur ne doit pas dépasser {}'.format(max_value))
return u_err(err, 'Valeur trop grande', 'La valeur attendue doit être inférieure à {}'.format(max_value))
def pattern_constraint(err, headers, schema):
......@@ -80,7 +91,7 @@ def pattern_constraint(err, headers, schema):
if 'example' in field:
addon_info_list.append('Exemple(s) de valeur correcte : {}'.format(field['example']))
addon_info = '<br/>' + '<br/>'.join(addon_info_list) if addon_info_list else ''
return update_msg(err, 'Valeur incorrecte. La valeur ne respecte pas le format attendu pour la colonne « {} ».{}'.format(col_name, addon_info))
return u_err(err, 'Erreur de format', 'La valeur ne respecte pas le format attendu pour la colonne « {} ».{}'.format(col_name, addon_info))
def required_constraint(err, headers, schema):
......@@ -89,7 +100,7 @@ def required_constraint(err, headers, schema):
col_nb = err['column-number']
if col_nb <= len(headers):
col_name = ' "{}"'.format(headers[col_nb - 1])
return update_msg(err, 'Le contenu de la colonne{} est obligatoire\n'.format(col_name)
return u_err(err, 'Cellule vide', 'Le contenu de la colonne{} est obligatoire\n'.format(col_name)
+ 'Merci d\'indiquer une valeur.')
......@@ -104,23 +115,24 @@ def type_or_format_error(err, headers, schema):
dm = FRENCH_DATE_RE.match(err_value)
if dm:
iso_date = datetime.strptime(err_value, '%d/%m/%Y').strftime('%Y-%m-%d')
return update_msg(err, "Le format de la date n'est correct.\nLa forme attendue est \"{}\"".format(iso_date))
return u_err(err, 'Format de date incorrect', "La forme attendue est \"{}\"".format(iso_date))
# Checks if date is yyyy-mm-ddThh:MM:ss
# print('DATE TIME ? [{}]'.format(err_value))
dm = DATETIME_RE.match(err_value)
if dm:
iso_date = err_value[:err_value.find('T')]
return update_msg(err, "Le format de la date n'est correct.\nLa forme attendue est \"{}\"".format(iso_date))
return u_err(err, 'Format de date incorrect', "La forme attendue est \"{}\"".format(iso_date))
# Default date err msg
return update_msg(err, 'Le format de la date est incorrect')
return u_err(err, 'Format de date incorrect', 'La date doit être écrite sous la forme aaaa-mm-jj.')
# Number
elif err_type == 'number':
if ',' in err_value:
en_number = err_value.replace(',', '.')
return update_msg(err, "Le format du nombre est incorrect.\nLa forme attendue est \"{}\"".format(en_number))
return u_err(err, 'Format de nombre incorrect', "Merci d'utiliser le point comme séparateur décimal («&#160;{}&#160;»).".format(en_number))
return u_err(err, 'Format de nombre incorrect', 'Vérifiez que la cellule ne comporte que des chiffres et le point comme séparateur décimal.')
# Boolean
elif err_type == 'boolean':
......@@ -128,49 +140,57 @@ def type_or_format_error(err, headers, schema):
field = schema['fields'][column_number]
true_values = field.get('trueValues', ['true'])
false_values = field.get('falseValues', ['false'])
return update_msg(err, "Valeur incorrecte. Les valeurs acceptées sont {} pour 'vrai' et {} pour 'faux'".format(et_join(false_values), et_join(true_values)))
return u_err(err, "Valeur booléenne incorrecte", "Les valeurs acceptées sont {} pour 'vrai' et {} pour 'faux'".format(et_join(false_values), et_join(true_values)))
# Default msg
return update_msg(err, 'XXXX {} XXXX'.format(err_type))
return u_err(err, 'Type ou format incorrect', 'La valeur de la cellule n\'est pas de type {}'.format(err_type))
# Validata custom checks
def french_siret_value(err, headers, schema):
""" french-siret-value error """
return update_msg(err, 'Le numéro SIRET est non valide')
return u_err(err, 'Numéro SIRET non valide',
'Le numéro de SIRET indiqué n\'est pas valide selon la définition de l\'<a href="https://www.insee.fr/fr/metadonnees/definition/c1841">INSEE</a>')
# Validata pre-checks
#
# -> Error message is stored in 'message' key
def invalid_column_delimiter(err, headers, schema):
""" invalid-column-delimiter """
md = err['message-data']
return update_msg(err, 'Le fichier CSV utilise le délimiteur de colonne « {} » au lieu du délimiteur attendu « {} ».'.format(md['detected'], md['expected'])
+ "<br/>Pour vous permettre de continuer la validation, un remplacement automatique a été réalisé.")
msg_tpl = 'Le fichier CSV utilise le délimiteur de colonne « {} » au lieu du délimiteur attendu « {} ».'
err['message'] = msg_tpl.format(md.get('detected'), md.get(
'expected')) + "<br/>Pour vous permettre de continuer la validation, un remplacement automatique a été réalisé."
return err
def missing_headers(err, headers, schema):
""" missing-headers """
cols = err['message-data']['headers']
if len(cols) == 1:
return update_msg(err, "La colonne \"{}\" n'a pas été trouvée dans le fichier".format(cols[0]))
err['message'] = "La colonne \"{}\" n'a pas été trouvée dans le fichier".format(cols[0])
else:
cols_ul = '<ul>' + '\n'.join(['<li>{}</li>'.format(col) for col in cols]) + '</ul>'
fields_nb = len(schema.get('fields', []))
addon_info = 'Utilisez-vous le bon schéma ?' if len(cols) == fields_nb else ''
return update_msg(err, "Les colonnes suivantes n'ont pas été trouvées dans le fichier : {}{}".format(cols_ul, addon_info))
err['message'] = "Les colonnes suivantes n'ont pas été trouvées dans le fichier : {}{}".format(
cols_ul, addon_info)
return err
def extra_headers(err, headers, schema):
""" extra-headers """
cols = err['message-data']['headers']
if len(cols) == 1:
return update_msg(err, "La colonne \"{}\" est inconnue dans le schéma".format(cols[0]))
err['message'] = "La colonne \"{}\" est inconnue dans le schéma".format(cols[0])
else:
cols_ul = '<ul>' + '\n'.join(['<li>{}</li>'.format(col) for col in cols]) + '</ul>'
addon_info = 'Utilisez-vous le bon schéma ?' if len(cols) == len(headers) else ''
return update_msg(err, "Les colonnes suivantes sont inconnues dans le schéma : {}{}".format(cols_ul, addon_info))
err['message'] = "Les colonnes suivantes sont inconnues dans le schéma : {}{}".format(cols_ul, addon_info)
return err
def wrong_headers_order(err, headers, schema):
......@@ -183,4 +203,5 @@ def wrong_headers_order(err, headers, schema):
continue
msgs.append('la colonne {} devrait être « {} » (au lieu de « {} »)'.format(i+1, field, header))
errors_str = '<ul>\n' + '\n'.join(['<li>{}</li>\n'.format(msg) for msg in msgs]) + '\n</ul>'
return update_msg(err, "Les colonnes du tableau ne sont pas dans l'ordre attendu :\n{}".format(errors_str))
err['message'] = "Les colonnes du tableau ne sont pas dans l'ordre attendu :\n{}".format(errors_str)
return err
......@@ -31,19 +31,20 @@
<thead class="thead-light">
<th scope="col">1</th>
{% for h in report.table.headers %}
<th scope="col" data-toggle="tooltip" title="{{ report.table.headers_title[loop.index - 1]}}">{{ h }}</th>
<th scope="col" data-toggle="popover" title="{{ report.table.headers_title[loop.index - 1] }}" data-content="{{ report.table.headers_description[loop.index - 1]}}">{{
h }}</th>
{% endfor %}
</thead>
<tbody>
{% for row in report.table.errors.by_rows %}
<tr>
{% if 'row' in row.errors %}
<th class="table-danger" data-toggle="tooltip" title="{{ row.errors.row.message }}">{{
row.row_id
}}</th>
<th class="table-danger" data-toggle="popover" title="{{ row.errors.row.title }}" data-content="{{ row.errors.row.content }}">
{{ row.row_id }}
</th>
{% if row.errors.row.code == 'blank-row' and not source_data.data_rows[row.row_id -2] %}
{% for _ in report.table.headers %}
<td class="table-danger" data-toggle="tooltip" title="{{ row.errors.row.message }}"></td>
<td class="table-danger" data-toggle="popover" title="{{ row.errors.row.title }}" data-content="{{ row.errors.row.content }}"></td>
{% endfor %}
{% endif %}
{% else %}
......@@ -51,9 +52,9 @@
{% endif %}
{% for d in source_data.data_rows[row.row_id - 2] %}
{% if loop.index in row.errors %}
<td class="table-danger" data-toggle="tooltip" title="{{ row.errors[loop.index].message }}">
<td class="table-danger" data-toggle="popover" title="{{row.errors[loop.index].title}}" data-content="{{ row.errors[loop.index].content }}">
{% elif 'row' in row.errors %}
<td class="table-danger" data-toggle="tooltip" title="{{ row.errors.row.message }}">
<td class="table-danger" data-toggle="popover" title="{{ row.errors.row.title }}" data-content="{{ row.errors.row.content }}">
{% else %}
<td>
{% endif %}
......
......@@ -81,7 +81,11 @@
<script>
$(function () {
// Errors tooltip activate
$('[data-toggle="tooltip"]').tooltip();
$('[data-toggle="popover"]').popover({
html: true,
placement: 'auto',
trigger: 'hover click'
});
// Show body errors
$('#show_body_errors').on('click', function () {
......
......@@ -60,6 +60,7 @@ ERROR_MESSAGE_FUNC = {
'blank-row': error_messages.blank_row,
'duplicate-row': error_messages.duplicate_row,
'enumerable-constraint': error_messages.enumerable_constraint,
'minimum-constraint': error_messages.minimum_constraint,
'maximum-constraint': error_messages.maximum_constraint,
'required-constraint': error_messages.required_constraint,
'type-or-format-error': error_messages.type_or_format_error,
......@@ -81,8 +82,8 @@ def improve_messages(errors, headers, schema):
def error_message_default_func(error, headers, schema):
""" Sets a new better error message """
data_str = json.dumps(error)
error['message'] = '[{}] {} ({})'.format(error['code'], error.get('message', ''), data_str)
error['title'] = error['code']
error['content'] = error.get('message', 'pas d\'information complémentaire')
return error
improved_errors = []
......@@ -132,10 +133,12 @@ def create_validata_report(goodtables_report, schema):
headers = report['table'].get('headers', [])
report['table']['col_count'] = len(headers)
# Headers title
# Computes column info
schema_fields = schema.get('fields', [])
fields_dict = {f['name']: f['title'] for f in schema_fields}
report['table']['headers_title'] = [fields_dict.get(h, r'/!\ colonne inconnue dans le schéma') for h in headers]
fields_dict = {f['name']: (f.get('title', 'titre non défini'), f.get('description', '')) for f in schema_fields}
report['table']['headers_title'] = [fields_dict[h][0] if h in fields_dict else 'colonne inconnue' for h in headers]
report['table']['headers_description'] = [fields_dict[h][1]
if h in fields_dict else 'Cette colonne n\'est pas définie dans le schema' for h in headers]
# Add context to errors
errors = contextualize(report['table']['errors'])
......
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