Projet

Général

Profil

0002-rewrite-file-validation-10444.patch

Benjamin Dauvergne, 12 avril 2016 11:06

Télécharger (53,3 ko)

Voir les différences:

Subject: [PATCH 2/7] 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            | 255 ++++++++++++++++++++++++++----
 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                             |  25 ++-
 wcs/file_validation.py                    | 129 +++++++++------
 wcs/forms/common.py                       |  70 +++++---
 wcs/qommon/form.py                        | 121 +++++++++++---
 wcs/qommon/static/css/qommon.css          |  10 ++
 wcs/qommon/static/js/qommon.fileupload.js |  45 +++++-
 11 files changed, 728 insertions(+), 163 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,
......
1665 1666
    assert resp.body == 'foo(%s);' % menu_json_str
1666 1667
    assert resp.headers['content-type'] == 'application/javascript'
1667 1668

  
1669
def test_backoffice_file_field_fargo_no_metadata(pub, fargo_url):
1670
    document_type = {
1671
        'id': 'justificatif-de-domicile',
1672
        'fargo': True,
1673
        'mimetypes': ['application/pdf'],
1674
        'label': 'Justificatif de domicile',
1675
    }
1676
    user = create_user(pub, is_admin=True)
1677
    user.name_identifiers = ['12345']
1678
    user.store()
1679
    FormDef.wipe()
1680
    formdef = FormDef()
1681
    formdef.name = 'form title'
1682
    formdef.fields = [fields.FileField(
1683
        id='0', label='1st field', type='file',
1684
        document_type=document_type)]
1685
    formdef.store()
1686
    formdef.data_class().wipe()
1687
    upload = Upload('test.pdf', '%PDF-1.4', 'application/pdf')
1688
    digest = hashlib.sha256('%PDF-1.4').hexdigest()
1689
    app = login(get_app(pub))
1690
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1691
        resp = app.get('/form-title/')
1692
        assert fargo_get.call_count == 0
1693
    resp.forms[0]['f0$file'] = upload
1694
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1695
        resp = resp.forms[0].submit('submit')
1696
        assert fargo_get.call_count == 0
1697
    assert 'Check values then click submit.' in resp.body
1698
    resp = resp.forms[0].submit('submit').follow()
1699
    assert formdef.data_class().count() == 1
1700
    formdata = formdef.data_class().select()[0]
1701
    form_id = formdata.id
1702
    assert not hasattr(formdata.data['0'], 'metadata')
1703
    assert not '0_structured' in formdata.data
1704
    resp = app.get('/backoffice/management/form-title/%s/' % form_id)
1705
    assert not 'Validate' in resp.body
1706
    with mock.patch('wcs.file_validation.http_post_request') as http_post_request:
1707
        resp = app.get('/backoffice/management/form-title/%s/validate?field_id=0' % form_id)
1708
        assert http_post_request.call_count == 0
1709
    resp = resp.follow()
1710
    assert not 'Valid ' in resp.body
1711
    assert not 'Validate' in resp.body
1712

  
1713

  
1668 1714
def test_backoffice_file_field_validation(pub, fargo_url):
1715
    document_type = {
1716
        'id': 'justificatif-de-domicile',
1717
        'fargo': True,
1718
        'mimetypes': ['application/pdf'],
1719
        'label': 'Justificatif de domicile',
1720
        'metadata': [
1721
            {'label': 'Nom', 'varname': 'nom', 'type': 'string'},
1722
            {'label': 'Prénom(s)', 'varname': 'prenoms', 'type': 'string'},
1723
            {'label': 'Numéro', 'varname': 'numero', 'type': 'string'},
1724
            {'label': 'Rue', 'varname': 'rue', 'type': 'string'},
1725
            {'label': 'Code postal', 'varname': 'code-postal', 'type': 'string'},
1726
            {'label': 'Ville', 'varname': 'ville', 'type': 'string'},
1727
        ],
1728
    }
1729
    metadata = {
1730
        'nom': 'Doe',
1731
        'prenoms': 'John',
1732
        'numero': '169',
1733
        'rue': 'rue du château',
1734
        'code-postal': '75014',
1735
        'ville': 'PARIS',
1736
    }
1669 1737
    user = create_user(pub, is_admin=True)
