general add support for blocks of fields (#8265)
This commit is contained in:
parent
2c0bba5f71
commit
95c65b6326
|
@ -49,6 +49,11 @@ def studio(request, pub):
|
|||
return
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def blocks_feature(request, pub):
|
||||
return site_options(request, pub, 'options', 'fields-blocks', 'true')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def emails():
|
||||
with EmailsMocking() as mock:
|
||||
|
|
|
@ -50,6 +50,7 @@ from wcs.wf.redirect_to_url import RedirectToUrlWorkflowStatusItem
|
|||
from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs import fields
|
||||
|
||||
from utilities import get_app, login, create_temporary_pub, clean_temporary_pub, HttpRequestsMocking
|
||||
|
@ -5989,3 +5990,204 @@ def test_create_formdata(pub):
|
|||
resp = resp.form.submit(name='submit')
|
||||
pq = resp.pyquery.remove_namespaces()
|
||||
assert pq('.error').text() == 'Some destination fields are duplicated'
|
||||
|
||||
|
||||
def test_block_new(pub, blocks_feature):
|
||||
create_superuser(pub)
|
||||
create_role()
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/')
|
||||
resp = resp.click('Fields blocks')
|
||||
resp = resp.click('New field block')
|
||||
resp.form['name'] = 'field block'
|
||||
resp = resp.form.submit()
|
||||
assert resp.location == 'http://example.net/backoffice/forms/blocks/1/'
|
||||
resp = resp.follow()
|
||||
assert '<h2>field block' in resp
|
||||
assert 'There are not yet any fields' in resp
|
||||
|
||||
resp.form['label'] = 'foobar'
|
||||
resp.form['type'] = 'string'
|
||||
resp = resp.form.submit()
|
||||
assert resp.location == 'http://example.net/backoffice/forms/blocks/1/'
|
||||
resp = resp.follow()
|
||||
|
||||
resp.form['label'] = 'barfoo'
|
||||
resp.form['type'] = 'string'
|
||||
resp = resp.form.submit()
|
||||
assert resp.location == 'http://example.net/backoffice/forms/blocks/1/'
|
||||
resp = resp.follow()
|
||||
|
||||
assert len(BlockDef.get(1).fields) == 2
|
||||
assert str(BlockDef.get(1).fields[0].id) != '1' # don't use integers
|
||||
|
||||
|
||||
def test_block_options(pub, blocks_feature):
|
||||
create_superuser(pub)
|
||||
create_role()
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^settings$'))
|
||||
assert 'readonly' not in resp.form['slug'].attrs
|
||||
resp.form['name'] = 'foo bar'
|
||||
resp = resp.form.submit('submit')
|
||||
assert BlockDef.get(block.id).name == 'foo bar'
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='0', label='test', type='block:%s' % block.slug),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^settings$'))
|
||||
assert 'readonly' in resp.form['slug'].attrs
|
||||
resp = resp.form.submit('cancel')
|
||||
resp = resp.follow()
|
||||
|
||||
|
||||
def test_block_export_import(pub, blocks_feature):
|
||||
create_superuser(pub)
|
||||
create_role()
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^export$'))
|
||||
xml_export = resp.text
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/')
|
||||
resp = resp.click(href='import')
|
||||
resp = resp.form.submit('cancel') # shouldn't block on missing file
|
||||
resp = resp.follow()
|
||||
|
||||
resp = resp.click(href='import')
|
||||
resp = resp.form.submit()
|
||||
assert 'ere were errors processing your form.' in resp
|
||||
|
||||
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
resp = resp.follow()
|
||||
assert BlockDef.count() == 2
|
||||
|
||||
new_blockdef = [x for x in BlockDef.select() if str(x.id) != str(block.id)][0]
|
||||
assert new_blockdef.name == 'Copy of foobar'
|
||||
assert new_blockdef.slug == 'foobar_1'
|
||||
assert len(new_blockdef.fields) == 1
|
||||
assert new_blockdef.fields[0].id == '123'
|
||||
|
||||
resp = app.get('/backoffice/forms/blocks/')
|
||||
resp = resp.click(href='import')
|
||||
resp.form['file'] = Upload('block', xml_export.encode('utf-8'))
|
||||
resp = resp.form.submit()
|
||||
assert 'Copy of foobar (2)' in [x.name for x in BlockDef.select()]
|
||||
|
||||
# import invalid content
|
||||
resp = app.get('/backoffice/forms/blocks/')
|
||||
resp = resp.click(href='import')
|
||||
resp.form['file'] = Upload('block', b'whatever')
|
||||
resp = resp.form.submit()
|
||||
assert 'Invalid File' in resp
|
||||
|
||||
|
||||
def test_block_delete(pub, blocks_feature):
|
||||
create_superuser(pub)
|
||||
create_role()
|
||||
BlockDef.wipe()
|
||||
FormDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^delete$'))
|
||||
assert 'You are about to irrevocably delete this block.' in resp
|
||||
resp = resp.form.submit()
|
||||
resp = resp.follow()
|
||||
assert BlockDef.count() == 0
|
||||
|
||||
# in use
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
|
||||
block.store()
|
||||
|
||||
FormDef.wipe()
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='0', label='test', type='block:%s' % block.slug),
|
||||
]
|
||||
formdef.store()
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('^delete$'))
|
||||
assert 'This block is still used' in resp
|
||||
|
||||
|
||||
def test_block_edit_duplicate_delete_field(pub, blocks_feature):
|
||||
create_superuser(pub)
|
||||
create_role()
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
|
||||
block.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/blocks/%s/' % block.id)
|
||||
resp = resp.click(href=re.compile('123/$'))
|
||||
resp.form['required'].checked = False
|
||||
resp.form['varname'] = 'test'
|
||||
resp = resp.form.submit('submit')
|
||||
resp = resp.follow()
|
||||
assert BlockDef.get(block.id).fields[0].required is False
|
||||
assert BlockDef.get(block.id).fields[0].varname == 'test'
|
||||
|
||||
resp = resp.click(href=re.compile('123/duplicate$'))
|
||||
resp = resp.follow()
|
||||
assert len(BlockDef.get(block.id).fields) == 2
|
||||
|
||||
resp = resp.click(href='%s/delete' % BlockDef.get(block.id).fields[1].id)
|
||||
resp = resp.form.submit('submit')
|
||||
resp = resp.follow()
|
||||
assert len(BlockDef.get(block.id).fields) == 1
|
||||
|
||||
|
||||
def test_block_use_in_formdef(pub, blocks_feature):
|
||||
create_superuser(pub)
|
||||
create_role()
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/forms/1/fields/')
|
||||
resp.forms[0]['label'] = 'a block field'
|
||||
resp.forms[0]['type'] = 'block:foobar'
|
||||
resp = resp.forms[0].submit().follow()
|
||||
assert 'a block field' in resp.text
|
||||
resp = resp.click('Edit', href='1/')
|
||||
assert resp.form['max_items'].value == '1'
|
||||
|
|
|
@ -30,6 +30,7 @@ from wcs.qommon import force_str
|
|||
from wcs.qommon.emails import docutils
|
||||
from wcs.qommon.form import UploadedFile
|
||||
from wcs.qommon.ident.password_accounts import PasswordAccount
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.workflows import (Workflow, EditableWorkflowStatusItem,
|
||||
|
@ -8228,3 +8229,404 @@ def test_structured_workflow_options(pub):
|
|||
'1_structured': {'id': '1', 'text': 'un', 'more': 'foo'},
|
||||
}
|
||||
assert '2020-04-18' in formdata.evolution[0].parts[0].content
|
||||
|
||||
|
||||
def test_block_simple(pub, blocks_feature):
|
||||
create_user(pub)
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test', type='string'),
|
||||
fields.StringField(id='234', required=True, label='Test2', type='string'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='1', label='test', type='block:foobar'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert resp.form['f1$element0$f123'].attrs['readonly']
|
||||
assert resp.form['f1$element0$f123'].value == 'foo'
|
||||
assert resp.form['f1$element0$f234'].attrs['readonly']
|
||||
assert resp.form['f1$element0$f234'].value == 'bar'
|
||||
resp = resp.form.submit('submit') # -> end page
|
||||
resp = resp.follow()
|
||||
assert '>foo<' in resp
|
||||
assert '>bar<' in resp
|
||||
|
||||
|
||||
def test_block_required(pub, blocks_feature):
|
||||
create_user(pub)
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test', type='string'),
|
||||
fields.StringField(id='234', required=True, label='Test2', type='string'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='1', label='test', type='block:foobar'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
resp = resp.form.submit('submit') # -> error page
|
||||
assert 'There were errors processing the form' in resp
|
||||
assert resp.text.count('required field') == 1
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp = resp.form.submit('submit') # -> error page
|
||||
assert 'There were errors processing the form' in resp
|
||||
assert resp.text.count('required field') == 1
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
|
||||
# only one required
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test', type='string'),
|
||||
fields.StringField(id='234', required=False, label='Test2', type='string'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
|
||||
# none required, but globally required
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=False, label='Test', type='string'),
|
||||
fields.StringField(id='234', required=False, label='Test2', type='string'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
resp = app.get(formdef.get_url())
|
||||
resp = resp.form.submit('submit') # -> error page
|
||||
assert 'There were errors processing the form' in resp
|
||||
assert resp.text.count('required field') == 1
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
|
||||
|
||||
def test_block_date(pub, blocks_feature):
|
||||
create_user(pub)
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test', type='string'),
|
||||
fields.DateField(id='234', required=True, label='Test2', type='date'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='1', label='test', type='block:foobar'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234'] = '2020-06-16'
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
resp = resp.form.submit('submit') # -> submit
|
||||
resp = resp.follow()
|
||||
assert '>2020-06-16<' in resp
|
||||
|
||||
|
||||
def test_block_multipage(pub, blocks_feature):
|
||||
create_user(pub)
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test', type='string'),
|
||||
fields.StringField(id='234', required=True, label='Test2', type='string'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page', type='page'),
|
||||
fields.BlockField(id='1', label='test', type='block:foobar'),
|
||||
fields.PageField(id='2', label='2nd page', type='page'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('submit') # -> 2nd page
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert resp.form['f1$element0$f123'].attrs['readonly']
|
||||
assert resp.form['f1$element0$f123'].value == 'foo'
|
||||
resp = resp.form.submit('previous') # -> 2nd page
|
||||
resp = resp.form.submit('previous') # -> 1st page
|
||||
assert 'readonly' not in resp.form['f1$element0$f123'].attrs
|
||||
assert resp.form['f1$element0$f123'].value == 'foo'
|
||||
resp = resp.form.submit('submit') # -> 2nd page
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
resp = resp.form.submit('submit') # -> submit
|
||||
resp = resp.follow()
|
||||
assert '>foo<' in resp
|
||||
assert '>bar<' in resp
|
||||
|
||||
|
||||
def test_block_repeated(pub, blocks_feature):
|
||||
create_user(pub)
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test', type='string'),
|
||||
fields.StringField(id='234', required=True, label='Test2', type='string'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page', type='page'),
|
||||
fields.BlockField(id='1', label='test', type='block:foobar', max_items=3),
|
||||
fields.PageField(id='2', label='2nd page', type='page'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
assert resp.text.count('>Test<') == 1
|
||||
assert 'Add another' in resp
|
||||
resp = resp.form.submit('f1$add_element')
|
||||
assert resp.text.count('>Test<') == 2
|
||||
resp = resp.form.submit('f1$add_element')
|
||||
assert resp.text.count('>Test<') == 3
|
||||
assert 'Add another' not in resp
|
||||
|
||||
# fill items (1st and 3rd row)
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp.form['f1$element2$f123'] = 'foo2'
|
||||
resp.form['f1$element2$f234'] = 'bar2'
|
||||
|
||||
resp = resp.form.submit('submit') # -> 2nd page
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
assert resp.form['f1$element0$f123'].value == 'foo'
|
||||
assert resp.form['f1$element0$f234'].value == 'bar'
|
||||
assert resp.form['f1$element1$f123'].value == 'foo2'
|
||||
assert resp.form['f1$element1$f234'].value == 'bar2'
|
||||
|
||||
resp = resp.form.submit('previous') # -> 2nd page
|
||||
resp = resp.form.submit('previous') # -> 1st page
|
||||
assert 'readonly' not in resp.form['f1$element0$f123'].attrs
|
||||
assert resp.form['f1$element0$f123'].value == 'foo'
|
||||
|
||||
resp = resp.form.submit('submit') # -> 2nd page
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
resp = resp.form.submit('submit') # -> submit
|
||||
resp = resp.follow()
|
||||
assert '>foo<' in resp
|
||||
assert '>bar<' in resp
|
||||
assert '>foo2<' in resp
|
||||
assert '>bar2<' in resp
|
||||
|
||||
|
||||
def test_block_repeated_over_limit(pub, blocks_feature):
|
||||
create_user(pub)
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test', type='string'),
|
||||
fields.StringField(id='234', required=True, label='Test2', type='string'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page', type='page'),
|
||||
fields.BlockField(id='1', label='test', type='block:foobar', max_items=3),
|
||||
fields.PageField(id='2', label='2nd page', type='page'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
assert resp.text.count('>Test<') == 1
|
||||
assert 'Add another' in resp
|
||||
resp = resp.form.submit('f1$add_element')
|
||||
assert resp.text.count('>Test<') == 2
|
||||
resp = resp.form.submit('f1$add_element')
|
||||
assert resp.text.count('>Test<') == 3
|
||||
assert 'Add another' not in resp
|
||||
|
||||
# fill items
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp.form['f1$element1$f123'] = 'foo1'
|
||||
resp.form['f1$element1$f234'] = 'bar1'
|
||||
resp.form['f1$element2$f123'] = 'foo2'
|
||||
resp.form['f1$element2$f234'] = 'bar2'
|
||||
|
||||
# (modify formdef to only allow 2)
|
||||
formdef.fields[1].max_items = 2
|
||||
formdef.store()
|
||||
|
||||
# submit form
|
||||
resp = resp.form.submit('submit')
|
||||
assert 'Too many elements (maximum: 2)' in resp
|
||||
|
||||
|
||||
def test_block_repeated_files(pub, blocks_feature):
|
||||
create_user(pub)
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test', type='string'),
|
||||
fields.FileField(id='234', required=True, label='Test2', type='file'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.PageField(id='0', label='1st page', type='page'),
|
||||
fields.BlockField(id='1', label='test', type='block:foobar', max_items=3),
|
||||
fields.PageField(id='2', label='2nd page', type='page'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
assert resp.text.count('>Test<') == 1
|
||||
assert 'Add another' in resp
|
||||
resp = resp.form.submit('f1$add_element')
|
||||
assert resp.text.count('>Test<') == 2
|
||||
resp = resp.form.submit('f1$add_element')
|
||||
assert resp.text.count('>Test<') == 3
|
||||
assert 'Add another' not in resp
|
||||
|
||||
# fill items (1st and 3rd row)
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234$file'] = Upload('test1.txt', b'foobar1', 'text/plain')
|
||||
resp.form['f1$element2$f123'] = 'foo2'
|
||||
resp.form['f1$element2$f234$file'] = Upload('test2.txt', b'foobar2', 'text/plain')
|
||||
|
||||
resp = resp.form.submit('submit') # -> 2nd page
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
assert 'Check values then click submit.' in resp.text
|
||||
assert resp.form['f1$element0$f123'].value == 'foo'
|
||||
assert 'test1.txt' in resp
|
||||
assert resp.form['f1$element1$f123'].value == 'foo2'
|
||||
assert 'test2.txt' in resp
|
||||
|
||||
resp = resp.form.submit('previous') # -> 2nd page
|
||||
resp = resp.form.submit('previous') # -> 1st page
|
||||
resp = resp.form.submit('submit') # -> 2nd page
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
resp = resp.form.submit('submit') # -> submit
|
||||
resp = resp.follow()
|
||||
assert '>foo<' in resp
|
||||
assert 'test1.txt' in resp
|
||||
assert '>foo2<' in resp
|
||||
assert 'test2.txt' in resp
|
||||
|
||||
|
||||
def test_block_digest(pub, blocks_feature):
|
||||
create_user(pub)
|
||||
FormDef.wipe()
|
||||
BlockDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test',
|
||||
type='string', varname='foo'),
|
||||
fields.StringField(id='234', required=True, label='Test2',
|
||||
type='string', varname='bar'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'form title'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='1', label='test', type='block:foobar', max_items=3),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
app = get_app(pub)
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('f1$add_element')
|
||||
resp.form['f1$element1$f123'] = 'foo2'
|
||||
resp.form['f1$element1$f234'] = 'bar2'
|
||||
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
resp = resp.form.submit('submit') # -> submit
|
||||
|
||||
assert formdef.data_class().select()[0].data['1']['data'] == [
|
||||
{'123': 'foo', '234': 'bar'}, {'123': 'foo2', '234': 'bar2'}]
|
||||
# by default it gets the type of object
|
||||
assert formdef.data_class().select()[0].data['1_display'] == 'foobar, foobar'
|
||||
|
||||
# set a digest template
|
||||
formdef.data_class().wipe()
|
||||
|
||||
block.digest_template = 'X{{foobar_var_foo}}Y'
|
||||
block.store()
|
||||
|
||||
resp = app.get(formdef.get_url())
|
||||
resp.form['f1$element0$f123'] = 'foo'
|
||||
resp.form['f1$element0$f234'] = 'bar'
|
||||
resp = resp.form.submit('f1$add_element')
|
||||
resp.form['f1$element1$f123'] = 'foo2'
|
||||
resp.form['f1$element1$f234'] = 'bar2'
|
||||
|
||||
resp = resp.form.submit('submit') # -> validation page
|
||||
resp = resp.form.submit('submit') # -> submit
|
||||
assert formdef.data_class().select()[0].data['1']['data'] == [
|
||||
{'123': 'foo', '234': 'bar'}, {'123': 'foo2', '234': 'bar2'}]
|
||||
assert formdef.data_class().select()[0].data['1_display'] == 'XfooY, Xfoo2Y'
|
||||
|
|
|
@ -15,6 +15,7 @@ from wcs.qommon.template import Template
|
|||
from wcs.qommon.form import PicklableUpload
|
||||
from wcs.qommon.http_request import HTTPRequest
|
||||
from wcs import fields, formdef
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.categories import Category
|
||||
from wcs.conditions import Condition
|
||||
from wcs.formdef import FormDef
|
||||
|
@ -2050,3 +2051,65 @@ def test_form_parent(pub):
|
|||
assert str(variables['form'].var.foo) == 'world'
|
||||
assert str(variables['form'].parent['form'].var.foo) == 'hello'
|
||||
assert variables['form'].parent is not None
|
||||
|
||||
|
||||
def test_block_variables(pub, blocks_feature):
|
||||
BlockDef.wipe()
|
||||
FormDef.wipe()
|
||||
|
||||
block = BlockDef()
|
||||
block.name = 'foobar'
|
||||
block.digest_template = 'X{{foobar_var_foo}}Y'
|
||||
block.fields = [
|
||||
fields.StringField(id='123', required=True, label='Test',
|
||||
type='string', varname='foo'),
|
||||
fields.StringField(id='234', required=True, label='Test2',
|
||||
type='string', varname='bar'),
|
||||
]
|
||||
block.store()
|
||||
|
||||
formdef = FormDef()
|
||||
formdef.name = 'testblock'
|
||||
formdef.fields = [
|
||||
fields.BlockField(id='1', label='test', type='block:foobar',
|
||||
max_items=3, varname='block'),
|
||||
]
|
||||
formdef.store()
|
||||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.just_created()
|
||||
# value from test_block_digest in tests/test_form_pages.py
|
||||
formdata.data = {
|
||||
'1': {
|
||||
'data': [{'123': 'foo', '234': 'bar'}, {'123': 'foo2', '234': 'bar2'}],
|
||||
'schema': {'123': 'string', '234': 'string'}
|
||||
},
|
||||
'1_display': 'XfooY, Xfoo2Y',
|
||||
}
|
||||
formdata.store()
|
||||
|
||||
variables = formdata.get_substitution_variables()
|
||||
assert 'form_var_block' in variables.get_flat_keys()
|
||||
assert 'form_var_block_0' in variables.get_flat_keys()
|
||||
assert 'form_var_block_0_foo' in variables.get_flat_keys()
|
||||
assert 'form_var_block_0_bar' in variables.get_flat_keys()
|
||||
assert 'form_var_block_1' in variables.get_flat_keys()
|
||||
assert 'form_var_block_1_foo' in variables.get_flat_keys()
|
||||
assert 'form_var_block_1_bar' in variables.get_flat_keys()
|
||||
|
||||
assert variables.get('form_var_block_0_foo') == 'foo'
|
||||
assert variables.get('form_var_block_1_foo') == 'foo2'
|
||||
assert variables.get('form_var_block_var_foo') == 'foo' # alias to 1st element
|
||||
|
||||
pub.substitutions.reset()
|
||||
pub.substitutions.feed(formdata)
|
||||
|
||||
context = pub.substitutions.get_context_variables(mode='lazy')
|
||||
tmpl = Template('{{ form_var_block }}')
|
||||
assert tmpl.render(context) == 'XfooY, Xfoo2Y'
|
||||
|
||||
tmpl = Template('{% for sub in form_var_block %}{{ sub.foo }} {% endfor %}')
|
||||
assert tmpl.render(context) == 'foo foo2 '
|
||||
|
||||
tmpl = Template('{{ form_var_block|length }}')
|
||||
assert tmpl.render(context) == '2'
|
||||
|
|
|
@ -0,0 +1,256 @@
|
|||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2020 Entr'ouvert
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from quixote import get_response, get_session, redirect
|
||||
from quixote.directory import Directory
|
||||
from quixote.html import TemplateIO, htmltext
|
||||
|
||||
from wcs.blocks import BlockDef, BlockdefImportError
|
||||
|
||||
from wcs.qommon.form import Form, StringWidget, HtmlWidget, FileWidget
|
||||
from wcs.qommon import _, misc, template
|
||||
from wcs.qommon.backoffice.menu import html_top
|
||||
|
||||
from wcs.admin.fields import FieldDefPage, FieldsDirectory
|
||||
from wcs.admin import utils
|
||||
|
||||
|
||||
class BlockFieldDefPage(FieldDefPage):
|
||||
blacklisted_attributes = ['condition']
|
||||
|
||||
def redirect_field_anchor(self, field):
|
||||
anchor = '#itemId_%s' % field.id if field else ''
|
||||
return redirect('../%s' % anchor)
|
||||
|
||||
|
||||
class BlockDirectory(FieldsDirectory):
|
||||
_q_exports = ['', 'update_order', 'new', 'delete', 'export', 'settings']
|
||||
field_def_page_class = BlockFieldDefPage
|
||||
blacklisted_types = ['page', 'table', 'table-select', 'tablerows', 'ranked-items', 'blocks']
|
||||
support_import = False
|
||||
|
||||
def __init__(self, section, *args, **kwargs):
|
||||
self.section = section
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def index_top(self):
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div id="appbar">')
|
||||
r += htmltext('<h2>%s</h2>') % self.objectdef.name
|
||||
r += htmltext('<span class="actions">')
|
||||
r += htmltext('<a href="delete" rel="popup">%s</a>') % _('Delete')
|
||||
r += htmltext('<a href="export">%s</a>') % _('Export')
|
||||
r += htmltext('<a href="settings" rel="popup">%s</a>') % _('Settings')
|
||||
r += htmltext('</span>')
|
||||
r += htmltext('</div>')
|
||||
r += utils.last_modification_block(obj=self.objectdef)
|
||||
r += get_session().display_message()
|
||||
|
||||
if not self.objectdef.fields:
|
||||
r += htmltext('<div class="infonotice">%s</div>') % _('There are not yet any fields defined.')
|
||||
return r.getvalue()
|
||||
|
||||
def index_bottom(self):
|
||||
formdefs = list(self.objectdef.get_usage_formdefs())
|
||||
formdefs.sort(key=lambda x: x.name.lower())
|
||||
if not formdefs:
|
||||
return
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<div class="section">')
|
||||
r += htmltext('<h3>%s</h3>') % _('Usage')
|
||||
r += htmltext('<ul class="objects-list single-links">')
|
||||
for formdef in formdefs:
|
||||
r += htmltext('<li><a href="%s">' % formdef.get_admin_url())
|
||||
r += htmltext('%s</a></li>') % formdef.name
|
||||
r += htmltext('</ul>')
|
||||
r += htmltext('</div>')
|
||||
return r.getvalue()
|
||||
|
||||
def delete(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
if not self.objectdef.is_used():
|
||||
form.widgets.append(
|
||||
HtmlWidget('<p>%s</p>' % _('You are about to irrevocably delete this block.'))
|
||||
)
|
||||
form.add_submit('delete', _('Submit'))
|
||||
else:
|
||||
form.widgets.append(
|
||||
HtmlWidget('<p>%s</p>' % _('This block is still used, it cannot be deleted.'))
|
||||
)
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
if form.get_widget('cancel').parse():
|
||||
return redirect('..')
|
||||
if not form.is_submitted() or form.has_errors():
|
||||
get_response().breadcrumb.append(('delete', _('Delete')))
|
||||
html_top(self.section, title=_('Delete Block'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s %s</h2>') % (_('Deleting Block:'), self.objectdef.name)
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
else:
|
||||
self.objectdef.remove_self()
|
||||
return redirect('..')
|
||||
|
||||
def export(self):
|
||||
x = self.objectdef.export_to_xml(include_id=True)
|
||||
misc.indent_xml(x)
|
||||
response = get_response()
|
||||
response.set_content_type('application/x-wcs-form')
|
||||
response.set_header('content-disposition', 'attachment; filename=block-%s.wcs' % self.objectdef.slug)
|
||||
return '<?xml version="1.0"?>\n' + ET.tostring(x).decode('utf-8')
|
||||
|
||||
def settings(self):
|
||||
form = Form()
|
||||
form.add(StringWidget, 'name', title=_('Name'), value=self.objectdef.name, size=50)
|
||||
disabled_slug = bool(self.objectdef.is_used())
|
||||
widget = form.add(
|
||||
StringWidget,
|
||||
'slug',
|
||||
title=_('Identifier'),
|
||||
value=self.objectdef.slug,
|
||||
size=50,
|
||||
readonly=disabled_slug,
|
||||
hint=_('It is for example used as prefix for variable names in the digest template below.'),
|
||||
)
|
||||
if disabled_slug:
|
||||
widget.hint = _('The identifier can not be modified as the block is in use.')
|
||||
form.add(
|
||||
StringWidget, 'digest_template', title=_('Digest Template'), value=self.objectdef.digest_template, size=50
|
||||
)
|
||||
form.add_submit('submit', _('Submit'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
||||
if form.get_widget('cancel').parse():
|
||||
return redirect('.')
|
||||
|
||||
if form.is_submitted() and not form.has_errors():
|
||||
self.objectdef.name = form.get_widget('name').parse()
|
||||
if form.get_widget('slug'):
|
||||
self.objectdef.slug = form.get_widget('slug').parse()
|
||||
self.objectdef.digest_template = form.get_widget('digest_template').parse()
|
||||
self.objectdef.store()
|
||||
return redirect('.')
|
||||
|
||||
html_top(self.section, title=_('Settings'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Settings')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
|
||||
class BlocksDirectory(Directory):
|
||||
_q_exports = ['', 'new', ('import', 'p_import')]
|
||||
do_not_call_in_templates = True
|
||||
|
||||
def __init__(self, section):
|
||||
super().__init__()
|
||||
self.section = section
|
||||
|
||||
def _q_traverse(self, path):
|
||||
get_response().breadcrumb.append(('blocks/', _('Fields Blocks')))
|
||||
return super()._q_traverse(path)
|
||||
|
||||
def _q_lookup(self, component):
|
||||
return BlockDirectory(self.section, BlockDef.get(component))
|
||||
|
||||
def _q_index(self):
|
||||
html_top(self.section, title=_('Fields Blocks'))
|
||||
return template.QommonTemplateResponse(
|
||||
templates=['wcs/backoffice/blocks.html'],
|
||||
context={'view': self, 'blocks': BlockDef.select(order_by='name')},
|
||||
)
|
||||
|
||||
def new(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
form.add(StringWidget, 'name', title=_('Name'), required=True, size=50)
|
||||
form.add_submit('submit', _('Add'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
if form.get_widget('cancel').parse():
|
||||
return redirect('.')
|
||||
|
||||
if form.is_submitted() and not form.has_errors():
|
||||
block = BlockDef(name=form.get_widget('name').parse())
|
||||
block.store()
|
||||
return redirect('%s/' % block.id)
|
||||
|
||||
get_response().breadcrumb.append(('new', _('New Fields Block')))
|
||||
html_top(self.section, title=_('New Fields Block'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('New Fields Block')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def p_import(self):
|
||||
form = Form(enctype='multipart/form-data')
|
||||
|
||||
form.add(FileWidget, 'file', title=_('File'), required=True)
|
||||
form.add_submit('submit', _('Import Fields Block'))
|
||||
form.add_submit('cancel', _('Cancel'))
|
||||
|
||||
if form.get_submit() == 'cancel':
|
||||
return redirect('.')
|
||||
|
||||
if form.is_submitted() and not form.has_errors():
|
||||
try:
|
||||
return self.import_submit(form)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
get_response().breadcrumb.append(('import', _('Import')))
|
||||
html_top(self.section, title=_('Import Fields Block'))
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<h2>%s</h2>') % _('Import Fields Block')
|
||||
r += htmltext('<p>%s</p>') % _('You can install a new fields block by uploading a file.')
|
||||
r += form.render()
|
||||
return r.getvalue()
|
||||
|
||||
def import_submit(self, form):
|
||||
fp = form.get_widget('file').parse().fp
|
||||
|
||||
error, reason = False, None
|
||||
try:
|
||||
blockdef = BlockDef.import_from_xml(fp)
|
||||
except BlockdefImportError as e:
|
||||
error = True
|
||||
reason = _(e) % e.msg_args
|
||||
except ValueError:
|
||||
error = True
|
||||
|
||||
if error:
|
||||
if reason:
|
||||
msg = _('Invalid File (%s)') % reason
|
||||
else:
|
||||
msg = _('Invalid File')
|
||||
form.set_error('file', msg)
|
||||
raise ValueError()
|
||||
|
||||
initial_blockdef_name = blockdef.name
|
||||
blockdef_names = [x.name for x in BlockDef.select()]
|
||||
copy_no = 1
|
||||
while blockdef.name in blockdef_names:
|
||||
if copy_no == 1:
|
||||
blockdef.name = _('Copy of %s') % initial_blockdef_name
|
||||
else:
|
||||
blockdef.name = _('Copy of %(name)s (%(no)d)') % {
|
||||
'name': initial_blockdef_name,
|
||||
'no': copy_no,
|
||||
}
|
||||
copy_no += 1
|
||||
blockdef.store()
|
||||
get_session().message = ('info', _('This fields block has been successfully imported.'))
|
||||
return redirect('%s/' % blockdef.id)
|
|
@ -174,7 +174,7 @@ class FieldsDirectory(Directory):
|
|||
field_def_page_class = FieldDefPage
|
||||
blacklisted_types = []
|
||||
page_id = None
|
||||
field_var_prefix = ''
|
||||
field_var_prefix = '..._'
|
||||
|
||||
support_import = True
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ from wcs.workflows import Workflow
|
|||
from wcs.forms.root import qrcode
|
||||
|
||||
from . import utils
|
||||
from .blocks import BlocksDirectory
|
||||
from .fields import FieldDefPage, FieldsDirectory
|
||||
from .categories import CategoriesDirectory
|
||||
from .data_sources import NamedDataSourcesDirectory
|
||||
|
@ -1467,9 +1468,10 @@ class NamedDataSourcesDirectoryInForms(NamedDataSourcesDirectory):
|
|||
|
||||
class FormsDirectory(AccessControlled, Directory):
|
||||
_q_exports = ['', 'new', ('import', 'p_import'),
|
||||
'categories', ('data-sources', 'data_sources')]
|
||||
'blocks', 'categories', ('data-sources', 'data_sources')]
|
||||
|
||||
categories = CategoriesDirectory()
|
||||
blocks = BlocksDirectory(section='forms')
|
||||
data_sources = NamedDataSourcesDirectoryInForms()
|
||||
formdef_class = FormDef
|
||||
formdef_page_class = FormDefPage
|
||||
|
@ -1507,6 +1509,8 @@ class FormsDirectory(AccessControlled, Directory):
|
|||
if has_roles:
|
||||
r += htmltext('<span class="actions">')
|
||||
r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources')
|
||||
if get_publisher().has_site_option('fields-blocks'):
|
||||
r += htmltext('<a href="blocks/">%s</a>') % _('Fields blocks')
|
||||
if get_publisher().get_backoffice_root().is_accessible('categories'):
|
||||
r += htmltext('<a href="categories/">%s</a>') % _('Categories')
|
||||
r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import')
|
||||
|
|
|
@ -52,6 +52,7 @@ from wcs.qommon.admin.settings import SettingsDirectory as QommonSettingsDirecto
|
|||
from wcs.qommon.admin.logger import LoggerDirectory
|
||||
from wcs.qommon import ident
|
||||
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.workflows import Workflow, WorkflowImportError
|
||||
|
@ -861,6 +862,8 @@ class SettingsDirectory(QommonSettingsDirectory):
|
|||
if StudioDirectory.is_visible():
|
||||
form.add(CheckboxWidget, 'carddefs', title=_('Card Models'), value=True)
|
||||
form.add(CheckboxWidget, 'workflows', title = _('Workflows'), value = True)
|
||||
if get_publisher().has_site_option('fields-blocks'):
|
||||
form.add(CheckboxWidget, 'blockdefs', title=_('Fields Blocks'), value=True)
|
||||
if not get_cfg('sp', {}).get('idp-manage-roles'):
|
||||
form.add(CheckboxWidget, 'roles', title = _('Roles'), value = True)
|
||||
form.add(CheckboxWidget, 'categories', title = _('Categories'), value = True)
|
||||
|
@ -918,6 +921,12 @@ class SettingsDirectory(QommonSettingsDirectory):
|
|||
misc.indent_xml(node)
|
||||
z.writestr(os.path.join('workflows_xml', str(workflow.id)),
|
||||
b'<?xml version="1.0"?>\n' + ET.tostring(node))
|
||||
if 'blockdefs' in self.dirs:
|
||||
for blockdef in BlockDef.select():
|
||||
node = blockdef.export_to_xml(include_id=True)
|
||||
misc.indent_xml(node)
|
||||
z.writestr(os.path.join('blockdefs_xml', str(blockdef.id)),
|
||||
b'<?xml version="1.0"?>\n' + ET.tostring(node))
|
||||
|
||||
if self.settings:
|
||||
z.write(os.path.join(self.app_dir, 'config.pck'), 'config.pck')
|
||||
|
@ -934,7 +943,7 @@ class SettingsDirectory(QommonSettingsDirectory):
|
|||
|
||||
dirs = []
|
||||
for w in ('formdefs', 'carddefs', 'workflows', 'roles', 'categories',
|
||||
'datasources', 'wscalls', 'mail-templates'):
|
||||
'datasources', 'wscalls', 'mail-templates', 'blockdefs'):
|
||||
if form.get_widget(w) and form.get_widget(w).parse():
|
||||
dirs.append(w)
|
||||
if not dirs and not form.get_widget('settings').parse():
|
||||
|
@ -1017,6 +1026,8 @@ class SettingsDirectory(QommonSettingsDirectory):
|
|||
r += htmltext('<li>%d %s</li>') % (results['formdefs'], _('forms'))
|
||||
if results['carddefs']:
|
||||
r += htmltext('<li>%d %s</li>') % (results['carddefs'], _('cards'))
|
||||
if results['blockdefs']:
|
||||
r += htmltext('<li>%d %s</li>') % (results['blockdefs'], _('fields blocks'))
|
||||
if results['workflows']:
|
||||
r += htmltext('<li>%d %s</li>') % (results['workflows'], _('workflows'))
|
||||
if results['roles']:
|
||||
|
|
|
@ -0,0 +1,319 @@
|
|||
# w.c.s. - web application for online forms
|
||||
# Copyright (C) 2005-2020 Entr'ouvert
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from quixote import get_request
|
||||
|
||||
from .qommon import _, N_, misc
|
||||
from .qommon.form import CompositeWidget, WidgetList
|
||||
from .qommon.storage import StorableObject
|
||||
from .qommon.template import Template
|
||||
|
||||
from . import data_sources
|
||||
from . import fields
|
||||
|
||||
|
||||
class BlockdefImportError(Exception):
|
||||
def __init__(self, msg, details=None):
|
||||
self.msg = msg
|
||||
self.details = details
|
||||
|
||||
|
||||
class BlockDef(StorableObject):
|
||||
_names = 'blockdefs'
|
||||
_indexes = ['slug']
|
||||
xml_root_node = 'block'
|
||||
|
||||
name = None
|
||||
slug = None
|
||||
fields = None
|
||||
digest_template = None
|
||||
|
||||
last_modification_time = None
|
||||
last_modification_user_id = None
|
||||
|
||||
# declarations for serialization
|
||||
TEXT_ATTRIBUTES = ['name', 'slug', 'digest_template']
|
||||
|
||||
def __init__(self, name=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.name = name
|
||||
self.fields = []
|
||||
|
||||
def store(self):
|
||||
if self.slug is None:
|
||||
# set slug if it's not yet there
|
||||
self.slug = self.get_new_slug()
|
||||
|
||||
self.last_modification_time = time.localtime()
|
||||
if get_request() and get_request().user:
|
||||
self.last_modification_user_id = str(get_request().user.id)
|
||||
else:
|
||||
self.last_modification_user_id = None
|
||||
|
||||
super().store()
|
||||
|
||||
def get_new_slug(self):
|
||||
new_slug = misc.simplify(self.name, space='_')
|
||||
base_new_slug = new_slug
|
||||
suffix_no = 0
|
||||
while True:
|
||||
try:
|
||||
obj = self.get_on_index(new_slug, 'slug', ignore_migration=True)
|
||||
except KeyError:
|
||||
break
|
||||
if obj.id == self.id:
|
||||
break
|
||||
suffix_no += 1
|
||||
new_slug = '%s_%s' % (base_new_slug, suffix_no)
|
||||
return new_slug
|
||||
|
||||
def get_new_field_id(self):
|
||||
return 'bf%s' % str(uuid.uuid4())
|
||||
|
||||
def get_display_value(self, value):
|
||||
if not self.digest_template:
|
||||
return self.name
|
||||
|
||||
from .qommon.substitution import CompatibilityNamesDict
|
||||
from .variables import LazyBlockDataVar
|
||||
|
||||
context = CompatibilityNamesDict({self.slug + '_var': LazyBlockDataVar(self.fields, value)})
|
||||
return Template(self.digest_template, autoescape=False).render(context)
|
||||
|
||||
def export_to_xml(self, include_id=False):
|
||||
root = ET.Element(self.xml_root_node)
|
||||
if include_id and self.id:
|
||||
root.attrib['id'] = str(self.id)
|
||||
for text_attribute in list(self.TEXT_ATTRIBUTES):
|
||||
if not hasattr(self, text_attribute) or not getattr(self, text_attribute):
|
||||
continue
|
||||
ET.SubElement(root, text_attribute).text = getattr(self, text_attribute)
|
||||
|
||||
if self.last_modification_time:
|
||||
elem = ET.SubElement(root, 'last_modification')
|
||||
elem.text = time.strftime('%Y-%m-%d %H:%M:%S', self.last_modification_time)
|
||||
if include_id:
|
||||
elem.attrib['user_id'] = str(self.last_modification_user_id)
|
||||
|
||||
fields = ET.SubElement(root, 'fields')
|
||||
for field in self.fields or []:
|
||||
fields.append(field.export_to_xml(charset='utf-8', include_id=True))
|
||||
|
||||
return root
|
||||
|
||||
@classmethod
|
||||
def import_from_xml(cls, fd, include_id=False, check_datasources=True):
|
||||
try:
|
||||
tree = ET.parse(fd)
|
||||
except:
|
||||
raise ValueError()
|
||||
blockdef = cls.import_from_xml_tree(tree, include_id=include_id)
|
||||
|
||||
if blockdef.slug:
|
||||
try:
|
||||
cls.get_on_index(blockdef.slug, 'slug', ignore_migration=True)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
blockdef.slug = blockdef.get_new_slug()
|
||||
|
||||
if check_datasources:
|
||||
# check if datasources are defined
|
||||
unknown_datasources = set()
|
||||
for field in blockdef.fields:
|
||||
data_source = getattr(field, 'data_source', None)
|
||||
if data_source:
|
||||
if isinstance(data_sources.get_object(data_source), data_sources.StubNamedDataSource):
|
||||
unknown_datasources.add(data_source.get('type'))
|
||||
if unknown_datasources:
|
||||
raise BlockdefImportError(
|
||||
N_('Unknown datasources'), details=', '.join(sorted(unknown_datasources))
|
||||
)
|
||||
|
||||
return blockdef
|
||||
|
||||
@classmethod
|
||||
def import_from_xml_tree(cls, tree, include_id=False):
|
||||
charset = 'utf-8'
|
||||
blockdef = cls()
|
||||
if tree.find('name') is None or not tree.find('name').text:
|
||||
raise BlockdefImportError(N_('Missing name'))
|
||||
|
||||
# if the tree we get is actually a ElementTree for real, we get its
|
||||
# root element and go on happily.
|
||||
if not ET.iselement(tree):
|
||||
tree = tree.getroot()
|
||||
|
||||
if tree.tag != cls.xml_root_node:
|
||||
raise BlockdefImportError(N_('Unexpected root node'))
|
||||
|
||||
if include_id and tree.attrib.get('id'):
|
||||
blockdef.id = tree.attrib.get('id')
|
||||
for text_attribute in list(cls.TEXT_ATTRIBUTES):
|
||||
value = tree.find(text_attribute)
|
||||
if value is None or value.text is None:
|
||||
continue
|
||||
setattr(blockdef, text_attribute, misc.xml_node_text(value))
|
||||
|
||||
blockdef.fields = []
|
||||
for i, field in enumerate(tree.find('fields')):
|
||||
try:
|
||||
field_o = fields.get_field_class_by_type(field.findtext('type'))()
|
||||
except KeyError:
|
||||
raise BlockdefImportError(N_('Unknown field type'), details=field.findtext('type'))
|
||||
field_o.init_with_xml(field, charset, include_id=True)
|
||||
blockdef.fields.append(field_o)
|
||||
|
||||
if tree.find('last_modification') is not None:
|
||||
node = tree.find('last_modification')
|
||||
blockdef.last_modification_time = time.strptime(node.text, '%Y-%m-%d %H:%M:%S')
|
||||
if include_id and node.attrib.get('user_id'):
|
||||
blockdef.last_modification_user_id = node.attrib.get('user_id')
|
||||
|
||||
return blockdef
|
||||
|
||||
def get_usage_formdefs(self):
|
||||
from wcs.formdef import get_formdefs_of_all_kinds
|
||||
|
||||
block_identifier = 'block:%s' % self.slug
|
||||
for formdef in get_formdefs_of_all_kinds():
|
||||
for field in formdef.fields:
|
||||
if field.type == block_identifier:
|
||||
yield formdef
|
||||
break
|
||||
|
||||
def is_used(self):
|
||||
return any(self.get_usage_formdefs())
|
||||
|
||||
|
||||
class BlockSubWidget(CompositeWidget):
|
||||
def __init__(self, name, value=None, *args, **kwargs):
|
||||
self.block = kwargs.pop('block')
|
||||
self.readonly = kwargs.get('readonly')
|
||||
super().__init__(name, value, *args, **kwargs)
|
||||
for field in self.block.fields:
|
||||
if 'readonly' in kwargs:
|
||||
field.add_to_view_form(form=self)
|
||||
else:
|
||||
field.add_to_form(form=self)
|
||||
if value:
|
||||
self.set_value(value)
|
||||
|
||||
def set_value(self, value):
|
||||
for widget in self.get_widgets():
|
||||
widget.set_value(value.get(widget.field.id))
|
||||
|
||||
def get_field_data(self, field, widget):
|
||||
from wcs.formdef import FormDef
|
||||
|
||||
return FormDef.get_field_data(field, widget)
|
||||
|
||||
def _parse(self, request):
|
||||
value = {}
|
||||
empty = True
|
||||
for widget in self.get_widgets():
|
||||
widget_value = self.get_field_data(widget.field, widget)
|
||||
value.update(widget_value)
|
||||
if widget_value.get(widget.field.id) is not None:
|
||||
empty = False
|
||||
if empty:
|
||||
value = None
|
||||
self.value = value
|
||||
|
||||
def add_media(self):
|
||||
for widget in self.get_widgets():
|
||||
widget.add_media()
|
||||
|
||||
|
||||
class BlockWidget(WidgetList):
|
||||
def __init__(
|
||||
self, name, value=None, title=None, block=None, max_items=None, add_element_label=None, **kwargs
|
||||
):
|
||||
self.block = block
|
||||
self.readonly = kwargs.get('readonly')
|
||||
element_values = None
|
||||
if value:
|
||||
element_values = value.get('data')
|
||||
if not max_items:
|
||||
max_items = 1
|
||||
element_kwargs = {'block': self.block, 'render_br': False}
|
||||
element_kwargs.update(kwargs)
|
||||
super().__init__(
|
||||
name,
|
||||
value=element_values,
|
||||
title=title,
|
||||
max_items=max_items,
|
||||
element_type=BlockSubWidget,
|
||||
element_kwargs=element_kwargs,
|
||||
add_element_label=add_element_label or _('Add another'),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def set_value(self, value):
|
||||
super().set_value(value['data'] if value else None)
|
||||
self.value = value
|
||||
|
||||
def _parse(self, request):
|
||||
# iterate over existing form keys to get actual list of elements.
|
||||
# (maybe this could be moved to WidgetList)
|
||||
prefix = '%s$element' % self.name
|
||||
known_prefixes = {x.split('$', 2)[1] for x in request.form.keys() if x.startswith(prefix)}
|
||||
for i in range(len(known_prefixes) - len(self.element_names)):
|
||||
self.add_element()
|
||||
super()._parse(request)
|
||||
if self.value:
|
||||
self.value = {'data': self.value}
|
||||
# keep "schema" next to data, this allows custom behaviour for
|
||||
# date fields (time.struct_time) when writing/reading from
|
||||
# database in JSON.
|
||||
self.value['schema'] = {x.id: x.key for x in self.block.fields}
|
||||
|
||||
def parse(self, request=None):
|
||||
if not self._parsed:
|
||||
self._parsed = True
|
||||
if request is None:
|
||||
request = get_request()
|
||||
self._parse(request)
|
||||
if self.required and self.value is None:
|
||||
self.set_error(_(self.REQUIRED_ERROR))
|
||||
return self.value
|
||||
|
||||
def add_media(self):
|
||||
for widget in self.get_widgets():
|
||||
if hasattr(widget, 'add_media'):
|
||||
widget.add_media()
|
||||
|
||||
def get_error(self, request=None):
|
||||
request = request or get_request()
|
||||
if request.get_method() == 'POST':
|
||||
self.parse(request=request)
|
||||
return self.error
|
||||
|
||||
def has_error(self, request=None):
|
||||
if self.get_error():
|
||||
return True
|
||||
# we know subwidgets have been parsed
|
||||
has_error = False
|
||||
for widget in self.widgets:
|
||||
if widget.value is None:
|
||||
continue
|
||||
if widget.has_error():
|
||||
has_error = True
|
||||
return has_error
|
132
wcs/fields.py
132
wcs/fields.py
|
@ -46,6 +46,7 @@ from .qommon import get_cfg, get_logger
|
|||
from . import data_sources
|
||||
from . import portfolio
|
||||
from .conditions import Condition
|
||||
from .blocks import BlockDef, BlockWidget
|
||||
|
||||
|
||||
class PrefillSelectionWidget(CompositeWidget):
|
||||
|
@ -499,7 +500,7 @@ class WidgetField(Field):
|
|||
widget_class = None
|
||||
|
||||
def add_to_form(self, form, value = None):
|
||||
kwargs = {'required': self.required}
|
||||
kwargs = {'required': self.required, 'render_br': False}
|
||||
if value:
|
||||
kwargs['value'] = value
|
||||
for k in self.extra_attributes:
|
||||
|
@ -545,6 +546,7 @@ class WidgetField(Field):
|
|||
value = value, readonly = 'readonly', **kwargs)
|
||||
widget = form.get_widget(self.field_key)
|
||||
widget.transfer_form_value(get_request())
|
||||
widget.field = self
|
||||
if self.extra_css_class:
|
||||
if hasattr(widget, 'extra_css_class') and widget.extra_css_class:
|
||||
widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class)
|
||||
|
@ -601,10 +603,10 @@ class WidgetField(Field):
|
|||
def get_csv_heading(self):
|
||||
return [self.label]
|
||||
|
||||
def get_view_value(self, value):
|
||||
def get_view_value(self, value, **kwargs):
|
||||
return str(value) if value else ''
|
||||
|
||||
def get_view_short_value(self, value, max_len = 30):
|
||||
def get_view_short_value(self, value, max_len=30, **kwargs):
|
||||
return self.get_view_value(value)
|
||||
|
||||
def get_csv_value(self, element, **kwargs):
|
||||
|
@ -792,7 +794,7 @@ class StringField(WidgetField):
|
|||
def get_admin_attributes(self):
|
||||
return WidgetField.get_admin_attributes(self) + ['size', 'validation', 'data_source', 'anonymise']
|
||||
|
||||
def get_view_value(self, value):
|
||||
def get_view_value(self, value, **kwargs):
|
||||
value = value or ''
|
||||
if value.startswith('http://') or value.startswith('https://'):
|
||||
charset = get_publisher().site_charset
|
||||
|
@ -869,7 +871,7 @@ class TextField(WidgetField):
|
|||
def convert_value_from_str(self, value):
|
||||
return value
|
||||
|
||||
def get_view_value(self, value):
|
||||
def get_view_value(self, value, **kwargs):
|
||||
if self.pre:
|
||||
return htmltext('<pre>') + value + htmltext('</pre>')
|
||||
else:
|
||||
|
@ -904,7 +906,7 @@ class EmailField(WidgetField):
|
|||
def convert_value_from_str(self, value):
|
||||
return value
|
||||
|
||||
def get_view_value(self, value):
|
||||
def get_view_value(self, value, **kwargs):
|
||||
return htmltext('<a href="mailto:%s">%s</a>') % (value, value)
|
||||
|
||||
def get_rst_view_value(self, value, indent=''):
|
||||
|
@ -937,7 +939,7 @@ class BoolField(WidgetField):
|
|||
get_request().form[self.field_key] = 'yes'
|
||||
self.field_key = 'f%sdisabled' % self.id
|
||||
|
||||
def get_view_value(self, value):
|
||||
def get_view_value(self, value, **kwargs):
|
||||
if value is True or value == 'True':
|
||||
return _('Yes')
|
||||
elif value is False or value == 'False':
|
||||
|
@ -1113,28 +1115,38 @@ class FileField(WidgetField):
|
|||
return upload
|
||||
raise ValueError('invalid data for file type (%r)' % value)
|
||||
|
||||
def get_view_short_value(self, value, max_len=30):
|
||||
return self.get_view_value(value, include_image_thumbnail=False)
|
||||
def get_view_short_value(self, value, max_len=30, **kwargs):
|
||||
return self.get_view_value(value, include_image_thumbnail=False, **kwargs)
|
||||
|
||||
def get_view_value(self, value, include_image_thumbnail=True):
|
||||
def get_download_query_string(self, **kwargs):
|
||||
if kwargs.get('parent_field'):
|
||||
return 'f=%s$%s$%s' % (
|
||||
kwargs['parent_field'].id,
|
||||
kwargs['parent_field_index'],
|
||||
self.id)
|
||||
return 'f=%s' % self.id
|
||||
|
||||
def get_view_value(self, value, include_image_thumbnail=True, **kwargs):
|
||||
show_link = True
|
||||
if value.has_redirect_url():
|
||||
is_in_backoffice = bool(get_request() and get_request().is_in_backoffice())
|
||||
show_link = bool(value.get_redirect_url(backoffice=is_in_backoffice))
|
||||
t = TemplateIO(html=True)
|
||||
t += htmltext('<div class="file-field">')
|
||||
if show_link or include_image_thumbnail:
|
||||
download_qs = self.get_download_query_string(**kwargs)
|
||||
if show_link:
|
||||
t += htmltext('<a href="[download]?f=%s">') % self.id
|
||||
t += htmltext('<a href="[download]?%s">') % download_qs
|
||||
if include_image_thumbnail and value.can_thumbnail():
|
||||
t += htmltext('<img alt="" src="[download]?f=%s&thumbnail=1"/>') % self.id
|
||||
t += htmltext('<img alt="" src="[download]?%s&thumbnail=1"/>') % download_qs
|
||||
t += htmltext('<span>%s</span>') % value
|
||||
if show_link:
|
||||
t += htmltext('</a>')
|
||||
t += htmltext('</div>')
|
||||
return t.getvalue()
|
||||
|
||||
def get_download_url(self, formdata):
|
||||
return '%sdownload?f=%s' % (formdata.get_url(), self.id)
|
||||
def get_download_url(self, formdata, **kwargs):
|
||||
return '%sdownload?%s' % (formdata.get_url(), self.get_download_query_string(**kwargs))
|
||||
|
||||
def get_opendocument_node_value(self, value, formdata=None, **kwargs):
|
||||
show_link = True
|
||||
|
@ -1143,7 +1155,7 @@ class FileField(WidgetField):
|
|||
show_link = bool(value.get_redirect_url(backoffice=is_in_backoffice))
|
||||
if show_link and formdata:
|
||||
node = ET.Element('{%s}a' % OD_NS['text'])
|
||||
node.attrib['{%s}href' % OD_NS['xlink']] = self.get_download_url(formdata)
|
||||
node.attrib['{%s}href' % OD_NS['xlink']] = self.get_download_url(formdata, **kwargs)
|
||||
else:
|
||||
node = ET.Element('{%s}span' % OD_NS['text'])
|
||||
node.text = od_clean_text(force_text(value))
|
||||
|
@ -1155,7 +1167,7 @@ class FileField(WidgetField):
|
|||
def get_json_value(self, value, formdata=None, include_file_content=True, **kwargs):
|
||||
out = value.get_json_value(include_file_content=include_file_content)
|
||||
if formdata:
|
||||
out['url'] = self.get_download_url(formdata)
|
||||
out['url'] = self.get_download_url(formdata, **kwargs)
|
||||
out['field_id'] = self.id
|
||||
return out
|
||||
|
||||
|
@ -1323,7 +1335,7 @@ class DateField(WidgetField):
|
|||
value = strftime(misc.date_format(), value)
|
||||
return super().add_to_view_form(form, value=value)
|
||||
|
||||
def get_view_value(self, value):
|
||||
def get_view_value(self, value, **kwargs):
|
||||
try:
|
||||
return strftime(misc.date_format(), value)
|
||||
except TypeError:
|
||||
|
@ -1500,7 +1512,7 @@ class ItemField(WidgetField):
|
|||
|
||||
return data_source.get_display_value(value)
|
||||
|
||||
def get_view_value(self, value, value_id=None):
|
||||
def get_view_value(self, value, value_id=None, **kwargs):
|
||||
value = super(ItemField, self).get_view_value(value)
|
||||
if not (value_id and
|
||||
get_request() and
|
||||
|
@ -2050,7 +2062,7 @@ class TableField(WidgetField):
|
|||
pass
|
||||
return t
|
||||
|
||||
def get_view_value(self, value):
|
||||
def get_view_value(self, value, **kwargs):
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<table><thead><tr><td></td>')
|
||||
for column in self.columns:
|
||||
|
@ -2238,7 +2250,7 @@ class TableRowsField(WidgetField):
|
|||
pass
|
||||
return t
|
||||
|
||||
def get_view_value(self, value):
|
||||
def get_view_value(self, value, **kwargs):
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<table><thead><tr>')
|
||||
for column in self.columns:
|
||||
|
@ -2401,7 +2413,7 @@ class MapField(WidgetField):
|
|||
return (None, False)
|
||||
return ('%(lat)s;%(lon)s' % coords, False)
|
||||
|
||||
def get_view_value(self, value):
|
||||
def get_view_value(self, value, **kwargs):
|
||||
widget = self.widget_class('x%s' % random.random(), value, readonly=True)
|
||||
return widget.render_widget_content()
|
||||
|
||||
|
@ -2471,7 +2483,7 @@ class RankedItemsField(WidgetField):
|
|||
attrs.remove('prefill')
|
||||
return attrs
|
||||
|
||||
def get_view_value(self, value):
|
||||
def get_view_value(self, value, **kwargs):
|
||||
r = TemplateIO(html=True)
|
||||
r += htmltext('<ul>')
|
||||
items = list(value.items())
|
||||
|
@ -2579,7 +2591,7 @@ class PasswordField(WidgetField):
|
|||
title=_('Label for confirmation input'),
|
||||
value=self.confirmation_title)
|
||||
|
||||
def get_view_value(self, value):
|
||||
def get_view_value(self, value, **kwargs):
|
||||
return '●'*8
|
||||
|
||||
def get_csv_value(self, value, **kwargs):
|
||||
|
@ -2591,10 +2603,76 @@ class PasswordField(WidgetField):
|
|||
register_field_class(PasswordField)
|
||||
|
||||
|
||||
class BlockField(WidgetField):
|
||||
key = 'block'
|
||||
widget_class = BlockWidget
|
||||
max_items = 1
|
||||
extra_attributes = ['block', 'max_items', 'add_element_label']
|
||||
add_element_label = ''
|
||||
|
||||
# cache
|
||||
_block = None
|
||||
|
||||
@property
|
||||
def block(self):
|
||||
if self._block:
|
||||
return self._block
|
||||
self._block = BlockDef.get_on_index(self.type[6:], 'slug')
|
||||
return self._block
|
||||
|
||||
def get_type_label(self):
|
||||
return _('Field Block (%s)') % self.block.name
|
||||
|
||||
def fill_admin_form(self, form):
|
||||
super().fill_admin_form(form)
|
||||
form.add(IntWidget, 'max_items', title=_('Maximum number of items'),
|
||||
value=self.max_items)
|
||||
form.add(StringWidget, 'add_element_label', title=_('Label of "Add" button'),
|
||||
value=self.add_element_label)
|
||||
|
||||
def get_admin_attributes(self):
|
||||
return super().get_admin_attributes() + ['max_items', 'add_element_label']
|
||||
|
||||
def store_display_value(self, data, field_id):
|
||||
value = data.get(field_id)
|
||||
parts = []
|
||||
if value and value.get('data'):
|
||||
for subvalue in value.get('data'):
|
||||
parts.append(self.block.get_display_value(subvalue))
|
||||
return ', '.join(parts)
|
||||
|
||||
def get_view_value(self, value, **kwargs):
|
||||
if 'value_id' not in kwargs:
|
||||
# when called from get_rst_view_value()
|
||||
return str(value or '')
|
||||
value = kwargs['value_id']
|
||||
r = TemplateIO(html=True)
|
||||
for i, row_value in enumerate(value['data']):
|
||||
for field in self.block.fields:
|
||||
css_classes = ['field', 'field-type-%s' % field.key]
|
||||
if field.extra_css_class:
|
||||
css_classes.append(field.extra_css_class)
|
||||
r += htmltext('<div class="%s">' % ' '.join(css_classes))
|
||||
r += htmltext('<span class="label">%s</span> ') % field.label
|
||||
sub_value = row_value.get(field.id)
|
||||
if sub_value is None:
|
||||
r += htmltext('<div class="value"><i>%s</i></div>') % _('Not set')
|
||||
else:
|
||||
r += htmltext('<div class="value">')
|
||||
r += field.get_view_value(sub_value, parent_field=self, parent_field_index=i)
|
||||
r += htmltext('</div>')
|
||||
r += htmltext('</div>\n')
|
||||
return r.getvalue()
|
||||
|
||||
|
||||
def get_field_class_by_type(type):
|
||||
for k in field_classes:
|
||||
if k.key == type:
|
||||
return k
|
||||
if type.startswith('block:'):
|
||||
# make sure block type exists (raises KeyError on missing data)
|
||||
BlockDef.get_on_index(type[6:], 'slug')
|
||||
return BlockField
|
||||
raise KeyError()
|
||||
|
||||
|
||||
|
@ -2612,4 +2690,12 @@ def get_field_options(blacklisted_types):
|
|||
else:
|
||||
non_widgets.append((klass.key, _(klass.description), klass.key))
|
||||
options = widgets + [('', '—', '')] + non_widgets
|
||||
if get_publisher().has_site_option('fields-blocks') and (
|
||||
not blacklisted_types or 'blocks' not in blacklisted_types):
|
||||
position = len(options)
|
||||
for blockdef in BlockDef.select(order_by='name'):
|
||||
options.append(('block:%s' % blockdef.slug, blockdef.name, 'block:%s' % blockdef.slug))
|
||||
if len(options) != position:
|
||||
# add separator
|
||||
options.insert(position, ('', '—', ''))
|
||||
return options
|
||||
|
|
|
@ -708,7 +708,8 @@ class FormDef(StorableObject):
|
|||
widget.live_condition_source = True
|
||||
widget.live_condition_fields = live_condition_fields[field.varname]
|
||||
|
||||
def get_field_data(self, field, widget):
|
||||
@classmethod
|
||||
def get_field_data(cls, field, widget):
|
||||
d = {}
|
||||
d[field.id] = widget.parse()
|
||||
if d.get(field.id) is not None and field.convert_value_from_str:
|
||||
|
@ -1654,6 +1655,7 @@ def clean_unused_files(publisher):
|
|||
|
||||
|
||||
def get_formdefs_of_all_kinds():
|
||||
from wcs.blocks import BlockDef
|
||||
from wcs.carddef import CardDef
|
||||
from wcs.wf.form import FormWorkflowStatusItem
|
||||
from wcs.admin.settings import UserFieldsFormDef
|
||||
|
@ -1665,6 +1667,7 @@ def get_formdefs_of_all_kinds():
|
|||
}
|
||||
formdefs = [UserFieldsFormDef()]
|
||||
formdefs += FormDef.select(**kwargs)
|
||||
formdefs += BlockDef.select(**kwargs)
|
||||
formdefs += CardDef.select(**kwargs)
|
||||
for workflow in Workflow.select(**kwargs):
|
||||
for status in workflow.possible_status:
|
||||
|
|
|
@ -49,8 +49,15 @@ class FileDirectory(Directory):
|
|||
self.reference = reference
|
||||
|
||||
def lookup_file_field(self, filename):
|
||||
if self.reference in self.formdata.data:
|
||||
return self.formdata.data[self.reference]
|
||||
try:
|
||||
if '$' in self.reference:
|
||||
# path to block field contents
|
||||
fn2, idx, sub = self.reference.split('$', 2)
|
||||
return self.formdata.data[fn2]['data'][int(idx)][sub]
|
||||
else:
|
||||
return self.formdata.data[self.reference]
|
||||
except (KeyError, ValueError):
|
||||
return None
|
||||
|
||||
def _q_lookup(self, component):
|
||||
if component == 'thumbnail':
|
||||
|
@ -632,11 +639,15 @@ class FormStatusPage(Directory, FormTemplateMixin):
|
|||
self.check_receiver()
|
||||
try:
|
||||
fn = get_request().form['f']
|
||||
f = self.filled.data[fn]
|
||||
if '$' in fn:
|
||||
# path to block field contents
|
||||
fn2, idx, sub = fn.split('$', 2)
|
||||
file = self.filled.data[fn2]['data'][int(idx)][sub]
|
||||
else:
|
||||
file = self.filled.data[fn]
|
||||
except (KeyError, ValueError):
|
||||
raise errors.TraversalError()
|
||||
|
||||
file = self.filled.data[fn]
|
||||
if not hasattr(file, 'content_type'):
|
||||
raise errors.TraversalError()
|
||||
|
||||
|
|
|
@ -1285,6 +1285,9 @@ class FormPage(Directory, FormTemplateMixin):
|
|||
def validating(self, data):
|
||||
get_request().view_name = 'validation'
|
||||
self.html_top(self.formdef.name)
|
||||
# fake a GET request to avoid previous page POST data being carried
|
||||
# over in rendering.
|
||||
get_request().environ['REQUEST_METHOD'] = 'GET'
|
||||
form = self.create_view_form(data)
|
||||
token_widget = form.get_widget(form.TOKEN_NAME)
|
||||
token_widget._parsed = True
|
||||
|
|
|
@ -163,7 +163,8 @@ class WcsPublisher(StubWcsPublisher):
|
|||
def import_zip(self, fd):
|
||||
z = zipfile.ZipFile(fd)
|
||||
results = {'formdefs': 0, 'carddefs': 0, 'workflows': 0, 'categories': 0, 'roles': 0,
|
||||
'settings': 0, 'datasources': 0, 'wscalls': 0, 'mail-templates': 0}
|
||||
'settings': 0, 'datasources': 0, 'wscalls': 0, 'mail-templates': 0,
|
||||
'blockdefs': 0}
|
||||
|
||||
def _decode_list(data):
|
||||
rv = []
|
||||
|
@ -193,7 +194,7 @@ class WcsPublisher(StubWcsPublisher):
|
|||
for f in z.namelist():
|
||||
if '.indexes' in f:
|
||||
continue
|
||||
if os.path.dirname(f) in ('formdefs_xml', 'carddefs_xml', 'workflows_xml'):
|
||||
if os.path.dirname(f) in ('formdefs_xml', 'carddefs_xml', 'workflows_xml', 'blockdefs_xml'):
|
||||
continue
|
||||
path = os.path.join(self.app_dir, f)
|
||||
if not os.path.exists(os.path.dirname(path)):
|
||||
|
@ -223,7 +224,15 @@ class WcsPublisher(StubWcsPublisher):
|
|||
if os.path.split(f)[0] in results:
|
||||
results[os.path.split(f)[0]] += 1
|
||||
|
||||
# second pass, workflows
|
||||
# second pass, fields blocks
|
||||
from wcs.blocks import BlockDef
|
||||
for f in z.namelist():
|
||||
if os.path.dirname(f) == 'blockdefs_xml' and os.path.basename(f):
|
||||
blockdef = BlockDef.import_from_xml(z.open(f), include_id=True)
|
||||
blockdef.store()
|
||||
results['blockdefs'] += 1
|
||||
|
||||
# third pass, workflows
|
||||
from wcs.workflows import Workflow
|
||||
for f in z.namelist():
|
||||
if os.path.dirname(f) == 'workflows_xml' and os.path.basename(f):
|
||||
|
@ -231,7 +240,7 @@ class WcsPublisher(StubWcsPublisher):
|
|||
workflow.store()
|
||||
results['workflows'] += 1
|
||||
|
||||
# third pass, forms and cards
|
||||
# fourth pass, forms and cards
|
||||
from wcs.formdef import FormDef
|
||||
from wcs.carddef import CardDef
|
||||
formdefs = []
|
||||
|
@ -263,6 +272,8 @@ class WcsPublisher(StubWcsPublisher):
|
|||
elif k == 'carddefs':
|
||||
from .carddef import CardDef
|
||||
klass = CardDef
|
||||
elif k == 'blockdefs':
|
||||
klass = BlockDef
|
||||
elif k == 'categories':
|
||||
from .categories import Category
|
||||
klass = Category
|
||||
|
|
30
wcs/sql.py
30
wcs/sql.py
|
@ -14,6 +14,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import copy
|
||||
import psycopg2
|
||||
import psycopg2.extensions
|
||||
import psycopg2.extras
|
||||
|
@ -35,6 +36,8 @@ from wcs.qommon import force_str, PICKLE_KWARGS
|
|||
from .qommon.storage import _take, deep_bytes2str, parse_clause as parse_storage_clause
|
||||
from .qommon.substitution import invalidate_substitution_cache
|
||||
from .qommon import get_cfg
|
||||
from .qommon.upload_storage import PicklableUpload
|
||||
from .qommon.misc import strftime
|
||||
from .publisher import UnpicklerClass
|
||||
|
||||
import wcs.categories
|
||||
|
@ -69,6 +72,8 @@ SQL_TYPE_MAPPING = {
|
|||
# mapping of dicts
|
||||
'ranked-items': 'text[][]',
|
||||
'password': 'text[][]',
|
||||
# field block
|
||||
'block': 'jsonb',
|
||||
}
|
||||
|
||||
|
||||
|
@ -1256,6 +1261,18 @@ class SqlMixin(object):
|
|||
value = datetime.datetime(value.tm_year, value.tm_mon, value.tm_mday)
|
||||
elif sql_type == 'bytea':
|
||||
value = bytearray(pickle.dumps(value, protocol=2))
|
||||
elif sql_type == 'jsonb' and value.get('schema'):
|
||||
# block field, adapt date/field values
|
||||
value = copy.deepcopy(value)
|
||||
for field_id, field_type in value.get('schema').items():
|
||||
if field_type not in ('date', 'file'):
|
||||
continue
|
||||
for entry in value.get('data') or []:
|
||||
subvalue = entry.get(field_id)
|
||||
if subvalue and field_type == 'date':
|
||||
entry[field_id] = strftime('%Y-%m-%d', subvalue)
|
||||
elif subvalue and field_type == 'file':
|
||||
entry[field_id] = subvalue.__getstate__()
|
||||
elif sql_type == 'boolean':
|
||||
pass
|
||||
sql_dict[get_field_id(field)] = value
|
||||
|
@ -1296,6 +1313,19 @@ class SqlMixin(object):
|
|||
value = value.timetuple()
|
||||
elif sql_type == 'bytea':
|
||||
value = pickle_loads(value)
|
||||
elif sql_type == 'jsonb' and value.get('schema'):
|
||||
# block field, adapt date/field values
|
||||
for field_id, field_type in value.get('schema').items():
|
||||
if field_type not in ('date', 'file'):
|
||||
continue
|
||||
for entry in value.get('data') or []:
|
||||
subvalue = entry.get(field_id)
|
||||
if subvalue and field_type == 'date':
|
||||
entry[field_id] = time.strptime(subvalue, '%Y-%m-%d')
|
||||
elif subvalue and field_type == 'file':
|
||||
entry[field_id] = PicklableUpload.__new__(PicklableUpload)
|
||||
entry[field_id].__setstate__(subvalue)
|
||||
|
||||
obdata[field.id] = value
|
||||
i += 1
|
||||
if field.store_display_value:
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
{% extends "wcs/backoffice/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar-title %}{% trans "Field Blocks" %}{% endblock %}
|
||||
|
||||
{% block appbar-actions %}
|
||||
<a rel="popup" href="import">{% trans "Import" %}</a>
|
||||
<a rel="popup" href="new">{% trans "New field block" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if blocks %}
|
||||
<ul class="objects-list single-links">
|
||||
{% for block in blocks %}
|
||||
<li><a href="{{ block.id }}/">{{ block.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="infonotice">
|
||||
{% trans "There are no field blocks defined." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -461,6 +461,13 @@ class LazyFormDataVar(object):
|
|||
self._varnames[field.varname] = field
|
||||
return self._varnames
|
||||
|
||||
def get_field_kwargs(self, field):
|
||||
return {
|
||||
'data': self._data,
|
||||
'field': field,
|
||||
'formdata': self._formdata
|
||||
}
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
field = self.varnames[key]
|
||||
|
@ -489,18 +496,18 @@ class LazyFormDataVar(object):
|
|||
if str(field.id) not in self._data:
|
||||
raise KeyError(key)
|
||||
|
||||
if field.key == 'date':
|
||||
return LazyFieldVarDate(self._data, field, self._formdata)
|
||||
if field.key == 'map':
|
||||
return LazyFieldVarMap(self._data, field, self._formdata)
|
||||
if field.key == 'password':
|
||||
return LazyFieldVarPassword(self._data, field, self._formdata)
|
||||
if field.key == 'file':
|
||||
return LazyFieldVarFile(self._data, field, self._formdata)
|
||||
klass = LazyFieldVar
|
||||
if field.store_structured_value:
|
||||
return LazyFieldVarStructured(self._data, field, self._formdata)
|
||||
klass = LazyFieldVarStructured
|
||||
klass = { # custom types
|
||||
'date': LazyFieldVarDate,
|
||||
'map': LazyFieldVarMap,
|
||||
'password': LazyFieldVarPassword,
|
||||
'file': LazyFieldVarFile,
|
||||
'block': LazyFieldVarBlock,
|
||||
}.get(field.key, klass)
|
||||
|
||||
return LazyFieldVar(self._data, field, self._formdata)
|
||||
return klass(**self.get_field_kwargs(field))
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
|
@ -510,10 +517,11 @@ class LazyFormDataVar(object):
|
|||
|
||||
|
||||
class LazyFieldVar(object):
|
||||
def __init__(self, data, field, formdata=None):
|
||||
def __init__(self, data, field, formdata=None, **kwargs):
|
||||
self._data = data
|
||||
self._field = field
|
||||
self._formdata = formdata
|
||||
self._field_kwargs = kwargs
|
||||
|
||||
@property
|
||||
def raw(self):
|
||||
|
@ -521,12 +529,6 @@ class LazyFieldVar(object):
|
|||
return self._data.get(self._field.id)
|
||||
raise AttributeError('raw')
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
if self._field.key != 'file' or not self._formdata:
|
||||
raise AttributeError('url')
|
||||
return '%sdownload?f=%s' % (self._formdata.get_url(), self._field.id)
|
||||
|
||||
def get_value(self):
|
||||
if self._field.store_display_value:
|
||||
return self._data.get('%s_display' % self._field.id)
|
||||
|
@ -768,7 +770,62 @@ class LazyFieldVarPassword(LazyFieldVar):
|
|||
|
||||
|
||||
class LazyFieldVarFile(LazyFieldVar):
|
||||
pass
|
||||
@property
|
||||
def url(self):
|
||||
return self._field.get_download_url(formdata=self._formdata, **self._field_kwargs)
|
||||
|
||||
|
||||
class LazyBlockDataVar(LazyFormDataVar):
|
||||
def __init__(self, fields, data, formdata=None, parent_field=None, parent_field_index=0):
|
||||
super().__init__(fields, data, formdata=formdata)
|
||||
self.parent_field = parent_field
|
||||
self.parent_field_index = parent_field_index
|
||||
|
||||
def get_field_kwargs(self, field):
|
||||
kwargs = super().get_field_kwargs(field)
|
||||
kwargs['parent_field'] = self.parent_field
|
||||
kwargs['parent_field_index'] = self.parent_field_index
|
||||
return kwargs
|
||||
|
||||
|
||||
class LazyFieldVarBlock(LazyFieldVar):
|
||||
def inspect_keys(self):
|
||||
if self._field.max_items > 1:
|
||||
data = self._formdata.data.get(self._field.id)['data']
|
||||
return [str(x) for x in range(len(data))]
|
||||
else:
|
||||
return ['var']
|
||||
|
||||
def get_value(self):
|
||||
# don't give access to underlying data dictionary.
|
||||
return self._data.get('%s_display' % self._field.id, '---')
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
int(key)
|
||||
except ValueError:
|
||||
return super().__getitem__(key)
|
||||
data = self._formdata.data.get(self._field.id)['data'][int(key)]
|
||||
return LazyBlockDataVar(self._field.block.fields,
|
||||
data,
|
||||
formdata=self._formdata,
|
||||
parent_field=self._field,
|
||||
parent_field_index=int(key),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
data = self._formdata.data.get(self._field.id)['data']
|
||||
return len(data)
|
||||
|
||||
@property
|
||||
def var(self):
|
||||
# alias when there's a single item
|
||||
return self[0]
|
||||
|
||||
def __iter__(self):
|
||||
data = self._formdata.data.get(self._field.id)['data']
|
||||
for i in range(len(data)):
|
||||
yield self[i]
|
||||
|
||||
|
||||
class LazyUser(object):
|
||||
|
|
Loading…
Reference in New Issue