Projet

Général

Profil

0003-rewrite-file-validation-fixes-10444.patch

Benjamin Dauvergne, 29 mars 2016 12:58

Télécharger (39,6 ko)

Voir les différences:

Subject: [PATCH 3/3] rewrite file validation (fixes #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.
- rewrittent tests around file validation
 tests/test_backoffice_pages.py            | 205 +++++++++++++++++++++++++-----
 tests/test_form_pages.py                  |  74 ++++++-----
 tests/test_formdata.py                    |  86 +++++++++++++
 wcs/fields.py                             |  17 ++-
 wcs/file_validation.py                    | 114 +++++++++--------
 wcs/forms/common.py                       |  55 ++++----
 wcs/qommon/form.py                        |  62 ++++++++-
 wcs/qommon/static/css/qommon.css          |  10 ++
 wcs/qommon/static/js/qommon.fileupload.js |  43 ++++++-
 9 files changed, 518 insertions(+), 148 deletions(-)
tests/test_backoffice_pages.py
1
# -*- coding: utf-8 -*-
1 2
import datetime
2 3
import json
3 4
import os
......
14 15
from quixote import cleanup, get_publisher
15 16
from wcs.qommon import errors, sessions
16 17
from qommon.ident.password_accounts import PasswordAccount
18
from qommon.misc import json_loads
17 19
from wcs.qommon.http_request import HTTPRequest
18 20
from wcs.roles import Role
19 21
from wcs.workflows import (Workflow, CommentableWorkflowStatusItem,
......
1577 1579
    assert resp.headers['content-type'] == 'application/javascript'
1578 1580

  
1579 1581
def test_backoffice_file_field_validation(pub, fargo_url):
1582
    document_type = {
1583
        'id': 'justificatif-de-domicile',
1584
        'fargo': True,
1585
        'mimetypes': ['application/pdf'],
1586
        'label': 'Justificatif de domicile',
1587
        'metadata': [
1588
            {'label': 'Nom', 'name': 'nom'},
1589
            {'label': 'Prénom(s)', 'name': 'prenoms'},
1590
            {'label': 'Numéro', 'name': 'numero'},
1591
            {'label': 'Rue', 'name': 'rue'},
1592
            {'label': 'Code postal', 'name': 'code-postal'},
1593
            {'label': 'Ville', 'name': 'ville'},
1594
        ],
1595
    }
1596
    metadata = {
1597
        'nom': 'Doe',
1598
        'prenoms': 'John',
1599
        'numero': '169',
1600
        'rue': 'rue du château',
1601
        'code-postal': '75014',
1602
        'ville': 'PARIS',
1603
    }
1580 1604
    user = create_user(pub, is_admin=True)
1581 1605
    user.name_identifiers = ['12345']
1582 1606
    user.store()
......
1585 1609
    formdef.name = 'form title'
1586 1610
    formdef.fields = [fields.FileField(
1587 1611
        id='0', label='1st field', type='file',
1588
        document_type={
1589
            'id': 'justificatif-de-domicile',
1590
            'fargo': True,
1591
            'mimetypes': ['application/pdf'],
1592
            'label': 'PDF files',
1593
        })
1594
    ]
1612
        document_type=document_type)]
1595 1613
    formdef.store()
1596 1614
    formdef.data_class().wipe()
1597 1615
    upload = Upload('test.pdf', '%PDF-1.4', 'application/pdf')
1598 1616
    digest = hashlib.sha256('%PDF-1.4').hexdigest()
1599 1617
    app = login(get_app(pub))
1600
    resp = app.get('/form-title/')
1618
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1619
        fargo_get.return_value = {'result': 1, 'data': {'results': []}}
1620
        resp = app.get('/form-title/')
1621
        fargo_get.assert_called_once_with(
1622
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
1601 1623
    resp.forms[0]['f0$file'] = upload
1602
    resp = resp.forms[0].submit('submit')
1624
    for key in metadata:
1625
        resp.forms[0]['f0$%s' % key] = metadata[key]
1626
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1627
        fargo_get.return_value = {'result': 1, 'data': {'results': []}}
1628
        resp = resp.forms[0].submit('submit')
1629
        fargo_get.assert_called_once_with(
1630
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
1631
    for key in metadata:
1632
        assert 'value="%s"' % metadata[key] in resp.body
1603 1633
    assert 'Check values then click submit.' in resp.body
1604
    resp = resp.forms[0].submit('submit')
1605
    assert resp.status_int == 302
1634
    resp = resp.forms[0].submit('submit').follow()
1635
    for metadata_field in document_type['metadata']:
1636

  
1637
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['name']])
1638
        assert fragment in resp.body