1670 1738
    user.name_identifiers = ['12345']
1671 1739
    user.store()
......
1674 1742
    formdef.name = 'form title'
1675 1743
    formdef.fields = [fields.FileField(
1676 1744
        id='0', label='1st field', type='file',
1677
        document_type={
1678
            'id': 'justificatif-de-domicile',
1679
            'fargo': True,
1680
            'mimetypes': ['application/pdf'],
1681
            'label': 'PDF files',
1682
        })
1683
    ]
1745
        document_type=document_type)]
1684 1746
    formdef.store()
1685 1747
    formdef.data_class().wipe()
1686 1748
    upload = Upload('test.pdf', '%PDF-1.4', 'application/pdf')
1687 1749
    digest = hashlib.sha256('%PDF-1.4').hexdigest()
1688 1750
    app = login(get_app(pub))
1689
    resp = app.get('/form-title/')
1751
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1752
        fargo_get.return_value = {'result': 1, 'data': {'results': []}}
1753
        resp = app.get('/form-title/')
1754
        fargo_get.assert_called_once_with(
1755
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
1690 1756
    resp.forms[0]['f0$file'] = upload
1691
    resp = resp.forms[0].submit('submit')
1757
    for i, meta_field in enumerate(document_type['metadata']):
1758
        resp.forms[0]['f0$f%s' % i] = metadata[meta_field['varname']]
1759
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1760
        fargo_get.return_value = {'result': 1, 'data': {'results': []}}
1761
        resp = resp.forms[0].submit('submit')
1762
        fargo_get.assert_called_once_with(
1763
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
1764
    for key in metadata:
1765
        assert 'value="%s"' % metadata[key] in resp.body
1692 1766
    assert 'Check values then click submit.' in resp.body
1693
    resp = resp.forms[0].submit('submit')
1694
    assert resp.status_int == 302
1767
    resp = resp.forms[0].submit('submit').follow()
1768
    for metadata_field in document_type['metadata']:
1769

  
1770
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['varname']])
1771
        assert fragment in resp.body
1695 1772
    assert formdef.data_class().count() == 1
1696
    form_id = formdef.data_class().select()[0].id
1697
    with mock.patch('wcs.file_validation.http_get_page') as http_get_page:
1698
        json_response = json.dumps({
1699
            'err': 0,
1700
            'data': {
1701
                'type': 'justificatif-de-domicile',
1702
                'label': 'Justificatif de domicile',
1703
                'creator': 'Jean Bono',
1704
                'created': '2014-01-01T01:01:01',
1705
                'start': '2014-01-01T01:01:01',
1706
                'end': '2014-01-01T01:01:01',
1707
                'metadata': [{
1708
                        'name': 'code_postal',
1709
                        'label': 'Code postal',
1710
                        'value': '13400',
1711
                }],
1712
            },
1773
    formdata = formdef.data_class().select()[0]
1774
    form_id = formdata.id
1775
    assert formdata.data['0'].metadata == metadata
1776
    assert formdata.data['0_structured'] == metadata
1777
    resp = app.get('/backoffice/management/form-title/%s/' % form_id)
1778
    assert 'Validate' in resp.body
1779
    for metadata_field in document_type['metadata']:
1780

  
1781
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['varname']])
1782
        assert fragment in resp.body
1783
    with mock.patch('wcs.file_validation.http_post_request') as http_post_request:
1784
        payload = {
1785
            'user_nameid': '12345',
1786
            'origin': 'example.net',
1787
            'creator': 'admin',
1788
            'content_hash': digest,
1789
        }
1790
        payload.update(metadata)
1791
        result = {
1792
            'result': 1,
1793
            'data': payload.copy()
1794
        }
1795
        result['data'].update({
1796
            'url': 'zob',
1797
            'created': '1970-01-01T10:10:10Z',
1798
            'start': '1970-01-01',
1799
            'end': '1978-01-01',
1800
            'display': 'John Doe, 169 rue du château, 75014 PARIS'
1713 1801
        })
1714
        http_get_page.return_value = None, 200, json_response, None
1715
        resp = app.get('/backoffice/management/form-title/%s/' % form_id)
1716
        http_get_page.assert_called_once_with('http://fargo.example.net/metadata/12345/%s/justificatif-de-domicile/' % digest)
