0002-manager-load-visualization-filter-choices-using-ajax.patch
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 |
- |