1606 1639
    assert formdef.data_class().count() == 1
1607
    form_id = formdef.data_class().select()[0].id
1608
    with mock.patch('wcs.file_validation.http_get_page') as http_get_page:
1609
        json_response = json.dumps({
1610
            'err': 0,
1611
            'data': {
1612
                'type': 'justificatif-de-domicile',
1613
                'label': 'Justificatif de domicile',
1614
                'creator': 'Jean Bono',
1615
                'created': '2014-01-01T01:01:01',
1616
                'start': '2014-01-01T01:01:01',
1617
                'end': '2014-01-01T01:01:01',
1618
                'metadata': [{
1619
                        'name': 'code_postal',
1620
                        'label': 'Code postal',
1621
                        'value': '13400',
1622
                }],
1623
            },
1640
    formdata = formdef.data_class().select()[0]
1641
    form_id = formdata.id
1642
    assert formdata.data['0'].metadata == metadata
1643
    assert formdata.data['0_structured'] == metadata
1644
    resp = app.get('/backoffice/management/form-title/%s/' % form_id)
1645
    for metadata_field in document_type['metadata']:
1646

  
1647
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['name']])
1648
        assert fragment in resp.body
1649
    with mock.patch('wcs.file_validation.http_post_request') as http_post_request:
1650
        payload = {
1651
            'user_nameid': '12345',
1652
            'origin': 'example.net',
1653
            'creator': 'admin',
1654
            'content_hash': digest,
1655
        }
1656
        payload.update(metadata)
1657
        result = {
1658
            'result': 1,
1659
            'data': payload.copy()
1660
        }
1661
        result['data'].update({
1662
            'url': 'zob',
1663
            'created': '1970-01-01T10:10:10',
1664
            'start': '1970-01-01',
1665
            'end': '1978-01-01',
1666
            'display': 'John Doe, 169 rue du château, 75014 PARIS'
1624 1667
        })
1625
        http_get_page.return_value = None, 200, json_response, None
1626
        resp = app.get('/backoffice/management/form-title/%s/' % form_id)
1627
        http_get_page.assert_called_once_with('http://fargo.example.net/metadata/12345/%s/justificatif-de-domicile/' % digest)
1628
        assert 'class="value valid"' in resp.body
1629
        assert 'Justificatif de domicile validated by Jean Bono on 2014-01-01T01:01:01' in resp.body
1630
        assert 'Code postal: 13400' in resp.body
1631
        assert 'Valid from 2014-01-01T01:01:01 to 2014-01-01T01:01:01' in resp.body
1668
        http_post_request.return_value = None, 201, json.dumps(result), None
1669
        resp = app.get('/backoffice/management/form-title/%s/validate?field_id=0' % form_id)
1670
        assert http_post_request.call_count == 1
1671
        assert http_post_request.call_args[0][0] == 'http://fargo.example.net/api/validation/justificatif-de-domicile/'
1672
        assert json_loads(http_post_request.call_args[0][1]) == payload
1673
        assert http_post_request.call_args[1] == {'headers': {'Content-Type': 'application/json'}}
1674
    resp = resp.follow()
1675

  
1676
    assert 'Valid from 1970-01-01 to 1978-01-01' in resp.body
1677

  
1678

  
1679
def test_backoffice_file_validation_no_upload(pub, fargo_url):
1680
    document_type = {
1681
        'id': 'justificatif-de-domicile',
1682
        'fargo': True,
1683
        'mimetypes': ['application/pdf'],
1684
        'label': 'Justificatif de domicile',
1685
        'metadata': [
1686
            {'label': 'Nom', 'name': 'nom'},
1687
            {'label': 'Prénom(s)', 'name': 'prenoms'},
1688
            {'label': 'Numéro', 'name': 'numero'},
1689
            {'label': 'Rue', 'name': 'rue'},
1690
            {'label': 'Code postal', 'name': 'code-postal'},
1691
            {'label': 'Ville', 'name': 'ville'},
1692
        ],
1693
    }