1717
        assert 'class="value valid"' in resp.body
1718
        assert 'Justificatif de domicile validated by Jean Bono on 2014-01-01T01:01:01' in resp.body
1719
        assert 'Code postal: 13400' in resp.body
1720
        assert 'Valid from 2014-01-01T01:01:01 to 2014-01-01T01:01:01' in resp.body
1802
        http_post_request.return_value = None, 201, json.dumps(result), None
1803
        resp = app.get('/backoffice/management/form-title/%s/validate?field_id=0' % form_id)
1804
        assert http_post_request.call_count == 1
1805
        assert http_post_request.call_args[0][0] == 'http://fargo.example.net/api/validation/justificatif-de-domicile/'
1806
        assert json_loads(http_post_request.call_args[0][1]) == payload
1807
        assert http_post_request.call_args[1] == {'headers': {'Content-Type': 'application/json'}}
1808
    resp = resp.follow()
1809

  
1810
    assert 'Valid from 1970-01-01 to 1978-01-01' in resp.body
1811

  
1812

  
1813
def test_backoffice_file_validation_no_upload(pub, fargo_url):
1814
    document_type = {
1815
        'id': 'justificatif-de-domicile',
1816
        'fargo': True,
1817
        'mimetypes': ['application/pdf'],
1818
        'label': 'Justificatif de domicile',
1819
        'metadata': [
1820
            {'label': 'Nom', 'varname': 'nom', 'type': 'string'},
1821
            {'label': 'Prénom(s)', 'varname': 'prenoms', 'type': 'string'},
1822
            {'label': 'Numéro', 'varname': 'numero', 'type': 'string'},
1823
            {'label': 'Rue', 'varname': 'rue', 'type': 'string'},
1824
            {'label': 'Code postal', 'varname': 'code-postal', 'type': 'string'},
1825
            {'label': 'Ville', 'varname': 'ville', 'type': 'string'},
1826
        ],
1827
    }
1828
    metadata = {
1829
        'nom': 'Doe',
1830
        'prenoms': 'John',
1831
        'numero': '169',
1832
        'rue': 'rue du château',
1833
        'code-postal': '75014',
1834
        'ville': 'PARIS',
1835
    }
1836
    validation = {
1837
        'url': 'zob',
1838
        'creator': 'admin',
1839
        'created': '1970-01-01T10:10:10Z',
1840
        'start': '1970-01-01',
1841
        'end': '1978-01-01',
1842
        'display': 'John Doe, 169 rue du château, 75014 PARIS'
1843
    }
1844
    validation.update(metadata)
1845

  
1846
    user = create_user(pub, is_admin=True)
1847
    user.name_identifiers = ['12345']
1848
    user.store()
1849
    FormDef.wipe()
1850
    formdef = FormDef()
1851
    formdef.name = 'form title'
1852
    formdef.fields = [fields.FileField(
1853
        id='0', label='1st field', type='file',
1854
        document_type=document_type)]
1855
    formdef.store()
1856
    formdef.data_class().wipe()
1857
    app = login(get_app(pub))
1858
    return_value = {'result': 1, 'data': {'results': [validation]}}
1859
    with mock.patch('wcs.file_validation.http_get_page') as http_get_page:
1860
        http_get_page.return_value = None, 200, json.dumps(return_value), None
1861
        resp = app.get('/form-title/')
1862
        http_get_page.assert_called_once_with(
1863
            'http://fargo.example.net/api/validation/justificatif-de-domicile/?user_nameid=12345')
1864
        assert validation['display'] in resp.body
1865
    resp.forms[0]['f0$validation_url'] = 'zob'
1866
    for i, meta_field in enumerate(document_type['metadata']):
1867
        resp.forms[0]['f0$f%s' % i] = metadata[meta_field['varname']]
1868
    with mock.patch('wcs.file_validation.http_get_page') as http_get_page:
1869
        return_value2 = {
1870
            'result': 1,
1871
            'data': validation,
1872
        }
1873

  
1874
        def side_effect(url):
1875
            if url == 'zob':
1876
                return None, 200, json.dumps(return_value2), None
1877
            else:
1878
                return None, 200, json.dumps(return_value), None
1879
        http_get_page.side_effect = side_effect
1880
        resp = resp.forms[0].submit('submit')
