Projet

Général

Profil

0003-backoffice-add-json-import-for-cards-60303.patch

Frédéric Péters, 02 août 2022 17:51

Télécharger (35,7 ko)

Voir les différences:

Subject: [PATCH 3/3] backoffice: add json import for cards (#60303)

 tests/api/test_carddef.py                     | 107 +++++++
 tests/backoffice_pages/test_carddata.py       | 264 +++++++++++++++++-
 wcs/api.py                                    |  43 ++-
 wcs/backoffice/data_management.py             | 131 +++++++--
 wcs/backoffice/management.py                  |  13 +-
 wcs/formdata.py                               |  56 +++-
 .../wcs/backoffice/card-data-import-form.html |  20 +-
 wcs/workflows.py                              |   1 +
 8 files changed, 563 insertions(+), 72 deletions(-)
tests/api/test_carddef.py
511 511
    assert resp.json['data']['creation_time'] <= resp.json['data']['completion_time']
512 512

  
513 513

  
514
@pytest.mark.parametrize('auth', ['signature', 'http-basic'])
515
def test_cards_import_json(pub, local_user, auth):
516
    pub.role_class.wipe()
517
    role = pub.role_class(name='test')
518
    role.store()
519
    local_user.roles = [role.id]
520
    local_user.store()
521

  
522
    ApiAccess.wipe()
523
    access = ApiAccess()
524
    access.name = 'test'
525
    access.access_identifier = 'test'
526
    access.access_key = '12345'
527
    access.store()
528

  
529
    app = get_app(pub)
530

  
531
    if auth == 'http-basic':
532
        access.roles = [role]
533
        access.store()
534

  
535
        def get_url(url, **kwargs):
536
            app.set_authorization(('Basic', ('test', '12345')))
537
            return app.get(url, **kwargs)
538

  
539
        def put_url(url, *args, **kwargs):
540
            app.set_authorization(('Basic', ('test', '12345')))
541
            return app.put_json(url, *args, **kwargs)
542

  
543
    else:
544

  
545
        def get_url(url, **kwargs):
546
            return app.get(
547
                sign_uri(url, user=local_user, orig=access.access_identifier, key=access.access_key), **kwargs
548
            )
549

  
550
        def put_url(url, *args, **kwargs):
551
            return app.put_json(
552
                sign_uri(url, user=local_user, orig=access.access_identifier, key=access.access_key),
553
                *args,
554
                **kwargs,
555
            )
556

  
557
    CardDef.wipe()
558
    carddef = CardDef()
559
    carddef.name = 'test'
560
    carddef.fields = [
561
        fields.StringField(id='0', label='foobar', varname='foo'),
562
        fields.StringField(id='1', label='foobar2', varname='foo2'),
563
    ]
564
    carddef.workflow_roles = {'_viewer': role.id}
565
    carddef.backoffice_submission_roles = [role.id]
566
    carddef.digest_templates = {'default': 'bla {{ form_var_foo }} xxx'}
567
    carddef.store()
568

  
569
    carddef.data_class().wipe()
570

  
571
    get_app(pub).get(sign_uri('/api/cards/test/import-json'), status=405)
572
    get_app(pub).put(sign_uri('/api/cards/test/import-json'), status=403)
573
    data = {
574
        'data': [
575
            {
576
                'fields': {
577
                    'foo': 'first entry',
578
                    'foo2': 'plop',
579
                }
580
            },
581
            {
582
                'fields': {
583
                    'foo': 'second entry',
584
                    'foo2': 'plop',
585
                }
586
            },
587
        ]
588
    }
589
    resp = put_url(
590
        '/api/cards/test/import-json',
591
        data,
592
        headers={'content-type': 'application/json'},
593
    )
594
    assert resp.json == {'err': 0}
595
    assert carddef.data_class().count() == 2
596
    assert {x.data['0'] for x in carddef.data_class().select()} == {'first entry', 'second entry'}
597

  
598
    # async mode
599
    carddef.data_class().wipe()
600
    assert carddef.data_class().count() == 0
601
    resp = put_url(
602
        '/api/cards/test/import-json?async=on',
603
        data,
604
        headers={'content-type': 'application/json'},
605
    )
606
    # afterjobs are not async in tests: job is already completed during request
607
    assert carddef.data_class().count() == 2
608
    assert {x.data['0'] for x in carddef.data_class().select()} == {'first entry', 'second entry'}
609
    assert resp.json['err'] == 0
610
    assert 'job' in resp.json['data']
611
    job_id = resp.json['data']['job']['id']
612
    assert AfterJob.get(job_id).status == 'completed'
613
    # get job status from its api url
614
    resp = get_url(resp.json['data']['job']['url'])
615
    assert resp.json['err'] == 0
616
    assert resp.json['data']['label'] == 'Importing data into cards'
617
    assert resp.json['data']['status'] == 'completed'
618
    assert resp.json['data']['creation_time'] <= resp.json['data']['completion_time']
619

  
620

  
514 621
def test_cards_restricted_api(pub, local_user):
515 622
    pub.role_class.wipe()
516 623
    role = pub.role_class(name='test')
tests/backoffice_pages/test_carddata.py
1
import base64
1 2
import datetime
3
import json
2 4
import os
3 5
import uuid
4 6

  
......
11 13
from wcs.categories import CardDefCategory
12 14
from wcs.formdef import FormDef
13 15
from wcs.qommon.http_request import HTTPRequest
14
from wcs.workflows import Workflow
16
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef
15 17

  
16 18
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
17 19
from .test_all import create_superuser, create_user
......
372 374
    app = login(get_app(pub))
373 375

  
374 376
    resp = app.get(carddef.get_url())
375
    assert 'Import data from a CSV file' not in resp.text
376
    resp = app.get(carddef.get_url() + 'import-csv', status=403)
377
    assert 'Import data from a file' not in resp.text
378
    resp = app.get(carddef.get_url() + 'import-file', status=403)
377 379

  
378 380
    carddef.backoffice_submission_roles = user.roles
379 381
    carddef.store()
380 382

  
381 383
    resp = app.get(carddef.get_url())
382
    resp = resp.click('Import data from a CSV file')
384
    resp = resp.click('Import data from a file')
383 385

  
384 386
    assert 'Table, File are required but cannot be filled from CSV.' in resp
385
    assert 'Download sample file for this card' not in resp
387
    assert 'Download sample CSV file for this card' not in resp
386 388
    carddef.fields[0].required = False
387 389
    carddef.fields[7].required = False
388 390
    carddef.store()
389 391

  
390 392
    resp = app.get(carddef.get_url())
391
    resp = resp.click('Import data from a CSV file')
392
    sample_resp = resp.click('Download sample file for this card')
393
    resp = resp.click('Import data from a file')
394
    sample_resp = resp.click('Download sample CSV file for this card')
393 395
    today = datetime.date.today()
394 396
    assert sample_resp.text == (
395 397
        '"Table","Map","Test","Boolean","List","Date","File","Email","Long","List2","Items"\r\n'
......
481 483
        b'foobar,item2',
482 484
        b'foobar@mail.com,item2',
483 485
    ]
484
    resp = app.get('/backoffice/data/test/import-csv')
486
    resp = app.get('/backoffice/data/test/import-file')
485 487
    resp.forms[0]['file'] = Upload('test.csv', b'\n'.join(data), 'text/csv')
486 488
    resp = resp.forms[0].submit().follow()
487 489
    assert carddef.data_class().count() == 5
......
504 506
        b'foobar',
505 507
        b'foobar@mail.com',
506 508
    ]
