error_messages.py 9.56 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
#!/usr/bin/env python3
import re
from datetime import datetime

import ujson as json

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}$')


Pierre Dittgen's avatar
Pierre Dittgen committed
11 12 13 14
def u_err(err, title, content):
    """ Update error """
    err['title'] = title
    err['content'] = content
15 16
    return err

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

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]])

37
# Core goodtables checks
Pierre Dittgen's avatar
Pierre Dittgen committed
38 39 40
# -> error message title is stored in 'title' attribute
# -> error message content is stored in 'content' attribute
# This is adapted to pophover display
41 42 43 44


def blank_row(err, headers, schema):
    """ blank-row error """
Pierre Dittgen's avatar
Pierre Dittgen committed
45
    return u_err(err, 'Ligne vide', 'Les lignes vides doivent être retirées de la table')
46 47 48 49 50 51 52


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:
Pierre Dittgen's avatar
Pierre Dittgen committed
53
        msg = msg_prefix + "à la ligne {}.".format(row_numbers)
54
    else:
Pierre Dittgen's avatar
Pierre Dittgen committed
55 56 57
        msg = msg_prefix + "aux lignes {}.".format(et_join(row_numbers))
    msg += "<br/>Vous pouvez la supprimer."
    return u_err(err, 'Ligne dupliquée', msg)
58 59 60 61


def enumerable_constraint(err, headers, schema):
    """ enumerable-constraint """
Pierre Dittgen's avatar
Pierre Dittgen committed
62
    constraint_values = eval(err['message-data']['constraint'])
63 64
    ok_values = ['"{}"'.format(val) for val in constraint_values]
    if len(ok_values) == 1:
Pierre Dittgen's avatar
Pierre Dittgen committed
65
        return u_err(err, 'Valeur incorrecte', 'L\'unique valeur autorisée pour cette colonne est: {}'.format(ok_values[0]))
66
    else:
Pierre Dittgen's avatar
Pierre Dittgen committed
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
        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))
Pierre Dittgen's avatar
Pierre Dittgen committed
82 83 84 85 86


def minimum_constraint(err, headers, schema):
    """ minimum-constraint """
    min_value = err['message-data']['constraint']
Pierre Dittgen's avatar
Pierre Dittgen committed
87
    return u_err(err, 'Valeur trop petite', 'La valeur attendue doit être au moins égale à {}'.format(min_value))
88 89


Pierre Dittgen's avatar
Pierre Dittgen committed
90 91 92 93 94
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))
95 96 97 98 99


def pattern_constraint(err, headers, schema):
    """ pattern-constraint """
    column_number = err['column-number']
Pierre Dittgen's avatar
Pierre Dittgen committed
100
    field = schema['fields'][column_number - 1]
101 102 103 104 105 106 107
    col_name = field['name']
    addon_info_list = []
    if 'description' in field:
        addon_info_list.append('Pour rappel, la description de cette colonne est « {} »'.format(field['description']))
    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 ''
Pierre Dittgen's avatar
Pierre Dittgen committed
108
    return u_err(err, 'Erreur de format', 'La valeur ne respecte pas le format attendu pour la colonne <strong>{}</strong>.{}'.format(col_name, addon_info))
109 110 111 112 113 114 115 116


def required_constraint(err, headers, schema):
    """ required-constraint error """
    col_name = ''
    col_nb = err['column-number']
    if col_nb <= len(headers):
        col_name = ' "{}"'.format(headers[col_nb - 1])
Pierre Dittgen's avatar
Pierre Dittgen committed
117
    return u_err(err, 'Cellule vide', 'Le contenu de la colonne{} est obligatoire\n'.format(col_name)
118 119 120 121 122 123 124 125 126 127 128 129 130 131
                      + 'Merci d\'indiquer une valeur.')


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')
Pierre Dittgen's avatar
Pierre Dittgen committed
132
            return u_err(err, 'Format de date incorrect', "La forme attendue est \"{}\"".format(iso_date))
133 134 135 136 137 138

        # 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')]
Pierre Dittgen's avatar
Pierre Dittgen committed
139
            return u_err(err, 'Format de date incorrect', "La forme attendue est \"{}\"".format(iso_date))
140 141

        # Default date err msg
Pierre Dittgen's avatar
Pierre Dittgen committed
142
        return u_err(err, 'Format de date incorrect', 'La date doit être écrite sous la forme aaaa-mm-jj.')
143 144 145 146 147

    # Number
    elif err_type == 'number':
        if ',' in err_value:
            en_number = err_value.replace(',', '.')
Pierre Dittgen's avatar
Pierre Dittgen committed
148 149
            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.')
150 151

    # Boolean
152 153 154 155 156
    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'])
Pierre Dittgen's avatar
Pierre Dittgen committed
157
        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)))
158 159

    # Default msg
Pierre Dittgen's avatar
Pierre Dittgen committed
160
    return u_err(err, 'Type ou format incorrect', 'La valeur de la cellule n\'est pas de type {}'.format(err_type))
161

Pierre Dittgen's avatar
Pierre Dittgen committed
162 163 164 165 166 167 168 169 170 171 172 173 174 175

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)


176 177 178 179 180
# Validata custom checks


def french_siret_value(err, headers, schema):
    """ french-siret-value error """
Pierre Dittgen's avatar
Pierre Dittgen committed
181 182
    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>')
183 184

# Validata pre-checks
Pierre Dittgen's avatar
Pierre Dittgen committed
185 186
#
# -> Error message is stored in 'message' key
187 188 189 190 191


def invalid_column_delimiter(err, headers, schema):
    """ invalid-column-delimiter """
    md = err['message-data']
Pierre Dittgen's avatar
Pierre Dittgen committed
192 193 194 195
    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
196 197 198 199 200 201


def missing_headers(err, headers, schema):
    """ missing-headers """
    cols = err['message-data']['headers']
    if len(cols) == 1:
Pierre Dittgen's avatar
Pierre Dittgen committed
202
        err['message'] = "La colonne \"{}\" n'a pas été trouvée dans le fichier".format(cols[0])
203 204 205 206
    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 ''
Pierre Dittgen's avatar
Pierre Dittgen committed
207 208 209
        err['message'] = "Les colonnes suivantes n'ont pas été trouvées dans le fichier : {}{}".format(
            cols_ul, addon_info)
    return err
210 211 212 213 214 215


def extra_headers(err, headers, schema):
    """ extra-headers """
    cols = err['message-data']['headers']
    if len(cols) == 1:
Pierre Dittgen's avatar
Pierre Dittgen committed
216
        err['message'] = "La colonne \"{}\" est inconnue dans le schéma".format(cols[0])
217 218 219
    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 ''
Pierre Dittgen's avatar
Pierre Dittgen committed
220 221
        err['message'] = "Les colonnes suivantes sont inconnues dans le schéma : {}{}".format(cols_ul, addon_info)
    return err
222 223 224 225 226 227 228 229 230 231 232 233


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>'
Pierre Dittgen's avatar
Pierre Dittgen committed
234 235
    err['message'] = "Les colonnes du tableau ne sont pas dans l'ordre attendu :\n{}".format(errors_str)
    return err