Projet

Général

Profil

0001-backoffice-add-support-for-custom-views-4507.patch

Frédéric Péters, 31 mars 2020 09:43

Télécharger (75,7 ko)

Voir les différences:

Subject: [PATCH] backoffice: add support for custom views (#4507)

 tests/test_api.py                   |  46 +++
 tests/test_backoffice_pages.py      | 451 ++++++++++++++++++++--------
 tests/utilities.py                  |   5 +-
 wcs/api.py                          |  10 +
 wcs/backoffice/data_management.py   |  29 +-
 wcs/backoffice/management.py        | 255 +++++++++++++---
 wcs/custom_views.py                 | 127 ++++++++
 wcs/publisher.py                    |   4 +
 wcs/qommon/static/css/dc2/admin.css |  29 +-
 wcs/qommon/static/js/wcs.listing.js |  46 ++-
 wcs/sql.py                          | 124 +++++++-
 11 files changed, 931 insertions(+), 195 deletions(-)
 create mode 100644 wcs/custom_views.py
tests/test_api.py
2031 2031
    assert len(ods_sheet.findall('.//{%s}table-row' % ods.NS['table'])) == 311
2032 2032

  
2033 2033

  
2034
def test_api_list_formdata_custom_view(pub, local_user):
2035
    Role.wipe()
2036
    role = Role(name='test')
2037
    role.store()
2038

  
2039
    FormDef.wipe()
2040
    formdef = FormDef()
2041
    formdef.name = 'test'
2042
    formdef.workflow_roles = {'_receiver': role.id}
2043
    formdef.fields = [fields.StringField(id='0', label='foobar', varname='foobar'),]
2044
    formdef.store()
2045

  
2046
    data_class = formdef.data_class()
2047
    data_class.wipe()
2048

  
2049
    for i in range(30):
2050
        formdata = data_class()
2051
        formdata.data = {'0': 'FOO BAR %d' % i}
2052
        formdata.user_id = local_user.id
2053
        formdata.just_created()
2054
        if i % 3 == 0:
2055
            formdata.jump_status('new')
2056
        else:
2057
            formdata.jump_status('finished')
2058
        formdata.store()
2059

  
2060
    # add proper role to user
2061
    local_user.roles = [role.id]
2062
    local_user.store()
2063

  
2064
    # check it now gets the data
2065
    resp = get_app(pub).get(sign_uri('/api/forms/test/list', user=local_user))
2066
    assert len(resp.json) == 30
2067

  
2068
    custom_view = pub.custom_view_class()
2069
    custom_view.title = 'custom view'
2070
    custom_view.formdef = formdef
2071
    custom_view.columns = {'list': [{'id': '0'}]}
2072
    custom_view.filters = {"filter": "done", "filter-status": "on"}
2073
    custom_view.visibility = 'any'
2074
    custom_view.store()
2075

  
2076
    resp = get_app(pub).get(sign_uri('/api/forms/test/list/custom-view', user=local_user))
2077
    assert len(resp.json['data']) == 20
2078

  
2079

  
2034 2080
def test_api_global_geojson(pub, local_user):
2035 2081
    Role.wipe()
2036 2082
    role = Role(name='test')
tests/test_backoffice_pages.py
122 122
    Workflow.wipe()
123 123
    Category.wipe()
124 124
    FormDef.wipe()
125
    pub.custom_view_class.wipe()
125 126
    formdef = FormDef()
126 127
    formdef.name = 'form title'
127 128
    if set_receiver:
......
415 416

  
416 417
    resp = resp.click(re.compile('^2$')) # second page
417 418
    assert resp.text.count('data-link') == 5
418
    assert resp.form['offset'].value == '5'
419
    assert resp.forms['listing-settings']['offset'].value == '5'
419 420

  
420 421
    resp = resp.click(re.compile('^3$')) # third page
421 422
    assert resp.text.count('data-link') == 5
422
    assert resp.form['offset'].value == '10'
423
    assert resp.forms['listing-settings']['offset'].value == '10'
423 424

  
424 425
    resp = resp.click(re.compile('^4$')) # fourth page
425 426
    assert resp.text.count('data-link') == 2
426
    assert resp.form['offset'].value == '15'
427
    assert resp.forms['listing-settings']['offset'].value == '15'
427 428

  
428 429
    with pytest.raises(IndexError): # no fifth page
429 430
        resp = resp.click(re.compile('^5$'))
......
437 438
    # try an overbound offset
438 439
    resp = app.get('/backoffice/management/form-title/?limit=5&offset=30')
439 440
    resp = resp.follow()
440
    assert resp.form['offset'].value == '0'
441
    assert resp.forms['listing-settings']['offset'].value == '0'
441 442

  
442 443

  
443 444
def test_backoffice_listing_order(pub):
......
541 542
    assert resp.text.count('data-link') == 17 # 17 rows
542 543
    assert resp.text.count('FOO BAR') == 0 # no field 1 column
543 544

  
545
    # change column order
546
    assert resp.forms['listing-settings']['columns-order'].value == 'id,time,last_update_time,user-label,2,status,1,3,anonymised'
547
    resp.forms['listing-settings']['columns-order'].value = 'user-label,id,time,last_update_time,2,status,1,3,anonymised'
548
    resp = resp.forms['listing-settings'].submit()
549
    assert resp.text.find('<span>User Label</span>') < resp.text.find('<span>Number</span>')
550

  
544 551

  
545 552
def test_backoffice_channel_column(pub):
546 553
    if not pub.site_options.has_section('variables'):
......
571 578

  
572 579
    app = login(get_app(pub))
573 580
    resp = app.get('/backoffice/management/form-title/')
574
    assert not 'submission_agent' in resp.form.fields
581
    assert not 'submission_agent' in resp.forms['listing-settings'].fields
575 582

  
576 583
    formdef = FormDef.get_by_urlname('form-title')
577 584
    formdef.backoffice_submission_roles = user.roles
578 585
    formdef.store()
579 586
    resp = app.get('/backoffice/management/form-title/')
580 587
    assert resp.text.count('</th>') == 8 # six columns
581
    resp.form['submission_agent'].checked = True
582
    resp = resp.form.submit()
588
    resp.forms['listing-settings']['submission_agent'].checked = True
589
    resp = resp.forms['listing-settings'].submit()
583 590
    assert resp.text.count('</th>') == 9 # seven columns
584 591
    assert resp.text.count('data-link') == 17 # 17 rows
585 592
    assert not '>agent<' in resp.text
......
592 599
        formdata.store()
593 600

  
594 601
    resp = app.get('/backoffice/management/form-title/')
595
    resp.form['submission_agent'].checked = True
596
    resp = resp.form.submit()
602
    resp.forms['listing-settings']['submission_agent'].checked = True
603
    resp = resp.forms['listing-settings'].submit()
597 604
    assert resp.text.count('>agent<') == 17
598 605

  
599 606
    resp = resp.click('Export as CSV File')
600 607
    assert len(resp.text.splitlines()) == 18 # 17 + header line
601
    assert resp.text.count(',agent,') == 17
608
    assert resp.text.count(',agent') == 17
602 609

  
603 610

  
604 611
def test_backoffice_image_column(pub):
......
669 676
    create_environment(pub)
670 677
    app = login(get_app(pub))
671 678
    resp = app.get('/backoffice/management/form-title/')
672
    assert not 'filter-2-value' in resp.form.fields
679
    assert not 'filter-2-value' in resp.forms['listing-settings'].fields
673 680

  
674 681
    formdef = FormDef.get_by_urlname('form-title')
675 682
    formdef.fields[1].in_filters = True
676 683
    formdef.store()
677 684
    resp = app.get('/backoffice/management/form-title/')
678
    assert 'filter-2-value' in resp.form.fields
685
    assert 'filter-2-value' in resp.forms['listing-settings'].fields
679 686

  
680 687
    # same check for items field
681 688
    formdef.fields.append(
682 689
            fields.ItemsField(id='4', label='4th field', type='items', items=['foo', 'bar', 'baz']))
683 690
    formdef.store()
684 691
    resp = app.get('/backoffice/management/form-title/')
685
    assert not 'filter-4-value' in resp.form.fields
692
    assert not 'filter-4-value' in resp.forms['listing-settings'].fields
686 693

  
687 694
    formdef.fields[-1].in_filters = True
688 695
    formdef.store()
689 696
    resp = app.get('/backoffice/management/form-title/')
690
    assert 'filter-4-value' in resp.form.fields
697
    assert 'filter-4-value' in resp.forms['listing-settings'].fields
691 698

  
692 699

  
693 700
def test_backoffice_bool_filter(pub):
......
705 712

  
706 713
    app = login(get_app(pub))
707 714
    resp = app.get('/backoffice/management/form-title/')
708
    resp.form['filter-4'].checked = True
709
    resp = resp.form.submit()
715
    resp.forms['listing-settings']['filter-4'].checked = True
716
    resp = resp.forms['listing-settings'].submit()
710 717

  
711
    assert resp.form['filter-4-value'].value == ''
718
    assert resp.forms['listing-settings']['filter-4-value'].value == ''
712 719

  
713
    resp.form['filter-4-value'].value = 'true'
714
    resp = resp.form.submit()
720
    resp.forms['listing-settings']['filter-4-value'].value = 'true'
721
    resp = resp.forms['listing-settings'].submit()
715 722
    assert resp.text.count('<td>Yes</td>') > 0
716 723
    assert resp.text.count('<td>No</td>') == 0
717 724

  
718
    resp.form['filter-4-value'].value = 'false'
719
    resp = resp.form.submit()
725
    resp.forms['listing-settings']['filter-4-value'].value = 'false'
726
    resp = resp.forms['listing-settings'].submit()
720 727
    assert resp.text.count('<td>Yes</td>') == 0
721 728
    assert resp.text.count('<td>No</td>') > 0
722 729

  
......
747 754

  
748 755
    app = login(get_app(pub))
749 756
    resp = app.get('/backoffice/management/form-title/')
750
    resp.form['filter-4'].checked = True
751
    resp = resp.form.submit()
757
    resp.forms['listing-settings']['filter-4'].checked = True
758
    resp = resp.forms['listing-settings'].submit()
752 759

  
753
    assert resp.form['filter-4-value'].value == ''
760
    assert resp.forms['listing-settings']['filter-4-value'].value == ''
754 761

  
755
    resp.form['filter-4-value'].value = 'â'
756
    resp = resp.form.submit()
762
    resp.forms['listing-settings']['filter-4-value'].value = 'â'
763
    resp = resp.forms['listing-settings'].submit()
757 764
    assert resp.text.count(u'<td>â</td>') > 0
758 765
    assert resp.text.count(u'<td>b</td>') == 0
759 766
    assert resp.text.count(u'<td>d</td>') == 0
760 767

  
761
    resp.form['filter-4-value'].value = 'b'
762
    resp = resp.form.submit()
768
    resp.forms['listing-settings']['filter-4-value'].value = 'b'
769
    resp = resp.forms['listing-settings'].submit()
763 770
    assert resp.text.count(u'<td>â</td>') == 0
764 771
    assert resp.text.count(u'<td>b</td>') > 0
765 772
    assert resp.text.count(u'<td>d</td>') == 0
766 773

  
767 774
    if not pub.is_using_postgresql():
768 775
        # in pickle all options are always displayed
769
        resp.form['filter-4-value'].value = 'c'
770
        resp = resp.form.submit()
776
        resp.forms['listing-settings']['filter-4-value'].value = 'c'
777
        resp = resp.forms['listing-settings'].submit()
771 778
        assert resp.text.count(u'<td>â</td>') == 0
772 779
        assert resp.text.count(u'<td>b</td>') == 0
773 780
        assert resp.text.count(u'<td>c</td>') == 0
......
775 782
    else:
776 783
        # in postgresql, option 'c' is never used so not even listed
777 784
        with pytest.raises(ValueError):
778
            resp.form['filter-4-value'].value = 'c'
785
            resp.forms['listing-settings']['filter-4-value'].value = 'c'
779 786

  
780 787
        # check json view used to fill select filters from javascript
781 788
        resp2 = app.get(resp.request.path + 'filter-options?filter_field_id=4&' + resp.request.query_string)
......
785 792
        resp2 = app.get(resp.request.path + 'filter-options?filter_field_id=7&' + resp.request.query_string, status=404)
786 793

  
787 794
        for status in ('all', 'waiting', 'pending', 'done', 'accepted'):
788
            resp.form['filter'] = status
789
            resp = resp.form.submit()
795
            resp.forms['listing-settings']['filter'] = status
796
            resp = resp.forms['listing-settings'].submit()
790 797
            resp2 = app.get(resp.request.path + 'filter-options?filter_field_id=4&' + resp.request.query_string)
791 798
            if status == 'accepted':
792 799
                assert [x['id'] for x in resp2.json['data']] == []
......
834 841

  
835 842
    app = login(get_app(pub))
836 843
    resp = app.get('/backoffice/management/form-title/')
837
    resp.form['filter-4'].checked = True
838
    resp.form['filter-5'].checked = True
839
    resp = resp.form.submit()
844
    resp.forms['listing-settings']['filter-4'].checked = True
845
    resp.forms['listing-settings']['filter-5'].checked = True
846
    resp = resp.forms['listing-settings'].submit()
840 847

  
841
    assert resp.form['filter-4-value'].value == ''
842
    assert resp.form['filter-5-value'].value == ''
843
    assert [x[0] for x in resp.form['filter-4-value'].options] == ['', 'a', 'b']
844
    assert [x[0] for x in resp.form['filter-5-value'].options] == ['', 'A', 'B', 'C']
848
    assert resp.forms['listing-settings']['filter-4-value'].value == ''
849
    assert resp.forms['listing-settings']['filter-5-value'].value == ''
850
    assert [x[0] for x in resp.forms['listing-settings']['filter-4-value'].options] == ['', 'a', 'b']
851
    assert [x[0] for x in resp.forms['listing-settings']['filter-5-value'].options] == ['', 'A', 'B', 'C']
845 852

  
846
    resp.form['filter-4-value'].value = 'a'
847
    resp = resp.form.submit()
848
    assert [x[0] for x in resp.form['filter-4-value'].options] == ['', 'a', 'b']
849
    assert [x[0] for x in resp.form['filter-5-value'].options] == ['', 'A', 'B', 'C']
853
    resp.forms['listing-settings']['filter-4-value'].value = 'a'
854
    resp = resp.forms['listing-settings'].submit()
855
    assert [x[0] for x in resp.forms['listing-settings']['filter-4-value'].options] == ['', 'a', 'b']
856
    assert [x[0] for x in resp.forms['listing-settings']['filter-5-value'].options] == ['', 'A', 'B', 'C']
850 857

  
851
    resp.form['filter-4-value'].value = 'b'
852
    resp = resp.form.submit()
853
    assert [x[0] for x in resp.form['filter-4-value'].options] == ['', 'a', 'b']
854
    assert [x[0] for x in resp.form['filter-5-value'].options] == ['', 'B']
858
    resp.forms['listing-settings']['filter-4-value'].value = 'b'
859
    resp = resp.forms['listing-settings'].submit()
860
    assert [x[0] for x in resp.forms['listing-settings']['filter-4-value'].options] == ['', 'a', 'b']
861
    assert [x[0] for x in resp.forms['listing-settings']['filter-5-value'].options] == ['', 'B']
855 862

  
856
    resp.form['filter-5-value'].value = 'B'
857
    resp = resp.form.submit()
858
    assert [x[0] for x in resp.form['filter-4-value'].options] == ['', 'a', 'b']
859
    assert [x[0] for x in resp.form['filter-5-value'].options] == ['', 'B']
863
    resp.forms['listing-settings']['filter-5-value'].value = 'B'
864
    resp = resp.forms['listing-settings'].submit()
865
    assert [x[0] for x in resp.forms['listing-settings']['filter-4-value'].options] == ['', 'a', 'b']
866
    assert [x[0] for x in resp.forms['listing-settings']['filter-5-value'].options] == ['', 'B']
860 867

  
861
    resp.form['filter-4-value'].value = ''
862
    resp = resp.form.submit()
863
    assert [x[0] for x in resp.form['filter-4-value'].options] == ['', 'a', 'b']
864
    assert [x[0] for x in resp.form['filter-5-value'].options] == ['', 'A', 'B', 'C']
868
    resp.forms['listing-settings']['filter-4-value'].value = ''
869
    resp = resp.forms['listing-settings'].submit()
870
    assert [x[0] for x in resp.forms['listing-settings']['filter-4-value'].options] == ['', 'a', 'b']
871
    assert [x[0] for x in resp.forms['listing-settings']['filter-5-value'].options] == ['', 'A', 'B', 'C']
865 872

  
866 873

  
867 874
def test_backoffice_bofield_item_filter(pub):
......
894 901

  
895 902
    app = login(get_app(pub))
896 903
    resp = app.get('/backoffice/management/form-title/')
897
    resp.form['filter-bo0-1'].checked = True
898
    resp = resp.form.submit()
904
    resp.forms['listing-settings']['filter-bo0-1'].checked = True
905
    resp = resp.forms['listing-settings'].submit()
899 906

  
900
    assert resp.form['filter-bo0-1-value'].value == ''
907
    assert resp.forms['listing-settings']['filter-bo0-1-value'].value == ''
901 908

  
902
    resp.form['filter-bo0-1-value'].value = 'â'
903
    resp = resp.form.submit()
909
    resp.forms['listing-settings']['filter-bo0-1-value'].value = 'â'
910
    resp = resp.forms['listing-settings'].submit()
904 911
    assert resp.text.count(u'<td>â</td>') > 0
905 912
    assert resp.text.count(u'<td>b</td>') == 0
906 913
    assert resp.text.count(u'<td>d</td>') == 0
907 914

  
908
    resp.form['filter-bo0-1-value'].value = 'b'
909
    resp = resp.form.submit()
915
    resp.forms['listing-settings']['filter-bo0-1-value'].value = 'b'
916
    resp = resp.forms['listing-settings'].submit()
910 917
    assert resp.text.count(u'<td>â</td>') == 0
911 918
    assert resp.text.count(u'<td>b</td>') > 0
912 919
    assert resp.text.count(u'<td>d</td>') == 0
913 920

  
914 921
    if not pub.is_using_postgresql():
915 922
        # in pickle all options are always displayed
916
        resp.form['filter-bo0-1-value'].value = 'c'
917
        resp = resp.form.submit()
923
        resp.forms['listing-settings']['filter-bo0-1-value'].value = 'c'
924
        resp = resp.forms['listing-settings'].submit()
918 925
        assert resp.text.count(u'<td>â</td>') == 0
919 926
        assert resp.text.count(u'<td>b</td>') == 0
920 927
        assert resp.text.count(u'<td>c</td>') == 0
......
922 929
    else:
923 930
        # in postgresql, option 'c' is never used so not even listed
924 931
        with pytest.raises(ValueError):
925
            resp.form['filter-bo0-1-value'].value = 'c'
932
            resp.forms['listing-settings']['filter-bo0-1-value'].value = 'c'
926 933

  
927 934
        # check json view used to fill select filters from javascript
928 935
        resp2 = app.get(resp.request.path + 'filter-options?filter_field_id=bo0-1&' + resp.request.query_string)
......
931 938
        assert [x['id'] for x in resp2.json['data']] == ['d']
932 939

  
933 940
        for status in ('all', 'waiting', 'pending', 'done', 'accepted'):
934
            resp.form['filter'] = status
935
            resp = resp.form.submit()
941
            resp.forms['listing-settings']['filter'] = status
942
            resp = resp.forms['listing-settings'].submit()
936 943
            resp2 = app.get(resp.request.path + 'filter-options?filter_field_id=bo0-1&' + resp.request.query_string)
937 944
            if status == 'accepted':
938 945
                assert [x['id'] for x in resp2.json['data']] == []
......
966 973

  
967 974
    app = login(get_app(pub))
968 975
    resp = app.get('/backoffice/management/form-title/')
969
    resp.form['filter-4'].checked = True
970
    resp = resp.form.submit()
976
    resp.forms['listing-settings']['filter-4'].checked = True
977
    resp = resp.forms['listing-settings'].submit()
971 978

  
972
    assert resp.form['filter-4-value'].value == ''
979
    assert resp.forms['listing-settings']['filter-4-value'].value == ''
973 980

  
974
    resp.form['filter-4-value'].value = 'â'
975
    resp = resp.form.submit()
981
    resp.forms['listing-settings']['filter-4-value'].value = 'â'
982
    resp = resp.forms['listing-settings'].submit()
976 983
    assert resp.text.count(u'<td>â, b</td>') > 0
977 984
    assert resp.text.count(u'<td>â</td>') > 0
978 985
    assert resp.text.count(u'<td>b, d</td>') == 0
979 986

  
980
    resp.form['filter-4-value'].value = 'b'
981
    resp = resp.form.submit()
987
    resp.forms['listing-settings']['filter-4-value'].value = 'b'
988
    resp = resp.forms['listing-settings'].submit()
982 989
    assert resp.text.count(u'<td>â, b</td>') > 0
983 990
    assert resp.text.count(u'<td>â</td>') == 0
984 991
    assert resp.text.count(u'<td>b, d</td>') > 0
......
986 993
    if pub.is_using_postgresql():
987 994
        # option 'c' is never used so not even listed
988 995
        with pytest.raises(ValueError):
989
            resp.form['filter-4-value'].value = 'c'
996
            resp.forms['listing-settings']['filter-4-value'].value = 'c'
990 997
    else:
991
        resp.form['filter-4-value'].value = 'c'
992
        resp = resp.form.submit()
998
        resp.forms['listing-settings']['filter-4-value'].value = 'c'
999
        resp = resp.forms['listing-settings'].submit()
993 1000
        assert resp.text.count(u'<td>â, b</td>') == 0
994 1001
        assert resp.text.count(u'<td>â</td>') == 0
995 1002
        assert resp.text.count(u'<td>b, d</td>') == 0
......
1020 1027
    assert resp.text.splitlines()[1].split(',')[7] == 'aa'
1021 1028

  
1022 1029
    resp = app.get('/backoffice/management/form-title/')
1023
    resp.forms[0]['filter'] = 'all'
1024
    resp = resp.forms[0].submit()
1030
    resp.forms['listing-settings']['filter'] = 'all'
1031
    resp = resp.forms['listing-settings'].submit()
1025 1032
    resp_csv = resp.click('Export as CSV File')
1026 1033
    assert len(resp_csv.text.splitlines()) == 51
1027 1034

  
1028 1035
    # test status filter
1029
    resp.forms[0]['filter'] = 'pending'
1030
    resp.forms[0]['filter-2'].checked = True
1031
    resp = resp.forms[0].submit()
1032
    resp.forms[0]['filter-2-value'] = 'baz'
1033
    resp = resp.forms[0].submit()
1036
    resp.forms['listing-settings']['filter'] = 'pending'
1037
    resp.forms['listing-settings']['filter-2'].checked = True
1038
    resp = resp.forms['listing-settings'].submit()
1039
    resp.forms['listing-settings']['filter-2-value'] = 'baz'
1040
    resp = resp.forms['listing-settings'].submit()
1034 1041
    resp_csv = resp.click('Export as CSV File')
1035 1042
    assert len(resp_csv.text.splitlines()) == 9
1036 1043

  
1037 1044
    # test criteria filters
1038
    resp.forms[0]['filter-start'].checked = True
1039
    resp = resp.forms[0].submit()
1040
    resp.forms[0]['filter-start-value'] = datetime.datetime(2015, 2, 1).strftime('%Y-%m-%d')
1041
    resp = resp.forms[0].submit()
1045
    resp.forms['listing-settings']['filter-start'].checked = True
1046
    resp = resp.forms['listing-settings'].submit()
1047
    resp.forms['listing-settings']['filter-start-value'] = datetime.datetime(2015, 2, 1).strftime('%Y-%m-%d')
1048
    resp = resp.forms['listing-settings'].submit()
1042 1049
    resp_csv = resp.click('Export as CSV File')
1043 1050
    assert len(resp_csv.text.splitlines()) == 1
1044 1051

  
1045
    resp.forms[0]['filter-start-value'] = datetime.datetime(2014, 2, 1).strftime('%Y-%m-%d')
1046
    resp = resp.forms[0].submit()
1047
    resp.forms[0]['filter-2-value'] = 'baz'
1048
    resp = resp.forms[0].submit()
1052
    resp.forms['listing-settings']['filter-start-value'] = datetime.datetime(2014, 2, 1).strftime('%Y-%m-%d')
1053
    resp = resp.forms['listing-settings'].submit()
1054
    resp.forms['listing-settings']['filter-2-value'] = 'baz'
1055
    resp = resp.forms['listing-settings'].submit()
1049 1056
    resp_csv = resp.click('Export as CSV File')
1050 1057
    assert len(resp_csv.text.splitlines()) == 9
1051 1058
    assert 'Created' in resp_csv.text.splitlines()[0]
1052 1059

  
1053 1060
    # test column selection
1054
    resp.form['time'].checked = False
1055
    resp = resp.forms[0].submit()
1061
    resp.forms['listing-settings']['time'].checked = False
1062
    resp = resp.forms['listing-settings'].submit()
1056 1063
    resp_csv = resp.click('Export as CSV File')
1057 1064
    assert 'Created' not in resp_csv.text.splitlines()[0]
1058 1065

  
......
1117 1124
    assert 'Channel' not in resp_csv.text.splitlines()[0]
1118 1125

  
1119 1126
    # add submission channel column
1120
    resp.form['submission_channel'].checked = True
1121
    resp = resp.forms[0].submit()
1127
    resp.forms['listing-settings']['submission_channel'].checked = True
1128
    resp = resp.forms['listing-settings'].submit()
1122 1129
    resp_csv = resp.click('Export as CSV File')
1123
    assert resp_csv.text.splitlines()[0].split(',')[1] == 'Channel'
1124
    assert resp_csv.text.splitlines()[1].split(',')[1] == 'Web'
1130
    assert resp_csv.text.splitlines()[0].split(',')[-1] == 'Channel'
1131
    assert resp_csv.text.splitlines()[1].split(',')[-1] == 'Web'
1125 1132

  
1126 1133

  
1127 1134
def test_backoffice_csv_export_anonymised(pub):
......
1137 1144
    assert resp_csv.text.splitlines()[0].split(',')[-1] != 'Anonymised'
1138 1145

  
1139 1146
    # add anonymised column
1140
    resp.form['anonymised'].checked = True
1141
    resp = resp.forms[0].submit()
1147
    resp.forms['listing-settings']['anonymised'].checked = True
1148
    resp = resp.forms['listing-settings'].submit()
1142 1149
    resp_csv = resp.click('Export as CSV File')
1143 1150
    assert resp_csv.text.splitlines()[0].split(',')[-1] == 'Anonymised'
1144 1151
    assert resp_csv.text.splitlines()[1].split(',')[-1] == 'No'
......
1265 1272
    assert 'To Status &quot;Finished&quot;' in resp.text
1266 1273
    assert not '<h2>Filters</h2>' in resp.text
1267 1274

  
1268
    resp.forms[0]['filter-end-value'] = '2013-01-01'
1269
    resp = resp.forms[0].submit()
1275
    resp.forms['listing-settings']['filter-end-value'] = '2013-01-01'
1276
    resp = resp.forms['listing-settings'].submit()
1270 1277
    assert 'Total number of records: 0' in resp.text
1271 1278
    assert '<h2>Filters</h2>' in resp.text
1272 1279
    assert 'End: 2013-01-01' in resp.text
......
1388 1395
    app = login(get_app(pub))
1389 1396
    resp = app.get('/backoffice/management/form-title/')
1390 1397
    resp = resp.click('Statistics')
1391
    assert 'filter' not in resp.forms[0].fields # status is not displayed by default
1398
    assert 'filter' not in resp.forms['listing-settings'].fields # status is not displayed by default
1392 1399
    assert not '<h2>Filters</h2>' in resp.text
1393 1400

  
1394 1401
    # add 'status' as a filter
1395
    resp.forms[0]['filter-status'].checked = True
1396
    resp = resp.forms[0].submit()
1397
    assert 'filter' in resp.forms[0].fields
1402
    resp.forms['listing-settings']['filter-status'].checked = True
1403
    resp = resp.forms['listing-settings'].submit()
1404
    assert 'filter' in resp.forms['listing-settings'].fields
1398 1405
    assert not '<h2>Filters</h2>' in resp.text
1399 1406

  
1400
    assert resp.forms[0]['filter'].value == 'all'
1401
    resp.forms[0]['filter'].value = 'pending'
1402
    resp = resp.forms[0].submit()
1407
    assert resp.forms['listing-settings']['filter'].value == 'all'
1408
    resp.forms['listing-settings']['filter'].value = 'pending'
1409
    resp = resp.forms['listing-settings'].submit()
1403 1410
    assert 'Total number of records: 17' in resp.text
1404 1411
    assert '<h2>Filters</h2>' in resp.text
1405 1412
    assert 'Status: Pending' in resp.text
1406 1413

  
1407
    resp.forms[0]['filter'].value = 'done'
1408
    resp = resp.forms[0].submit()
1414
    resp.forms['listing-settings']['filter'].value = 'done'
1415
    resp = resp.forms['listing-settings'].submit()
1409 1416
    assert 'Total number of records: 33' in resp.text
1410 1417
    assert '<h2>Filters</h2>' in resp.text
1411 1418
    assert 'Status: Done' in resp.text
1412 1419

  
1413
    resp.forms[0]['filter'].value = 'rejected'
1414
    resp = resp.forms[0].submit()
1420
    resp.forms['listing-settings']['filter'].value = 'rejected'
1421
    resp = resp.forms['listing-settings'].submit()
1415 1422
    assert 'Total number of records: 0' in resp.text
1416 1423
    assert '<h2>Filters</h2>' in resp.text
1417 1424
    assert 'Status: Rejected' in resp.text
1418 1425

  
1419
    resp.forms[0]['filter'].value = 'all'
1420
    resp = resp.forms[0].submit()
1426
    resp.forms['listing-settings']['filter'].value = 'all'
1427
    resp = resp.forms['listing-settings'].submit()
1421 1428
    assert 'Total number of records: 50' in resp.text
1422 1429

  
1423 1430

  
......
1429 1436
    resp = resp.click('Statistics')
1430 1437
    assert not 'filter-2-value' in resp.form.fields
1431 1438

  
1432
    resp.forms[0]['filter-2'].checked = True
1433
    resp = resp.forms[0].submit()
1434
    resp.forms[0]['filter-2-value'].value = 'bar'
1435
    resp = resp.forms[0].submit()
1439
    resp.forms['listing-settings']['filter-2'].checked = True
1440
    resp = resp.forms['listing-settings'].submit()
1441
    resp.forms['listing-settings']['filter-2-value'].value = 'bar'
1442
    resp = resp.forms['listing-settings'].submit()
1436 1443
    assert 'Total number of records: 13' in resp.text
1437 1444

  
1438
    resp.forms[0]['filter-2-value'].value = 'baz'
1439
    resp = resp.forms[0].submit()
1445
    resp.forms['listing-settings']['filter-2-value'].value = 'baz'
1446
    resp = resp.forms['listing-settings'].submit()
1440 1447
    assert 'Total number of records: 24' in resp.text
1441 1448

  
1442
    resp.forms[0]['filter-2-value'].value = 'foo'
1443
    resp = resp.forms[0].submit()
1449
    resp.forms['listing-settings']['filter-2-value'].value = 'foo'
1450
    resp = resp.forms['listing-settings'].submit()
1444 1451
    assert 'Total number of records: 13' in resp.text
1445 1452
    assert '<h2>Filters</h2>' in resp.text
1446 1453
    assert '2nd field: foo' in resp.text
1447 1454

  
1448 1455
    # check it's also possible to get back to the complete list
1449
    resp.forms[0]['filter-2-value'].value = ''
1450
    resp = resp.forms[0].submit()
1456
    resp.forms['listing-settings']['filter-2-value'].value = ''
1457
    resp = resp.forms['listing-settings'].submit()
1451 1458
    assert 'Total number of records: 50' in resp.text
1452 1459

  
1453 1460
    # check it also works with item fields with a data source
1454 1461
    resp = app.get('/backoffice/management/form-title/')
1455 1462
    resp = resp.click('Statistics')
1456
    resp.forms[0]['filter-3'].checked = True
1457
    resp = resp.forms[0].submit()
1458
    resp.forms[0]['filter-3-value'].value = 'A'
1459
    resp = resp.forms[0].submit()
1463
    resp.forms['listing-settings']['filter-3'].checked = True
1464
    resp = resp.forms['listing-settings'].submit()
1465
    resp.forms['listing-settings']['filter-3-value'].value = 'A'
1466
    resp = resp.forms['listing-settings'].submit()
1460 1467
    assert 'Total number of records: 13' in resp.text
1461 1468
    assert '<h2>Filters</h2>' in resp.text
1462 1469
    assert '3rd field: aa' in resp.text
......
6185 6192
    resp.form['comment'] = 'plop'
6186 6193
    resp = resp.form.submit('submit')
6187 6194
    assert resp.location == 'http://example.net/backoffice/management/form-title/%s/#' % formdata.id
6195

  
6196

  
6197
def test_backoffice_custom_view(pub):
6198
    create_superuser(pub)
6199
    create_environment(pub)
6200

  
6201
    app = login(get_app(pub))
6202
    resp = app.get('/backoffice/management/form-title/')
6203
    assert resp.text.count('<span>User Label</span>') == 1
6204
    assert resp.text.count('<tr') == 18
6205

  
6206
    # columns
6207
    resp.forms['listing-settings']['user-label'].checked = False
6208
    resp = resp.forms['listing-settings'].submit()
6209
    # filters
6210
    resp.forms[0]['filter-2'].checked = True
6211
    resp = resp.forms['listing-settings'].submit()
6212

  
6213
    resp.forms['listing-settings']['filter-2-value'] = 'baz'
6214
    resp = resp.forms['listing-settings'].submit()
6215

  
6216
    assert resp.text.count('<span>User Label</span>') == 0
6217
    assert resp.text.count('<tr') == 9
6218

  
6219
    resp.forms['save-custom-view']['title'] = 'custom test view'
6220
    resp = resp.forms['save-custom-view'].submit()
6221
    assert resp.location.endswith('/custom-test-view/')
6222
    resp = resp.follow()
6223
    assert resp.text.count('<span>User Label</span>') == 0
6224
    assert resp.text.count('<tr') == 9
6225

  
6226
    resp.forms['listing-settings']['filter-2-value'] = 'foo'
6227
    resp = resp.forms['listing-settings'].submit()
6228
    assert resp.text.count('<tr') == 6
6229
    assert resp.forms['save-custom-view']['update'].checked is True
6230
    resp = resp.forms['save-custom-view'].submit()
6231
    assert resp.location.endswith('/custom-test-view/')
6232
    resp = resp.follow()
6233
    assert resp.text.count('<tr') == 6
6234

  
6235
    resp = app.get('/backoffice/management/other-form/')
6236
    assert 'custom test view' not in resp
6237

  
6238
    # check it's not possible to create a view without any columns
6239
    for field_key in resp.forms['listing-settings'].fields:
6240
        if not field_key:
6241
            continue
6242
        if field_key.startswith('filter'):
6243
            continue
6244
        if resp.forms['listing-settings'][field_key].attrs.get('type') != 'checkbox':
6245
            continue
6246
        resp.forms['listing-settings'][field_key].checked = False
6247
    resp = resp.forms['listing-settings'].submit()
6248
    resp.forms['save-custom-view']['title'] = 'custom test view'
6249
    resp = resp.forms['save-custom-view'].submit().follow()
6250
    assert 'Views must have at least one column.' in resp.text
6251

  
6252

  
6253
def test_backoffice_custom_view_delete(pub):
6254
    create_superuser(pub)
6255
    create_environment(pub)
6256

  
6257
    app = login(get_app(pub))
6258
    resp = app.get('/backoffice/management/form-title/')
6259

  
6260
    # columns
6261
    resp.forms['listing-settings']['user-label'].checked = False
6262
    resp = resp.forms['listing-settings'].submit()
6263
    resp.forms['save-custom-view']['title'] = 'custom test view'
6264
    resp = resp.forms['save-custom-view'].submit()
6265
    assert resp.location.endswith('/custom-test-view/')
6266
    resp = resp.follow()
6267
    resp = resp.click('Delete View')
6268
    resp = resp.form.submit()
6269
    assert resp.location.endswith('/management/form-title/')
6270
    resp = resp.follow()
6271
    assert 'custom test view' not in resp.text
6272

  
6273

  
6274
def test_backoffice_custom_map_view(pub):
6275
    test_backoffice_custom_view(pub)
6276

  
6277
    formdef = FormDef.get_by_urlname('form-title')
6278
    formdef.geolocations = {'base': 'Geolocafoobar'}
6279
    formdef.store()
6280

  
6281
    app = login(get_app(pub))
6282
    resp = app.get('/backoffice/management/form-title/')
6283
    resp = resp.click('custom test view')
6284
    assert resp.text.count('<span>User Label</span>') == 0
6285
    assert resp.text.count('<tr') == 6
6286
    resp = resp.click('Plot on a Map')
6287
    assert resp.forms['listing-settings']['filter-2-value'].value == 'foo'
6288

  
6289

  
6290
def test_backoffice_custom_view_visibility(pub):
6291
    create_environment(pub)
6292
    create_superuser(pub)
6293

  
6294
    formdef = FormDef.get_by_urlname('form-title')
6295
    agent = pub.user_class(name='agent')
6296
    agent.roles = [formdef.workflow_roles['_receiver']]
6297
    agent.store()
6298

  
6299
    account = PasswordAccount(id='agent')
6300
    account.set_password('agent')
6301
    account.user_id = agent.id
6302
    account.store()
6303

  
6304
    app = login(get_app(pub), username='agent', password='agent')
6305
    resp = app.get('/backoffice/management/form-title/')
6306

  
6307
    # columns
6308
    resp.forms['listing-settings']['user-label'].checked = False
6309
    resp = resp.forms['listing-settings'].submit()
6310

  
6311
    assert resp.text.count('<span>User Label</span>') == 0
6312

  
6313
    resp.forms['save-custom-view']['title'] = 'custom test view'
6314
    assert 'visibility' not in resp.forms['save-custom-view'].fields
6315
    resp = resp.forms['save-custom-view'].submit()
6316
    assert resp.location.endswith('/custom-test-view/')
6317
    resp = resp.follow()
6318
    assert resp.text.count('<span>User Label</span>') == 0
6319

  
6320
    # second agent
6321
    agent2 = pub.user_class(name='agent2')
6322
    agent2.roles = [formdef.workflow_roles['_receiver']]
6323
    agent2.store()
6324

  
6325
    account = PasswordAccount(id='agent2')
6326
    account.set_password('agent2')
6327
    account.user_id = agent2.id
6328
    account.store()
6329

  
6330
    app = login(get_app(pub), username='agent2', password='agent2')
6331
    resp = app.get('/backoffice/management/form-title/')
6332
    assert 'custom test view' not in resp
6333

  
6334
    # shared custom view
6335
    app = login(get_app(pub))
6336
    resp = app.get('/backoffice/management/form-title/')
6337
    resp = resp.forms['listing-settings'].submit()
6338
    resp.forms['save-custom-view']['title'] = 'shared view'
6339
    resp.forms['save-custom-view']['visibility'] = 'any'
6340
    resp = resp.forms['save-custom-view'].submit()
6341

  
6342
    app = login(get_app(pub), username='agent2', password='agent2')
6343
    resp = app.get('/backoffice/management/form-title/')
6344
    resp = resp.click('shared view')
6345

  
6346

  
6347
def test_carddata_custom_view(pub, studio):
6348
    CardDef.wipe()
6349
    user = create_user(pub)
6350
    app = login(get_app(pub))
6351
    carddef = CardDef()
6352
    carddef.name = 'foo'
6353
    carddef.fields = [
6354
        fields.StringField(id='1', label='Test', type='string', varname='foo'),
6355
    ]
6356
    carddef.backoffice_submission_roles = user.roles
6357
    carddef.workflow_roles = {'_editor': user.roles[0]}
6358
    carddef.store()
6359
    carddef.data_class().wipe()
6360

  
6361
    for i in range(50):
6362
        carddata = carddef.data_class()()
6363
        carddata.data = {'1': 'FOO %s' % i}
6364
        carddata.just_created()
6365
        carddata.store()
6366

  
6367
    resp = app.get('/backoffice/data/foo/')
6368
    if pub.is_using_postgresql():
6369
        assert resp.text.count('<tr') == 21  # header + rows of data
6370
    else:
6371
        # no pagination
6372
        assert resp.text.count('<tr') == 51  # header + rows of data
6373

  
6374
    resp = resp.forms['listing-settings'].submit()
6375
    resp.forms['save-custom-view']['title'] = 'card view'
6376
    resp = resp.forms['save-custom-view'].submit()
6377
    assert resp.location.endswith('/card-view/')
6378
    resp = resp.follow()
tests/utilities.py
9 9
import sys
10 10
import threading
11 11

  
12
from wcs import sql, sessions
12
from wcs import sql, sessions, custom_views
13 13

  
14 14
from webtest import TestApp
15 15
from quixote import cleanup, get_publisher
......
79 79
        pub.user_class = sql.SqlUser
80 80
        pub.tracking_code_class = sql.TrackingCode
81 81
        pub.session_class = sql.Session
82
        pub.custom_view_class = sql.CustomView
82 83
        pub.is_using_postgresql = lambda: True
83 84
    else:
84 85
        pub.user_class = User
85 86
        pub.tracking_code_class = TrackingCode
86 87
        pub.session_class = sessions.BasicSession
88
        pub.custom_view_class = custom_views.CustomView
87 89
        pub.is_using_postgresql = lambda: False
88 90

  
89 91
    pub.session_manager_class = sessions.StorageSessionManager
......
165 167
        sql.do_user_table()
166 168
        sql.do_tracking_code_table()
167 169
        sql.do_session_table()
170
        sql.do_custom_views_table()
168 171
        sql.do_meta_table()
169 172

  
170 173
        conn.close()
wcs/api.py
31 31
from .qommon.errors import (AccessForbiddenError, QueryError, TraversalError,
32 32
    UnknownNameIdAccessForbiddenError, RequestError)
33 33
from .qommon.form import ComputedExpressionWidget, ConditionWidget
34
from .qommon.storage import Equal
34 35

  
35 36
from wcs.categories import Category
36 37
from wcs.conditions import Condition, ValidationError
......
202 203
        return ApiFormdataPage(self.formdef, formdata)
203 204

  
204 205
    def _q_traverse(self, path):
206
        if len(path) == 2 and path[0] == 'list':
207
            if path[1] == '':
208
                path = ['list']  # default view, with trailing slash
209
            else:
210
                # custom view
211
                for view in self.get_custom_views([Equal('slug', path[1])]):
212
                    self.view = view
213
                    path = ['list']
214

  
205 215
        self.is_webhook = False
206 216
        if len(path) > 1:
207 217
            # webhooks have their own access checks, request cannot be blocked
wcs/backoffice/data_management.py
80 80

  
81 81
class CardPage(FormPage):
82 82
    _q_exports = ['', 'csv', 'xls', 'ods', 'json', 'export', 'map', 'geojson', 'add',
83
                  ('save-view', 'save_view'), ('delete-view', 'delete_view'),
83 84
                  ('import-csv', 'import_csv'),
84 85
                  ('data-sample-csv', 'data_sample_csv')]
86
    admin_permisison = 'cards'
85 87

  
86
    def __init__(self, component):
88
    def __init__(self, component=None, formdef=None, view=None):
87 89
        try:
88
            self.formdef = CardDef.get_by_urlname(component)
90
            self.formdef = formdef if formdef else CardDef.get_by_urlname(component)
89 91
        except KeyError:
90 92
            raise errors.TraversalError()
91
        self.add = CardFillPage(component)
93
        self.add = CardFillPage(self.formdef.url_name)
94
        if view:
95
            self.view = view
92 96

  
93 97
    def can_user_add_cards(self):
94 98
        if not self.formdef.backoffice_submission_roles:
......
104 108
        return htmltext('<span class="actions"><a href="./add/">%s</a></span>') % _('Add')
105 109

  
106 110
    def get_default_filters(self, mode):
111
        if self.view:
112
            return self.view.get_default_filters()
107 113
        return ()
108 114

  
109 115
    def get_default_columns(self):
110
        field_ids = ['id', 'time']
111
        for field in self.formdef.get_all_fields():
112
            if hasattr(field, 'get_view_value') and field.include_in_listing:
113
                field_ids.append(field.id)
116
        if self.view:
117
            field_ids = self.view.get_columns()
118
        else:
119
            field_ids = ['id', 'time']
120
            for field in self.formdef.get_all_fields():
121
                if hasattr(field, 'get_view_value') and field.include_in_listing:
122
                    field_ids.append(field.id)
114 123
        return field_ids
115 124

  
116 125
    def get_filter_from_query(self, default=Ellipsis):
......
270 279
        return redirect('import-csv?job=%s' % job.id)
271 280

  
272 281
    def _q_lookup(self, component):
282

  
283
        if not self.view:
284
            for view in self.get_custom_views():
285
                if view.slug == component:
286
                    return self.__class__(formdef=self.formdef, view=view)
287

  
273 288
        try:
274 289
            filled = self.formdef.data_class().get(component)
275 290
        except KeyError:
wcs/backoffice/management.py
1010 1010

  
1011 1011
class FormPage(Directory):
1012 1012
    _q_exports = ['', 'csv', 'stats', 'xls', 'ods', 'json', 'export', 'map',
1013
                  'geojson', ('filter-options', 'filter_options')]
1014

  
1015
    def __init__(self, component):
1016
        try:
1017
            self.formdef = FormDef.get_by_urlname(component)
1018
        except KeyError:
1019
            raise errors.TraversalError()
1020
        get_response().breadcrumb.append( (component + '/', self.formdef.name) )
1013
                  'geojson', ('filter-options', 'filter_options'),
1014
                  ('save-view', 'save_view'), ('delete-view', 'delete_view'),]
