backoffice: add support for custom views (#4507)

This commit is contained in:
Frédéric Péters 2020-03-09 09:35:09 +01:00
parent db6448a5e4
commit 5c04a4402a
11 changed files with 966 additions and 194 deletions

View File

@ -2031,6 +2031,52 @@ def test_api_ods_formdata(pub, local_user):
assert len(ods_sheet.findall('.//{%s}table-row' % ods.NS['table'])) == 311
def test_api_list_formdata_custom_view(pub, local_user):
Role.wipe()
role = Role(name='test')
role.store()
FormDef.wipe()
formdef = FormDef()
formdef.name = 'test'
formdef.workflow_roles = {'_receiver': role.id}
formdef.fields = [fields.StringField(id='0', label='foobar', varname='foobar'),]
formdef.store()
data_class = formdef.data_class()
data_class.wipe()
for i in range(30):
formdata = data_class()
formdata.data = {'0': 'FOO BAR %d' % i}
formdata.user_id = local_user.id
formdata.just_created()
if i % 3 == 0:
formdata.jump_status('new')
else:
formdata.jump_status('finished')
formdata.store()
# add proper role to user
local_user.roles = [role.id]
local_user.store()
# check it now gets the data
resp = get_app(pub).get(sign_uri('/api/forms/test/list', user=local_user))
assert len(resp.json) == 30
custom_view = pub.custom_view_class()
custom_view.title = 'custom view'
custom_view.formdef = formdef
custom_view.columns = {'list': [{'id': '0'}]}
custom_view.filters = {"filter": "done", "filter-status": "on"}
custom_view.visibility = 'any'
custom_view.store()
resp = get_app(pub).get(sign_uri('/api/forms/test/list/custom-view', user=local_user))
assert len(resp.json['data']) == 20
def test_api_global_geojson(pub, local_user):
Role.wipe()
role = Role(name='test')

View File

@ -122,6 +122,7 @@ def create_environment(pub, set_receiver=True):
Workflow.wipe()
Category.wipe()
FormDef.wipe()
pub.custom_view_class.wipe()
formdef = FormDef()
formdef.name = 'form title'
if set_receiver:
@ -415,15 +416,15 @@ def test_backoffice_listing_pagination(pub):
resp = resp.click(re.compile('^2$')) # second page
assert resp.text.count('data-link') == 5
assert resp.form['offset'].value == '5'
assert resp.forms['listing-settings']['offset'].value == '5'
resp = resp.click(re.compile('^3$')) # third page
assert resp.text.count('data-link') == 5
assert resp.form['offset'].value == '10'
assert resp.forms['listing-settings']['offset'].value == '10'
resp = resp.click(re.compile('^4$')) # fourth page
assert resp.text.count('data-link') == 2
assert resp.form['offset'].value == '15'
assert resp.forms['listing-settings']['offset'].value == '15'
with pytest.raises(IndexError): # no fifth page
resp = resp.click(re.compile('^5$'))
@ -437,7 +438,7 @@ def test_backoffice_listing_pagination(pub):
# try an overbound offset
resp = app.get('/backoffice/management/form-title/?limit=5&offset=30')
resp = resp.follow()
assert resp.form['offset'].value == '0'
assert resp.forms['listing-settings']['offset'].value == '0'
def test_backoffice_listing_order(pub):
@ -541,6 +542,12 @@ def test_backoffice_columns(pub):
assert resp.text.count('data-link') == 17 # 17 rows
assert resp.text.count('FOO BAR') == 0 # no field 1 column
# change column order
assert resp.forms['listing-settings']['columns-order'].value == 'id,time,last_update_time,user-label,2,status,1,3,anonymised'
resp.forms['listing-settings']['columns-order'].value = 'user-label,id,time,last_update_time,2,status,1,3,anonymised'
resp = resp.forms['listing-settings'].submit()
assert resp.text.find('<span>User Label</span>') < resp.text.find('<span>Number</span>')
def test_backoffice_channel_column(pub):
if not pub.site_options.has_section('variables'):
@ -571,15 +578,15 @@ def test_backoffice_submission_agent_column(pub):
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
assert not 'submission_agent' in resp.form.fields
assert not 'submission_agent' in resp.forms['listing-settings'].fields
formdef = FormDef.get_by_urlname('form-title')
formdef.backoffice_submission_roles = user.roles
formdef.store()
resp = app.get('/backoffice/management/form-title/')
assert resp.text.count('</th>') == 8 # six columns
resp.form['submission_agent'].checked = True
resp = resp.form.submit()
resp.forms['listing-settings']['submission_agent'].checked = True
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('</th>') == 9 # seven columns
assert resp.text.count('data-link') == 17 # 17 rows
assert not '>agent<' in resp.text
@ -592,13 +599,13 @@ def test_backoffice_submission_agent_column(pub):
formdata.store()
resp = app.get('/backoffice/management/form-title/')
resp.form['submission_agent'].checked = True
resp = resp.form.submit()
resp.forms['listing-settings']['submission_agent'].checked = True
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('>agent<') == 17
resp = resp.click('Export as CSV File')
assert len(resp.text.splitlines()) == 18 # 17 + header line
assert resp.text.count(',agent,') == 17
assert resp.text.count(',agent') == 17
def test_backoffice_image_column(pub):
@ -669,25 +676,25 @@ def test_backoffice_default_filter(pub):
create_environment(pub)
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
assert not 'filter-2-value' in resp.form.fields
assert not 'filter-2-value' in resp.forms['listing-settings'].fields
formdef = FormDef.get_by_urlname('form-title')
formdef.fields[1].in_filters = True
formdef.store()
resp = app.get('/backoffice/management/form-title/')
assert 'filter-2-value' in resp.form.fields
assert 'filter-2-value' in resp.forms['listing-settings'].fields
# same check for items field
formdef.fields.append(
fields.ItemsField(id='4', label='4th field', type='items', items=['foo', 'bar', 'baz']))
formdef.store()
resp = app.get('/backoffice/management/form-title/')
assert not 'filter-4-value' in resp.form.fields
assert not 'filter-4-value' in resp.forms['listing-settings'].fields
formdef.fields[-1].in_filters = True
formdef.store()
resp = app.get('/backoffice/management/form-title/')
assert 'filter-4-value' in resp.form.fields
assert 'filter-4-value' in resp.forms['listing-settings'].fields
def test_backoffice_bool_filter(pub):
@ -705,18 +712,18 @@ def test_backoffice_bool_filter(pub):
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
resp.form['filter-4'].checked = True
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4'].checked = True
resp = resp.forms['listing-settings'].submit()
assert resp.form['filter-4-value'].value == ''
assert resp.forms['listing-settings']['filter-4-value'].value == ''
resp.form['filter-4-value'].value = 'true'
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4-value'].value = 'true'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('<td>Yes</td>') > 0
assert resp.text.count('<td>No</td>') == 0
resp.form['filter-4-value'].value = 'false'
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4-value'].value = 'false'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('<td>Yes</td>') == 0
assert resp.text.count('<td>No</td>') > 0
@ -747,27 +754,27 @@ def test_backoffice_item_filter(pub):
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
resp.form['filter-4'].checked = True
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4'].checked = True
resp = resp.forms['listing-settings'].submit()
assert resp.form['filter-4-value'].value == ''
assert resp.forms['listing-settings']['filter-4-value'].value == ''
resp.form['filter-4-value'].value = 'â'
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4-value'].value = 'â'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count(u'<td>â</td>') > 0
assert resp.text.count(u'<td>b</td>') == 0
assert resp.text.count(u'<td>d</td>') == 0
resp.form['filter-4-value'].value = 'b'
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4-value'].value = 'b'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count(u'<td>â</td>') == 0
assert resp.text.count(u'<td>b</td>') > 0
assert resp.text.count(u'<td>d</td>') == 0
if not pub.is_using_postgresql():
# in pickle all options are always displayed
resp.form['filter-4-value'].value = 'c'
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4-value'].value = 'c'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count(u'<td>â</td>') == 0
assert resp.text.count(u'<td>b</td>') == 0
assert resp.text.count(u'<td>c</td>') == 0
@ -775,7 +782,7 @@ def test_backoffice_item_filter(pub):
else:
# in postgresql, option 'c' is never used so not even listed
with pytest.raises(ValueError):
resp.form['filter-4-value'].value = 'c'
resp.forms['listing-settings']['filter-4-value'].value = 'c'
# check json view used to fill select filters from javascript
resp2 = app.get(resp.request.path + 'filter-options?filter_field_id=4&' + resp.request.query_string)
@ -785,8 +792,8 @@ def test_backoffice_item_filter(pub):
resp2 = app.get(resp.request.path + 'filter-options?filter_field_id=7&' + resp.request.query_string, status=404)
for status in ('all', 'waiting', 'pending', 'done', 'accepted'):
resp.form['filter'] = status
resp = resp.form.submit()
resp.forms['listing-settings']['filter'] = status
resp = resp.forms['listing-settings'].submit()
resp2 = app.get(resp.request.path + 'filter-options?filter_field_id=4&' + resp.request.query_string)
if status == 'accepted':
assert [x['id'] for x in resp2.json['data']] == []
@ -834,34 +841,34 @@ def test_backoffice_item_double_filter(pub):
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
resp.form['filter-4'].checked = True
resp.form['filter-5'].checked = True
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4'].checked = True
resp.forms['listing-settings']['filter-5'].checked = True
resp = resp.forms['listing-settings'].submit()
assert resp.form['filter-4-value'].value == ''
assert resp.form['filter-5-value'].value == ''
assert [x[0] for x in resp.form['filter-4-value'].options] == ['', 'a', 'b']
assert [x[0] for x in resp.form['filter-5-value'].options] == ['', 'A', 'B', 'C']
assert resp.forms['listing-settings']['filter-4-value'].value == ''
assert resp.forms['listing-settings']['filter-5-value'].value == ''
assert [x[0] for x in resp.forms['listing-settings']['filter-4-value'].options] == ['', 'a', 'b']
assert [x[0] for x in resp.forms['listing-settings']['filter-5-value'].options] == ['', 'A', 'B', 'C']
resp.form['filter-4-value'].value = 'a'
resp = resp.form.submit()
assert [x[0] for x in resp.form['filter-4-value'].options] == ['', 'a', 'b']
assert [x[0] for x in resp.form['filter-5-value'].options] == ['', 'A', 'B', 'C']
resp.forms['listing-settings']['filter-4-value'].value = 'a'
resp = resp.forms['listing-settings'].submit()
assert [x[0] for x in resp.forms['listing-settings']['filter-4-value'].options] == ['', 'a', 'b']
assert [x[0] for x in resp.forms['listing-settings']['filter-5-value'].options] == ['', 'A', 'B', 'C']
resp.form['filter-4-value'].value = 'b'
resp = resp.form.submit()
assert [x[0] for x in resp.form['filter-4-value'].options] == ['', 'a', 'b']
assert [x[0] for x in resp.form['filter-5-value'].options] == ['', 'B']
resp.forms['listing-settings']['filter-4-value'].value = 'b'
resp = resp.forms['listing-settings'].submit()
assert [x[0] for x in resp.forms['listing-settings']['filter-4-value'].options] == ['', 'a', 'b']
assert [x[0] for x in resp.forms['listing-settings']['filter-5-value'].options] == ['', 'B']
resp.form['filter-5-value'].value = 'B'
resp = resp.form.submit()
assert [x[0] for x in resp.form['filter-4-value'].options] == ['', 'a', 'b']
assert [x[0] for x in resp.form['filter-5-value'].options] == ['', 'B']
resp.forms['listing-settings']['filter-5-value'].value = 'B'
resp = resp.forms['listing-settings'].submit()
assert [x[0] for x in resp.forms['listing-settings']['filter-4-value'].options] == ['', 'a', 'b']
assert [x[0] for x in resp.forms['listing-settings']['filter-5-value'].options] == ['', 'B']
resp.form['filter-4-value'].value = ''
resp = resp.form.submit()
assert [x[0] for x in resp.form['filter-4-value'].options] == ['', 'a', 'b']
assert [x[0] for x in resp.form['filter-5-value'].options] == ['', 'A', 'B', 'C']
resp.forms['listing-settings']['filter-4-value'].value = ''
resp = resp.forms['listing-settings'].submit()
assert [x[0] for x in resp.forms['listing-settings']['filter-4-value'].options] == ['', 'a', 'b']
assert [x[0] for x in resp.forms['listing-settings']['filter-5-value'].options] == ['', 'A', 'B', 'C']
def test_backoffice_bofield_item_filter(pub):
@ -894,27 +901,27 @@ def test_backoffice_bofield_item_filter(pub):
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
resp.form['filter-bo0-1'].checked = True
resp = resp.form.submit()
resp.forms['listing-settings']['filter-bo0-1'].checked = True
resp = resp.forms['listing-settings'].submit()
assert resp.form['filter-bo0-1-value'].value == ''
assert resp.forms['listing-settings']['filter-bo0-1-value'].value == ''
resp.form['filter-bo0-1-value'].value = 'â'
resp = resp.form.submit()
resp.forms['listing-settings']['filter-bo0-1-value'].value = 'â'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count(u'<td>â</td>') > 0
assert resp.text.count(u'<td>b</td>') == 0
assert resp.text.count(u'<td>d</td>') == 0
resp.form['filter-bo0-1-value'].value = 'b'
resp = resp.form.submit()
resp.forms['listing-settings']['filter-bo0-1-value'].value = 'b'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count(u'<td>â</td>') == 0
assert resp.text.count(u'<td>b</td>') > 0
assert resp.text.count(u'<td>d</td>') == 0
if not pub.is_using_postgresql():
# in pickle all options are always displayed
resp.form['filter-bo0-1-value'].value = 'c'
resp = resp.form.submit()
resp.forms['listing-settings']['filter-bo0-1-value'].value = 'c'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count(u'<td>â</td>') == 0
assert resp.text.count(u'<td>b</td>') == 0
assert resp.text.count(u'<td>c</td>') == 0
@ -922,7 +929,7 @@ def test_backoffice_bofield_item_filter(pub):
else:
# in postgresql, option 'c' is never used so not even listed
with pytest.raises(ValueError):
resp.form['filter-bo0-1-value'].value = 'c'
resp.forms['listing-settings']['filter-bo0-1-value'].value = 'c'
# check json view used to fill select filters from javascript
resp2 = app.get(resp.request.path + 'filter-options?filter_field_id=bo0-1&' + resp.request.query_string)
@ -931,8 +938,8 @@ def test_backoffice_bofield_item_filter(pub):
assert [x['id'] for x in resp2.json['data']] == ['d']
for status in ('all', 'waiting', 'pending', 'done', 'accepted'):
resp.form['filter'] = status
resp = resp.form.submit()
resp.forms['listing-settings']['filter'] = status
resp = resp.forms['listing-settings'].submit()
resp2 = app.get(resp.request.path + 'filter-options?filter_field_id=bo0-1&' + resp.request.query_string)
if status == 'accepted':
assert [x['id'] for x in resp2.json['data']] == []
@ -966,19 +973,19 @@ def test_backoffice_items_filter(pub):
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
resp.form['filter-4'].checked = True
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4'].checked = True
resp = resp.forms['listing-settings'].submit()
assert resp.form['filter-4-value'].value == ''
assert resp.forms['listing-settings']['filter-4-value'].value == ''
resp.form['filter-4-value'].value = 'â'
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4-value'].value = 'â'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count(u'<td>â, b</td>') > 0
assert resp.text.count(u'<td>â</td>') > 0
assert resp.text.count(u'<td>b, d</td>') == 0
resp.form['filter-4-value'].value = 'b'
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4-value'].value = 'b'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count(u'<td>â, b</td>') > 0
assert resp.text.count(u'<td>â</td>') == 0
assert resp.text.count(u'<td>b, d</td>') > 0
@ -986,10 +993,10 @@ def test_backoffice_items_filter(pub):
if pub.is_using_postgresql():
# option 'c' is never used so not even listed
with pytest.raises(ValueError):
resp.form['filter-4-value'].value = 'c'
resp.forms['listing-settings']['filter-4-value'].value = 'c'
else:
resp.form['filter-4-value'].value = 'c'
resp = resp.form.submit()
resp.forms['listing-settings']['filter-4-value'].value = 'c'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count(u'<td>â, b</td>') == 0
assert resp.text.count(u'<td>â</td>') == 0
assert resp.text.count(u'<td>b, d</td>') == 0
@ -1020,39 +1027,39 @@ def test_backoffice_csv(pub):
assert resp.text.splitlines()[1].split(',')[7] == 'aa'
resp = app.get('/backoffice/management/form-title/')
resp.forms[0]['filter'] = 'all'
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter'] = 'all'
resp = resp.forms['listing-settings'].submit()
resp_csv = resp.click('Export as CSV File')
assert len(resp_csv.text.splitlines()) == 51
# test status filter
resp.forms[0]['filter'] = 'pending'
resp.forms[0]['filter-2'].checked = True
resp = resp.forms[0].submit()
resp.forms[0]['filter-2-value'] = 'baz'
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter'] = 'pending'
resp.forms['listing-settings']['filter-2'].checked = True
resp = resp.forms['listing-settings'].submit()
resp.forms['listing-settings']['filter-2-value'] = 'baz'
resp = resp.forms['listing-settings'].submit()
resp_csv = resp.click('Export as CSV File')
assert len(resp_csv.text.splitlines()) == 9
# test criteria filters
resp.forms[0]['filter-start'].checked = True
resp = resp.forms[0].submit()
resp.forms[0]['filter-start-value'] = datetime.datetime(2015, 2, 1).strftime('%Y-%m-%d')
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter-start'].checked = True
resp = resp.forms['listing-settings'].submit()
resp.forms['listing-settings']['filter-start-value'] = datetime.datetime(2015, 2, 1).strftime('%Y-%m-%d')
resp = resp.forms['listing-settings'].submit()
resp_csv = resp.click('Export as CSV File')
assert len(resp_csv.text.splitlines()) == 1
resp.forms[0]['filter-start-value'] = datetime.datetime(2014, 2, 1).strftime('%Y-%m-%d')
resp = resp.forms[0].submit()
resp.forms[0]['filter-2-value'] = 'baz'
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter-start-value'] = datetime.datetime(2014, 2, 1).strftime('%Y-%m-%d')
resp = resp.forms['listing-settings'].submit()
resp.forms['listing-settings']['filter-2-value'] = 'baz'
resp = resp.forms['listing-settings'].submit()
resp_csv = resp.click('Export as CSV File')
assert len(resp_csv.text.splitlines()) == 9
assert 'Created' in resp_csv.text.splitlines()[0]
# test column selection
resp.form['time'].checked = False
resp = resp.forms[0].submit()
resp.forms['listing-settings']['time'].checked = False
resp = resp.forms['listing-settings'].submit()
resp_csv = resp.click('Export as CSV File')
assert 'Created' not in resp_csv.text.splitlines()[0]
@ -1117,11 +1124,11 @@ def test_backoffice_csv_export_channel(pub):
assert 'Channel' not in resp_csv.text.splitlines()[0]
# add submission channel column
resp.form['submission_channel'].checked = True
resp = resp.forms[0].submit()
resp.forms['listing-settings']['submission_channel'].checked = True
resp = resp.forms['listing-settings'].submit()
resp_csv = resp.click('Export as CSV File')
assert resp_csv.text.splitlines()[0].split(',')[1] == 'Channel'
assert resp_csv.text.splitlines()[1].split(',')[1] == 'Web'
assert resp_csv.text.splitlines()[0].split(',')[-1] == 'Channel'
assert resp_csv.text.splitlines()[1].split(',')[-1] == 'Web'
def test_backoffice_csv_export_anonymised(pub):
@ -1137,8 +1144,8 @@ def test_backoffice_csv_export_anonymised(pub):
assert resp_csv.text.splitlines()[0].split(',')[-1] != 'Anonymised'
# add anonymised column
resp.form['anonymised'].checked = True
resp = resp.forms[0].submit()
resp.forms['listing-settings']['anonymised'].checked = True
resp = resp.forms['listing-settings'].submit()
resp_csv = resp.click('Export as CSV File')
assert resp_csv.text.splitlines()[0].split(',')[-1] == 'Anonymised'
assert resp_csv.text.splitlines()[1].split(',')[-1] == 'No'
@ -1265,8 +1272,8 @@ def test_backoffice_statistics(pub):
assert 'To Status &quot;Finished&quot;' in resp.text
assert not '<h2>Filters</h2>' in resp.text
resp.forms[0]['filter-end-value'] = '2013-01-01'
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter-end-value'] = '2013-01-01'
resp = resp.forms['listing-settings'].submit()
assert 'Total number of records: 0' in resp.text
assert '<h2>Filters</h2>' in resp.text
assert 'End: 2013-01-01' in resp.text
@ -1388,36 +1395,36 @@ def test_backoffice_statistics_status_filter(pub):
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
resp = resp.click('Statistics')
assert 'filter' not in resp.forms[0].fields # status is not displayed by default
assert 'filter' not in resp.forms['listing-settings'].fields # status is not displayed by default
assert not '<h2>Filters</h2>' in resp.text
# add 'status' as a filter
resp.forms[0]['filter-status'].checked = True
resp = resp.forms[0].submit()
assert 'filter' in resp.forms[0].fields
resp.forms['listing-settings']['filter-status'].checked = True
resp = resp.forms['listing-settings'].submit()
assert 'filter' in resp.forms['listing-settings'].fields
assert not '<h2>Filters</h2>' in resp.text
assert resp.forms[0]['filter'].value == 'all'
resp.forms[0]['filter'].value = 'pending'
resp = resp.forms[0].submit()
assert resp.forms['listing-settings']['filter'].value == 'all'
resp.forms['listing-settings']['filter'].value = 'pending'
resp = resp.forms['listing-settings'].submit()
assert 'Total number of records: 17' in resp.text
assert '<h2>Filters</h2>' in resp.text
assert 'Status: Pending' in resp.text
resp.forms[0]['filter'].value = 'done'
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter'].value = 'done'
resp = resp.forms['listing-settings'].submit()
assert 'Total number of records: 33' in resp.text
assert '<h2>Filters</h2>' in resp.text
assert 'Status: Done' in resp.text
resp.forms[0]['filter'].value = 'rejected'
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter'].value = 'rejected'
resp = resp.forms['listing-settings'].submit()
assert 'Total number of records: 0' in resp.text
assert '<h2>Filters</h2>' in resp.text
assert 'Status: Rejected' in resp.text
resp.forms[0]['filter'].value = 'all'
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter'].value = 'all'
resp = resp.forms['listing-settings'].submit()
assert 'Total number of records: 50' in resp.text
@ -1429,34 +1436,34 @@ def test_backoffice_statistics_status_select(pub):
resp = resp.click('Statistics')
assert not 'filter-2-value' in resp.form.fields
resp.forms[0]['filter-2'].checked = True
resp = resp.forms[0].submit()
resp.forms[0]['filter-2-value'].value = 'bar'
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter-2'].checked = True
resp = resp.forms['listing-settings'].submit()
resp.forms['listing-settings']['filter-2-value'].value = 'bar'
resp = resp.forms['listing-settings'].submit()
assert 'Total number of records: 13' in resp.text
resp.forms[0]['filter-2-value'].value = 'baz'
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter-2-value'].value = 'baz'
resp = resp.forms['listing-settings'].submit()
assert 'Total number of records: 24' in resp.text
resp.forms[0]['filter-2-value'].value = 'foo'
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter-2-value'].value = 'foo'
resp = resp.forms['listing-settings'].submit()
assert 'Total number of records: 13' in resp.text
assert '<h2>Filters</h2>' in resp.text
assert '2nd field: foo' in resp.text
# check it's also possible to get back to the complete list
resp.forms[0]['filter-2-value'].value = ''
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter-2-value'].value = ''
resp = resp.forms['listing-settings'].submit()
assert 'Total number of records: 50' in resp.text
# check it also works with item fields with a data source
resp = app.get('/backoffice/management/form-title/')
resp = resp.click('Statistics')
resp.forms[0]['filter-3'].checked = True
resp = resp.forms[0].submit()
resp.forms[0]['filter-3-value'].value = 'A'
resp = resp.forms[0].submit()
resp.forms['listing-settings']['filter-3'].checked = True
resp = resp.forms['listing-settings'].submit()
resp.forms['listing-settings']['filter-3-value'].value = 'A'
resp = resp.forms['listing-settings'].submit()
assert 'Total number of records: 13' in resp.text
assert '<h2>Filters</h2>' in resp.text
assert '3rd field: aa' in resp.text
@ -6189,3 +6196,213 @@ def test_backoffice_after_submit_location(pub):
resp.form['comment'] = 'plop'
resp = resp.form.submit('submit')
assert resp.location == 'http://example.net/backoffice/management/form-title/%s/#' % formdata.id
def test_backoffice_custom_view(pub):
create_superuser(pub)
create_environment(pub)
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
assert resp.text.count('<span>User Label</span>') == 1
assert resp.text.count('<tr') == 18
# columns
resp.forms['listing-settings']['user-label'].checked = False
resp = resp.forms['listing-settings'].submit()
# filters
resp.forms[0]['filter-2'].checked = True
resp = resp.forms['listing-settings'].submit()
resp.forms['listing-settings']['filter-2-value'] = 'baz'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('<span>User Label</span>') == 0
assert resp.text.count('<tr') == 9
resp.forms['save-custom-view']['title'] = 'custom test view'
resp = resp.forms['save-custom-view'].submit()
assert resp.location.endswith('/user-custom-test-view/')
resp = resp.follow()
assert resp.text.count('<span>User Label</span>') == 0
assert resp.text.count('<tr') == 9
resp.forms['listing-settings']['filter-2-value'] = 'foo'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('<tr') == 6
assert resp.forms['save-custom-view']['update'].checked is True
resp = resp.forms['save-custom-view'].submit()
assert resp.location.endswith('/user-custom-test-view/')
resp = resp.follow()
assert resp.text.count('<tr') == 6
resp = app.get('/backoffice/management/other-form/')
assert 'custom test view' not in resp
# check it's not possible to create a view without any columns
for field_key in resp.forms['listing-settings'].fields:
if not field_key:
continue
if field_key.startswith('filter'):
continue
if resp.forms['listing-settings'][field_key].attrs.get('type') != 'checkbox':
continue
resp.forms['listing-settings'][field_key].checked = False
resp = resp.forms['listing-settings'].submit()
resp.forms['save-custom-view']['title'] = 'custom test view'
resp = resp.forms['save-custom-view'].submit().follow()
assert 'Views must have at least one column.' in resp.text
def test_backoffice_custom_view_delete(pub):
create_superuser(pub)
create_environment(pub)
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
# columns
resp.forms['listing-settings']['user-label'].checked = False
resp = resp.forms['listing-settings'].submit()
resp.forms['save-custom-view']['title'] = 'custom test view'
resp = resp.forms['save-custom-view'].submit()
assert resp.location.endswith('/user-custom-test-view/')
resp = resp.follow()
resp = resp.click('Delete View')
resp = resp.form.submit()
assert resp.location.endswith('/management/form-title/')
resp = resp.follow()
assert 'custom test view' not in resp.text
def test_backoffice_custom_map_view(pub):
test_backoffice_custom_view(pub)
formdef = FormDef.get_by_urlname('form-title')
formdef.geolocations = {'base': 'Geolocafoobar'}
formdef.store()
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
resp = resp.click('custom test view')
assert resp.text.count('<span>User Label</span>') == 0
assert resp.text.count('<tr') == 6
resp = resp.click('Plot on a Map')
assert resp.forms['listing-settings']['filter-2-value'].value == 'foo'
def test_backoffice_custom_view_reserved_slug(pub):
create_superuser(pub)
create_environment(pub)
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
resp.forms['listing-settings']['user-label'].checked = False
resp = resp.forms['listing-settings'].submit()
resp.forms['save-custom-view']['title'] = 'user custom test view'
resp = resp.forms['save-custom-view'].submit()
# check slug not created with "user" as prefix
assert resp.location.endswith('/user-userx-custom-test-view/')
resp = resp.follow()
def test_backoffice_custom_view_visibility(pub):
create_environment(pub)
create_superuser(pub)
formdef = FormDef.get_by_urlname('form-title')
agent = pub.user_class(name='agent')
agent.roles = [formdef.workflow_roles['_receiver']]
agent.store()
account = PasswordAccount(id='agent')
account.set_password('agent')
account.user_id = agent.id
account.store()
app = login(get_app(pub), username='agent', password='agent')
resp = app.get('/backoffice/management/form-title/')
# columns
resp.forms['listing-settings']['user-label'].checked = False
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('<span>User Label</span>') == 0
resp.forms['save-custom-view']['title'] = 'custom test view'
assert 'visibility' not in resp.forms['save-custom-view'].fields
resp = resp.forms['save-custom-view'].submit()
assert resp.location.endswith('/user-custom-test-view/')
resp = resp.follow()
assert resp.text.count('<span>User Label</span>') == 0
# second agent
agent2 = pub.user_class(name='agent2')
agent2.roles = [formdef.workflow_roles['_receiver']]
agent2.store()
account = PasswordAccount(id='agent2')
account.set_password('agent2')
account.user_id = agent2.id
account.store()
app = login(get_app(pub), username='agent2', password='agent2')
resp = app.get('/backoffice/management/form-title/')
assert 'custom test view' not in resp
# shared custom view
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
resp = resp.forms['listing-settings'].submit()
resp.forms['save-custom-view']['title'] = 'shared view'
resp.forms['save-custom-view']['visibility'] = 'any'
resp = resp.forms['save-custom-view'].submit()
app = login(get_app(pub), username='agent2', password='agent2')
resp = app.get('/backoffice/management/form-title/')
resp = resp.click('shared view')
# don't allow a second "any" view with same slug
app = login(get_app(pub))
resp = app.get('/backoffice/management/form-title/')
resp = resp.forms['listing-settings'].submit()
resp.forms['save-custom-view']['title'] = 'shared view'
resp.forms['save-custom-view']['visibility'] = 'any'
resp = resp.forms['save-custom-view'].submit()
assert set([(x.slug, x.visibility) for x in get_publisher().custom_view_class.select()]) == set(
[('custom-test-view', 'owner'), ('shared-view', 'any'), ('shared-view-2', 'any')])
def test_carddata_custom_view(pub, studio):
CardDef.wipe()
user = create_user(pub)
app = login(get_app(pub))
carddef = CardDef()
carddef.name = 'foo'
carddef.fields = [
fields.StringField(id='1', label='Test', type='string', varname='foo'),
]
carddef.backoffice_submission_roles = user.roles
carddef.workflow_roles = {'_editor': user.roles[0]}
carddef.store()
carddef.data_class().wipe()
for i in range(50):
carddata = carddef.data_class()()
carddata.data = {'1': 'FOO %s' % i}
carddata.just_created()
carddata.store()
resp = app.get('/backoffice/data/foo/')
if pub.is_using_postgresql():
assert resp.text.count('<tr') == 21 # header + rows of data
else:
# no pagination
assert resp.text.count('<tr') == 51 # header + rows of data
resp = resp.forms['listing-settings'].submit()
resp.forms['save-custom-view']['title'] = 'card view'
resp = resp.forms['save-custom-view'].submit()
assert resp.location.endswith('/user-card-view/')
resp = resp.follow()

View File

@ -9,7 +9,7 @@ import shutil
import sys
import threading
from wcs import sql, sessions
from wcs import sql, sessions, custom_views
from webtest import TestApp
from quixote import cleanup, get_publisher
@ -79,11 +79,13 @@ def create_temporary_pub(sql_mode=False, templates_mode=False, lazy_mode=False):
pub.user_class = sql.SqlUser
pub.tracking_code_class = sql.TrackingCode
pub.session_class = sql.Session
pub.custom_view_class = sql.CustomView
pub.is_using_postgresql = lambda: True
else:
pub.user_class = User
pub.tracking_code_class = TrackingCode
pub.session_class = sessions.BasicSession
pub.custom_view_class = custom_views.CustomView
pub.is_using_postgresql = lambda: False
pub.session_manager_class = sessions.StorageSessionManager
@ -165,6 +167,7 @@ def create_temporary_pub(sql_mode=False, templates_mode=False, lazy_mode=False):
sql.do_user_table()
sql.do_tracking_code_table()
sql.do_session_table()
sql.do_custom_views_table()
sql.do_meta_table()
conn.close()

View File

@ -31,6 +31,7 @@ from .qommon import misc
from .qommon.errors import (AccessForbiddenError, QueryError, TraversalError,
UnknownNameIdAccessForbiddenError, RequestError)
from .qommon.form import ComputedExpressionWidget, ConditionWidget
from .qommon.storage import Equal
from wcs.categories import Category
from wcs.conditions import Condition, ValidationError
@ -202,6 +203,15 @@ class ApiFormPage(BackofficeFormPage):
return ApiFormdataPage(self.formdef, formdata)
def _q_traverse(self, path):
if len(path) == 2 and path[0] == 'list':
if path[1] == '':
path = ['list'] # default view, with trailing slash
else:
# custom view
for view in self.get_custom_views([Equal('visibility', 'any'), Equal('slug', path[1])]):
self.view = view
path = ['list']
self.is_webhook = False
if len(path) > 1:
# webhooks have their own access checks, request cannot be blocked

View File

@ -80,15 +80,19 @@ class DataManagementDirectory(ManagementDirectory):
class CardPage(FormPage):
_q_exports = ['', 'csv', 'xls', 'ods', 'json', 'export', 'map', 'geojson', 'add',
('save-view', 'save_view'), ('delete-view', 'delete_view'),
('import-csv', 'import_csv'),
('data-sample-csv', 'data_sample_csv')]
admin_permission = 'cards'
def __init__(self, component):
def __init__(self, component=None, formdef=None, view=None):
try:
self.formdef = CardDef.get_by_urlname(component)
self.formdef = formdef if formdef else CardDef.get_by_urlname(component)
except KeyError:
raise errors.TraversalError()
self.add = CardFillPage(component)
self.add = CardFillPage(self.formdef.url_name)
if view:
self.view = view
def can_user_add_cards(self):
if not self.formdef.backoffice_submission_roles:
@ -104,13 +108,18 @@ class CardPage(FormPage):
return htmltext('<span class="actions"><a href="./add/">%s</a></span>') % _('Add')
def get_default_filters(self, mode):
if self.view:
return self.view.get_default_filters()
return ()
def get_default_columns(self):
field_ids = ['id', 'time']
for field in self.formdef.get_all_fields():
if hasattr(field, 'get_view_value') and field.include_in_listing:
field_ids.append(field.id)
if self.view:
field_ids = self.view.get_columns()
else:
field_ids = ['id', 'time']
for field in self.formdef.get_all_fields():
if hasattr(field, 'get_view_value') and field.include_in_listing:
field_ids.append(field.id)
return field_ids
def get_filter_from_query(self, default=Ellipsis):
@ -270,6 +279,12 @@ class CardPage(FormPage):
return redirect('import-csv?job=%s' % job.id)
def _q_lookup(self, component):
if not self.view:
for view in self.get_custom_views():
if view.get_url_slug() == component:
return self.__class__(formdef=self.formdef, view=view)
try:
filled = self.formdef.data_class().get(component)
except KeyError:

View File

@ -1010,14 +1010,23 @@ class ManagementDirectory(Directory):
class FormPage(Directory):
_q_exports = ['', 'csv', 'stats', 'xls', 'ods', 'json', 'export', 'map',
'geojson', ('filter-options', 'filter_options')]
'geojson', ('filter-options', 'filter_options'),
('save-view', 'save_view'), ('delete-view', 'delete_view'),]
view = None
admin_permission = 'forms'
def __init__(self, component):
try:
self.formdef = FormDef.get_by_urlname(component)
except KeyError:
raise errors.TraversalError()
get_response().breadcrumb.append( (component + '/', self.formdef.name) )
def __init__(self, component=None, formdef=None, view=None):
self.view_type = None
if component:
try:
self.formdef = FormDef.get_by_urlname(component)
except KeyError:
raise errors.TraversalError()
get_response().breadcrumb.append((component + '/', self.formdef.name))
else:
self.formdef = formdef
self.view = view
get_response().breadcrumb.append((view.slug + '/', view.title))
def check_access(self, api_name=None):
session = get_session()
@ -1033,6 +1042,11 @@ class FormPage(Directory):
else:
raise errors.AccessUnauthorizedError()
def get_custom_views(self, criterias=None):
for view in get_publisher().custom_view_class.select(clause=criterias):
if view.match(get_request().user, self.formdef):
yield view
def get_formdata_sidebar_actions(self, qs=''):
r = TemplateIO(html=True)
r += htmltext(' <li><a data-base-href="ods" href="ods%s">%s</a></li>') % (
@ -1054,9 +1068,24 @@ class FormPage(Directory):
r += htmltext('<ul id="sidebar-actions">')
r += self.get_formdata_sidebar_actions(qs=qs)
r += htmltext('</ul>')
views = list(self.get_custom_views())
if views:
r += htmltext('<h3>%s</h3>') % _('Custom Views')
r += htmltext('<ul id="sidebar-custom-views">')
view_type = 'map' if self.view_type == 'map' else ''
for view in sorted(views, key=lambda x: getattr(x, 'title')):
if self.view:
active = bool(self.view.get_url_slug() == view.get_url_slug())
r += htmltext('<li class="active">' if active else '<li>')
r += htmltext('<a href="../%s/%s">%s</a></li>') % (view.get_url_slug(), view_type, view.title)
else:
r += htmltext('<li><a href="%s/%s">%s</a></li>') % (view.get_url_slug(), view_type, view.title)
r += htmltext('</ul>')
return r.getvalue()
def get_default_filters(self, mode):
if self.view:
return self.view.get_default_filters()
if mode == 'listing':
# enable status filter by default
return ('status',)
@ -1135,6 +1164,7 @@ class FormPage(Directory):
FakeField('end', 'period-date', _('End')),
]
default_filters = self.get_default_filters(mode)
filter_fields = []
for field in period_fake_fields + self.get_formdef_fields():
field.enabled = False
@ -1148,18 +1178,31 @@ class FormPage(Directory):
field.enabled = 'filter-%s' % field.id in get_request().form
else:
field.enabled = (field.id in default_filters)
if field.type in ('item', 'items'):
if not self.view and field.type in ('item', 'items'):
field.enabled = field.in_filters
r += htmltext('<h3><span>%s</span> <span class="change">(<a id="filter-settings">%s</a>)</span></h3>' % (
_('Filters'), _('change')))
r += htmltext('<h3><span>%s</span>') % _('Options')
r += htmltext('<span class="change">(')
r += htmltext('<a id="filter-settings">%s</a>') % _('filters')
if self.view_type in ('table', 'map'):
if self.view_type == 'table':
columns_settings_labels = (_('Columns Settings'), _('columns'))
elif self.view_type == 'map':
columns_settings_labels = (_('Marker Settings'), _('markers'))
r += htmltext(' - <a id="columns-settings" title="%s">%s</a>') % columns_settings_labels
r += htmltext(')</span></h3>')
filters_dict = {}
if self.view:
filters_dict.update(self.view.get_filters_dict())
filters_dict.update(get_request().form)
for filter_field in filter_fields:
if not filter_field.enabled:
continue
filter_field_key = 'filter-%s-value' % filter_field.id
filter_field_value = get_request().form.get(filter_field_key)
filter_field_value = filters_dict.get(filter_field_key)
if filter_field.type == 'status':
r += htmltext('<div class="widget">')
@ -1214,7 +1257,7 @@ class FormPage(Directory):
options.insert(0, (None, '', ''))
attrs = {'data-refresh-options': str(filter_field.id)}
else:
current_filter = get_request().form.get('filter-%s-value' % filter_field.id)
current_filter = filters_dict.get('filter-%s-value' % filter_field.id)
options = [(current_filter, '', current_filter or '')]
attrs = {'data-remote-options': str(filter_field.id)}
get_response().add_javascript(['jquery.js', '../../i18n.js', 'qommon.forms.js', 'select2.js'])
@ -1263,7 +1306,7 @@ class FormPage(Directory):
return r.getvalue()
def get_fields_sidebar(self, selected_filter, fields, offset=None,
limit=None, order_by=None, columns_settings_label=None,
limit=None, order_by=None,
query=None, criterias=None):
get_response().add_javascript(['jquery.js', 'jquery-ui.js', 'wcs.listing.js'])
@ -1292,30 +1335,115 @@ class FormPage(Directory):
r += self.get_filter_sidebar(selected_filter=selected_filter, query=query, criterias=criterias)
r += htmltext('<button class="refresh">%s</button>') % _('Refresh')
if columns_settings_label:
r += htmltext('<button id="columns-settings">%s</button>') % columns_settings_label
r += htmltext('<button class="refresh" hidden>%s</button>') % _('Refresh')
if self.view_type in ('table', 'map'):
# column settings dialog content
r += htmltext('<div style="display: none;">')
r += htmltext('<ul id="columns-filter">')
for field in self.get_formdef_fields():
r += htmltext('<ul id="columns-filter" class="objects-list columns-filter">')
column_order = []
field_ids = [x.id for x in fields]
def get_column_position(x):
if x.id in field_ids:
return field_ids.index(x.id)
return 9999
for field in sorted(self.get_formdef_fields(), key=get_column_position):
if not hasattr(field, str('get_view_value')):
continue
r += htmltext('<li><input type="checkbox" name="%s"') % field.id
if field.id in [x.id for x in fields]:
r += htmltext('<li><span class="handle">⣿</span><label><input type="checkbox" name="%s"') % field.id
if field.id in field_ids:
r += htmltext(' checked="checked"')
r += htmltext(' id="fields-column-%s"') % field.id
r += htmltext('/>')
r += htmltext('<label for="fields-column-%s">%s</label>') % (
field.id, misc.ellipsize(field.label, 70))
r += htmltext('%s</label>') % misc.ellipsize(field.label, 70)
r += htmltext('</li>')
column_order.append(str(field.id))
r += htmltext('</ul>')
r += htmltext('</div>')
r += htmltext('<input type="hidden" name="columns-order" value="%s">' % ','.join(column_order))
r += htmltext('</form>')
r += self.get_custom_view_form().render()
r += htmltext('<button id="save-view">%s</button>') % _('Save View')
if self.can_delete_view():
r += htmltext(' <a data-popup id="delete-view" href="./delete-view" class="button">%s</a>') % _('Delete View')
return r.getvalue()
def get_custom_view_form(self):
form = Form(method='post', id='save-custom-view', hidden='hidden', action='save-view')
form.add(HiddenWidget, 'qs', value=get_request().get_query())
form.add(StringWidget, 'title', title=_('Title'), required=True,
value=self.view.title if self.view else None)
if get_publisher().get_backoffice_root().is_accessible(self.admin_permission):
# admins can create views accessible to everyone
form.add(RadiobuttonsWidget, 'visibility', title=_('Visibility'),
value=self.view.visibility if self.view else 'owner',
options=[
('owner', _('to me only'), 'owner'),
('any', _('to any users'), 'any')
])
if self.view and (self.view.user_id == get_request().user.id or
get_publisher().get_backoffice_root().is_accessible(self.admin_permission)):
form.add(CheckboxWidget, 'update', title=_('Update existing view settings'), value=True)
form.add_submit('submit', _('Save View'))
form.add_submit('cancel', _('Cancel'))
return form
def save_view(self):
form = self.get_custom_view_form()
if form.get_widget('update') and form.get_widget('update').parse():
custom_view = self.view
else:
custom_view = get_publisher().custom_view_class()
custom_view.title = form.get_widget('title').parse()
if not custom_view.title:
get_session().message = ('error', _('Missing title.'))
return redirect('.')
custom_view.user = get_request().user
custom_view.formdef = self.formdef
custom_view.set_from_qs(form.get_widget('qs').parse())
if not custom_view.columns['list']:
get_session().message = ('error', _('Views must have at least one column.'))
return redirect('.')
if form.get_widget('visibility'):
custom_view.visibility = form.get_widget('visibility').parse()
custom_view.store()
if self.view:
return redirect('../' + custom_view.get_url_slug() + '/')
else:
return redirect(custom_view.get_url_slug() + '/')
def can_delete_view(self):
if not self.view:
return False
if str(self.view.user_id) == str(get_request().user.id):
return True
return get_publisher().get_backoffice_root().is_accessible(self.admin_permission)
def delete_view(self):
if not self.can_delete_view():
raise errors.AccessForbiddenError()
form = Form(enctype='multipart/form-data')
form.widgets.append(HtmlWidget('<p>%s</p>' % _(
'You are about to remove the \"%s\" custom view.') % self.view.title))
if self.view.visibility == 'any':
form.widgets.append(HtmlWidget('<div class="warningnotice"<p>%s</p></div>' % _(
'Beware this view is available to all users, and will thus be removed for everyone.')))
form.add_submit('delete', _('Delete'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('.')
if not form.is_submitted() or form.has_errors():
r = TemplateIO(html=True)
r += htmltext('<h2>%s</h2>') % (_('Delete Custom View'))
r += form.render()
return r.getvalue()
else:
self.view.remove_self()
return redirect('..')
def get_formdef_fields(self):
fields = []
fields.append(FakeField('id', 'id', _('Number')))
@ -1333,11 +1461,14 @@ class FormPage(Directory):
return fields
def get_default_columns(self):
field_ids = ['id', 'time', 'last_update_time', 'user-label']
for field in self.formdef.get_all_fields():
if hasattr(field, 'get_view_value') and field.include_in_listing:
field_ids.append(field.id)
field_ids.append('status')
if self.view:
field_ids = self.view.get_columns()
else:
field_ids = ['id', 'time', 'last_update_time', 'user-label']
for field in self.formdef.get_all_fields():
if hasattr(field, 'get_view_value') and field.include_in_listing:
field_ids.append(field.id)
field_ids.append('status')
return field_ids
def get_fields_from_query(self, ignore_form=False):
@ -1350,6 +1481,19 @@ class FormPage(Directory):
if field.id in field_ids:
fields.append(field)
if 'columns-order' in get_request().form or self.view:
if ignore_form or 'columns-order' not in get_request().form:
field_order = field_ids
else:
field_order = get_request().form['columns-order'].split(',')
def field_position(x):
if x.id in field_order:
return field_order.index(x.id)
return 9999
fields.sort(key=field_position)
if not fields:
return self.get_fields_from_query(ignore_form=True)
@ -1358,6 +1502,10 @@ class FormPage(Directory):
def get_filter_from_query(self, default='waiting'):
if 'filter' in get_request().form:
return get_request().form['filter']
if self.view:
view_filter = self.view.get_filter()
if view_filter:
return view_filter
if self.formdef.workflow.possible_status:
return default
return 'all'
@ -1371,6 +1519,12 @@ class FormPage(Directory):
]
filter_fields = []
criterias = []
filters_dict = {}
if self.view:
filters_dict.update(self.view.get_filters_dict())
filters_dict.update(get_request().form)
for filter_field in period_fake_fields + self.get_formdef_fields():
if filter_field.type not in ('item', 'bool', 'items', 'period-date'):
continue
@ -1380,10 +1534,10 @@ class FormPage(Directory):
if filter_field.varname:
# if this is a field with a varname and filter-%(varname)s is
# present in the query string, enable this filter.
if get_request().form.get('filter-%s' % filter_field.varname):
if filters_dict.get('filter-%s' % filter_field.varname):
filter_field_key = 'filter-%s' % filter_field.varname
if get_request().form.get('filter-%s' % filter_field.id):
if filters_dict.get('filter-%s' % filter_field.id):
# if there's a filter-%(id)s, it is used to enable the actual
# filter, and the value will be found in filter-%s-value.
filter_field_key = 'filter-%s-value' % filter_field.id
@ -1392,7 +1546,7 @@ class FormPage(Directory):
# if there's not known filter key, skip.
continue
filter_field_value = get_request().form.get(filter_field_key)
filter_field_value = filters_dict.get(filter_field_key)
if not filter_field_value:
continue
@ -1449,6 +1603,7 @@ class FormPage(Directory):
return mass_actions
def _q_index(self):
self.view_type = 'table'
self.check_access()
get_logger().info('backoffice - form %s - listing' % self.formdef.name)
@ -1467,8 +1622,11 @@ class FormPage(Directory):
else:
limit = get_request().form.get('limit', 0)
offset = get_request().form.get('offset', 0)
order_by = get_request().form.get('order_by',
get_publisher().get_site_option('default-sort-order') or '-receipt_time')
order_by = get_request().form.get('order_by')
if self.view and not order_by:
order_by = self.view.order_by
if not order_by:
order_by = get_publisher().get_site_option('default-sort-order') or '-receipt_time'
query = get_request().form.get('q')
qs = ''
@ -1510,10 +1668,11 @@ class FormPage(Directory):
get_response().filter = {'raw': True}
return table
html_top('management', '%s - %s' % (_('Listing'), self.formdef.name))
view_name = self.view.title if self.view else _('Listing')
html_top('management', '%s - %s' % (view_name, self.formdef.name))
r = TemplateIO(html=True)
r += htmltext('<div id="appbar">')
r += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, _('Listing'))
r += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, view_name)
r += get_session().display_message()
r += self.listing_top_actions()
r += htmltext('</div>')
@ -1526,8 +1685,7 @@ class FormPage(Directory):
get_response().filter['sidebar'] = self.get_formdata_sidebar(qs) + \
self.get_fields_sidebar(selected_filter, fields, limit=limit,
query=query, criterias=criterias,
offset=offset, order_by=order_by,
columns_settings_label=_('Columns Settings'))
offset=offset, order_by=order_by)
return r.getvalue()
@ -1841,6 +1999,8 @@ class FormPage(Directory):
selected_filter = self.get_filter_from_query(default='all')
criterias = self.get_criterias_from_query()
order_by = get_request().form.get('order_by', None)
if self.view and not order_by:
order_by = self.view.order_by
query = get_request().form.get('q') if not anonymise else None
offset = None
if 'offset' in get_request().form:
@ -1872,7 +2032,9 @@ class FormPage(Directory):
'receipt_time': datetime.datetime(*filled.receipt_time[:6]),
'last_update_time': datetime.datetime(*filled.last_update_time[:6]),
} for filled in items]
if isinstance(self.formdef, CardDef):
if isinstance(self.formdef, CardDef) or self.view:
# for cards and custom views return results in a dictionary, as it
# provides a better path for evolutions
output = {'data': output}
return json.dumps(output,
cls=misc.JSONEncoder)
@ -1988,6 +2150,7 @@ class FormPage(Directory):
return IcsDirectory()
def map(self):
self.view_type = 'map'
get_response().add_javascript(['qommon.map.js'])
html_top('management', '%s - %s' % (_('Form'), self.formdef.name))
r = TemplateIO(html=True)
@ -2003,8 +2166,12 @@ class FormPage(Directory):
fields = self.get_fields_from_query()
selected_filter = self.get_filter_from_query()
get_response().filter['sidebar'] = self.get_fields_sidebar(selected_filter,
fields, columns_settings_label=_('Markers Settings'))
qs = ''
if get_request().get_query():
qs = '?' + get_request().get_query()
get_response().filter['sidebar'] = self.get_formdata_sidebar(qs) + \
self.get_fields_sidebar(selected_filter, fields)
r += htmltext('<h2>%s - %s</h2>') % (self.formdef.name, _('Map'))
r += htmltext('<div %s></div>' % ' '.join(['%s="%s"' % x for x in attrs.items()]))
@ -2220,6 +2387,11 @@ class FormPage(Directory):
if component == 'ics':
return self.ics()
if not self.view:
for view in self.get_custom_views():
if view.get_url_slug() == component:
return self.__class__(formdef=self.formdef, view=view)
try:
filled = self.formdef.data_class().get(component)
except KeyError:

