views.py 21 KB
Newer Older
1
2
3
"""
    Routes
"""
4
import copy
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
5
import io
Pierre Dittgen's avatar
Pierre Dittgen committed
6
import itertools
7
import json
Christophe Benz's avatar
Christophe Benz committed
8
import logging
9
import subprocess
Christophe Benz's avatar
Christophe Benz committed
10
import tempfile
11
from datetime import datetime
12
from operator import itemgetter
13
from pathlib import Path
14
from urllib.parse import quote_plus, urlencode
15

Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
16
import requests
17
import tableschema
18
from backports.datetime_fromisoformat import MonkeyPatch
19
from commonmark import commonmark
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
20
from flask import make_response, redirect, render_template, request, url_for
Pierre Dittgen's avatar
Pierre Dittgen committed
21
22
from validata_core import compute_badge, csv_helpers, messages
from validata_core.loaders import custom_loaders
23

Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
24
25
import tabulator

26
from . import app, config, ui_config, table_schema_catalog, schema_from_url
27
28
from .ui_util import flash_error, flash_warning
from .validata_util import ValidataSource
29

30
31
MonkeyPatch.patch_fromisoformat()

Christophe Benz's avatar
Christophe Benz committed
32
33
log = logging.getLogger(__name__)

34

35
36
37
class SchemaInstance():
    """Handly class to handle schema information"""

Pierre Dittgen's avatar
Pierre Dittgen committed
38
    def __init__(self, url=None, name=None, ref=None, spec=None, versions=None):
39
40
41
42
43
44
        """This function is not intended to be called directly
        but via from_parameters() static method!"""
        self.url = url
        self.name = name
        self.ref = ref
        self.spec = spec
Pierre Dittgen's avatar
Pierre Dittgen committed
45
        self.versions = versions
46
47
48
49
50

    @staticmethod
    def from_parameters(parameter_dict, table_schema_catalog):
        """Initializes schema instance from requests dict and tableschema catalog (for name ref)
        """
Pierre Dittgen's avatar
Pierre Dittgen committed
51
        schema_url, schema_name, schema_ref, versions = None, None, None, None
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

        # From schema_url
        if 'schema_url' in parameter_dict:
            schema_url = parameter_dict["schema_url"]

        # from schema_name (and schema_ref)
        elif 'schema_name' in parameter_dict:
            schema_name = parameter_dict['schema_name']
            schema_ref = parameter_dict.get('schema_ref')

            # Unknown schema name?
            table_schema_reference = table_schema_catalog.references.get(schema_name)
            if table_schema_reference is None:
                return None

Pierre Dittgen's avatar
Pierre Dittgen committed
67
68
69
70
71
            # Git refs
            if table_schema_reference:
                versions = table_schema_reference.get_refs()
            options = {'ref': schema_ref} if schema_ref else {}
            schema_url = table_schema_reference.get_schema_url(**options)
72
73
74
75
76

        # else???
        else:
            return None

Pierre Dittgen's avatar
Pierre Dittgen committed
77
        return SchemaInstance(schema_url, schema_name, schema_ref, schema_from_url(schema_url), versions)
78
79
80
81
82
83
84
85
86
87
88
89

    def request_parameters(self):
        if self.name:
            return {
                'schema_name': self.name,
                'schema_ref': '' if self.ref is None else self.ref
            }
        return {
            'schema_url': self.url
        }


90
def extract_source_data(source: ValidataSource, preview_rows_nb=5):
91
    """ Computes table preview """
92
93
94

    def stringify(val):
        """ Transform value into string """
Pierre Dittgen's avatar
Pierre Dittgen committed
95
        return '' if val is None else str(val)
96

97
98
    header = None
    rows = []
Pierre Dittgen's avatar
Pierre Dittgen committed
99
    nb_rows = 0
100

101
    options = {}
102
    if source.format == "csv":
103
        options['delimiter'] = csv_helpers.detect_dialect(source.source, format=source.format, scheme=source.scheme,
104
                                                          custom_loaders=custom_loaders).delimiter
105
    with tabulator.Stream(source.source, format=source.format, scheme=source.scheme, custom_loaders=custom_loaders,
106
                          **options) as stream:
107
108
        for row in stream:
            if header is None:
109
                header = ['' if v is None else v for v in row]
110
            else:
111
                rows.append(list(map(stringify, row)))
Pierre Dittgen's avatar
Pierre Dittgen committed
112
                nb_rows += 1
