views.py 22.6 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
8
import logging
9
import subprocess
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
import tabulator
19
from backports.datetime_fromisoformat import MonkeyPatch
20
from commonmark import commonmark
21
from flask import abort, make_response, redirect, render_template, request, url_for
Pierre Dittgen's avatar
Pierre Dittgen committed
22
from validata_core import messages
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
23

24 25 26
from opendataschema import GitSchemaReference

from . import app, config, schema_catalog_map, tableschema_from_url
27
from .ui_util import flash_error, flash_warning
28
from .validata_util import UploadedFileValidataResource, URLValidataResource, ValidataResource
29

30 31
MonkeyPatch.patch_fromisoformat()

32 33
log = logging.getLogger(__name__)

34

35 36 37 38 39 40 41 42 43 44 45 46 47 48
class SchemaInstance:
    """Handy class to handle schema information"""

    def __init__(self, parameter_dict, schema_catalog_map):
        """Initializes schema instance from requests dict and tableschema catalog (for name ref)"""
        self.section_name = None
        self.section_title = None
        self.name = None
        self.url = None
        self.ref = None
        self.reference = None
        self.doc_url = None
        self.branches = None
        self.tags = None
49 50

        # From schema_url
Christophe Benz's avatar
Christophe Benz committed
51
        if parameter_dict.get("schema_url"):
52 53
            self.url = parameter_dict["schema_url"]
            self.section_title = "Autre schéma"
54 55

        # from schema_name (and schema_ref)
Christophe Benz's avatar
Christophe Benz committed
56
        elif parameter_dict.get('schema_name'):
57 58
            self.schema_and_section_name = parameter_dict['schema_name']
            self.ref = parameter_dict.get('schema_ref')
59

60
            # Check schema name
61
            chunks = self.schema_and_section_name.split('.')
62
            if len(chunks) != 2:
63
                abort(400, "Paramètre 'schema_name' invalide")
64

65 66
            self.section_name, self.name = chunks
            self.section_title = self.find_section_title(self.section_name)
67 68

            # Look for schema catalog first
69
            table_schema_catalog = schema_catalog_map.get(self.section_name)
70
            if table_schema_catalog is None:
71 72 73 74 75 76 77
                abort(400, "Section '{}' non trouvée dans la configuration".format(self.section_name))

            schema_reference = table_schema_catalog.reference_by_name.get(self.name)
            if schema_reference is None:
                abort(400, "Schéma '{}' non trouvé dans le catalogue de la section '{}'".format(self.name, self.section_name))

            if isinstance(schema_reference, GitSchemaReference):
78
                latest_tag = schema_reference.get_latest_tag()
79
                if self.ref is None:
80
                    schema_ref = latest_tag or schema_reference.get_default_branch()
81 82 83 84 85 86 87 88
                    abort(redirect(compute_validation_form_url({
                        'schema_name': self.schema_and_section_name,
                        'schema_ref': schema_ref.name
                    })))
                self.tags = list(schema_reference.iter_tags())
                tag_names = [tag.name for tag in self.tags]
                self.branches = [branch for branch in schema_reference.iter_branches()
                                 if branch.name not in tag_names]
89 90 91
                self.doc_url = schema_reference.doc_url \
                    if self.ref == latest_tag.name \
                    else schema_reference.get_project_url(self.ref)
92

93
            self.url = schema_reference.get_schema_url(ref=self.ref)
94 95

        else:
96
            abort(400, "L'un des paramètres est nécessaire : 'schema_name', 'schema_url'")
97

98 99 100 101 102 103 104 105 106 107
        try:
            self.schema = tableschema_from_url(self.url)
        except json.JSONDecodeError as e:
            log.exception(e)
            flash_error("Format de schéma non reconnu")
            abort(redirect(url_for('home')))
        except Exception as e:
            log.exception(e)
            flash_error("Erreur lors de l'obtention du schéma")
            abort(redirect(url_for('home')))
108 109 110 111

    def request_parameters(self):
        if self.name:
            return {
112
                'schema_name': self.schema_and_section_name,
113 114 115 116 117 118
                'schema_ref': '' if self.ref is None else self.ref
            }
        return {
            'schema_url': self.url
        }

119
    def find_section_title(self, section_name):