136
wcs/custom_views.py Normal file
View File

@ -0,0 +1,136 @@
# 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/>.
from django.utils.six.moves.urllib import parse as urlparse
from quixote import get_publisher
from wcs.carddef import CardDef
from wcs.formdef import FormDef
from wcs.qommon.storage import StorableObject, Equal
from wcs.qommon.misc import simplify
class CustomView(StorableObject):
_names = 'custom-views'
title = None
slug = None
user_id = None
visibility = 'owner'
formdef_type = None
formdef_id = None
columns = None
filters = None
order_by = None
@property
def user(self):
return get_publisher().user_class.get(self.user_id)
@user.setter
def user(self, value):
self.user_id = str(value.id)
@property
def formdef(self):
if self.formdef_type == 'formdef':
return FormDef.get(self.formdef_id)
else:
return CardDef.get(self.formdef_id)
@formdef.setter
def formdef(self, value):
self.formdef_id = str(value.id)
self.formdef_type = value.xml_root_node
def match(self, user, formdef):
if self.visibility == 'owner' and self.user_id != str(user.id):
return False
if self.formdef_type != formdef.xml_root_node:
return False
if self.formdef_id != str(formdef.id):
return False
return True
def set_from_qs(self, qs):
parsed_qs = urlparse.parse_qsl(qs)
self.columns = {
'list': [
{'id': key} for (key, value) in parsed_qs if value == 'on' and not key.startswith('filter-')
],
}
columns_order = [x[1] for x in parsed_qs if x[0] == 'columns-order']
if columns_order:
field_order = columns_order[0].split(',')
def field_position(x):
if x['id'] in field_order:
return field_order.index(x['id'])
return 9999
self.columns['list'].sort(key=field_position)
order_by = [x[1] for x in parsed_qs if x[0] == 'order_by']
if order_by:
self.order_by = order_by[0]
self.filters = {key: value for (key, value) in parsed_qs if key.startswith('filter')}
def ensure_slug(self):
if self.slug:
return
clauses = [
Equal('formdef_type', self.formdef_type),
Equal('formdef_id', self.formdef_id),
Equal('visibility', self.visibility),
]
if self.visibility == 'owner':
clauses.append(Equal('user_id', self.user_id))
existing_slugs = set([x.slug for x in self.select(clauses)])
base_slug = simplify(self.title)
if base_slug.startswith('user-'):
# prevent a slug starting with user- as it's used in URLs
base_slug = 'userx-' + base_slug[5:]
self.slug = base_slug
i = 2
while self.slug in existing_slugs:
self.slug = '%s-%s' % (base_slug, i)
i += 1
def get_url_slug(self):
if self.visibility == 'owner':
return 'user-%s' % self.slug
return self.slug
def store(self, *args, **kwargs):
self.ensure_slug()
return super(CustomView, self).store(*args, **kwargs)
def get_columns(self):
if self.columns and 'list' in self.columns:
return [x['id'] for x in self.columns['list']]
else:
return []
def get_filter(self):
return self.filters.get('filter')
def get_filters_dict(self):
return self.filters
def get_default_filters(self):
return [key[7:] for key in self.filters if key.startswith('filter-')]

