Projet

Général

Profil

0001-csvdatasource-remove-advanced-lookup-filters-13748.patch

Serghei Mihai, 14 novembre 2018 10:52

Télécharger (9,91 ko)

Voir les différences:

Subject: [PATCH] csvdatasource: remove advanced lookup filters (#13748)

 passerelle/apps/csvdatasource/lookups.py | 70 ------------------------
 passerelle/apps/csvdatasource/models.py  | 39 +------------
 passerelle/apps/csvdatasource/views.py   | 49 ++++-------------
 tests/test_csv_datasource.py             | 42 +-------------
 4 files changed, 13 insertions(+), 187 deletions(-)
 delete mode 100644 passerelle/apps/csvdatasource/lookups.py
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
-