113
    preview_rows_nb = min(preview_rows_nb, nb_rows)
114
115
    return {'header': header,
            'rows_nb': nb_rows,
116
117
118
            'data_rows': rows,
            'preview_rows_nb': preview_rows_nb,
            'preview_rows': rows[:preview_rows_nb]}
119
120


Pierre Dittgen's avatar
Pierre Dittgen committed
121
122
def improve_errors(errors):
    """Add context to errors, converts markdown content to HTML"""
123

Pierre Dittgen's avatar
Pierre Dittgen committed
124
125
    def improve_err(err):
        """Adds context info based on row-nb presence and converts content to HTML"""
126

Pierre Dittgen's avatar
Pierre Dittgen committed
127
128
129
130
        # Context
        update_keys = {
            'context': 'body' if 'row-number' in err and not err['row-number'] is None else 'table',
        }
Pierre Dittgen's avatar
Pierre Dittgen committed
131

Pierre Dittgen's avatar
Pierre Dittgen committed
132
        # markdown to HTML (with default values for 'title' and 'content')
Pierre Dittgen's avatar
Pierre Dittgen committed
133

Pierre Dittgen's avatar
Pierre Dittgen committed
134
135
136
        # Set default title if no title
        if not 'title' in err:
            update_keys['title'] = '[{}]'.format(err['code'])
Pierre Dittgen's avatar
Pierre Dittgen committed
137

Pierre Dittgen's avatar
Pierre Dittgen committed
138
139
140
141
        # Convert message to markdown only if no content
        # => for pre-checks errors
        if 'message' in err and not 'content' in err:
            update_keys['message'] = commonmark(err['message'])
Pierre Dittgen's avatar
Pierre Dittgen committed
142

Pierre Dittgen's avatar
Pierre Dittgen committed
143
144
145
        # Else, default message
        elif not 'message' in err or err['message'] is None:
            update_keys['message'] = '[{}]'.format(err['code'])
Pierre Dittgen's avatar
Pierre Dittgen committed
146

Pierre Dittgen's avatar
Pierre Dittgen committed
147
148
149
        # Message content
        md_content = '*content soon available*' if not 'content' in err else err['content']
        update_keys['content'] = commonmark(md_content)
150

Pierre Dittgen's avatar
Pierre Dittgen committed
151
        return {**err, **update_keys}
152

Pierre Dittgen's avatar
Pierre Dittgen committed
153
    return list(map(improve_err, errors))
154
155


Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
156
def create_validata_ui_report(validata_core_report, schema_dict):
157
158
159
160
161
162
    """ Creates an error report easier to handle and display in templates:
        - only one table
        - errors are contextualized
        - error-counts is ok
        - errors are grouped by lines
        - errors are separated into "structure" and "body"
163
        - error messages are improved
164
    """
Pierre Dittgen's avatar
Pierre Dittgen committed
165
    report = copy.deepcopy(validata_core_report)
166
167
168
169
170
171
172
173
174

    # One table is enough
    del report['table-count']
    report['table'] = report['tables'][0]
    del report['tables']
    del report['table']['error-count']
    del report['table']['time']
    del report['table']['valid']
    del report['valid']
175
176
177
    # use _ instead of - to ease information picking in jinja2 template
    report['table']['row_count'] = report['table']['row-count']

Pierre Dittgen's avatar
Pierre Dittgen committed
178
179
180
181
    # Handy col_count info
    headers = report['table'].get('headers', [])
    report['table']['col_count'] = len(headers)

Pierre Dittgen's avatar
Pierre Dittgen committed
182
    # Computes column info
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
183
184
    fields_dict = {f['name']: (f.get('title', 'titre non défini'), f.get('description', ''))
                   for f in schema_dict.get('fields', [])}
Pierre Dittgen's avatar
Pierre Dittgen committed
185
186
187
    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]
188

189
    # Provide better (french) messages
Pierre Dittgen's avatar
Pierre Dittgen committed
190
191
    errors = improve_errors(report['table']['errors'])
    del report['table']['errors']
192

193
194
195
196
197
198
199
    # Count errors
    report['error_count'] = len(errors)
    del report['error-count']

    # Then group them in 2 groups : structure and body
    report['table']['errors'] = {'structure': [], 'body': []}
    for err in errors:
200
        if err['tag'] == 'structure':
201
202
203
204
            report['table']['errors']['structure'].append(err)
        else:
            report['table']['errors']['body'].append(err)