View File

@ -46,6 +46,7 @@ set_publisher_class(StubWcsPublisher)
from .root import RootDirectory
from .backoffice import RootDirectory as BackofficeRootDirectory
from .admin import RootDirectory as AdminRootDirectory
from . import custom_views
from . import sessions
from .qommon.cron import CronJob
@ -148,11 +149,13 @@ class WcsPublisher(StubWcsPublisher):
self.user_class = sql.SqlUser
self.tracking_code_class = sql.TrackingCode
self.session_class = sql.Session
self.custom_view_class = sql.CustomView
sql.get_connection(new=True)
else:
self.user_class = User
self.tracking_code_class = TrackingCode
self.session_class = sessions.BasicSession
self.custom_view_class = custom_views.CustomView
self.session_manager_class = sessions.StorageSessionManager
self.set_session_manager(self.session_manager_class(session_class=self.session_class))
@ -298,6 +301,7 @@ class WcsPublisher(StubWcsPublisher):
sql.do_session_table()
sql.do_user_table()
sql.do_tracking_code_table()
sql.do_custom_views_table()
sql.do_meta_table()
from .formdef import FormDef
from .carddef import CardDef

View File

@ -1081,15 +1081,37 @@ div.PrefillSelectionWidget div.content input[type=submit] {
}
ul#field-filter,
ul#columns-filter {
ul.columns-filter {
list-style: none;
padding-left: 0;
margin-left: 0;
}
ul#field-filter {
-webkit-column-count: 2;
-moz-column-count: 2;
column-count: 2;
}
ul.columns-filter span.handle {
padding: 0;
position: absolute;
width: 2em;
cursor: move;
display: inline-block;
padding: 0 0.5ex;
text-align: center;
width: 1em;
}
ul.columns-filter li {
padding-left: 0;
}
ul.columns-filter li label {
padding-left: 2em;
}
ul.multipage li {
margin-left: 2em;
}
@ -1290,6 +1312,7 @@ fieldset.form-plus.closed legend:after {
}
}
a#columns-settings,
a#filter-settings {
cursor: pointer;
}
@ -1880,3 +1903,7 @@ div.mail-body {
margin-top: 1em;
white-space: pre-line;
}
#sidebar-custom-views .active {
font-weight: bold;
}

