Projet

Général

Profil

0001-general-add-support-for-blocks-of-fields-8265.patch

Frédéric Péters, 16 juin 2020 16:54

Télécharger (76,5 ko)

Voir les différences:

Subject: [PATCH] general add support for blocks of fields (#8265)

 tests/conftest.py                        |   5 +
 tests/test_admin_pages.py                | 202 +++++++++++++
 tests/test_form_pages.py                 | 353 +++++++++++++++++++++++
 tests/test_formdata.py                   |  60 ++++
 wcs/admin/blocks.py                      | 253 ++++++++++++++++
 wcs/admin/fields.py                      |   2 +-
 wcs/admin/forms.py                       |   6 +-
 wcs/admin/settings.py                    |  13 +-
 wcs/blocks.py                            | 317 ++++++++++++++++++++
 wcs/fields.py                            | 130 +++++++--
 wcs/formdef.py                           |   5 +-
 wcs/forms/common.py                      |  17 +-
 wcs/forms/root.py                        |   3 +
 wcs/publisher.py                         |  19 +-
 wcs/sql.py                               |  30 ++
 wcs/templates/wcs/backoffice/blocks.html |  23 ++
 wcs/variables.py                         |  89 ++++--
 17 files changed, 1474 insertions(+), 53 deletions(-)
 create mode 100644 wcs/admin/blocks.py
 create mode 100644 wcs/blocks.py
 create mode 100644 wcs/templates/wcs/backoffice/blocks.html
tests/conftest.py
49 49
    return
50 50

  
51 51

  
52
@pytest.fixture
53
def blocks_feature(request, pub):
54
    return site_options(request, pub, 'options', 'fields-blocks', 'true')
55

  
56

  
52 57
@pytest.fixture
53 58
def emails():
54 59
    with EmailsMocking() as mock:
tests/test_admin_pages.py
50 50
from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping
51 51
from wcs.formdef import FormDef
52 52
from wcs.carddef import CardDef
53
from wcs.blocks import BlockDef
53 54
from wcs import fields
54 55

  
55 56
from utilities import get_app, login, create_temporary_pub, clean_temporary_pub, HttpRequestsMocking
......
5894 5895
    resp = resp.form.submit(name='submit')
5895 5896
    pq = resp.pyquery.remove_namespaces()
5896 5897
    assert pq('.error').text() == 'Some destination fields are duplicated'
5898

  
5899

  
5900
def test_block_new(pub, blocks_feature):
5901
    create_superuser(pub)
5902
    create_role()
5903
    app = login(get_app(pub))
5904
    resp = app.get('/backoffice/forms/')
5905
    resp = resp.click('Fields blocks')
5906
    resp = resp.click('New field block')
5907
    resp.form['name'] = 'field block'
5908
    resp = resp.form.submit()
5909
    assert resp.location == 'http://example.net/backoffice/forms/blocks/1/'
5910
    resp = resp.follow()
5911
    assert '<h2>field block' in resp
5912
    assert 'There are not yet any fields' in resp
5913

  
5914
    resp.form['label'] = 'foobar'
5915
    resp.form['type'] = 'string'
5916
    resp = resp.form.submit()
5917
    assert resp.location == 'http://example.net/backoffice/forms/blocks/1/'
5918
    resp = resp.follow()
5919

  
5920
    resp.form['label'] = 'barfoo'
5921
    resp.form['type'] = 'string'
5922
    resp = resp.form.submit()
5923
    assert resp.location == 'http://example.net/backoffice/forms/blocks/1/'
5924
    resp = resp.follow()
5925

  
5926
    assert len(BlockDef.get(1).fields) == 2
5927
    assert str(BlockDef.get(1).fields[0].id) != '1'  # don't use integers
5928

  
5929

  
5930
def test_block_options(pub, blocks_feature):
5931
    create_superuser(pub)
5932
    create_role()
5933
    BlockDef.wipe()
5934
    block = BlockDef()
5935
    block.name = 'foobar'
5936
    block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
5937
    block.store()
5938

  
5939
    app = login(get_app(pub))
5940
    resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
5941
    resp = resp.click(href=re.compile('^settings$'))
5942
    assert 'readonly' not in resp.form['slug'].attrs
5943
    resp.form['name'] = 'foo bar'
5944
    resp = resp.form.submit('submit')
5945
    assert BlockDef.get(block.id).name == 'foo bar'
5946

  
5947
    FormDef.wipe()
5948
    formdef = FormDef()
5949
    formdef.name = 'form title'
5950
    formdef.fields = [
5951
        fields.BlockField(id='0', label='test', type='block:%s' % block.slug),
5952
    ]
5953
    formdef.store()
5954

  
5955
    resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
5956
    resp = resp.click(href=re.compile('^settings$'))
5957
    assert 'readonly' in resp.form['slug'].attrs
5958
    resp = resp.form.submit('cancel')
5959
    resp = resp.follow()
5960

  
5961

  
5962
def test_block_export_import(pub, blocks_feature):
5963
    create_superuser(pub)
5964
    create_role()
5965
    BlockDef.wipe()
5966
    block = BlockDef()
5967
    block.name = 'foobar'
5968
    block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
5969
    block.store()
5970

  
5971
    app = login(get_app(pub))
5972
    resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
5973
    resp = resp.click(href=re.compile('^export$'))
5974
    xml_export = resp.text
5975

  
5976
    resp = app.get('/backoffice/forms/blocks/')
5977
    resp = resp.click(href='import')
5978
    resp = resp.form.submit('cancel')  # shouldn't block on missing file
5979
    resp = resp.follow()
5980

  
5981
    resp = resp.click(href='import')
5982
    resp = resp.form.submit()
5983
    assert 'ere were errors processing your form.' in resp
5984

  
5985
    resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
5986
    resp = resp.form.submit()
5987
    resp = resp.follow()
5988
    assert BlockDef.count() == 2
5989

  
5990
    new_blockdef = [x for x in BlockDef.select() if str(x.id) != str(block.id)][0]
5991
    assert new_blockdef.name == 'Copy of foobar'
5992
    assert new_blockdef.slug == 'foobar-1'
5993
    assert len(new_blockdef.fields) == 1
5994
    assert new_blockdef.fields[0].id == '123'
5995

  
5996
    resp = app.get('/backoffice/forms/blocks/')
5997
    resp = resp.click(href='import')
5998
    resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
5999
    resp = resp.form.submit()
6000
    assert 'Copy of foobar (2)' in [x.name for x in BlockDef.select()]
6001

  
6002
    # import invalid content
6003
    resp = app.get('/backoffice/forms/blocks/')
6004
    resp = resp.click(href='import')
6005
    resp.form['file'] = Upload('block', b'whatever')
6006
    resp = resp.form.submit()
6007
    assert 'Invalid File' in resp
6008

  
6009

  
6010
def test_block_delete(pub, blocks_feature):
6011
    create_superuser(pub)
6012
    create_role()
6013
    BlockDef.wipe()
6014
    FormDef.wipe()
6015
    block = BlockDef()
6016
    block.name = 'foobar'
6017
    block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
6018
    block.store()
6019

  
6020
    app = login(get_app(pub))
6021
    resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
6022
    resp = resp.click(href=re.compile('^delete$'))
6023
    assert 'You are about to irrevocably delete this block.' in resp
6024
    resp = resp.form.submit()
6025
    resp = resp.follow()
6026
    assert BlockDef.count() == 0
6027

  
6028
    # in use
6029
    BlockDef.wipe()
6030
    block = BlockDef()
6031
    block.name = 'foobar'
6032
    block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
6033
    block.store()
6034

  
6035
    FormDef.wipe()
6036
    formdef = FormDef()
6037
    formdef.name = 'form title'
6038
    formdef.fields = [
6039
        fields.BlockField(id='0', label='test', type='block:%s' % block.slug),
6040
    ]
6041
    formdef.store()
6042
    resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
6043
    resp = resp.click(href=re.compile('^delete$'))
6044
    assert 'This block is still used' in resp
6045

  
6046

  
6047
def test_block_edit_duplicate_delete_field(pub, blocks_feature):
6048
    create_superuser(pub)
6049
    create_role()
6050
    BlockDef.wipe()
6051
    block = BlockDef()
6052
    block.name = 'foobar'
6053
    block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
6054
    block.store()
6055

  
6056
    app = login(get_app(pub))
6057
    resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
6058
    resp = resp.click(href=re.compile('123/$'))
6059
    resp.form['required'].checked = False
6060
    resp.form['varname'] = 'test'
6061
    resp = resp.form.submit('submit')
6062
    resp = resp.follow()
6063
    assert BlockDef.get(block.id).fields[0].required is False
6064
    assert BlockDef.get(block.id).fields[0].varname == 'test'
6065

  
6066
    resp = resp.click(href=re.compile('123/duplicate$'))
6067
    resp = resp.follow()
6068
    assert len(BlockDef.get(block.id).fields) == 2
6069

  
6070
    resp = resp.click(href='%s/delete' % BlockDef.get(block.id).fields[1].id)
6071
    resp = resp.form.submit('submit')
6072
    resp = resp.follow()
6073
    assert len(BlockDef.get(block.id).fields) == 1
6074

  
6075

  
6076
def test_block_use_in_formdef(pub, blocks_feature):
6077
    create_superuser(pub)
6078
    create_role()
6079
    FormDef.wipe()
6080
    BlockDef.wipe()
6081
    block = BlockDef()
6082
    block.name = 'foobar'
6083
    block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
6084
    block.store()
6085

  
6086
    formdef = FormDef()
6087
    formdef.name = 'form title'
6088
    formdef.fields = []
6089
    formdef.store()
6090

  
6091
    app = login(get_app(pub))
6092
    resp = app.get('/backoffice/forms/1/fields/')
6093
    resp.forms[0]['label'] = 'a block field'
6094
    resp.forms[0]['type'] = 'block:foobar'
6095
    resp = resp.forms[0].submit().follow()
6096
    assert 'a block field' in resp.text
6097
    resp = resp.click('Edit', href='1/')
6098
    assert resp.form['max_items'].value == '1'
tests/test_form_pages.py
29 29
from wcs.qommon.emails import docutils
30 30
from wcs.qommon.form import UploadedFile
31 31
from wcs.qommon.ident.password_accounts import PasswordAccount
32
from wcs.blocks import BlockDef
32 33
from wcs.carddef import CardDef
33 34
from wcs.formdef import FormDef
34 35
from wcs.workflows import (Workflow, EditableWorkflowStatusItem,
......
8164 8165
        '1_structured': {'id': '1', 'text': 'un', 'more': 'foo'},
8165 8166
    }
8166 8167
    assert '2020-04-18' in formdata.evolution[0].parts[0].content
8168

  
8169

  
8170
def test_block_simple(pub, blocks_feature):
8171
    create_user(pub)
8172
    FormDef.wipe()
8173
    BlockDef.wipe()
8174

  
8175
    block = BlockDef()
8176
    block.name = 'foobar'
8177
    block.fields = [
8178
        fields.StringField(id='123', required=True, label='Test', type='string'),
8179
        fields.StringField(id='234', required=True, label='Test2', type='string'),
8180
    ]
8181
    block.store()
8182

  
8183
    formdef = FormDef()
8184
    formdef.name = 'form title'
8185
    formdef.fields = [
8186
        fields.BlockField(id='1', label='test', type='block:foobar'),
8187
    ]
8188
    formdef.store()
8189

  
8190
    app = get_app(pub)
8191
    resp = app.get(formdef.get_url())
8192
    resp.form['f1$element0$f123'] = 'foo'
8193
    resp.form['f1$element0$f234'] = 'bar'
8194
    resp = resp.form.submit('submit')  # -> validation page
8195
    assert resp.form['f1$element0$f123'].attrs['readonly']
8196
    assert resp.form['f1$element0$f123'].value == 'foo'
8197
    assert resp.form['f1$element0$f234'].attrs['readonly']
8198
    assert resp.form['f1$element0$f234'].value == 'bar'
8199
    resp = resp.form.submit('submit')  # -> end page
8200
    resp = resp.follow()
8201
    assert '>foo<' in resp
8202
    assert '>bar<' in resp
8203

  
8204

  
8205
def test_block_required(pub, blocks_feature):
8206
    create_user(pub)
8207
    FormDef.wipe()
8208
    BlockDef.wipe()
8209

  
8210
    block = BlockDef()
8211
    block.name = 'foobar'
8212
    block.fields = [
8213
        fields.StringField(id='123', required=True, label='Test', type='string'),
8214
        fields.StringField(id='234', required=True, label='Test2', type='string'),
8215
    ]
8216
    block.store()
8217

  
8218
    formdef = FormDef()
8219
    formdef.name = 'form title'
8220
    formdef.fields = [
8221
        fields.BlockField(id='1', label='test', type='block:foobar'),
8222
    ]
8223
    formdef.store()
8224

  
8225
    app = get_app(pub)
8226
    resp = app.get(formdef.get_url())
8227
    resp = resp.form.submit('submit')  # -> error page
8228
    assert 'There were errors processing the form' in resp
8229
    assert resp.text.count('required field') == 1
8230
    resp.form['f1$element0$f123'] = 'foo'
8231
    resp.form['f1$element0$f234'] = 'bar'
8232
    resp = resp.form.submit('submit')  # -> validation page
8233
    assert 'Check values then click submit.' in resp.text
8234

  
8235
    resp = app.get(formdef.get_url())
8236
    resp.form['f1$element0$f123'] = 'foo'
8237
    resp = resp.form.submit('submit')  # -> error page
8238
    assert 'There were errors processing the form' in resp
8239
    assert resp.text.count('required field') == 1
8240
    resp.form['f1$element0$f234'] = 'bar'
8241
    resp = resp.form.submit('submit')  # -> validation page
8242
    assert 'Check values then click submit.' in resp.text
8243

  
8244
    # only one required
8245
    block.fields = [
8246
        fields.StringField(id='123', required=True, label='Test', type='string'),
8247
        fields.StringField(id='234', required=False, label='Test2', type='string'),
8248
    ]
8249
    block.store()
8250

  
8251
    resp = app.get(formdef.get_url())
8252
    resp.form['f1$element0$f123'] = 'foo'
8253
    resp = resp.form.submit('submit')  # -> validation page
8254
    assert 'Check values then click submit.' in resp.text
8255

  
8256
    # none required, but globally required
8257
    block.fields = [
8258
        fields.StringField(id='123', required=False, label='Test', type='string'),
8259
        fields.StringField(id='234', required=False, label='Test2', type='string'),
8260
    ]
8261
    block.store()
8262

  
8263
    resp = app.get(formdef.get_url())
8264
    resp = resp.form.submit('submit')  # -> error page
8265
    assert 'There were errors processing the form' in resp
8266
    assert resp.text.count('required field') == 1
8267
    resp.form['f1$element0$f234'] = 'bar'
8268
    resp = resp.form.submit('submit')  # -> validation page
8269
    assert 'Check values then click submit.' in resp.text
8270

  
8271

  
8272
def test_block_date(pub, blocks_feature):
8273
    create_user(pub)
8274
    FormDef.wipe()
8275
    BlockDef.wipe()
8276

  
8277
    block = BlockDef()
8278
    block.name = 'foobar'
8279
    block.fields = [
8280
        fields.StringField(id='123', required=True, label='Test', type='string'),
8281
        fields.DateField(id='234', required=True, label='Test2', type='date'),
8282
    ]
8283
    block.store()
8284

  
8285
    formdef = FormDef()
8286
    formdef.name = 'form title'
8287
    formdef.fields = [
8288
        fields.BlockField(id='1', label='test', type='block:foobar'),
8289
    ]
8290
    formdef.store()
8291

  
8292
    app = get_app(pub)
8293
    resp = app.get(formdef.get_url())
8294
    resp.form['f1$element0$f123'] = 'foo'
8295
    resp.form['f1$element0$f234'] = '2020-06-16'
8296
    resp = resp.form.submit('submit')  # -> validation page
8297
    assert 'Check values then click submit.' in resp.text
8298
    resp = resp.form.submit('submit')  # -> submit
8299
    resp = resp.follow()
8300
    assert '>2020-06-16<' in resp
8301

  
8302

  
8303
def test_block_multipage(pub, blocks_feature):
8304
    create_user(pub)
8305
    FormDef.wipe()
8306
    BlockDef.wipe()
8307

  
8308
    block = BlockDef()
8309
    block.name = 'foobar'
8310
    block.fields = [
8311
        fields.StringField(id='123', required=True, label='Test', type='string'),
8312
        fields.StringField(id='234', required=True, label='Test2', type='string'),
8313
    ]
8314
    block.store()
8315

  
8316
    formdef = FormDef()
8317
    formdef.name = 'form title'
8318
    formdef.fields = [
8319
        fields.PageField(id='0', label='1st page', type='page'),
8320
        fields.BlockField(id='1', label='test', type='block:foobar'),
8321
        fields.PageField(id='2', label='2nd page', type='page'),
8322
    ]
8323
    formdef.store()
8324

  
8325
    app = get_app(pub)
8326
    resp = app.get(formdef.get_url())
8327
    resp.form['f1$element0$f123'] = 'foo'
8328
    resp.form['f1$element0$f234'] = 'bar'
8329
    resp = resp.form.submit('submit')  # -> 2nd page
8330
    resp = resp.form.submit('submit')  # -> validation page
8331
    assert resp.form['f1$element0$f123'].attrs['readonly']
8332
    assert resp.form['f1$element0$f123'].value == 'foo'
8333
    resp = resp.form.submit('previous')  # -> 2nd page
8334
    resp = resp.form.submit('previous')  # -> 1st page
8335
    assert 'readonly' not in resp.form['f1$element0$f123'].attrs
8336
    assert resp.form['f1$element0$f123'].value == 'foo'
8337
    resp = resp.form.submit('submit')  # -> 2nd page
8338
    resp = resp.form.submit('submit')  # -> validation page
8339
    resp = resp.form.submit('submit')  # -> submit
8340
    resp = resp.follow()
8341
    assert '>foo<' in resp
8342
    assert '>bar<' in resp
8343

  
8344

  
8345
def test_block_repeated(pub, blocks_feature):
8346
    create_user(pub)
8347
    FormDef.wipe()
8348
    BlockDef.wipe()
8349

  
8350
    block = BlockDef()
8351
    block.name = 'foobar'
8352
    block.fields = [
8353
        fields.StringField(id='123', required=True, label='Test', type='string'),
8354
        fields.StringField(id='234', required=True, label='Test2', type='string'),
8355
    ]
8356
    block.store()
8357

  
8358
    formdef = FormDef()
8359
    formdef.name = 'form title'
8360
    formdef.fields = [
8361
        fields.PageField(id='0', label='1st page', type='page'),
8362
        fields.BlockField(id='1', label='test', type='block:foobar', max_items=3),
8363
        fields.PageField(id='2', label='2nd page', type='page'),
8364
    ]
8365
    formdef.store()
8366

  
8367
    app = get_app(pub)
8368
    resp = app.get(formdef.get_url())
8369
    assert resp.text.count('>Test<') == 1
8370
    assert 'Add another' in resp
8371
    resp = resp.form.submit('f1$add_element')
8372
    assert resp.text.count('>Test<') == 2
8373
    resp = resp.form.submit('f1$add_element')
8374
    assert resp.text.count('>Test<') == 3
8375
    assert 'Add another' not in resp
8376

  
8377
    # fill items (1st and 3rd row)
8378
    resp.form['f1$element0$f123'] = 'foo'
8379
    resp.form['f1$element0$f234'] = 'bar'
8380
    resp.form['f1$element2$f123'] = 'foo2'
8381
    resp.form['f1$element2$f234'] = 'bar2'
8382

  
8383
    resp = resp.form.submit('submit')  # -> 2nd page
8384
    resp = resp.form.submit('submit')  # -> validation page
8385
    assert 'Check values then click submit.' in resp.text
8386
    assert resp.form['f1$element0$f123'].value == 'foo'
8387
    assert resp.form['f1$element0$f234'].value == 'bar'
8388
    assert resp.form['f1$element1$f123'].value == 'foo2'
8389
    assert resp.form['f1$element1$f234'].value == 'bar2'
8390

  
8391
    resp = resp.form.submit('previous')  # -> 2nd page
8392
    resp = resp.form.submit('previous')  # -> 1st page
8393
    assert 'readonly' not in resp.form['f1$element0$f123'].attrs
8394
    assert resp.form['f1$element0$f123'].value == 'foo'
8395

  
8396
    resp = resp.form.submit('submit')  # -> 2nd page
8397
    resp = resp.form.submit('submit')  # -> validation page
8398
    resp = resp.form.submit('submit')  # -> submit
8399
    resp = resp.follow()
8400
    assert '>foo<' in resp
8401
    assert '>bar<' in resp
8402
    assert '>foo2<' in resp
8403
    assert '>bar2<' in resp
8404

  
8405

  
8406
def test_block_repeated_files(pub, blocks_feature):
8407
    create_user(pub)
8408
    FormDef.wipe()
8409
    BlockDef.wipe()
8410

  
8411
    block = BlockDef()
8412
    block.name = 'foobar'
8413
    block.fields = [
8414
        fields.StringField(id='123', required=True, label='Test', type='string'),
8415
        fields.FileField(id='234', required=True, label='Test2', type='file'),
8416
    ]
8417
    block.store()
8418

  
8419
    formdef = FormDef()
8420
    formdef.name = 'form title'
8421
    formdef.fields = [
8422
        fields.PageField(id='0', label='1st page', type='page'),
8423
        fields.BlockField(id='1', label='test', type='block:foobar', max_items=3),
8424
        fields.PageField(id='2', label='2nd page', type='page'),
8425
    ]
8426
    formdef.store()
8427

  
8428
    app = get_app(pub)
8429
    resp = app.get(formdef.get_url())
8430
    assert resp.text.count('>Test<') == 1
8431
    assert 'Add another' in resp
8432
    resp = resp.form.submit('f1$add_element')
8433
    assert resp.text.count('>Test<') == 2
8434
    resp = resp.form.submit('f1$add_element')
8435
    assert resp.text.count('>Test<') == 3
8436
    assert 'Add another' not in resp
8437

  
8438
    # fill items (1st and 3rd row)
8439
    resp.form['f1$element0$f123'] = 'foo'
8440
    resp.form['f1$element0$f234$file'] = Upload('test1.txt', b'foobar1', 'text/plain')
8441
    resp.form['f1$element2$f123'] = 'foo2'
8442
    resp.form['f1$element2$f234$file'] = Upload('test2.txt', b'foobar2', 'text/plain')
8443

  
8444
    resp = resp.form.submit('submit')  # -> 2nd page
8445
    resp = resp.form.submit('submit')  # -> validation page
8446
    assert 'Check values then click submit.' in resp.text
8447
    assert resp.form['f1$element0$f123'].value == 'foo'
8448
    assert 'test1.txt' in resp
8449
    assert resp.form['f1$element1$f123'].value == 'foo2'
8450
    assert 'test2.txt' in resp
8451

  
8452
    resp = resp.form.submit('previous')  # -> 2nd page
8453
    resp = resp.form.submit('previous')  # -> 1st page
8454
    resp = resp.form.submit('submit')  # -> 2nd page
8455
    resp = resp.form.submit('submit')  # -> validation page
8456
    resp = resp.form.submit('submit')  # -> submit
8457
    resp = resp.follow()
8458
    assert '>foo<' in resp
8459
    assert 'test1.txt' in resp
8460
    assert '>foo2<' in resp
8461
    assert 'test2.txt' in resp
8462

  
8463

  
8464
def test_block_digest(pub, blocks_feature):
8465
    create_user(pub)
8466
    FormDef.wipe()
8467
    BlockDef.wipe()
8468

  
8469
    block = BlockDef()
8470
    block.name = 'foobar'
8471
    block.fields = [
8472
        fields.StringField(id='123', required=True, label='Test',
8473
            type='string', varname='foo'),
8474
        fields.StringField(id='234', required=True, label='Test2',
8475
            type='string', varname='bar'),
8476
    ]
8477
    block.store()
8478

  
8479
    formdef = FormDef()
8480
    formdef.name = 'form title'
8481
    formdef.fields = [
8482
        fields.BlockField(id='1', label='test', type='block:foobar', max_items=3),
8483
    ]
8484
    formdef.store()
8485

  
8486
    app = get_app(pub)
8487
    resp = app.get(formdef.get_url())
8488
    resp.form['f1$element0$f123'] = 'foo'
8489
    resp.form['f1$element0$f234'] = 'bar'
8490
    resp = resp.form.submit('f1$add_element')
8491
    resp.form['f1$element1$f123'] = 'foo2'
8492
    resp.form['f1$element1$f234'] = 'bar2'
8493

  
8494
    resp = resp.form.submit('submit')  # -> validation page
8495
    resp = resp.form.submit('submit')  # -> submit
8496

  
8497
    assert formdef.data_class().select()[0].data['1']['data'] == [
8498
            {'123': 'foo', '234': 'bar'}, {'123': 'foo2', '234': 'bar2'}]
8499
    # by default it gets the type of object
8500
    assert formdef.data_class().select()[0].data['1_display'] == 'foobar, foobar'
8501

  
8502
    # set a digest template
8503
    formdef.data_class().wipe()
8504

  
8505
    block.digest_template = 'X{{foobar_var_foo}}Y'
8506
    block.store()
8507

  
8508
    resp = app.get(formdef.get_url())
8509
    resp.form['f1$element0$f123'] = 'foo'
8510
    resp.form['f1$element0$f234'] = 'bar'
8511
    resp = resp.form.submit('f1$add_element')
8512
    resp.form['f1$element1$f123'] = 'foo2'
8513
    resp.form['f1$element1$f234'] = 'bar2'
8514

  
8515
    resp = resp.form.submit('submit')  # -> validation page
8516
    resp = resp.form.submit('submit')  # -> submit
8517
    assert formdef.data_class().select()[0].data['1']['data'] == [
8518
            {'123': 'foo', '234': 'bar'}, {'123': 'foo2', '234': 'bar2'}]
8519
    assert formdef.data_class().select()[0].data['1_display'] == 'XfooY, Xfoo2Y'
tests/test_formdata.py
15 15
from wcs.qommon.form import PicklableUpload
16 16
from wcs.qommon.http_request import HTTPRequest
17 17
from wcs import fields, formdef
18
from wcs.blocks import BlockDef
18 19
from wcs.categories import Category
19 20
from wcs.conditions import Condition
20 21
from wcs.formdef import FormDef
......
2038 2039
    assert str(variables['form'].var.foo) == 'world'
2039 2040
    assert str(variables['form'].parent['form'].var.foo) == 'hello'
2040 2041
    assert variables['form'].parent is not None
2042

  
2043

  
2044
def test_block_variables(pub, blocks_feature):
2045
    BlockDef.wipe()
2046
    FormDef.wipe()
2047

  
2048
    block = BlockDef()
2049
    block.name = 'foobar'
2050
    block.digest_template = 'X{{foobar_var_foo}}Y'
2051
    block.fields = [
2052
        fields.StringField(id='123', required=True, label='Test',
2053
            type='string', varname='foo'),
2054
        fields.StringField(id='234', required=True, label='Test2',
2055
            type='string', varname='bar'),
2056
    ]
2057
    block.store()
2058

  
2059
    formdef = FormDef()
2060
    formdef.name = 'testblock'
2061
    formdef.fields = [
2062
        fields.BlockField(id='1', label='test', type='block:foobar',
2063
            max_items=3, varname='block'),
2064
    ]
2065
    formdef.store()
2066

  
2067
    formdata = formdef.data_class()()
2068
    formdata.just_created()
2069
    # value from test_block_digest in tests/test_form_pages.py
2070
    formdata.data = {
2071
        '1': {
2072
            'data': [{'123': 'foo', '234': 'bar'}, {'123': 'foo2', '234': 'bar2'}],
2073
            'schema': {'123': 'string', '234': 'string'}
2074
            },
2075
        '1_display': 'XfooY, Xfoo2Y',
2076
    }
2077
    formdata.store()
2078

  
2079
    variables = formdata.get_substitution_variables()
2080
    assert 'form_var_block' in variables.get_flat_keys()
2081
    assert 'form_var_block_0' in variables.get_flat_keys()
2082
    assert 'form_var_block_0_foo' in variables.get_flat_keys()
2083
    assert 'form_var_block_0_bar' in variables.get_flat_keys()
2084
    assert 'form_var_block_1' in variables.get_flat_keys()
2085
    assert 'form_var_block_1_foo' in variables.get_flat_keys()
2086
    assert 'form_var_block_1_bar' in variables.get_flat_keys()
2087

  
2088
    assert variables.get('form_var_block_0_foo') == 'foo'
2089
    assert variables.get('form_var_block_1_foo') == 'foo2'
2090
    assert variables.get('form_var_block_var_foo') == 'foo'  # alias to 1st element
2091

  
2092
    pub.substitutions.reset()
2093
    pub.substitutions.feed(formdata)
2094

  
2095
    context = pub.substitutions.get_context_variables(mode='lazy')
2096
    tmpl = Template('{{ form_var_block }}')
2097
    assert tmpl.render(context) == 'XfooY, Xfoo2Y'
2098

  
2099
    tmpl = Template('{% for sub in form_var_block %}{{ sub.foo }} {% endfor %}')
2100
    assert tmpl.render(context) == 'foo foo2 '
wcs/admin/blocks.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
import xml.etree.ElementTree as ET
18

  
19
from quixote import get_response, get_session, redirect
20
from quixote.directory import Directory
21
from quixote.html import TemplateIO, htmltext
22

  
23
from wcs.blocks import BlockDef, BlockdefImportError
24

  
25
from wcs.qommon.form import Form, StringWidget, HtmlWidget, FileWidget
26
from wcs.qommon import _, misc, template
27
from wcs.qommon.backoffice.menu import html_top
28

  
29
from wcs.admin.fields import FieldDefPage, FieldsDirectory
30
from wcs.admin import utils
31

  
32

  
33
class BlockFieldDefPage(FieldDefPage):
34
    blacklisted_attributes = ['condition']
35

  
36
    def redirect_field_anchor(self, field):
37
        anchor = '#itemId_%s' % field.id if field else ''
38
        return redirect('../%s' % anchor)
39

  
40

  
41
class BlockDirectory(FieldsDirectory):
42
    _q_exports = ['', 'update_order', 'new', 'delete', 'export', 'settings']
43
    field_def_page_class = BlockFieldDefPage
44
    blacklisted_types = ['page', 'table', 'table-select', 'tablerows', 'ranked-items', 'blocks']
45
    field_var_prefix = ''
46
    support_import = False
47

  
48
    def __init__(self, section, *args, **kwargs):
49
        self.section = section
50
        super().__init__(*args, **kwargs)
51

  
52
    def index_top(self):
53
        r = TemplateIO(html=True)
54
        r += htmltext('<div id="appbar">')
55
        r += htmltext('<h2>%s</h2>') % self.objectdef.name
56
        r += htmltext('<span class="actions">')
57
        r += htmltext('<a href="delete" rel="popup">%s</a>') % _('Delete')
58
        r += htmltext('<a href="export">%s</a>') % _('Export')
59
        r += htmltext('<a href="settings" rel="popup">%s</a>') % _('Settings')
60
        r += htmltext('</span>')
61
        r += htmltext('</div>')
62
        r += utils.last_modification_block(obj=self.objectdef)
63
        r += get_session().display_message()
64

  
65
        if not self.objectdef.fields:
66
            r += htmltext('<div class="infonotice">%s</div>') % _('There are not yet any fields defined.')
67
        return r.getvalue()
68

  
69
    def index_bottom(self):
70
        formdefs = list(self.objectdef.get_usage_formdefs())
71
        formdefs.sort(key=lambda x: x.name.lower())
72
        if not formdefs:
73
            return
74
        r = TemplateIO(html=True)
75
        r += htmltext('<div class="section">')
76
        r += htmltext('<h3>%s</h3>') % _('Usage')
77
        r += htmltext('<ul class="objects-list single-links">')
78
        for formdef in formdefs:
79
            r += htmltext('<li><a href="%s">' % formdef.get_admin_url())
80
            r += htmltext('%s</a></li>') % formdef.name
81
        r += htmltext('</ul>')
82
        r += htmltext('</div>')
83
        return r.getvalue()
84

  
85
    def delete(self):
86
        form = Form(enctype='multipart/form-data')
87
        if not self.objectdef.is_used():
88
            form.widgets.append(
89
                HtmlWidget('<p>%s</p>' % _('You are about to irrevocably delete this block.'))
90
            )
91
            form.add_submit('delete', _('Submit'))
92
        else:
93
            form.widgets.append(
94
                HtmlWidget('<p>%s</p>' % _('This block is still used, it cannot be deleted.'))
95
            )
96
        form.add_submit('cancel', _('Cancel'))
97
        if form.get_widget('cancel').parse():
98
            return redirect('..')
99
        if not form.is_submitted() or form.has_errors():
100
            get_response().breadcrumb.append(('delete', _('Delete')))
101
            html_top(self.section, title=_('Delete Block'))
102
            r = TemplateIO(html=True)
103
            r += htmltext('<h2>%s %s</h2>') % (_('Deleting Block:'), self.objectdef.name)
104
            r += form.render()
105
            return r.getvalue()
106
        else:
107
            self.objectdef.remove_self()
108
            return redirect('..')
109

  
110
    def export(self):
111
        x = self.objectdef.export_to_xml(include_id=True)
112
        misc.indent_xml(x)
113
        response = get_response()
114
        response.set_content_type('application/x-wcs-form')
115
        response.set_header('content-disposition', 'attachment; filename=block-%s.wcs' % self.objectdef.slug)
116
        return '<?xml version="1.0"?>\n' + ET.tostring(x).decode('utf-8')
117

  
118
    def settings(self):
119
        form = Form()
120
        form.add(StringWidget, 'name', title=_('Name'), value=self.objectdef.name, size=50)
121
        form.add(
122
            StringWidget,
123
            'slug',
124
            title=_('Identifier'),
125
            value=self.objectdef.slug,
126
            size=50,
127
            readonly=bool(self.objectdef.is_used()),
128
        )
129
        form.add(
130
            StringWidget, 'digest_template', title=_('Digest'), value=self.objectdef.digest_template, size=50
131
        )
132
        form.add_submit('submit', _('Submit'))
133
        form.add_submit('cancel', _('Cancel'))
134

  
135
        if form.get_widget('cancel').parse():
136
            return redirect('.')
137

  
138
        if form.is_submitted() and not form.has_errors():
139
            self.objectdef.name = form.get_widget('name').parse()
140
            if form.get_widget('slug'):
141
                self.objectdef.slug = form.get_widget('slug').parse()
142
            self.objectdef.digest_template = form.get_widget('digest_template').parse()
143
            self.objectdef.store()
144
            return redirect('.')
145

  
146
        html_top(self.section, title=_('Settings'))
147
        r = TemplateIO(html=True)
148
        r += htmltext('<h2>%s</h2>') % _('Settings')
149
        r += form.render()
150
        return r.getvalue()
151

  
152

  
153
class BlocksDirectory(Directory):
154
    _q_exports = ['', 'new', ('import', 'p_import')]
155
    do_not_call_in_templates = True
156

  
157
    def __init__(self, section):
158
        super().__init__()
159
        self.section = section
160

  
161
    def _q_traverse(self, path):
162
        get_response().breadcrumb.append(('blocks/', _('Fields Blocks')))
163
        return super()._q_traverse(path)
164

  
165
    def _q_lookup(self, component):
166
        return BlockDirectory(self.section, BlockDef.get(component))
167

  
168
    def _q_index(self):
169
        html_top(self.section, title=_('Fields Blocks'))
170
        return template.QommonTemplateResponse(
171
            templates=['wcs/backoffice/blocks.html'],
172
            context={'view': self, 'blocks': BlockDef.select(order_by='name')},
173
        )
174

  
175
    def new(self):
176
        form = Form(enctype='multipart/form-data')
177
        form.add(StringWidget, 'name', title=_('Name'), required=True, size=50)
178
        form.add_submit('submit', _('Add'))
179
        form.add_submit('cancel', _('Cancel'))
180
        if form.get_widget('cancel').parse():
181
            return redirect('.')
182

  
183
        if form.is_submitted() and not form.has_errors():
184
            block = BlockDef(name=form.get_widget('name').parse())
185
            block.store()
186
            return redirect('%s/' % block.id)
187

  
188
        get_response().breadcrumb.append(('new', _('New Fields Block')))
189
        html_top(self.section, title=_('New Fields Block'))
190
        r = TemplateIO(html=True)
191
        r += htmltext('<h2>%s</h2>') % _('New Fields Block')
192
        r += form.render()
193
        return r.getvalue()
194

  
195
    def p_import(self):
196
        form = Form(enctype='multipart/form-data')
197

  
198
        form.add(FileWidget, 'file', title=_('File'), required=True)
199
        form.add_submit('submit', _('Import Fields Block'))
200
        form.add_submit('cancel', _('Cancel'))
201

  
202
        if form.get_submit() == 'cancel':
203
            return redirect('.')
204

  
205
        if form.is_submitted() and not form.has_errors():
206
            try:
207
                return self.import_submit(form)
208
            except ValueError:
209
                pass
210

  
211
        get_response().breadcrumb.append(('import', _('Import')))
212
        html_top(self.section, title=_('Import Fields Block'))
213
        r = TemplateIO(html=True)
214
        r += htmltext('<h2>%s</h2>') % _('Import Fields Block')
215
        r += htmltext('<p>%s</p>') % _('You can install a new fields block by uploading a file.')
216
        r += form.render()
217
        return r.getvalue()
218

  
219
    def import_submit(self, form):
220
        fp = form.get_widget('file').parse().fp
221

  
222
        error, reason = False, None
223
        try:
224
            blockdef = BlockDef.import_from_xml(fp)
225
        except BlockdefImportError as e:
226
            error = True
227
            reason = _(e) % e.msg_args
228
        except ValueError:
229
            error = True
230

  
231
        if error:
232
            if reason:
233
                msg = _('Invalid File (%s)') % reason
234
            else:
235
                msg = _('Invalid File')
236
            form.set_error('file', msg)
237
            raise ValueError()
238

  
239
        initial_blockdef_name = blockdef.name
240
        blockdef_names = [x.name for x in BlockDef.select()]
241
        copy_no = 1
242
        while blockdef.name in blockdef_names:
243
            if copy_no == 1:
244
                blockdef.name = _('Copy of %s') % initial_blockdef_name
245
            else:
246
                blockdef.name = _('Copy of %(name)s (%(no)d)') % {
247
                    'name': initial_blockdef_name,
248
                    'no': copy_no,
249
                }
250
            copy_no += 1
251
        blockdef.store()
252
        get_session().message = ('info', _('This fields block has been successfully imported.'))
253
        return redirect('%s/' % blockdef.id)
wcs/admin/fields.py
174 174
    field_def_page_class = FieldDefPage
175 175
    blacklisted_types = []
176 176
    page_id = None
177
    field_var_prefix = ''
177
    field_var_prefix = '..._'
178 178

  
179 179
    support_import = True
180 180

  
wcs/admin/forms.py
45 45
from wcs.forms.root import qrcode
46 46

  
47 47
from . import utils
48
from .blocks import BlocksDirectory
48 49
from .fields import FieldDefPage, FieldsDirectory
49 50
from .categories import CategoriesDirectory
50 51
from .data_sources import NamedDataSourcesDirectory
......
1467 1468

  
1468 1469
class FormsDirectory(AccessControlled, Directory):
1469 1470
    _q_exports = ['', 'new', ('import', 'p_import'),
1470
            'categories', ('data-sources', 'data_sources')]
1471
            'blocks', 'categories', ('data-sources', 'data_sources')]