Pierre Dittgen's avatar
Pierre Dittgen committed
205
    # Checks if there are structure errors different to invalid-column-delimiter
Pierre Dittgen's avatar
Pierre Dittgen committed
206
207
208
    structure_errors = report['table']['errors']['structure']
    report['table']['do_display_body_errors'] = len(structure_errors) == 0 or \
        all(err['code'] == 'invalid-column-delimiter' for err in structure_errors)
209

Pierre Dittgen's avatar
Pierre Dittgen committed
210
211
212
213
214
215
216
217
    # Checks if a column comparison is needed
    header_errors = ('missing-headers', 'extra-headers', 'wrong-headers-order')
    structure_errors = [{**err, 'in_column_comp': err['code'] in header_errors} for err in structure_errors]
    report['table']['errors']['structure'] = structure_errors
    column_comparison_needed = any(err['in_column_comp'] == True for err in structure_errors)
    column_comparison_table = []
    if column_comparison_needed:
        column_comparison_table = []
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
218
        field_names = [f['name'] for f in schema_dict.get('fields', [])]
219
        has_case_errors = False
Pierre Dittgen's avatar
Pierre Dittgen committed
220
221
        for t in itertools.zip_longest(headers, field_names, fillvalue=''):
            status = 'ok' if t[0] == t[1] else 'ko'
222
223
            if not has_case_errors and status == 'ko' and t[0].lower() == t[1].lower():
                has_case_errors = True
Pierre Dittgen's avatar
Pierre Dittgen committed
224
            column_comparison_table.append((*t, status))
225
226
227
228
229
        info = {}
        info['table'] = column_comparison_table
        info['has_missing'] = len(headers) < len(field_names)
        info['has_case_errors'] = has_case_errors
        report['table']['column_comparison_info'] = info
Pierre Dittgen's avatar
Pierre Dittgen committed
230
231
    report['table']['column_comparison_needed'] = column_comparison_needed

Pierre Dittgen's avatar
Pierre Dittgen committed
232
233
234
235
    # Group body errors by row id
    rows = []
    current_row_id = 0
    for err in report['table']['errors']['body']:
236
237
        if not 'row-number' in err:
            print('ERR', err)
Pierre Dittgen's avatar
Pierre Dittgen committed
238
239
240
241
242
243
244
245
246
247
248
249
250
        row_id = err['row-number']
        del err['row-number']
        del err['context']
        if row_id != current_row_id:
            current_row_id = row_id
            rows.append({'row_id': current_row_id, 'errors': {}})

        column_id = err.get('column-number')
        if column_id is not None:
            del err['column-number']
            rows[-1]['errors'][column_id] = err
        else:
            rows[-1]['errors']['row'] = err
Pierre Dittgen's avatar
Pierre Dittgen committed
251
    report['table']['errors']['body_by_rows'] = rows
Pierre Dittgen's avatar
Pierre Dittgen committed
252

253
254
255
256
257
258
259
260
261
    # Sort by error names in statistics
    stats = report['table']['error-stats']
    code_title_map = messages.ERROR_MESSAGE_DEFAULT_TITLE
    for key in ('structure-errors', 'value-errors'):
        # convert dict into tuples with french title instead of error code
        # and sorts by title
        stats[key]['count-by-code'] = sorted([(code_title_map.get(k, k), v) for k, v in stats[key]['count-by-code'].items()],
                                             key=itemgetter(0))

Pierre Dittgen's avatar
Pierre Dittgen committed
262
263
264
    return report


Pierre Dittgen's avatar
Pierre Dittgen committed
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
def compute_badge_message_and_color(badge):
    """Computes message and color from badge information"""
    structure = badge['structure']
    body = badge.get('body')

    # Bad structure, stop here
    if structure == 'KO':
        return (
            'structure invalide', 'red')

    # No body error
    if body == 'OK':
        return ('structure invalide', 'orange') if structure == 'WARN' else ('valide', 'green')

    # else compute quality ratio percent
Christophe Benz's avatar
Christophe Benz committed
280
    p = (1 - badge['error-ratio']) * 100.0
Pierre Dittgen's avatar
Pierre Dittgen committed
281
282
283
284
285
286
287
288
    msg = 'cellules valides : {:.1f}%'.format(p)
    return (msg, 'red') if body == 'KO' else (msg, 'orange')


def get_badge_url_and_message(badge):
    """Gets badge url from badge information"""

    msg, color = compute_badge_message_and_color(badge)