View File

@ -202,26 +202,22 @@ $(function() {
/* column settings */
$('#columns-settings').click(function() {
var dialog = $('<form>');
$('#columns-filter').clone().appendTo(dialog);
$(dialog).find('input').each(function(idx, elem) {
$(this).attr('id', 'dlg-' + $(this).attr('id'));
});
$(dialog).find('label').each(function(idx, elem) {
$(this).attr('for', 'dlg-' + $(this).attr('for'));
});
var $dialog_filter = $('#columns-filter').clone().attr('id', null);
$dialog_filter.appendTo(dialog);
$dialog_filter.sortable({handle: '.handle'})
$(dialog).dialog({
modal: true,
resizable: false,
title: $('#columns-settings').text(),
title: $('#columns-settings').attr('title'),
width: '30em'});
$(dialog).dialog('option', 'buttons', [
{text: $('form#listing-settings button.refresh').text(),
click: function() {
$(this).find('input[type="checkbox"]').each(function(idx, elem) {
var checked = $(elem).prop('checked');
$('form#listing-settings input[name="' + $(elem).attr('name') + '"]').attr('checked', checked);
$('form#listing-settings input[name="' + $(elem).attr('name') + '"]').prop('checked', checked);
});
var $container = $('#columns-filter').parent();
$('#columns-filter').remove();
$dialog_filter.attr('id', 'columns-filter');
$dialog_filter.appendTo($container);
$('[name="columns-order"]').val($('#columns-filter input:checked').map(function() { return $(this).attr('name'); }).get().join());
$(this).dialog('close');
$('form#listing-settings').submit();
}
@ -303,6 +299,30 @@ $(function() {
return false;
});
$('button#save-view').on('click', function() {
var div_dialog = $('<div>');
$('#save-custom-view').clone().attr('hidden', null).appendTo(div_dialog);
$(div_dialog).find('[name=qs]').val($('form#listing-settings').serialize());
$(div_dialog).find('.buttons').hide();
var dialog = $(div_dialog).dialog({
modal: true,
resizable: false,
title: $(this).text(),
width: 'auto',
buttons: [
{text: $(div_dialog).find('.cancel-button').text(),
class: 'cancel-button',
click: function() { $(this).dialog('close'); }
},
{text: $(div_dialog).find('.submit-button').text(),
class: 'submit-button',
click: function() { $(div_dialog).find('.submit-button button').click(); return false; }
}
]
});
return false;
});
/* automatically refresh on filter change */
$('form#listing-settings select').change(function() {
$('form#listing-settings').submit();

View File

@ -16,6 +16,7 @@
import psycopg2
import psycopg2.extensions
import psycopg2.extras
import datetime
import time
import re
@ -38,6 +39,7 @@ from .publisher import UnpicklerClass
import wcs.categories
import wcs.carddata
import wcs.custom_views
import wcs.formdata
import wcs.tracking_code
import wcs.users
@ -47,6 +49,10 @@ import wcs.users
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY)
# automatically adapt dictionaries into json fields
psycopg2.extensions.register_adapter(dict, psycopg2.extras.Json)
SQL_TYPE_MAPPING = {
'title': None,
'subtitle': None,
@ -754,6 +760,40 @@ def do_session_table():
cur.close()
def do_custom_views_table():
conn, cur = get_connection_and_cursor()
table_name = 'custom_views'
cur.execute('''SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = %s''', (table_name,))
if cur.fetchone()[0] == 0:
cur.execute('''CREATE TABLE %s (id varchar PRIMARY KEY,
title varchar,
slug varchar,
user_id varchar,
visibility varchar,
formdef_type varchar,
formdef_id varchar,
order_by varchar,
columns jsonb,
filters jsonb
)''' % table_name)
cur.execute('''SELECT column_name FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = %s''', (table_name,))
existing_fields = set([x[0] for x in cur.fetchall()])
needed_fields = set([x[0] for x in CustomView._table_static_fields])
# delete obsolete fields
for field in (existing_fields - needed_fields):
cur.execute('''ALTER TABLE %s DROP COLUMN %s''' % (table_name, field))
conn.commit()
cur.close()
@guard_postgres
def do_meta_table(conn=None, cur=None, insert_current_sql_level=True):
own_conn = False
@ -2103,6 +2143,85 @@ class TrackingCode(SqlMixin, wcs.tracking_code.TrackingCode):
return []
class CustomView(SqlMixin, wcs.custom_views.CustomView):
_table_name = 'custom_views'
_table_static_fields = [
('id', 'varchar'),
('title', 'varchar'),
('slug', 'varchar'),
('user_id', 'varchar'),
('visibility', 'varchar'),
('formdef_type', 'varchar'),
('formdef_id', 'varchar'),
('order_by', 'varchar'),
('columns', 'jsonb'),
('filters', 'jsonb'),
]
@guard_postgres
@invalidate_substitution_cache
def store(self):
self.ensure_slug()
sql_dict = {
'id': self.id,
'title': self.title,
'slug': self.slug,
'user_id': self.user_id,
'visibility': self.visibility,
'formdef_type': self.formdef_type,
'formdef_id': self.formdef_id,
'order_by': self.order_by,
'columns': self.columns,
'filters': self.filters,
}
conn, cur = get_connection_and_cursor()
if not self.id:
column_names = sql_dict.keys()
sql_dict['id'] = self.get_new_id()
sql_statement = '''INSERT INTO %s (%s)
VALUES (%s)
RETURNING id''' % (
self._table_name,
', '.join(column_names),
', '.join(['%%(%s)s' % x for x in column_names]))
while True:
try:
cur.execute(sql_statement, sql_dict)
except psycopg2.IntegrityError:
conn.rollback()
sql_dict['id'] = self.get_new_id()
else:
break
self.id = str_encode(cur.fetchone()[0])
else:
column_names = sql_dict.keys()
sql_dict['id'] = self.id
sql_statement = '''UPDATE %s SET %s WHERE id = %%(id)s RETURNING id''' % (
self._table_name,
', '.join(['%s = %%(%s)s' % (x, x) for x in column_names]))
cur.execute(sql_statement, sql_dict)
if cur.fetchone() is None:
raise AssertionError()
conn.commit()
cur.close()
@classmethod
def _row2ob(cls, row):
o = cls()
for field, value in zip(cls._table_static_fields, tuple(row)):
if field[1] == 'varchar':
setattr(o, field[0], str_encode(value))
elif field[1] == 'jsonb':
setattr(o, field[0], value)
return o
@classmethod
def get_data_fields(cls):
return []
class classproperty(object):
def __init__(self, f):
self.f = f
@ -2333,7 +2452,7 @@ def get_yearly_totals(period_start=None, period_end=None, criterias=None):
return result
SQL_LEVEL = 36
SQL_LEVEL = 37
def migrate_global_views(conn, cur):
@ -2464,6 +2583,9 @@ def migrate():
# 25: create session_table
# 32: add last_update_time column to session table
do_session_table()
if sql_level < 37:
# 37: create custom_views tabl
do_custom_views_table()
if sql_level < 30:
# 30: actually remove evo.who on anonymised formdatas
from wcs.formdef import FormDef