Projet

Général

Profil

0002-manager-load-visualization-filter-choices-using-ajax.patch

Valentin Deniaud, 18 mars 2021 15:37

Télécharger (14,4 ko)

Voir les différences:

Subject: [PATCH 2/2] manager: load visualization filter choices using ajax
 (#35569)

 bijoe/visualization/forms.py         | 24 ++++++++++-
 bijoe/visualization/urls.py          |  1 +
 bijoe/visualization/views.py         | 59 +++++++++++++++++++++++++++-
 tests/fixtures/schema1/01_schema.sql |  4 +-
 tests/settings.py                    |  1 +
 tests/test_schema1.py                | 57 ++++++++++++++++++++-------
 tests/utils.py                       | 10 +++++
 7 files changed, 138 insertions(+), 18 deletions(-)
bijoe/visualization/forms.py
24 24
from django.forms import ModelForm, TextInput, NullBooleanField
25 25
from django.conf import settings
26 26

  
27
from django_select2.forms import Select2MultipleWidget
27
from django_select2.forms import HeavySelect2MultipleWidget
28 28

  
29 29
from . import models
30 30

  
......
142 142
        return {'start': None, 'end': None}
143 143

  
144 144

  
145
class Select2ChoicesWidget(HeavySelect2MultipleWidget):
146
    max_results = 10
147

  
148
    def __init__(self, *args, **kwargs):
149
        self.warehouse = kwargs.pop('warehouse')
150
        self.cube = kwargs.pop('cube')
151
        self.dimension = kwargs.pop('dimension')
152
        return super().__init__(*args, **kwargs)
153

  
154
    def optgroups(self, name, value, attrs=None):
155
        # Render only selected options
156
        self.choices = [(k, v) for k, v in self.choices if k in value]
157
        return super().optgroups(name, value, attrs=attrs)
158

  
159

  
145 160
class CubeForm(forms.Form):
146 161
    representation = forms.ChoiceField(
147 162
        label=_(u'Presentation'),
......
192 207
                    choices=[(s, label) for v, s, label in members],
193 208
                    coerce=coercion_function(members),
194 209
                    required=False,
195
                    widget=Select2MultipleWidget()())
210
                    widget=Select2ChoicesWidget(
211
                        data_view='select2-choices',
212
                        warehouse=cube.engine.warehouse.name,
213
                        cube=cube.name,
214
                        dimension=dimension.name
215
                    ))
196 216

  
197 217
        # group by
198 218
        self.base_fields['drilldown_x'] = forms.ChoiceField(
bijoe/visualization/urls.py
42 42
    url(r'(?P<pk>\d+)/delete/$', views.delete_visualization, name='delete-visualization'),
43 43
    url(r'(?P<pk>\d+)/export$', views.export_visualization, name='export-visualization'),
44 44
    url(r'(?P<pk>\d+)/save-as/$', views.save_as_visualization, name='save-as-visualization'),
45
    url(r'^select2choices.json$', views.select2_choices, name='select2-choices'),
45 46
]
bijoe/visualization/views.py
20 20
import json
21 21

  
22 22
from django.conf import settings
23
from django.core import signing
24
from django.core.signing import BadSignature
23 25
from django.contrib import messages
24 26
from django.utils.encoding import force_bytes, force_text
25 27
from django.utils.text import slugify
......
30 32
from django.views.generic import DetailView, ListView, View, TemplateView
31 33
from django.shortcuts import redirect
32 34
from django.urls import reverse, reverse_lazy
33
from django.http import HttpResponse, Http404
35
from django.http import HttpResponse, Http404, JsonResponse
34 36
from django.core.exceptions import PermissionDenied
35 37
from django.views.decorators.clickjacking import xframe_options_exempt
36 38

  
39
from django_select2.cache import cache
37 40
from rest_framework import generics
38 41
from rest_framework.response import Response
39 42

  
......
425 428
        return response
426 429

  
427 430

  
431
class Select2ChoicesView(View):
432

  
433
    def get(self, request, *args, **kwargs):
434
        widget = self.get_widget_or_404()
435
        try:
436
            warehouse = Engine([warehouse for warehouse in get_warehouses()
437
                                     if warehouse.name == widget.warehouse][0])
438
            cube = warehouse[widget.cube]
439
            self.dimension = cube.dimensions[widget.dimension]
440
        except IndexError:
441
            raise Http404()
442

  
443
        try:
444
            page_number = int(request.GET.get('page', 1)) - 1
445
        except ValueError:
446
            raise Http404('Invalid page number.')
447

  
448
        term = request.GET.get('term', '')
449
        choices = self.get_choices(term, page_number, widget.max_results)
450

  
451
        return JsonResponse({
452
            'results': [{'text': label, 'id': s} for s, label in choices],
453
            'more': not(len(choices) < widget.max_results),
454
        })
455

  
456
    def get_choices(self, term, page_number, max_results):