507
    resp = app.get('/backoffice/data/test/import-csv')
509
    resp = app.get('/backoffice/data/test/import-file')
508 510
    resp.forms[0]['file'] = Upload('test.csv', b'\n'.join(data), 'text/csv')
509 511
    resp = resp.forms[0].submit().follow()
510 512
    assert carddef.data_class().count() == 4
......
529 531

  
530 532
    app = login(get_app(pub))
531 533
    resp = app.get(carddef.get_url())
532
    resp = resp.click('Import data from a CSV file')
534
    resp = resp.click('Import data from a file')
533 535

  
534 536
    csv_data = '''String1,String2,Text
535 537
1,2,3
......
551 553
    assert '(line numbers 4, 5, 7, 8, 9 and more)' in resp.text
552 554

  
553 555

  
556
def test_backoffice_cards_import_data_from_json(pub):
557
    user = create_user(pub)
558

  
559
    data_source = {
560
        'type': 'formula',
561
        'value': repr([{'id': '1', 'text': 'un', 'more': 'foo'}, {'id': '2', 'text': 'deux', 'more': 'bar'}]),
562
    }
563

  
564
    CardDef.wipe()
565
    carddef = CardDef()
566
    carddef.name = 'test'
567
    carddef.fields = [
568
        fields.StringField(id='1', label='Test', varname='string'),
569
        fields.BoolField(id='2', label='Boolean', varname='bool'),
570
        fields.ItemField(id='3', label='List', varname='item', data_source=data_source),
571
        fields.FileField(id='4', label='File', varname='file'),
572
        fields.DateField(id='5', label='Date', varname='date'),
573
    ]
574
    carddef.workflow_roles = {'_editor': user.roles[0]}
575
    carddef.backoffice_submission_roles = user.roles
576
    carddef.store()
577
    carddef.data_class().wipe()
578

  
579
    app = login(get_app(pub))
580
    resp = app.get(carddef.get_url())
581
    resp = resp.click('Import data from a file')
582
    data = {
583
        'data': [
584
            {
585
                'fields': {
586
                    'string': 'a string',
587
                    'bool': True,
588
                    'item': '1',
589
                    'file': {
590
                        'filename': 'test.txt',
591
                        'content_type': 'application/pdf',
592
                        'content': base64.encodebytes(b'%PDF-1.4 ...').decode(),
593
                    },
594
                    'date': '2022-07-19',
595
                }
596
            }
597
        ]
598
    }
599
    resp.forms[0]['file'] = Upload('test.json', json.dumps(data).encode(), 'application/json')
600
    resp = resp.forms[0].submit()
601
    assert '/backoffice/processing?job=' in resp.location
602
    resp = resp.follow()
603

  
604
    carddata_export = carddef.data_class().select()[0].get_json_export_dict()
605
    assert carddata_export['workflow']['status']['id'] == 'recorded'
606
    assert carddata_export['fields'] == {
607
        'string': 'a string',
608
        'bool': True,
609
        'item': 'un',
610
        'item_raw': '1',
611
        'item_structured': {'id': '1', 'more': 'foo', 'text': 'un'},
612
        'file': {
613
            'field_id': '4',
614
            'filename': 'test.txt',
615
            'content_type': 'application/pdf',
616
            'content': base64.encodebytes(b'%PDF-1.4 ...').decode().strip(),
617
            'url': 'http://example.net/api/cards/test/1/download?f=4',
618
        },
619
        'date': '2022-07-19',
620
    }
621

  
622

  
623
def test_backoffice_cards_import_data_with_no_varname_from_json(pub):
624
    user = create_user(pub)
625

  
626
    CardDef.wipe()
627
    carddef = CardDef()
628
    carddef.name = 'test'
629
    carddef.fields = [
630
        fields.StringField(id='1', label='Test'),
631
    ]
632
    carddef.workflow_roles = {'_editor': user.roles[0]}
633
    carddef.backoffice_submission_roles = user.roles
634
    carddef.store()
635
    carddef.data_class().wipe()
636

  
637
    app = login(get_app(pub))
638
    resp = app.get(carddef.get_url())
639
    resp = resp.click('Import data from a file')
640
    data = {
641
        'data': [
642
            {
643
                'fields': {
644
                    '_unnamed': {
645
                        '1': 'a string',
646
                    }
647
                }
648
            }
649
        ]
650
    }
651
    resp.forms[0]['file'] = Upload('test.json', json.dumps(data).encode(), 'application/json')
652
    resp = resp.forms[0].submit()
653
    assert '/backoffice/processing?job=' in resp.location
654

  
655
    carddata = carddef.data_class().select()[0]
656
    assert carddata.data['1'] == 'a string'
657

  
658

  
659
def test_backoffice_cards_import_status_from_json(pub):
660
    user = create_user(pub)
661

  
662
    workflow = CardDef.get_default_workflow()
663
    workflow.id = None
664
    st2 = workflow.add_status('status2')
665
    workflow.store()
666

  
667
    CardDef.wipe()
668
    carddef = CardDef()
669
    carddef.name = 'test'
670
    carddef.fields = [
671
        fields.StringField(id='1', label='Test', varname='string'),
672
    ]
673
    carddef.workflow_roles = {'_editor': user.roles[0]}
674
    carddef.workflow = workflow
675
    carddef.backoffice_submission_roles = user.roles
676
    carddef.store()
677
    carddef.data_class().wipe()
678

  
679
    app = login(get_app(pub))
680
    resp = app.get(carddef.get_url())
681
    data = {
682
        'data': [
683
            {
684
                'fields': {
685
                    'string': 'a string',
686
                },
687
                'workflow': {
688
                    'status': {
689
                        'id': 'wf-%s' % st2.id,
690
                    }
691
                },
692
            }
693
        ]
694
    }
695
    resp = resp.click('Import data from a file')
696
    resp.forms[0]['file'] = Upload('test.json', json.dumps(data).encode(), 'application/json')
697
    resp = resp.forms[0].submit()
698
    assert '/backoffice/processing?job=' in resp.location
699

  
700
    carddata = carddef.data_class().select()[0]
701
    assert carddata.status == 'wf-%s' % st2.id
702

  
703

  
704
def test_backoffice_cards_import_backoffice_fields_from_json(pub):
705
    user = create_user(pub)
706

  
707
    workflow = CardDef.get_default_workflow()
708
    workflow.id = None
709
    workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
710
    workflow.backoffice_fields_formdef.fields = [
711
        fields.StringField(id='bo1', label='bo field 1', varname='bo_data'),
712
    ]
713
    workflow.store()
714

  
715
    CardDef.wipe()
716
    carddef = CardDef()
717
    carddef.name = 'test'
718
    carddef.fields = [
719
        fields.StringField(id='1', label='Test', varname='string'),
720
    ]
721
    carddef.workflow_roles = {'_editor': user.roles[0]}
722
    carddef.workflow = workflow
723
    carddef.backoffice_submission_roles = user.roles
724
    carddef.store()
725
    carddef.data_class().wipe()
726

  
727
    app = login(get_app(pub))
728
    resp = app.get(carddef.get_url())
729
    data = {
730
        'data': [
731
            {
732
                'fields': {
733
                    'string': 'a string',
734
                },
735
                'workflow': {
736
                    'status': {
737
                        'id': 'recorded',
738
                    },
739
                    'fields': {
740
                        'bo_data': 'foo',
741
                    },
742
                },
743
            }
744
        ]
745
    }
746
    resp = resp.click('Import data from a file')
747
    resp.forms[0]['file'] = Upload('test.json', json.dumps(data).encode(), 'application/json')
748
    resp = resp.forms[0].submit()
749
    assert '/backoffice/processing?job=' in resp.location
750

  
751
    carddata = carddef.data_class().select()[0]
752
    assert carddata.status == 'recorded'
753
    assert carddata.data == {'1': 'a string', 'bo1': 'foo'}
754

  
755

  
756
def test_backoffice_cards_import_user_from_json(pub):
757
    user = create_user(pub)
758

  
759
    user2 = pub.user_class(name='card import')
760
    user2.email = 'card-import@example.org'
761
    user2.store()
762

  
763
    CardDef.wipe()
764
    carddef = CardDef()
765
    carddef.name = 'test'
766
    carddef.fields = [
767
        fields.StringField(id='1', label='Test', varname='string'),
768
    ]
769
    carddef.workflow_roles = {'_editor': user.roles[0]}
770
    carddef.backoffice_submission_roles = user.roles
771
    carddef.user_support = 'optional'
772
    carddef.store()
773
    carddef.data_class().wipe()
774

  
775
    app = login(get_app(pub))
776
    resp = app.get(carddef.get_url())
777
    data = {
778
        'data': [
779
            {
780
                'fields': {
781
                    'string': 'a string',
782
                },
783
                'user': {
784
                    'email': 'card-import@example.org',
785
                },
786
            }
787
        ]
788
    }
789
    resp = resp.click('Import data from a file')
790
    resp.forms[0]['file'] = Upload('test.json', json.dumps(data).encode(), 'application/json')
791
    resp = resp.forms[0].submit()
792
    assert '/backoffice/processing?job=' in resp.location
793

  
794
    carddata = carddef.data_class().select()[0]
795
    assert str(carddata.user_id) == str(user2.id)
796

  
797

  
554 798
def test_backoffice_cards_wscall_failure_display(http_requests, pub):
555 799
    user = create_user(pub)
556 800

  
wcs/api.py
74 74
        if field.store_structured_value and structured in data:
75 75
            data['%s_structured' % field.id] = data.pop(structured)
76 76

  
77
    # merge unnamed fields if they exist
78
    if '_unnamed' in data:
79
        data.update(data.pop('_unnamed'))
80

  
77 81
    # create a temporary formdata so datasources using previous fields in
78 82
    # parameters can find their values.
79 83
    transient_formdata = formdef.data_class()()
......
282 286
    _q_exports = [  # restricted to API endpoints
283 287
        ('list', 'json'),
284 288
        ('import-csv', 'import_csv'),
289
        ('import-json', 'import_json'),
285 290
        'geojson',
286 291
        'ods',
287 292
        ('@schema', 'schema'),
......
344 349
        formdata.data = posted_json_data_to_formdata_data(self.formdef, data)
345 350

  
346 351
        if 'user' in json_input:
347
            formdata_user = None
348
            for name_id in json_input['user'].get('NameID') or []:
349
                formdata_user = get_publisher().user_class.get_users_with_name_identifier(name_id)
350
                if formdata_user:
351
                    break
352
            else:
353
                if json_input['user'].get('email'):
354
                    formdata_user = get_publisher().user_class.get_users_with_email(
355
                        json_input['user'].get('email')
356
                    )
357
            if formdata_user:
358
                formdata.user_id = formdata_user[0].id
352
            formdata.set_user_from_json(json_input['user'])
359 353
        elif user and not user.is_api_user:
360 354
            formdata.user_id = user.id
361 355

  
......
377 371
        )
378 372

  
379 373
    def import_csv(self):
374
        return self.import_file('csv')
375

  
376
    def import_json(self):
377
        return self.import_file('json')
378

  
379
    def import_file(self, file_format):
380 380
        if get_request().get_method() != 'PUT':
381 381
            raise MethodNotAllowedError(allowed_methods=['PUT'])
382 382
        get_request()._user = get_user_from_api_query_string()
......
386 386
        afterjob = bool(get_request().form.get('async') == 'on')
387 387
        get_response().set_content_type('application/json')
388 388
        try:
389
            job = self.import_csv_submit(get_request().stdin, afterjob=afterjob, api=True)
389
            if file_format == 'csv':
390
                content = get_request().stdin.read()
391
                job = self.import_csv_submit(content, afterjob=afterjob, api=True)
392
            elif file_format == 'json':
393
                job = self.import_json_submit(get_request().json, afterjob=afterjob, api=True)
390 394
        except ValueError as e:
391 395
            return json.dumps({'err': 1, 'err_desc': str(e)})
392 396
        if job is None:
......
602 606
            formdata.backoffice_submission = True
603 607
        if not meta.get('backoffice-submission'):
604 608
            if 'user' in json_input:
605
                formdata_user = None
606
                for name_id in json_input['user'].get('NameID') or []:
607
                    formdata_user = get_publisher().user_class.get_users_with_name_identifier(name_id)
608
                    if formdata_user:
609
                        break
610
                else:
611
                    if json_input['user'].get('email'):
612
                        formdata_user = get_publisher().user_class.get_users_with_email(
613
                            json_input['user'].get('email')
614
                        )
615
                if formdata_user:
616
                    formdata.user_id = formdata_user[0].id
609
                formdata.set_user_from_json(json_input['user'])
617 610
            elif user and not user.is_api_user:
618 611
                formdata.user_id = user.id
619 612

  
wcs/backoffice/data_management.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 copy
17 18
import csv
18 19
import datetime
19 20
import io
......
25 26
from wcs import fields
26 27
from wcs.carddef import CardDef
27 28
from wcs.categories import CardDefCategory
29
from wcs.workflows import ActionsTracingEvolutionPart
28 30

  
29
from ..qommon import _, errors, template
31
from ..qommon import _, errors, ngettext, template
30 32
from ..qommon.afterjobs import AfterJob
31 33
from ..qommon.backoffice.menu import html_top
32 34
from ..qommon.form import FileWidget, Form
......
115 117
        ('export-spreadsheet', 'export_spreadsheet'),
116 118
        ('save-view', 'save_view'),
117 119
        ('delete-view', 'delete_view'),
118
        ('import-csv', 'import_csv'),
120
        ('import-file', 'import_file'),
119 121
        ('filter-options', 'filter_options'),
120 122
        ('data-sample-csv', 'data_sample_csv'),
121 123
    ]
......
156 158
    def get_formdata_sidebar_actions(self, qs=''):
157 159
        r = super().get_formdata_sidebar_actions(qs=qs)
158 160
        if self.formdef.can_user_add_cards(get_request().user):
159
            r += htmltext('<li><a rel="popup" href="import-csv">%s</a></li>') % _(
160
                'Import data from a CSV file'
161
            )
161
            r += htmltext('<li><a rel="popup" href="import-file">%s</a></li>') % _('Import data from a file')
162 162
        return r
163 163

  
164 164
    def data_sample_csv(self):
......
195 195
        )
196 196
        return output.getvalue()
197 197

  
198
    def import_csv(self):
198
    def import_file(self):
199 199
        if not self.formdef.can_user_add_cards(get_request().user):
200 200
            raise errors.AccessForbiddenError()
201 201
        context = {'required_fields': []}
......
208 208
            return redirect('.')
209 209

  
210 210
        if form.is_submitted() and not form.has_errors():
211
            file_content = form.get_widget('file').parse().fp.read()
211 212
            try:
212
                return self.import_csv_submit(form.get_widget('file').parse().fp)
213
            except ValueError as e:
214
                form.set_error('file', e)
213
                json_content = json.loads(file_content)
214
            except ValueError:
215
                # not json -> CSV
216
                try:
217
                    return self.import_csv_submit(file_content)
218
                except ValueError as e:
219
                    form.set_error('file', e)
220
            else:
221
                try:
222
                    return self.import_json_submit(json_content)
223
                except ValueError as e:
224
                    form.set_error('file', e)
215 225

  
216
        get_response().breadcrumb.append(('import_csv', _('Import CSV')))
217
        html_top('data_management', _('Import CSV'))
226
        get_response().breadcrumb.append(('import-file', _('Import File')))
227
        html_top('data_management', _('Import File'))
218 228
        context['html_form'] = form
229
        context['impossible_csv_fields'] = self.get_csv_impossible_fields()
230

  
231
        return template.QommonTemplateResponse(
232
            templates=['wcs/backoffice/card-data-import-form.html'], context=context
233
        )
219 234

  
235
    def get_csv_impossible_fields(self):
236
        impossible_fields = []
220 237
        for field in get_import_csv_fields(self.formdef):
221 238
            if not hasattr(field, 'required'):
222 239
                continue
223 240
            if field.required and field.convert_value_from_str is None:
224
                context['required_fields'].append(field.label)
241
                impossible_fields.append(field.label)
242
        return impossible_fields
225 243

  
226
        return template.QommonTemplateResponse(
227
            templates=['wcs/backoffice/card-data-import-form.html'], context=context
228
        )
229

  
230
    def import_csv_submit(self, fd, afterjob=True, api=False):
231
        content = fd.read()
244
    def import_csv_submit(self, content, afterjob=True, api=False):
232 245
        if b'\0' in content:
233 246
            raise ValueError(_('Invalid file format.'))
234 247

  
248
        impossible_fields = self.get_csv_impossible_fields()
249
        if impossible_fields:
250
            error = ngettext(
251
                '%s is required but cannot be filled from CSV.',
252
                '%s are required but cannot be filled from CSV.',
253
                len(impossible_fields),
254
            ) % ', '.join(impossible_fields)
255
            raise ValueError(error)
256

  
235 257
        for charset in ('utf-8', 'iso-8859-15'):
236 258
            try:
237 259
                content = content.decode(charset)
......
280 302
        else:
281 303
            job.execute()
282 304

  
305
    def import_json_submit(self, json_content, afterjob=True, api=False):
306
        # basic check, looks like valid json card content?
307
        if not isinstance(json_content, dict) or 'data' not in json_content:
308
            raise ValueError(_('Invalid JSON file'))
309

  
310
        job = ImportFromJsonAfterJob(carddef=self.formdef, json_content=json_content)
311
        if afterjob:
312
            get_response().add_after_job(job)
313
            if api:
314
                return job
315
            return redirect(job.get_processing_url())
316
        else:
317
            job.execute()
318

  
283 319
    def _q_lookup(self, component):
284 320

  
285 321
        if not self.view:
......
408 444

  
409 445
    def done_button_attributes(self):
410 446
        return {'data-redirect-auto': 'true'}
447

  
448

  
449
class ImportFromJsonAfterJob(AfterJob):
450
    def __init__(self, carddef, json_content):
451
        super().__init__(
452
            label=_('Importing data into cards'),
453
            carddef_class=carddef.__class__,
454
            carddef_id=carddef.id,
455
            json_content=json_content,
456
        )
457

  
458
    @property
459
    def carddef(self):
460
        return self.kwargs['carddef_class'].get(self.kwargs['carddef_id'])
461

  
462
    def execute(self):
463
        json_content = self.kwargs['json_content']
464
        from wcs.api import posted_json_data_to_formdata_data
465

  
466
        for json_data in json_content['data']:
467
            json_data = copy.deepcopy(json_data)
468
            carddata = self.carddef.data_class()()
469
            carddata.data = posted_json_data_to_formdata_data(self.carddef, json_data['fields'])
470
            if 'user' in json_data:
471
                carddata.set_user_from_json(json_data['user'])
472

  
473
            carddata.just_created()
474

  
475
            if 'workflow' not in json_data:
476
                # perform as new
477
                carddata.store()
478

  
479
                get_publisher().substitutions.reset()
480
                get_publisher().substitutions.feed(get_publisher())
481
                get_publisher().substitutions.feed(carddata)
482
                carddata.perform_workflow(event='json-import-created')
483
            else:
484
                if 'fields' in json_data['workflow']:
485
                    backoffice_data_dict = posted_json_data_to_formdata_data(
486
                        self.carddef, json_data['workflow']['fields']
487
                    )
488
                    carddata.data.update(backoffice_data_dict)
489

  
490
                status = json_data['workflow'].get('real_status') or json_data['workflow'].get('status')
491
                carddata.status = status.get('id')
492
                carddata.evolution[-1].status = status
493
                carddata.evolution[-1].add_part(ActionsTracingEvolutionPart('json-import-created', []))
494
                carddata.store()
495

  
496
            self.increment_count()
497

  
498
    def done_action_url(self):
499
        return self.carddef.get_url()
500

  
501
    def done_action_label(self):
502
        return _('Back to Listing')
503

  
504
    def done_button_attributes(self):
505
        return {'data-redirect-auto': 'true'}
wcs/backoffice/management.py
2417 2417
                include_evolution=True,
2418 2418
                include_files=False,
2419 2419
                include_roles=True,
2420
                include_unnamed_fields=False,
2420 2421
            )
2421 2422
        else:
2422 2423
            output = [
......
4208 4209
        self.file_name = '%s.json' % formdef.url_name
4209 4210

  
4210 4211
    def create_json_export(
4211
        self, items, user, anonymise, digest_key, include_evolution, include_files, include_roles
4212
        self,
4213
        items,
4214
        user,
4215
        anonymise,
4216
        digest_key,
4217
        include_evolution,
4218
        include_files,
4219
        include_roles,
4220
        include_unnamed_fields,
4212 4221
    ):
4213 4222
        formdef = self.kwargs['formdef_class'].get(self.kwargs['formdef_id'])
4214 4223
        prefetched_users = None
......
4244 4253
                include_evolution=include_evolution,
4245 4254
                include_files=include_files,
4246 4255
                include_roles=include_roles,
4256
                include_unnamed_fields=include_unnamed_fields,
4247 4257
            )
4248 4258
            data.pop('digests')
4249 4259
            if digest_key:
......
4263 4273
                    include_evolution=False,
4264 4274
                    include_files=True,
4265 4275
                    include_roles=False,
4276
                    include_unnamed_fields=True,
4266 4277
                )
4267 4278
            },
4268 4279
            indent=2,
wcs/formdata.py
390 390

  
391 391
    user = property(get_user, set_user)
392 392

  
393
    def set_user_from_json(self, json_user):
394
        formdata_user = None
395
        for name_id in json_user.get('NameID') or []:
396
            formdata_user = get_publisher().user_class.get_users_with_name_identifier(name_id)
397
            if formdata_user:
398
                break
399
        else:
400
            if json_user.get('email'):
401
                formdata_user = get_publisher().user_class.get_users_with_email(json_user.get('email'))
402
        if formdata_user:
403
            self.user_id = formdata_user[0].id
404

  
393 405
    def get_user_label(self):
394 406
        user = self.user
395 407
        if user:
......
1263 1275
        return None
1264 1276

  
1265 1277
    @classmethod
1266
    def get_json_data_dict(cls, data, fields, formdata=None, include_files=True, anonymise=False):
1278
    def get_json_data_dict(
1279
        cls, data, fields, formdata=None, include_files=True, anonymise=False, include_unnamed_fields=False
1280
    ):
1267 1281
        new_data = {}
1268 1282
        seen = set()
1269 1283
        for field in fields:
1270 1284
            if anonymise and field.anonymise:
1271 1285
                continue
1272
            if not field.varname:  # exports only named fields
1286
            if not field.varname and not include_unnamed_fields:
1273 1287
                continue
1274 1288
            if field.varname in seen:
1275 1289
                # skip fields with a varname that is used by another non-empty
......
1281 1295
                    value = field.get_json_value(value, formdata=formdata, include_file_content=include_files)
1282 1296
            else:
1283 1297
                value = None
1284
            if value:
1298

  
1299
            if value and field.varname:
1285 1300
                seen.add(field.varname)
1301

  
1302
            if not field.varname:
1303
                # include unnamed fields in a dedicated key
1304
                if '_unnamed' not in new_data:
1305
                    new_data['_unnamed'] = {}
1306
                store_dict = new_data['_unnamed']
1307
                store_key = str(field.id)
1308
            else:
1309
                store_dict = new_data
1310
                store_key = field.varname
1311

  
1286 1312
            if field.store_display_value:
1287
                new_data[field.varname + '_raw'] = value
1288
                new_data[field.varname] = data.get('%s_display' % field.id)
1313
                store_dict[store_key + '_raw'] = value
1314
                store_dict[store_key] = data.get('%s_display' % field.id)
1289 1315
            else:
1290
                new_data[field.varname] = value
1316
                store_dict[store_key] = value
1291 1317
            if field.store_structured_value:
1292 1318
                if data.get('%s_structured' % field.id):
1293
                    new_data[field.varname + '_structured'] = data.get('%s_structured' % field.id)
1319
                    store_dict[store_key + '_structured'] = data.get('%s_structured' % field.id)
1294 1320
        return new_data
1295 1321

  
1296
    def get_json_dict(self, fields, include_files=True, anonymise=False):
1322
    def get_json_dict(self, fields, include_files=True, anonymise=False, include_unnamed_fields=False):
1297 1323
        return self.get_json_data_dict(
1298
            self.data, fields, formdata=self, include_files=include_files, anonymise=anonymise
1324
            self.data,
1325
            fields,
1326
            formdata=self,
1327
            include_files=include_files,
1328
            anonymise=anonymise,
1329
            include_unnamed_fields=include_unnamed_fields,
1299 1330
        )
1300 1331

  
1301 1332
    def get_json_export_dict(
......
1308 1339
        prefetched_roles=None,
1309 1340
        include_evolution=True,
1310 1341
        include_roles=True,
1342
        include_unnamed_fields=False,
1311 1343
    ):
1312 1344
        data = {}
1313 1345
        data['id'] = str(self.id)
......
1333 1365
            data['user'] = user.get_json_export_dict()
1334 1366

  
1335 1367
        data['fields'] = self.get_json_dict(
1336
            self.formdef.fields, include_files=include_files, anonymise=anonymise
1368
            self.formdef.fields,
1369
            include_files=include_files,
1370
            anonymise=anonymise,
1371
            include_unnamed_fields=include_unnamed_fields,
1337 1372
        )
1338 1373

  
1339 1374
        data['workflow'] = {}
......
1351 1386
                self.formdef.workflow.get_backoffice_fields(),
1352 1387
                include_files=include_files,
1353 1388
                anonymise=anonymise,
1389
                include_unnamed_fields=include_unnamed_fields,
1354 1390
            )
1355 1391

  
1356 1392
        if include_roles:
wcs/templates/wcs/backoffice/card-data-import-form.html
3 3

  
4 4
{% block appbar %}
5 5
<div id="appbar">
6
<h2>{% trans "Import CSV" %}</h2>
6
<h2>{% trans "Import File" %}</h2>
7 7
</div>
8 8
{% endblock %}
9 9

  
10 10
{% block content %}
11 11

  
12
{% if required_fields %}
13
<div class="errornotice">
12
<p>{% trans "You can add data to this card by uploading a file." %}</p>
13

  
14
{% if not impossible_csv_fields %}
15
<p><a href="data-sample-csv">{% trans "Download sample CSV file for this card" %}</a></p>
16
{% else %}
17
<div class="warningnotice">
14 18
  <p>
15
  {% blocktrans count fields=required_fields|length with labels=required_fields|join:", " %}
19
  {% blocktrans count fields=impossible_csv_fields|length with labels=impossible_csv_fields|join:", " %}
16 20
  {{ labels }} is required but cannot be filled from CSV.
17 21
  {% plural %}
18 22
  {{ labels }} are required but cannot be filled from CSV.
19 23
  {% endblocktrans %}
24
  {% trans "Only JSON files can be imported." %}
20 25
  </p>
21 26
</div>
22
{% else %}
23
<p>{% trans "You can add data to this card by uploading a file." %}</p>
24
<p><a href="data-sample-csv">{% trans "Download sample file for this card" %}</a></p>
25
{{ html_form.render|safe }}
26 27
{% endif %}
28

  
29
{{ html_form.render|safe }}
30

  
27 31
{% endblock %}
wcs/workflows.py
405 405
            'global-action-timeout': _('Global action timeout'),
406 406
            'global-api-trigger': _('API Trigger'),
407 407
            'global-external-workflow': _('Trigger by external workflow'),
408
            'json-import-created': _('Created (by JSON import)'),
408 409
            'timeout-jump': _('Timeout jump'),
409 410
            'workflow-created': _('Created (by workflow action)'),
410 411
            'workflow-form-submit': _('Action in workflow form'),
411
-