1015
    view = None
1016
    admin_permisison = 'forms'
1017

  
1018
    def __init__(self, component=None, formdef=None, view=None):
1019
        self.view_type = None
1020
        if component:
1021
            try:
1022
                self.formdef = FormDef.get_by_urlname(component)
1023
            except KeyError:
1024
                raise errors.TraversalError()
1025
            get_response().breadcrumb.append((component + '/', self.formdef.name))
1026
        else:
1027
            self.formdef = formdef
1028
            self.view = view
1029
            get_response().breadcrumb.append((view.slug + '/', view.title))
1021 1030

  
1022 1031
    def check_access(self, api_name=None):
1023 1032
        session = get_session()
......
1033 1042
            else:
1034 1043
                raise errors.AccessUnauthorizedError()
1035 1044

  
1045
    def get_custom_views(self, criterias=None):
1046
        for view in get_publisher().custom_view_class.select(clause=criterias):
1047
            if view.match(get_request().user, self.formdef):
1048
                yield view
1049

  
1036 1050
    def get_formdata_sidebar_actions(self, qs=''):
1037 1051
        r = TemplateIO(html=True)
1038 1052
        r += htmltext(' <li><a data-base-href="ods" href="ods%s">%s</a></li>') % (
......
1054 1068
        r += htmltext('<ul id="sidebar-actions">')
1055 1069
        r += self.get_formdata_sidebar_actions(qs=qs)
1056 1070
        r += htmltext('</ul>')
1071
        views = list(self.get_custom_views())
1072
        if views:
1073
            r += htmltext('<h3>%s</h3>') % _('Custom Views')
1074
            r += htmltext('<ul id="sidebar-custom-views">')
1075
            view_type = 'map' if self.view_type == 'map' else ''
1076
            for view in views:
1077
                if self.view:
1078
                    active = bool(self.view.slug == view.slug)
1079
                    r += htmltext('<li class="active">' if active else '<li>')
1080
                    r += htmltext('<a href="../%s/%s">%s</a></li>') % (view.slug, view_type, view.title)
1081
                else:
1082
                    r += htmltext('<li><a href="%s/%s">%s</a></li>') % (view.slug, view_type, view.title)
1083
            r += htmltext('</ul>')
1057 1084
        return r.getvalue()
1058 1085

  
1059 1086
    def get_default_filters(self, mode):
1087
        if self.view:
1088
            return self.view.get_default_filters()
1060 1089
        if mode == 'listing':
1061 1090
            # enable status filter by default
1062 1091
            return ('status',)
......
1135 1164
            FakeField('end', 'period-date', _('End')),
1136 1165
        ]
