0001-general-add-support-for-blocks-of-fields-8265.patch
tests/conftest.py | ||
---|---|---|
49 | 49 |
return |
50 | 50 | |
51 | 51 | |
52 |
@pytest.fixture |
|
53 |
def blocks_feature(request, pub): |
|
54 |
return site_options(request, pub, 'options', 'fields-blocks', 'true') |
|
55 | ||
56 | ||
52 | 57 |
@pytest.fixture |
53 | 58 |
def emails(): |
54 | 59 |
with EmailsMocking() as mock: |
tests/test_admin_pages.py | ||
---|---|---|
50 | 50 |
from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping |
51 | 51 |
from wcs.formdef import FormDef |
52 | 52 |
from wcs.carddef import CardDef |
53 |
from wcs.blocks import BlockDef |
|
53 | 54 |
from wcs import fields |
54 | 55 | |
55 | 56 |
from utilities import get_app, login, create_temporary_pub, clean_temporary_pub, HttpRequestsMocking |
... | ... | |
5894 | 5895 |
resp = resp.form.submit(name='submit') |
5895 | 5896 |
pq = resp.pyquery.remove_namespaces() |
5896 | 5897 |
assert pq('.error').text() == 'Some destination fields are duplicated' |
5898 | ||
5899 | ||
5900 |
def test_block_new(pub, blocks_feature): |
|
5901 |
create_superuser(pub) |
|
5902 |
create_role() |
|
5903 |
app = login(get_app(pub)) |
|
5904 |
resp = app.get('/backoffice/forms/') |
|
5905 |
resp = resp.click('Fields blocks') |
|
5906 |
resp = resp.click('New field block') |
|
5907 |
resp.form['name'] = 'field block' |
|
5908 |
resp = resp.form.submit() |
|
5909 |
assert resp.location == 'http://example.net/backoffice/forms/blocks/1/' |
|
5910 |
resp = resp.follow() |
|
5911 |
assert '<h2>field block' in resp |
|
5912 |
assert 'There are not yet any fields' in resp |
|
5913 | ||
5914 |
resp.form['label'] = 'foobar' |
|
5915 |
resp.form['type'] = 'string' |
|
5916 |
resp = resp.form.submit() |
|
5917 |
assert resp.location == 'http://example.net/backoffice/forms/blocks/1/' |
|
5918 |
resp = resp.follow() |
|
5919 | ||
5920 |
resp.form['label'] = 'barfoo' |
|
5921 |
resp.form['type'] = 'string' |
|
5922 |
resp = resp.form.submit() |
|
5923 |
assert resp.location == 'http://example.net/backoffice/forms/blocks/1/' |
|
5924 |
resp = resp.follow() |
|
5925 | ||
5926 |
assert len(BlockDef.get(1).fields) == 2 |
|
5927 |
assert str(BlockDef.get(1).fields[0].id) != '1' # don't use integers |
|
5928 | ||
5929 | ||
5930 |
def test_block_options(pub, blocks_feature): |
|
5931 |
create_superuser(pub) |
|
5932 |
create_role() |
|
5933 |
BlockDef.wipe() |
|
5934 |
block = BlockDef() |
|
5935 |
block.name = 'foobar' |
|
5936 |
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')] |
|
5937 |
block.store() |
|
5938 | ||
5939 |
app = login(get_app(pub)) |
|
5940 |
resp = app.get('/backoffice/forms/blocks/%s/' % block.id) |
|
5941 |
resp = resp.click(href=re.compile('^settings$')) |
|
5942 |
assert 'readonly' not in resp.form['slug'].attrs |
|
5943 |
resp.form['name'] = 'foo bar' |
|
5944 |
resp = resp.form.submit('submit') |
|
5945 |
assert BlockDef.get(block.id).name == 'foo bar' |
|
5946 | ||
5947 |
FormDef.wipe() |
|
5948 |
formdef = FormDef() |
|
5949 |
formdef.name = 'form title' |
|
5950 |
formdef.fields = [ |
|
5951 |
fields.BlockField(id='0', label='test', type='block:%s' % block.slug), |
|
5952 |
] |
|
5953 |
formdef.store() |
|
5954 | ||
5955 |
resp = app.get('/backoffice/forms/blocks/%s/' % block.id) |
|
5956 |
resp = resp.click(href=re.compile('^settings$')) |
|
5957 |
assert 'readonly' in resp.form['slug'].attrs |
|
5958 |
resp = resp.form.submit('cancel') |
|
5959 |
resp = resp.follow() |
|
5960 | ||
5961 | ||
5962 |
def test_block_export_import(pub, blocks_feature): |
|
5963 |
create_superuser(pub) |
|
5964 |
create_role() |
|
5965 |
BlockDef.wipe() |
|
5966 |
block = BlockDef() |
|
5967 |
block.name = 'foobar' |
|
5968 |
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')] |
|
5969 |
block.store() |
|
5970 | ||
5971 |
app = login(get_app(pub)) |
|
5972 |
resp = app.get('/backoffice/forms/blocks/%s/' % block.id) |
|
5973 |
resp = resp.click(href=re.compile('^export$')) |
|
5974 |
xml_export = resp.text |
|
5975 | ||
5976 |
resp = app.get('/backoffice/forms/blocks/') |
|
5977 |
resp = resp.click(href='import') |
|
5978 |
resp = resp.form.submit('cancel') # shouldn't block on missing file |
|
5979 |
resp = resp.follow() |
|
5980 | ||
5981 |
resp = resp.click(href='import') |
|
5982 |
resp = resp.form.submit() |
|
5983 |
assert 'ere were errors processing your form.' in resp |
|
5984 | ||
5985 |
resp.form['file'] = Upload('block', xml_export.encode('utf-8')) |
|
5986 |
resp = resp.form.submit() |
|
5987 |
resp = resp.follow() |
|
5988 |
assert BlockDef.count() == 2 |
|
5989 | ||
5990 |
new_blockdef = [x for x in BlockDef.select() if str(x.id) != str(block.id)][0] |
|
5991 |
assert new_blockdef.name == 'Copy of foobar' |
|
5992 |
assert new_blockdef.slug == 'foobar-1' |
|
5993 |
assert len(new_blockdef.fields) == 1 |
|
5994 |
assert new_blockdef.fields[0].id == '123' |
|
5995 | ||
5996 |
resp = app.get('/backoffice/forms/blocks/') |
|
5997 |
resp = resp.click(href='import') |
|
5998 |
resp.form['file'] = Upload('block', xml_export.encode('utf-8')) |
|
5999 |
resp = resp.form.submit() |
|
6000 |
assert 'Copy of foobar (2)' in [x.name for x in BlockDef.select()] |
|
6001 | ||
6002 |
# import invalid content |
|
6003 |
resp = app.get('/backoffice/forms/blocks/') |
|
6004 |
resp = resp.click(href='import') |
|
6005 |
resp.form['file'] = Upload('block', b'whatever') |
|
6006 |
resp = resp.form.submit() |
|
6007 |
assert 'Invalid File' in resp |
|
6008 | ||
6009 | ||
6010 |
def test_block_delete(pub, blocks_feature): |
|
6011 |
create_superuser(pub) |
|
6012 |
create_role() |
|
6013 |
BlockDef.wipe() |
|
6014 |
FormDef.wipe() |
|
6015 |
block = BlockDef() |
|
6016 |
block.name = 'foobar' |
|
6017 |
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')] |
|
6018 |
block.store() |
|
6019 | ||
6020 |
app = login(get_app(pub)) |
|
6021 |
resp = app.get('/backoffice/forms/blocks/%s/' % block.id) |
|
6022 |
resp = resp.click(href=re.compile('^delete$')) |
|
6023 |
assert 'You are about to irrevocably delete this block.' in resp |
|
6024 |
resp = resp.form.submit() |
|
6025 |
resp = resp.follow() |
|
6026 |
assert BlockDef.count() == 0 |
|
6027 | ||
6028 |
# in use |
|
6029 |
BlockDef.wipe() |
|
6030 |
block = BlockDef() |
|
6031 |
block.name = 'foobar' |
|
6032 |
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')] |
|
6033 |
block.store() |
|
6034 | ||
6035 |
FormDef.wipe() |
|
6036 |
formdef = FormDef() |
|
6037 |
formdef.name = 'form title' |
|
6038 |
formdef.fields = [ |
|
6039 |
fields.BlockField(id='0', label='test', type='block:%s' % block.slug), |
|
6040 |
] |
|
6041 |
formdef.store() |
|
6042 |
resp = app.get('/backoffice/forms/blocks/%s/' % block.id) |
|
6043 |
resp = resp.click(href=re.compile('^delete$')) |
|
6044 |
assert 'This block is still used' in resp |
|
6045 | ||
6046 | ||
6047 |
def test_block_edit_duplicate_delete_field(pub, blocks_feature): |
|
6048 |
create_superuser(pub) |
|
6049 |
create_role() |
|
6050 |
BlockDef.wipe() |
|
6051 |
block = BlockDef() |
|
6052 |
block.name = 'foobar' |
|
6053 |
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')] |
|
6054 |
block.store() |
|
6055 | ||
6056 |
app = login(get_app(pub)) |
|
6057 |
resp = app.get('/backoffice/forms/blocks/%s/' % block.id) |
|
6058 |
resp = resp.click(href=re.compile('123/$')) |
|
6059 |
resp.form['required'].checked = False |
|
6060 |
resp.form['varname'] = 'test' |
|
6061 |
resp = resp.form.submit('submit') |
|
6062 |
resp = resp.follow() |
|
6063 |
assert BlockDef.get(block.id).fields[0].required is False |
|
6064 |
assert BlockDef.get(block.id).fields[0].varname == 'test' |
|
6065 | ||
6066 |
resp = resp.click(href=re.compile('123/duplicate$')) |
|
6067 |
resp = resp.follow() |
|
6068 |
assert len(BlockDef.get(block.id).fields) == 2 |
|
6069 | ||
6070 |
resp = resp.click(href='%s/delete' % BlockDef.get(block.id).fields[1].id) |
|
6071 |
resp = resp.form.submit('submit') |
|
6072 |
resp = resp.follow() |
|
6073 |
assert len(BlockDef.get(block.id).fields) == 1 |
|
6074 | ||
6075 | ||
6076 |
def test_block_use_in_formdef(pub, blocks_feature): |
|
6077 |
create_superuser(pub) |
|
6078 |
create_role() |
|
6079 |
FormDef.wipe() |
|
6080 |
BlockDef.wipe() |
|
6081 |
block = BlockDef() |
|
6082 |
block.name = 'foobar' |
|
6083 |
block.fields = [fields.StringField(id='123', required=True, label='Test', type='string')] |
|
6084 |
block.store() |
|
6085 | ||
6086 |
formdef = FormDef() |
|
6087 |
formdef.name = 'form title' |
|
6088 |
formdef.fields = [] |
|
6089 |
formdef.store() |
|
6090 | ||
6091 |
app = login(get_app(pub)) |
|
6092 |
resp = app.get('/backoffice/forms/1/fields/') |
|
6093 |
resp.forms[0]['label'] = 'a block field' |
|
6094 |
resp.forms[0]['type'] = 'block:foobar' |
|
6095 |
resp = resp.forms[0].submit().follow() |
|
6096 |
assert 'a block field' in resp.text |
|
6097 |
resp = resp.click('Edit', href='1/') |
|
6098 |
assert resp.form['max_items'].value == '1' |
tests/test_form_pages.py | ||
---|---|---|
29 | 29 |
from wcs.qommon.emails import docutils |
30 | 30 |
from wcs.qommon.form import UploadedFile |
31 | 31 |
from wcs.qommon.ident.password_accounts import PasswordAccount |
32 |
from wcs.blocks import BlockDef |
|
32 | 33 |
from wcs.carddef import CardDef |
33 | 34 |
from wcs.formdef import FormDef |
34 | 35 |
from wcs.workflows import (Workflow, EditableWorkflowStatusItem, |
... | ... | |
8164 | 8165 |
'1_structured': {'id': '1', 'text': 'un', 'more': 'foo'}, |
8165 | 8166 |
} |
8166 | 8167 |
assert '2020-04-18' in formdata.evolution[0].parts[0].content |
8168 | ||
8169 | ||
8170 |
def test_block_simple(pub, blocks_feature): |
|
8171 |
create_user(pub) |
|
8172 |
FormDef.wipe() |
|
8173 |
BlockDef.wipe() |
|
8174 | ||
8175 |
block = BlockDef() |
|
8176 |
block.name = 'foobar' |
|
8177 |
block.fields = [ |
|
8178 |
fields.StringField(id='123', required=True, label='Test', type='string'), |
|
8179 |
fields.StringField(id='234', required=True, label='Test2', type='string'), |
|
8180 |
] |
|
8181 |
block.store() |
|
8182 | ||
8183 |
formdef = FormDef() |
|
8184 |
formdef.name = 'form title' |
|
8185 |
formdef.fields = [ |
|
8186 |
fields.BlockField(id='1', label='test', type='block:foobar'), |
|
8187 |
] |
|
8188 |
formdef.store() |
|
8189 | ||
8190 |
app = get_app(pub) |
|
8191 |
resp = app.get(formdef.get_url()) |
|
8192 |
resp.form['f1$element0$f123'] = 'foo' |
|
8193 |
resp.form['f1$element0$f234'] = 'bar' |
|
8194 |
resp = resp.form.submit('submit') # -> validation page |
|
8195 |
assert resp.form['f1$element0$f123'].attrs['readonly'] |
|
8196 |
assert resp.form['f1$element0$f123'].value == 'foo' |
|
8197 |
assert resp.form['f1$element0$f234'].attrs['readonly'] |
|
8198 |
assert resp.form['f1$element0$f234'].value == 'bar' |
|
8199 |
resp = resp.form.submit('submit') # -> end page |
|
8200 |
resp = resp.follow() |
|
8201 |
assert '>foo<' in resp |
|
8202 |
assert '>bar<' in resp |
|
8203 | ||
8204 | ||
8205 |
def test_block_required(pub, blocks_feature): |
|
8206 |
create_user(pub) |
|
8207 |
FormDef.wipe() |
|
8208 |
BlockDef.wipe() |
|
8209 | ||
8210 |
block = BlockDef() |
|
8211 |
block.name = 'foobar' |
|
8212 |
block.fields = [ |
|
8213 |
fields.StringField(id='123', required=True, label='Test', type='string'), |
|
8214 |
fields.StringField(id='234', required=True, label='Test2', type='string'), |
|
8215 |
] |
|
8216 |
block.store() |
|
8217 | ||
8218 |
formdef = FormDef() |
|
8219 |
formdef.name = 'form title' |
|
8220 |
formdef.fields = [ |
|
8221 |
fields.BlockField(id='1', label='test', type='block:foobar'), |
|
8222 |
] |
|
8223 |
formdef.store() |
|
8224 | ||
8225 |
app = get_app(pub) |
|
8226 |
resp = app.get(formdef.get_url()) |
|
8227 |
resp = resp.form.submit('submit') # -> error page |
|
8228 |
assert 'There were errors processing the form' in resp |
|
8229 |
assert resp.text.count('required field') == 1 |
|
8230 |
resp.form['f1$element0$f123'] = 'foo' |
|
8231 |
resp.form['f1$element0$f234'] = 'bar' |
|
8232 |
resp = resp.form.submit('submit') # -> validation page |
|
8233 |
assert 'Check values then click submit.' in resp.text |
|
8234 | ||
8235 |
resp = app.get(formdef.get_url()) |
|
8236 |
resp.form['f1$element0$f123'] = 'foo' |
|
8237 |
resp = resp.form.submit('submit') # -> error page |
|
8238 |
assert 'There were errors processing the form' in resp |
|
8239 |
assert resp.text.count('required field') == 1 |
|
8240 |
resp.form['f1$element0$f234'] = 'bar' |
|
8241 |
resp = resp.form.submit('submit') # -> validation page |
|
8242 |
assert 'Check values then click submit.' in resp.text |
|
8243 | ||
8244 |
# only one required |
|
8245 |
block.fields = [ |
|
8246 |
fields.StringField(id='123', required=True, label='Test', type='string'), |
|
8247 |
fields.StringField(id='234', required=False, label='Test2', type='string'), |
|
8248 |
] |
|
8249 |
block.store() |
|
8250 | ||
8251 |
resp = app.get(formdef.get_url()) |
|
8252 |
resp.form['f1$element0$f123'] = 'foo' |
|
8253 |
resp = resp.form.submit('submit') # -> validation page |
|
8254 |
assert 'Check values then click submit.' in resp.text |
|
8255 | ||
8256 |
# none required, but globally required |
|
8257 |
block.fields = [ |
|
8258 |
fields.StringField(id='123', required=False, label='Test', type='string'), |
|
8259 |
fields.StringField(id='234', required=False, label='Test2', type='string'), |
|
8260 |
] |
|
8261 |
block.store() |
|
8262 | ||
8263 |
resp = app.get(formdef.get_url()) |
|
8264 |
resp = resp.form.submit('submit') # -> error page |
|
8265 |
assert 'There were errors processing the form' in resp |
|
8266 |
assert resp.text.count('required field') == 1 |
|
8267 |
resp.form['f1$element0$f234'] = 'bar' |
|
8268 |
resp = resp.form.submit('submit') # -> validation page |
|
8269 |
assert 'Check values then click submit.' in resp.text |
|
8270 | ||
8271 | ||
8272 |
def test_block_date(pub, blocks_feature): |
|
8273 |
create_user(pub) |
|
8274 |
FormDef.wipe() |
|
8275 |
BlockDef.wipe() |
|
8276 | ||
8277 |
block = BlockDef() |
|
8278 |
block.name = 'foobar' |
|
8279 |
block.fields = [ |
|
8280 |
fields.StringField(id='123', required=True, label='Test', type='string'), |
|
8281 |
fields.DateField(id='234', required=True, label='Test2', type='date'), |
|
8282 |
] |
|
8283 |
block.store() |
|
8284 | ||
8285 |
formdef = FormDef() |
|
8286 |
formdef.name = 'form title' |
|
8287 |
formdef.fields = [ |
|
8288 |
fields.BlockField(id='1', label='test', type='block:foobar'), |
|
8289 |
] |
|
8290 |
formdef.store() |
|
8291 | ||
8292 |
app = get_app(pub) |
|
8293 |
resp = app.get(formdef.get_url()) |
|
8294 |
resp.form['f1$element0$f123'] = 'foo' |
|
8295 |
resp.form['f1$element0$f234'] = '2020-06-16' |
|
8296 |
resp = resp.form.submit('submit') # -> validation page |
|
8297 |
assert 'Check values then click submit.' in resp.text |
|
8298 |
resp = resp.form.submit('submit') # -> submit |
|
8299 |
resp = resp.follow() |
|
8300 |
assert '>2020-06-16<' in resp |
|
8301 | ||
8302 | ||
8303 |
def test_block_multipage(pub, blocks_feature): |
|
8304 |
create_user(pub) |
|
8305 |
FormDef.wipe() |
|
8306 |
BlockDef.wipe() |
|
8307 | ||
8308 |
block = BlockDef() |
|
8309 |
block.name = 'foobar' |
|
8310 |
block.fields = [ |
|
8311 |
fields.StringField(id='123', required=True, label='Test', type='string'), |
|
8312 |
fields.StringField(id='234', required=True, label='Test2', type='string'), |
|
8313 |
] |
|
8314 |
block.store() |
|
8315 | ||
8316 |
formdef = FormDef() |
|
8317 |
formdef.name = 'form title' |
|
8318 |
formdef.fields = [ |
|
8319 |
fields.PageField(id='0', label='1st page', type='page'), |
|
8320 |
fields.BlockField(id='1', label='test', type='block:foobar'), |
|
8321 |
fields.PageField(id='2', label='2nd page', type='page'), |
|
8322 |
] |
|
8323 |
formdef.store() |
|
8324 | ||
8325 |
app = get_app(pub) |
|
8326 |
resp = app.get(formdef.get_url()) |
|
8327 |
resp.form['f1$element0$f123'] = 'foo' |
|
8328 |
resp.form['f1$element0$f234'] = 'bar' |
|
8329 |
resp = resp.form.submit('submit') # -> 2nd page |
|
8330 |
resp = resp.form.submit('submit') # -> validation page |
|
8331 |
assert resp.form['f1$element0$f123'].attrs['readonly'] |
|
8332 |
assert resp.form['f1$element0$f123'].value == 'foo' |
|
8333 |
resp = resp.form.submit('previous') # -> 2nd page |
|
8334 |
resp = resp.form.submit('previous') # -> 1st page |
|
8335 |
assert 'readonly' not in resp.form['f1$element0$f123'].attrs |
|
8336 |
assert resp.form['f1$element0$f123'].value == 'foo' |
|
8337 |
resp = resp.form.submit('submit') # -> 2nd page |
|
8338 |
resp = resp.form.submit('submit') # -> validation page |
|
8339 |
resp = resp.form.submit('submit') # -> submit |
|
8340 |
resp = resp.follow() |
|
8341 |
assert '>foo<' in resp |
|
8342 |
assert '>bar<' in resp |
|
8343 | ||
8344 | ||
8345 |
def test_block_repeated(pub, blocks_feature): |
|
8346 |
create_user(pub) |
|
8347 |
FormDef.wipe() |
|
8348 |
BlockDef.wipe() |
|
8349 | ||
8350 |
block = BlockDef() |
|
8351 |
block.name = 'foobar' |
|
8352 |
block.fields = [ |
|
8353 |
fields.StringField(id='123', required=True, label='Test', type='string'), |
|
8354 |
fields.StringField(id='234', required=True, label='Test2', type='string'), |
|
8355 |
] |
|
8356 |
block.store() |
|
8357 | ||
8358 |
formdef = FormDef() |
|
8359 |
formdef.name = 'form title' |
|
8360 |
formdef.fields = [ |
|
8361 |
fields.PageField(id='0', label='1st page', type='page'), |
|
8362 |
fields.BlockField(id='1', label='test', type='block:foobar', max_items=3), |
|
8363 |
fields.PageField(id='2', label='2nd page', type='page'), |
|
8364 |
] |
|
8365 |
formdef.store() |
|
8366 | ||
8367 |
app = get_app(pub) |
|
8368 |
resp = app.get(formdef.get_url()) |
|
8369 |
assert resp.text.count('>Test<') == 1 |
|
8370 |
assert 'Add another' in resp |
|
8371 |
resp = resp.form.submit('f1$add_element') |
|
8372 |
assert resp.text.count('>Test<') == 2 |
|
8373 |
resp = resp.form.submit('f1$add_element') |
|
8374 |
assert resp.text.count('>Test<') == 3 |
|
8375 |
assert 'Add another' not in resp |
|
8376 | ||
8377 |
# fill items (1st and 3rd row) |
|
8378 |
resp.form['f1$element0$f123'] = 'foo' |
|
8379 |
resp.form['f1$element0$f234'] = 'bar' |
|
8380 |
resp.form['f1$element2$f123'] = 'foo2' |
|
8381 |
resp.form['f1$element2$f234'] = 'bar2' |
|
8382 | ||
8383 |
resp = resp.form.submit('submit') # -> 2nd page |
|
8384 |
resp = resp.form.submit('submit') # -> validation page |
|
8385 |
assert 'Check values then click submit.' in resp.text |
|
8386 |
assert resp.form['f1$element0$f123'].value == 'foo' |
|
8387 |
assert resp.form['f1$element0$f234'].value == 'bar' |
|
8388 |
assert resp.form['f1$element1$f123'].value == 'foo2' |
|
8389 |
assert resp.form['f1$element1$f234'].value == 'bar2' |
|
8390 | ||
8391 |
resp = resp.form.submit('previous') # -> 2nd page |
|
8392 |
resp = resp.form.submit('previous') # -> 1st page |
|
8393 |
assert 'readonly' not in resp.form['f1$element0$f123'].attrs |
|
8394 |
assert resp.form['f1$element0$f123'].value == 'foo' |
|
8395 | ||
8396 |
resp = resp.form.submit('submit') # -> 2nd page |
|
8397 |
resp = resp.form.submit('submit') # -> validation page |
|
8398 |
resp = resp.form.submit('submit') # -> submit |
|
8399 |
resp = resp.follow() |
|
8400 |
assert '>foo<' in resp |
|
8401 |
assert '>bar<' in resp |
|
8402 |
assert '>foo2<' in resp |
|
8403 |
assert '>bar2<' in resp |
|
8404 | ||
8405 | ||
8406 |
def test_block_repeated_files(pub, blocks_feature): |
|
8407 |
create_user(pub) |
|
8408 |
FormDef.wipe() |
|
8409 |
BlockDef.wipe() |
|
8410 | ||
8411 |
block = BlockDef() |
|
8412 |
block.name = 'foobar' |
|
8413 |
block.fields = [ |
|
8414 |
fields.StringField(id='123', required=True, label='Test', type='string'), |
|
8415 |
fields.FileField(id='234', required=True, label='Test2', type='file'), |
|
8416 |
] |
|
8417 |
block.store() |
|
8418 | ||
8419 |
formdef = FormDef() |
|
8420 |
formdef.name = 'form title' |
|
8421 |
formdef.fields = [ |
|
8422 |
fields.PageField(id='0', label='1st page', type='page'), |
|
8423 |
fields.BlockField(id='1', label='test', type='block:foobar', max_items=3), |
|
8424 |
fields.PageField(id='2', label='2nd page', type='page'), |
|
8425 |
] |
|
8426 |
formdef.store() |
|
8427 | ||
8428 |
app = get_app(pub) |
|
8429 |
resp = app.get(formdef.get_url()) |
|
8430 |
assert resp.text.count('>Test<') == 1 |
|
8431 |
assert 'Add another' in resp |
|
8432 |
resp = resp.form.submit('f1$add_element') |
|
8433 |
assert resp.text.count('>Test<') == 2 |
|
8434 |
resp = resp.form.submit('f1$add_element') |
|
8435 |
assert resp.text.count('>Test<') == 3 |
|
8436 |
assert 'Add another' not in resp |
|
8437 | ||
8438 |
# fill items (1st and 3rd row) |
|
8439 |
resp.form['f1$element0$f123'] = 'foo' |
|
8440 |
resp.form['f1$element0$f234$file'] = Upload('test1.txt', b'foobar1', 'text/plain') |
|
8441 |
resp.form['f1$element2$f123'] = 'foo2' |
|
8442 |
resp.form['f1$element2$f234$file'] = Upload('test2.txt', b'foobar2', 'text/plain') |
|
8443 | ||
8444 |
resp = resp.form.submit('submit') # -> 2nd page |
|
8445 |
resp = resp.form.submit('submit') # -> validation page |
|
8446 |
assert 'Check values then click submit.' in resp.text |
|
8447 |
assert resp.form['f1$element0$f123'].value == 'foo' |
|
8448 |
assert 'test1.txt' in resp |
|
8449 |
assert resp.form['f1$element1$f123'].value == 'foo2' |
|
8450 |
assert 'test2.txt' in resp |
|
8451 | ||
8452 |
resp = resp.form.submit('previous') # -> 2nd page |
|
8453 |
resp = resp.form.submit('previous') # -> 1st page |
|
8454 |
resp = resp.form.submit('submit') # -> 2nd page |
|
8455 |
resp = resp.form.submit('submit') # -> validation page |
|
8456 |
resp = resp.form.submit('submit') # -> submit |
|
8457 |
resp = resp.follow() |
|
8458 |
assert '>foo<' in resp |
|
8459 |
assert 'test1.txt' in resp |
|
8460 |
assert '>foo2<' in resp |
|
8461 |
assert 'test2.txt' in resp |
|
8462 | ||
8463 | ||
8464 |
def test_block_digest(pub, blocks_feature): |
|
8465 |
create_user(pub) |
|
8466 |
FormDef.wipe() |
|
8467 |
BlockDef.wipe() |
|
8468 | ||
8469 |
block = BlockDef() |
|
8470 |
block.name = 'foobar' |
|
8471 |
block.fields = [ |
|
8472 |
fields.StringField(id='123', required=True, label='Test', |
|
8473 |
type='string', varname='foo'), |
|
8474 |
fields.StringField(id='234', required=True, label='Test2', |
|
8475 |
type='string', varname='bar'), |
|
8476 |
] |
|
8477 |
block.store() |
|
8478 | ||
8479 |
formdef = FormDef() |
|
8480 |
formdef.name = 'form title' |
|
8481 |
formdef.fields = [ |
|
8482 |
fields.BlockField(id='1', label='test', type='block:foobar', max_items=3), |
|
8483 |
] |
|
8484 |
formdef.store() |
|
8485 | ||
8486 |
app = get_app(pub) |
|
8487 |
resp = app.get(formdef.get_url()) |
|
8488 |
resp.form['f1$element0$f123'] = 'foo' |
|
8489 |
resp.form['f1$element0$f234'] = 'bar' |
|
8490 |
resp = resp.form.submit('f1$add_element') |
|
8491 |
resp.form['f1$element1$f123'] = 'foo2' |
|
8492 |
resp.form['f1$element1$f234'] = 'bar2' |
|
8493 | ||
8494 |
resp = resp.form.submit('submit') # -> validation page |
|
8495 |
resp = resp.form.submit('submit') # -> submit |
|
8496 | ||
8497 |
assert formdef.data_class().select()[0].data['1']['data'] == [ |
|
8498 |
{'123': 'foo', '234': 'bar'}, {'123': 'foo2', '234': 'bar2'}] |
|
8499 |
# by default it gets the type of object |
|
8500 |
assert formdef.data_class().select()[0].data['1_display'] == 'foobar, foobar' |
|
8501 | ||
8502 |
# set a digest template |
|
8503 |
formdef.data_class().wipe() |
|
8504 | ||
8505 |
block.digest_template = 'X{{foobar_var_foo}}Y' |
|
8506 |
block.store() |
|
8507 | ||
8508 |
resp = app.get(formdef.get_url()) |
|
8509 |
resp.form['f1$element0$f123'] = 'foo' |
|
8510 |
resp.form['f1$element0$f234'] = 'bar' |
|
8511 |
resp = resp.form.submit('f1$add_element') |
|
8512 |
resp.form['f1$element1$f123'] = 'foo2' |
|
8513 |
resp.form['f1$element1$f234'] = 'bar2' |
|
8514 | ||
8515 |
resp = resp.form.submit('submit') # -> validation page |
|
8516 |
resp = resp.form.submit('submit') # -> submit |
|
8517 |
assert formdef.data_class().select()[0].data['1']['data'] == [ |
|
8518 |
{'123': 'foo', '234': 'bar'}, {'123': 'foo2', '234': 'bar2'}] |
|
8519 |
assert formdef.data_class().select()[0].data['1_display'] == 'XfooY, Xfoo2Y' |
tests/test_formdata.py | ||
---|---|---|
15 | 15 |
from wcs.qommon.form import PicklableUpload |
16 | 16 |
from wcs.qommon.http_request import HTTPRequest |
17 | 17 |
from wcs import fields, formdef |
18 |
from wcs.blocks import BlockDef |
|
18 | 19 |
from wcs.categories import Category |
19 | 20 |
from wcs.conditions import Condition |
20 | 21 |
from wcs.formdef import FormDef |
... | ... | |
2038 | 2039 |
assert str(variables['form'].var.foo) == 'world' |
2039 | 2040 |
assert str(variables['form'].parent['form'].var.foo) == 'hello' |
2040 | 2041 |
assert variables['form'].parent is not None |
2042 | ||
2043 | ||
2044 |
def test_block_variables(pub, blocks_feature): |
|
2045 |
BlockDef.wipe() |
|
2046 |
FormDef.wipe() |
|
2047 | ||
2048 |
block = BlockDef() |
|
2049 |
block.name = 'foobar' |
|
2050 |
block.digest_template = 'X{{foobar_var_foo}}Y' |
|
2051 |
block.fields = [ |
|
2052 |
fields.StringField(id='123', required=True, label='Test', |
|
2053 |
type='string', varname='foo'), |
|
2054 |
fields.StringField(id='234', required=True, label='Test2', |
|
2055 |
type='string', varname='bar'), |
|
2056 |
] |
|
2057 |
block.store() |
|
2058 | ||
2059 |
formdef = FormDef() |
|
2060 |
formdef.name = 'testblock' |
|
2061 |
formdef.fields = [ |
|
2062 |
fields.BlockField(id='1', label='test', type='block:foobar', |
|
2063 |
max_items=3, varname='block'), |
|
2064 |
] |
|
2065 |
formdef.store() |
|
2066 | ||
2067 |
formdata = formdef.data_class()() |
|
2068 |
formdata.just_created() |
|
2069 |
# value from test_block_digest in tests/test_form_pages.py |
|
2070 |
formdata.data = { |
|
2071 |
'1': { |
|
2072 |
'data': [{'123': 'foo', '234': 'bar'}, {'123': 'foo2', '234': 'bar2'}], |
|
2073 |
'schema': {'123': 'string', '234': 'string'} |
|
2074 |
}, |
|
2075 |
'1_display': 'XfooY, Xfoo2Y', |
|
2076 |
} |
|
2077 |
formdata.store() |
|
2078 | ||
2079 |
variables = formdata.get_substitution_variables() |
|
2080 |
assert 'form_var_block' in variables.get_flat_keys() |
|
2081 |
assert 'form_var_block_0' in variables.get_flat_keys() |
|
2082 |
assert 'form_var_block_0_foo' in variables.get_flat_keys() |
|
2083 |
assert 'form_var_block_0_bar' in variables.get_flat_keys() |
|
2084 |
assert 'form_var_block_1' in variables.get_flat_keys() |
|
2085 |
assert 'form_var_block_1_foo' in variables.get_flat_keys() |
|
2086 |
assert 'form_var_block_1_bar' in variables.get_flat_keys() |
|
2087 | ||
2088 |
assert variables.get('form_var_block_0_foo') == 'foo' |
|
2089 |
assert variables.get('form_var_block_1_foo') == 'foo2' |
|
2090 |
assert variables.get('form_var_block_var_foo') == 'foo' # alias to 1st element |
|
2091 | ||
2092 |
pub.substitutions.reset() |
|
2093 |
pub.substitutions.feed(formdata) |
|
2094 | ||
2095 |
context = pub.substitutions.get_context_variables(mode='lazy') |
|
2096 |
tmpl = Template('{{ form_var_block }}') |
|
2097 |
assert tmpl.render(context) == 'XfooY, Xfoo2Y' |
|
2098 | ||
2099 |
tmpl = Template('{% for sub in form_var_block %}{{ sub.foo }} {% endfor %}') |
|
2100 |
assert tmpl.render(context) == 'foo foo2 ' |
wcs/admin/blocks.py | ||
---|---|---|
1 |
# w.c.s. - web application for online forms |
|
2 |
# Copyright (C) 2005-2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or modify |
|
5 |
# it under the terms of the GNU General Public License as published by |
|
6 |
# the Free Software Foundation; either version 2 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import xml.etree.ElementTree as ET |
|
18 | ||
19 |
from quixote import get_response, get_session, redirect |
|
20 |
from quixote.directory import Directory |
|
21 |
from quixote.html import TemplateIO, htmltext |
|
22 | ||
23 |
from wcs.blocks import BlockDef, BlockdefImportError |
|
24 | ||
25 |
from wcs.qommon.form import Form, StringWidget, HtmlWidget, FileWidget |
|
26 |
from wcs.qommon import _, misc, template |
|
27 |
from wcs.qommon.backoffice.menu import html_top |
|
28 | ||
29 |
from wcs.admin.fields import FieldDefPage, FieldsDirectory |
|
30 |
from wcs.admin import utils |
|
31 | ||
32 | ||
33 |
class BlockFieldDefPage(FieldDefPage): |
|
34 |
blacklisted_attributes = ['condition'] |
|
35 | ||
36 |
def redirect_field_anchor(self, field): |
|
37 |
anchor = '#itemId_%s' % field.id if field else '' |
|
38 |
return redirect('../%s' % anchor) |
|
39 | ||
40 | ||
41 |
class BlockDirectory(FieldsDirectory): |
|
42 |
_q_exports = ['', 'update_order', 'new', 'delete', 'export', 'settings'] |
|
43 |
field_def_page_class = BlockFieldDefPage |
|
44 |
blacklisted_types = ['page', 'table', 'table-select', 'tablerows', 'ranked-items', 'blocks'] |
|
45 |
field_var_prefix = '' |
|
46 |
support_import = False |
|
47 | ||
48 |
def __init__(self, section, *args, **kwargs): |
|
49 |
self.section = section |
|
50 |
super().__init__(*args, **kwargs) |
|
51 | ||
52 |
def index_top(self): |
|
53 |
r = TemplateIO(html=True) |
|
54 |
r += htmltext('<div id="appbar">') |
|
55 |
r += htmltext('<h2>%s</h2>') % self.objectdef.name |
|
56 |
r += htmltext('<span class="actions">') |
|
57 |
r += htmltext('<a href="delete" rel="popup">%s</a>') % _('Delete') |
|
58 |
r += htmltext('<a href="export">%s</a>') % _('Export') |
|
59 |
r += htmltext('<a href="settings" rel="popup">%s</a>') % _('Settings') |
|
60 |
r += htmltext('</span>') |
|
61 |
r += htmltext('</div>') |
|
62 |
r += utils.last_modification_block(obj=self.objectdef) |
|
63 |
r += get_session().display_message() |
|
64 | ||
65 |
if not self.objectdef.fields: |
|
66 |
r += htmltext('<div class="infonotice">%s</div>') % _('There are not yet any fields defined.') |
|
67 |
return r.getvalue() |
|
68 | ||
69 |
def index_bottom(self): |
|
70 |
formdefs = list(self.objectdef.get_usage_formdefs()) |
|
71 |
formdefs.sort(key=lambda x: x.name.lower()) |
|
72 |
if not formdefs: |
|
73 |
return |
|
74 |
r = TemplateIO(html=True) |
|
75 |
r += htmltext('<div class="section">') |
|
76 |
r += htmltext('<h3>%s</h3>') % _('Usage') |
|
77 |
r += htmltext('<ul class="objects-list single-links">') |
|
78 |
for formdef in formdefs: |
|
79 |
r += htmltext('<li><a href="%s">' % formdef.get_admin_url()) |
|
80 |
r += htmltext('%s</a></li>') % formdef.name |
|
81 |
r += htmltext('</ul>') |
|
82 |
r += htmltext('</div>') |
|
83 |
return r.getvalue() |
|
84 | ||
85 |
def delete(self): |
|
86 |
form = Form(enctype='multipart/form-data') |
|
87 |
if not self.objectdef.is_used(): |
|
88 |
form.widgets.append( |
|
89 |
HtmlWidget('<p>%s</p>' % _('You are about to irrevocably delete this block.')) |
|
90 |
) |
|
91 |
form.add_submit('delete', _('Submit')) |
|
92 |
else: |
|
93 |
form.widgets.append( |
|
94 |
HtmlWidget('<p>%s</p>' % _('This block is still used, it cannot be deleted.')) |
|
95 |
) |
|
96 |
form.add_submit('cancel', _('Cancel')) |
|
97 |
if form.get_widget('cancel').parse(): |
|
98 |
return redirect('..') |
|
99 |
if not form.is_submitted() or form.has_errors(): |
|
100 |
get_response().breadcrumb.append(('delete', _('Delete'))) |
|
101 |
html_top(self.section, title=_('Delete Block')) |
|
102 |
r = TemplateIO(html=True) |
|
103 |
r += htmltext('<h2>%s %s</h2>') % (_('Deleting Block:'), self.objectdef.name) |
|
104 |
r += form.render() |
|
105 |
return r.getvalue() |
|
106 |
else: |
|
107 |
self.objectdef.remove_self() |
|
108 |
return redirect('..') |
|
109 | ||
110 |
def export(self): |
|
111 |
x = self.objectdef.export_to_xml(include_id=True) |
|
112 |
misc.indent_xml(x) |
|
113 |
response = get_response() |
|
114 |
response.set_content_type('application/x-wcs-form') |
|
115 |
response.set_header('content-disposition', 'attachment; filename=block-%s.wcs' % self.objectdef.slug) |
|
116 |
return '<?xml version="1.0"?>\n' + ET.tostring(x).decode('utf-8') |
|
117 | ||
118 |
def settings(self): |
|
119 |
form = Form() |
|
120 |
form.add(StringWidget, 'name', title=_('Name'), value=self.objectdef.name, size=50) |
|
121 |
form.add( |
|
122 |
StringWidget, |
|
123 |
'slug', |
|
124 |
title=_('Identifier'), |
|
125 |
value=self.objectdef.slug, |
|
126 |
size=50, |
|
127 |
readonly=bool(self.objectdef.is_used()), |
|
128 |
) |
|
129 |
form.add( |
|
130 |
StringWidget, 'digest_template', title=_('Digest'), value=self.objectdef.digest_template, size=50 |
|
131 |
) |
|
132 |
form.add_submit('submit', _('Submit')) |
|
133 |
form.add_submit('cancel', _('Cancel')) |
|
134 | ||
135 |
if form.get_widget('cancel').parse(): |
|
136 |
return redirect('.') |
|
137 | ||
138 |
if form.is_submitted() and not form.has_errors(): |
|
139 |
self.objectdef.name = form.get_widget('name').parse() |
|
140 |
if form.get_widget('slug'): |
|
141 |
self.objectdef.slug = form.get_widget('slug').parse() |
|
142 |
self.objectdef.digest_template = form.get_widget('digest_template').parse() |
|
143 |
self.objectdef.store() |
|
144 |
return redirect('.') |
|
145 | ||
146 |
html_top(self.section, title=_('Settings')) |
|
147 |
r = TemplateIO(html=True) |
|
148 |
r += htmltext('<h2>%s</h2>') % _('Settings') |
|
149 |
r += form.render() |
|
150 |
return r.getvalue() |
|
151 | ||
152 | ||
153 |
class BlocksDirectory(Directory): |
|
154 |
_q_exports = ['', 'new', ('import', 'p_import')] |
|
155 |
do_not_call_in_templates = True |
|
156 | ||
157 |
def __init__(self, section): |
|
158 |
super().__init__() |
|
159 |
self.section = section |
|
160 | ||
161 |
def _q_traverse(self, path): |
|
162 |
get_response().breadcrumb.append(('blocks/', _('Fields Blocks'))) |
|
163 |
return super()._q_traverse(path) |
|
164 | ||
165 |
def _q_lookup(self, component): |
|
166 |
return BlockDirectory(self.section, BlockDef.get(component)) |
|
167 | ||
168 |
def _q_index(self): |
|
169 |
html_top(self.section, title=_('Fields Blocks')) |
|
170 |
return template.QommonTemplateResponse( |
|
171 |
templates=['wcs/backoffice/blocks.html'], |
|
172 |
context={'view': self, 'blocks': BlockDef.select(order_by='name')}, |
|
173 |
) |
|
174 | ||
175 |
def new(self): |
|
176 |
form = Form(enctype='multipart/form-data') |
|
177 |
form.add(StringWidget, 'name', title=_('Name'), required=True, size=50) |
|
178 |
form.add_submit('submit', _('Add')) |
|
179 |
form.add_submit('cancel', _('Cancel')) |
|
180 |
if form.get_widget('cancel').parse(): |
|
181 |
return redirect('.') |
|
182 | ||
183 |
if form.is_submitted() and not form.has_errors(): |
|
184 |
block = BlockDef(name=form.get_widget('name').parse()) |
|
185 |
block.store() |
|
186 |
return redirect('%s/' % block.id) |
|
187 | ||
188 |
get_response().breadcrumb.append(('new', _('New Fields Block'))) |
|
189 |
html_top(self.section, title=_('New Fields Block')) |
|
190 |
r = TemplateIO(html=True) |
|
191 |
r += htmltext('<h2>%s</h2>') % _('New Fields Block') |
|
192 |
r += form.render() |
|
193 |
return r.getvalue() |
|
194 | ||
195 |
def p_import(self): |
|
196 |
form = Form(enctype='multipart/form-data') |
|
197 | ||
198 |
form.add(FileWidget, 'file', title=_('File'), required=True) |
|
199 |
form.add_submit('submit', _('Import Fields Block')) |
|
200 |
form.add_submit('cancel', _('Cancel')) |
|
201 | ||
202 |
if form.get_submit() == 'cancel': |
|
203 |
return redirect('.') |
|
204 | ||
205 |
if form.is_submitted() and not form.has_errors(): |
|
206 |
try: |
|
207 |
return self.import_submit(form) |
|
208 |
except ValueError: |
|
209 |
pass |
|
210 | ||
211 |
get_response().breadcrumb.append(('import', _('Import'))) |
|
212 |
html_top(self.section, title=_('Import Fields Block')) |
|
213 |
r = TemplateIO(html=True) |
|
214 |
r += htmltext('<h2>%s</h2>') % _('Import Fields Block') |
|
215 |
r += htmltext('<p>%s</p>') % _('You can install a new fields block by uploading a file.') |
|
216 |
r += form.render() |
|
217 |
return r.getvalue() |
|
218 | ||
219 |
def import_submit(self, form): |
|
220 |
fp = form.get_widget('file').parse().fp |
|
221 | ||
222 |
error, reason = False, None |
|
223 |
try: |
|
224 |
blockdef = BlockDef.import_from_xml(fp) |
|
225 |
except BlockdefImportError as e: |
|
226 |
error = True |
|
227 |
reason = _(e) % e.msg_args |
|
228 |
except ValueError: |
|
229 |
error = True |
|
230 | ||
231 |
if error: |
|
232 |
if reason: |
|
233 |
msg = _('Invalid File (%s)') % reason |
|
234 |
else: |
|
235 |
msg = _('Invalid File') |
|
236 |
form.set_error('file', msg) |
|
237 |
raise ValueError() |
|
238 | ||
239 |
initial_blockdef_name = blockdef.name |
|
240 |
blockdef_names = [x.name for x in BlockDef.select()] |
|
241 |
copy_no = 1 |
|
242 |
while blockdef.name in blockdef_names: |
|
243 |
if copy_no == 1: |
|
244 |
blockdef.name = _('Copy of %s') % initial_blockdef_name |
|
245 |
else: |
|
246 |
blockdef.name = _('Copy of %(name)s (%(no)d)') % { |
|
247 |
'name': initial_blockdef_name, |
|
248 |
'no': copy_no, |
|
249 |
} |
|
250 |
copy_no += 1 |
|
251 |
blockdef.store() |
|
252 |
get_session().message = ('info', _('This fields block has been successfully imported.')) |
|
253 |
return redirect('%s/' % blockdef.id) |
wcs/admin/fields.py | ||
---|---|---|
174 | 174 |
field_def_page_class = FieldDefPage |
175 | 175 |
blacklisted_types = [] |
176 | 176 |
page_id = None |
177 |
field_var_prefix = '' |
|
177 |
field_var_prefix = '..._'
|
|
178 | 178 | |
179 | 179 |
support_import = True |
180 | 180 |
wcs/admin/forms.py | ||
---|---|---|
45 | 45 |
from wcs.forms.root import qrcode |
46 | 46 | |
47 | 47 |
from . import utils |
48 |
from .blocks import BlocksDirectory |
|
48 | 49 |
from .fields import FieldDefPage, FieldsDirectory |
49 | 50 |
from .categories import CategoriesDirectory |
50 | 51 |
from .data_sources import NamedDataSourcesDirectory |
... | ... | |
1467 | 1468 | |
1468 | 1469 |
class FormsDirectory(AccessControlled, Directory): |
1469 | 1470 |
_q_exports = ['', 'new', ('import', 'p_import'), |
1470 |
'categories', ('data-sources', 'data_sources')] |
|
1471 |
'blocks', 'categories', ('data-sources', 'data_sources')]
|
|
1471 | 1472 | |
1472 | 1473 |
categories = CategoriesDirectory() |
1474 |
blocks = BlocksDirectory(section='forms') |
|
1473 | 1475 |
data_sources = NamedDataSourcesDirectoryInForms() |
1474 | 1476 |
formdef_class = FormDef |
1475 | 1477 |
formdef_page_class = FormDefPage |
... | ... | |
1507 | 1509 |
if has_roles: |
1508 | 1510 |
r += htmltext('<span class="actions">') |
1509 | 1511 |
r += htmltext('<a href="data-sources/">%s</a>') % _('Data sources') |
1512 |
if get_publisher().has_site_option('fields-blocks'): |
|
1513 |
r += htmltext('<a href="blocks/">%s</a>') % _('Fields blocks') |
|
1510 | 1514 |
if get_publisher().get_backoffice_root().is_accessible('categories'): |
1511 | 1515 |
r += htmltext('<a href="categories/">%s</a>') % _('Categories') |
1512 | 1516 |
r += htmltext('<a href="import" rel="popup">%s</a>') % _('Import') |
wcs/admin/settings.py | ||
---|---|---|
52 | 52 |
from wcs.qommon.admin.logger import LoggerDirectory |
53 | 53 |
from wcs.qommon import ident |
54 | 54 | |
55 |
from wcs.blocks import BlockDef |
|
55 | 56 |
from wcs.formdef import FormDef |
56 | 57 |
from wcs.carddef import CardDef |
57 | 58 |
from wcs.workflows import Workflow, WorkflowImportError |
... | ... | |
861 | 862 |
if StudioDirectory.is_visible(): |
862 | 863 |
form.add(CheckboxWidget, 'carddefs', title=_('Card Models'), value=True) |
863 | 864 |
form.add(CheckboxWidget, 'workflows', title = _('Workflows'), value = True) |
865 |
if get_publisher().has_site_option('fields-blocks'): |
|
866 |
form.add(CheckboxWidget, 'blockdefs', title=_('Fields Blocks'), value=True) |
|
864 | 867 |
if not get_cfg('sp', {}).get('idp-manage-roles'): |
865 | 868 |
form.add(CheckboxWidget, 'roles', title = _('Roles'), value = True) |
866 | 869 |
form.add(CheckboxWidget, 'categories', title = _('Categories'), value = True) |
... | ... | |
918 | 921 |
misc.indent_xml(node) |
919 | 922 |
z.writestr(os.path.join('workflows_xml', str(workflow.id)), |
920 | 923 |
b'<?xml version="1.0"?>\n' + ET.tostring(node)) |
924 |
if 'blockdefs' in self.dirs: |
|
925 |
for blockdef in BlockDef.select(): |
|
926 |
node = blockdef.export_to_xml(include_id=True) |
|
927 |
misc.indent_xml(node) |
|
928 |
z.writestr(os.path.join('blockdefs_xml', str(blockdef.id)), |
|
929 |
b'<?xml version="1.0"?>\n' + ET.tostring(node)) |
|
921 | 930 | |
922 | 931 |
if self.settings: |
923 | 932 |
z.write(os.path.join(self.app_dir, 'config.pck'), 'config.pck') |
... | ... | |
934 | 943 | |
935 | 944 |
dirs = [] |
936 | 945 |
for w in ('formdefs', 'carddefs', 'workflows', 'roles', 'categories', |
937 |
'datasources', 'wscalls', 'mail-templates'): |
|
946 |
'datasources', 'wscalls', 'mail-templates', 'blockdefs'):
|
|
938 | 947 |
if form.get_widget(w) and form.get_widget(w).parse(): |
939 | 948 |
dirs.append(w) |
940 | 949 |
if not dirs and not form.get_widget('settings').parse(): |
... | ... | |
1017 | 1026 |
r += htmltext('<li>%d %s</li>') % (results['formdefs'], _('forms')) |
1018 | 1027 |
if results['carddefs']: |
1019 | 1028 |
r += htmltext('<li>%d %s</li>') % (results['carddefs'], _('cards')) |
1029 |
if results['blockdefs']: |
|
1030 |
r += htmltext('<li>%d %s</li>') % (results['blockdefs'], _('fields blocks')) |
|
1020 | 1031 |
if results['workflows']: |
1021 | 1032 |
r += htmltext('<li>%d %s</li>') % (results['workflows'], _('workflows')) |
1022 | 1033 |
if results['roles']: |
wcs/blocks.py | ||
---|---|---|
1 |
# w.c.s. - web application for online forms |
|
2 |
# Copyright (C) 2005-2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or modify |
|
5 |
# it under the terms of the GNU General Public License as published by |
|
6 |
# the Free Software Foundation; either version 2 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
import uuid |
|
18 |
import time |
|
19 |
import xml.etree.ElementTree as ET |
|
20 | ||
21 |
from quixote import get_request |
|
22 | ||
23 |
from .qommon import _, N_, misc |
|
24 |
from .qommon.form import CompositeWidget, WidgetList |
|
25 |
from .qommon.storage import StorableObject |
|
26 |
from .qommon.template import Template |
|
27 | ||
28 |
from . import data_sources |
|
29 |
from . import fields |
|
30 | ||
31 | ||
32 |
class BlockdefImportError(Exception): |
|
33 |
def __init__(self, msg, details=None): |
|
34 |
self.msg = msg |
|
35 |
self.details = details |
|
36 | ||
37 | ||
38 |
class BlockDef(StorableObject): |
|
39 |
_names = 'blockdefs' |
|
40 |
_indexes = ['slug'] |
|
41 |
xml_root_node = 'block' |
|
42 | ||
43 |
name = None |
|
44 |
slug = None |
|
45 |
fields = None |
|
46 |
digest_template = None |
|
47 | ||
48 |
last_modification_time = None |
|
49 |
last_modification_user_id = None |
|
50 | ||
51 |
# declarations for serialization |
|
52 |
TEXT_ATTRIBUTES = ['name', 'slug', 'digest_template'] |
|
53 | ||
54 |
def __init__(self, name=None, **kwargs): |
|
55 |
super().__init__(**kwargs) |
|
56 |
self.name = name |
|
57 |
self.fields = [] |
|
58 | ||
59 |
def store(self): |
|
60 |
if self.slug is None: |
|
61 |
# set slug if it's not yet there |
|
62 |
self.slug = self.get_new_slug() |
|
63 | ||
64 |
self.last_modification_time = time.localtime() |
|
65 |
if get_request() and get_request().user: |
|
66 |
self.last_modification_user_id = str(get_request().user.id) |
|
67 |
else: |
|
68 |
self.last_modification_user_id = None |
|
69 | ||
70 |
super().store() |
|
71 | ||
72 |
def get_new_slug(self): |
|
73 |
new_slug = misc.simplify(self.name, space='_') |
|
74 |
base_new_slug = new_slug |
|
75 |
suffix_no = 0 |
|
76 |
while True: |
|
77 |
try: |
|
78 |
obj = self.get_on_index(new_slug, 'slug', ignore_migration=True) |
|
79 |
except KeyError: |
|
80 |
break |
|
81 |
if obj.id == self.id: |
|
82 |
break |
|
83 |
suffix_no += 1 |
|
84 |
new_slug = '%s-%s' % (base_new_slug, suffix_no) |
|
85 |
return new_slug |
|
86 | ||
87 |
def get_new_field_id(self): |
|
88 |
return 'bf%s' % str(uuid.uuid4()) |
|
89 | ||
90 |
def get_display_value(self, value): |
|
91 |
if not self.digest_template: |
|
92 |
return self.name |
|
93 | ||
94 |
from .qommon.substitution import CompatibilityNamesDict |
|
95 |
from .variables import LazyBlockDataVar |
|
96 | ||
97 |
context = CompatibilityNamesDict({self.slug + '_var': LazyBlockDataVar(self.fields, value)}) |
|
98 |
return Template(self.digest_template, autoescape=False).render(context) |
|
99 | ||
100 |
def export_to_xml(self, include_id=False): |
|
101 |
root = ET.Element(self.xml_root_node) |
|
102 |
if include_id and self.id: |
|
103 |
root.attrib['id'] = str(self.id) |
|
104 |
for text_attribute in list(self.TEXT_ATTRIBUTES): |
|
105 |
if not hasattr(self, text_attribute) or not getattr(self, text_attribute): |
|
106 |
continue |
|
107 |
ET.SubElement(root, text_attribute).text = getattr(self, text_attribute) |
|
108 | ||
109 |
if self.last_modification_time: |
|
110 |
elem = ET.SubElement(root, 'last_modification') |
|
111 |
elem.text = time.strftime('%Y-%m-%d %H:%M:%S', self.last_modification_time) |
|
112 |
if include_id: |
|
113 |
elem.attrib['user_id'] = str(self.last_modification_user_id) |
|
114 | ||
115 |
fields = ET.SubElement(root, 'fields') |
|
116 |
for field in self.fields or []: |
|
117 |
fields.append(field.export_to_xml(charset='utf-8', include_id=True)) |
|
118 | ||
119 |
return root |
|
120 | ||
121 |
@classmethod |
|
122 |
def import_from_xml(cls, fd, include_id=False, check_datasources=True): |
|
123 |
try: |
|
124 |
tree = ET.parse(fd) |
|
125 |
except: |
|
126 |
raise ValueError() |
|
127 |
blockdef = cls.import_from_xml_tree(tree, include_id=include_id) |
|
128 | ||
129 |
if blockdef.slug: |
|
130 |
try: |
|
131 |
cls.get_on_index(blockdef.slug, 'slug', ignore_migration=True) |
|
132 |
except KeyError: |
|
133 |
pass |
|
134 |
else: |
|
135 |
blockdef.slug = blockdef.get_new_slug() |
|
136 | ||
137 |
if check_datasources: |
|
138 |
# check if datasources are defined |
|
139 |
unknown_datasources = set() |
|
140 |
for field in blockdef.fields: |
|
141 |
data_source = getattr(field, 'data_source', None) |
|
142 |
if data_source: |
|
143 |
if isinstance(data_sources.get_object(data_source), data_sources.StubNamedDataSource): |
|
144 |
unknown_datasources.add(data_source.get('type')) |
|
145 |
if unknown_datasources: |
|
146 |
raise BlockdefImportError( |
|
147 |
N_('Unknown datasources'), details=', '.join(sorted(unknown_datasources)) |
|
148 |
) |
|
149 | ||
150 |
return blockdef |
|
151 | ||
152 |
@classmethod |
|
153 |
def import_from_xml_tree(cls, tree, include_id=False): |
|
154 |
charset = 'utf-8' |
|
155 |
blockdef = cls() |
|
156 |
if tree.find('name') is None or not tree.find('name').text: |
|
157 |
raise BlockdefImportError(N_('Missing name')) |
|
158 | ||
159 |
# if the tree we get is actually a ElementTree for real, we get its |
|
160 |
# root element and go on happily. |
|
161 |
if not ET.iselement(tree): |
|
162 |
tree = tree.getroot() |
|
163 | ||
164 |
if tree.tag != cls.xml_root_node: |
|
165 |
raise BlockdefImportError(N_('Unexpected root node')) |
|
166 | ||
167 |
if include_id and tree.attrib.get('id'): |
|
168 |
blockdef.id = tree.attrib.get('id') |
|
169 |
for text_attribute in list(cls.TEXT_ATTRIBUTES): |
|
170 |
value = tree.find(text_attribute) |
|
171 |
if value is None or value.text is None: |
|
172 |
continue |
|
173 |
setattr(blockdef, text_attribute, misc.xml_node_text(value)) |
|
174 | ||
175 |
blockdef.fields = [] |
|
176 |
for i, field in enumerate(tree.find('fields')): |
|
177 |
try: |
|
178 |
field_o = fields.get_field_class_by_type(field.findtext('type'))() |
|
179 |
except KeyError: |
|
180 |
raise BlockdefImportError(N_('Unknown field type'), details=field.findtext('type')) |
|
181 |
field_o.init_with_xml(field, charset, include_id=True) |
|
182 |
blockdef.fields.append(field_o) |
|
183 | ||
184 |
if tree.find('last_modification') is not None: |
|
185 |
node = tree.find('last_modification') |
|
186 |
blockdef.last_modification_time = time.strptime(node.text, '%Y-%m-%d %H:%M:%S') |
|
187 |
if include_id and node.attrib.get('user_id'): |
|
188 |
blockdef.last_modification_user_id = node.attrib.get('user_id') |
|
189 | ||
190 |
return blockdef |
|
191 | ||
192 |
def get_usage_formdefs(self): |
|
193 |
from wcs.formdef import get_formdefs_of_all_kinds |
|
194 | ||
195 |
block_identifier = 'block:%s' % self.slug |
|
196 |
for formdef in get_formdefs_of_all_kinds(): |
|
197 |
for field in formdef.fields: |
|
198 |
if field.type == block_identifier: |
|
199 |
yield formdef |
|
200 |
break |
|
201 | ||
202 |
def is_used(self): |
|
203 |
return any(self.get_usage_formdefs()) |
|
204 | ||
205 | ||
206 |
class BlockSubWidget(CompositeWidget): |
|
207 |
def __init__(self, name, value=None, *args, **kwargs): |
|
208 |
self.block = kwargs.pop('block') |
|
209 |
self.readonly = kwargs.get('readonly') |
|
210 |
super().__init__(name, value, *args, **kwargs) |
|
211 |
for field in self.block.fields: |
|
212 |
if 'readonly' in kwargs: |
|
213 |
field.add_to_view_form(form=self) |
|
214 |
else: |
|
215 |
field.add_to_form(form=self) |
|
216 |
if value: |
|
217 |
self.set_value(value) |
|
218 | ||
219 |
def set_value(self, value): |
|
220 |
for widget in self.get_widgets(): |
|
221 |
widget.set_value(value.get(widget.field.id)) |
|
222 | ||
223 |
def get_field_data(self, field, widget): |
|
224 |
from wcs.formdef import FormDef |
|
225 | ||
226 |
return FormDef.get_field_data(field, widget) |
|
227 | ||
228 |
def _parse(self, request): |
|
229 |
value = {} |
|
230 |
empty = True |
|
231 |
for widget in self.get_widgets(): |
|
232 |
widget_value = self.get_field_data(widget.field, widget) |
|
233 |
value.update(widget_value) |
|
234 |
if widget_value.get(widget.field.id) is not None: |
|
235 |
empty = False |
|
236 |
if empty: |
|
237 |
value = None |
|
238 |
self.value = value |
|
239 | ||
240 |
def add_media(self): |
|
241 |
for widget in self.get_widgets(): |
|
242 |
widget.add_media() |
|
243 | ||
244 | ||
245 |
class BlockWidget(WidgetList): |
|
246 |
def __init__( |
|
247 |
self, name, value=None, title=None, block=None, max_items=None, add_element_label=None, **kwargs |
|
248 |
): |
|
249 |
self.block = block |
|
250 |
self.readonly = kwargs.get('readonly') |
|
251 |
element_values = None |
|
252 |
if value: |
|
253 |
element_values = value.get('data') |
|
254 |
element_kwargs = {'block': self.block, 'render_br': False} |
|
255 |
element_kwargs.update(kwargs) |
|
256 |
super().__init__( |
|
257 |
name, |
|
258 |
value=element_values, |
|
259 |
title=title, |
|
260 |
max_items=max_items, |
|
261 |
element_type=BlockSubWidget, |
|
262 |
element_kwargs=element_kwargs, |
|
263 |
add_element_label=add_element_label or _('Add another'), |
|
264 |
**kwargs, |
|
265 |
) |
|
266 | ||
267 |
def set_value(self, value): |
|
268 |
super().set_value(value['data'] if value else None) |
|
269 |
self.value = value |
|
270 | ||
271 |
def _parse(self, request): |
|
272 |
# iterate over existing form keys to get actual list of elements. |
|
273 |
# (maybe this could be moved to WidgetList) |
|
274 |
prefix = '%s$element' % self.name |
|
275 |
known_prefixes = {x.split('$', 2)[1] for x in request.form.keys() if x.startswith(prefix)} |
|
276 |
for i in range(len(known_prefixes) - len(self.element_names)): |
|
277 |
self.add_element() |
|
278 |
super()._parse(request) |
|
279 |
if self.value: |
|
280 |
self.value = {'data': self.value} |
|
281 |
# keep "schema" next to data, this allows custom behaviour for |
|
282 |
# date fields (time.struct_time) when writing/reading from |
|
283 |
# database in JSON. |
|
284 |
self.value['schema'] = {x.id: x.key for x in self.block.fields} |
|
285 | ||
286 |
def parse(self, request=None): |
|
287 |
if not self._parsed: |
|
288 |
self._parsed = True |
|
289 |
if request is None: |
|
290 |
request = get_request() |
|
291 |
self._parse(request) |
|
292 |
if self.required and self.value is None: |
|
293 |
self.set_error(_(self.REQUIRED_ERROR)) |
|
294 |
return self.value |
|
295 | ||
296 |
def add_media(self): |
|
297 |
for widget in self.get_widgets(): |
|
298 |
if hasattr(widget, 'add_media'): |
|
299 |
widget.add_media() |
|
300 | ||
301 |
def get_error(self, request=None): |
|
302 |
request = request or get_request() |
|
303 |
if request.get_method() == 'POST': |
|
304 |
self.parse(request=request) |
|
305 |
return self.error |
|
306 | ||
307 |
def has_error(self, request=None): |
|
308 |
if self.get_error(): |
|
309 |
return True |
|
310 |
# we know subwidgets have been parsed |
|
311 |
has_error = False |
|
312 |
for widget in self.widgets: |
|
313 |
if widget.value is None: |
|
314 |
continue |
|
315 |
if widget.has_error(): |
|
316 |
has_error = True |
|
317 |
return has_error |
wcs/fields.py | ||
---|---|---|
46 | 46 |
from . import data_sources |
47 | 47 |
from . import portfolio |
48 | 48 |
from .conditions import Condition |
49 |
from .blocks import BlockDef, BlockWidget |
|
49 | 50 | |
50 | 51 | |
51 | 52 |
class PrefillSelectionWidget(CompositeWidget): |
... | ... | |
499 | 500 |
widget_class = None |
500 | 501 | |
501 | 502 |
def add_to_form(self, form, value = None): |
502 |
kwargs = {'required': self.required} |
|
503 |
kwargs = {'required': self.required, 'render_br': False}
|
|
503 | 504 |
if value: |
504 | 505 |
kwargs['value'] = value |
505 | 506 |
for k in self.extra_attributes: |
... | ... | |
545 | 546 |
value = value, readonly = 'readonly', **kwargs) |
546 | 547 |
widget = form.get_widget(self.field_key) |
547 | 548 |
widget.transfer_form_value(get_request()) |
549 |
widget.field = self |
|
548 | 550 |
if self.extra_css_class: |
549 | 551 |
if hasattr(widget, 'extra_css_class') and widget.extra_css_class: |
550 | 552 |
widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class) |
... | ... | |
601 | 603 |
def get_csv_heading(self): |
602 | 604 |
return [self.label] |
603 | 605 | |
604 |
def get_view_value(self, value): |
|
606 |
def get_view_value(self, value, **kwargs):
|
|
605 | 607 |
return str(value) if value else '' |
606 | 608 | |
607 |
def get_view_short_value(self, value, max_len = 30):
|
|
609 |
def get_view_short_value(self, value, max_len=30, **kwargs):
|
|
608 | 610 |
return self.get_view_value(value) |
609 | 611 | |
610 | 612 |
def get_csv_value(self, element, **kwargs): |
... | ... | |
792 | 794 |
def get_admin_attributes(self): |
793 | 795 |
return WidgetField.get_admin_attributes(self) + ['size', 'validation', 'data_source', 'anonymise'] |
794 | 796 | |
795 |
def get_view_value(self, value): |
|
797 |
def get_view_value(self, value, **kwargs):
|
|
796 | 798 |
value = value or '' |
797 | 799 |
if value.startswith('http://') or value.startswith('https://'): |
798 | 800 |
charset = get_publisher().site_charset |
... | ... | |
869 | 871 |
def convert_value_from_str(self, value): |
870 | 872 |
return value |
871 | 873 | |
872 |
def get_view_value(self, value): |
|
874 |
def get_view_value(self, value, **kwargs):
|
|
873 | 875 |
if self.pre: |
874 | 876 |
return htmltext('<pre>') + value + htmltext('</pre>') |
875 | 877 |
else: |
... | ... | |
904 | 906 |
def convert_value_from_str(self, value): |
905 | 907 |
return value |
906 | 908 | |
907 |
def get_view_value(self, value): |
|
909 |
def get_view_value(self, value, **kwargs):
|
|
908 | 910 |
return htmltext('<a href="mailto:%s">%s</a>') % (value, value) |
909 | 911 | |
910 | 912 |
def get_rst_view_value(self, value, indent=''): |
... | ... | |
937 | 939 |
get_request().form[self.field_key] = 'yes' |
938 | 940 |
self.field_key = 'f%sdisabled' % self.id |
939 | 941 | |
940 |
def get_view_value(self, value): |
|
942 |
def get_view_value(self, value, **kwargs):
|
|
941 | 943 |
if value is True or value == 'True': |
942 | 944 |
return _('Yes') |
943 | 945 |
elif value is False or value == 'False': |
... | ... | |
1113 | 1115 |
return upload |
1114 | 1116 |
raise ValueError('invalid data for file type (%r)' % value) |
1115 | 1117 | |
1116 |
def get_view_short_value(self, value, max_len=30): |
|
1117 |
return self.get_view_value(value, include_image_thumbnail=False) |
|
1118 |
def get_view_short_value(self, value, max_len=30, **kwargs):
|
|
1119 |
return self.get_view_value(value, include_image_thumbnail=False, **kwargs)
|
|
1118 | 1120 | |
1119 |
def get_view_value(self, value, include_image_thumbnail=True): |
|
1121 |
def get_download_query_string(self, **kwargs): |
|
1122 |
if kwargs.get('parent_field'): |
|
1123 |
return 'f=%s$%s$%s' % ( |
|
1124 |
kwargs['parent_field'].id, |
|
1125 |
kwargs['parent_field_index'], |
|
1126 |
self.id) |
|
1127 |
return 'f=%s' % self.id |
|
1128 | ||
1129 |
def get_view_value(self, value, include_image_thumbnail=True, **kwargs): |
|
1120 | 1130 |
show_link = True |
1121 | 1131 |
if value.has_redirect_url(): |
1122 | 1132 |
is_in_backoffice = bool(get_request() and get_request().is_in_backoffice()) |
1123 | 1133 |
show_link = bool(value.get_redirect_url(backoffice=is_in_backoffice)) |
1124 | 1134 |
t = TemplateIO(html=True) |
1125 | 1135 |
t += htmltext('<div class="file-field">') |
1136 |
if show_link or include_image_thumbnail: |
|
1137 |
download_qs = self.get_download_query_string(**kwargs) |
|
1126 | 1138 |
if show_link: |
1127 |
t += htmltext('<a href="[download]?f=%s">') % self.id
|
|
1139 |
t += htmltext('<a href="[download]?%s">') % download_qs
|
|
1128 | 1140 |
if include_image_thumbnail and value.can_thumbnail(): |
1129 |
t += htmltext('<img alt="" src="[download]?f=%s&thumbnail=1"/>') % self.id
|
|
1141 |
t += htmltext('<img alt="" src="[download]?%s&thumbnail=1"/>') % download_qs
|
|
1130 | 1142 |
t += htmltext('<span>%s</span>') % value |
1131 | 1143 |
if show_link: |
1132 | 1144 |
t += htmltext('</a>') |
1133 | 1145 |
t += htmltext('</div>') |
1134 | 1146 |
return t.getvalue() |
1135 | 1147 | |
1136 |
def get_download_url(self, formdata): |
|
1137 |
return '%sdownload?f=%s' % (formdata.get_url(), self.id)
|
|
1148 |
def get_download_url(self, formdata, **kwargs):
|
|
1149 |
return '%sdownload?%s' % (formdata.get_url(), self.get_download_query_string(**kwargs))
|
|
1138 | 1150 | |
1139 | 1151 |
def get_opendocument_node_value(self, value, formdata=None, **kwargs): |
1140 | 1152 |
show_link = True |
... | ... | |
1143 | 1155 |
show_link = bool(value.get_redirect_url(backoffice=is_in_backoffice)) |
1144 | 1156 |
if show_link and formdata: |
1145 | 1157 |
node = ET.Element('{%s}a' % OD_NS['text']) |
1146 |
node.attrib['{%s}href' % OD_NS['xlink']] = self.get_download_url(formdata) |
|
1158 |
node.attrib['{%s}href' % OD_NS['xlink']] = self.get_download_url(formdata, **kwargs)
|
|
1147 | 1159 |
else: |
1148 | 1160 |
node = ET.Element('{%s}span' % OD_NS['text']) |
1149 | 1161 |
node.text = od_clean_text(force_text(value)) |
... | ... | |
1155 | 1167 |
def get_json_value(self, value, formdata=None, include_file_content=True, **kwargs): |
1156 | 1168 |
out = value.get_json_value(include_file_content=include_file_content) |
1157 | 1169 |
if formdata: |
1158 |
out['url'] = self.get_download_url(formdata) |
|
1170 |
out['url'] = self.get_download_url(formdata, **kwargs)
|
|
1159 | 1171 |
out['field_id'] = self.id |
1160 | 1172 |
return out |
1161 | 1173 | |
... | ... | |
1323 | 1335 |
value = strftime(misc.date_format(), value) |
1324 | 1336 |
return super().add_to_view_form(form, value=value) |
1325 | 1337 | |
1326 |
def get_view_value(self, value): |
|
1338 |
def get_view_value(self, value, **kwargs):
|
|
1327 | 1339 |
try: |
1328 | 1340 |
return strftime(misc.date_format(), value) |
1329 | 1341 |
except TypeError: |
... | ... | |
1500 | 1512 | |
1501 | 1513 |
return data_source.get_display_value(value) |
1502 | 1514 | |
1503 |
def get_view_value(self, value, value_id=None): |
|
1515 |
def get_view_value(self, value, value_id=None, **kwargs):
|
|
1504 | 1516 |
value = super(ItemField, self).get_view_value(value) |
1505 | 1517 |
if not (value_id and |
1506 | 1518 |
get_request() and |
... | ... | |
2050 | 2062 |
pass |
2051 | 2063 |
return t |
2052 | 2064 | |
2053 |
def get_view_value(self, value): |
|
2065 |
def get_view_value(self, value, **kwargs):
|
|
2054 | 2066 |
r = TemplateIO(html=True) |
2055 | 2067 |
r += htmltext('<table><thead><tr><td></td>') |
2056 | 2068 |
for column in self.columns: |
... | ... | |
2238 | 2250 |
pass |
2239 | 2251 |
return t |
2240 | 2252 | |
2241 |
def get_view_value(self, value): |
|
2253 |
def get_view_value(self, value, **kwargs):
|
|
2242 | 2254 |
r = TemplateIO(html=True) |
2243 | 2255 |
r += htmltext('<table><thead><tr>') |
2244 | 2256 |
for column in self.columns: |
... | ... | |
2401 | 2413 |
return (None, False) |
2402 | 2414 |
return ('%(lat)s;%(lon)s' % coords, False) |
2403 | 2415 | |
2404 |
def get_view_value(self, value): |
|
2416 |
def get_view_value(self, value, **kwargs):
|
|
2405 | 2417 |
widget = self.widget_class('x%s' % random.random(), value, readonly=True) |
2406 | 2418 |
return widget.render_widget_content() |
2407 | 2419 | |
... | ... | |
2471 | 2483 |
attrs.remove('prefill') |
2472 | 2484 |
return attrs |
2473 | 2485 | |
2474 |
def get_view_value(self, value): |
|
2486 |
def get_view_value(self, value, **kwargs):
|
|
2475 | 2487 |
r = TemplateIO(html=True) |
2476 | 2488 |
r += htmltext('<ul>') |
2477 | 2489 |
items = list(value.items()) |
... | ... | |
2579 | 2591 |
title=_('Label for confirmation input'), |
2580 | 2592 |
value=self.confirmation_title) |
2581 | 2593 | |
2582 |
def get_view_value(self, value): |
|
2594 |
def get_view_value(self, value, **kwargs):
|
|
2583 | 2595 |
return '●'*8 |
2584 | 2596 | |
2585 | 2597 |
def get_csv_value(self, value, **kwargs): |
... | ... | |
2591 | 2603 |
register_field_class(PasswordField) |
2592 | 2604 | |
2593 | 2605 | |
2606 |
class BlockField(WidgetField): |
|
2607 |
key = 'block' |
|
2608 |
widget_class = BlockWidget |
|
2609 |
max_items = 1 |
|
2610 |
extra_attributes = ['block', 'max_items', 'add_element_label'] |
|
2611 |
add_element_label = '' |
|
2612 | ||
2613 |
# cache |
|
2614 |
_block = None |
|
2615 | ||
2616 |
@property |
|
2617 |
def block(self): |
|
2618 |
if self._block: |
|
2619 |
return self._block |
|
2620 |
self._block = BlockDef.get_on_index(self.type[6:], 'slug') |
|
2621 |
return self._block |
|
2622 | ||
2623 |
def get_type_label(self): |
|
2624 |
return _('Field Block (%s)') % self.block.name |
|
2625 | ||
2626 |
def fill_admin_form(self, form): |
|
2627 |
super().fill_admin_form(form) |
|
2628 |
form.add(IntWidget, 'max_items', title=_('Maximum number of items'), |
|
2629 |
value=self.max_items) |
|
2630 |
form.add(StringWidget, 'add_element_label', title=_('Label of "Add" button'), |
|
2631 |
value=self.add_element_label) |
|
2632 | ||
2633 |
def get_admin_attributes(self): |
|
2634 |
return super().get_admin_attributes() + ['max_items', 'add_element_label'] |
|
2635 | ||
2636 |
def store_display_value(self, data, field_id): |
|
2637 |
value = data.get(field_id) |
|
2638 |
parts = [] |
|
2639 |
if value and value.get('data'): |
|
2640 |
for subvalue in value.get('data'): |
|
2641 |
parts.append(self.block.get_display_value(subvalue)) |
|
2642 |
return ', '.join(parts) |
|
2643 | ||
2644 |
def get_view_value(self, value, **kwargs): |
|
2645 |
if 'value_id' not in kwargs: |
|
2646 |
# when called from get_rst_view_value() |
|
2647 |
return str(value or '') |
|
2648 |
value = kwargs['value_id'] |
|
2649 |
r = TemplateIO(html=True) |
|
2650 |
for i, row_value in enumerate(value['data']): |
|
2651 |
for field in self.block.fields: |
|
2652 |
css_classes = ['field', 'field-type-%s' % field.key] |
|
2653 |
if field.extra_css_class: |
|
2654 |
css_classes.append(field.extra_css_class) |
|
2655 |
r += htmltext('<div class="%s">' % ' '.join(css_classes)) |
|
2656 |
r += htmltext('<span class="label">%s</span> ') % field.label |
|
2657 |
sub_value = row_value.get(field.id) |
|
2658 |
if sub_value is None: |
|
2659 |
r += htmltext('<div class="value"><i>%s</i></div>') % _('Not set') |
|
2660 |
else: |
|
2661 |
r += htmltext('<div class="value">') |
|
2662 |
r += field.get_view_value(sub_value, parent_field=self, parent_field_index=i) |
|
2663 |
r += htmltext('</div>') |
|
2664 |
r += htmltext('</div>\n') |
|
2665 |
return r.getvalue() |
|
2666 | ||
2667 | ||
2594 | 2668 |
def get_field_class_by_type(type): |
2595 | 2669 |
for k in field_classes: |
2596 | 2670 |
if k.key == type: |
2597 | 2671 |
return k |
2672 |
if type.startswith('block:'): |
|
2673 |
return BlockField |
|
2598 | 2674 |
raise KeyError() |
2599 | 2675 | |
2600 | 2676 | |
... | ... | |
2612 | 2688 |
else: |
2613 | 2689 |
non_widgets.append((klass.key, _(klass.description), klass.key)) |
2614 | 2690 |
options = widgets + [('', '—', '')] + non_widgets |
2691 |
if get_publisher().has_site_option('fields-blocks') and ( |
|
2692 |
not blacklisted_types or 'blocks' not in blacklisted_types): |
|
2693 |
position = len(options) |
|
2694 |
for blockdef in BlockDef.select(order_by='name'): |
|
2695 |
options.append(('block:%s' % blockdef.slug, blockdef.name, 'block:%s' % blockdef.slug)) |
|
2696 |
if len(options) != position: |
|
2697 |
# add separator |
|
2698 |
options.insert(position, ('', '—', '')) |
|
2615 | 2699 |
return options |
wcs/formdef.py | ||
---|---|---|
708 | 708 |
widget.live_condition_source = True |
709 | 709 |
widget.live_condition_fields = live_condition_fields[field.varname] |
710 | 710 | |
711 |
def get_field_data(self, field, widget): |
|
711 |
@classmethod |
|
712 |
def get_field_data(cls, field, widget): |
|
712 | 713 |
d = {} |
713 | 714 |
d[field.id] = widget.parse() |
714 | 715 |
if d.get(field.id) is not None and field.convert_value_from_str: |
... | ... | |
1654 | 1655 | |
1655 | 1656 | |
1656 | 1657 |
def get_formdefs_of_all_kinds(): |
1658 |
from wcs.blocks import BlockDef |
|
1657 | 1659 |
from wcs.carddef import CardDef |
1658 | 1660 |
from wcs.wf.form import FormWorkflowStatusItem |
1659 | 1661 |
from wcs.admin.settings import UserFieldsFormDef |
... | ... | |
1665 | 1667 |
} |
1666 | 1668 |
formdefs = [UserFieldsFormDef()] |
1667 | 1669 |
formdefs += FormDef.select(**kwargs) |
1670 |
formdefs += BlockDef.select(**kwargs) |
|
1668 | 1671 |
formdefs += CardDef.select(**kwargs) |
1669 | 1672 |
for workflow in Workflow.select(**kwargs): |
1670 | 1673 |
for status in workflow.possible_status: |
wcs/forms/common.py | ||
---|---|---|
49 | 49 |
self.reference = reference |
50 | 50 | |
51 | 51 |
def lookup_file_field(self, filename): |
52 |
if self.reference in self.formdata.data: |
|
53 |
return self.formdata.data[self.reference] |
|
52 |
try: |
|
53 |
if '$' in self.reference: |
|
54 |
fn2, idx, sub = self.reference.split('$', 2) |
|
55 |
return self.formdata.data[fn2]['data'][int(idx)][sub] |
|
56 |
else: |
|
57 |
return self.formdata.data[self.reference] |
|
58 |
except (KeyError, ValueError): |
|
59 |
return None |
|
54 | 60 | |
55 | 61 |
def _q_lookup(self, component): |
56 | 62 |
if component == 'thumbnail': |
... | ... | |
632 | 638 |
self.check_receiver() |
633 | 639 |
try: |
634 | 640 |
fn = get_request().form['f'] |
635 |
f = self.filled.data[fn] |
|
641 |
if '$' in fn: |
|
642 |
fn2, idx, sub = fn.split('$', 2) |
|
643 |
file = self.filled.data[fn2]['data'][int(idx)][sub] |
|
644 |
else: |
|
645 |
file = self.filled.data[fn] |
|
636 | 646 |
except (KeyError, ValueError): |
637 | 647 |
raise errors.TraversalError() |
638 | 648 | |
639 |
file = self.filled.data[fn] |
|
640 | 649 |
if not hasattr(file, 'content_type'): |
641 | 650 |
raise errors.TraversalError() |
642 | 651 |
wcs/forms/root.py | ||
---|---|---|
1285 | 1285 |
def validating(self, data): |
1286 | 1286 |
get_request().view_name = 'validation' |
1287 | 1287 |
self.html_top(self.formdef.name) |
1288 |
# fake a GET request to avoid previous page POST data being carried |
|
1289 |
# over in rendering. |
|
1290 |
get_request().environ['REQUEST_METHOD'] = 'GET' |
|
1288 | 1291 |
form = self.create_view_form(data) |
1289 | 1292 |
token_widget = form.get_widget(form.TOKEN_NAME) |
1290 | 1293 |
token_widget._parsed = True |
wcs/publisher.py | ||
---|---|---|
163 | 163 |
def import_zip(self, fd): |
164 | 164 |
z = zipfile.ZipFile(fd) |
165 | 165 |
results = {'formdefs': 0, 'carddefs': 0, 'workflows': 0, 'categories': 0, 'roles': 0, |
166 |
'settings': 0, 'datasources': 0, 'wscalls': 0, 'mail-templates': 0} |
|
166 |
'settings': 0, 'datasources': 0, 'wscalls': 0, 'mail-templates': 0, |
|
167 |
'blockdefs': 0} |
|
167 | 168 | |
168 | 169 |
def _decode_list(data): |
169 | 170 |
rv = [] |
... | ... | |
193 | 194 |
for f in z.namelist(): |
194 | 195 |
if '.indexes' in f: |
195 | 196 |
continue |
196 |
if os.path.dirname(f) in ('formdefs_xml', 'carddefs_xml', 'workflows_xml'): |
|
197 |
if os.path.dirname(f) in ('formdefs_xml', 'carddefs_xml', 'workflows_xml', 'blockdefs_xml'):
|
|
197 | 198 |
continue |
198 | 199 |
path = os.path.join(self.app_dir, f) |
199 | 200 |
if not os.path.exists(os.path.dirname(path)): |
... | ... | |
223 | 224 |
if os.path.split(f)[0] in results: |
224 | 225 |
results[os.path.split(f)[0]] += 1 |
225 | 226 | |
226 |
# second pass, workflows |
|
227 |
# second pass, fields blocks |
|
228 |
from wcs.blocks import BlockDef |
|
229 |
for f in z.namelist(): |
|
230 |
if os.path.dirname(f) == 'blockdefs_xml' and os.path.basename(f): |
|
231 |
blockdef = BlockDef.import_from_xml(z.open(f), include_id=True) |
|
232 |
blockdef.store() |
|
233 |
results['blockdefs'] += 1 |
|
234 | ||
235 |
# third pass, workflows |
|
227 | 236 |
from wcs.workflows import Workflow |
228 | 237 |
for f in z.namelist(): |
229 | 238 |
if os.path.dirname(f) == 'workflows_xml' and os.path.basename(f): |
... | ... | |
231 | 240 |
workflow.store() |
232 | 241 |
results['workflows'] += 1 |
233 | 242 | |
234 |
# third pass, forms and cards
|
|
243 |
# fourth pass, forms and cards
|
|
235 | 244 |
from wcs.formdef import FormDef |
236 | 245 |
from wcs.carddef import CardDef |
237 | 246 |
formdefs = [] |
... | ... | |
263 | 272 |
elif k == 'carddefs': |
264 | 273 |
from .carddef import CardDef |
265 | 274 |
klass = CardDef |
275 |
elif k == 'blockdefs': |
|
276 |
klass = BlockDef |
|
266 | 277 |
elif k == 'categories': |
267 | 278 |
from .categories import Category |
268 | 279 |
klass = Category |
wcs/sql.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU General Public License |
15 | 15 |
# along with this program; if not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import copy |
|
17 | 18 |
import psycopg2 |
18 | 19 |
import psycopg2.extensions |
19 | 20 |
import psycopg2.extras |
... | ... | |
35 | 36 |
from .qommon.storage import _take, deep_bytes2str, parse_clause as parse_storage_clause |
36 | 37 |
from .qommon.substitution import invalidate_substitution_cache |
37 | 38 |
from .qommon import get_cfg |
39 |
from .qommon.upload_storage import PicklableUpload |
|
40 |
from .qommon.misc import strftime |
|
38 | 41 |
from .publisher import UnpicklerClass |
39 | 42 | |
40 | 43 |
import wcs.categories |
... | ... | |
69 | 72 |
# mapping of dicts |
70 | 73 |
'ranked-items': 'text[][]', |
71 | 74 |
'password': 'text[][]', |
75 |
# field block |
|
76 |
'block': 'jsonb', |
|
72 | 77 |
} |
73 | 78 | |
74 | 79 | |
... | ... | |
1256 | 1261 |
value = datetime.datetime(value.tm_year, value.tm_mon, value.tm_mday) |
1257 | 1262 |
elif sql_type == 'bytea': |
1258 | 1263 |
value = bytearray(pickle.dumps(value, protocol=2)) |
1264 |
elif sql_type == 'jsonb' and value.get('schema'): |
|
1265 |
# block field, adapt date/field values |
|
1266 |
value = copy.deepcopy(value) |
|
1267 |
for field_id, field_type in value.get('schema').items(): |
|
1268 |
if field_type not in ('date', 'file'): |
|
1269 |
continue |
|
1270 |
for entry in value.get('data') or []: |
|
1271 |
subvalue = entry.get(field_id) |
|
1272 |
if subvalue and field_type == 'date': |
|
1273 |
entry[field_id] = strftime('%Y-%m-%d', subvalue) |
|
1274 |
elif subvalue and field_type == 'file': |
|
1275 |
entry[field_id] = subvalue.__getstate__() |
|
1259 | 1276 |
elif sql_type == 'boolean': |
1260 | 1277 |
pass |
1261 | 1278 |
sql_dict[get_field_id(field)] = value |
... | ... | |
1296 | 1313 |
value = value.timetuple() |
1297 | 1314 |
elif sql_type == 'bytea': |
1298 | 1315 |
value = pickle_loads(value) |
1316 |
elif sql_type == 'jsonb' and value.get('schema'): |
|
1317 |
# block field, adapt date/field values |
|
1318 |
for field_id, field_type in value.get('schema').items(): |
|
1319 |
if field_type not in ('date', 'file'): |
|
1320 |
continue |
|
1321 |
for entry in value.get('data') or []: |
|
1322 |
subvalue = entry.get(field_id) |
|
1323 |
if subvalue and field_type == 'date': |
|
1324 |
entry[field_id] = time.strptime(subvalue, '%Y-%m-%d') |
|
1325 |
elif subvalue and field_type == 'file': |
|
1326 |
entry[field_id] = PicklableUpload.__new__(PicklableUpload) |
|
1327 |
entry[field_id].__setstate__(subvalue) |
|
1328 | ||
1299 | 1329 |
obdata[field.id] = value |
1300 | 1330 |
i += 1 |
1301 | 1331 |
if field.store_display_value: |
wcs/templates/wcs/backoffice/blocks.html | ||
---|---|---|
1 |
{% extends "wcs/backoffice/base.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block appbar-title %}{% trans "Field Blocks" %}{% endblock %} |
|
5 | ||
6 |
{% block appbar-actions %} |
|
7 |
<a rel="popup" href="import">{% trans "Import" %}</a> |
|
8 |
<a rel="popup" href="new">{% trans "New field block" %}</a> |
|
9 |
{% endblock %} |
|
10 | ||
11 |
{% block content %} |
|
12 |
{% if blocks %} |
|
13 |
<ul class="objects-list single-links"> |
|
14 |
{% for block in blocks %} |
|
15 |
<li><a href="{{ block.id }}/">{{ block.name }}</a></li> |
|
16 |
{% endfor %} |
|
17 |
</ul> |
|
18 |
{% else %} |
|
19 |
<div class="infonotice"> |
|
20 |
{% trans "There are no field blocks defined." %} |
|
21 |
</div> |
|
22 |
{% endif %} |
|
23 |
{% endblock %} |
wcs/variables.py | ||
---|---|---|
461 | 461 |
self._varnames[field.varname] = field |
462 | 462 |
return self._varnames |
463 | 463 | |
464 |
def get_field_kwargs(self, field): |
|
465 |
return { |
|
466 |
'data': self._data, |
|
467 |
'field': field, |
|
468 |
'formdata': self._formdata |
|
469 |
} |
|
470 | ||
464 | 471 |
def __getitem__(self, key): |
465 | 472 |
try: |
466 | 473 |
field = self.varnames[key] |
... | ... | |
489 | 496 |
if str(field.id) not in self._data: |
490 | 497 |
raise KeyError(key) |
491 | 498 | |
492 |
if field.key == 'date': |
|
493 |
return LazyFieldVarDate(self._data, field, self._formdata) |
|
494 |
if field.key == 'map': |
|
495 |
return LazyFieldVarMap(self._data, field, self._formdata) |
|
496 |
if field.key == 'password': |
|
497 |
return LazyFieldVarPassword(self._data, field, self._formdata) |
|
498 |
if field.key == 'file': |
|
499 |
return LazyFieldVarFile(self._data, field, self._formdata) |
|
499 |
klass = LazyFieldVar |
|
500 | 500 |
if field.store_structured_value: |
501 |
return LazyFieldVarStructured(self._data, field, self._formdata) |
|
501 |
klass = LazyFieldVarStructured |
|
502 |
klass = { # custom types |
|
503 |
'date': LazyFieldVarDate, |
|
504 |
'map': LazyFieldVarMap, |
|
505 |
'password': LazyFieldVarPassword, |
|
506 |
'file': LazyFieldVarFile, |
|
507 |
'block': LazyFieldVarBlock, |
|
508 |
}.get(field.key, klass) |
|
502 | 509 | |
503 |
return LazyFieldVar(self._data, field, self._formdata)
|
|
510 |
return klass(**self.get_field_kwargs(field))
|
|
504 | 511 | |
505 | 512 |
def __getattr__(self, attr): |
506 | 513 |
try: |
... | ... | |
510 | 517 | |
511 | 518 | |
512 | 519 |
class LazyFieldVar(object): |
513 |
def __init__(self, data, field, formdata=None): |
|
520 |
def __init__(self, data, field, formdata=None, **kwargs):
|
|
514 | 521 |
self._data = data |
515 | 522 |
self._field = field |
516 | 523 |
self._formdata = formdata |
524 |
self._field_kwargs = kwargs |
|
517 | 525 | |
518 | 526 |
@property |
519 | 527 |
def raw(self): |
... | ... | |
521 | 529 |
return self._data.get(self._field.id) |
522 | 530 |
raise AttributeError('raw') |
523 | 531 | |
524 |
@property |
|
525 |
def url(self): |
|
526 |
if self._field.key != 'file' or not self._formdata: |
|
527 |
raise AttributeError('url') |
|
528 |
return '%sdownload?f=%s' % (self._formdata.get_url(), self._field.id) |
|
529 | ||
530 | 532 |
def get_value(self): |
531 | 533 |
if self._field.store_display_value: |
532 | 534 |
return self._data.get('%s_display' % self._field.id) |
... | ... | |
764 | 766 | |
765 | 767 | |
766 | 768 |
class LazyFieldVarFile(LazyFieldVar): |
767 |
pass |
|
769 |
@property |
|
770 |
def url(self): |
|
771 |
return self._field.get_download_url(formdata=self._formdata, **self._field_kwargs) |
|
772 | ||
773 | ||
774 |
class LazyBlockDataVar(LazyFormDataVar): |
|
775 |
def __init__(self, fields, data, formdata=None, parent_field=None, parent_field_index=0): |
|
776 |
super().__init__(fields, data, formdata=formdata) |
|
777 |
self.parent_field = parent_field |
|
778 |
self.parent_field_index = parent_field_index |
|
779 | ||
780 |
def get_field_kwargs(self, field): |
|
781 |
kwargs = super().get_field_kwargs(field) |
|
782 |
kwargs['parent_field'] = self.parent_field |
|
783 |
kwargs['parent_field_index'] = self.parent_field_index |
|
784 |
return kwargs |
|
785 | ||
786 | ||
787 |
class LazyFieldVarBlock(LazyFieldVar): |
|
788 |
def inspect_keys(self): |
|
789 |
if self._field.max_items > 1: |
|
790 |
data = self._formdata.data.get(self._field.id)['data'] |
|
791 |
return [str(x) for x in range(len(data))] |
|
792 |
else: |
|
793 |
return ['var'] |
|
794 | ||
795 |
def get_value(self): |
|
796 |
# don't give access to underlying data dictionary. |
|
797 |
return self._data.get('%s_display' % self._field.id, '---') |
|
798 | ||
799 |
def __getitem__(self, key): |
|
800 |
try: |
|
801 |
int(key) |
|
802 |
except ValueError: |
|
803 |
return super().__getitem__(key) |
|
804 |
data = self._formdata.data.get(self._field.id)['data'][int(key)] |
|
805 |
return LazyBlockDataVar(self._field.block.fields, |
|
806 |
data, |
|
807 |
formdata=self._formdata, |
|
808 |
parent_field=self._field, |
|
809 |
parent_field_index=int(key), |
|
810 |
) |
|
811 | ||
812 |
@property |
|
813 |
def var(self): |
|
814 |
# alias when there's a single item |
|
815 |
return self[0] |
|
816 | ||
817 |
def __iter__(self): |
|
818 |
data = self._formdata.data.get(self._field.id)['data'] |
|
819 |
for i in range(len(data)): |
|
820 |
yield self[i] |
|
768 | 821 | |
769 | 822 | |
770 | 823 |
class LazyUser(object): |
771 |
- |