457
        members = []
458
        for _id, label in self.dimension.members():
459
            members.append((_id, str(_id), label))
460
        members.append((None, '__none__', _('None')))
461

  
462
        choices = [(s, label) for v, s, label in members if term in label.lower()]
463
        choices = choices[page_number * max_results:(page_number * max_results) + max_results]
464
        return choices
465

  
466
    def get_widget_or_404(self):
467
        field_id = self.request.GET.get('field_id', None)
468
        if not field_id:
469
            raise Http404('No "field_id" provided.')
470
        try:
471
            key = signing.loads(field_id)
472
        except BadSignature:
473
            raise Http404('Invalid "field_id".')
474
        else:
475
            cache_key = '%s%s' % (settings.SELECT2_CACHE_PREFIX, key)
476
            widget_dict = cache.get(cache_key)
477
            if widget_dict is None:
478
                raise Http404('field_id not found')
479
            if widget_dict.pop('url') != self.request.path:
480
                raise Http404('field_id was issued for the view.')
481
        return widget_dict['widget']
482

  
483

  
428 484
warehouse = WarehouseView.as_view()
429 485
cube = CubeView.as_view()
430 486
cube_iframe = xframe_options_exempt(CubeIframeView.as_view())
......
442 498
visualization_json = VisualizationJSONView.as_view()
443 499
visualizations_import = VisualizationsImportView.as_view()
444 500
visualizations_export = VisualizationsExportView.as_view()
501
select2_choices = Select2ChoicesView.as_view()
445 502

  
446 503
cube_iframe.mellon_no_passive = True
447 504
visualization_iframe.mellon_no_passive = True
tests/fixtures/schema1/01_schema.sql
45 45
   (2, 0, 'subé6'),
46 46
   (3, 1, 'subé7'),
47 47
   (3, 0, 'subé8'),
48
   (3, 0, 'subé9');
48
   (3, 0, 'subé9'),
49
   (3, 0, 'subé10'),
50
   (3, 0, 'subé11');
49 51

  
50 52

  
51 53
INSERT INTO "Facts" (date, datetime, integer, boolean, cnt, innersubcategory_id, leftsubcategory_id, rightsubcategory_id, outersubcategory_id, "String", geo, json) VALUES
tests/settings.py
3 3
    'SET lc_time = \'fr_FR.UTF-8\'',
4 4
]
5 5
PAGE_LENGTH = 0
6
SELECT2_CACHE_PREFIX = 'select2_'
tests/test_schema1.py
2 2

  
3 3
import json
4 4

  
5
from utils import login, get_table, get_ods_table, get_ods_document
5
from utils import login, get_table, get_ods_table, get_ods_document, request_select2
6 6

  
7 7
from bijoe.visualization.ods import OFFICE_NS, TABLE_NS
8 8
from bijoe.visualization.models import Visualization as VisualizationModel
......
24 24
    response = form.submit('visualize')
25 25
    assert 'big-msg-info' not in response