1694
    metadata = {
1695
        'nom': 'Doe',
1696
        'prenoms': 'John',
1697
        'numero': '169',
1698
        'rue': 'rue du château',
1699
        'code-postal': '75014',
1700
        'ville': 'PARIS',
1701
    }
1702
    validation = {
1703
        'url': 'zob',
1704
        'creator': 'admin',
1705
        'created': '1970-01-01T10:10:10',
1706
        'start': '1970-01-01',
1707
        'end': '1978-01-01',
1708
        'display': 'John Doe, 169 rue du château, 75014 PARIS'
1709
    }
1710
    validation.update(metadata)
1711

  
1712
    user = create_user(pub, is_admin=True)
1713
    user.name_identifiers = ['12345']
1714
    user.store()
1715
    FormDef.wipe()
1716
    formdef = FormDef()
1717
    formdef.name = 'form title'
1718
    formdef.fields = [fields.FileField(
1719
        id='0', label='1st field', type='file',
1720
        document_type=document_type)]
1721
    formdef.store()
1722
    formdef.data_class().wipe()
1723
    app = login(get_app(pub))
1724
    return_value = {'result': 1, 'data': {'results': [validation]}}
1725
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
1726
        fargo_get.return_value = return_value
1727
        resp = app.get('/form-title/')
1728
        fargo_get.assert_called_once_with(
1729
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
1730
        assert validation['display'] in resp.body
1731
    resp.forms[0]['f0$validation_url'] = 'zob'
1732
    for key in metadata:
1733
        resp.forms[0]['f0$%s' % key] = metadata[key]
1734
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get, \
1735
            mock.patch('wcs.file_validation.http_get_page') as http_get_page:
1736
        fargo_get.return_value = return_value
1737
        return_value = {
1738
            'result': 1,
1739
            'data': validation,
1740
        }
1741
        http_get_page.return_value = None, 200, json.dumps(return_value), None
1742
        resp = resp.forms[0].submit('submit')
1743
        fargo_get.assert_called_once_with(
1744
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
1745
        http_get_page.assert_called_with('zob')
1746
        for key in metadata:
1747
            assert 'value="%s"' % metadata[key] in resp.body
1748
        assert 'Check values then click submit.' in resp.body
1749
        resp = resp.forms[0].submit('submit').follow()
1750
    assert formdef.data_class().count() == 1
1751
    formdata = formdef.data_class().select()[0]
1752
    form_id = formdata.id
1753
    assert formdata.data['0'].metadata == validation
1754
    assert formdata.data['0_structured'] == validation
1755
    for metadata_field in document_type['metadata']:
1756

  
1757
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['name']])
1758
        assert fragment in resp.body
1759
    resp = app.get('/backoffice/management/form-title/%s/' % form_id)
1760
    assert 'Code postal : 75014' in resp.body
1761
    for metadata_field in document_type['metadata']:
1762

  
1763
        fragment = '%s : %s' % (metadata_field['label'], metadata[metadata_field['name']])
1764
        assert fragment in resp.body
1765
    assert 'Valid from 1970-01-01 to 1978-01-01' in resp.body
1766

  
1632 1767

  
1633 1768
def test_360_user_view(pub):
1634 1769
    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
......
2289 2290
    assert formdef.data_class().select()[0].data['1'] == 'foobar3'
2290 2291

  
2291 2292
def test_file_field_validation(pub, fargo_url):
2293
    document_type = {
2294
        'id': 'justificatif-de-domicile',
2295
        'fargo': True,
2296
        'label': 'Justificatif de domicile',
2297
        'metadata': [
2298
            {'label': 'Nom', 'name': 'nom'},
2299
            {'label': 'Prénom(s)', 'name': 'prenoms'},
2300
            {'label': 'Numéro', 'name': 'numero'},
2301
            {'label': 'Rue', 'name': 'rue'},
2302
            {'label': 'Code postal', 'name': 'code-postal'},
2303
            {'label': 'Ville', 'name': 'ville'},
2304
        ],
2305
    }