289
290
    return ('{}static/v1.svg?label=Validata&message={}&color={}'.format(
        config.SHIELDS_IO_BASE_URL, quote_plus(msg), color), msg)
Pierre Dittgen's avatar
Pierre Dittgen committed
291
292


293
def validate(schema_instance: SchemaInstance, source: ValidataSource):
294
295
    """ Validate source and display report """

296
    # Validation is done through http call to validata-api
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
297
298
299
300
    if config.API_VALIDATE_ENDPOINT is None:
        flash_error("No Validate endpoint defined :-(")
        return redirect(url_for("custom_validator"))
    api_url = config.API_VALIDATE_ENDPOINT
301
302

    # Useful to receive response as JSON
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
303
304
    headers = {"Accept": "application/json"}

Pierre Dittgen's avatar
Pierre Dittgen committed
305
    try:
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
306
307
        if source.is_url():
            params = {
308
                "schema": schema_instance.url,
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
309
310
311
312
313
314
                "url": source.get_url(),
            }
            req = requests.get(api_url, params=params, headers=headers)

        else:
            files = {'file': (source.name, io.BytesIO(source.source))}
315
            data = {"schema": schema_instance.url}
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
316
317
            req = requests.post(api_url, data=data, files=files, headers=headers)

Pierre Dittgen's avatar
Pierre Dittgen committed
318
319
320
321
322
323
324
        # 400
        if req.status_code == 400:
            json_response = req.json()
            flash_error("Une erreur est survenue durant la validation: {}"
                        .format(json_response.get('message')))
            return redirect(url_for("home"))

Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
325
        if not req.ok:
Pierre Dittgen's avatar
Pierre Dittgen committed
326
            flash_error("Un erreur s'est produite côté serveur :-(")
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
327
            return redirect(url_for("home"))
328

Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
329
330
331
        json_response = req.json()
        validata_core_report = json_response['report']
        schema_dict = json_response['schema']
332

Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
333
334
335
336
    except requests.ConnectionError as err:
        logging.exception(err)
        flash_error(str(err))
        return redirect(url_for('home'))
337

Pierre Dittgen's avatar
Pierre Dittgen committed
338
    # Computes badge from report and badge configuration
339
    badge = compute_badge(validata_core_report, config.BADGE_CONFIG)
Pierre Dittgen's avatar
Pierre Dittgen committed
340
341
    badge_url, badge_msg = get_badge_url_and_message(badge)

342
343
344
345
346
347
    source_errors = [err for err in validata_core_report['tables'][0]['errors'] if err['code'] == 'source-error']
    if source_errors:
        err = source_errors[0]
        msg = "l'encodage du fichier est invalide. Veuillez le corriger" if 'charmap' in err[
            'message'] else err['message']
        flash_error('Erreur de source : {}'.format(msg))
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
348
        return redirect(url_for('custom_validator'))
349

350
    source_data = extract_source_data(source)
351

Pierre Dittgen's avatar
Pierre Dittgen committed
352
    # handle report date
353
    report_datetime = datetime.fromisoformat(validata_core_report['date']).astimezone()
354

Pierre Dittgen's avatar
Pierre Dittgen committed
355
    # Enhance validata_core_report
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
356
    validata_report = create_validata_ui_report(validata_core_report, schema_dict)
Pierre Dittgen's avatar
Pierre Dittgen committed
357

Pierre Dittgen's avatar
Pierre Dittgen committed
358
    # Display report to the user
359
    validator_form_url = compute_validation_form_url(schema_instance)
Pierre Dittgen's avatar
Pierre Dittgen committed
360
    schema_info, validator_title = compute_schema_info(schema_instance.spec, schema_instance.url)
361
    pdf_report_url = url_for('pdf_report')+'?'+urlencode(schema_instance.request_parameters())
362
    return render_template('validation_report.html', title='Rapport de validation',
363
364
                           schema_info=schema_info, report=validata_report,
                           pdf_report_url=pdf_report_url,
365
                           validation_date=report_datetime.strftime('le %d/%m/%Y à %Hh%M'),
366
                           source=source, source_type=source.type, source_data=source_data,
Pierre Dittgen's avatar
Pierre Dittgen committed
367
                           schema_current_version=schema_instance.ref,
368
                           print_mode=request.args.get('print', 'false') == 'true',
Pierre Dittgen's avatar
Pierre Dittgen committed
369
                           badge_url=badge_url, badge_msg=badge_msg,
370
                           report_str=json.dumps(validata_report, sort_keys=True, indent=2),
Pierre Dittgen's avatar
Pierre Dittgen committed
371
                           breadcrumbs=[{'url': url_for('home'), 'title': 'Accueil'},
Pierre Dittgen's avatar
Pierre Dittgen committed
372
                                        {'url': validator_form_url, 'title': validator_title},
Pierre Dittgen's avatar
Pierre Dittgen committed
373
                                        ])