Christophe Benz's avatar
Christophe Benz committed
120 121 122 123 124 125
        if config.HOMEPAGE_CONFIG:
            for section in config.HOMEPAGE_CONFIG['sections']:
                if section["name"] == section_name:
                    return section.get("title")
        return None

126

127
def extract_source_data(source: ValidataResource, preview_rows_nb=5):
128
    """ Computes table preview """
129 130 131

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

134 135
    header = None
    rows = []
Pierre Dittgen's avatar
Pierre Dittgen committed
136
    nb_rows = 0
137

138
    tabulator_source, tabulator_options = source.build_tabulator_stream_args()
Pierre Dittgen's avatar
Pierre Dittgen committed
139
    with tabulator.Stream(tabulator_source, **tabulator_options) as stream:
140 141
        for row in stream:
            if header is None:
142
                header = ['' if v is None else v for v in row]
143
            else:
144
                rows.append(list(map(stringify, row)))
Pierre Dittgen's avatar
Pierre Dittgen committed
145
                nb_rows += 1
146
    preview_rows_nb = min(preview_rows_nb, nb_rows)
147 148
    return {'header': header,
            'rows_nb': nb_rows,
149 150 151
            'data_rows': rows,
            'preview_rows_nb': preview_rows_nb,
            'preview_rows': rows[:preview_rows_nb]}
152 153


Pierre Dittgen's avatar
Pierre Dittgen committed
154 155
def improve_errors(errors):
    """Add context to errors, converts markdown content to HTML"""
156

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

Pierre Dittgen's avatar
Pierre Dittgen committed
160 161 162 163
        # 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
164

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

Pierre Dittgen's avatar
Pierre Dittgen committed
167 168 169
        # Set default title if no title
        if not 'title' in err:
            update_keys['title'] = '[{}]'.format(err['code'])
Pierre Dittgen's avatar
Pierre Dittgen committed
170

Pierre Dittgen's avatar
Pierre Dittgen committed
171 172 173 174
        # 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
175

Pierre Dittgen's avatar
Pierre Dittgen committed
176 177 178
        # 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
179

Pierre Dittgen's avatar
Pierre Dittgen committed
180 181 182
        # Message content
        md_content = '*content soon available*' if not 'content' in err else err['content']
        update_keys['content'] = commonmark(md_content)
183

Pierre Dittgen's avatar
Pierre Dittgen committed
184
        return {**err, **update_keys}
185

Pierre Dittgen's avatar
Pierre Dittgen committed
186
    return list(map(improve_err, errors))
187 188


Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
189
def create_validata_ui_report(validata_core_report, schema_dict):
190 191 192 193 194 195
    """ 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"
196
        - error messages are improved
197
    """
Pierre Dittgen's avatar
Pierre Dittgen committed
198
    report = copy.deepcopy(validata_core_report)
199 200 201 202 203 204 205 206 207

    # 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']
208 209 210
    # 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
211 212 213 214
    # Handy col_count info
    headers = report['table'].get('headers', [])
    report['table']['col_count'] = len(headers)

Pierre Dittgen's avatar
Pierre Dittgen committed
215
    # Computes column info
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
216 217
    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
218 219 220
    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]
221

222
    # Provide better (french) messages
Pierre Dittgen's avatar
Pierre Dittgen committed
223 224
    errors = improve_errors(report['table']['errors'])
    del report['table']['errors']
225

226 227 228 229 230 231 232
    # 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:
233
        if err['tag'] == 'structure':
234 235 236 237
            report['table']['errors']['structure'].append(err)
        else:
            report['table']['errors']['body'].append(err)

Pierre Dittgen's avatar
Pierre Dittgen committed
238
    # Checks if there are structure errors different to invalid-column-delimiter
Pierre Dittgen's avatar
Pierre Dittgen committed
239 240 241
    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)
242

Pierre Dittgen's avatar
Pierre Dittgen committed
243 244 245 246 247 248 249 250
    # 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
251
        field_names = [f['name'] for f in schema_dict.get('fields', [])]
252
        has_case_errors = False
Pierre Dittgen's avatar
Pierre Dittgen committed
253 254
        for t in itertools.zip_longest(headers, field_names, fillvalue=''):
            status = 'ok' if t[0] == t[1] else 'ko'