1471 1472

  
1472 1473
    categories = CategoriesDirectory()
1474
    blocks = BlocksDirectory(section='forms')
1473 1475
    data_sources = NamedDataSourcesDirectoryInForms()
1474 1476
    formdef_class = FormDef
1475 1477
    formdef_page_class = FormDefPage
......
1507 1509
        if has_roles:
1508 1510
            r += htmltext('<span class="actions">')
1509 1511
            r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
1512
            if get_publisher().has_site_option('fields-blocks'):
1513
                r += htmltext('<a href="blocks/">%s</a>') % _('Fields blocks')
1510 1514
            if get_publisher().get_backoffice_root().is_accessible('categories'):
1511 1515
                r += htmltext('<a href="categories/">%s</a>') % _('Categories')
1512 1516
            r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import')
wcs/admin/settings.py
52 52
from wcs.qommon.admin.logger import LoggerDirectory
53 53
from wcs.qommon import ident
54 54

  
55
from wcs.blocks import BlockDef
55 56
from wcs.formdef import FormDef
56 57
from wcs.carddef import CardDef
57 58
from wcs.workflows import Workflow, WorkflowImportError
......
861 862
        if StudioDirectory.is_visible():
862 863
            form.add(CheckboxWidget, 'carddefs', title=_('Card Models'), value=True)
