0001-csvdatasource-remove-advanced-lookup-filters-13748.patch
passerelle/apps/csvdatasource/lookups.py | ||
---|---|---|
1 |
DELIMITER = '__' |
|
2 | ||
3 |
class InvalidOperatorError(Exception): |
|
4 |
pass |
|
5 | ||
6 |
compare_str = cmp |
|
7 | ||
8 | ||
9 |
def is_int(value): |
|
10 |
try: |
|
11 |
int(value) |
|
12 |
return True |
|
13 |
except (ValueError, TypeError): |
|
14 |
return False |
|
15 | ||
16 | ||
17 |
class Lookup(object): |
|
18 | ||
19 |
def contains(self, key, value): |
|
20 |
return lambda x: value in x[key] |
|
21 | ||
22 |
def icontains(self, key, value): |
|
23 |
return lambda x: value.lower() in x[key].lower() |
|
24 | ||
25 |
def gt(self, key, value): |
|
26 |
return lambda x: int(x[key]) > int(value) |
|
27 | ||
28 |
def igt(self, key, value): |
|
29 |
return lambda x: compare_str(x[key].lower(), value.lower()) > 0 |
|
30 | ||
31 |
def ge(self, key, value): |
|
32 |
return lambda x: int(x[key]) >= int(value) |
|
33 | ||
34 |
def ige(self, key, value): |
|
35 |
return lambda x: compare_str(x[key].lower(), value.lower()) >= 0 |
|
36 | ||
37 |
def lt(self, key, value): |
|
38 |
return lambda x: int(x[key]) < int(value) |
|
39 | ||
40 |
def ilt(self, key, value): |
|
41 |
return lambda x: compare_str(x[key].lower(), value.lower()) < 0 |
|
42 | ||
43 |
def le(self, key, value): |
|
44 |
return lambda x: int(x[key]) <= int(value) |
|
45 | ||
46 |
def ile(self, key, value): |
|
47 |
return lambda x: compare_str(x[key].lower(), value.lower()) <= 0 |
|
48 | ||
49 |
def eq(self, key, value): |
|
50 |
if is_int(value): |
|
51 |
return lambda x: int(value) == int(x[key]) |
|
52 |
return lambda x: value == x[key] |
|
53 | ||
54 |
def ieq(self, key, value): |
|
55 |
return lambda x: value.lower() == x[key].lower() |
|
56 | ||
57 |
def ne(self, key, value): |
|
58 |
if is_int(value): |
|
59 |
return lambda x: int(value) != int(x[key]) |
|
60 |
return lambda x: value != x[key] |
|
61 | ||
62 |
def ine(self, key, value): |
|
63 |
return lambda x: value.lower() != x[key].lower() |
|
64 | ||
65 | ||
66 |
def get_lookup(operator, key, value): |
|
67 |
try: |
|
68 |
return getattr(Lookup(), operator)(key, value) |
|
69 |
except (AttributeError,): |
|
70 |
raise InvalidOperatorError('%s is not a valid operator' % operator) |
passerelle/apps/csvdatasource/models.py | ||
---|---|---|
35 | 35 |
from passerelle.utils.jsonresponse import APIError |
36 | 36 |
from passerelle.utils.api import endpoint |
37 | 37 | |
38 |
import lookups |
|
39 | ||
40 | 38 |
identifier_re = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE) |
41 | 39 | |
42 | 40 | |
... | ... | |
242 | 240 |
for data in self.get_cached_rows(initial=False): |
243 | 241 |
yield data |
244 | 242 | |
245 |
def get_data(self, filters=None):
|
|
243 |
def get_data(self, filters={}, case_insensitive=False):
|
|
246 | 244 |
titles = [t.strip() for t in self.columns_keynames.split(',')] |
247 | 245 | |
248 |
# validate filters (appropriate columns must exist) |
|
249 |
if filters: |
|
250 |
for filter_key in filters.keys(): |
|
251 |
if not filter_key.split(lookups.DELIMITER)[0] in titles: |
|
252 |
del filters[filter_key] |
|
246 |
for filter_key in filters.keys(): |
|
247 |
# allow 'q' filter |
|
248 |
if filter_key == 'q': |
|
249 |
continue |
|
250 |
if filter_key not in titles: |
|
251 |
del filters[filter_key] |
|
253 | 252 | |
254 | 253 |
rows = self.get_cached_rows() |
255 | 254 |
data = [] |
256 | 255 | |
257 | 256 |
# build a generator of all filters |
258 |
def filters_generator(filters, titles):
|
|
257 |
def filters_generator(filters, case_insensitive):
|
|
259 | 258 |
if not filters: |
260 | 259 |
return |
261 | 260 |
for key, value in filters.items(): |
262 |
try: |
|
263 |
key, op = key.split(lookups.DELIMITER) |
|
264 |
except (ValueError,): |
|
265 |
op = 'eq' |
|
266 |
yield lookups.get_lookup(op, key, value) |
|
261 |
def operation(key, value, case_insensitive): |
|
262 |
if key == 'q': |
|
263 |
return lambda x: value.lower() in x['text'].lower() |
|
264 |
if case_insensitive: |
|
265 |
return lambda x: value.lower() == x[key].lower() |
|
266 |
return lambda x: value == x[key] |
|
267 |
yield operation(key, value, case_insensitive) |
|
267 | 268 | |
268 | 269 |
# apply filters to data |
269 | 270 |
def super_filter(filters, data): |
... | ... | |
272 | 273 |
return data |
273 | 274 | |
274 | 275 |
data = list(super_filter( |
275 |
filters_generator(filters, titles), rows
|
|
276 |
filters_generator(filters, case_insensitive), rows
|
|
276 | 277 |
)) |
277 | 278 | |
278 | 279 |
return data |
passerelle/apps/csvdatasource/views.py | ||
---|---|---|
34 | 34 |
def _filters_builder(self, request): |
35 | 35 |
filters = {} |
36 | 36 |
obj = self.get_object() |
37 | ||
38 | 37 |
params = request.GET |
39 | ||
40 |
case_insensitive = 'case-insensitive' in params |
|
41 | 38 |
query = params.get('q', None) |
42 | 39 | |
43 | 40 |
if query: |
44 |
if case_insensitive: |
|
45 |
filters['text__icontains'] = query.lower() |
|
46 |
else: |
|
47 |
filters['text__contains'] = query |
|
41 |
filters['q'] = query |
|
48 | 42 | |
49 | 43 |
# builds filters according to csv file header |
50 | 44 |
for column_title in [t.strip() for t in obj.columns_keynames.split(',') if t]: |
51 |
match = filter( |
|
52 |
(lambda ct: lambda x: x.startswith(ct))(column_title), params.keys() |
|
53 |
) |
|
54 |
for key in match: |
|
55 |
if case_insensitive: |
|
56 |
filters[key + '__ieq'] = params[key].lower() |
|
57 |
else: |
|
58 |
filters[key] = params[key] |
|
59 | ||
60 |
if 'text' in filters: |
|
61 |
if case_insensitive: |
|
62 |
filters['text__ieq'] = filters['text'].lower() |
|
63 |
else: |
|
64 |
filters['text__eq'] = filters['text'] |
|
65 |
filters.pop('text') |
|
45 |
if column_title in params.keys(): |
|
46 |
filters[column_title] = params[column_title] |
|
66 | 47 | |
67 | 48 |
return filters |
68 | 49 | |
... | ... | |
72 | 53 |
def get(self, request, *args, **kwargs): |
73 | 54 |
obj = self.get_object() |
74 | 55 |
filters = self._filters_builder(request) |
75 |
return {'data': obj.get_data(filters)} |
|
56 |
case_insensitive = 'case-insensitive' in request.GET |
|
57 |
return {'data': obj.get_data(filters, case_insensitive)} |
|
76 | 58 | |
77 | 59 | |
78 | 60 |
class NewQueryView(CreateView): |
tests/test_csv_datasource.py | ||
---|---|---|
234 | 234 |
assert result[0]['text'] == 'Eliot' |
235 | 235 |
assert len(result) == 1 |
236 | 236 | |
237 |
def test_advanced_filters(client, setup, filetype): |
|
238 |
csvdata, url = setup(filename=filetype, data=get_file_content(filetype)) |
|
239 |
filters = {'id__gt':20, 'id__lt': 40} |
|
240 |
resp = client.get(url, filters) |
|
241 |
result = parse_response(resp) |
|
242 |
assert len(result) == 3 |
|
243 |
for stuff in result: |
|
244 |
assert stuff['id'] in ('22', '36', '38') |
|
245 | ||
246 |
def test_advanced_filters_combo(client, setup, filetype): |
|
247 |
csvdata, url = setup(filename=filetype, data=get_file_content(filetype)) |
|
248 |
filters = { |
|
249 |
'id__ge': '20', |
|
250 |
'id__lt': '40', |
|
251 |
'fam__gt': '234', |
|
252 |
'fam__le': '235', |
|
253 |
'fname__icontains': 'Sandra' |
|
254 |
} |
|
255 |
resp = client.get(url, filters) |
|
256 |
result = parse_response(resp) |
|
257 |
assert len(result) == 1 |
|
258 |
assert result[0]['id'] == '22' |
|
259 |
assert result[0]['lname'] == 'MARTIN' |
|
260 | ||
261 |
def test_unknown_operator(client, setup, filetype): |
|
262 |
csvdata, url = setup(filename=filetype, data=get_file_content(filetype)) |
|
263 |
filters = {'id__whatever': '25', 'fname__icontains':'Eliot'} |
|
264 |
resp = client.get(url, filters) |
|
265 |
result = json.loads(resp.content) |
|
266 |
assert result['err'] == 1 |
|
267 |
assert result['err_class'] == 'passerelle.apps.csvdatasource.lookups.InvalidOperatorError' |
|
268 |
assert result['err_desc'] == 'whatever is not a valid operator' |
|
269 | ||
270 | 237 | |
271 | 238 |
def test_dialect(client, setup): |
272 | 239 |
csvdata, url = setup(data=data) |
... | ... | |
280 | 247 |
} |
281 | 248 | |
282 | 249 |
assert expected == csvdata.dialect_options |
283 |
filters = {'id__gt': '20', 'id__lt': '40', 'fname__icontains': 'Sandra'}
|
|
250 |
filters = {'id': '22', 'fname': 'Sandra'}
|
|
284 | 251 |
resp = client.get(url, filters) |
285 | 252 |
result = parse_response(resp) |
286 | 253 |
assert len(result) == 1 |
287 |
- |