255 256
            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
257
            column_comparison_table.append((*t, status))
258 259 260 261 262
        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
263 264
    report['table']['column_comparison_needed'] = column_comparison_needed

Pierre Dittgen's avatar
Pierre Dittgen committed
265 266 267 268
    # Group body errors by row id
    rows = []
    current_row_id = 0
    for err in report['table']['errors']['body']:
269 270
        if not 'row-number' in err:
            print('ERR', err)
Pierre Dittgen's avatar
Pierre Dittgen committed
271 272 273 274 275 276 277 278 279 280 281 282 283
        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
284
    report['table']['errors']['body_by_rows'] = rows
Pierre Dittgen's avatar
Pierre Dittgen committed
285

286 287 288 289 290 291
    # 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
Christophe Benz's avatar
Christophe Benz committed
292 293
        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))
294

Pierre Dittgen's avatar
Pierre Dittgen committed
295 296 297
    return report


Pierre Dittgen's avatar
Pierre Dittgen committed
298 299 300 301 302 303 304
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':
305
        return ('structure invalide', 'red')
Pierre Dittgen's avatar
Pierre Dittgen committed
306 307 308 309 310 311

    # 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
312
    p = (1 - badge['error-ratio']) * 100.0
Pierre Dittgen's avatar
Pierre Dittgen committed
313 314 315 316 317 318 319 320
    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)
321 322
    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
323 324


325
def validate(schema_instance: SchemaInstance, source: ValidataResource):
326 327
    """ Validate source and display report """

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

331
    try:
332
        if source.type == 'url':
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
333
            params = {
334
                "schema": schema_instance.url,
335
                "url": source.url,
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
336
            }
337
            response = requests.get(config.API_VALIDATE_ENDPOINT, params=params, headers=headers)
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
338
        else:
339
            files = {'file': (source.filename, source.build_reader())}
340
            data = {"schema": schema_instance.url}
341
            response = requests.post(config.API_VALIDATE_ENDPOINT, data=data, files=files, headers=headers)
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
342 343
    except requests.ConnectionError as err:
        logging.exception(err)
Christophe Benz's avatar
Christophe Benz committed
344
        flash_error("Erreur technique lors de la validation")
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
345
        return redirect(url_for('home'))
346

347 348 349
    if not response.ok:
        flash_error("Erreur technique lors de la validation")
        return redirect(compute_validation_form_url(schema_instance.request_parameters()))
350

351
    json_response = response.json()
352
    validata_core_report = json_response['report']
Pierre Dittgen's avatar
Pierre Dittgen committed
353
    badge_info = json_response.get('badge')
354

Pierre Dittgen's avatar
Pierre Dittgen committed
355
    # Computes badge from report and badge configuration
Pierre Dittgen's avatar
Pierre Dittgen committed
356 357 358 359
    badge_url, badge_msg = None, None
    display_badge = badge_info and config.SHIELDS_IO_BASE_URL
    if display_badge:
        badge_url, badge_msg = get_badge_url_and_message(badge_info)
Pierre Dittgen's avatar
Pierre Dittgen committed
360

361 362 363 364 365
    source_errors = [
        err
        for err in validata_core_report['tables'][0]['errors']
        if err['code'] in {'source-error', 'unknown-csv-dialect'}
    ]
366 367 368 369 370
    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
371
        return redirect(url_for('custom_validator'))
372

373
    source_data = extract_source_data(source)
374

Pierre Dittgen's avatar
Pierre Dittgen committed
375
    # handle report date
376
    report_datetime = datetime.fromisoformat(validata_core_report['date']).astimezone()
377

Pierre Dittgen's avatar
Pierre Dittgen committed
378
    # Enhance validata_core_report
Pierre Dittgen's avatar
Pierre Dittgen committed
379
    validata_report = create_validata_ui_report(validata_core_report, schema_instance.schema.descriptor)
Pierre Dittgen's avatar
Pierre Dittgen committed
380

Pierre Dittgen's avatar
Pierre Dittgen committed
381
    # Display report to the user
382
    validator_form_url = compute_validation_form_url(schema_instance.request_parameters())
Christophe Benz's avatar
Christophe Benz committed
383
    schema_info = compute_schema_info(schema_instance.schema, schema_instance.url)