1137 1166
        default_filters = self.get_default_filters(mode)
1167

  
1138 1168
        filter_fields = []
1139 1169
        for field in period_fake_fields + self.get_formdef_fields():
1140 1170
            field.enabled = False
......
1148 1178
                field.enabled = 'filter-%s' % field.id in get_request().form
1149 1179
            else:
1150 1180
                field.enabled = (field.id in default_filters)
1151
                if field.type in ('item', 'items'):
1181
                if not self.view and field.type in ('item', 'items'):
1152 1182
                    field.enabled = field.in_filters
1153 1183

  
1154
        r += htmltext('<h3><span>%s</span> <span class="change">(<a id="filter-settings">%s</a>)</span></h3>' % (
1155
            _('Filters'), _('change')))
1184
        r += htmltext('<h3><span>%s</span>') % _('Options')
1185
        r += htmltext('<span class="change">(')
1186
        r += htmltext('<a id="filter-settings">%s</a>') % _('filters')
1187
        if self.view_type in ('table', 'map'):
1188
            if self.view_type == 'table':
1189
                columns_settings_labels = (_('Columns Settings'), _('columns'))
1190
            elif self.view_type == 'map':
1191
                columns_settings_labels = (_('Marker Settings'), _('markers'))
1192
            r += htmltext(' - <a id="columns-settings" title="%s">%s</a>') % columns_settings_labels