374
375


376
377
def bytes_data(f):
    """ Gets bytes data from Werkzeug FileStorage instance """
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
378
    iob = io.BytesIO()
379
380
381
382
383
    f.save(iob)
    iob.seek(0)
    return iob.getvalue()


384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
def hydrate_ui_config(ui_config, table_schema_catalog):

    hydrated_ui_config = ui_config.copy()

    table_schema_ref_list = []
    for name, ref in sorted(table_schema_catalog.references.items(), key=itemgetter(0)):
        table_schema = ref.get_table_schema()
        info = {
            "name": name,
            **{k: v for k, v in table_schema.descriptor.items() if k != 'fields'}
        }
        table_schema_ref_list.append(info)

    # TODO: change this hard-coded affectation
    hydrated_ui_config['sections'][0]['catalog'] = table_schema_ref_list

    return hydrated_ui_config

402
403
404
405
406
407
# Routes


@app.route('/')
def home():
    """ Home page """
Christophe Benz's avatar
Christophe Benz committed
408
    flash_warning('Ce service est fourni en mode beta - certains problèmes peuvent subsister - nous mettons tout en œuvre pour améliorer son fonctionnement en continu.')
409
    return render_template('home.html', title='Accueil', config=hydrate_ui_config(ui_config, table_schema_catalog))
410
411


Pierre Dittgen's avatar
Pierre Dittgen committed
412
413
@app.route('/pdf')
def pdf_report():
414
    """PDF report generation"""
415
    err_prefix = 'Erreur de génération du rapport PDF'
416
417
418

    url_param = request.args.get('url')
    if not url_param:
419
        flash_error(err_prefix + ': URL non fournie')
Pierre Dittgen's avatar
Pierre Dittgen committed
420
        return redirect(url_for('home'))
421

422
423
424
    schema_instance = SchemaInstance.from_parameters(request.args, table_schema_catalog)
    if schema_instance is None:
        flash_error(err_prefix + ': Information de schema non fournie')
Pierre Dittgen's avatar
Pierre Dittgen committed
425
426
        return redirect(url_for('home'))

427
428
429
430
431
432
433
434
435
436
437
    # Compute pdf url report
    base_url = url_for('custom_validator', _external=True)
    parameter_dict = {
        'input': 'url',
        'print': 'true',
        'url': url_param,
        **schema_instance.request_parameters()
    }
    validation_url = base_url + '?' + urlencode(parameter_dict)

    # Create temp file to save validation report
Pierre Dittgen's avatar
Pierre Dittgen committed
438
    with tempfile.NamedTemporaryFile(prefix='validata_{}_report_'.format(datetime.now().timestamp()), suffix='.pdf') as tmpfile:
Christophe Benz's avatar
Christophe Benz committed
439
        tmp_pdf_report = Path(tmpfile.name)
440

441
    # Use chromium headless to generate PDF from validation report page
442
443
444
445
    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:
446
447
        flash_error(err_prefix)
        log.error("Command %r returned an error: %r", cmd, result.stdout.decode('utf-8'))
448
449
        if tmp_pdf_report.exists():
            tmp_pdf_report.unlink()
Pierre Dittgen's avatar
Pierre Dittgen committed
450
        return redirect(url_for('home'))
451

452
    # Send PDF report
Pierre Dittgen's avatar
Pierre Dittgen committed
453
    pdf_filename = 'Rapport de validation {}.pdf'.format(datetime.now().strftime('%d-%m-%Y %Hh%M'))
Christophe Benz's avatar
Christophe Benz committed
454
    response = make_response(tmp_pdf_report.read_bytes())
Pierre Dittgen's avatar
Pierre Dittgen committed
455
    response.headers.set('Content-disposition', 'attachment', filename=pdf_filename)
456
457
458
459
460
461
462
463
    response.headers.set('Content-type', 'application/pdf')
    response.headers.set('Content-length', tmp_pdf_report.stat().st_size)

    tmp_pdf_report.unlink()

    return response