2306
    metadata = {
2307
        'nom': 'Doe',
2308
        'prenoms': 'John',
2309
        'numero': '169',
2310
        'rue': 'rue du château',
2311
        'code-postal': '75014',
2312
        'ville': 'PARIS',
2313
    }
2292 2314
    user = create_user(pub)
2293 2315
    user.name_identifiers = ['12345']
2294 2316
    user.store()
......
2297 2319
    formdef.name = 'form title'
2298 2320
    formdef.fields = [fields.FileField(
2299 2321
        id='0', label='1st field', type='file',
2300
        document_type={
2301
            'id': 'justificatif-de-domicile',
2302
            'fargo': True,
2303
            'mimetypes': ['application/pdf'],
2304
            'label': 'PDF files',
2305
        })
2322
        document_type=document_type)
2306 2323
    ]
2307 2324
    formdef.store()
2308 2325
    formdef.data_class().wipe()
2309 2326
    upload = Upload('test.pdf', '%PDF-1.4', 'application/pdf')
2310
    digest = hashlib.sha256('%PDF-1.4').hexdigest()
2311 2327
    app = login(get_app(pub), username='foo', password='foo')
2312
    resp = app.get('/form-title/')
2328
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
2329
        fargo_get.return_value = {'result': 1, 'data': {'results': []}}
2330
        resp = app.get('/form-title/')