1193
        r += htmltext(')</span></h3>')
1194

  
1195
        filters_dict = {}
1196
        if self.view:
1197
            filters_dict.update(self.view.get_filters_dict())
1198
        filters_dict.update(get_request().form)
1156 1199

  
1157 1200
        for filter_field in filter_fields:
1158 1201
            if not filter_field.enabled:
1159 1202
                continue
1160 1203

  
1161 1204
            filter_field_key = 'filter-%s-value' % filter_field.id
1162
            filter_field_value = get_request().form.get(filter_field_key)
1205
            filter_field_value = filters_dict.get(filter_field_key)
1163 1206

  
1164 1207
            if filter_field.type == 'status':
1165 1208
                r += htmltext('<div class="widget">')
......
1214 1257
                        options.insert(0, (None, '', ''))
1215 1258
                        attrs = {'data-refresh-options': str(filter_field.id)}
1216 1259
                    else:
1217
                        current_filter = get_request().form.get('filter-%s-value' % filter_field.id)
1260
                        current_filter = filters_dict.get('filter-%s-value' % filter_field.id)
1218 1261
                        options = [(current_filter, '', current_filter or '')]
1219 1262
                        attrs = {'data-remote-options': str(filter_field.id)}
1220 1263
                        get_response().add_javascript(['jquery.js', '../../i18n.js', 'qommon.forms.js', 'select2.js'])
