Projet

Général

Profil

0002-rewrite-file-validation-10444.patch

Benjamin Dauvergne, 31 mars 2016 15:19

Télécharger (49,1 ko)

Voir les différences:

Subject: [PATCH 2/4] rewrite file validation (#10444)

- metadata fields are now directly added to the form, as part of the
  FileWithPreviewWidget sub-widgets (it's a CompositeWidget)
- existing validated metadatas are proposed for prefilling
- if prefilled no file is uploadee, and a special upload class NoUpload is used
  instead of a PicklableUpload, it only contains the metadatas.
- prefilled fields are disabled,
- on upload of a new file all prefilled fields are emptied and re-enabled.
- modify XML import/export to encode metadata sub-field as JSON in exports
- add test on import/export of FileField with metadata
- rewritten tests around file validation
 tests/test_backoffice_pages.py            | 250 +++++++++++++++++++++++++-----
 tests/test_form_pages.py                  | 110 +++++++++----
 tests/test_formdata.py                    | 109 +++++++++++++
 tests/test_formdef_import.py              |  15 ++
 wcs/backoffice/management.py              |   2 +-
 wcs/fields.py                             |  24 ++-
 wcs/file_validation.py                    | 114 +++++++-------
 wcs/forms/common.py                       |  60 ++++---
 wcs/qommon/form.py                        |  83 ++++++++--
 wcs/qommon/static/css/qommon.css          |  10 ++
 wcs/qommon/static/js/qommon.fileupload.js |  45 +++++-
 11 files changed, 663 insertions(+), 159 deletions(-)
tests/test_backoffice_pages.py
15 15
from quixote import cleanup, get_publisher
16 16
from wcs.qommon import errors, sessions
17 17
from qommon.ident.password_accounts import PasswordAccount
18
from qommon.misc import json_loads
18 19
from wcs.qommon.http_request import HTTPRequest
19 20
from wcs.roles import Role
20 21
from wcs.workflows import (Workflow, CommentableWorkflowStatusItem,
......
1577 1578
    assert resp.body == 'foo(%s);' % menu_json_str
1578 1579
    assert resp.headers['content-type'] == 'application/javascript'
1579 1580

  
1581
def test_backoffice_file_field_fargo_no_metadata(pub, fargo_url):
1582
    document_type = {
1583
        'id': 'justificatif-de-domicile',
1584
        'fargo': True,
1585
        'mimetypes': ['application/pdf'],
1586
        'label': 'Justificatif de domicile',
1587
    }
1588
    user = create_user(pub, is_admin=True)
1589
    user.name_identifiers = ['12345']
1590
    user.store()
1591
    FormDef.wipe()
1592
    formdef = FormDef()
1593
    formdef.name = 'form title'
1594
    formdef.fields = [fields.FileField(
1595
        id='0', label='1st field', type='file',
1596
        document_type=document_type)]
1597
    formdef.store()
1598
    formdef.data_class().wipe()
1599
    upload = Upload('test.pdf', '%PDF-1.4', 'application/pdf')
1600
    digest = hashlib.sha256('%PDF-1.4').hexdigest()
1601
    app = login(get_app(pub))
1602
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1603
        resp = app.get('/form-title/')
1604
        assert fargo_get.call_count == 0
1605
    resp.forms[0]['f0$file'] = upload
1606
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1607
        resp = resp.forms[0].submit('submit')
1608
        assert fargo_get.call_count == 0
1609
    assert 'Check values then click submit.' in resp.body
1610
    resp = resp.forms[0].submit('submit').follow()
1611
    assert formdef.data_class().count() == 1
1612
    formdata = formdef.data_class().select()[0]
1613
    form_id = formdata.id
1614
    assert not hasattr(formdata.data['0'], 'metadata')
1615
    assert not '0_structured' in formdata.data
1616
    resp = app.get('/backoffice/management/form-title/%s/' % form_id)
1617
    assert not 'Validate' in resp.body
1618
    with mock.patch('wcs.file_validation.http_post_request') as http_post_request:
1619
        resp = app.get('/backoffice/management/form-title/%s/validate?field_id=0' % form_id)
1620
        assert http_post_request.call_count == 0
1621
    resp = resp.follow()
1622
    assert not 'Valid ' in resp.body
1623
    assert not 'Validate' in resp.body
1624

  
1625

  
1580 1626
def test_backoffice_file_field_validation(pub, fargo_url):
1627
    document_type = {
1628
        'id': 'justificatif-de-domicile',
1629
        'fargo': True,
1630
        'mimetypes': ['application/pdf'],
1631
        'label': 'Justificatif de domicile',
1632
        'metadata': [
1633
            {'label': 'Nom', 'name': 'nom'},
1634
            {'label': 'Prénom(s)', 'name': 'prenoms'},
1635
            {'label': 'Numéro', 'name': 'numero'},
1636
            {'label': 'Rue', 'name': 'rue'},
1637
            {'label': 'Code postal', 'name': 'code-postal'},
1638
            {'label': 'Ville', 'name': 'ville'},
1639
        ],
1640
    }
1641
    metadata = {
1642
        'nom': 'Doe',
1643
        'prenoms': 'John',
1644
        'numero': '169',
1645
        'rue': 'rue du château',
1646
        'code-postal': '75014',
1647
        'ville': 'PARIS',
1648
    }
1581 1649
    user = create_user(pub, is_admin=True)
1582 1650
    user.name_identifiers = ['12345']
1583 1651
    user.store()
......
1586 1654
    formdef.name = 'form title'
1587 1655
    formdef.fields = [fields.FileField(
1588 1656
        id='0', label='1st field', type='file',
1589
        document_type={
1590
            'id': 'justificatif-de-domicile',
1591
            'fargo': True,
1592
            'mimetypes': ['application/pdf'],
1593
            'label': 'PDF files',
1594
        })
1595
    ]
1657
        document_type=document_type)]
1596 1658
    formdef.store()
1597 1659
    formdef.data_class().wipe()
1598 1660
    upload = Upload('test.pdf', '%PDF-1.4', 'application/pdf')
1599 1661
    digest = hashlib.sha256('%PDF-1.4').hexdigest()
1600 1662
    app = login(get_app(pub))
1601
    resp = app.get('/form-title/')
1663
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1664
        fargo_get.return_value = {'result': 1, 'data': {'results': []}}
1665
        resp = app.get('/form-title/')
1666
        fargo_get.assert_called_once_with(
1667
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
1602 1668
    resp.forms[0]['f0$file'] = upload
1603
    resp = resp.forms[0].submit('submit')
1669
    for key in metadata:
1670
        resp.forms[0]['f0$%s' % key] = metadata[key]
1671
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1672
        fargo_get.return_value = {'result': 1, 'data': {'results': []}}
1673
        resp = resp.forms[0].submit('submit')
1674
        fargo_get.assert_called_once_with(
1675
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
1676
    for key in metadata:
1677
        assert 'value="%s"' % metadata[key] in resp.body
1604 1678
    assert 'Check values then click submit.' in resp.body
1605
    resp = resp.forms[0].submit('submit')
1606
    assert resp.status_int == 302
1679
    resp = resp.forms[0].submit('submit').follow()
1680
    for metadata_field in document_type['metadata']:
1681

  
1682
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['name']])
1683
        assert fragment in resp.body
1607 1684
    assert formdef.data_class().count() == 1
1608
    form_id = formdef.data_class().select()[0].id
1609
    with mock.patch('wcs.file_validation.http_get_page') as http_get_page:
1610
        json_response = json.dumps({
1611
            'err': 0,
1612
            'data': {
1613
                'type': 'justificatif-de-domicile',
1614
                'label': 'Justificatif de domicile',
1615
                'creator': 'Jean Bono',
1616
                'created': '2014-01-01T01:01:01',
1617
                'start': '2014-01-01T01:01:01',
1618
                'end': '2014-01-01T01:01:01',
1619
                'metadata': [{
1620
                        'name': 'code_postal',
1621
                        'label': 'Code postal',
1622
                        'value': '13400',
1623
                }],
1624
            },
1685
    formdata = formdef.data_class().select()[0]
1686
    form_id = formdata.id
1687
    assert formdata.data['0'].metadata == metadata
1688
    assert formdata.data['0_structured'] == metadata
1689
    resp = app.get('/backoffice/management/form-title/%s/' % form_id)
1690
    assert 'Validate' in resp.body
1691
    for metadata_field in document_type['metadata']:
1692

  
1693
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['name']])
1694
        assert fragment in resp.body
1695
    with mock.patch('wcs.file_validation.http_post_request') as http_post_request:
1696
        payload = {
1697
            'user_nameid': '12345',
1698
            'origin': 'example.net',
1699
            'creator': 'admin',
1700
            'content_hash': digest,
1701
        }
1702
        payload.update(metadata)
1703
        result = {
1704
            'result': 1,
1705
            'data': payload.copy()
1706
        }
1707
        result['data'].update({
1708
            'url': 'zob',
1709
            'created': '1970-01-01T10:10:10',
1710
            'start': '1970-01-01',
1711
            'end': '1978-01-01',
1712
            'display': 'John Doe, 169 rue du château, 75014 PARIS'
1625 1713
        })
1626
        http_get_page.return_value = None, 200, json_response, None
1627
        resp = app.get('/backoffice/management/form-title/%s/' % form_id)
1628
        http_get_page.assert_called_once_with('http://fargo.example.net/metadata/12345/%s/justificatif-de-domicile/' % digest)
1629
        assert 'class="value valid"' in resp.body
1630
        assert 'Justificatif de domicile validated by Jean Bono on 2014-01-01T01:01:01' in resp.body
1631
        assert 'Code postal: 13400' in resp.body
1632
        assert 'Valid from 2014-01-01T01:01:01 to 2014-01-01T01:01:01' in resp.body
1714
        http_post_request.return_value = None, 201, json.dumps(result), None
1715
        resp = app.get('/backoffice/management/form-title/%s/validate?field_id=0' % form_id)
1716
        assert http_post_request.call_count == 1
1717
        assert http_post_request.call_args[0][0] == 'http://fargo.example.net/api/validation/justificatif-de-domicile/'
1718
        assert json_loads(http_post_request.call_args[0][1]) == payload
1719
        assert http_post_request.call_args[1] == {'headers': {'Content-Type': 'application/json'}}
1720
    resp = resp.follow()
1721

  
1722
    assert 'Valid from 1970-01-01 to 1978-01-01' in resp.body
1723

  
1724

  
1725
def test_backoffice_file_validation_no_upload(pub, fargo_url):
1726
    document_type = {
1727
        'id': 'justificatif-de-domicile',
1728
        'fargo': True,
1729
        'mimetypes': ['application/pdf'],
1730
        'label': 'Justificatif de domicile',
1731
        'metadata': [
1732
            {'label': 'Nom', 'name': 'nom'},
1733
            {'label': 'Prénom(s)', 'name': 'prenoms'},
1734
            {'label': 'Numéro', 'name': 'numero'},
1735
            {'label': 'Rue', 'name': 'rue'},
1736
            {'label': 'Code postal', 'name': 'code-postal'},
1737
            {'label': 'Ville', 'name': 'ville'},
1738
        ],
1739
    }
1740
    metadata = {
1741
        'nom': 'Doe',
1742
        'prenoms': 'John',
1743
        'numero': '169',
1744
        'rue': 'rue du château',
1745
        'code-postal': '75014',
1746
        'ville': 'PARIS',
1747
    }
1748
    validation = {
1749
        'url': 'zob',
1750
        'creator': 'admin',
1751
        'created': '1970-01-01T10:10:10',
1752
        'start': '1970-01-01',
1753
        'end': '1978-01-01',
1754
        'display': 'John Doe, 169 rue du château, 75014 PARIS'
1755
    }
1756
    validation.update(metadata)
1757

  
1758
    user = create_user(pub, is_admin=True)
1759
    user.name_identifiers = ['12345']
1760
    user.store()
1761
    FormDef.wipe()
1762
    formdef = FormDef()
1763
    formdef.name = 'form title'
1764
    formdef.fields = [fields.FileField(
1765
        id='0', label='1st field', type='file',
1766
        document_type=document_type)]
1767
    formdef.store()
1768
    formdef.data_class().wipe()
1769
    app = login(get_app(pub))
1770
    return_value = {'result': 1, 'data': {'results': [validation]}}
1771
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1772
        fargo_get.return_value = return_value
1773
        resp = app.get('/form-title/')
1774
        fargo_get.assert_called_once_with(
1775
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
1776
        assert validation['display'] in resp.body
1777
    resp.forms[0]['f0$validation_url'] = 'zob'
1778
    for key in metadata:
1779
        resp.forms[0]['f0$%s' % key] = metadata[key]
1780
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get, \
1781
            mock.patch('wcs.file_validation.http_get_page') as http_get_page:
1782
        fargo_get.return_value = return_value
1783
        return_value = {
1784
            'result': 1,
1785
            'data': validation,
1786
        }
1787
        http_get_page.return_value = None, 200, json.dumps(return_value), None
1788
        resp = resp.forms[0].submit('submit')
1789
        fargo_get.assert_called_once_with(
1790
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
1791
        http_get_page.assert_called_with('zob')
1792
        for key in metadata:
1793
            assert 'value="%s"' % metadata[key] in resp.body
1794
        assert 'Check values then click submit.' in resp.body
1795
        resp = resp.forms[0].submit('submit').follow()
1796
    assert formdef.data_class().count() == 1
1797
    formdata = formdef.data_class().select()[0]
1798
    form_id = formdata.id
1799
    assert formdata.data['0'].metadata == validation
1800
    assert formdata.data['0_structured'] == validation
1801
    for metadata_field in document_type['metadata']:
1802

  
1803
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['name']])
1804
        assert fragment in resp.body
1805
    resp = app.get('/backoffice/management/form-title/%s/' % form_id)
1806
    assert not 'Validate' in resp.body
1807
    for metadata_field in document_type['metadata']:
1808

  
1809
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['name']])
1810
        assert fragment in resp.body