1881
        assert http_get_page.call_args_list[0][0][0].endswith(
1882
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
1883
        assert http_get_page.call_args_list[1][0][0] == 'zob'
1884
        assert http_get_page.call_args_list[2][0][0] == 'zob'
1885
        for key in metadata:
1886
            assert 'value="%s"' % metadata[key] in resp.body
1887
        assert 'Check values then click submit.' in resp.body
1888
        resp = resp.forms[0].submit('submit').follow()
1889
    assert formdef.data_class().count() == 1
1890
    formdata = formdef.data_class().select()[0]
1891
    form_id = formdata.id
1892
    assert formdata.data['0'].metadata == validation
1893
    assert formdata.data['0_structured'] == validation
1894
    for metadata_field in document_type['metadata']:
1895

  
1896
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['varname']])
1897
        assert fragment in resp.body
1898
    resp = app.get('/backoffice/management/form-title/%s/' % form_id)
1899
    assert not 'Validate' in resp.body
1900
    for metadata_field in document_type['metadata']:
1901

  
1902
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['varname']])
1903
        assert fragment in resp.body
1904
    assert 'Valid from 1970-01-01 to 1978-01-01' in resp.body
1905

  
1721 1906

  
1722 1907
def test_360_user_view(pub):
1723 1908
    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
......
2355 2356
    assert formdef.data_class().select()[0].data['1'] == 'foobar3'
2356 2357

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

  
2417

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

  
2407 2455

  
2408 2456
def test_form_string_field_autocomplete(pub):
2409 2457
    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', 'varname': 'nom', 'type': 'string'},
186
                {'label': 'Prénom(s)', 'varname': 'prenoms', 'type': 'string'},
187
                {'label': 'Numéro', 'varname': 'numero', 'type': 'string'},
188
                {'label': 'Rue', 'varname': 'rue', 'type': 'string'},
189
                {'label': 'Code postal', 'varname': 'code-postal', 'type': 'string'},
190
                {'label': 'Ville', 'varname': 'ville', 'type': 'string'},
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', 'varname': 'nom', 'type': 'string'},
227
                {'label': 'Prénom(s)', 'varname': 'prenoms', 'type': 'string'},
228
                {'label': 'Numéro', 'varname': 'numero', 'type': 'string'},
229
                {'label': 'Rue', 'varname': 'rue', 'type': 'string'},
230
                {'label': 'Code postal', 'varname': 'code-postal', 'type': 'string'},
231
                {'label': 'Ville', 'varname': 'ville', 'type': 'string'},
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
            {'varname': 'nom', 'label': 'Nom', 'type': 'string'},
253
            {'varname': 'rue', 'label': 'Rue', 'type': 'string'},
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
1642 1642

  
1643 1643

  
1644 1644
class FormBackOfficeStatusPage(FormStatusPage):
1645
    _q_exports = ['', 'download', 'json', 'action', 'inspect']
1645
    _q_exports = ['', 'download', 'json', 'action', 'inspect', 'validate']
1646 1646
    form_page_class = FormFillPage
1647 1647

  
1648 1648
    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
            if 'varname' in meta_field:
766
                metadata_value = getattr(value, 'metadata', {}).get(meta_field['varname'], '')
767
                r += htmltext('<p>%s&nbsp;: %s</p>') % (meta_field['label'], metadata_value)
768
        return r.getvalue()
757 769

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

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

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

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

  
870 893

  
871 894
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 import get_logger
23
from qommon.misc import http_get_page, json_loads, http_post_request, ConnectionError
24
from quixote import get_publisher, get_request
25 25

  
26 26

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

  
30

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

  
39

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

  
43

  
41 44
def get_document_types():
42 45
    if not has_file_validation():
43 46
        return {}
44
    response = fargo_get('/document-types/')
45
    publisher = get_publisher()
47
    try:
48
        response = fargo_get('/document-types/')
49
    except ConnectionError:
50
        get_logger().warning('unable to retrieve document types from fargo')
51
        return {}
46 52
    if response.get('err') == 0:
47 53
        result = {}
48 54
        for schema in response['data']:
......
52 58
                'fargo': True,
53 59
            }
54 60
            if 'mimetypes' in schema:
55
                d['mimetypes'] = shema['mimetypes']