......
1263 1306
        return r.getvalue()
1264 1307

  
1265 1308
    def get_fields_sidebar(self, selected_filter, fields, offset=None,
1266
            limit=None, order_by=None, columns_settings_label=None,
1309
            limit=None, order_by=None,
1267 1310
            query=None, criterias=None):
1268 1311
        get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'wcs.listing.js'])
1269 1312

  
......
1292 1335

  
1293 1336
        r += self.get_filter_sidebar(selected_filter=selected_filter, query=query, criterias=criterias)
1294 1337

  
1295
        r += htmltext('<button class="refresh">%s</button>') % _('Refresh')
1296

  
1297
        if columns_settings_label:
1298
            r += htmltext('<button id="columns-settings">%s</button>') % columns_settings_label
1338
        r += htmltext('<button class="refresh" hidden>%s</button>') % _('Refresh')
1299 1339

  
1340
        if self.view_type in ('table', 'map'):
1300 1341
            # column settings dialog content
1301 1342
            r += htmltext('<div style="display: none;">')
1302
            r += htmltext('<ul id="columns-filter">')
1303
            for field in self.get_formdef_fields():
1343
            r += htmltext('<ul id="columns-filter" class="objects-list columns-filter">')
1344
            column_order = []
1345
            field_ids = [x.id for x in fields]
