manager: load visualization filter choices using ajax (#35569)

This commit is contained in:
Valentin Deniaud 2021-03-18 14:18:10 +01:00
parent 5375d4c9dc
commit 9a16eee553
7 changed files with 138 additions and 18 deletions

View File

@ -24,7 +24,7 @@ from django.utils.safestring import mark_safe
from django.forms import ModelForm, TextInput, NullBooleanField
from django.conf import settings
from django_select2.forms import Select2MultipleWidget
from django_select2.forms import HeavySelect2MultipleWidget
from . import models
@ -142,6 +142,21 @@ class DateRangeField(forms.MultiValueField):
return {'start': None, 'end': None}
class Select2ChoicesWidget(HeavySelect2MultipleWidget):
max_results = 10
def __init__(self, *args, **kwargs):
self.warehouse = kwargs.pop('warehouse')
self.cube = kwargs.pop('cube')
self.dimension = kwargs.pop('dimension')
return super().__init__(*args, **kwargs)
def optgroups(self, name, value, attrs=None):
# Render only selected options
self.choices = [(k, v) for k, v in self.choices if k in value]
return super().optgroups(name, value, attrs=attrs)
class CubeForm(forms.Form):
representation = forms.ChoiceField(
label=_(u'Presentation'),
@ -192,7 +207,12 @@ class CubeForm(forms.Form):
choices=[(s, label) for v, s, label in members],
coerce=coercion_function(members),
required=False,
widget=Select2MultipleWidget())
widget=Select2ChoicesWidget(
data_view='select2-choices',
warehouse=cube.engine.warehouse.name,
cube=cube.name,
dimension=dimension.name
))
# group by
self.base_fields['drilldown_x'] = forms.ChoiceField(

View File

@ -42,4 +42,5 @@ urlpatterns = [
url(r'(?P<pk>\d+)/delete/$', views.delete_visualization, name='delete-visualization'),
url(r'(?P<pk>\d+)/export$', views.export_visualization, name='export-visualization'),
url(r'(?P<pk>\d+)/save-as/$', views.save_as_visualization, name='save-as-visualization'),
url(r'^select2choices.json$', views.select2_choices, name='select2-choices'),
]

View File

@ -20,6 +20,8 @@ import hashlib
import json
from django.conf import settings
from django.core import signing
from django.core.signing import BadSignature
from django.contrib import messages
from django.utils.encoding import force_bytes, force_text
from django.utils.text import slugify
@ -30,10 +32,11 @@ from django.views.generic.list import MultipleObjectMixin
from django.views.generic import DetailView, ListView, View, TemplateView
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.http import HttpResponse, Http404
from django.http import HttpResponse, Http404, JsonResponse
from django.core.exceptions import PermissionDenied
from django.views.decorators.clickjacking import xframe_options_exempt
from django_select2.cache import cache
from rest_framework import generics
from rest_framework.response import Response
@ -425,6 +428,59 @@ class VisualizationsExportView(views.AuthorizationMixin, View):
return response
class Select2ChoicesView(View):
def get(self, request, *args, **kwargs):
widget = self.get_widget_or_404()
try:
warehouse = Engine([warehouse for warehouse in get_warehouses()
if warehouse.name == widget.warehouse][0])
cube = warehouse[widget.cube]
self.dimension = cube.dimensions[widget.dimension]
except IndexError:
raise Http404()
try:
page_number = int(request.GET.get('page', 1)) - 1
except ValueError:
raise Http404('Invalid page number.')
term = request.GET.get('term', '')
choices = self.get_choices(term, page_number, widget.max_results)
return JsonResponse({
'results': [{'text': label, 'id': s} for s, label in choices],
'more': not(len(choices) < widget.max_results),
})
def get_choices(self, term, page_number, max_results):
members = []
for _id, label in self.dimension.members():
members.append((_id, str(_id), label))
members.append((None, '__none__', _('None')))
choices = [(s, label) for v, s, label in members if term in label.lower()]
choices = choices[page_number * max_results:(page_number * max_results) + max_results]
return choices
def get_widget_or_404(self):
field_id = self.request.GET.get('field_id', None)
if not field_id:
raise Http404('No "field_id" provided.')
try:
key = signing.loads(field_id)
except BadSignature:
raise Http404('Invalid "field_id".')
else:
cache_key = '%s%s' % (settings.SELECT2_CACHE_PREFIX, key)
widget_dict = cache.get(cache_key)
if widget_dict is None:
raise Http404('field_id not found')
if widget_dict.pop('url') != self.request.path:
raise Http404('field_id was issued for the view.')
return widget_dict['widget']
warehouse = WarehouseView.as_view()
cube = CubeView.as_view()
cube_iframe = xframe_options_exempt(CubeIframeView.as_view())
@ -442,6 +498,7 @@ visualization_ods = VisualizationODSView.as_view()
visualization_json = VisualizationJSONView.as_view()
visualizations_import = VisualizationsImportView.as_view()
visualizations_export = VisualizationsExportView.as_view()
select2_choices = Select2ChoicesView.as_view()
cube_iframe.mellon_no_passive = True
visualization_iframe.mellon_no_passive = True

View File

@ -45,7 +45,9 @@ INSERT INTO subcategory (category_id, ord, label) VALUES
(2, 0, 'subé6'),
(3, 1, 'subé7'),
(3, 0, 'subé8'),
(3, 0, 'subé9');
(3, 0, 'subé9'),
(3, 0, 'subé10'),
(3, 0, 'subé11');
INSERT INTO "Facts" (date, datetime, integer, boolean, cnt, innersubcategory_id, leftsubcategory_id, rightsubcategory_id, outersubcategory_id, "String", geo, json) VALUES

View File

@ -3,3 +3,4 @@ BIJOE_INIT_SQL = [
'SET lc_time = \'fr_FR.UTF-8\'',
]
PAGE_LENGTH = 0
SELECT2_CACHE_PREFIX = 'select2_'

View File

@ -2,7 +2,7 @@
import json
from utils import login, get_table, get_ods_table, get_ods_document
from utils import login, get_table, get_ods_table, get_ods_document, request_select2
from bijoe.visualization.ods import OFFICE_NS, TABLE_NS
from bijoe.visualization.models import Visualization as VisualizationModel
@ -24,9 +24,9 @@ def test_simple(schema1, app, admin):
response = form.submit('visualize')
assert 'big-msg-info' not in response
assert get_table(response) == [
['Inner SubCategory', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98',
['Inner SubCategory', u'sub\xe910', u'sub\xe911', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98',
u'sub\xe99', u'sub\xe97', u'sub\xe92', u'sub\xe93', u'sub\xe91'],
['number of rows', '0', '0', '0', '0', '0', '0', '0', '1', '15']
['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15']
]
form = response.form
form.set('representation', 'table')
@ -77,7 +77,7 @@ def test_boolean_dimension(schema1, app, admin):
form.set('drilldown_x', 'boolean')
response = form.submit('visualize')
assert get_table(response) == [['Boolean', 'Oui', 'Non'], ['number of rows', '8', '9']]
form.set('filter__boolean', [o[0] for o in form.fields['filter__boolean'][0].options if o[2] == 'Oui'][0])
form['filter__boolean'].force_value([o[0] for o in form.fields['filter__boolean'][0].options if o[2] == 'Oui'][0])
response = form.submit('visualize')
assert get_table(response) == [['Boolean', 'Oui', 'Non'], ['number of rows', '8', '0']]
@ -93,7 +93,7 @@ def test_string_dimension(schema1, app, admin):
form.set('drilldown_x', 'string')
response = form.submit('visualize')
assert get_table(response) == [['String', 'a', 'b', 'c', 'Aucun(e)'], ['number of rows', '11', '2', '3', '1']]
form.set('filter__string', ['a', 'b', '__none__'])
form['filter__string'].force_value(['a', 'b', '__none__'])
response = form.submit('visualize')
assert get_table(response) == [['String', 'a', 'b', 'Aucun(e)'], ['number of rows', '11', '2', '1']]
@ -126,11 +126,11 @@ def test_item_dimension(schema1, app, admin):
form.set('drilldown_x', 'outersubcategory')
response = form.submit('visualize')
assert get_table(response) == [
['Outer SubCategory', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98',
['Outer SubCategory', u'sub\xe910', u'sub\xe911', u'sub\xe94', u'sub\xe95', u'sub\xe96', u'sub\xe98',
u'sub\xe99', u'sub\xe97', u'sub\xe92', u'sub\xe93', u'sub\xe91', 'Aucun(e)'],
['number of rows', '0', '0', '0', '0', '0', '0', '0', '1', '15', '1']
['number of rows', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '15', '1']
]
form.set('filter__outersubcategory', ['__none__'])
form['filter__outersubcategory'].force_value(['__none__'])
response = form.submit('visualize')
assert get_table(response) == [
['Outer SubCategory', 'Aucun(e)'],
@ -176,7 +176,7 @@ def test_ods(schema1, app, admin):
assert get_table(response) == get_ods_table(ods_response)[1:]
root = get_ods_document(ods_response)
nodes = root.findall('.//{%s}table-cell' % TABLE_NS)
assert len([node for node in nodes if node.attrib['{%s}value-type' % OFFICE_NS] == 'float']) == 11
assert len([node for node in nodes if node.attrib['{%s}value-type' % OFFICE_NS] == 'float']) == 13
app.reset() # logout
assert 'login' in app.get(ods_response.request.url, status=302).location
@ -368,9 +368,10 @@ def test_json_dimensions(schema1, app, admin):
]
assert 'filter__a' in form.fields
assert set([o[0] for o in form['filter__a'].options]) == {'x', 'y', 'z', '__none__'}
choices = [o['id'] for o in request_select2(app, response, 'filter__a')['results']]
assert set(choices) == {'x', 'y', 'z', '__none__'}
form.set('filter__a', ['x', 'y'])
form['filter__a'].force_value(['x', 'y'])
response = form.submit('visualize')
assert get_table(response) == [
['A', 'x', 'y', 'z'],
@ -395,9 +396,10 @@ def test_json_dimensions_having_percent(schema1, app, admin):
]
assert 'filter__a' in form.fields
assert set([o[0] for o in form['filter__a'].options]) == {'x', 'y', 'z', '__none__'}
choices = [o['id'] for o in request_select2(app, response, 'filter__a')['results']]
assert set(choices) == {'x', 'y', 'z', '__none__'}
form.set('filter__a', ['x', 'y'])
form['filter__a'].force_value(['x', 'y'])
response = form.submit('visualize')
assert get_table(response) == [
['A', 'x', 'y', 'z'],
@ -419,9 +421,36 @@ def test_sum_integer_measure(schema1, app, admin):
['String', 'a', 'b', 'c', 'Aucun(e)'],
['sum of integer column', '11', '2', '3', '1'],
]
form.set('filter__string', ['a', 'b', '__none__'])
form['filter__string'].force_value(['a', 'b', '__none__'])
response = form.submit('visualize')
assert get_table(response) == [
['String', 'a', 'b', 'Aucun(e)'],
['sum of integer column', '11', '2', '1'],
]
def test_select2_filter_widget(schema1, app, admin):
login(app, admin)
response = app.get('/')
response = response.click('schema1')
response = response.click('Facts 1')
resp = request_select2(app, response, 'filter__innersubcategory')
assert len(resp['results']) == 10
assert resp['more'] is True
resp = request_select2(app, response, 'filter__innersubcategory', page=2)
assert len(resp['results']) == 2
assert resp['more'] is False
resp = request_select2(app, response, 'filter__innersubcategory', term='aucun')
assert len(resp['results']) == 1
assert resp['more'] is False
resp = request_select2(app, response, 'filter__innersubcategory', term='é')
assert len(resp['results']) == 10
assert resp['more'] is True
resp = request_select2(app, response, 'filter__innersubcategory', term='é', page=2)
assert len(resp['results']) == 1
assert resp['more'] is False

View File

@ -68,3 +68,13 @@ def get_ods_table(response):
for cell_node in row_node.findall('.//{%s}table-cell' % TABLE_NS):
row.append(xml_node_text_content(cell_node))
return table
def request_select2(app, response, field_id, term='', page=None):
field = response.pyquery('#id_%s' % field_id)[0]
select2_url = field.attrib['data-ajax--url']
select2_field_id = field.attrib['data-field_id']
params = {'field_id': select2_field_id, 'term': term}
if page:
params['page'] = page
return app.get(select2_url, params=params).json