384 385
    pdf_report_url = url_for('pdf_report') + '?' + urlencode(schema_instance.request_parameters())

Christophe Benz's avatar
Christophe Benz committed
386
    return render_template('validation_report.html',
387 388 389 390 391 392 393 394 395
                           badge_msg=badge_msg,
                           badge_url=badge_url,
                           breadcrumbs=[
                               {'title': 'Accueil', 'url': url_for('home')},
                               {'title': schema_instance.section_title},
                               {'title': schema_info['title'], 'url': validator_form_url},
                               {'title': 'Rapport de validation'},
                           ],
                           display_badge=display_badge,
396
                           doc_url=schema_instance.doc_url,
397
                           pdf_report_url=pdf_report_url,
398
                           print_mode=request.args.get('print', 'false') == 'true',
399
                           report_str=json.dumps(validata_report, sort_keys=True, indent=2),
400 401 402
                           report=validata_report,
                           schema_current_version=schema_instance.ref,
                           schema_info=schema_info,
Christophe Benz's avatar
Christophe Benz committed
403
                           section_title=schema_instance.section_title,
404 405 406 407
                           source_data=source_data,
                           source=source,
                           validation_date=report_datetime.strftime('le %d/%m/%Y à %Hh%M'),
                           )
408 409


410 411
def bytes_data(f):
    """ Gets bytes data from Werkzeug FileStorage instance """
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
412
    iob = io.BytesIO()
413 414 415 416 417
    f.save(iob)
    iob.seek(0)
    return iob.getvalue()


418
def homepage_config_with_schema_metadata(ui_config, schema_catalog_map):
419 420 421 422 423
    """Replace catalog url within ui_config by schema references
    containing schema metadata properties"""

    extended_ui_config = ui_config.copy()
    for section in extended_ui_config['sections']:
Christophe Benz's avatar
Christophe Benz committed
424 425
        section_name = section['name']
        if section_name not in schema_catalog_map:
426
            continue
Christophe Benz's avatar
Christophe Benz committed
427
        schema_catalog = schema_catalog_map[section_name]
428 429 430
        schema_list = []
        for ref in schema_catalog.references:
            # Loads default table schema for each schema reference
431
            table_schema = tableschema_from_url(ref.get_schema_url())
432
            schema_list.append({
Christophe Benz's avatar
Christophe Benz committed
433
                'name': '{}.{}'.format(section_name, ref.name),
434 435 436 437 438
                # Extracts title, description, ...
                **extract_schema_metadata(table_schema)
            })
        section['catalog'] = schema_list
    return extended_ui_config
439

440 441 442 443 444 445
# Routes


@app.route('/')
def home():
    """ Home page """
446
    home_config = homepage_config_with_schema_metadata(config.HOMEPAGE_CONFIG, schema_catalog_map)
Christophe Benz's avatar
Christophe Benz committed
447
    return render_template('home.html', config=home_config)
448 449


Pierre Dittgen's avatar
Pierre Dittgen committed
450 451
@app.route('/pdf')
def pdf_report():
452
    """PDF report generation"""
453
    err_prefix = 'Erreur de génération du rapport PDF'
454 455 456

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

460
    schema_instance = SchemaInstance(request.args, schema_catalog_map)
Pierre Dittgen's avatar
Pierre Dittgen committed
461

462 463 464 465 466 467 468 469 470 471 472
    # 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
473
    with tempfile.NamedTemporaryFile(prefix='validata_{}_report_'.format(datetime.now().timestamp()), suffix='.pdf') as tmpfile:
474
        tmp_pdf_report = Path(tmpfile.name)
475

476
    # Use chromium headless to generate PDF from validation report page
477 478 479 480
    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:
481 482
        flash_error(err_prefix)
        log.error("Command %r returned an error: %r", cmd, result.stdout.decode('utf-8'))
483 484
        if tmp_pdf_report.exists():
            tmp_pdf_report.unlink()
Pierre Dittgen's avatar
Pierre Dittgen committed
485
        return redirect(url_for('home'))
486

487
    # Send PDF report
Pierre Dittgen's avatar
Pierre Dittgen committed
488
    pdf_filename = 'Rapport de validation {}.pdf'.format(datetime.now().strftime('%d-%m-%Y %Hh%M'))