863 864
        form.add(CheckboxWidget, 'workflows', title = _('Workflows'), value = True)
865
        if get_publisher().has_site_option('fields-blocks'):
866
            form.add(CheckboxWidget, 'blockdefs', title=_('Fields Blocks'), value=True)
864 867
        if not get_cfg('sp', {}).get('idp-manage-roles'):
865 868
            form.add(CheckboxWidget, 'roles', title = _('Roles'), value = True)
866 869
        form.add(CheckboxWidget, 'categories', title = _('Categories'), value = True)
......
918 921
                        misc.indent_xml(node)
919 922
                        z.writestr(os.path.join('workflows_xml', str(workflow.id)),
920 923
                                b'<?xml version="1.0"?>\n' + ET.tostring(node))
924
                if 'blockdefs' in self.dirs:
925
                    for blockdef in BlockDef.select():
926
                        node = blockdef.export_to_xml(include_id=True)
927
                        misc.indent_xml(node)
928
                        z.writestr(os.path.join('blockdefs_xml', str(blockdef.id)),
929
                                b'<?xml version="1.0"?>\n' + ET.tostring(node))
921 930

  
922 931
                if self.settings:
923 932
                    z.write(os.path.join(self.app_dir, 'config.pck'), 'config.pck')
......
934 943

  
935 944
        dirs = []
