Commit 7a46f296 authored by Pierre Dittgen's avatar Pierre Dittgen

Merge branch 'messages'

parents d3e1dca2 d5ee2f04
......@@ -24,12 +24,13 @@ setup(
include_package_data=True,
install_requires=[
'flask',
'goodtables',
'requests',
'validata_validate >= 0.1, < 0.2',
'goodtables',
'tabulator',
'commonmark',
'ezodf',
'lxml'
'lxml',
'tabulator',
]
)
#!/usr/bin/env python3
import json
import re
from datetime import datetime
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 u_err(err, title, content):
""" Update error """
err['title'] = title
err['content'] = content
return err
def et_join(values):
"""french enum
>>> et_join([])
''
>>> et_join(['a'])
'a'
>>> et_join(['a','b'])
'a et b'
>>> et_join(['a','b','c'])
'a, b et c'
>>> et_join(['a','b','c','d','e'])
'a, b, c, d et e'
"""
if values is None or len(values) == 0:
return ''
if len(values) == 1:
return values[0]
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 u_err(err, 'Ligne vide', 'Les lignes vides doivent être retirées de la table')
def duplicate_row(err, headers, schema):
""" duplicate-row error """
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)
else:
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 = eval(err['message-data']['constraint'])
ok_values = ['"{}"'.format(val) for val in constraint_values]
if len(ok_values) == 1:
return u_err(err, 'Valeur incorrecte', 'L\'unique valeur autorisée pour cette colonne est: {}'.format(ok_values[0]))
else:
html_str = '<ul>' + '\n'.join(['<li>{}</li>'.format(val.strip('"')) for val in ok_values]) + '</ul>'
return u_err(err, 'Valeur incorrecte', 'Les seules valeurs autorisées pour cette colonne sont : {}'.format(html_str))
def maximum_constraint(err, headers, schema):
""" maximum-constraint """
max_value = err['message-data']['constraint']
return u_err(err, 'Valeur trop grande', 'La valeur attendue doit être inférieure à {}'.format(max_value))
def maximum_length_constraint(err, headers, schema):
""" maximum-length-constraint """
max_value = err['message-data']['constraint']
text_value_len = len(err['message-data']['value'])
return u_err(err, 'Valeur trop longue', 'La valeur texte attendue ne doit pas comporter plus de {} caractère(s) (au lieu de {} actuellement)'.format(max_value, text_value_len))
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 au moins égale à {}'.format(min_value))
def minimum_length_constraint(err, headers, schema):
""" minimum-length-constraint """
min_value = err['message-data']['constraint']
text_value_len = len(err['message-data']['value'])
return u_err(err, 'Valeur trop courte', 'Le texte attendu doit comporter au moins {} caractère(s) (au lieu de {} actuellement)'.format(min_value, text_value_len))
def pattern_constraint(err, headers, schema):
""" pattern-constraint """
column_number = err['column-number']
field = schema['fields'][column_number - 1]
info_list = []
if 'description' in field:
info_list.append('<dt>Description</dt>\n<dd>{}</dd>'.format(field['description']))
if 'example' in field:
info_list.append("<dt>Exemple(s) valide(s)</dt><dd>{}</dd>".format(field['example']))
msg = '\n'.join(info_list) if info_list else '<em>Aucune description ni exemple à afficher.</em>'
return u_err(err, 'Format incorrect', msg)
def required_constraint(err, headers, schema):
""" required-constraint error """
return u_err(err, 'Cellule vide', 'Une valeur doit être renseignée.')
def type_or_format_error(err, headers, schema):
""" type-or-format-value """
err_type = err['message-data']['field_type']
err_value = err['message-data']['value']
# Date
if err_type == 'date':
# Checks if date is dd/mm/yyyy
dm = FRENCH_DATE_RE.match(err_value)
if dm:
iso_date = datetime.strptime(err_value, '%d/%m/%Y').strftime('%Y-%m-%d')
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 u_err(err, 'Format de date incorrect', "La forme attendue est \"{}\"".format(iso_date))
# Default date err msg
return u_err(err, 'Format de date incorrect', 'La date doit être écrite sous la forme aaaa-mm-jj.')
# Year
elif err_type == 'year':
return u_err(err, 'Format d\'année incorrect', 'L\'année doit être composée de 4 chiffres')
# Number
elif err_type == 'number':
if ',' in err_value:
en_number = err_value.replace(',', '.')
return u_err(err, 'Format de nombre incorrect', "Utilisez le point comme séparateur décimal («&#160;{}&#160;»).".format(en_number))
return u_err(err, 'Format de nombre incorrect', 'La valeur ne doit comporter que des chiffres et le point comme séparateur décimal.')
# Number
elif err_type == 'integer':
return u_err(err, 'Format entier incorrect', 'La valeur doit être un nombre entier.')
# String
elif err_type == 'string':
return u_err(err, 'Format de chaîne incorrect', 'La valeur doit être une chaîne de caractères.')
# Boolean
elif err_type == 'boolean':
column_number = err['column-number']
field = schema['fields'][column_number]
true_values = field.get('trueValues', ['true'])
false_values = field.get('falseValues', ['false'])
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 u_err(err, 'Type ou format incorrect', 'La valeur de la cellule n\'est pas de type {}'.format(err_type))
def unique_constraint(err, headers, schema):
""" unique-constraint """
msg_prefix = 'Cette valeur est déjà présente '
row_numbers = err['message-data']['row_numbers']
if not ',' in row_numbers:
msg = msg_prefix + "à la ligne {}.".format(row_numbers)
else:
msg = msg_prefix + "aux lignes {}.".format(et_join(row_numbers))
msg += " Or une contrainte d'unicité est définie pour cette colonne."
msg += "Veuillez corriger les doublons de retirez la contrainte d'unicité du schéma."
return u_err(err, 'Valeur déjà utilisée', msg)
# Validata custom checks
def french_siret_value(err, headers, schema):
""" french-siret-value error """
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']
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:
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 ''
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:
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 ''
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):
""" wrong-headers-order """
fields = [f['name'] for f in schema.get('fields', [])]
assert len(headers) == len(fields), 'Wrong column order between two lists of different lengths'
msgs = []
for i, (header, field) in enumerate(zip(headers, fields)):
if header == field:
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>'
err['message'] = "Les colonnes du tableau ne sont pas dans l'ordre attendu :\n{}".format(errors_str)
return err
/* Custom CSS rules for Validata-UI */
/* Validation report
Customize h2 display in popovers
*/
div.popover h2 {
font-size: 1.4em;
font-weight: bold;
}
\ No newline at end of file
......@@ -36,7 +36,7 @@
{% endfor %}
</thead>
<tbody>
{% for row in report.table.errors.by_rows %}
{% for row in report.table.errors.body_by_rows %}
<tr>
{% if 'row' in row.errors %}
<th class="table-danger" data-toggle="popover" title="{{ row.errors.row.title }}" data-content="{{ row.errors.row.content }}">
......
......@@ -52,35 +52,6 @@ def extract_source_data(source: ValidataSource, preview_rows_nb=5):
'preview_rows': rows[:preview_rows_nb]}
ERROR_MESSAGE_FUNC = {
# Core checks
# blank-header
'blank-row': error_messages.blank_row,
# duplicate-header
'duplicate-row': error_messages.duplicate_row,
'enumerable-constraint': error_messages.enumerable_constraint,
'maximum-constraint': error_messages.maximum_constraint,
'maximum-length-constraint': error_messages.maximum_length_constraint,
'minimum-constraint': error_messages.minimum_constraint,
'minimum-length-constraint': error_messages.minimum_length_constraint,
# missing-value
'pattern-constraint': error_messages.pattern_constraint,
'required-constraint': error_messages.required_constraint,
'type-or-format-error': error_messages.type_or_format_error,
'unique-constraint': error_messages.unique_constraint,
# Validata pre-checks
'extra-headers': error_messages.extra_headers,
'invalid-column-delimiter': error_messages.invalid_column_delimiter,
'missing-headers': error_messages.missing_headers,
'wrong-headers-order': error_messages.wrong_headers_order,
# Validata custom checks
'french-siret-value': error_messages.french_siret_value,
}
def improve_messages(errors, headers, schema):
""" Translates and improve error messages """
......
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