489
    response = make_response(tmp_pdf_report.read_bytes())
Pierre Dittgen's avatar
Pierre Dittgen committed
490
    response.headers.set('Content-disposition', 'attachment', filename=pdf_filename)
491 492 493 494 495 496 497 498
    response.headers.set('Content-type', 'application/pdf')
    response.headers.set('Content-length', tmp_pdf_report.stat().st_size)

    tmp_pdf_report.unlink()

    return response


499 500 501 502 503
def extract_schema_metadata(table_schema: tableschema.Schema):
    """Gets author, contibutor, version...metadata from schema header"""
    return {k: v for k, v in table_schema.descriptor.items() if k != 'fields'}


504
def compute_schema_info(table_schema: tableschema.Schema, schema_url):
Pierre Dittgen's avatar
Pierre Dittgen committed
505
    """Factor code for validator form page"""
506

507 508
    # Schema URL + schema metadata info
    schema_info = {
Christophe Benz's avatar
Christophe Benz committed
509 510 511
        'path': schema_url,
        # a "path" metadata property can be found in Table Schema, and we'd like it to override the `schema_url`
        # given by the user (in case schema was given by URL)
512 513
        **extract_schema_metadata(table_schema)
    }
Christophe Benz's avatar
Christophe Benz committed
514
    return schema_info
515 516


517
def compute_validation_form_url(request_parameters: dict):
518 519
    """Computes validation form url with schema URL parameter"""
    url = url_for('custom_validator')
520
    param_list = ['{}={}'.format(k, quote_plus(v)) for k, v in request_parameters.items()]
521
    return "{}?{}".format(url, '&'.join(param_list))
Pierre Dittgen's avatar
Pierre Dittgen committed
522 523


524
@app.route('/table-schema', methods=['GET', 'POST'])
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
525
def custom_validator():
526
    """Validator form"""
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
527 528 529

    if request.method == 'GET':

530 531
        # 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
532
        input_param = request.args.get('input')
533 534

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

537
        schema_instance = SchemaInstance(request.args, schema_catalog_map)
Pierre Dittgen's avatar
Pierre Dittgen committed
538

Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
539 540
        # First form display
        if input_param is None:
Christophe Benz's avatar
Christophe Benz committed
541 542
            schema_info = compute_schema_info(schema_instance.schema, schema_instance.url)
            return render_template('validation_form.html',
543
                                   branches=schema_instance.branches,
Christophe Benz's avatar
Christophe Benz committed
544 545 546 547
                                   breadcrumbs=[
                                       {'url': url_for('home'), 'title': 'Accueil'},
                                       {'title': schema_instance.section_title},
                                       {'title': schema_info['title']},
548 549 550 551 552 553 554 555
                                   ],
                                   doc_url=schema_instance.doc_url,
                                   schema_current_version=schema_instance.ref,
                                   schema_info=schema_info,
                                   schema_params=schema_instance.request_parameters(),
                                   section_title=schema_instance.section_title,
                                   tags=schema_instance.tags,
                                   )
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
556 557 558

        # Process URL
        else:
559
            if not url_param:
Christophe Benz's avatar
Christophe Benz committed
560
                flash_error("Vous n'avez pas indiqué d'URL à valider")
561 562 563 564 565 566
                return redirect(compute_validation_form_url(schema_instance.request_parameters()))
            return validate(schema_instance, URLValidataResource(url_param))

    elif request.method == 'POST':

        schema_instance = SchemaInstance(request.form, schema_catalog_map)
Pierre Dittgen's avatar
Pierre Dittgen committed
567 568 569 570

        input_param = request.form.get('input')
        if input_param is None:
            flash_error("Vous n'avez pas indiqué de fichier à valider")
571
            return redirect(compute_validation_form_url(schema_instance.request_parameters()))
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
572 573 574 575 576 577

        # 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")
578
                return redirect(compute_validation_form_url(schema_instance.request_parameters()))
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
579

580
            return validate(schema_instance, UploadedFileValidataResource(f.filename, bytes_data(f)))
Pierre Dittgen's avatar
wip  
Pierre Dittgen committed
581

582 583 584 585
        return 'Combinaison de paramètres non supportée', 400

    else:
        return "Method not allowed", 405