1346

  
1347
            def get_column_position(x):
1348
                if x.id in field_ids:
1349
                    return field_ids.index(x.id)
1350
                return 9999
1351

  
1352
            for field in sorted(self.get_formdef_fields(), key=get_column_position):
1304 1353
                if not hasattr(field, str('get_view_value')):
1305 1354
                    continue
1306
                r += htmltext('<li><input type="checkbox" name="%s"') % field.id
1307
                if field.id in [x.id for x in fields]:
1355
                r += htmltext('<li><span class="handle">⣿</span><label><input type="checkbox" name="%s"') % field.id
1356
                if field.id in field_ids:
1308 1357
                    r += htmltext(' checked="checked"')
1309
                r += htmltext(' id="fields-column-%s"') % field.id
1310 1358
                r += htmltext('/>')
1311
                r += htmltext('<label for="fields-column-%s">%s</label>') % (
1312
                        field.id, misc.ellipsize(field.label, 70))
1359
                r += htmltext('%s</label>') % misc.ellipsize(field.label, 70)
1313 1360
                r += htmltext('</li>')
1361
                column_order.append(str(field.id))
1314 1362
            r += htmltext('</ul>')
1315 1363
            r += htmltext('</div>')
1364
            r += htmltext('<input type="hidden" name="columns-order" value="%s">' % ','.join(column_order))
1316 1365
        r += htmltext('</form>')
1366

  
1367
        r += self.get_custom_view_form().render()
1368
        r += htmltext('<button id="save-view">%s</button>') % _('Save View')
1369
        if self.can_delete_view():
1370
            r += htmltext(' <a data-popup id="delete-view" href="./delete-view" class="button">%s</a>') % _('Delete View')
1371

  
1317 1372
        return r.getvalue()
1318 1373

  
1374
    def get_custom_view_form(self):
1375
        form = Form(method='post', id='save-custom-view', hidden='hidden', action='save-view')
1376
        form.add(HiddenWidget, 'qs', value=get_request().get_query())
1377
        form.add(StringWidget, 'title', title=_('Title'), required=True,
1378
                value=self.view.title if self.view else None)
1379
        if get_publisher().get_backoffice_root().is_accessible(self.admin_permisison):
1380
            # admins can create views accessible to everyone
1381
            form.add(RadiobuttonsWidget, 'visibility', title=_('Visibility'),
1382
                    value=self.view.visibility if self.view else 'owner',
1383
                    options=[
1384
                        ('owner', _('to me only'), 'owner'),
1385
                        ('any', _('to any users'), 'any')
1386
                    ])
1387
        if self.view and (self.view.user_id == get_request().user.id or
1388
                          get_publisher().get_backoffice_root().is_accessible('forms')):
1389
            form.add(CheckboxWidget, 'update', title=_('Update existing view settings'), value=True)
1390
        form.add_submit('submit', _('Save View'))
1391
        form.add_submit('cancel', _('Cancel'))
1392
        return form
1393

  
1394
    def save_view(self):
1395
        form = self.get_custom_view_form()
1396
        if form.get_widget('update') and form.get_widget('update').parse():
1397
            custom_view = self.view
1398
        else:
1399
            custom_view = get_publisher().custom_view_class()
1400
        custom_view.title = form.get_widget('title').parse()
1401
        if not custom_view.title:
1402
            get_session().message = ('error', _('Missing title.'))
1403
            return redirect('.')
1404
        custom_view.user = get_request().user
1405
        custom_view.formdef = self.formdef
1406
        custom_view.set_from_qs(form.get_widget('qs').parse())
1407
        if not custom_view.columns['list']:
1408
            get_session().message = ('error', _('Views must have at least one column.'))
1409
            return redirect('.')
1410
        if form.get_widget('visibility'):
1411
            custom_view.visibility = form.get_widget('visibility').parse()
1412
        custom_view.store()
1413
        if self.view:
1414
            return redirect('../' + custom_view.slug + '/')
1415
        else:
1416
            return redirect(custom_view.slug + '/')
1417

  
1418
    def can_delete_view(self):
1419
        if not self.view:
1420
            return False
1421
        if str(self.view.user_id) == str(get_request().user.id):
1422
            return True
1423
        return get_publisher().get_backoffice_root().is_accessible(self.admin_permisison)
1424

  
1425
    def delete_view(self):
1426
        if not self.can_delete_view():
1427
            raise errors.AccessForbiddenError()
1428
        form = Form(enctype='multipart/form-data')
1429
        form.widgets.append(HtmlWidget('<p>%s</p>' % _(
1430
            'You are about to remove the \"%s\" custom view.') % self.view.title))
1431
        if self.view.visibility == 'any':
1432
            form.widgets.append(HtmlWidget('<div class="warningnotice"<p>%s</p></div>' % _(
1433
                'Beware this view is available to all users, and will thus be removed for everyone.')))
1434
        form.add_submit('delete', _('Delete'))
1435
        form.add_submit('cancel', _('Cancel'))
1436
        if form.get_widget('cancel').parse():
1437
            return redirect('.')
1438
        if not form.is_submitted() or form.has_errors():
1439
            r = TemplateIO(html=True)
1440
            r += htmltext('<h2>%s</h2>') % (_('Delete Custom View'))
1441
            r += form.render()
1442
            return r.getvalue()
1443
        else:
1444
            self.view.remove_self()
1445
            return redirect('..')
1446

  
1319 1447
    def get_formdef_fields(self):
1320 1448
        fields = []
1321 1449
        fields.append(FakeField('id', 'id', _('Number')))
......
1333 1461
        return fields
1334 1462

  
1335 1463
    def get_default_columns(self):
1336
        field_ids = ['id', 'time', 'last_update_time', 'user-label']
1337
        for field in self.formdef.get_all_fields():
1338
            if hasattr(field, 'get_view_value') and field.include_in_listing:
1339
                field_ids.append(field.id)
1340
        field_ids.append('status')
1464
        if self.view:
1465
            field_ids = self.view.get_columns()
1466
        else:
1467
            field_ids = ['id', 'time', 'last_update_time', 'user-label']
1468
            for field in self.formdef.get_all_fields():
1469
                if hasattr(field, 'get_view_value') and field.include_in_listing:
1470
                    field_ids.append(field.id)
1471
            field_ids.append('status')
1341 1472
        return field_ids
1342 1473

  
1343 1474
    def get_fields_from_query(self, ignore_form=False):
......
1350 1481
            if field.id in field_ids:
1351 1482
                fields.append(field)
1352 1483

  
1484
        if 'columns-order' in get_request().form or self.view:
1485
            if ignore_form or 'columns-order' not in get_request().form:
1486
                field_order = field_ids
1487
            else:
1488
                field_order = get_request().form['columns-order'].split(',')
1489

  
1490
            def field_position(x):
1491
                if x.id in field_order:
1492
                    return field_order.index(x.id)
1493
                return 9999
1494

  
1495
            fields.sort(key=field_position)
1496

  
1353 1497
        if not fields:
1354 1498
            return self.get_fields_from_query(ignore_form=True)
1355 1499

  
......
1358 1502
    def get_filter_from_query(self, default='waiting'):
1359 1503
        if 'filter' in get_request().form:
1360 1504
            return get_request().form['filter']
1505
        if self.view:
1506
            view_filter = self.view.get_filter()
1507
            if view_filter:
1508
                return view_filter
1361 1509
        if self.formdef.workflow.possible_status:
1362 1510
            return default
1363 1511
        return 'all'
......
1371 1519
        ]
1372 1520
        filter_fields = []
1373 1521
        criterias = []
1522

  
1523
        filters_dict = {}
1524
        if self.view:
1525
            filters_dict.update(self.view.get_filters_dict())
1526
        filters_dict.update(get_request().form)
1527

  
1374 1528
        for filter_field in period_fake_fields + self.get_formdef_fields():
1375 1529
            if filter_field.type not in ('item', 'bool', 'items', 'period-date'):
1376 1530
                continue
......
1380 1534
            if filter_field.varname:
1381 1535
                # if this is a field with a varname and filter-%(varname)s is
1382 1536
                # present in the query string, enable this filter.
1383
                if get_request().form.get('filter-%s' % filter_field.varname):
1537
                if filters_dict.get('filter-%s' % filter_field.varname):
1384 1538
                    filter_field_key = 'filter-%s' % filter_field.varname
1385 1539

  
1386
            if get_request().form.get('filter-%s' % filter_field.id):
1540
            if filters_dict.get('filter-%s' % filter_field.id):