936 945
        for w in ('formdefs', 'carddefs', 'workflows', 'roles', 'categories',
937
                'datasources', 'wscalls', 'mail-templates'):
946
                'datasources', 'wscalls', 'mail-templates', 'blockdefs'):
938 947
            if form.get_widget(w) and form.get_widget(w).parse():
939 948
                dirs.append(w)
940 949
        if not dirs and not form.get_widget('settings').parse():
......
1017 1026
                    r += htmltext('<li>%d %s</li>') % (results['formdefs'], _('forms'))
1018 1027
                if results['carddefs']:
1019 1028
                    r += htmltext('<li>%d %s</li>') % (results['carddefs'], _('cards'))
1029
                if results['blockdefs']:
1030
                    r += htmltext('<li>%d %s</li>') % (results['blockdefs'], _('fields blocks'))
1020 1031
                if results['workflows']:
1021 1032
                    r += htmltext('<li>%d %s</li>') % (results['workflows'], _('workflows'))
1022 1033
                if results['roles']:
wcs/blocks.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
import uuid
18
import time
19
import xml.etree.ElementTree as ET
20

  
21
from quixote import get_request
22

  
23
from .qommon import _, N_, misc
24
from .qommon.form import CompositeWidget, WidgetList
25
from .qommon.storage import StorableObject
26
from .qommon.template import Template
27

  
28
from . import data_sources
29
from . import fields
30

  
31

  
32
class BlockdefImportError(Exception):
33
    def __init__(self, msg, details=None):