61
                d['mimetypes'] = schema['mimetypes']
56 62
            result[d['id']] = d
57

  
63
            d['metadata'] = schema.get('metadata', [])
58 64
        return result
59 65
    return {}
60 66

  
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'):
67

  
68
def get_validation(url):
69
    try:
70
        response, status, data, auth_header = http_get_page(url)
71
    except ConnectionError:
72
        get_logger().warning('unable to retrieve validation from fargo')
68 73
        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):
74
    if status == 200:
75
        return json_loads(data)['data']
76
    return None
77

  
78

  
79
def get_validations(document_type):
80
    request = get_request()
81
    document_type_id = document_type['id']
82
    qs = {}
83
    if not request.user:
84
        return []
85
    if request.user.name_identifiers:
86
        qs['user_nameid'] = request.user.name_identifiers[0]
87
    elif request.user.email:
88
        qs['user_email'] = request.user.email
89
    else:
90
        return []
91
    path = 'api/validation/%s/' % urllib.quote(document_type_id)
92
    try:
93
        validations = fargo_get(path + '?%s' % urllib.urlencode(qs))
94
    except ConnectionError:
95
        get_logger().warning('unable to retrieve validations from fargo')
96
        return []
97
    if validations and validations.get('data', {}).get('results', []):
98
        return validations['data']['results']
99
    return []
100

  
101

  
102
def is_valid(filled, field, upload):
80 103
    '''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']
104
    return 'url' in getattr(upload, 'metadata', {})
90 105

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

  
94
def validation_link(filled, field, upload):
107
def validate(filled, field, upload):
95 108
    '''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'])
109
    document_type_id = field.document_type['id']
110
    path = 'api/validation/%s/' % urllib.quote(document_type_id)
102 111
    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
    }
112
    url = urlparse.urljoin(fargo_url, path)
113
    payload = {}
114
    if filled.user:
115
        if filled.user.name_identifiers:
116
            payload['user_nameid'] = filled.user.name_identifiers[0]
117
        else:
118
            payload['user_email'] = filled.user.email
119
    payload['origin'] = get_request().get_server()
120
    payload['creator'] = get_request().user.display_name
121
    payload['content_hash'] = sha256_of_upload(upload)
122
    for meta_field in field.metadata:
123
        if 'varname' in meta_field:
124
            payload[meta_field['varname']] = upload.metadata.get(meta_field['varname'], '')
125
    headers = {'Content-Type': 'application/json'}
126
    try:
127
        response, status, response_payload, auth_header = http_post_request(url,
128
                                                                            json.dumps(payload),
129
                                                                            headers=headers)
130
    except ConnectionError:
131
        get_logger().warning('unable to validate document on fargo for %s', filled.get_display_id())
132
        return
133
    if status == 201:
134
        upload.metadata = json_loads(response_payload)['data']
135
        filled.data['%s_structured' % field.id] = upload.metadata
136
        filled.store()
wcs/forms/common.py
17 17
import sys
18 18
import hashlib
19 19
import urlparse
20
import time
20 21

  
21 22
from quixote import get_publisher, get_request, get_response, get_session, redirect
22 23
from quixote.directory import Directory
......
97 98
    def html_top(self, title = None):
98 99
        template.html_top(title = title, default_org = _('Forms'))
99 100

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

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

  
613 635
    def display_file_field(self, form_url, field, value):
614 636
        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:
637
        validated = None
638
        is_fargo_dt = field.document_type.get('fargo', False)
639
        has_metadata = bool(field.document_type.get('metadata', []))
640
        if file_validation.has_file_validation() and is_fargo_dt and has_metadata:
641
            validated = file_validation.is_valid(self.filled, field, value)
642
            if validated is False:
619 643
                extra_class = ' invalid'
620
            elif status is None:
621
                extra_class = ''
622 644
            else:
623 645
                extra_class = ' valid'
624 646
            r += htmltext('<div class="value%s">' % extra_class)
......
627 649
        s = field.get_view_value(value)
628 650
        s = s.replace(str('[download]'), str('%sdownload' % form_url))
629 651
        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')
652
        if validated is not None and get_request().is_in_backoffice():
633 653
            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>')
654
            if validated:
655
                creator = value.metadata['creator']