Pierre Dittgen's avatar
Pierre Dittgen committed
464
def compute_schema_info(table_schema: tableschema.Schema, schema_url):
Pierre Dittgen's avatar
Pierre Dittgen committed
465
    """Factor code for validator form page"""
466
467

    schema_info = {k: v for k, v in table_schema.descriptor.items() if k != 'fields'}
Pierre Dittgen's avatar
Pierre Dittgen committed
468
    schema_info['url'] = schema_url
469
470
471
472
473
474
475
476
477
478
    title = "Schéma « {} »".format(schema_info.get('title'))
    return schema_info, title


def compute_validation_form_url(schema_instance: SchemaInstance):
    """Computes validation form url with schema URL parameter"""
    url = url_for('custom_validator')
    param_list = ['{}={}'.format(k, quote_plus(v))
                  for k, v in schema_instance.request_parameters().items()]
    return "{}?{}".format(url, '&'.join(param_list))
Pierre Dittgen's avatar
Pierre Dittgen committed
479
480


481
@app.route('/table_schema', methods=['GET', 'POST'])
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
482
def custom_validator():
483
    """Validator form"""
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
484

485
    # Check that validata-api URL is set
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
486
487
488
489
490
491
    if config.API_VALIDATE_ENDPOINT is None:
        flash_error("URL de connexion à l'API non indiquée :-(")
        return redirect(url_for('home'))

    if request.method == 'GET':

492
493
        # input is a hidden form parameter to know
        # if this is the initial page display or if the validation has been asked for
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
494
        input_param = request.args.get('input')
495
496

        # url of resource to be validated
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
497
498
        url_param = request.args.get("url")

499
500
501
        schema_instance = SchemaInstance.from_parameters(request.args, table_schema_catalog)
        if schema_instance is None:
            flash_error("Aucun schéma passé en paramètre")
Pierre Dittgen's avatar
Pierre Dittgen committed
502
503
            return redirect(url_for('home'))

Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
504
505
        # First form display
        if input_param is None:
Pierre Dittgen's avatar
Pierre Dittgen committed
506
507
508

            schema_versions = schema_instance.versions
            schema_info, title = compute_schema_info(schema_instance.spec, schema_instance.url)
Pierre Dittgen's avatar
Pierre Dittgen committed
509
            return render_template('validation_form.html', title=title,
Pierre Dittgen's avatar
Pierre Dittgen committed
510
511
                                   schema_info=schema_info, schema_versions=schema_versions,
                                   schema_current_version=schema_instance.ref,
512
                                   schema_params=schema_instance.request_parameters(),
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
513
514
515
516
517
518
                                   breadcrumbs=[{'url': url_for('home'), 'title': 'Accueil'}, ])

        # Process URL
        else:
            if url_param is None or url_param == '':
                flash_error("Vous n'avez pas indiqué d'url à valider")
519
                return redirect(compute_validation_form_url(schema_instance))
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
520
            try:
521
                return validate(schema_instance, ValidataSource('url', url_param, url_param))
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
522
523
524
            except tabulator.exceptions.FormatError as e:
                flash_error('Erreur : Format de ressource non supporté')
                log.info(e)
525
                return redirect(compute_validation_form_url(schema_instance))
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
526
527
528
            except tabulator.exceptions.HTTPError as e:
                flash_error('Erreur : impossible d\'accéder au fichier source en ligne')
                log.info(e)
529
                return redirect(compute_validation_form_url(schema_instance))
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
530
531

    else:  # POST
532
533
534

        schema_instance = SchemaInstance.from_parameters(request.form, table_schema_catalog)
        if schema_instance is None:
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
535
            flash_error('Aucun schéma défini')
Pierre Dittgen's avatar
Pierre Dittgen committed
536
537
538
539
540
            return redirect(url_for('home'))

        input_param = request.form.get('input')
        if input_param is None:
            flash_error("Vous n'avez pas indiqué de fichier à valider")
541
            return redirect(compute_validation_form_url(schema_instance))
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
542
543
544
545
546
547

        # File validation
        if input_param == 'file':
            f = request.files.get('file')
            if f is None:
                flash_warning("Vous n'avez pas indiqué de fichier à valider")
548
                return redirect(compute_validation_form_url(schema_instance))
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
549
550

            b_content = bytes_data(f)
551
            return validate(schema_instance, ValidataSource('file', f.filename, b_content))
Pierre Dittgen's avatar
wip    
Pierre Dittgen committed
552
553

        return 'Bizarre, vous avez dit bizarre ?'