backoffice: add support for custom views (#4507)
This commit is contained in:
parent
db6448a5e4
commit
5c04a4402a
|
@ -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')
|
||||
|
|
|
@ -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 "Finished"' 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()
|
||||
|
|
|
@ -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()
|
||||
|
|
10
wcs/api.py
10
wcs/api.py
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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-')]
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
124
wcs/sql.py
124
wcs/sql.py
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue