0003-backoffice-add-json-import-for-cards-60303.patch
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 |
- |