34
        self.msg = msg
35
        self.details = details
36

  
37

  
38
class BlockDef(StorableObject):
39
    _names = 'blockdefs'
40
    _indexes = ['slug']
41
    xml_root_node = 'block'
42

  
43
    name = None
44
    slug = None
45
    fields = None
46
    digest_template = None
47

  
48
    last_modification_time = None
49
    last_modification_user_id = None
50

  
51
    # declarations for serialization
52
    TEXT_ATTRIBUTES = ['name', 'slug', 'digest_template']
53

  
54
    def __init__(self, name=None, **kwargs):
55
        super().__init__(**kwargs)
56
        self.name = name
57
        self.fields = []
58

  
59
    def store(self):
60
        if self.slug is None:
61
            # set slug if it's not yet there
62
            self.slug = self.get_new_slug()
63

  
64
        self.last_modification_time = time.localtime()
65
        if get_request() and get_request().user:
66
            self.last_modification_user_id = str(get_request().user.id)
67
        else:
68
            self.last_modification_user_id = None
69

  
70
        super().store()
71

  
72
    def get_new_slug(self):
73
        new_slug = misc.simplify(self.name, space='_')
74
        base_new_slug = new_slug
75
        suffix_no = 0
76
        while True:
77
            try:
78
                obj = self.get_on_index(new_slug, 'slug', ignore_migration=True)
79
            except KeyError:
80
                break
81
            if obj.id == self.id:
82
                break
83
            suffix_no += 1
84
            new_slug = '%s-%s' % (base_new_slug, suffix_no)
85
        return new_slug
86

  
87
    def get_new_field_id(self):
88
        return 'bf%s' % str(uuid.uuid4())
89

  
90
    def get_display_value(self, value):
91
        if not self.digest_template:
92
            return self.name
93

  
94
        from .qommon.substitution import CompatibilityNamesDict
95
        from .variables import LazyBlockDataVar
96

  
97
        context = CompatibilityNamesDict({self.slug + '_var': LazyBlockDataVar(self.fields, value)})
98
        return Template(self.digest_template, autoescape=False).render(context)
99

  
100
    def export_to_xml(self, include_id=False):
101
        root = ET.Element(self.xml_root_node)
102
        if include_id and self.id:
103
            root.attrib['id'] = str(self.id)
104
        for text_attribute in list(self.TEXT_ATTRIBUTES):
105
            if not hasattr(self, text_attribute) or not getattr(self, text_attribute):
106
                continue
107
            ET.SubElement(root, text_attribute).text = getattr(self, text_attribute)
108

  
109
        if self.last_modification_time:
110
            elem = ET.SubElement(root, 'last_modification')
111
            elem.text = time.strftime('%Y-%m-%d %H:%M:%S', self.last_modification_time)
112
            if include_id:
113
                elem.attrib['user_id'] = str(self.last_modification_user_id)
114

  
115
        fields = ET.SubElement(root, 'fields')
116
        for field in self.fields or []:
117
            fields.append(field.export_to_xml(charset='utf-8', include_id=True))
118

  
119
        return root
120

  
121
    @classmethod
122
    def import_from_xml(cls, fd, include_id=False, check_datasources=True):
123
        try:
124
            tree = ET.parse(fd)
125
        except:
126
            raise ValueError()
127
        blockdef = cls.import_from_xml_tree(tree, include_id=include_id)
128

  
129
        if blockdef.slug:
130
            try:
131
                cls.get_on_index(blockdef.slug, 'slug', ignore_migration=True)
132
            except KeyError:
133
                pass
134
            else:
135
                blockdef.slug = blockdef.get_new_slug()
136

  
137
        if check_datasources:
138
            # check if datasources are defined
139
            unknown_datasources = set()
140
            for field in blockdef.fields:
141
                data_source = getattr(field, 'data_source', None)
142
                if data_source:
143
                    if isinstance(data_sources.get_object(data_source), data_sources.StubNamedDataSource):
144
                        unknown_datasources.add(data_source.get('type'))
145
            if unknown_datasources:
146
                raise BlockdefImportError(
147
                    N_('Unknown datasources'), details=', '.join(sorted(unknown_datasources))
148
                )
149

  
150
        return blockdef
151

  
152
    @classmethod
153
    def import_from_xml_tree(cls, tree, include_id=False):
154
        charset = 'utf-8'
155
        blockdef = cls()
156
        if tree.find('name') is None or not tree.find('name').text:
157
            raise BlockdefImportError(N_('Missing name'))
158

  
159
        # if the tree we get is actually a ElementTree for real, we get its
160
        # root element and go on happily.
161
        if not ET.iselement(tree):
162
            tree = tree.getroot()
163

  
164
        if tree.tag != cls.xml_root_node:
165
            raise BlockdefImportError(N_('Unexpected root node'))
166

  
167
        if include_id and tree.attrib.get('id'):
168
            blockdef.id = tree.attrib.get('id')
169
        for text_attribute in list(cls.TEXT_ATTRIBUTES):
170
            value = tree.find(text_attribute)
171
            if value is None or value.text is None:
172
                continue
173
            setattr(blockdef, text_attribute, misc.xml_node_text(value))
174

  
175
        blockdef.fields = []
176
        for i, field in enumerate(tree.find('fields')):
177
            try:
178
                field_o = fields.get_field_class_by_type(field.findtext('type'))()
179
            except KeyError:
180
                raise BlockdefImportError(N_('Unknown field type'), details=field.findtext('type'))
181
            field_o.init_with_xml(field, charset, include_id=True)
182
            blockdef.fields.append(field_o)
183

  
184
        if tree.find('last_modification') is not None:
185
            node = tree.find('last_modification')
186
            blockdef.last_modification_time = time.strptime(node.text, '%Y-%m-%d %H:%M:%S')