2331
        fargo_get.assert_called_once_with(
2332
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
2313 2333
    resp.forms[0]['f0$file'] = upload
2314
    resp = resp.forms[0].submit('submit')
2334
    for key in metadata:
2335
        resp.forms[0]['f0$%s' % key] = metadata[key]
2336
    with mock.patch('wcs.file_validation.fargo_get') as fargo_get:
2337
        fargo_get.return_value = {'result': 1, 'data': {'results': []}}
2338
        resp = resp.forms[0].submit('submit')
2339
        fargo_get.assert_called_once_with(
2340
            'api/validation/justificatif-de-domicile/?user_nameid=12345')
2315 2341
    assert 'Check values then click submit.' in resp.body
2316 2342
    resp = resp.forms[0].submit('submit')
2317 2343
    assert resp.status_int == 302
2318
    with mock.patch('wcs.file_validation.http_get_page') as http_get_page:
2319
        json_response = json.dumps({
2320
            'err': 0,
2321
            'data': {
2322
                'type': 'justificatif-de-domicile',
2323
                'label': 'Justificatif de domicile',
2324
                'creator': 'Jean Bono',
2325
                'created': '2014-01-01T01:01:01',
2326
                'start': '2014-01-01T01:01:01',
2327
                'end': '2014-01-01T01:01:01',
2328
                'metadata': [{
2329
                        'name': 'code_postal',
2330
                        'label': 'Code postal',
2331
                        'value': '13400',
2332
                }],
2333
            },
2334
        })
2335
        http_get_page.return_value = None, 200, json_response, None
2336
        resp = resp.follow()
2337
        http_get_page.assert_called_once_with('http://fargo.example.net/metadata/12345/%s/justificatif-de-domicile/' % digest)
2338
        http_get_page.reset_mock()
2339
        assert 'The form has been recorded' in resp.body
2340
        assert 'class="value valid"' in resp.body
2344
    resp = resp.follow()
2345
    assert 'The form has been recorded' in resp.body
2346
    assert formdef.data_class().count() == 1
2347
    formdata = formdef.data_class().select()[0]
2348
    assert formdata.data['0'].metadata == metadata
2349
    assert formdata.data['0_structured'] == metadata
2350

  
2341 2351

  
2342 2352
def test_form_string_field_autocomplete(pub):
2343 2353
    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_with_metadata(pub):
156
    document_types = {
157
        'justificatif-de-domicile': {
158
            'id': 'justificatif-de-domicile',
159
            'fargo': True,
160
            'label': 'Justificatif de domicile',
161
            'metadata': [
162
                {'label': 'Nom', 'name': 'nom'},
163
                {'label': 'Prénom(s)', 'name': 'prenoms'},
164
                {'label': 'Numéro', 'name': 'numero'},
165
                {'label': 'Rue', 'name': 'rue'},
166
                {'label': 'Code postal', 'name': 'code-postal'},
167
                {'label': 'Ville', 'name': 'ville'},
168
            ],
169
        }
170
    }
171
    formdef.data_class().wipe()
172
    formdef.fields = [fields.FileField(id='0', label='file', varname='foo',
173
                                       document_type=document_types['justificatif-de-domicile'])]
174
    formdef.store()
175
    formdata = formdef.data_class()()
176
    upload = Upload('test.txt', 'text/plain', 'ascii')
177
    upload.receive(['first line', 'second line'])
178
    upload.metadata = {
179
        'nom': 'Doe',
180
        'prenoms': 'John',
181
        'numero': '169',
182
        'rue': 'rue du château',
183
        'code-postal': '75014',
184
        'ville': 'PARIS',
185
    }
186
    formdata.data = {'0': upload, '0_structured': upload.metadata}
187
    formdata.id = 1
188
    substvars = formdata.get_substitution_variables()
189
    assert substvars.get('form_var_foo') == 'test.txt'
190
    assert substvars.get('form_var_foo_url').endswith('/foobar/1/download?f=0')
191
    assert isinstance(substvars.get('form_var_foo_raw'), Upload)
192
    for key in upload.metadata:
193
        assert substvars.get('form_var_foo_%s' % key) == upload.metadata[key]
194

  
195

  
196
def test_file_field_no_file_with_metadata(pub):
197
    document_types = {
198
        'justificatif-de-domicile': {
199
            'id': 'justificatif-de-domicile',
200
            'fargo': True,
201
            'label': 'Justificatif de domicile',
202
            'metadata': [
203
                {'label': 'Nom', 'name': 'nom'},
204
                {'label': 'Prénom(s)', 'name': 'prenoms'},
205
                {'label': 'Numéro', 'name': 'numero'},
206
                {'label': 'Rue', 'name': 'rue'},
207
                {'label': 'Code postal', 'name': 'code-postal'},
208
                {'label': 'Ville', 'name': 'ville'},
209
            ],
210
        }
211
    }
212
    formdef.data_class().wipe()
213
    formdef.fields = [fields.FileField(id='0', label='file', varname='foo',
214
                                       document_type=document_types['justificatif-de-domicile'])]
215
    formdef.store()
216
    formdata = formdef.data_class()()
217
    metadata = {
218
        'nom': 'Doe',
219
        'prenoms': 'John',
220
        'numero': '169',
221
        'rue': 'rue du château',
222
        'code-postal': '75014',
223
        'ville': 'PARIS',
224
    }
225
    with mock.patch('wcs.file_validation.get_validation', return_value=metadata):
226
        upload = NoUpload('http://whatever.com/')
227
    formdata.data = {'0': upload, '0_structured': upload.metadata}
228
    formdata.id = 1
229
    substvars = formdata.get_substitution_variables()
230
    assert isinstance(substvars.get('form_var_foo'), NoUpload)
231
    assert not 'form_var_foo_url' in substvars
232
    assert not 'form_var_foo_raw' in substvars
233
    for key in upload.metadata:
234
        assert substvars.get('form_var_foo_%s' % key) == upload.metadata[key]
235

  
236

  
151 237
def test_get_submitter(pub):
152 238
    formdef.data_class().wipe()
153 239
    formdef.fields = [fields.StringField(id='0', label='email', varname='foo',
wcs/fields.py
720 720
        self.document_type = self.document_type or {}
721 721

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

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

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

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

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

  
792 803
    def get_document_types(self):
793 804
        document_types = {
......
867 878
        if self.document_type and self.document_type.get('fargo'):
868 879
            self.document_type['fargo'] = self.document_type['fargo'] == 'True'
869 880

  
881
    def store_structured_value(self, data, field_id):
882
        value = data.get(field_id)
883
        return getattr(value, 'metadata', {})
884

  
870 885

  
871 886
register_field_class(FileField)
872 887

  
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
        value = self.filled.data.get(field_id)
116
        file_validation.validate(self.filled, field, value)
117
        return redirect('.')
118

  
100 119
    def __init__(self, formdef, filled, register_workflow_subdirs=True):
101 120
        get_publisher().substitutions.feed(filled)
102 121
        self.formdef = formdef
......
612 631

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

  
63 64
QuixoteForm = Form
64 65

  
......
581 582
            self.value = None
582 583

  
583 584

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

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

  
592

  
584 593
class FileWithPreviewWidget(CompositeWidget):
585 594
    """Widget that proposes a File Upload widget but that stores the file
586 595
    ondisk so it has a "readonly" mode where the filename is shown."""
......
591 600

  
592 601
    max_file_size_bytes = None # will be filled automatically
593 602

  
603
    PARTIAL_FILL_ERROR = _('This field is normally not required, but you must leave it completely '
604
                           'empty or fill it, You cannot fill it partially')
605

  
594 606
    def __init__(self, name, value=None, **kwargs):
595 607
        CompositeWidget.__init__(self, name, value, **kwargs)
596 608
        self.value = value
609
        self.document_type = kwargs.pop('document_type', None) or {}
597 610
        self.preview = kwargs.get('readonly')
598 611
        self.max_file_size = kwargs.pop('max_file_size', None)
599 612
        self.allow_portfolio_picking = kwargs.pop('allow_portfolio_picking', True)
......
609 622
                # this could be used for client size validation of file size
610 623
                attrs['data-max-file-size'] = str(self.max_file_size_bytes)
611 624
            self.add(FileWidget, 'file', render_br=False, attrs=attrs)
625
        if self.document_type.get('metadata'):
626
            if self.preview:
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
                    self.add(SingleSelectWidget, 'validation_url', options=options,
634
                             attrs={'data-validations': json.dumps(validations)})
635
            for meta_field in self.metadata:
636
                subvalue = getattr(value, 'metadata', {}).get(meta_field['name'])
637
                self.add(StringWidget, meta_field['name'],
638
                         title=meta_field['label'],
639
                         required=meta_field.get('required', True) and self.required,
640
                         readonly=self.preview,
641
                         value=subvalue)
642
                self.get_widget(meta_field['name']).extra_css_class = 'subwidget'
612 643
        if value:
613 644
            self.set_value(value)
614 645

  
......
651 682
        r += htmltext('</div>')
652 683
        return r.getvalue()
653 684

  
685
    @property
686
    def metadata(self):
687
        return self.document_type.get('metadata', [])
688

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

  
664 702
        session = get_session()
665 703
        if token and session.tempfiles and session.tempfiles.has_key(token):
666
            temp = session.tempfiles[token]
667 704
            self.value = session.get_tempfile_content(token)
668 705

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

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

  
725

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

  
673 731
        # Don't trust the browser supplied MIME type, update the Upload object
674 732
        # with a MIME type created with magic (or based on the extension if the
675 733
        # 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
            base_widget.find('.subwidget input').val('');
17
            base_widget.find('.subwidget input').prop('disabled', false);
18
        }
9 19
        $(this).find('input[type=file]').fileupload({
10 20
            dataType: 'json',
11 21
            add: function (e, data) {
......
19 29
                $(base_widget).find('.fileprogress').hide();
20 30
                $(base_widget).find('.filename').text(data.result[0].name);
21 31
                $(base_widget).find('.fileinfo').show();
22
                $(base_widget).find('input[type=hidden]').val(data.result[0].token);
32
                $(base_widget).find('input[name$="$token"]').val(data.result[0].token);
33
                $(base_widget).find('input[name$="$validation_url"]').val('');
34
                enable();
23 35
                $(base_widget).parents('form').find('input[name=submit]').prop('disabled', false);
24 36
                $(this).hide();
25 37
            },
......
29 41
            }
30 42
        });
31 43
        $(this).find('a.remove').click(function() {
32
            $(base_widget).find('input[type=hidden]').val('');
44
            $(base_widget).find('input[name$="$token"]').val('');
33 45
            $(base_widget).find('.fileinfo').hide();
34 46
            $(base_widget).find('input[type=file]').show();
35 47
            return false;
......
38 50
            $(base_widget).find('input[type=file]').click();
39 51
            return false;
40 52
        });
53
        if ($(this).find('select[name$="$validation_url"] option:selected').val()) {
54
            disable();
55
        }
56
        $(this).find('select[name$="$validation_url"]').on('change', function() {
57
            var url = $(this).find(':selected').val();
58
            if (url) {
59
                var validations = $(this).data('validations');
60

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