656
                created = misc.localstrftime(
657
                    time.localtime(
658
                        misc.parse_isotime(value.metadata['created'])))
659
                r += htmltext('<p class="validation-validated">%s</p>') % _(
660
                    'validated by %(creator)s on %(created)s') % {
661
                        'creator': creator, 'created': created}
644 662
                r += htmltext('<p>%s</p>') % (_('Valid from %(start)s to %(end)s') % {
645
                    'start': status['start'],
646
                    'end': status['end'],
663
                    'start': strftime(
664
                        misc.date_format(),
665
                        misc.get_as_datetime(value.metadata['start'])),
666
                    'end': strftime(
667
                        misc.date_format(),
668
                        misc.get_as_datetime(value.metadata['end'])),
647 669
                })
648
            else:
649
                r += file_validation.validation_link(self.filled, field, value)
670
            elif self.filled.user:
671
                r += htmltext('<form method="post" action="./validate?field_id=%s">'
672
                              '<button>%s</button></form>') % (
673
                    field.id, _('Validate'))
650 674
            r += htmltext('</div>')
651 675
        r += htmltext('</div>')
652 676
        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):
605
        from wcs import fields
606

  
596 607
        CompositeWidget.__init__(self, name, value, **kwargs)
597 608
        self.value = value
598
        self.preview = kwargs.get('readonly')
609
        self.document_type = kwargs.pop('document_type', None) or {}
610
        self.readonly = kwargs.get('readonly')
599 611
        self.max_file_size = kwargs.pop('max_file_size', None)
600 612
        self.allow_portfolio_picking = kwargs.pop('allow_portfolio_picking', True)
601 613
        if self.max_file_size:
602 614
            self.max_file_size_bytes = FileSizeWidget.parse_file_size(self.max_file_size)
603 615
        self.add(HiddenWidget, 'token')
604
        if not self.preview:
616
        if not self.readonly:
605 617
            attrs = {'data-url': get_publisher().get_root_url() + 'tmp-upload'}
606 618
            self.file_type = kwargs.pop('file_type', None)
607 619
            if self.file_type:
......
610 622
                # this could be used for client size validation of file size
611 623
                attrs['data-max-file-size'] = str(self.max_file_size_bytes)
612 624
            self.add(FileWidget, 'file', render_br=False, attrs=attrs)
625
        if self.document_type.get('metadata'):
626
            if self.readonly:
627
                self.add(HiddenWidget, 'validation_url')
628
            else:
629
                validations = file_validation.get_validations(self.document_type)
630
                if validations:
631
                    options = [('', _('Known documents'), '')]
632
                    options += [(v['url'], v['display'], v['url']) for v in validations]
633
                    for validation in validations:
634
                        for i, meta_field in enumerate(self.metadata):
635
                            # translate varname to f<self.id>$f<subwidget.id>
636
                            if meta_field['varname'] in validation:
637
                                value = validation.pop(meta_field['varname'])
638
                                validation['f%s' % i] = value
639
                    self.add(SingleSelectWidget, 'validation_url', options=options,
640
                             attrs={'data-validations': json.dumps(validations)})
641
            for i, meta_field in enumerate(self.metadata):
642
                field = fields.get_field_class_by_type(meta_field.get('type', 'string'))()
643
                field.id = i
644
                field.init_with_json(meta_field, include_id=False)
645
                if meta_field.get('varname'):
646
                    subvalue = getattr(value, 'metadata', {}).get(meta_field['varname'])
647
                else:
648
                    subvalue = None
649
                # required only if composite field is required
650
                field.required = field.required and self.required
651
                field.extra_css_class = 'subwidget'
652
                if self.readonly:
653
                    # preview
654
                    field.add_to_view_form(self, subvalue)
655
                else:
656
                    field.add_to_form(self, subvalue)
613 657
        if value:
614 658
            self.set_value(value)
615 659

  
......
625 669
                self.get_widget('token').set_value(self.value.token)
626 670

  
627 671
    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'])
672
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'jquery.iframe-transport.js',
673
                                       'jquery.fileupload.js', 'qommon.fileupload.js'])
631 674

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

  
634 677
        r = TemplateIO(html=True)
635
        for widget in self.get_widgets():
636
            r += widget.render()
678
        if self.get_widget('file'):
679
            r += self.get_widget('file').render()