187
            if include_id and node.attrib.get('user_id'):
188
                blockdef.last_modification_user_id = node.attrib.get('user_id')
189

  
190
        return blockdef
191

  
192
    def get_usage_formdefs(self):
193
        from wcs.formdef import get_formdefs_of_all_kinds
194

  
195
        block_identifier = 'block:%s' % self.slug
196
        for formdef in get_formdefs_of_all_kinds():
197
            for field in formdef.fields:
198
                if field.type == block_identifier:
199
                    yield formdef
200
                    break
201

  
202
    def is_used(self):
203
        return any(self.get_usage_formdefs())
204

  
205

  
206
class BlockSubWidget(CompositeWidget):
207
    def __init__(self, name, value=None, *args, **kwargs):
208
        self.block = kwargs.pop('block')
209
        self.readonly = kwargs.get('readonly')
210
        super().__init__(name, value, *args, **kwargs)
211
        for field in self.block.fields:
212
            if 'readonly' in kwargs:
213
                field.add_to_view_form(form=self)
214
            else:
215
                field.add_to_form(form=self)
216
        if value:
217
            self.set_value(value)
218

  
219
    def set_value(self, value):
220
        for widget in self.get_widgets():
221
            widget.set_value(value.get(widget.field.id))
222

  
223
    def get_field_data(self, field, widget):
224
        from wcs.formdef import FormDef
225

  
226
        return FormDef.get_field_data(field, widget)
227

  
228
    def _parse(self, request):
229
        value = {}
230
        empty = True
231
        for widget in self.get_widgets():
232
            widget_value = self.get_field_data(widget.field, widget)
233
            value.update(widget_value)
234
            if widget_value.get(widget.field.id) is not None:
235
                empty = False
236
        if empty:
237
            value = None
238
        self.value = value
239

  
240
    def add_media(self):
241
        for widget in self.get_widgets():
242
            widget.add_media()
243

  
244

  
245
class BlockWidget(WidgetList):
246
    def __init__(
247
        self, name, value=None, title=None, block=None, max_items=None, add_element_label=None, **kwargs
248
    ):
249
        self.block = block
250
        self.readonly = kwargs.get('readonly')
251
        element_values = None
252
        if value:
253
            element_values = value.get('data')
254
        element_kwargs = {'block': self.block, 'render_br': False}
255
        element_kwargs.update(kwargs)
256
        super().__init__(
257
            name,
258
            value=element_values,
259
            title=title,
260
            max_items=max_items,
261
            element_type=BlockSubWidget,
262
            element_kwargs=element_kwargs,
263
            add_element_label=add_element_label or _('Add another'),
264
            **kwargs,
265
        )
266

  
267
    def set_value(self, value):
268
        super().set_value(value['data'] if value else None)
269
        self.value = value
270

  
271
    def _parse(self, request):
272
        # iterate over existing form keys to get actual list of elements.
273
        # (maybe this could be moved to WidgetList)
274
        prefix = '%s$element' % self.name
275
        known_prefixes = {x.split('$', 2)[1] for x in request.form.keys() if x.startswith(prefix)}
276
        for i in range(len(known_prefixes) - len(self.element_names)):
277
            self.add_element()
278
        super()._parse(request)
279
        if self.value:
280
            self.value = {'data': self.value}
281
            # keep "schema" next to data, this allows custom behaviour for
282
            # date fields (time.struct_time) when writing/reading from
283
            # database in JSON.
284
            self.value['schema'] = {x.id: x.key for x in self.block.fields}
285

  
286
    def parse(self, request=None):
287
        if not self._parsed:
288
            self._parsed = True
289
            if request is None:
290
                request = get_request()
291
            self._parse(request)
292
            if self.required and self.value is None:
293
                self.set_error(_(self.REQUIRED_ERROR))
294
        return self.value
295

  
296
    def add_media(self):
297
        for widget in self.get_widgets():
298
            if hasattr(widget, 'add_media'):
299
                widget.add_media()
300

  
301
    def get_error(self, request=None):
302
        request = request or get_request()
303
        if request.get_method() == 'POST':
304
            self.parse(request=request)
305
        return self.error
306

  
307
    def has_error(self, request=None):
308
        if self.get_error():
309
            return True
310
        # we know subwidgets have been parsed
311
        has_error = False
312
        for widget in self.widgets:
313
            if widget.value is None:
314
                continue
315
            if widget.has_error():
316
                has_error = True
317
        return has_error
wcs/fields.py
46 46
from . import data_sources
47 47
from . import portfolio
48 48
from .conditions import Condition
49
from .blocks import BlockDef, BlockWidget
49 50

  
50 51

  
51 52
class PrefillSelectionWidget(CompositeWidget):
......
499 500
    widget_class = None
500 501

  
501 502
    def add_to_form(self, form, value = None):
502
        kwargs = {'required': self.required}
503
        kwargs = {'required': self.required, 'render_br': False}
503 504
        if value:
504 505
            kwargs['value'] = value
505 506
        for k in self.extra_attributes:
......
545 546
                value = value, readonly = 'readonly', **kwargs)
546 547
        widget = form.get_widget(self.field_key)
547 548
        widget.transfer_form_value(get_request())
549
        widget.field = self
548 550
        if self.extra_css_class:
549 551
            if hasattr(widget, 'extra_css_class') and widget.extra_css_class:
550 552
                widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class)
......
601 603
    def get_csv_heading(self):
602 604
        return [self.label]
603 605

  
604
    def get_view_value(self, value):
606
    def get_view_value(self, value, **kwargs):
605 607
        return str(value) if value else ''
606 608

  
607
    def get_view_short_value(self, value, max_len = 30):
609
    def get_view_short_value(self, value, max_len=30, **kwargs):
608 610
        return self.get_view_value(value)
609 611

  
610 612
    def get_csv_value(self, element, **kwargs):
......
792 794
    def get_admin_attributes(self):
793 795
        return WidgetField.get_admin_attributes(self) + ['size', 'validation', 'data_source', 'anonymise']
794 796

  
795
    def get_view_value(self, value):
797
    def get_view_value(self, value, **kwargs):
796 798
        value = value or ''
797 799
        if value.startswith('http://') or value.startswith('https://'):
798 800
            charset = get_publisher().site_charset
......
869 871
    def convert_value_from_str(self, value):
870 872
        return value
871 873

  
872
    def get_view_value(self, value):
874
    def get_view_value(self, value, **kwargs):
873 875
        if self.pre:
874 876
            return htmltext('<pre>') + value + htmltext('</pre>')
875 877
        else:
......
904 906
    def convert_value_from_str(self, value):
905 907
        return value
906 908

  
907
    def get_view_value(self, value):
909
    def get_view_value(self, value, **kwargs):
908 910
        return htmltext('<a href="mailto:%s">%s</a>') % (value, value)
909 911

  
910 912
    def get_rst_view_value(self, value, indent=''):
......
937 939
                get_request().form[self.field_key] = 'yes'
938 940
            self.field_key = 'f%sdisabled' % self.id
939 941

  
940
    def get_view_value(self, value):
942
    def get_view_value(self, value, **kwargs):
941 943
        if value is True or value == 'True':
942 944
            return _('Yes')
943 945
        elif value is False or value == 'False':
......
1113 1115
                return upload
1114 1116
        raise ValueError('invalid data for file type (%r)' % value)
1115 1117

  
1116
    def get_view_short_value(self, value, max_len=30):
1117
        return self.get_view_value(value, include_image_thumbnail=False)
1118
    def get_view_short_value(self, value, max_len=30, **kwargs):
1119
        return self.get_view_value(value, include_image_thumbnail=False, **kwargs)
1118 1120

  
1119
    def get_view_value(self, value, include_image_thumbnail=True):
1121
    def get_download_query_string(self, **kwargs):
1122
        if kwargs.get('parent_field'):
1123
            return 'f=%s$%s$%s' % (
1124
                    kwargs['parent_field'].id,
1125
                    kwargs['parent_field_index'],
1126
                    self.id)
1127
        return 'f=%s' % self.id
1128

  
1129
    def get_view_value(self, value, include_image_thumbnail=True, **kwargs):
1120 1130
        show_link = True
1121 1131
        if value.has_redirect_url():
1122 1132
            is_in_backoffice = bool(get_request() and get_request().is_in_backoffice())
1123 1133
            show_link = bool(value.get_redirect_url(backoffice=is_in_backoffice))
1124 1134
        t = TemplateIO(html=True)
1125 1135
        t += htmltext('<div class="file-field">')
1136
        if show_link or include_image_thumbnail:
1137
            download_qs = self.get_download_query_string(**kwargs)
1126 1138
        if show_link:
1127
            t += htmltext('<a href="[download]?f=%s">') % self.id
1139
            t += htmltext('<a href="[download]?%s">') % download_qs
1128 1140
        if include_image_thumbnail and value.can_thumbnail():
1129
            t += htmltext('<img alt="" src="[download]?f=%s&thumbnail=1"/>') % self.id
1141
            t += htmltext('<img alt="" src="[download]?%s&thumbnail=1"/>') % download_qs
1130 1142
        t += htmltext('<span>%s</span>') % value
1131 1143
        if show_link:
1132 1144
            t += htmltext('</a>')
1133 1145
        t += htmltext('</div>')
1134 1146
        return t.getvalue()
1135 1147

  
1136
    def get_download_url(self, formdata):
1137
        return '%sdownload?f=%s' % (formdata.get_url(), self.id)
1148
    def get_download_url(self, formdata, **kwargs):
1149
        return '%sdownload?%s' % (formdata.get_url(), self.get_download_query_string(**kwargs))
1138 1150

  
1139 1151
    def get_opendocument_node_value(self, value, formdata=None, **kwargs):
1140 1152
        show_link = True
......
1143 1155
            show_link = bool(value.get_redirect_url(backoffice=is_in_backoffice))
1144 1156
        if show_link and formdata:
1145 1157
            node = ET.Element('{%s}a' % OD_NS['text'])