1387 1541
                # if there's a filter-%(id)s, it is used to enable the actual
1388 1542
                # filter, and the value will be found in filter-%s-value.
1389 1543
                filter_field_key = 'filter-%s-value' % filter_field.id
......
1392 1546
                # if there's not known filter key, skip.
1393 1547
                continue
1394 1548

  
1395
            filter_field_value = get_request().form.get(filter_field_key)
1549
            filter_field_value = filters_dict.get(filter_field_key)
1396 1550
            if not filter_field_value:
1397 1551
                continue
1398 1552

  
......
1449 1603
        return mass_actions
1450 1604

  
1451 1605
    def _q_index(self):
1606
        self.view_type = 'table'
1452 1607
        self.check_access()
1453 1608
        get_logger().info('backoffice - form %s - listing' % self.formdef.name)
1454 1609

  
......
1467 1622
        else:
1468 1623
            limit = get_request().form.get('limit', 0)
1469 1624
        offset = get_request().form.get('offset', 0)
1470
        order_by = get_request().form.get('order_by',
1471
            get_publisher().get_site_option('default-sort-order') or '-receipt_time')
1625
        order_by = get_request().form.get('order_by')
1626
        if self.view and not order_by:
1627
            order_by = self.view.order_by
1628
        if not order_by:
1629
            order_by = get_publisher().get_site_option('default-sort-order') or '-receipt_time'
1472 1630
        query = get_request().form.get('q')
1473 1631

  
1474 1632
        qs = ''
......
1510 1668
            get_response().filter = {'raw': True}
1511 1669
            return table
1512 1670

  
1513
        html_top('management', '%s - %s' % (_('Listing'), self.formdef.name))
1671
        view_name = self.view.title if self.view else _('Listing')
1672
        html_top('management', '%s - %s' % (view_name, self.formdef.name))
1514 1673
        r = TemplateIO(html=True)
1515 1674
        r += htmltext('<div id="appbar">')
1516
        r += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, _('Listing'))
1675
        r += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, view_name)
1517 1676
        r += get_session().display_message()
1518 1677
        r += self.listing_top_actions()
1519 1678
        r += htmltext('</div>')
......
1526 1685
        get_response().filter['sidebar'] = self.get_formdata_sidebar(qs) + \
1527 1686
                self.get_fields_sidebar(selected_filter, fields, limit=limit,
1528 1687
                        query=query, criterias=criterias,
1529
                        offset=offset, order_by=order_by,
1530
                        columns_settings_label=_('Columns Settings'))
1688
                        offset=offset, order_by=order_by)
1531 1689

  
1532 1690
        return r.getvalue()
1533 1691

  
......
1841 1999
        selected_filter = self.get_filter_from_query(default='all')
1842 2000
        criterias = self.get_criterias_from_query()
1843 2001
        order_by = get_request().form.get('order_by', None)
2002
        if self.view and not order_by:
2003
            order_by = self.view.order_by
1844 2004
        query = get_request().form.get('q') if not anonymise else None
1845 2005
        offset = None
1846 2006
        if 'offset' in get_request().form:
......
1872 2032
                'receipt_time': datetime.datetime(*filled.receipt_time[:6]),
1873 2033
                'last_update_time': datetime.datetime(*filled.last_update_time[:6]),
1874 2034
            } for filled in items]
1875
        if isinstance(self.formdef, CardDef):
2035
        if isinstance(self.formdef, CardDef) or self.view:
2036
            # for cards and custom views return results in a dictionary, as it
2037
            # provides a better path for evolutions
1876 2038
            output = {'data': output}
1877 2039
        return json.dumps(output,
1878 2040
                cls=misc.JSONEncoder)
......
1980 2142
        return IcsDirectory()
1981 2143

  
1982 2144
    def map(self):
2145
        self.view_type = 'map'
1983 2146
        get_response().add_javascript(['qommon.map.js'])
1984 2147
        html_top('management', '%s - %s' % (_('Form'), self.formdef.name))
1985 2148
        r = TemplateIO(html=True)
......
1995 2158

  
1996 2159
        fields = self.get_fields_from_query()
1997 2160
        selected_filter = self.get_filter_from_query()
1998
        get_response().filter['sidebar'] = self.get_fields_sidebar(selected_filter,
1999
                fields, columns_settings_label=_('Markers Settings'))
2161

  
2162
        qs = ''
2163
        if get_request().get_query():
2164
            qs = '?' + get_request().get_query()
2165
        get_response().filter['sidebar'] = self.get_formdata_sidebar(qs) + \
2166
                self.get_fields_sidebar(selected_filter, fields)
2000 2167

  
2001 2168
        r += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, _('Map'))
2002 2169
        r += htmltext('<div %s></div>' % ' '.join(['%s="%s"' % x for x in attrs.items()]))
......
2212 2379
        if component == 'ics':
2213 2380
            return self.ics()
2214 2381

  
2382
        if not self.view:
2383
            for view in self.get_custom_views([Equal('slug', component)]):
2384
                return self.__class__(formdef=self.formdef, view=view)
2385

  
2215 2386
        try:
2216 2387
            filled = self.formdef.data_class().get(component)
2217 2388
        except KeyError:
wcs/custom_views.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2020  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.utils.six.moves.urllib import parse as urlparse
18
from quixote import get_publisher
19

  
20
from wcs.carddef import CardDef
21
from wcs.formdef import FormDef
22
from wcs.qommon.storage import StorableObject
23
from wcs.qommon.misc import simplify
24

  
25

  
26
class CustomView(StorableObject):
27
    _names = 'custom-views'
28

  
29
    title = None
30
    slug = None
31
    user_id = None
32
    visibility = 'owner'
33
    formdef_type = None
34
    formdef_id = None
35
    columns = None
36
    filters = None
37
    order_by = None
38

  
39
    @property
40
    def user(self):
41
        return get_publisher().user_class.get(self.user_id)
42

  
43
    @user.setter
44
    def user(self, value):
45
        self.user_id = str(value.id)
46

  
47
    @property
48
    def formdef(self):
49
        if self.formdef_type == 'formdef':
50
            return FormDef.get(self.formdef_id)
51
        else:
52
            return CardDef.get(self.formdef_id)
53

  
54
    @formdef.setter
55
    def formdef(self, value):
56
        self.formdef_id = str(value.id)
57
        self.formdef_type = value.xml_root_node
58

  
59
    def match(self, user, formdef):
60
        if self.visibility == 'owner' and self.user_id != str(user.id):
61
            return False
62
        if self.formdef_type != formdef.xml_root_node:
63
            return False
64
        if self.formdef_id != str(formdef.id):
65
            return False
66
        return True
67

  
68
    def set_from_qs(self, qs):
69
        parsed_qs = urlparse.parse_qsl(qs)
70
        self.columns = {
71
            'list': [
72
                {'id': key} for (key, value) in parsed_qs if value == 'on' and not key.startswith('filter-')
73
            ],
74
        }
75

  
76
        columns_order = [x[1] for x in parsed_qs if x[0] == 'columns-order']
77
        if columns_order:
78
            field_order = columns_order[0].split(',')
79

  
80
            def field_position(x):
81
                if x['id'] in field_order:
82
                    return field_order.index(x['id'])
83
                return 9999
84

  
85
            self.columns['list'].sort(key=field_position)
86

  
87
        order_by = [x[1] for x in parsed_qs if x[0] == 'order_by']
88
        if order_by:
89
            self.order_by = order_by[0]
90

  
91
        self.filters = {key: value for (key, value) in parsed_qs if key.startswith('filter')}
92

  
93
    def ensure_slug(self):
94
        if self.slug:
95
            return
96
        existing_slugs = {
97
            x.slug: True
98
            for x in self.select(ignore_errors=True)
99
            if (x.user_id == self.user_id and x.visibility == 'owner' and self.visibility == 'owner')
100
            and x.formdef_type == self.formdef_type
101
            and x.formdef_id == self.formdef_id
102
        }
103
        base_slug = simplify(self.title)
104
        self.slug = base_slug
105
        i = 2
106
        while self.slug in existing_slugs:
107
            self.slug = '%s-%s' % (base_slug, i)
108
            i += 1
109

  
110
    def store(self, *args, **kwargs):
111
        self.ensure_slug()
112
        return super(CustomView, self).store(*args, **kwargs)
113

  
114
    def get_columns(self):
115
        if self.columns and 'list' in self.columns:
116
            return [x['id'] for x in self.columns['list']]
117
        else:
118
            return []
119

  
120
    def get_filter(self):
121
        return self.filters.get('filter')
122

  
123
    def get_filters_dict(self):
124
        return self.filters
125

  
126
    def get_default_filters(self):
127
        return [key[7:] for key in self.filters if key.startswith('filter-')]
wcs/publisher.py
46 46
from .root import RootDirectory
47 47
from .backoffice import RootDirectory as BackofficeRootDirectory
48 48
from .admin import RootDirectory as AdminRootDirectory
49
from . import custom_views
49 50
from . import sessions
50 51
from .qommon.cron import CronJob
51 52

  
......
148 149
            self.user_class = sql.SqlUser
149 150
            self.tracking_code_class = sql.TrackingCode
150 151
            self.session_class = sql.Session
152
            self.custom_view_class = sql.CustomView
151 153
            sql.get_connection(new=True)
152 154
        else:
153 155
            self.user_class = User
154 156
            self.tracking_code_class = TrackingCode
155 157
            self.session_class = sessions.BasicSession
158
            self.custom_view_class = custom_views.CustomView
156 159

  
157 160
        self.session_manager_class = sessions.StorageSessionManager
158 161
        self.set_session_manager(self.session_manager_class(session_class=self.session_class))
......
298 301
        sql.do_session_table()
299 302
        sql.do_user_table()
300 303
        sql.do_tracking_code_table()
304
        sql.do_custom_views_table()
301 305
        sql.do_meta_table()
302 306
        from .formdef import FormDef
303 307
        from .carddef import CardDef
wcs/qommon/static/css/dc2/admin.css
1084 1084
}
1085 1085

  
1086 1086
ul#field-filter,
1087
ul#columns-filter {
1087
ul.columns-filter {
1088 1088
	list-style: none;
1089 1089
	padding-left: 0;
1090 1090
	margin-left: 0;
1091
}
1092

  
1093
ul#field-filter {
1091 1094
	-webkit-column-count: 2;