637 680

  
638 681
        r += htmltext('<div class="fileprogress" style="display: none;">')
639 682
        r += htmltext(' <div class="bar">%s</div>' % _('Upload in progress...'))
640 683
        r += htmltext('</div>')
641
        r += htmltext('<div class="fileinfo"><span class="filename">%s</span>' % temp.get('base_filename', ''))
642
        if not self.preview:
643
            r += htmltext(' <a href="#" class="remove" title="%s">%s</a>' % (
644
                                    _('Remove this file'),
645
                                    _('remove')))
684
        r += htmltext('<div class="fileinfo"><span class="filename">%s</span>'
685
                      % temp.get('base_filename', ''))
686
        if not self.readonly:
687
            r += htmltext(' <a href="#" class="remove" title="%s">%s</a>'
688
                          % (_('Remove this file'), _('remove')))
646 689
        elif temp:
647 690
            filetype = mimetypes.guess_type(temp.get('orig_filename', ''))
648 691
            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'))
651

  
692
                r += htmltext('<img alt="" src="tempfile?t=%s&thumbnail=1" />' %
693
                              self.get('token'))
652 694
        r += htmltext('</div>')
695

  
696
        for widget in self.get_widgets():
697
            if widget is self.get_widget('file'):
698
                continue
699
            r += widget.render()
700

  
653 701
        return r.getvalue()
654 702

  
703
    @property
704
    def metadata(self):
705
        return self.document_type.get('metadata', [])
706

  
655 707
    def _parse(self, request):
656 708
        self.value = None
657 709
        if self.get('token'):
658 710
            token = self.get('token')
711
        elif self.get('validation_url'):
712
            self.value = NoUpload(self.get('validation_url'))
713
            return
659 714
        elif self.get('file'):
660 715
            token = get_session().add_tempfile(self.get('file'))
661 716
            request.form[self.get_widget('token').get_name()] = token
......
664 719

  
665 720
        session = get_session()
666 721
        if token and session.tempfiles and session.tempfiles.has_key(token):
667
            temp = session.tempfiles[token]
668 722
            self.value = session.get_tempfile_content(token)
669 723

  
670 724
        if self.value is None:
671
            # there's no file, the other checks are irrelevant.
725
            # there's no file, check all metadata field are empty too
726
            # if not file and required metadata field become required
727
            if (self.get_widget('file')
728
                    and not self.required
729
                    and any([self.get('f%s' % i) for i, meta_field in enumerate(self.metadata)])):
730
                self.get_widget('file').required = True
731
                self.get_widget('file').set_error(self.REQUIRED_ERROR)
732
                for i, meta_field in enumerate(self.metadata):
733
                    name = 'f%s' % i
734
                    required = meta_field.get('required', True)
735
                    if required:
736
                        widget = self.get_widget(name)
737
                        widget.required = True
738
                        if not self.get(name):
739
                            widget.set_error(self.REQUIRED_ERROR)
672 740
            return
673 741

  
742
        # There is some file, check all required metadata files have been filled
743
        for i, meta_field in enumerate(self.metadata):
744
            name = 'f%s' % i
745
            required = meta_field.get('required', True)
746
            if required:
747
                widget = self.get_widget(name)
748
                widget.required = True
749
                if not self.get(name):
750
                    widget.set_error(self.REQUIRED_ERROR)
751

  
752
        if self.metadata:
753
            self.value.metadata = {}
754
            for i, meta_field in enumerate(self.metadata):
755
                name = 'f%s' % i
756
                if 'varname' in meta_field:
757
                    self.value.metadata[meta_field['varname']] = self.get(name)
758

  
674 759
        # Don't trust the browser supplied MIME type, update the Upload object
675 760
        # with a MIME type created with magic (or based on the extension if the
676 761
        # module is missing).
wcs/qommon/static/css/qommon.css
87 87
	display: inline-block;
88 88
}
89 89

  
90
/* nested widgets */
91
.widget .subwidget {
92
	padding-left: 2em;
93
}
94
div.FileWithPreviewWidget .validation {
95
	display: inline-block;
96
	font-size: xx-small;
97
	margin-right: 2em;
98
}
99

  
90 100
div.form .title, form.quixote .title {
91 101
	font-weight: bold;
92 102
}
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
-