1811
    assert 'Valid from 1970-01-01 to 1978-01-01' in resp.body
1812

  
1633 1813

  
1634 1814
def test_360_user_view(pub):
1635 1815
    if not pub.is_using_postgresql():
tests/test_form_pages.py
1
# -*- coding: utf-8 -*-
1 2
import json
2 3
import pytest
3 4
import hashlib
......
2346 2347
    assert formdef.data_class().select()[0].data['1'] == 'foobar3'
2347 2348

  
2348 2349
def test_file_field_validation(pub, fargo_url):
2350
    document_type = {
2351
        'id': 'justificatif-de-domicile',
2352
        'fargo': True,
2353
        'label': 'Justificatif de domicile',
2354
        'metadata': [
2355
            {'label': 'Nom', 'name': 'nom'},
2356
            {'label': 'Prénom(s)', 'name': 'prenoms'},
2357
            {'label': 'Numéro', 'name': 'numero'},
2358
            {'label': 'Rue', 'name': 'rue'},
2359
            {'label': 'Code postal', 'name': 'code-postal'},
2360
            {'label': 'Ville', 'name': 'ville'},
2361
        ],
2362
    }
2363
    metadata = {
2364
        'nom': 'Doe',
2365
        'prenoms': 'John',
2366
        'numero': '169',
2367
        'rue': 'rue du château',
2368
        'code-postal': '75014',
2369
        'ville': 'PARIS',
2370
    }