26 26
    assert get_table(response) == [
27
        ['Inner SubCategory', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98',
27
        ['Inner SubCategory', u'sub\xe910', u'sub\xe911', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98',
28 28
         u'sub\xe99', u'sub\xe97', u'sub\xe92', u'sub\xe93', u'sub\xe91'],
29
        ['number of rows', '0', '0', '0', '0', '0', '0', '0', '1', '15']
29
        ['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15']
30 30
    ]
31 31
    form = response.form
32 32
    form.set('representation', 'table')
......
77 77
    form.set('drilldown_x', 'boolean')
78 78
    response = form.submit('visualize')
79 79
    assert get_table(response) == [['Boolean', 'Oui', 'Non'], ['number of rows', '8', '9']]
80
    form.set('filter__boolean', [o[0] for o in form.fields['filter__boolean'][0].options if o[2] == 'Oui'][0])
80
    form['filter__boolean'].force_value([o[0] for o in form.fields['filter__boolean'][0].options if o[2] == 'Oui'][0])
81 81
    response = form.submit('visualize')
82 82
    assert get_table(response) == [['Boolean', 'Oui', 'Non'], ['number of rows', '8', '0']]
83 83

  
......
93 93
    form.set('drilldown_x', 'string')
94 94
    response = form.submit('visualize')
95 95
    assert get_table(response) == [['String', 'a', 'b', 'c', 'Aucun(e)'], ['number of rows', '11', '2', '3', '1']]
96
    form.set('filter__string', ['a', 'b', '__none__'])
96
    form['filter__string'].force_value(['a', 'b', '__none__'])
97 97
    response = form.submit('visualize')
98 98
    assert get_table(response) == [['String', 'a', 'b', 'Aucun(e)'], ['number of rows', '11', '2', '1']]
99 99

  
......
126 126
    form.set('drilldown_x', 'outersubcategory')
127 127
    response = form.submit('visualize')
128 128
    assert get_table(response) == [
129
        ['Outer SubCategory', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98',
129
        ['Outer SubCategory', u'sub\xe910', u'sub\xe911', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98',
130 130
         u'sub\xe99', u'sub\xe97', u'sub\xe92', u'sub\xe93', u'sub\xe91', 'Aucun(e)'],
131
        ['number of rows', '0', '0', '0', '0', '0', '0', '0', '1', '15', '1']
131
        ['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15', '1']
132 132
    ]
133
    form.set('filter__outersubcategory', ['__none__'])
133
    form['filter__outersubcategory'].force_value(['__none__'])
134 134
    response = form.submit('visualize')
135 135
    assert get_table(response) == [
136 136
        ['Outer SubCategory', 'Aucun(e)'],
......
176 176
    assert get_table(response) == get_ods_table(ods_response)[1:]
177 177
    root = get_ods_document(ods_response)
178 178
    nodes = root.findall('.//{%s}table-cell' % TABLE_NS)
179
    assert len([node for node in nodes if node.attrib['{%s}value-type' % OFFICE_NS] == 'float']) == 11
179
    assert len([node for node in nodes if node.attrib['{%s}value-type' % OFFICE_NS] == 'float']) == 13
180 180

  
181 181
    app.reset()  # logout
182 182
    assert 'login' in app.get(ods_response.request.url, status=302).location
......
368 368
    ]
369 369

  
370 370
    assert 'filter__a' in form.fields
371
    assert set([o[0] for o in form['filter__a'].options]) == {'x', 'y', 'z', '__none__'}
371
    choices = [o['id'] for o in request_select2(app, response, 'filter__a')['results']]
372
    assert set(choices) == {'x', 'y', 'z', '__none__'}
372 373

  
373
    form.set('filter__a', ['x', 'y'])
374
    form['filter__a'].force_value(['x', 'y'])
374 375
    response = form.submit('visualize')
375 376
    assert get_table(response) == [
376 377
        ['A', 'x', 'y', 'z'],
......
395 396
    ]
396 397

  
397 398
    assert 'filter__a' in form.fields
398
    assert set([o[0] for o in form['filter__a'].options]) == {'x', 'y', 'z', '__none__'}
399
    choices = [o['id'] for o in request_select2(app, response, 'filter__a')['results']]
400
    assert set(choices) == {'x', 'y', 'z', '__none__'}
399 401

  
400
    form.set('filter__a', ['x', 'y'])
402
    form['filter__a'].force_value(['x', 'y'])
401 403
    response = form.submit('visualize')
402 404
    assert get_table(response) == [
403 405
        ['A', 'x', 'y', 'z'],
......
419 421
        ['String', 'a', 'b', 'c', 'Aucun(e)'],
420 422
        ['sum of integer column', '11', '2', '3', '1'],
421 423
    ]
422
    form.set('filter__string', ['a', 'b', '__none__'])
424
    form['filter__string'].force_value(['a', 'b', '__none__'])
423 425
    response = form.submit('visualize')
424 426
    assert get_table(response) == [
425 427
        ['String', 'a', 'b', 'Aucun(e)'],
426 428
        ['sum of integer column', '11', '2', '1'],
427 429
    ]
430

  
431

  
432
def test_select2_filter_widget(schema1, app, admin):
433
    login(app, admin)
434
    response = app.get('/')
435
    response = response.click('schema1')
436
    response = response.click('Facts 1')
437

  
438
    resp = request_select2(app, response, 'filter__innersubcategory')
439
    assert len(resp['results']) == 10
440
    assert resp['more'] is True
441

  
442
    resp = request_select2(app, response, 'filter__innersubcategory', page=2)
443
    assert len(resp['results']) == 2
444
    assert resp['more'] is False
445

  
446
    resp = request_select2(app, response, 'filter__innersubcategory', term='aucun')
447
    assert len(resp['results']) == 1
448
    assert resp['more'] is False
449

  
450
    resp = request_select2(app, response, 'filter__innersubcategory', term='é')
451
    assert len(resp['results']) == 10
452
    assert resp['more'] is True
453

  
454
    resp = request_select2(app, response, 'filter__innersubcategory', term='é', page=2)
455
    assert len(resp['results']) == 1
456
    assert resp['more'] is False
tests/utils.py
68 68
        for cell_node in row_node.findall('.//{%s}table-cell' % TABLE_NS):
69 69
            row.append(xml_node_text_content(cell_node))
70 70
    return table
71

  
72

  
73
def request_select2(app, response, field_id, term='', page=None):
74
    field = response.pyquery('#id_%s' % field_id)[0]
75
    select2_url = field.attrib['data-ajax--url']
76
    select2_field_id = field.attrib['data-field_id']
77
    params = {'field_id': select2_field_id, 'term': term}
78
    if page:
79
        params['page'] = page
80
    return app.get(select2_url, params=params).json
71
-