Nouveaux opérateurs pour les vues personnalisées (#76758) #283

Merged
lguerin merged 9 commits from wip/76758-new-operators-api-and-ui into main 2023-06-12 15:08:14 +02:00
9 changed files with 850 additions and 279 deletions

View File

@ -1289,14 +1289,21 @@ def test_api_list_formdata_string_filter(pub, local_user):
data_class = formdef.data_class()
data_class.wipe()
for i in range(4):
for i in range(5):
formdata = data_class()
formdata.data = {}
if i < 3: # None values for the last one
formdata.data = {
'0': 'FOO %s' % i,
'1': '%s' % (9 + i),
}
if i == 3:
# Empty values
formdata.data = {
'0': 'FOO %s' % i,
'1': '%s' % (9 + i),
'0': '',
'1': '',
}
if i == 4:
# None values
formdata.data = {}
formdata.user_id = local_user.id
formdata.just_created()
formdata.jump_status('new')
@ -1306,12 +1313,20 @@ def test_api_list_formdata_string_filter(pub, local_user):
assert len(resp.json) == 1
params = [
('eq', 'FOO 2', 1),
('ne', 'FOO 2', 3),
('lt', 'FOO 2', 2),
('lte', 'FOO 2', 3),
('ne', 'FOO 2', 4),
('lt', 'FOO 2', 3),
('lte', 'FOO 2', 4),
('gt', 'FOO 2', 0),
('gt', '42', 0),
('gte', 'FOO 2', 1),
('in', 'FOO 2', 1),
('in', 'FOO 2|FOO 1', 2),
('not_in', 'FOO 2', 3),
('not_in', 'FOO 2|FOO 1', 2),
('absent', 'on', 2),
('existing', 'on', 3),
('between', 'FOO 1|FOO 2', 1),
('between', 'FOO 2|FOO 1', 1),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -1322,15 +1337,34 @@ def test_api_list_formdata_string_filter(pub, local_user):
)
assert len(resp.json) == result
resp = get_app(pub).get(
sign_uri(
'/api/forms/test/list?filter-string=plop&filter-string-operator=between',
user=local_user,
),
status=400,
)
assert resp.json['err_desc'] == 'Invalid value "plop" for operator "between" and filter "filter-string"'
params = [
('eq', '10', 1),
('eq', '010', 1),
('ne', '10', 3),
('lt', '10', 1),
('lte', '10', 2),
('gt', '10', 1),
('gt', '9', 2),
('gte', '10', 2),
('in', '10', 1),
('in', '10|9', 2),
('in', '10|42', 1),
('in', '10|a', 1),
('not_in', '10', 2),
('not_in', '10|9', 1),
('not_in', '10|42', 2),
('absent', 'on', 2),
('existing', 'on', 3),
('between', '9|10', 1),
('between', '10|9', 1),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -1373,14 +1407,22 @@ def test_api_list_formdata_item_filter(pub, local_user):
data_class = formdef.data_class()
data_class.wipe()
for i in range(4):
for i in range(5):
formdata = data_class()
formdata.data = {}
if i < 3: # None values for the last one
formdata.data = {
'0': str(9 + i),
'1': 'foo' if i % 2 else 'bar',
}
if i == 3:
# Empty values
formdata.data = {
'0': str(9 + i),
'1': 'foo' if i % 2 else 'bar',
'0': '',
'1': '',
}
if i == 4:
# None values
formdata.data = {}
formdata.user_id = local_user.id
formdata.just_created()
formdata.jump_status('new')
@ -1390,13 +1432,23 @@ def test_api_list_formdata_item_filter(pub, local_user):
assert len(resp.json) == 1
params = [
('eq', '10', 1),
('eq', '010', 1),
('ne', '10', 3),
('lt', '10', 1),
('lte', '10', 2),
('gt', '10', 1),
('gt', '9', 2),
('gte', '10', 2),
('in', '10', 1),
('in', '10|9', 2),
('in', '10|42', 1),
('in', '10|a', 1),
('not_in', '10', 2),
('not_in', '10|9', 1),
('not_in', '10|42', 2),
('absent', 'on', 2),
('existing', 'on', 3),
('between', '9|10', 1),
('between', '10|9', 1),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -1411,12 +1463,22 @@ def test_api_list_formdata_item_filter(pub, local_user):
assert len(resp.json) == 1
params = [
('eq', 'foo', 1),
('ne', 'foo', 3),
('lt', 'foo', 2),
('lte', 'foo', 3),
('ne', 'foo', 4),
('lt', 'foo', 3),
('lte', 'foo', 4),
('gt', 'foo', 0),
('gt', '42', 0),
('gte', 'foo', 1),
('in', 'foo', 1),
('in', 'foo|bar', 3),
('in', 'foo|baz', 1),
('not_in', 'foo', 3),
('not_in', 'foo|bar', 1),
('not_in', 'foo|42', 3),
('absent', 'on', 2),
('existing', 'on', 3),
('between', 'bar|foo', 2),
('between', 'foo|bar', 2),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -1475,8 +1537,9 @@ def test_api_list_formdata_item_filter_on_cards(pub, local_user, sql_queries):
resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter-item=%s' % carddata.id, user=local_user))
assert len(resp.json) == 1
carddata_sql_queries = [q for q in sql_queries if 'FROM carddata' in q]
assert len(carddata_sql_queries) == 1
assert len(carddata_sql_queries) == 2
assert ' id = ' in carddata_sql_queries[0]
assert ' id = ' in carddata_sql_queries[1]
def test_api_list_formdata_items_filter(pub, local_user):
@ -1512,14 +1575,22 @@ def test_api_list_formdata_items_filter(pub, local_user):
data_class = formdef.data_class()
data_class.wipe()
for i in range(4):
for i in range(5):
formdata = data_class()
formdata.data = {}
if i < 3: # None values for the last one
formdata.data = {
'0': ['9000' if i % 2 else '11000', '10000'],
'1': ['foo' if i % 2 else 'bar', 'baz'],
}
if i == 3:
# Empty values
formdata.data = {
'0': ['9000' if i % 2 else '11000', '10000'],
'1': ['foo' if i % 2 else 'bar', 'baz'],
'0': [],
'1': [],
}
if i == 4:
# None values
formdata.data = {}
formdata.user_id = local_user.id
formdata.just_created()
formdata.jump_status('new')
@ -1529,15 +1600,26 @@ def test_api_list_formdata_items_filter(pub, local_user):
assert len(resp.json) == 2
params = [
('eq', '11000', 2),
('eq', '011000', 2),
('eq', '10000', 3),
('ne', '9000', 3),
('ne', '10000', 1),
('ne', '9000', 4),
('ne', '10000', 2),
('lt', '10000', 1),
('lte', '10000', 3),
('gt', '10000', 2),
('gt', '9000', 3),
('gte', '11000', 2),
('in', '11000', 2),
('in', '11000|10000', 3),
('in', '11000|9000', 3),
('not_in', '11000', 3),
('not_in', '11000|10000', 2),
('not_in', '11000|9000', 2),
('not_in', '11001|9000', 4),
('absent', 'on', 2),
('existing', 'on', 3),
('between', '9000|10000', 1),
('between', '10000|9000', 1),
('between', '9000|9001', 1),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -1550,12 +1632,20 @@ def test_api_list_formdata_items_filter(pub, local_user):
params = [
('eq', 'foo', 1),
('ne', 'foo', 3),
('ne', 'foo', 4),
('lt', 'foo', 3),
('lte', 'foo', 3),
('gt', 'foo', 0),
('gt', '42', 0),
('gte', 'foo', 1),
('in', 'foo', 1),
('in', 'foo|bar', 3),
('not_in', 'foo', 4),
('not_in', 'foo|bar', 2),
('absent', 'on', 2),
('existing', 'on', 3),
('between', 'bar|bazz', 3),
('between', 'bazz|bar', 3),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -1607,6 +1697,8 @@ def test_api_list_formdata_bool_filter(pub, local_user):
('eq', 'true', 1),
('ne', 'true', 3),
('ne', 'false', 2),
('absent', 'on', 1),
('existing', 'on', 3),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -1616,7 +1708,7 @@ def test_api_list_formdata_bool_filter(pub, local_user):
)
)
assert len(resp.json) == result
for operator in ['lt', 'lte', 'gt', 'gte']:
for operator in ['lt', 'lte', 'gt', 'gte', 'in', 'not_in', 'between']:
resp = get_app(pub).get(
sign_uri(
'/api/forms/test/list?filter-bool=true&filter-bool-operator=%s' % operator, user=local_user
@ -1659,22 +1751,30 @@ def test_api_list_formdata_date_filter(pub, local_user):
for value in ['2021-06-11', '11/06/2021']:
resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter-date=%s' % value, user=local_user))
assert len(resp.json) == 1
params = [
('eq', 1),
('ne', 3),
('lt', 1),
('lte', 2),
('gt', 1),
('gte', 2),
]
for operator, result in params:
resp = get_app(pub).get(
sign_uri(
'/api/forms/test/list?filter-date=%s&filter-date-operator=%s' % (value, operator),
user=local_user,
)
params = [
('eq', '2021-06-11', 1),
('ne', '2021-06-11', 3),
('lt', '2021-06-11', 1),
('lte', '2021-06-11', 2),
('gt', '2021-06-11', 1),
('gte', '2021-06-11', 2),
('in', '2021-06-12', 1),
('in', '2021-06-12|2021-06-15', 1),
('not_in', '2021-06-12', 2),
('not_in', '2021-06-12|2021-06-15', 2),
('absent', 'on', 1),
('existing', 'on', 3),
('between', '2021-06-12|2021-06-15', 1),
('between', '2021-06-15|2021-06-12', 1),
]
for operator, value, result in params:
resp = get_app(pub).get(
sign_uri(
'/api/forms/test/list?filter-date=%s&filter-date-operator=%s' % (value, operator),
user=local_user,
)
assert len(resp.json) == result
)
assert len(resp.json) == result
def test_api_list_formdata_email_filter(pub, local_user):
@ -1697,11 +1797,18 @@ def test_api_list_formdata_email_filter(pub, local_user):
data_class = formdef.data_class()
data_class.wipe()
for i in range(4):
for i in range(5):
formdata = data_class()
formdata.data = {}
if i < 3: # None values for the last one
formdata.data = {'0': 'a@localhost' if i % 2 else 'b@localhost'}
formdata.data = {'0': 'a@localhost' if i % 2 else 'b@localhost'}
if i == 3:
# Empty values
formdata.data = {
'0': '',
}
if i == 4:
# None values
formdata.data = {}
formdata.user_id = local_user.id
formdata.just_created()
formdata.jump_status('new')
@ -1711,7 +1818,13 @@ def test_api_list_formdata_email_filter(pub, local_user):
assert len(resp.json) == 1
params = [
('eq', 'a@localhost', 1),
('ne', 'a@localhost', 3),
('ne', 'a@localhost', 4),
('in', 'a@localhost', 1),
('in', 'a@localhost|b@localhost', 3),
('not_in', 'a@localhost', 3),
('not_in', 'a@localhost|b@localhost', 1),
('absent', 'on', 2),
('existing', 'on', 3),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -1721,7 +1834,7 @@ def test_api_list_formdata_email_filter(pub, local_user):
)
)
assert len(resp.json) == result
for operator in ['lt', 'lte', 'gt', 'gte']:
for operator in ['lt', 'lte', 'gt', 'gte', 'between']:
resp = get_app(pub).get(
sign_uri(
'/api/forms/test/list?filter-email=a@localhost&filter-email-operator=%s' % operator,
@ -1767,7 +1880,6 @@ def test_api_list_formdata_internal_id_filter(pub, local_user):
params = [
('eq', '1', 1),
('eq', '01', 1),
('ne', '1', 10),
('lt', '1', 0),
('lte', '1', 1),
@ -1789,6 +1901,16 @@ def test_api_list_formdata_internal_id_filter(pub, local_user):
)
assert resp.json['err_desc'] == 'Invalid value "blabla" for "filter-internal-id-value"'
for operator in ['in', 'not_in', 'absent', 'existing', 'between']:
resp = get_app(pub).get(
sign_uri(
'/api/forms/test/list?filter-internal-id=42&filter-internal-id-operator=%s' % operator,
user=local_user,
),
status=400,
)
assert resp.json['err_desc'] == 'Invalid operator "%s" for "filter-internal-id"' % operator
# multi-ids
resp = get_app(pub).get(
sign_uri(
@ -1908,38 +2030,68 @@ def test_api_list_formdata_block_field_filter(pub, local_user):
data_class = formdef.data_class()
data_class.wipe()
for i in range(11):
for i in range(14):
formdata = data_class()
if i < 10: # None values for the last one
formdata.data = {
'0': {
'data': [
{
'1': 'plop%s' % i,
'2': '1' if i % 2 else '2',
'2_display': 'foo' if i % 2 else 'bar',
'2_structured': 'XXX' if i % 2 else 'YYY',
'3': bool(i % 2),
'4': '2021-06-%02d' % (i + 1),
'5': 'a@localhost' if i % 2 else 'b@localhost',
},
],
'schema': {}, # not important here
},
'0_display': 'hello',
}
if i == 0:
# 2 elements with values
formdata.data['0']['data'].append(
{
'1': 'plop%s' % (i + 1),
'2': '1',
'2_display': 'foo',
'2_structured': 'XXX',
'3': True,
'4': '2021-06-02',
'5': 'a@localhost',
},
)
if i == 10:
# 2 elements, the second without values
formdata.data['0']['data'].append(
{
'1': '',
'2': '',
'4': '',
'5': '',
}
)
if i == 11:
# 2 elements, the second with non values
formdata.data['0']['data'].append({})
if i == 12:
# only one element, without values
formdata.data = {
'0': {
'data': [
{
'1': 'plop%s' % i,
'2': '1' if i % 2 else '2',
'2_display': 'foo' if i % 2 else 'bar',
'2_structured': 'XXX' if i % 2 else 'YYY',
'3': bool(i % 2),
'4': '2021-06-%02d' % (i + 1),
'5': 'a@localhost' if i % 2 else 'b@localhost',
},
],
'schema': {}, # not important here
},
'0_display': 'hello',
'1': '',
'2': '',
'4': '',
'5': '',
}
]
}
}
if i == 0:
formdata.data['0']['data'].append(
{
'1': 'plop%s' % (i + 1),
'2': '1',
'2_display': 'foo',
'2_structured': 'XXX',
'3': True,
'4': '2021-06-02',
'5': 'a@localhost',
},
)
if i == 13:
# no element
formdata.data = {}
formdata.user_id = local_user.id
formdata.just_created()
formdata.jump_status('new')
@ -1950,17 +2102,25 @@ def test_api_list_formdata_block_field_filter(pub, local_user):
assert len(resp.json) == 1
resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter-blockdata_string=plop2', user=local_user))
assert len(resp.json) == 1
resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter-blockdata_string=plop10', user=local_user))
resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter-blockdata_string=plop12', user=local_user))
assert len(resp.json) == 0
params = [
('eq', 'plop5', 1),
('ne', 'plop5', 10),
('ne', 'plop1', 9),
('lt', 'plop5', 5),
('lte', 'plop5', 6),
('ne', 'plop5', 13),
('ne', 'plop1', 12),
('lt', 'plop5', 8),
('lte', 'plop5', 9),
('gt', 'plop5', 4),
('gt', '42', 0),
('gte', 'plop5', 5),
('in', 'plop5', 1),
('in', 'plop5|plop4', 2),
('not_in', 'plop5', 13),
('not_in', 'plop5|plop4', 12),
('absent', 'on', 2),
('existing', 'on', 12),
('between', 'plop1|plop5', 7),
('between', 'plop5|plop1', 7),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -1973,18 +2133,30 @@ def test_api_list_formdata_block_field_filter(pub, local_user):
assert len(resp.json) == result
# item
resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter-blockdata_item=1', user=local_user))
assert len(resp.json) == 6
assert len(resp.json) == 7
resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter-blockdata_item=2', user=local_user))
assert len(resp.json) == 5
assert len(resp.json) == 6
resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter-blockdata_item=3', user=local_user))
assert len(resp.json) == 0
params = [
('eq', '1', 6),
('ne', '1', 5),
('lt', '2', 6),
('lte', '1', 6),
('gt', '1', 5),
('gte', '2', 5),
('eq', '1', 7),
('ne', '1', 7),
('lt', '2', 7),
('lte', '1', 7),
('gt', '1', 6),
('gte', '2', 6),
('in', '1', 7),
('in', '1|2', 12),
('in', '1|42', 7),
('in', '1|a', 7),
('not_in', '1', 7),
('not_in', '1|2', 2),
('not_in', '1|42', 7),
('absent', 'on', 2),
('existing', 'on', 12),
('between', '1|2', 7),
('between', '1|3', 12),
('between', '3|1', 12),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -1997,16 +2169,18 @@ def test_api_list_formdata_block_field_filter(pub, local_user):
assert len(resp.json) == result
# bool
resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter-blockdata_bool=true', user=local_user))
assert len(resp.json) == 6
assert len(resp.json) == 7
resp = get_app(pub).get(sign_uri('/api/forms/test/list?filter-blockdata_bool=false', user=local_user))
assert len(resp.json) == 5
assert len(resp.json) == 6
resp = get_app(pub).get(
sign_uri('/api/forms/test/list?filter-blockdata_bool=foobar', user=local_user), status=400
)
assert resp.json['err_desc'] == 'Invalid value "foobar" for "filter-blockdata_bool"'
params = [
('eq', 'true', 6),
('ne', 'true', 5),
('eq', 'true', 7),
('ne', 'true', 7),
('absent', 'on', 2),
('existing', 'on', 12),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -2017,7 +2191,7 @@ def test_api_list_formdata_block_field_filter(pub, local_user):
)
)
assert len(resp.json) == result
for operator in ['lt', 'lte', 'gt', 'gte']:
for operator in ['lt', 'lte', 'gt', 'gte', 'in', 'not_in', 'between']:
resp = get_app(pub).get(
sign_uri(
'/api/forms/test/list?filter-blockdata_bool=true&filter-blockdata_bool-operator=%s'
@ -2042,11 +2216,19 @@ def test_api_list_formdata_block_field_filter(pub, local_user):
assert len(resp.json) == 2
params = [
('eq', '2021-06-02', 2),
('ne', '2021-06-02', 9),
('lt', '2021-06-02', 1),
('lte', '2021-06-02', 2),
('gt', '2021-06-02', 8),
('gte', '2021-06-02', 10),
('ne', '2021-06-02', 12),
('lt', '2021-06-02', 3),
('lte', '2021-06-02', 4),
('gt', '2021-06-02', 10),
('gte', '2021-06-02', 12),
('in', '2021-06-02', 2),
('in', '2021-06-02|2021-06-05', 3),
('not_in', '2021-06-02', 12),
('not_in', '2021-06-02|2021-06-05', 11),
('absent', 'on', 2),
('existing', 'on', 12),
('between', '2021-06-02|2021-06-05', 4),
('between', '2021-06-05|2021-06-02', 4),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -2061,18 +2243,24 @@ def test_api_list_formdata_block_field_filter(pub, local_user):
resp = get_app(pub).get(
sign_uri('/api/forms/test/list?filter-blockdata_email=a@localhost', user=local_user)
)
assert len(resp.json) == 6
assert len(resp.json) == 7
resp = get_app(pub).get(
sign_uri('/api/forms/test/list?filter-blockdata_email=b@localhost', user=local_user)
)
assert len(resp.json) == 5
assert len(resp.json) == 6
resp = get_app(pub).get(
sign_uri('/api/forms/test/list?filter-blockdata_email=c@localhost', user=local_user)
)
assert len(resp.json) == 0
params = [
('eq', 'a@localhost', 6),
('ne', 'a@localhost', 5),
('eq', 'a@localhost', 7),
('ne', 'a@localhost', 7),
('in', 'a@localhost', 7),
('in', 'a@localhost|b@localhost', 12),
('not_in', 'a@localhost', 7),
('not_in', 'a@localhost|b@localhost', 2),
('absent', 'on', 2),
('existing', 'on', 12),
]
for operator, value, result in params:
resp = get_app(pub).get(
@ -2083,7 +2271,7 @@ def test_api_list_formdata_block_field_filter(pub, local_user):
)
)
assert len(resp.json) == result
for operator in ['lt', 'lte', 'gt', 'gte']:
for operator in ['lt', 'lte', 'gt', 'gte', 'between']:
resp = get_app(pub).get(
sign_uri(
'/api/forms/test/list?filter-blockdata_email=plop0&filter-blockdata_email-operator=%s'
@ -2125,21 +2313,24 @@ def test_api_list_formdata_block_field_filter(pub, local_user):
return None
return d['fields']['blockdata_raw'][0]['string']
plop_list = ['', 'plop0', 'plop1', 'plop10', 'plop11'] + ['plop%s' % i for i in range(2, 10)] + [None]
reversed_plop_list = list(reversed(plop_list))
resp = get_app(pub).get(sign_uri('/api/forms/test/list?full=on&order_by=f0-1', user=local_user))
assert [get_string(d) for d in resp.json] == ['plop%s' % i for i in range(0, 10)] + [None]
assert [get_string(d) for d in resp.json] == plop_list
resp = get_app(pub).get(sign_uri('/api/forms/test/list?full=on&order_by=-f0-1', user=local_user))
assert [get_string(d) for d in resp.json] == [None] + ['plop%s' % i for i in range(9, -1, -1)]
assert [get_string(d) for d in resp.json] == reversed_plop_list
resp = get_app(pub).get(
sign_uri('/api/forms/test/list?full=on&order_by=blockdata_string', user=local_user)
)
assert [get_string(d) for d in resp.json] == ['plop%s' % i for i in range(0, 10)] + [None]
assert [get_string(d) for d in resp.json] == plop_list
resp = get_app(pub).get(
sign_uri('/api/forms/test/list?full=on&order_by=-blockdata_string', user=local_user)
)
assert [get_string(d) for d in resp.json] == [None] + ['plop%s' % i for i in range(9, -1, -1)]
assert [get_string(d) for d in resp.json] == reversed_plop_list
def test_api_anonymized_formdata(pub, local_user, admin_user):

View File

@ -251,6 +251,8 @@ def test_backoffice_bool_filter(pub):
assert [x[0] for x in resp.forms['listing-settings']['filter-%s-operator' % field.id].options] == [
'eq',
'ne',
'absent',
'existing',
]
resp.forms['listing-settings']['filter-%s-value' % field.id].value = 'true'
@ -330,6 +332,11 @@ def test_backoffice_item_filter(pub):
'lte',
'gt',
'gte',
'between',
'in',
'not_in',
'absent',
'existing',
]
resp.forms['listing-settings']['filter-4-value'].value = 'â'
@ -529,6 +536,11 @@ def test_backoffice_bofield_item_filter(pub):
'lte',
'gt',
'gte',
'between',
'in',
'not_in',
'absent',
'existing',
]
resp.forms['listing-settings']['filter-bo0-1-value'].value = 'â'
@ -631,6 +643,11 @@ def test_backoffice_items_filter(pub):
'lte',
'gt',
'gte',
'between',
'in',
'not_in',
'absent',
'existing',
]
assert [x[2] for x in resp.forms['listing-settings']['filter-4-value'].options] == ['', 'â', 'b', 'd']
@ -792,6 +809,11 @@ def test_backoffice_string_filter(pub):
'lte',
'gt',
'gte',
'between',
'in',
'not_in',
'absent',
'existing',
]
resp.forms['listing-settings']['filter-4-value'].value = 'a'
@ -899,7 +921,14 @@ def test_backoffice_email_filter(pub):
assert resp.forms['listing-settings']['filter-4-value'].value == ''
assert resp.forms['listing-settings']['filter-4-operator'].value == 'eq'
assert [x[0] for x in resp.forms['listing-settings']['filter-4-operator'].options] == ['eq', 'ne']
assert [x[0] for x in resp.forms['listing-settings']['filter-4-operator'].options] == [
'eq',
'ne',
'in',
'not_in',
'absent',
'existing',
]
resp.forms['listing-settings']['filter-4-value'].value = 'a@localhost'
resp = resp.forms['listing-settings'].submit()
@ -959,6 +988,11 @@ def test_backoffice_date_filter(pub):
'lte',
'gt',
'gte',
'between',
'in',
'not_in',
'absent',
'existing',
]
resp.forms['listing-settings']['filter-4-value'].value = '2020-04-24'
@ -1305,6 +1339,11 @@ def test_backoffice_table_varname_filter(pub):
'lte',
'gt',
'gte',
'between',
'in',
'not_in',
'absent',
'existing',
]
resp = resp.forms['listing-settings'].submit()
@ -1418,6 +1457,11 @@ def test_backoffice_block_field_filter(pub):
'lte',
'gt',
'gte',
'between',
'in',
'not_in',
'absent',
'existing',
]
resp.forms['listing-settings']['filter-0-1-value'].value = 'plop0'
resp = resp.forms['listing-settings'].submit()
@ -1446,6 +1490,11 @@ def test_backoffice_block_field_filter(pub):
'lte',
'gt',
'gte',
'between',
'in',
'not_in',
'absent',
'existing',
]
resp.forms['listing-settings']['filter-0-2-value'].value = '1'
resp = resp.forms['listing-settings'].submit()
@ -1468,7 +1517,12 @@ def test_backoffice_block_field_filter(pub):
resp = resp.forms['listing-settings'].submit()
assert resp.forms['listing-settings']['filter-0-3-value'].value == ''
assert resp.forms['listing-settings']['filter-0-3-operator'].value == 'eq'
assert [x[0] for x in resp.forms['listing-settings']['filter-0-3-operator'].options] == ['eq', 'ne']
assert [x[0] for x in resp.forms['listing-settings']['filter-0-3-operator'].options] == [
'eq',
'ne',
'absent',
'existing',
]
resp.forms['listing-settings']['filter-0-3-value'].value = 'true'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('<tr') == 1 + 6
@ -1493,6 +1547,11 @@ def test_backoffice_block_field_filter(pub):
'lte',
'gt',
'gte',
'between',
'in',
'not_in',
'absent',
'existing',
]
resp.forms['listing-settings']['filter-0-4-value'].value = '2021-06-01'
resp = resp.forms['listing-settings'].submit()
@ -1514,7 +1573,14 @@ def test_backoffice_block_field_filter(pub):
resp = resp.forms['listing-settings'].submit()
assert resp.forms['listing-settings']['filter-0-5-value'].value == ''
assert resp.forms['listing-settings']['filter-0-5-operator'].value == 'eq'
assert [x[0] for x in resp.forms['listing-settings']['filter-0-5-operator'].options] == ['eq', 'ne']
assert [x[0] for x in resp.forms['listing-settings']['filter-0-5-operator'].options] == [
'eq',
'ne',
'in',
'not_in',
'absent',
'existing',
]
resp.forms['listing-settings']['filter-0-5-value'].value = 'a@localhost'
resp = resp.forms['listing-settings'].submit()
assert resp.text.count('<tr') == 1 + 6

View File

@ -1404,54 +1404,111 @@ def test_dynamic_item_field_from_custom_view_on_cards(pub, field_type):
formdef.fields = [
fields.PageField(id='2', label='1st page'),
fields.ItemField(id='0', label='item', varname='blah', items=['foo', 'bar', 'baz']),
fields.ItemField(id='3', label='item', varname='blah2', items=['foo', 'bar', 'baz']),
fields.ItemField(id='1', label='string', data_source=ds, display_disabled_items=True),
]
formdef.store()
resp = get_app(pub).get('/test/')
assert resp.form['f1'].options == [('', False, '---')]
resp.form['f0'] = 'baz'
live_resp = app.post('/test/live?modified_field_id=0', params=resp.form.submit_fields())
assert len(live_resp.json['result']['1']['items']) == 10
assert {str(x['id']) for x in live_resp.json['result']['1']['items']} == baz_ids
def test(app):
formdef.fields[3].display_mode = 'list'
formdef.store()
resp.form['f1'].options = []
for item in live_resp.json['result']['1']['items']:
# simulate javascript filling the <select>
resp.form['f1'].options.append((str(item['id']), False, item['text']))
resp = get_app(pub).get('/test/')
assert resp.form['f1'].options == [('', False, '---')]
resp.form['f0'] = 'baz'
resp.form['f3'] = 'foo'
live_resp = app.post('/test/live?modified_field_id=0', params=resp.form.submit_fields())
assert len(live_resp.json['result']['1']['items']) == 10
assert {str(x['id']) for x in live_resp.json['result']['1']['items']} == baz_ids
resp.form['f1'] = resp.form['f1'].options[0][0]
resp = resp.form.submit('submit') # -> validation page
assert 'Technical error' not in resp.text
resp = resp.form.submit('submit') # -> submit
assert formdef.data_class().select()[0].data['1'] in baz_ids
assert formdef.data_class().select()[0].data['1_structured']['item'] == 'baz'
resp.form['f1'].options = []
for item in live_resp.json['result']['1']['items']:
# simulate javascript filling the <select>
resp.form['f1'].options.append((str(item['id']), False, item['text']))
# same in autocomplete mode
formdef.fields[2].display_mode = 'autocomplete'
formdef.store()
app = get_app(pub)
resp = app.get('/test/')
# simulate select2 mode, with qommon.forms.js adding an extra hidden widget
resp.form.fields['f1_display'] = Hidden(form=resp.form, tag='input', name='f1_display', pos=10)
select2_url = resp.pyquery('select:last').attr['data-select2-url']
resp_json = app.get(select2_url + '?q=')
assert len(resp_json.json['data']) == 0
resp.form['f0'] = 'baz'
resp.form['f1'] = resp.form['f1'].options[0][0]
resp = resp.form.submit('submit') # -> validation page
assert 'Technical error' not in resp.text
resp = resp.form.submit('submit') # -> submit
assert formdef.data_class().select()[0].data['1'] in baz_ids
assert formdef.data_class().select()[0].data['1_structured']['item'] == 'baz'
live_resp = app.post('/test/live?modified_field_id=0', params=resp.form.submit_fields())
new_select2_url = live_resp.json['result']['1']['source_url']
resp_json = app.get(new_select2_url + '?q=')
assert len(resp_json.json['data']) == 10
assert {str(x['id']) for x in resp_json.json['data']} == baz_ids
# same in autocomplete mode
formdef.fields[3].display_mode = 'autocomplete'
formdef.store()
app = get_app(pub)
resp = app.get('/test/')
# simulate select2 mode, with qommon.forms.js adding an extra hidden widget
resp.form.fields['f1_display'] = Hidden(form=resp.form, tag='input', name='f1_display', pos=10)
select2_url = resp.pyquery('select:last').attr['data-select2-url']
resp_json = app.get(select2_url + '?q=')
assert len(resp_json.json['data']) == 0
resp.form['f0'] = 'baz'
resp.form['f3'] = 'foo'
resp.form['f1'].force_value(str(resp_json.json['data'][0]['id']))
resp.form.fields['f1_display'].force_value(resp_json.json['data'][0]['text'])
live_resp = app.post('/test/live?modified_field_id=0', params=resp.form.submit_fields())
new_select2_url = live_resp.json['result']['1']['source_url']
resp_json = app.get(new_select2_url + '?q=')
assert len(resp_json.json['data']) == 10
assert {str(x['id']) for x in resp_json.json['data']} == baz_ids
resp = resp.form.submit('submit') # -> validation page
resp = resp.form.submit('submit') # -> submit
assert formdef.data_class().select()[0].data['1'] in baz_ids
assert formdef.data_class().select()[0].data['1_structured']['item'] == 'baz'
resp.form['f1'].force_value(str(resp_json.json['data'][0]['id']))
resp.form.fields['f1_display'].force_value(resp_json.json['data'][0]['text'])
resp = resp.form.submit('submit') # -> validation page
resp = resp.form.submit('submit') # -> submit
assert formdef.data_class().select()[0].data['1'] in baz_ids
assert formdef.data_class().select()[0].data['1_structured']['item'] == 'baz'
test(app)
# operator with multi values - IN
resp = app.get('/backoffice/data/items/%s/' % custom_view.slug)
resp.forms['listing-settings']['filter-0-operator'] = 'in'
resp = resp.forms['listing-settings'].submit()
if field_type == 'item':
# some javascript to get a text input for filter value
assert resp.forms['listing-settings']['filter-0-value'].attrs['data-allow-template']
assert 'custom value' in [x[2] for x in resp.forms['listing-settings']['filter-0-value'].options]
resp.forms['listing-settings']['filter-0-value'].force_value(
'{{ form_var_blah }}|{{ form_var_blah }}'
)
else:
resp.forms['listing-settings']['filter-0-value'] = '{{ form_var_blah }}|{{ form_var_blah }}'
resp = resp.forms['listing-settings'].submit()
assert resp.forms['listing-settings']['filter-0-value'].value == '{{ form_var_blah }}|{{ form_var_blah }}'
assert resp.text.count('<tr') == 1 # thead only
# save custom view with filter
resp = resp.forms['save-custom-view'].submit().follow()
test(app)
if field_type != 'email':
# operator with multi values - BETWEEN
resp = app.get('/backoffice/data/items/%s/' % custom_view.slug)
resp.forms['listing-settings']['filter-0-operator'] = 'between'
resp = resp.forms['listing-settings'].submit()
if field_type == 'item':
# some javascript to get a text input for filter value
assert resp.forms['listing-settings']['filter-0-value'].attrs['data-allow-template']
assert 'custom value' in [x[2] for x in resp.forms['listing-settings']['filter-0-value'].options]
resp.forms['listing-settings']['filter-0-value'].force_value(
'{{ form_var_blah }}|{{ form_var_blah2 }}'
)
else:
resp.forms['listing-settings']['filter-0-value'] = '{{ form_var_blah }}|{{ form_var_blah2 }}'
resp = resp.forms['listing-settings'].submit()
assert (
resp.forms['listing-settings']['filter-0-value'].value
== '{{ form_var_blah }}|{{ form_var_blah2 }}'
)
assert resp.text.count('<tr') == 1 # thead only
# save custom view with filter
resp = resp.forms['save-custom-view'].submit().follow()
test(app)
# delete custom view
pub.loggederror_class.wipe()

View File

@ -51,10 +51,8 @@ from wcs.sql_criterias import (
ElementIntersects,
Equal,
FtsMatch,
Greater,
GreaterOrEqual,
Intersects,
Less,
LessOrEqual,
Not,
NotContains,
@ -65,10 +63,10 @@ from wcs.sql_criterias import (
StrictNotEqual,
get_field_id,
)
from wcs.variables import LazyFieldVar, LazyList, NoneFieldVar
from wcs.variables import LazyFieldVar, LazyFormDefObjectsManager, LazyList, NoneFieldVar
from wcs.workflows import WorkflowStatusItem, item_classes, template_on_formdata
from ..qommon import _, audit, errors, ezt, force_str, get_cfg, misc, ngettext, ods, pgettext_lazy, template
from ..qommon import _, audit, errors, ezt, get_cfg, misc, ngettext, ods, pgettext_lazy, template
from ..qommon.afterjobs import AfterJob
from ..qommon.evalutils import make_datetime
from ..qommon.form import (
@ -939,10 +937,16 @@ class FormPage(FormdefDirectoryBase):
criterias=None,
anonymised=False,
):
criterias = (criterias or [])[:]
# remove potential filter on self
filter_field_id = get_field_id(filter_field)
criterias = [x for x in criterias if x.attribute != filter_field_id]
filtered_criterias = []
for criteria in criterias or []:
if getattr(criteria, 'attribute', None) == filter_field_id:
continue
if isinstance(criteria, Not) and getattr(criteria.criteria, 'attribute', None) == filter_field_id:
continue
filtered_criterias.append(criteria)
criterias = filtered_criterias
# apply other filters
if not anonymised:
criterias.append(Null('anonymised'))
@ -1183,7 +1187,9 @@ class FormPage(FormdefDirectoryBase):
filter_field_operator_key = '%s-operator' % filter_field_key.replace('-value', '')
filter_field_operator = filters_dict.get(filter_field_operator_key) or 'eq'
operators = self.get_field_allowed_operators(filter_field)
lazy_manager = LazyFormDefObjectsManager(formdef=self.formdef)
operators = lazy_manager.get_field_allowed_operators(filter_field) or []
if filter_field.key == 'status':
operators = [
@ -1301,6 +1307,7 @@ class FormPage(FormdefDirectoryBase):
if filter_field.key == 'item' and filter_field.get_display_mode() == 'autocomplete':
display_mode = 'select2'
is_multi_values = filter_field_operator in ['in', 'not_in', 'between']
if display_mode == 'select':
options = self.get_item_filter_options(
filter_field,
@ -1312,7 +1319,9 @@ class FormPage(FormdefDirectoryBase):
options.insert(0, (None, '', ''))
attrs = {'data-refresh-options': str(filter_field.contextual_id)}
else:
options = [(filter_field_value, filter_field_value or '', filter_field_value or '')]
options = [(None, '', '')]
if not is_multi_values:
options = [(filter_field_value, filter_field_value or '', filter_field_value or '')]
attrs = {'data-remote-options': str(filter_field.contextual_id)}
get_response().add_javascript(
['jquery.js', '../../i18n.js', 'qommon.forms.js', 'select2.js']
@ -1323,6 +1332,8 @@ class FormPage(FormdefDirectoryBase):
if filter_field_value and filter_field_value not in [x[0] for x in options]:
options.append((filter_field_value, filter_field_value, filter_field_value))
attrs['data-allow-template'] = 'true'
if is_multi_values:
attrs['data-multi-values'] = filter_field_value
widget = SingleSelectWidget(
filter_field_key,
@ -1756,39 +1767,6 @@ class FormPage(FormdefDirectoryBase):
query_overrides = get_request().form
return self.get_view_criterias(query_overrides, statistics_fields_only=statistics_fields_only)
def get_field_allowed_operators(self, field):
operators = [
('eq', '='),
('ne', '!='),
]
full_operators = operators + [
('lt', '<'),
('lte', '<='),
('gt', '>'),
('gte', '>='),
]
if field.key in ['internal-id', 'date', 'item', 'items', 'string']:
return full_operators
if field.key in ['bool', 'email']:
return operators
return None
def get_field_criteria(self, field, operator, field_key):
mapping = {
'eq': Equal,
'ne': NotEqual,
'lt': Less,
'lte': LessOrEqual,
'gt': Greater,
'gte': GreaterOrEqual,
}
operators = self.get_field_allowed_operators(field)
if operators is None:
return
if operator not in [o[0] for o in operators]:
raise RequestError('Invalid operator "%s" for "%s"' % (operator, field_key))
return mapping[operator]
def get_view_criterias(
self,
query_overrides=None,
@ -1939,10 +1917,17 @@ class FormPage(FormdefDirectoryBase):
# get operator and criteria
filter_field_operator_key = '%s-operator' % filter_field_key.replace('-value', '')
filter_field_operator = filters_dict.get(filter_field_operator_key) or 'eq'
criteria = self.get_field_criteria(filter_field, filter_field_operator, filter_field_key)
report_error_type = 'request-error' if self.view_type == 'json' else 'session-error'
lazy_manager = LazyFormDefObjectsManager(
formdef=self.formdef, report_error_type=report_error_type
)
# check value types
if filter_field.key == 'internal-id':
if filter_field_operator not in ['eq', 'ne', 'lt', 'lte', 'gt', 'gte']:
raise RequestError(
'Invalid operator "%s" for "filter-internal-id"' % (filter_field_operator)
)
def _report_error(value, operator):
if custom_view:
@ -1965,7 +1950,8 @@ class FormPage(FormdefDirectoryBase):
if Template.is_template_string(filter_field_value, ezt_support=False):
if keep_templates:
criterias.append(criteria('id', filter_field_value))
# use Equal criteria here, the only use is in CardDef.get_data_source_referenced_varnames
criterias.append(Equal('id', filter_field_value))
continue
if not compile_templates:
criterias.append(Nothing())
@ -2002,11 +1988,8 @@ class FormPage(FormdefDirectoryBase):
filter_date_value = misc.get_as_datetime(filter_field_value).timetuple()
except ValueError:
continue
elif filter_field.key == 'date':
try:
filter_field_value = misc.get_as_datetime(filter_field_value).date().strftime('%Y-%m-%d')
except ValueError:
continue
elif filter_field_value == 'on' and filter_field_operator in ['absent', 'existing']:
filter_field_value = Ellipsis
elif filter_field.key == 'bool':
if filter_field_value == 'true':
filter_field_value = True
@ -2017,24 +2000,36 @@ class FormPage(FormdefDirectoryBase):
elif filter_field.key in ('item', 'items', 'string', 'email'):
if Template.is_template_string(filter_field_value, ezt_support=False):
if keep_templates:
criterias.append(criteria(filter_field.id, filter_field_value))
# use Equal criteria here, the only use is in CardDef.get_data_source_referenced_varnames
criterias.append(Equal(filter_field.id, filter_field_value))
continue
if not compile_templates:
criterias.append(Nothing())
continue
filter_field_value = WorkflowStatusItem.compute(filter_field_value)
if filter_field.key in ('item', 'items', 'bool', 'string', 'email', 'date'):
operators = lazy_manager.get_field_allowed_operators(filter_field) or []
if filter_field_operator not in [o[0] for o in operators]:
raise RequestError(
'Invalid operator "%s" for "%s"' % (filter_field_operator, filter_field_key)
)
try:
# cast to integer so it can be used with numerical operators
# (limit to 32bits to match postgresql integer range)
int_filter_field_value = int(filter_field_value)
if -(2**31) <= int_filter_field_value < 2**31:
filter_field_value = int_filter_field_value
except (ValueError, TypeError):
filter_field_value = str(filter_field_value)
filter_field_value = lazy_manager.format_value(
op=filter_field_operator,
value=filter_field_value,
field=filter_field,
)
except ValueError:
continue
# add criteria
if filter_field.key == 'internal-id':
criterias.append(criteria('id', filter_field_value))
criterias.append(
lazy_manager.get_criteria_from_operator(
op=filter_field_operator, value=filter_field_value, field_id='id'
)
)
elif filter_field.key == 'number':
criterias.append(Equal('id_display', str(filter_field_value)))
elif filter_field.key == 'period-date':
@ -2088,7 +2083,14 @@ class FormPage(FormdefDirectoryBase):
center = misc.normalize_geolocation({'lat': center_lat, 'lon': center_lon})
criterias.append(Distance(center, float(filter_field_value)))
elif filter_field.key in ('item', 'items', 'bool', 'string', 'email', 'date'):
criterias.append(criteria('f%s' % filter_field.id, filter_field_value, field=filter_field))
criterias.append(
lazy_manager.get_criteria_from_operator(
op=filter_field_operator,
value=filter_field_value,
field_id='f%s' % filter_field.id,
field=filter_field,
)
)
if filter_field.key in ('item', 'items'):
criterias[-1]._label = '%s: %s' % (
filter_field.label,

View File

@ -2511,7 +2511,7 @@ del {
border-bottom: 1px solid #AAA;
appearance: auto;
padding-right: 0;
width: 4em;
width: 6em;
background: none;
}
}

View File

@ -626,3 +626,14 @@ div.file-button .widget-message {
}
}
}
.multi-value-filter {
&--add-value-button,
&--remove-value-button {
width: 2.2rem;
height: 2.2rem;
padding: 0;
margin: auto 0 auto 0.5rem;
}
}

View File

@ -152,6 +152,40 @@ function refresh_table() {
{qs: $('form#listing-settings').serialize(), auto: false});
}
function prepare_select2($elem) {
var filter_field_id = $elem.data('remote-options');
var options = {
language: {
errorLoading: function() { return WCS_I18N.s2_errorloading; },
noResults: function () { return WCS_I18N.s2_nomatches; },
inputTooShort: function (input, min) { return WCS_I18N.s2_tooshort; },
loadingMore: function () { return WCS_I18N.s2_loadmore; },
searching: function () { return WCS_I18N.s2_searching; },
},
placeholder: '',
allowClear: true,
minimumInputLength: 1,
ajax: {
url: function() {
var pathname = window.location.pathname.replace(/^\/+/, '/').replace(/stats$/, '');
var filter_settings = $('form#listing-settings').serialize();
return pathname + 'filter-options?filter_field_id=' + filter_field_id + '&' + filter_settings;
},
dataType: 'json',
data: function(params) {
var query = {
_search: params.term,
}
return query;
},
processResults: function (data, params) {
return {results: data.data};
},
},
};
$elem.select2(options);
}
$(document).on('backoffice-filter-change', function(event, listing_settings) {
/* makes sure it doesn't start with a double slash */
var pathname = window.location.pathname.replace(/^\/+/, '/');
@ -226,7 +260,13 @@ $(document).on('backoffice-filter-change', function(event, listing_settings) {
var $option = $('<option></option>', {value: current_value, text: current_value});
$option.attr('selected', 'selected');
$option.appendTo($select);
} else if (current_value && !found_current_value && $select.is('[data-multi-values]')) {
var $option = $('<option data-option-for-multi-values>multi</option>');
$option.attr('value', current_value);
$option.attr('selected', 'selected');
$option.appendTo($select);
}
$('.operator select').trigger('change', true);
}
});
});
@ -309,37 +349,7 @@ $(function() {
/* set filter options from server (select2) */
$('[data-remote-options]').each(function(idx, elem) {
var filter_field_id = $(elem).data('remote-options');
var options = {
language: {
errorLoading: function() { return WCS_I18N.s2_errorloading; },
noResults: function () { return WCS_I18N.s2_nomatches; },
inputTooShort: function (input, min) { return WCS_I18N.s2_tooshort; },
loadingMore: function () { return WCS_I18N.s2_loadmore; },
searching: function () { return WCS_I18N.s2_searching; },
},
placeholder: '',
allowClear: true,
minimumInputLength: 1,
ajax: {
url: function() {
var pathname = window.location.pathname.replace(/^\/+/, '/').replace(/stats$/, '');
var filter_settings = $('form#listing-settings').serialize();
return pathname + 'filter-options?filter_field_id=' + filter_field_id + '&' + filter_settings;
},
dataType: 'json',
data: function(params) {
var query = {
_search: params.term,
}
return query;
},
processResults: function (data, params) {
return {results: data.data};
},
},
};
$(elem).select2(options);
prepare_select2($(elem));
});
$('button#save-view').on('click', function() {
@ -372,7 +382,7 @@ $(function() {
});
/* automatically refresh onfilter change */
$('form#listing-settings input[type=date], form#listing-settings input[type=text], form#listing-settings select').change(function() {
$(document).on('change', 'form#listing-settings input[type=date], form#listing-settings input[type=text], form#listing-settings .value select', function () {
if (this.type == 'date' && $(this).val() && $(this).val()[0] == '0') {
// input date with year starting with 0, it's currently being typed in,
// wait for the year to be complete before acting on it.
@ -400,6 +410,197 @@ $(function() {
return false;
});
/* operator selection */
$('.operator select').change(function (event, init) {
var operator = $(this).val();
var $container = $(this).parents('.operator-and-value-widget');
var $value_container = $container.find('.value:not([data-for-multi-values])');
var $value_input = $value_container.find('input');
var $value_select = $value_container.find('select');
$value_container.show();
// remove added 'on' value of select options
$value_select.find('option[data-on-option]').remove();
// remove inputs and selects for multi values
$container.find('.value[data-for-multi-values]').each(function() {
$(this).remove();
if ($(this).data('select2-id')) {
$(this).select2('destroy');
}
});
// remove added 'multi' value of select options
$value_select.find('option[data-option-for-multi-values]').remove();
if (operator == 'absent' || operator == 'existing') {
// absent or existing filter, no input/select to set a value, but 'on' value is needed to apply filter
$value_input.val('on');
if ($value_select.length) {
// add an 'on' value for absent/existing operators
$('<option value="on" data-on-option>on</option>').appendTo($value_select);
}
$value_select.val('on');
$value_container.hide();
} else {
if ($value_input.val() == 'on') {
$value_input.val('');
}
if ($value_select.val() == 'on') {
$value_select.val('');
}
}
if (operator == 'between' || operator == 'in' || operator == 'not_in') {
// operator with multi values
// hide original value input or select
$value_container.hide();
// multi input/select needed
var $value_element = null;
if ($value_select.length) {
$value_element = $value_select;
} else {
$value_element = $value_input;
}
var values = [];
if ($value_element.attr('data-multi-values')) {
values = $value_element.attr('data-multi-values').split('|');
} else if ($value_element.val()) {
if ($value_element.val().indexOf('|') == -1) {
values = [$value_element.val()];
} else {
values = $value_element.val().split('|')
}
}
for (let value of values) {
var $copy = $value_container.clone().attr('data-for-multi-values', '').show();
$copy.find($value_element.prop('tagName'))
.removeAttr('id')
.removeAttr('data-refresh-options')
.removeAttr('data-multi-values')
.attr('name', 'multi-' + $value_element.attr('name'))
.val(value);
if($value_input.length) {
$remove_button = $('<button class="multi-value-filter--remove-value-button">-</button>')
$remove_button.click(function() {
values = values.filter(item => item !== value)
if($value_select.length) {
$value_select
.find('option[data-option-for-multi-values]')
.attr('value', values.join('|'));
}
else {
$value_input.val(values.join('|'))
}
$('form#listing-settings').submit();
});
$copy.append($remove_button);
}
$container.append($copy);
if ($value_select.data('select2-id')) {
$copy.find('span.select2').remove();
if (! $copy.find('option[value="' + value + '"]').length) {
var $option = $('<option></option>', {value: value, text: value});
$option.attr('selected', 'selected');
$option.appendTo($copy.find('select'));
}
$select = $copy.find('select')
prepare_select2($select);
$select.on('select2:opening', (e) => {
e.preventDefault()
})
}
}
var $add_item = $value_container.clone().attr('data-for-multi-values', '').show();
$add_item.find($value_element.prop('tagName'))
.removeAttr('id')
.removeAttr('data-refresh-options')
.removeAttr('data-multi-values')
.attr('name', 'multi-' + $value_element.attr('name'))
.val('');
if($value_input.length) {
$add_button = $('<button class="multi-value-filter--add-value-button">+</button>')
$add_item.append($add_button)
$add_button.click(function() {
$('form#listing-settings').submit();
});
}
if ($value_select.data('select2-id')) {
$add_item.find('span.select2').remove();
prepare_select2($add_item.find('select'));
}
$container.append($add_item)
if ($value_select.length && values.length) {
// add an option for 'multi' value
var $option = $('<option data-option-for-multi-values>multi</option>');
$option.attr('value', values.join('|'));
$option.attr('selected', 'selected');
$option.appendTo($value_select);
$value_select.attr('data-multi-values', values.join('|'));
} else if ($value_input.length && values.length) {
$value_input.val(values.join('|'));
}
} else {
// remove multi values
if ($value_input.length && $value_input.val()) {
var values = $value_input.val().split('|');
$value_input.val(values[0]);
}
if ($value_select.length && $value_select.attr('data-multi-values')) {
var values = $value_select.attr('data-multi-values').split('|');
$value_select.val(values[0]);
$value_select.removeAttr('data-multi-values')
if ($value_select.data('select2-id') && ! $value_select.find('option[value="' + values[0] + '"]').length) {
var $option = $('<option></option>', {value: values[0], text: values[0]});
$option.attr('selected', 'selected');
$option.appendTo($value_select);
}
}
}
if (!init) {
$('form#listing-settings').submit();
}
});
$('.operator select').trigger('change', true);
$(document).on('change', '.value[data-for-multi-values] input, .value[data-for-multi-values] select', function () {
var $container = $(this).parents('.operator-and-value-widget');
var $operator = $container.find('.operator select');
var $value_input = $container.find('.value:not([data-for-multi-values]) input');
var $value_select = $container.find('.value:not([data-for-multi-values]) select');
var values = [];
$container.find('.value[data-for-multi-values] input, .value[data-for-multi-values] select').each(function () {
if ($(this).val()) {
values.push($(this).val());
}
});
if ($operator.val() == 'between' && values.length != 2) {
// wrong number of values, don't call backoffice
return;
}
if ($value_input.length) {
// set multi value
$value_input.val(values.join('|'));
} else {
// add a 'multi' value
var $option = $('<option data-option-for-multi-values>multi</option>');
$option.attr('value', values.join('|'));
$option.attr('selected', 'selected');
$option.appendTo($value_select);
// and update data-multi-values
$value_select.attr('data-multi-values', values.join('|'));
}
// submit form
$('form#listing-settings').submit();
});
/* refresh every 30 seconds (idle_id) after any user activity
* on inactivity for more than 5 minutes (longidle_id), stop refreshing (clear idle_id)
*/

View File

@ -170,7 +170,7 @@ class Criteria:
def __repr__(self):
return '<%s (attribute: %r%s)>' % (
self.__class__.__name__,
self.attribute,
getattr(self, 'attribute', None),
', value: %r' % self.value if hasattr(self, 'value') else '',
)

View File

@ -19,7 +19,8 @@ import warnings
from django.utils import formats
from django.utils.encoding import force_str
from django.utils.functional import SimpleLazyObject
from quixote import get_publisher, get_request
from quixote import get_publisher, get_request, get_session
from quixote.errors import RequestError
from wcs.sql_criterias import (
And,
@ -51,7 +52,14 @@ from .qommon.templatetags.qommon import parse_datetime
class LazyFormDefObjectsManager:
def __init__(
self, formdef, formdata=None, geoloc_center_formdata=None, criterias=None, order_by=None, limit=None
self,
formdef,
formdata=None,
geoloc_center_formdata=None,
criterias=None,
order_by=None,
limit=None,
report_error_type=None,
):
self._formdef = formdef
self._formdata = formdata
@ -69,6 +77,18 @@ class LazyFormDefObjectsManager:
self._order_by = order_by
self._limit = limit
self._cached_resultset = None
self._report_error_type = report_error_type or 'record-error'
def report_error(self, error_message):
if self._report_error_type == 'request-error':
raise RequestError(error_message)
if self._report_error_type == 'session-error':
get_session().message = ('warning', error_message)
if self._report_error_type == 'record-error':
get_publisher().record_error(
error_message,
formdata=self._formdata,
)
@property # @property for backward compatibility
def count(self):
@ -88,9 +108,8 @@ class LazyFormDefObjectsManager:
def order_by(self, attribute):
if not isinstance(attribute, str):
get_publisher().record_error(
self.report_error(
_('Invalid value %r for "order_by"') % attribute,
formdata=self._formdata,
)
return self.none()
field = self.get_field(attribute)
@ -119,10 +138,9 @@ class LazyFormDefObjectsManager:
def filter_by_user(self, user, op='eq'):
if op not in ['eq']:
get_publisher().record_error(
_('Invalid operator "%(op)s" for filter "%(filter)s"')
% {'op': self.get_operator_name(op), 'filter': self.pending_attr},
formdata=self._formdata,
self.report_error(
_('Invalid operator "%(operator)s" for filter "%(filter)s"')
% {'operator': self.get_operator_name(op), 'filter': self.pending_attr},
)
return self.none()
if isinstance(user, str):
@ -133,10 +151,9 @@ class LazyFormDefObjectsManager:
def filter_by_status(self, status, op='eq'):
if op not in ['eq', 'ne']:
get_publisher().record_error(
_('Invalid operator "%(op)s" for filter "%(filter)s"')
% {'op': self.get_operator_name(op), 'filter': self.pending_attr},
formdata=self._formdata,
self.report_error(
_('Invalid operator "%(operator)s" for filter "%(filter)s"')
% {'operator': self.get_operator_name(op), 'filter': self.pending_attr},
)
return self.none()
for wfs in self._formdef.workflow.possible_status:
@ -205,19 +222,21 @@ class LazyFormDefObjectsManager:
return qs
def filter_by_internal_id(self, value, op='eq'):
if op not in ['eq', 'ne', 'lt', 'lte', 'gt', 'gte']:
get_publisher().record_error(
_('Invalid operator "%(op)s" for filter "%(filter)s"')
% {'op': self.get_operator_name(op), 'filter': self.pending_attr},
formdata=self._formdata,
from wcs.backoffice.management import FakeField
field = FakeField('internal-id', 'internal-id', 'fake')
operators = self.get_field_allowed_operators(field)
if op not in [o[0] for o in operators]:
self.report_error(
_('Invalid operator "%(operator)s" for filter "%(filter)s"')
% {'operator': self.get_operator_name(op), 'filter': self.pending_attr},
)
return self.none()
try:
int(value)
except (ValueError, TypeError):
get_publisher().record_error(
self.report_error(
_('Invalid value "%s" for filter "internal_id"') % (value),
formdata=self._formdata,
)
return self.none()
return self._clone(
@ -226,10 +245,9 @@ class LazyFormDefObjectsManager:
def filter_by_number(self, value, op='eq'):
if op not in ['eq']:
get_publisher().record_error(
self.report_error(
_('Invalid operator "%(op)s" for filter "%(filter)s"')
% {'op': self.get_operator_name(op), 'filter': self.pending_attr},
formdata=self._formdata,
)
return self.none()
return self._clone(self._criterias + [Equal('id_display', str(value))])
@ -248,15 +266,41 @@ class LazyFormDefObjectsManager:
return field
def get_field_allowed_operators(self, field):
operators = ['eq', 'ne', 'absent', 'existing']
more_operators = operators + ['in', 'not_in']
full_operators = more_operators + ['lt', 'lte', 'gt', 'gte', 'between']
equality_operators = [
('eq', '='),
('ne', '!='),
]
comparison_operators = [
('lt', '<'),
('lte', '<='),
('gt', '>'),
('gte', '>='),
]
more_comparison_operators = [
('between', _('between')),
]
empty_operators = [
('absent', _('absent')),
('existing', _('existing')),
]
in_operators = [
('in', _('in')),
('not_in', _('not in')),
]
if field.key == 'internal-id':
return equality_operators + comparison_operators
if field.key in ['date', 'item', 'items', 'string']:
return full_operators
return (
equality_operators
+ comparison_operators
+ more_comparison_operators
+ in_operators
+ empty_operators
)
if field.key == 'bool':
return operators
return equality_operators + empty_operators
if field.key == 'email':
return more_operators
return equality_operators + in_operators + empty_operators
return None
def format_value(self, op, value, field):
@ -276,10 +320,9 @@ class LazyFormDefObjectsManager:
try:
value = field.convert_value_from_anything(value)
except (ValueError, AttributeError):
get_publisher().record_error(
self.report_error(
_('Invalid value "%(value)s" for filter "%(filter)s"')
% {'value': value, 'filter': self.pending_attr},
formdata=self._formdata,
)
raise ValueError
if value is not None and hasattr(field, 'block_field') and hasattr(field, 'get_json_value'):
@ -300,14 +343,18 @@ class LazyFormDefObjectsManager:
convert_value(value=x.strip(), field=field) for x in str(value).split('|') if x.strip()
]
if op == 'between' and len(new_value) != 2:
get_publisher().record_error(
pending_attr = getattr(self, 'pending_attr', None)
if not pending_attr:
pending_attr = field.varname or field.id
if self._report_error_type == 'request-error':
pending_attr = 'filter-%s' % pending_attr
self.report_error(
_('Invalid value "%(value)s" for operator "%(operator)s" and filter "%(filter)s"')
% {
'value': value,
'operator': self.get_operator_name(op),
'filter': self.pending_attr,
'filter': pending_attr,
},
formdata=self._formdata,
)
raise ValueError
value = new_value
@ -402,9 +449,7 @@ class LazyFormDefObjectsManager:
fields = list(self.get_fields(self.pending_attr))
if not fields:
get_publisher().record_error(
_('Invalid filter "%s"') % self.pending_attr, formdata=self._formdata
)
self.report_error(_('Invalid filter "%s"') % self.pending_attr)
return self.none()
# check operator
@ -412,11 +457,10 @@ class LazyFormDefObjectsManager:
if field.key not in ['date', 'item', 'items', 'string', 'bool', 'email']:
continue
operators = self.get_field_allowed_operators(field) or []
if op not in operators:
get_publisher().record_error(
if op not in [o[0] for o in operators]:
self.report_error(
_('Invalid operator "%(operator)s" for filter "%(filter)s"')
% {'operator': self.get_operator_name(op), 'filter': self.pending_attr},
formdata=self._formdata,
)
return self.none()
@ -453,9 +497,8 @@ class LazyFormDefObjectsManager:
def apply_exclude_value(self, value):
if hasattr(self, 'pending_op'):
get_publisher().record_error(
self.report_error(
_('Operator filter is not allowed for exclude_value filter'),
formdata=self._formdata,
)
return self.none()
return self.apply_filter_value(value, exclude=True)