2349 2371
    user = create_user(pub)
2350 2372
    user.name_identifiers = ['12345']
2351 2373
    user.store()
......
2354 2376
    formdef.name = 'form title'
2355 2377
    formdef.fields = [fields.FileField(
2356 2378
        id='0', label='1st field', type='file',
2357
        document_type={
2358
            'id': 'justificatif-de-domicile',
2359
            'fargo': True,
2360
            'mimetypes': ['application/pdf'],
2361
            'label': 'PDF files',
2362
        })
2379
        document_type=document_type)
2363 2380
    ]
2364 2381
    formdef.store()
2365 2382
    formdef.data_class().wipe()
2366 2383
    upload = Upload('test.pdf', '%PDF-1.4', 'application/pdf')
2367
    digest = hashlib.sha256('%PDF-1.4').hexdigest()
2368 2384
    app = login(get_app(pub), username='foo', password='foo')
2369
    resp = app.get('/form-title/')
2385
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
2386
        fargo_get.return_value = {'result': 1, 'data': {'results': []}}
2387
        resp = app.get('/form-title/')
2388
        fargo_get.assert_called_once_with(
2389
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
2370 2390
    resp.forms[0]['f0$file'] = upload
2391
    for key in metadata:
2392
        resp.forms[0]['f0$%s' % key] = metadata[key]
2393
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
2394
        fargo_get.return_value = {'result': 1, 'data': {'results': []}}
2395
        resp = resp.forms[0].submit('submit')
2396
        fargo_get.assert_called_once_with(
2397
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
2398
    assert 'Check values then click submit.' in resp.body
2371 2399
    resp = resp.forms[0].submit('submit')
2400
    assert resp.status_int == 302
2401
    resp = resp.follow()
2402
    assert 'The form has been recorded' in resp.body
2403
    assert formdef.data_class().count() == 1
2404
    formdata = formdef.data_class().select()[0]
2405
    assert formdata.data['0'].metadata == metadata
2406
    assert formdata.data['0_structured'] == metadata
2407

  
2408

  
2409
def test_file_field_fargo_no_metadata(pub, fargo_url):
2410
    document_type = {
2411
        'id': 'justificatif-de-domicile',
2412
        'fargo': True,
2413
        'label': 'Justificatif de domicile',
2414
    }
2415
    user = create_user(pub)
2416
    user.name_identifiers = ['12345']
2417
    user.store()
2418
    FormDef.wipe()
2419
    formdef = FormDef()
2420
    formdef.name = 'form title'
2421
    formdef.fields = [fields.FileField(
2422
        id='0', label='1st field', type='file',
2423
        document_type=document_type)
2424
    ]
2425
    formdef.store()
2426
    formdef.data_class().wipe()
2427
    upload = Upload('test.pdf', '%PDF-1.4', 'application/pdf')
2428
    app = login(get_app(pub), username='foo', password='foo')
2429
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
2430
        resp = app.get('/form-title/')
2431
        assert fargo_get.call_count == 0
2432
    resp.forms[0]['f0$file'] = upload
2433
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
2434
        resp = resp.forms[0].submit('submit')
2435
        assert fargo_get.call_count == 0
2372 2436
    assert 'Check values then click submit.' in resp.body
2373 2437
    resp = resp.forms[0].submit('submit')
2374 2438
    assert resp.status_int == 302
2375
    with mock.patch('wcs.file_validation.http_get_page') as http_get_page:
2376
        json_response = json.dumps({
2377
            'err': 0,
2378
            'data': {
2379
                'type': 'justificatif-de-domicile',
2380
                'label': 'Justificatif de domicile',
2381
                'creator': 'Jean Bono',
2382
                'created': '2014-01-01T01:01:01',
2383
                'start': '2014-01-01T01:01:01',
2384
                'end': '2014-01-01T01:01:01',
2385
                'metadata': [{
2386
                        'name': 'code_postal',
2387
                        'label': 'Code postal',
2388
                        'value': '13400',
2389
                }],
2390
            },
2391
        })
2392
        http_get_page.return_value = None, 200, json_response, None
2393
        resp = resp.follow()
2394
        http_get_page.assert_called_once_with('http://fargo.example.net/metadata/12345/%s/justificatif-de-domicile/' % digest)
2395
        http_get_page.reset_mock()
2396
        assert 'The form has been recorded' in resp.body
2397
        assert 'class="value valid"' in resp.body
2439
    resp = resp.follow()
2440
    assert 'The form has been recorded' in resp.body
2441
    assert formdef.data_class().count() == 1
2442
    formdata = formdef.data_class().select()[0]
2443
    assert not hasattr(formdata.data['0'], 'metadata')
2444
    assert not '0_structured' in formdata.data
2445

  
2398 2446

  
2399 2447
def test_form_string_field_autocomplete(pub):
2400 2448
    formdef = create_formdef()
tests/test_formdata.py
1
# -*- coding: utf-8 -*-
1 2
import pytest
2 3
import sys
3 4
import shutil
......
11 12
from wcs.formdata import Evolution
12 13
from wcs.workflows import Workflow, WorkflowCriticalityLevel
13 14
from wcs.wf.anonymise import AnonymiseWorkflowStatusItem
15
from wcs.qommon.form import NoUpload
16
import mock
14 17

  
15 18
from utilities import create_temporary_pub, clean_temporary_pub
16 19

  
......
148 151
    assert substvars.get('form_var_foo_url').endswith('/foobar/1/download?f=0')
149 152
    assert isinstance(substvars.get('form_var_foo_raw'), Upload)
150 153

  
154

  
155
def test_file_field_fargo_no_metadata(pub):
156
    document_types = {
157
        'justificatif-de-domicile': {
158
            'id': 'justificatif-de-domicile',
159
            'fargo': True,
160
            'label': 'Justificatif de domicile',
161
        }
162
    }
163
    formdef.data_class().wipe()
164
    formdef.fields = [fields.FileField(id='0', label='file', varname='foo',
165
                                       document_type=document_types['justificatif-de-domicile'])]
166
    formdef.store()
167
    formdata = formdef.data_class()()
168
    upload = Upload('test.txt', 'text/plain', 'ascii')
169
    upload.receive(['first line', 'second line'])
170
    formdata.data = {'0': upload}
171
    formdata.id = 1
172
    substvars = formdata.get_substitution_variables()
173
    assert substvars.get('form_var_foo') == 'test.txt'
174
    assert substvars.get('form_var_foo_url').endswith('/foobar/1/download?f=0')
175
    assert isinstance(substvars.get('form_var_foo_raw'), Upload)
176

  
177

  
178
def test_file_field_with_metadata(pub):
179
    document_types = {
180
        'justificatif-de-domicile': {
181
            'id': 'justificatif-de-domicile',
182
            'fargo': True,
183
            'label': 'Justificatif de domicile',
184
            'metadata': [
185
                {'label': 'Nom', 'name': 'nom'},
186
                {'label': 'Prénom(s)', 'name': 'prenoms'},
187
                {'label': 'Numéro', 'name': 'numero'},
188
                {'label': 'Rue', 'name': 'rue'},
189
                {'label': 'Code postal', 'name': 'code-postal'},
190
                {'label': 'Ville', 'name': 'ville'},
191
            ],
192
        }
193
    }
194
    formdef.data_class().wipe()
195
    formdef.fields = [fields.FileField(id='0', label='file', varname='foo',
196
                                       document_type=document_types['justificatif-de-domicile'])]
197
    formdef.store()
198
    formdata = formdef.data_class()()
199
    upload = Upload('test.txt', 'text/plain', 'ascii')
200
    upload.receive(['first line', 'second line'])
201
    upload.metadata = {
202
        'nom': 'Doe',
203
        'prenoms': 'John',
204
        'numero': '169',
205
        'rue': 'rue du château',
206
        'code-postal': '75014',
207
        'ville': 'PARIS',
208
    }
209
    formdata.data = {'0': upload, '0_structured': upload.metadata}
210
    formdata.id = 1
211
    substvars = formdata.get_substitution_variables()
212
    assert substvars.get('form_var_foo') == 'test.txt'
213
    assert substvars.get('form_var_foo_url').endswith('/foobar/1/download?f=0')
214
    assert isinstance(substvars.get('form_var_foo_raw'), Upload)
215
    for key in upload.metadata:
216
        assert substvars.get('form_var_foo_%s' % key) == upload.metadata[key]
217

  
218

  
219
def test_file_field_no_file_with_metadata(pub):
220
    document_types = {
221
        'justificatif-de-domicile': {
222
            'id': 'justificatif-de-domicile',
223
            'fargo': True,
224
            'label': 'Justificatif de domicile',
225
            'metadata': [
226
                {'label': 'Nom', 'name': 'nom'},
227
                {'label': 'Prénom(s)', 'name': 'prenoms'},
228
                {'label': 'Numéro', 'name': 'numero'},
229
                {'label': 'Rue', 'name': 'rue'},
230
                {'label': 'Code postal', 'name': 'code-postal'},
231
                {'label': 'Ville', 'name': 'ville'},
232
            ],
233
        }
234
    }
235
    formdef.data_class().wipe()
236
    formdef.fields = [fields.FileField(id='0', label='file', varname='foo',
237
                                       document_type=document_types['justificatif-de-domicile'])]
238
    formdef.store()
239
    formdata = formdef.data_class()()
240
    metadata = {
241
        'nom': 'Doe',
242
        'prenoms': 'John',
243
        'numero': '169',
244
        'rue': 'rue du château',
245
        'code-postal': '75014',
246
        'ville': 'PARIS',
247
    }
248
    with mock.patch('wcs.file_validation.get_validation', return_value=metadata):
249
        upload = NoUpload('http://whatever.com/')
250
    formdata.data = {'0': upload, '0_structured': upload.metadata}
251
    formdata.id = 1
252
    substvars = formdata.get_substitution_variables()
253
    assert isinstance(substvars.get('form_var_foo'), NoUpload)
254
    assert not 'form_var_foo_url' in substvars
255
    assert not 'form_var_foo_raw' in substvars
256
    for key in upload.metadata:
257
        assert substvars.get('form_var_foo_%s' % key) == upload.metadata[key]
258

  
259

  
151 260
def test_get_submitter(pub):
152 261
    formdef.data_class().wipe()
153 262
    formdef.fields = [fields.StringField(id='0', label='email', varname='foo',
tests/test_formdef_import.py
242 242
    assert_xml_import_export_works(formdef)
243 243
    assert_json_import_export_works(formdef, include_id=True)
244 244
    assert_json_import_export_works(formdef)
245
    formdef = FormDef()
246
    formdef.name = 'foo'
247
    formdef.fields = [fields.FileField(type='file', id='1', document_type={
248
        'id': 'justificatif-de-domicile',
249
        'fargo': True,
250
        'mimetypes': ['application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'image/*'],
251
        'metadata': [
252
            {'name': 'nom', 'label': 'Nom'},
253
            {'name': 'rue', 'label': 'Rue'},
254
        ]
255
    })]
256
    assert_xml_import_export_works(formdef, include_id=True)
257
    assert_xml_import_export_works(formdef)
258
    assert_json_import_export_works(formdef, include_id=True)
259
    assert_json_import_export_works(formdef)
245 260

  
246 261
def test_unknown_data_source():
247 262
    formdef = FormDef()
wcs/backoffice/management.py
1623 1623

  
1624 1624

  
1625 1625
class FormBackOfficeStatusPage(FormStatusPage):
1626
    _q_exports = ['', 'download', 'json', 'action', 'inspect']
1626
    _q_exports = ['', 'download', 'json', 'action', 'inspect', 'validate']
1627 1627
    form_page_class = FormFillPage
1628 1628

  
1629 1629
    def html_top(self, title = None):
wcs/fields.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import json
17 18
import time
18 19
import random
19 20
import re
......
720 721
        self.document_type = self.document_type or {}
721 722

  
722 723
    @property
724
    def metadata(self):
725
        return self.document_type.get('metadata', [])
726

  
727
    @property
723 728
    def file_type(self):
724 729
        return (self.document_type or {}).get('mimetypes', [])
725 730

  
......
752 757
                'document_type', 'max_file_size', 'allow_portfolio_picking']
753 758

  
754 759
    def get_view_value(self, value):
755
        return htmltext('<a download="%s" href="[download]?f=%s">%s</a>') % (
760
        r = TemplateIO(html=True)
761
        if not getattr(value, 'no_file', False):
762
            r += htmltext('<a download="%s" href="[download]?f=%s">%s</a>') % (
756 763
                value.base_filename, self.id, value)
764
        for meta_field in self.metadata:
765
            metadata_value = getattr(value, 'metadata', {}).get(meta_field['name'], '')
766
            r += htmltext('<p>%s&nbsp;: %s</p>') % (meta_field['label'], metadata_value)
767
        return r.getvalue()
757 768

  
758 769
    def get_csv_value(self, value, hint=None):
759 770
        if not value:
......
788 799
            value = get_request().get_field(self.field_key)
789 800
            if value and hasattr(value, 'token'):
790 801
                get_request().form[self.field_key + '$token'] = value.token
802
        kwargs['document_type'] = self.document_type
791 803

  
792 804
    def get_document_types(self):
793 805
        document_types = {
......
854 866
        if self.document_type and self.document_type.get('mimetypes'):
855 867
            old_value = self.document_type['mimetypes']
856 868
            self.document_type['mimetypes'] = '|'.join(self.document_type['mimetypes'])
869
        if self.document_type and self.document_type.get('metadata'):
870
            self.document_type['metadata'] = json.dumps(self.document_type['metadata'])
857 871
        result = super(FileField, self).export_to_xml(charset, include_id=include_id)
858 872
        if self.document_type and self.document_type.get('mimetypes'):
859 873
            self.document_type['mimetypes'] = old_value
874
        if self.document_type and self.document_type.get('metadata'):
875
            self.document_type['metadata'] = json.loads(self.document_type['metadata'])
860 876
        return result
861 877

  
862 878
    def init_with_xml(self, element, charset, include_id=False):
......
866 882
            self.document_type['mimetypes'] = self.document_type['mimetypes'].split('|')
867 883
        if self.document_type and self.document_type.get('fargo'):
868 884
            self.document_type['fargo'] = self.document_type['fargo'] == 'True'
885
        if self.document_type and self.document_type.get('metadata'):
886
            self.document_type['metadata'] = json.loads(self.document_type['metadata'])
887

  
888
    def store_structured_value(self, data, field_id):
889
        value = data.get(field_id)
890
        return getattr(value, 'metadata', {})
869 891

  
870 892

  
871 893
register_field_class(FileField)
wcs/file_validation.py
19 19
import hashlib
20 20
import urllib
21 21

  
22
from qommon.misc import http_get_page, json_loads
23
from quixote import get_publisher, get_response
24
from quixote.html import htmltext
22
from qommon.misc import http_get_page, json_loads, http_post_request
23
from quixote import get_publisher, get_request
25 24

  
26 25

  
27 26
def has_file_validation():
28 27
    return get_publisher().get_site_option('fargo_url') is not None
29 28

  
29

  
30 30
def fargo_get(path):
31 31
    fargo_url = get_publisher().get_site_option('fargo_url')
32 32
    url = urlparse.urljoin(fargo_url, path)
......
35 35
        return json_loads(data)
36 36
    return None
37 37

  
38

  
38 39
def sha256_of_upload(upload):
39 40
    return hashlib.sha256(upload.get_content()).hexdigest()
40 41

  
42

  
41 43
def get_document_types():
42 44
    if not has_file_validation():
43 45
        return {}
44 46
    response = fargo_get('/document-types/')
45
    publisher = get_publisher()
46 47
    if response.get('err') == 0:
47 48
        result = {}
48 49
        for schema in response['data']:
......
52 53
                'fargo': True,
53 54
            }
54 55
            if 'mimetypes' in schema:
55
                d['mimetypes'] = shema['mimetypes']
56
                d['mimetypes'] = schema['mimetypes']
56 57
            result[d['id']] = d
57

  
58
            d['metadata'] = schema.get('metadata', [])
58 59
        return result
59 60
    return {}
60 61

  
61
def validation_path(filled, field, upload):
62
    user = filled.get_user()
63
    if not user:
64
        return None
65
    if not user.name_identifiers:
66
        return None
67
    if not field.document_type or not field.document_type.get('fargo'):
68
        return None
69
    name_id = user.name_identifiers[0]
70
    sha_256 = sha256_of_upload(upload)
71
    document_type = field.document_type['id']
72
    path = '%s/%s/%s/' % (
73
        urllib.quote(name_id),
74
        urllib.quote(sha_256),
75
        urllib.quote(document_type),
76
    )
77
    return path
78

  
79
def validate_upload(filled, field, upload):
62

  
63
def get_validation(url):
64
    response, status, data, auth_header = http_get_page(url)
65
    if status == 200:
66
        return json_loads(data)['data']
67
    return None
68

  
69

  
70
def get_validations(document_type):
71
    request = get_request()
72
    document_type_id = document_type['id']
73
    qs = {}
74
    if not request.user:
75
        return []
76
    if request.user.name_identifiers:
77
        qs['user_nameid'] = request.user.name_identifiers[0]
78
    elif request.user.email:
79
        qs['user_email'] = request.user.email
80
    else:
81
        return []
82
    path = 'api/validation/%s/' % urllib.quote(document_type_id)
83
    validations = fargo_get(path + '?%s' % urllib.urlencode(qs))
84
    if validations and validations.get('data', {}).get('results', []):
85
        return validations['data']['results']
86
    return []
87

  
88

  
89
def is_valid(filled, field, upload):
80 90
    '''Check validation of the uploaded file with Fargo'''
81
    path = validation_path(filled, field, upload)
82
    if not path:
83
        return None
84
    response = fargo_get('metadata/' + path)
85
    if response is None:
86
        return None
87
    if response['err'] == 1:
88
        return False
89
    return response['data']
90

  
91
def get_document_type_label(document_type):
92
    return get_document_types().get(document_type, {}).get('label')
93

  
94
def validation_link(filled, field, upload):
91
    return 'url' in getattr(upload, 'metadata', {})
92

  
93

  
94
def validate(filled, field, upload):
95 95
    '''Compute link to Fargo to validate the given document'''
96
    path = validation_path(filled, field, upload)
97
    if not path:
98
        return ''
99
    get_response().add_css_include('../js/smoothness/jquery-ui-1.10.0.custom.min.css')
100
    get_response().add_javascript(['jquery-ui.js', 'jquery.js', 'fargo.js'])
101
    label = get_document_type_label(field.document_type['id'])
96
    document_type_id = field.document_type['id']
97
    path = 'api/validation/%s/' % urllib.quote(document_type_id)
102 98
    fargo_url = get_publisher().get_site_option('fargo_url')
103
    url = urlparse.urljoin(fargo_url, 'validation/' + path)
104
    next_url = get_publisher().get_frontoffice_url() + '/reload-top'
105
    url += '?next=%s' % urllib.quote(next_url)
106
    title = _('Validate as a %s') % label
107
    return htmltext('<a data-title="%(title)s" data-width="800" '
108
                    'data-height="500" href="%(url)s">%(title)s</a>') % {
109
        'title': title,
110
        'url': url,
111
    }
99
    url = urlparse.urljoin(fargo_url, path)
100
    payload = {}
101
    if filled.user:
102
        if filled.user.name_identifiers:
103
            payload['user_nameid'] = filled.user.name_identifiers[0]
104
        else:
105
            payload['user_email'] = filled.user.email
106
    payload['origin'] = get_request().get_server()
107
    payload['creator'] = get_request().user.display_name
108
    payload['content_hash'] = sha256_of_upload(upload)
109
    for meta_field in field.metadata:
110
        payload[meta_field['name']] = upload.metadata.get(meta_field['name'], '')
111
    headers = {'Content-Type': 'application/json'}
112
    response, status, response_payload, auth_header = http_post_request(url, json.dumps(payload),
113
                                                                        headers=headers)
114
    if status == 201:
115
        upload.metadata = json_loads(response_payload)['data']
116
        filled.data['%s_structured' % field.id] = upload.metadata
117
        filled.store()
wcs/forms/common.py
97 97
    def html_top(self, title = None):
98 98
        template.html_top(title = title, default_org = _('Forms'))
99 99

  
100
    def validate(self):
101
        if not file_validation.has_file_validation():
102
            return redirect('.')
103
        field_id = get_request().form.get('field_id')
104
        if not field_id:
105
            return redirect('.')
106
        for field in self.formdef.fields:
107
            if field.id == field_id:
108
                break
109
        else:
110
            return redirect('.')
111
        if field.key != 'file':
112
            return redirect('.')
113
        if not field.document_type.get('fargo', False):
114
            return redirect('.')
115
        if not field.document_type.get('metadata', []):
116
            return redirect('.')
117
        value = self.filled.data.get(field_id)
118
        file_validation.validate(self.filled, field, value)
119
        return redirect('.')
120

  
100 121
    def __init__(self, formdef, filled, register_workflow_subdirs=True):
101 122
        get_publisher().substitutions.feed(filled)
102 123
        self.formdef = formdef
......
612 633

  
613 634
    def display_file_field(self, form_url, field, value):
614 635
        r = TemplateIO(html=True)
615
        status = None
616
        if file_validation.has_file_validation():
617
            status = file_validation.validate_upload(self.filled, field, value)
618
            if status is False:
636
        validated = None
637
        is_fargo_dt = field.document_type.get('fargo', False)
638
        has_metadata = bool(field.document_type.get('metadata', []))
639
        if file_validation.has_file_validation() and is_fargo_dt and has_metadata:
640
            validated = file_validation.is_valid(self.filled, field, value)
641
            if validated is False:
619 642
                extra_class = ' invalid'
620
            elif status is None:
621
                extra_class = ''
622 643
            else:
623 644
                extra_class = ' valid'
624 645
            r += htmltext('<div class="value%s">' % extra_class)
......
627 648
        s = field.get_view_value(value)
628 649
        s = s.replace(str('[download]'), str('%sdownload' % form_url))
629 650
        r += s
630
        if status is not None and get_request().is_in_backoffice():
631
            if status is False:
632
                r += htmltext(' <span class="validation-status">%s</span>') % _('not validated')
651
        if validated is not None and get_request().is_in_backoffice():
633 652
            r += htmltext('<div class="file-validation">')
634
            if status:
635
                r += htmltext('<p class="validation-status">%s</p>') % _(
636
                        '%(label)s validated by %(creator)s on %(created)s') % status
637
                r += htmltext('<ul>')
638
                for meta in status['metadata']:
639
                    r += htmltext(_('<li>%(label)s: %(value)s</li>')) % {
640
                        'label': meta['label'],
641
                        'value': meta['value']
642
                    }
643
                r += htmltext('</ul>')
653
            if validated:
654
                r += htmltext('<p class="validation-validated">%s</p>') % _(
655
                    'validated by %(creator)s on %(created)s') % value.metadata
644 656
                r += htmltext('<p>%s</p>') % (_('Valid from %(start)s to %(end)s') % {
645
                    'start': status['start'],
646
                    'end': status['end'],
657
                    'start': value.metadata['start'],
658
                    'end': value.metadata['end'],
647 659
                })
648
            else:
649
                r += file_validation.validation_link(self.filled, field, value)
660
            elif self.filled.user:
661
                r += htmltext('<form method="post" action="./validate?field_id=%s">'
662
                              '<button>%s</button></form>') % (
663
                    field.id, _('Validate'))
650 664
            r += htmltext('</div>')
651 665
        r += htmltext('</div>')
652 666
        return str(r)
wcs/qommon/form.py
60 60
import misc
61 61
from strftime import strftime
62 62
from publisher import get_cfg
63
from wcs import file_validation
63 64

  
64 65
QuixoteForm = Form
65 66

  
......
582 583
            self.value = None
583 584

  
584 585

  
586
class NoUpload(object):
587
    no_file = True
588
    metadata = None
589

  
590
    def __init__(self, validation_url):
591
        self.metadata = file_validation.get_validation(validation_url)
592

  
593

  
585 594
class FileWithPreviewWidget(CompositeWidget):
586 595
    """Widget that proposes a File Upload widget but that stores the file
587 596
    ondisk so it has a "readonly" mode where the filename is shown."""
......
590 599
    max_file_size = None
591 600
    file_type = None
592 601

  
593
    max_file_size_bytes = None # will be filled automatically
602
    max_file_size_bytes = None  # will be filled automatically
594 603

  
595 604
    def __init__(self, name, value=None, **kwargs):
596 605
        CompositeWidget.__init__(self, name, value, **kwargs)
597 606
        self.value = value
607
        self.document_type = kwargs.pop('document_type', None) or {}
598 608
        self.preview = kwargs.get('readonly')
599 609
        self.max_file_size = kwargs.pop('max_file_size', None)
600 610
        self.allow_portfolio_picking = kwargs.pop('allow_portfolio_picking', True)
......
610 620
                # this could be used for client size validation of file size
611 621
                attrs['data-max-file-size'] = str(self.max_file_size_bytes)
612 622
            self.add(FileWidget, 'file', render_br=False, attrs=attrs)
623
        if self.document_type.get('metadata'):
624
            if self.preview:
625
                self.add(HiddenWidget, 'validation_url')
626
            else:
627
                validations = file_validation.get_validations(self.document_type)
628
                if validations:
629
                    options = [('', _('Known documents'), '')]
630
                    options += [(v['url'], v['display'], v['url']) for v in validations]
631
                    self.add(SingleSelectWidget, 'validation_url', options=options,
632
                             attrs={'data-validations': json.dumps(validations)})
633
            for meta_field in self.metadata:
634
                subvalue = getattr(value, 'metadata', {}).get(meta_field['name'])
635
                self.add(StringWidget, meta_field['name'],
636
                         title=meta_field['label'],
637
                         required=meta_field.get('required', True) and self.required,
638
                         readonly=self.preview,
639
                         value=subvalue)
640
                self.get_widget(meta_field['name']).extra_css_class = 'subwidget'
613 641
        if value:
614 642
            self.set_value(value)
615 643

  
......
625 653
                self.get_widget('token').set_value(self.value.token)
626 654

  
627 655
    def render_content(self):
628
        get_response().add_javascript(['jquery.js', 'jquery-ui.js',
629
                        'jquery.iframe-transport.js', 'jquery.fileupload.js',
630
                        'qommon.fileupload.js'])
656
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'jquery.iframe-transport.js',
657
                                       'jquery.fileupload.js', 'qommon.fileupload.js'])
631 658

  
632 659
        temp = get_session().get_tempfile(self.get('token')) or {}
633 660

  
......
638 665
        r += htmltext('<div class="fileprogress" style="display: none;">')
639 666
        r += htmltext(' <div class="bar">%s</div>' % _('Upload in progress...'))
640 667
        r += htmltext('</div>')
641
        r += htmltext('<div class="fileinfo"><span class="filename">%s</span>' % temp.get('base_filename', ''))
668
        r += htmltext('<div class="fileinfo"><span class="filename">%s</span>'
669
                      % temp.get('base_filename', ''))
642 670
        if not self.preview:
643
            r += htmltext(' <a href="#" class="remove" title="%s">%s</a>' % (
644
                                    _('Remove this file'),
645
                                    _('remove')))
671
            r += htmltext(' <a href="#" class="remove" title="%s">%s</a>'
672
                          % (_('Remove this file'), _('remove')))
646 673
        elif temp:
647 674
            filetype = mimetypes.guess_type(temp.get('orig_filename', ''))
648 675
            if filetype and filetype[0] and filetype[0].startswith('image'):
649
                r += htmltext('<img alt="" src="tempfile?t=%s&thumbnail=1" />' % \
650
                                             self.get('token'))
676
                r += htmltext('<img alt="" src="tempfile?t=%s&thumbnail=1" />' %
677
                              self.get('token'))
651 678

  
652 679
        r += htmltext('</div>')
653 680
        return r.getvalue()
654 681

  
682
    @property
683
    def metadata(self):
684
        return self.document_type.get('metadata', [])
685

  
655 686
    def _parse(self, request):
656 687
        self.value = None
657 688
        if self.get('token'):
658 689
            token = self.get('token')
690
        elif self.get('validation_url'):
691
            self.value = NoUpload(self.get('validation_url'))
692
            return
659 693
        elif self.get('file'):
660 694
            token = get_session().add_tempfile(self.get('file'))
661 695
            request.form[self.get_widget('token').get_name()] = token
......
664 698

  
665 699
        session = get_session()
666 700
        if token and session.tempfiles and session.tempfiles.has_key(token):
667
            temp = session.tempfiles[token]
668 701
            self.value = session.get_tempfile_content(token)
669 702

  
670 703
        if self.value is None:
671
            # there's no file, the other checks are irrelevant.
704
            # there's no file, check all metadata field are empty too
705
            # if not file and required metadata field become required
706
            if (self.get_widget('file')
707
                    and not self.required
708
                    and any([self.get(meta_field['name']) for meta_field in self.metadata])):
709
                self.get_widget('file').required = True
710
                for meta_field in self.metadata:
711
                    required = meta_field.get('required', True)
712
                    if required:
713
                        widget = self.get_widget(meta_field['name'])
714
                        widget.required = True
715
                        if not self.get(meta_field['name']):
716
                            widget.set_error(self.REQUIRED_ERROR)
672 717
            return
673 718

  
719
        # There is some file, check all required metadata files have been filled
720
        for meta_field in self.metadata:
721
            required = meta_field.get('required', True)
722
            if required:
723
                widget = self.get_widget(meta_field['name'])
724
                widget.required = True
725
                if not self.get(meta_field['name']):
726
                    widget.set_error(self.REQUIRED_ERROR)
727

  
728
        if self.metadata:
729
            self.value.metadata = {}
730
            for meta_field in self.metadata:
731
                self.value.metadata[meta_field['name']] = self.get(meta_field['name'])
732

  
674 733
        # Don't trust the browser supplied MIME type, update the Upload object
675 734
        # with a MIME type created with magic (or based on the extension if the
676 735
        # module is missing).
wcs/qommon/static/css/qommon.css
79 79
	vertical-align: middle;
80 80
}
81 81

  
82
/* nested widgets */
83
.widget .subwidget {
84
	padding-left: 2em;
85
}
86
div.FileWithPreviewWidget .validation {
87
	display: inline-block;
88
	font-size: xx-small;
89
	margin-right: 2em;
90
}
91

  
82 92
div.form .title, form.quixote .title {
83 93
	font-weight: bold;
84 94
}
wcs/qommon/static/js/qommon.fileupload.js
6 6
        } else {
7 7
            $(base_widget).find('.fileinfo').hide();
8 8
        }
9
        function disable() {
10
            base_widget.find('.subwidget input').prop('disabled', true);
11
            $('form').on('submit', function () {
12
                $('input').prop('disabled', false);
13
            });
14
        }
15
        function enable() {
16
            if (base_widget.find('.subwidget input:disabled').length) {
17
                base_widget.find('.subwidget input').val('');
18
                base_widget.find('.subwidget input').prop('disabled', false);
19
            }
20
        }
9 21
        $(this).find('input[type=file]').fileupload({
10 22
            dataType: 'json',
11 23
            add: function (e, data) {
......
19 31
                $(base_widget).find('.fileprogress').hide();
20 32
                $(base_widget).find('.filename').text(data.result[0].name);
21 33
                $(base_widget).find('.fileinfo').show();
22
                $(base_widget).find('input[type=hidden]').val(data.result[0].token);
34
                $(base_widget).find('input[name$="$token"]').val(data.result[0].token);
35
                $(base_widget).find('select[name$="$validation_url"]').val('');
36
                enable();
23 37
                $(base_widget).parents('form').find('input[name=submit]').prop('disabled', false);
24 38
                $(this).hide();
25 39
            },
......
29 43
            }
30 44
        });
31 45
        $(this).find('a.remove').click(function() {
32
            $(base_widget).find('input[type=hidden]').val('');
46
            $(base_widget).find('input[name$="$token"]').val('');
33 47
            $(base_widget).find('.fileinfo').hide();
34 48
            $(base_widget).find('input[type=file]').show();
35 49
            return false;
......
38 52
            $(base_widget).find('input[type=file]').click();
39 53
            return false;
40 54
        });
55
        if ($(this).find('select[name$="$validation_url"] option:selected').val()) {
56
            disable();
57
        }
58
        $(this).find('select[name$="$validation_url"]').on('change', function() {
59
            var url = $(this).find(':selected').val();
60
            if (url) {
61
                var validations = $(this).data('validations');
62

  
63
                for (var i = 0; i < validations.length; i++) {
64
                    if (validations[i].url == url) {
65
                        base_widget.find('a.remove').trigger('click');
66
                        for (var item in validations[i]) {
67
                            if (! item) {
68
                                continue;
69
                            }
70
                            var $input = base_widget.find('input[name$="$' + item + '"]');
71
                            if ($input.length) {
72
                                $input.val(validations[i][item]);
73
                            }
74
                        }
75
                        disable();
76
                    }
77
                }
78
            } else {
79
                enable();
80
            }
81
        });
41 82
    });
42 83
});
43
-