1092 1095
	-moz-column-count: 2;
1093 1096
	column-count: 2;
1094 1097
}
1095 1098

  
1099
ul.columns-filter span.handle {
1100
	padding: 0;
1101
	position: absolute;
1102
	width: 2em;
1103
	cursor: move;
1104
	display: inline-block;
1105
	padding: 0 0.5ex;
1106
	text-align: center;
1107
	width: 1em;
1108
}
1109

  
1110
ul.columns-filter li {
1111
	padding-left: 0;
1112
}
1113

  
1114
ul.columns-filter li label {
1115
	padding-left: 2em;
1116
}
1117

  
1096 1118
ul.multipage li {
1097 1119
	margin-left: 2em;
1098 1120
}
......
1293 1315
	}
1294 1316
}
1295 1317

  
1318
a#columns-settings,
1296 1319
a#filter-settings {
1297 1320
	cursor: pointer;
1298 1321
}
......
1883 1906
	margin-top: 1em;
1884 1907
	white-space: pre-line;
1885 1908
}
1909

  
1910
#sidebar-custom-views .active {
1911
	font-weight: bold;
1912
}
wcs/qommon/static/js/wcs.listing.js
201 201
  /* column settings */
202 202
  $('#columns-settings').click(function() {
203 203
    var dialog = $('<form>');
204
    $('#columns-filter').clone().appendTo(dialog);
205
    $(dialog).find('input').each(function(idx, elem) {
206
      $(this).attr('id', 'dlg-' + $(this).attr('id'));
207
    });
208
    $(dialog).find('label').each(function(idx, elem) {
209
      $(this).attr('for', 'dlg-' + $(this).attr('for'));
210
    });
204
    var $dialog_filter = $('#columns-filter').clone().attr('id', null);
205
    $dialog_filter.appendTo(dialog);
206
    $dialog_filter.sortable({handle: '.handle'})
211 207
    $(dialog).dialog({
212 208
            modal: true,
213 209
            resizable: false,
214
            title: $('#columns-settings').text(),
210
            title: $('#columns-settings').attr('title'),
215 211
            width: '30em'});
216 212
    $(dialog).dialog('option', 'buttons', [
217 213
            {text: $('form#listing-settings button.refresh').text(),
218 214
             click: function() {
219
                $(this).find('input[type="checkbox"]').each(function(idx, elem) {
220
                  var checked = $(elem).prop('checked');
221
                  $('form#listing-settings input[name="' + $(elem).attr('name') + '"]').attr('checked', checked);
222
                  $('form#listing-settings input[name="' + $(elem).attr('name') + '"]').prop('checked', checked);
223
                });
215
                var $container = $('#columns-filter').parent();
216
                $('#columns-filter').remove();
217
                $dialog_filter.attr('id', 'columns-filter');
218
                $dialog_filter.appendTo($container);
219
                $('[name="columns-order"]').val($('#columns-filter input:checked').map(function() { return $(this).attr('name'); }).get().join());
224 220
                $(this).dialog('close');
225 221
                $('form#listing-settings').submit();
226 222
              }
......
302 298
    return false;
303 299
  });
304 300

  
301
  $('button#save-view').on('click', function() {
302
    var div_dialog = $('<div>');
303
    $('#save-custom-view').clone().attr('hidden', null).appendTo(div_dialog);
304
    $(div_dialog).find('[name=qs]').val($('form#listing-settings').serialize());
305
    $(div_dialog).find('.buttons').hide();
306
    var dialog = $(div_dialog).dialog({
307
            modal: true,
308
            resizable: false,
309
            title: $(this).text(),
310
            width: 'auto',
311
            buttons: [
312
              {text: $(div_dialog).find('.cancel-button').text(),
313
               class: 'cancel-button',
314
               click: function() { $(this).dialog('close'); }
315
              },
316
              {text: $(div_dialog).find('.submit-button').text(),
317
               class: 'submit-button',
318
               click: function() { $(div_dialog).find('.submit-button button').click(); return false; }
319
              }
320
            ]
321
    });
322
    return false;
323
  });
324

  
305 325
  /* automatically refresh on filter change */
306 326
  $('form#listing-settings select').change(function() {
307 327
    $('form#listing-settings').submit();
wcs/sql.py
16 16

  
17 17
import psycopg2
18 18
import psycopg2.extensions
19
import psycopg2.extras
19 20
import datetime
20 21
import time
21 22
import re
......
38 39

  
39 40
import wcs.categories
40 41
import wcs.carddata
42
import wcs.custom_views
41 43
import wcs.formdata
42 44
import wcs.tracking_code
43 45
import wcs.users
......
47 49
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
48 50
psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY)
49 51

  
52
# automatically adapt dictionaries into json fields
53
psycopg2.extensions.register_adapter(dict, psycopg2.extras.Json)
54

  
55

  
50 56
SQL_TYPE_MAPPING = {
51 57
    'title': None,
52 58
    'subtitle': None,
......
754 760
    cur.close()
755 761

  
756 762

  
763
def do_custom_views_table():
764
    conn, cur = get_connection_and_cursor()
765
    table_name = 'custom_views'
766

  
767
    cur.execute('''SELECT COUNT(*) FROM information_schema.tables
768
                    WHERE table_schema = 'public'
769
                      AND table_name = %s''', (table_name,))
770
    if cur.fetchone()[0] == 0:
771
        cur.execute('''CREATE TABLE %s (id varchar PRIMARY KEY,
772
                                        title varchar,
773
                                        slug varchar,
774
                                        user_id varchar,
775
                                        visibility varchar,
776
                                        formdef_type varchar,
777
                                        formdef_id varchar,
778
                                        order_by varchar,
779
                                        columns jsonb,
780
                                        filters jsonb
781
                                        )''' % table_name)
782
    cur.execute('''SELECT column_name FROM information_schema.columns
783
                    WHERE table_schema = 'public'
784
                      AND table_name = %s''', (table_name,))
785
    existing_fields = set([x[0] for x in cur.fetchall()])
786

  
787
    needed_fields = set([x[0] for x in CustomView._table_static_fields])
788

  
789
    # delete obsolete fields
790
    for field in (existing_fields - needed_fields):
791
        cur.execute('''ALTER TABLE %s DROP COLUMN %s''' % (table_name, field))
792

  
793
    conn.commit()
794
    cur.close()
795

  
796

  
757 797
@guard_postgres
758 798
def do_meta_table(conn=None, cur=None, insert_current_sql_level=True):
759 799
    own_conn = False
......
2103 2143
        return []
2104 2144

  
2105 2145

  
2146
class CustomView(SqlMixin, wcs.custom_views.CustomView):
2147
    _table_name = 'custom_views'
2148
    _table_static_fields = [
2149
        ('id', 'varchar'),
2150
        ('title', 'varchar'),
2151
        ('slug', 'varchar'),
2152
        ('user_id', 'varchar'),
2153
        ('visibility', 'varchar'),
2154
        ('formdef_type', 'varchar'),
2155
        ('formdef_id', 'varchar'),
2156
        ('order_by', 'varchar'),
2157
        ('columns', 'jsonb'),
2158
        ('filters', 'jsonb'),
2159
    ]
2160

  
2161
    @guard_postgres
2162
    @invalidate_substitution_cache
2163
    def store(self):
2164
        self.ensure_slug()
2165
        sql_dict = {
2166
            'id': self.id,
2167
            'title': self.title,
2168
            'slug': self.slug,
2169
            'user_id': self.user_id,
2170
            'visibility': self.visibility,
2171
            'formdef_type': self.formdef_type,
2172
            'formdef_id': self.formdef_id,
2173
            'order_by': self.order_by,
2174
            'columns': self.columns,
2175
            'filters': self.filters,
2176
        }
2177

  
2178
        conn, cur = get_connection_and_cursor()
2179
        if not self.id:
2180
            column_names = sql_dict.keys()
2181
            sql_dict['id'] = self.get_new_id()
2182
            sql_statement = '''INSERT INTO %s (%s)
2183
                               VALUES (%s)
2184
                               RETURNING id''' % (
2185
                                       self._table_name,
2186
                                       ', '.join(column_names),
2187
                                       ', '.join(['%%(%s)s' % x for x in column_names]))
2188
            while True:
2189
                try:
2190
                    cur.execute(sql_statement, sql_dict)
2191
                except psycopg2.IntegrityError:
2192
                    conn.rollback()
2193
                    sql_dict['id'] = self.get_new_id()
2194
                else:
2195
                    break
2196
            self.id = str_encode(cur.fetchone()[0])
2197
        else:
2198
            column_names = sql_dict.keys()
2199
            sql_dict['id'] = self.id
2200
            sql_statement = '''UPDATE %s SET %s WHERE id = %%(id)s RETURNING id''' % (
2201
                                       self._table_name,
2202
                                       ', '.join(['%s = %%(%s)s' % (x, x) for x in column_names]))
2203
            cur.execute(sql_statement, sql_dict)
2204
            if cur.fetchone() is None:
2205
                raise AssertionError()
2206

  
2207
        conn.commit()
2208
        cur.close()
2209

  
2210
    @classmethod
2211
    def _row2ob(cls, row):
2212
        o = cls()
2213
        for field, value in zip(cls._table_static_fields, tuple(row)):
2214
            if field[1] == 'varchar':
2215
                setattr(o, field[0], str_encode(value))
2216
            elif field[1] == 'jsonb':
2217
                setattr(o, field[0], value)
2218
        return o
2219

  
2220
    @classmethod
2221
    def get_data_fields(cls):
2222
        return []
2223

  
2224

  
2106 2225
class classproperty(object):
2107 2226
    def __init__(self, f):
2108 2227
        self.f = f
......
2333 2452
    return result
2334 2453

  
2335 2454

  
2336
SQL_LEVEL = 36
2455
SQL_LEVEL = 37
2337 2456

  
2338 2457

  
2339 2458
def migrate_global_views(conn, cur):
......
2464 2583
        # 25: create session_table
2465 2584
        # 32: add last_update_time column to session table
2466 2585
        do_session_table()
2586
    if sql_level < 37:
2587
        # 37: create custom_views tabl
2588
        do_custom_views_table()
2467 2589
    if sql_level < 30:
2468 2590
        # 30: actually remove evo.who on anonymised formdatas
2469 2591
        from wcs.formdef import FormDef
2470
-