general add support for blocks of fields (#8265)

This commit is contained in:
Frédéric Péters 2020-05-19 15:00:02 +02:00
parent 2c0bba5f71
commit 95c65b6326
17 changed files with 1539 additions and 53 deletions

View File

@ -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:

View File

@ -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'

View File

@ -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'

View File

@ -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'

256
wcs/admin/blocks.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -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']:

319
wcs/blocks.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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 %}

View File

@ -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):