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): |
|
246 |
titles = [t.strip() for t in self.columns_keynames.split(',')] |
|
247 | ||
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] |
|
253 | ||
254 |
rows = self.get_cached_rows() |
|
255 |
data = [] |
|
256 | ||
257 |
# build a generator of all filters |
|
258 |
def filters_generator(filters, titles): |
|
259 |
if not filters: |
|
260 |
return |
|
261 |
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) |
|
267 | ||
268 |
# apply filters to data |
|
269 |
def super_filter(filters, data): |
|
270 |
for f in filters: |
|
271 |
data = itertools.ifilter(f, data) |
|
272 |
return data |
|
273 | ||
274 |
data = list(super_filter( |
|
275 |
filters_generator(filters, titles), rows |
|
276 |
)) |
|
277 | ||
278 |
return data |
|
279 | ||
280 | 243 |
@property |
281 | 244 |
def titles(self): |
282 | 245 |
return [smart_text(t.strip()) for t in self.columns_keynames.split(',')] |
... | ... | |
288 | 251 |
query = Query.objects.get(resource=self.id, slug=query_name) |
289 | 252 |
except Query.DoesNotExist: |
290 | 253 |
raise APIError(u'no such query') |
254 |
return self.execute_query(request, query, **kwargs) |
|
291 | 255 | |
256 |
def execute_query(self, request, query, **kwargs): |
|
292 | 257 |
titles = self.titles |
293 | 258 |
data = self.get_cached_rows() |
294 | 259 |
passerelle/apps/csvdatasource/views.py | ||
---|---|---|
31 | 31 |
class CsvDataView(View, SingleObjectMixin): |
32 | 32 |
model = CsvDataSource |
33 | 33 | |
34 |
def _filters_builder(self, request): |
|
35 |
filters = {} |
|
36 |
obj = self.get_object() |
|
37 | ||
38 |
params = request.GET |
|
39 | ||
40 |
case_insensitive = 'case-insensitive' in params |
|
41 |
query = params.get('q', None) |
|
42 | ||
43 |
if query: |
|
44 |
if case_insensitive: |
|
45 |
filters['text__icontains'] = query.lower() |
|
46 |
else: |
|
47 |
filters['text__contains'] = query |
|
48 | ||
49 |
# builds filters according to csv file header |
|
50 |
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') |
|
66 | ||
67 |
return filters |
|
68 | ||
69 | ||
70 | 34 |
@utils.protected_api('can_access') |
71 | 35 |
@utils.to_json() |
72 | 36 |
def get(self, request, *args, **kwargs): |
73 |
obj = self.get_object() |
|
74 |
filters = self._filters_builder(request) |
|
75 |
return {'data': obj.get_data(filters)} |
|
37 |
params = request.GET |
|
38 |
filters = [] |
|
39 |
for column_title in [t.strip() for t in self.get_object().columns_keynames.split(',') if t]: |
|
40 |
if column_title in params.keys(): |
|
41 |
if 'case-insensitive' in params: |
|
42 |
filters.append("%s.lower() == query.get('%s', '').lower()" % (column_title, column_title)) |
|
43 |
else: |
|
44 |
filters.append("%s == query.get('%s')" % (column_title, column_title)) |
|
45 |
query = Query(filters='\n'.join(filters)) |
|
46 |
return self.get_object().execute_query(request, query, **params.dict()) |
|
76 | 47 | |
77 | 48 | |
78 | 49 |
class NewQueryView(CreateView): |
tests/test_csv_datasource.py | ||
---|---|---|
155 | 155 |
result = parse_response(resp) |
156 | 156 |
assert len(result) == 1 |
157 | 157 | |
158 |
def test_skipped_header_data(): |
|
159 |
csv = CsvDataSource.objects.create(csv_file=File(StringIO(get_file_content('data.csv')), 'data.csv'), |
|
160 |
columns_keynames=',id,,text,', |
|
161 |
skip_header=True) |
|
162 |
result = csv.get_data({'text': 'Eliot'}) |
|
163 |
assert len(result) == 0 |
|
164 | ||
165 | 158 |
def test_data(client, setup, filetype): |
166 | 159 |
csvdata, url = setup('fam,id,, text,sexe ', filename=filetype, data=get_file_content(filetype)) |
167 | 160 |
filters = {'text': 'Sacha'} |
... | ... | |
234 | 227 |
assert result[0]['text'] == 'Eliot' |
235 | 228 |
assert len(result) == 1 |
236 | 229 | |
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 | 230 | |
271 | 231 |
def test_dialect(client, setup): |
272 | 232 |
csvdata, url = setup(data=data) |
... | ... | |
280 | 240 |
} |
281 | 241 | |
282 | 242 |
assert expected == csvdata.dialect_options |
283 |
filters = {'id__gt': '20', 'id__lt': '40', 'fname__icontains': 'Sandra'}
|
|
243 |
filters = {'id': '22', 'fname': 'Sandra'}
|
|
284 | 244 |
resp = client.get(url, filters) |
285 | 245 |
result = parse_response(resp) |
286 | 246 |
assert len(result) == 1 |
287 |
- |