1146
            node.attrib['{%s}href' % OD_NS['xlink']] = self.get_download_url(formdata)
1158
            node.attrib['{%s}href' % OD_NS['xlink']] = self.get_download_url(formdata, **kwargs)
1147 1159
        else:
1148 1160
            node = ET.Element('{%s}span' % OD_NS['text'])
1149 1161
        node.text = od_clean_text(force_text(value))
......
1155 1167
    def get_json_value(self, value, formdata=None, include_file_content=True, **kwargs):
1156 1168
        out = value.get_json_value(include_file_content=include_file_content)
1157 1169
        if formdata:
1158
            out['url'] = self.get_download_url(formdata)
1170
            out['url'] = self.get_download_url(formdata, **kwargs)
1159 1171
        out['field_id'] = self.id
1160 1172
        return out
1161 1173

  
......
1323 1335
        value = strftime(misc.date_format(), value)
1324 1336
        return super().add_to_view_form(form, value=value)
1325 1337

  
1326
    def get_view_value(self, value):
1338
    def get_view_value(self, value, **kwargs):
1327 1339
        try:
1328 1340
            return strftime(misc.date_format(), value)
1329 1341
        except TypeError:
......
1500 1512

  
1501 1513
        return data_source.get_display_value(value)
1502 1514

  
1503
    def get_view_value(self, value, value_id=None):
1515
    def get_view_value(self, value, value_id=None, **kwargs):
1504 1516
        value = super(ItemField, self).get_view_value(value)
1505 1517
        if not (value_id and
1506 1518
                get_request() and
......
2050 2062
            pass
2051 2063
        return t
2052 2064

  
2053
    def get_view_value(self, value):
2065
    def get_view_value(self, value, **kwargs):
2054 2066
        r = TemplateIO(html=True)
2055 2067
        r += htmltext('<table><thead><tr><td></td>')
2056 2068
        for column in self.columns:
......
2238 2250
            pass
2239 2251
        return t
2240 2252

  
2241
    def get_view_value(self, value):
2253
    def get_view_value(self, value, **kwargs):
2242 2254
        r = TemplateIO(html=True)
2243 2255
        r += htmltext('<table><thead><tr>')
2244 2256
        for column in self.columns:
......
2401 2413
            return (None, False)
2402 2414
        return ('%(lat)s;%(lon)s' % coords, False)
2403 2415

  
2404
    def get_view_value(self, value):
2416
    def get_view_value(self, value, **kwargs):
2405 2417
        widget = self.widget_class('x%s' % random.random(), value, readonly=True)
2406 2418
        return widget.render_widget_content()
2407 2419

  
......
2471 2483
            attrs.remove('prefill')
2472 2484
        return attrs
2473 2485

  
2474
    def get_view_value(self, value):
2486
    def get_view_value(self, value, **kwargs):
2475 2487
        r = TemplateIO(html=True)
2476 2488
        r += htmltext('<ul>')
2477 2489
        items = list(value.items())
......
2579 2591
                title=_('Label for confirmation input'),
2580 2592
                value=self.confirmation_title)
2581 2593

  
2582
    def get_view_value(self, value):
2594
    def get_view_value(self, value, **kwargs):
2583 2595
        return '●'*8
2584 2596

  
2585 2597
    def get_csv_value(self, value, **kwargs):
......
2591 2603
register_field_class(PasswordField)
2592 2604

  
2593 2605

  
2606
class BlockField(WidgetField):
2607
    key = 'block'
2608
    widget_class = BlockWidget
2609
    max_items = 1
2610
    extra_attributes = ['block', 'max_items', 'add_element_label']
2611
    add_element_label = ''
2612

  
2613
    # cache
2614
    _block = None
2615

  
2616
    @property
2617
    def block(self):
2618
        if self._block:
2619
            return self._block
2620
        self._block = BlockDef.get_on_index(self.type[6:], 'slug')
2621
        return self._block
2622

  
2623
    def get_type_label(self):
2624
        return _('Field Block (%s)') % self.block.name
2625

  
2626
    def fill_admin_form(self, form):
2627
        super().fill_admin_form(form)
2628
        form.add(IntWidget, 'max_items', title=_('Maximum number of items'),
2629
                value=self.max_items)
2630
        form.add(StringWidget, 'add_element_label', title=_('Label of "Add" button'),
2631
                value=self.add_element_label)
2632

  
2633
    def get_admin_attributes(self):
2634
        return super().get_admin_attributes() + ['max_items', 'add_element_label']
2635

  
2636
    def store_display_value(self, data, field_id):
2637
        value = data.get(field_id)
2638
        parts = []
2639
        if value and value.get('data'):
2640
            for subvalue in value.get('data'):
2641
                parts.append(self.block.get_display_value(subvalue))
2642
        return ', '.join(parts)
2643

  
2644
    def get_view_value(self, value, **kwargs):
2645
        if 'value_id' not in kwargs:
2646
            # when called from get_rst_view_value()
2647
            return str(value or '')
2648
        value = kwargs['value_id']
2649
        r = TemplateIO(html=True)
2650
        for i, row_value in enumerate(value['data']):
2651
            for field in self.block.fields:
2652
                css_classes = ['field', 'field-type-%s' % field.key]
2653
                if field.extra_css_class:
2654
                    css_classes.append(field.extra_css_class)
2655
                r += htmltext('<div class="%s">' % ' '.join(css_classes))
2656
                r += htmltext('<span class="label">%s</span> ') % field.label
2657
                sub_value = row_value.get(field.id)
2658
                if sub_value is None:
2659
                    r += htmltext('<div class="value"><i>%s</i></div>') % _('Not set')
2660
                else:
2661
                    r += htmltext('<div class="value">')
2662
                    r += field.get_view_value(sub_value, parent_field=self, parent_field_index=i)
2663
                    r += htmltext('</div>')
2664
                r += htmltext('</div>\n')
2665
        return r.getvalue()
2666

  
2667

  
2594 2668
def get_field_class_by_type(type):
2595 2669
    for k in field_classes:
2596 2670
        if k.key == type:
2597 2671
            return k
2672
    if type.startswith('block:'):
2673
        return BlockField
2598 2674
    raise KeyError()
2599 2675

  
2600 2676

  
......
2612 2688
        else:
2613 2689
            non_widgets.append((klass.key, _(klass.description), klass.key))
2614 2690
    options = widgets + [('', '—', '')] + non_widgets
2691
    if get_publisher().has_site_option('fields-blocks') and (
2692
            not blacklisted_types or 'blocks' not in blacklisted_types):
2693
        position = len(options)
2694
        for blockdef in BlockDef.select(order_by='name'):
2695
            options.append(('block:%s' % blockdef.slug, blockdef.name, 'block:%s' % blockdef.slug))
2696
        if len(options) != position:
2697
            # add separator
2698
            options.insert(position, ('', '—', ''))
2615 2699
    return options
wcs/formdef.py
708 708
                    widget.live_condition_source = True
709 709
                    widget.live_condition_fields = live_condition_fields[field.varname]
710 710

  
711
    def get_field_data(self, field, widget):
711
    @classmethod
712
    def get_field_data(cls, field, widget):
712 713
        d = {}
713 714
        d[field.id] = widget.parse()
714 715
        if d.get(field.id) is not None and field.convert_value_from_str:
......
1654 1655

  
1655 1656

  
1656 1657
def get_formdefs_of_all_kinds():
1658
    from wcs.blocks import BlockDef
1657 1659
    from wcs.carddef import CardDef
1658 1660
    from wcs.wf.form import FormWorkflowStatusItem
1659 1661
    from wcs.admin.settings import UserFieldsFormDef
......
1665 1667
    }
1666 1668
    formdefs = [UserFieldsFormDef()]
1667 1669
    formdefs += FormDef.select(**kwargs)
1670
    formdefs += BlockDef.select(**kwargs)
1668 1671
    formdefs += CardDef.select(**kwargs)
1669 1672
    for workflow in Workflow.select(**kwargs):
1670 1673
        for status in workflow.possible_status:
wcs/forms/common.py
49 49
        self.reference = reference
50 50

  
51 51
    def lookup_file_field(self, filename):
52
        if self.reference in self.formdata.data:
53
            return self.formdata.data[self.reference]
52
        try:
53
            if '$' in self.reference:
54
                fn2, idx, sub = self.reference.split('$', 2)
55
                return self.formdata.data[fn2]['data'][int(idx)][sub]
56
            else:
57
                return self.formdata.data[self.reference]
58
        except (KeyError, ValueError):
59
            return None
54 60

  
55 61
    def _q_lookup(self, component):
56 62
        if component == 'thumbnail':
......
632 638
        self.check_receiver()
633 639
        try:
634 640
            fn = get_request().form['f']
635
            f = self.filled.data[fn]
641
            if '$' in fn:
642
                fn2, idx, sub = fn.split('$', 2)
643
                file = self.filled.data[fn2]['data'][int(idx)][sub]
644
            else:
645
                file = self.filled.data[fn]
636 646
        except (KeyError, ValueError):
637 647
            raise errors.TraversalError()
638 648

  
639
        file = self.filled.data[fn]
640 649
        if not hasattr(file, 'content_type'):
641 650
            raise errors.TraversalError()
642 651

  
wcs/forms/root.py
1285 1285
    def validating(self, data):
1286 1286
        get_request().view_name = 'validation'
1287 1287
        self.html_top(self.formdef.name)
1288
        # fake a GET request to avoid previous page POST data being carried
1289
        # over in rendering.
1290
        get_request().environ['REQUEST_METHOD'] = 'GET'
1288 1291
        form = self.create_view_form(data)
1289 1292
        token_widget = form.get_widget(form.TOKEN_NAME)
1290 1293
        token_widget._parsed = True
wcs/publisher.py
163 163
    def import_zip(self, fd):
164 164
        z = zipfile.ZipFile(fd)
165 165
        results = {'formdefs': 0, 'carddefs': 0, 'workflows': 0, 'categories': 0, 'roles': 0,
166
                'settings': 0, 'datasources': 0, 'wscalls': 0, 'mail-templates': 0}
166
                'settings': 0, 'datasources': 0, 'wscalls': 0, 'mail-templates': 0,
167
                'blockdefs': 0}
167 168

  
168 169
        def _decode_list(data):
169 170
            rv = []
......
193 194
        for f in z.namelist():
194 195
            if '.indexes' in f:
195 196
                continue
196
            if os.path.dirname(f) in ('formdefs_xml', 'carddefs_xml', 'workflows_xml'):
197
            if os.path.dirname(f) in ('formdefs_xml', 'carddefs_xml', 'workflows_xml', 'blockdefs_xml'):
197 198
                continue
198 199
            path = os.path.join(self.app_dir, f)
199 200
            if not os.path.exists(os.path.dirname(path)):
......
223 224
            if os.path.split(f)[0] in results:
224 225
                results[os.path.split(f)[0]] += 1
225 226

  
226
        # second pass, workflows
227
        # second pass, fields blocks
228
        from wcs.blocks import BlockDef
229
        for f in z.namelist():
230
            if os.path.dirname(f) == 'blockdefs_xml' and os.path.basename(f):
231
                blockdef = BlockDef.import_from_xml(z.open(f), include_id=True)
232
                blockdef.store()
233
                results['blockdefs'] += 1
234

  
235
        # third pass, workflows
227 236
        from wcs.workflows import Workflow
228 237
        for f in z.namelist():
229 238
            if os.path.dirname(f) == 'workflows_xml' and os.path.basename(f):
......
231 240
                workflow.store()
232 241
                results['workflows'] += 1
233 242

  
234
        # third pass, forms and cards
243
        # fourth pass, forms and cards
235 244
        from wcs.formdef import FormDef
236 245
        from wcs.carddef import CardDef
237 246
        formdefs = []
......
263 272
            elif k == 'carddefs':
264 273
                from .carddef import CardDef
265 274
                klass = CardDef
275
            elif k == 'blockdefs':
276
                klass = BlockDef
266 277
            elif k == 'categories':
267 278
                from .categories import Category
268 279
                klass = Category
wcs/sql.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 psycopg2
18 19
import psycopg2.extensions
19 20
import psycopg2.extras
......
35 36
from .qommon.storage import _take, deep_bytes2str, parse_clause as parse_storage_clause
36 37
from .qommon.substitution import invalidate_substitution_cache
37 38
from .qommon import get_cfg
39
from .qommon.upload_storage import PicklableUpload
40
from .qommon.misc import strftime
38 41
from .publisher import UnpicklerClass
39 42

  
40 43
import wcs.categories
......
69 72
    # mapping of dicts
70 73
    'ranked-items': 'text[][]',
71 74
    'password': 'text[][]',
75
    # field block
76
    'block': 'jsonb',
72 77
}
73 78

  
74 79

  
......
1256 1261
                    value = datetime.datetime(value.tm_year, value.tm_mon, value.tm_mday)
1257 1262
                elif sql_type == 'bytea':
1258 1263
                    value = bytearray(pickle.dumps(value, protocol=2))
1264
                elif sql_type == 'jsonb' and value.get('schema'):
1265
                    # block field, adapt date/field values
1266
                    value = copy.deepcopy(value)
1267
                    for field_id, field_type in value.get('schema').items():
1268
                        if field_type not in ('date', 'file'):
1269
                            continue
1270
                        for entry in value.get('data') or []:
1271
                            subvalue = entry.get(field_id)
1272
                            if subvalue and field_type == 'date':
1273
                                entry[field_id] = strftime('%Y-%m-%d', subvalue)
1274
                            elif subvalue and field_type == 'file':
1275
                                entry[field_id] = subvalue.__getstate__()
1259 1276
                elif sql_type == 'boolean':
1260 1277
                    pass
1261 1278
            sql_dict[get_field_id(field)] = value
......
1296 1313
                    value = value.timetuple()
1297 1314
                elif sql_type == 'bytea':
1298 1315
                    value = pickle_loads(value)
1316
                elif sql_type == 'jsonb' and value.get('schema'):
1317
                    # block field, adapt date/field values
1318
                    for field_id, field_type in value.get('schema').items():
1319
                        if field_type not in ('date', 'file'):
1320
                            continue
1321
                        for entry in value.get('data') or []:
1322
                            subvalue = entry.get(field_id)
1323
                            if subvalue and field_type == 'date':
1324
                                entry[field_id] = time.strptime(subvalue, '%Y-%m-%d')
1325
                            elif subvalue and field_type == 'file':
1326
                                entry[field_id] = PicklableUpload.__new__(PicklableUpload)
1327
                                entry[field_id].__setstate__(subvalue)
1328

  
1299 1329
            obdata[field.id] = value
1300 1330
            i += 1
1301 1331
            if field.store_display_value:
wcs/templates/wcs/backoffice/blocks.html
1
{% extends "wcs/backoffice/base.html" %}
2
{% load i18n %}
3

  
4
{% block appbar-title %}{% trans "Field Blocks" %}{% endblock %}
5

  
6
{% block appbar-actions %}
7
<a rel="popup" href="import">{% trans "Import" %}</a>
8
<a rel="popup" href="new">{% trans "New field block" %}</a>
9
{% endblock %}
10

  
11
{% block content %}
12
{% if blocks %}
13
<ul class="objects-list single-links">
14
  {% for block in blocks %}
15
  <li><a href="{{ block.id }}/">{{ block.name }}</a></li>
16
  {% endfor %}
17
</ul>
18
{% else %}
19
<div class="infonotice">
20
{% trans "There are no field blocks defined." %}
21
</div>
22
{% endif %}
23
{% endblock %}
wcs/variables.py
461 461
            self._varnames[field.varname] = field
462 462
        return self._varnames
463 463

  
464
    def get_field_kwargs(self, field):
465
        return {
466
            'data': self._data,
467
            'field': field,
468
            'formdata': self._formdata
469
        }
470

  
464 471
    def __getitem__(self, key):
465 472
        try:
466 473
            field = self.varnames[key]
......
489 496
        if str(field.id) not in self._data:
490 497
            raise KeyError(key)
491 498

  
492
        if field.key == 'date':
493
            return LazyFieldVarDate(self._data, field, self._formdata)
494
        if field.key == 'map':
495
            return LazyFieldVarMap(self._data, field, self._formdata)
496
        if field.key == 'password':
497
            return LazyFieldVarPassword(self._data, field, self._formdata)
498
        if field.key == 'file':
499
            return LazyFieldVarFile(self._data, field, self._formdata)
499
        klass = LazyFieldVar
500 500
        if field.store_structured_value:
501
            return LazyFieldVarStructured(self._data, field, self._formdata)
501
            klass = LazyFieldVarStructured
502
        klass = {  # custom types
503
            'date': LazyFieldVarDate,
504
            'map': LazyFieldVarMap,
505
            'password': LazyFieldVarPassword,
506
            'file': LazyFieldVarFile,
507
            'block': LazyFieldVarBlock,
508
        }.get(field.key, klass)
502 509

  
503
        return LazyFieldVar(self._data, field, self._formdata)
510
        return klass(**self.get_field_kwargs(field))
504 511

  
505 512
    def __getattr__(self, attr):
506 513
        try:
......
510 517

  
511 518

  
512 519
class LazyFieldVar(object):
513
    def __init__(self, data, field, formdata=None):
520
    def __init__(self, data, field, formdata=None, **kwargs):
514 521
        self._data = data
515 522
        self._field = field
516 523
        self._formdata = formdata
524
        self._field_kwargs = kwargs
517 525

  
518 526
    @property
519 527
    def raw(self):
......
521 529
            return self._data.get(self._field.id)
522 530
        raise AttributeError('raw')
523 531

  
524
    @property
525
    def url(self):
526
        if self._field.key != 'file' or not self._formdata:
527
            raise AttributeError('url')
528
        return '%sdownload?f=%s' % (self._formdata.get_url(), self._field.id)
529

  
530 532
    def get_value(self):
531 533
        if self._field.store_display_value:
532 534
            return self._data.get('%s_display' % self._field.id)
......
764 766

  
765 767

  
766 768
class LazyFieldVarFile(LazyFieldVar):
767
    pass
769
    @property
770
    def url(self):
771
        return self._field.get_download_url(formdata=self._formdata, **self._field_kwargs)
772

  
773

  
774
class LazyBlockDataVar(LazyFormDataVar):
775
    def __init__(self, fields, data, formdata=None, parent_field=None, parent_field_index=0):
776
        super().__init__(fields, data, formdata=formdata)
777
        self.parent_field = parent_field
778
        self.parent_field_index = parent_field_index
779

  
780
    def get_field_kwargs(self, field):
781
        kwargs = super().get_field_kwargs(field)
782
        kwargs['parent_field'] = self.parent_field
783
        kwargs['parent_field_index'] = self.parent_field_index
784
        return kwargs
785

  
786

  
787
class LazyFieldVarBlock(LazyFieldVar):
788
    def inspect_keys(self):
789
        if self._field.max_items > 1:
790
            data = self._formdata.data.get(self._field.id)['data']
791
            return [str(x) for x in range(len(data))]
792
        else:
793
            return ['var']
794

  
795
    def get_value(self):
796
        # don't give access to underlying data dictionary.
797
        return self._data.get('%s_display' % self._field.id, '---')
798

  
799
    def __getitem__(self, key):
800
        try:
801
            int(key)
802
        except ValueError:
803
            return super().__getitem__(key)
804
        data = self._formdata.data.get(self._field.id)['data'][int(key)]
805
        return LazyBlockDataVar(self._field.block.fields,
806
                data,
807
                formdata=self._formdata,
808
                parent_field=self._field,
809
                parent_field_index=int(key),
810
                )
811

  
812
    @property
813
    def var(self):
814
        # alias when there's a single item
815
        return self[0]
816

  
817
    def __iter__(self):
818
        data = self._formdata.data.get(self._field.id)['data']
819
        for i in range(len(data)):
820
            yield self[i]
768 821

  
769 822

  
770 823
class LazyUser(object):
771
-