dataviz: use select2 widget for all filters (#71885)

This commit is contained in:
Valentin Deniaud 2023-08-17 17:28:41 +02:00
parent e8bd91b44e
commit 4bad6b488b
5 changed files with 262 additions and 47 deletions

View File

@ -26,12 +26,12 @@ from django.db.models import Q
from django.db.models.fields import BLANK_CHOICE_DASH
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist
from django.utils.translation import gettext_lazy as _
from gadjo.forms.widgets import MultiSelectWidget
from combo.utils import cache_during_request, requests, spooler
from .fields import StaticField
from .models import ChartCell, ChartFiltersCell, ChartNgCell
from .widgets import MultipleSelect2Widget, Select2Widget
@dataclasses.dataclass
@ -108,7 +108,8 @@ class ChartFiltersMixin:
return fields
def build_choice_field(self, cell, filter_, initial):
@classmethod
def get_filter_options(cls, cell, filter_, initial):
filter_id = filter_['id']
filter_options = filter_['options']
@ -122,11 +123,11 @@ class ChartFiltersMixin:
]
if filter_id == 'time_interval':
self.extend_time_interval_choices(choices)
cls.extend_time_interval_choices(choices)
required = filter_.get('required', False)
multiple = filter_.get('multiple')
if not required:
if not required and not multiple:
choices.insert(0, Choice(*BLANK_CHOICE_DASH[0]))
extra_variables = cell.page.get_extra_variables_keys()
@ -149,15 +150,24 @@ class ChartFiltersMixin:
if variable_choices and not multiple and filter_id != 'time_interval':
choices.extend(variable_choices)
return choices
def build_choice_field(self, cell, filter_, initial):
multiple = filter_.get('multiple')
required = filter_.get('required', False)
choices = self.get_filter_options(cell, filter_, initial)
widget_class = MultipleSelect2Widget if multiple else Select2Widget
widget = widget_class(cell, filter_['id'], choices, initial)
field_class = forms.MultipleChoiceField if multiple else forms.ChoiceField
widget_class = MultiSelectWidget if multiple else forms.Select
field = field_class(
label=filter_['label'],
choices=Choice.get_field_choices(choices),
required=required,
initial=initial,
widget=widget_class,
)
field.widget = widget
field.dataviz_choices = choices
return field
@ -168,9 +178,10 @@ class ChartFiltersMixin:
initial=bool(initial == 'true'),
)
def extend_time_interval_choices(self, choices):
@classmethod
def extend_time_interval_choices(cls, choices):
if any(choice.id == 'day' for choice in choices):
for choice in self.time_intervals:
for choice in cls.time_intervals:
if choice not in choices:
choices.append(choice)

View File

@ -15,8 +15,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf.urls import re_path
from django.urls import path
from .views import ajax_gauge_count, dataviz_graph, dataviz_graph_export
from combo.urls_utils import manager_required
from .views import ajax_gauge_count, dataviz_choices, dataviz_graph, dataviz_graph_export
urlpatterns = [
re_path(r'^ajax/gauge-count/(?P<cell>[\w_-]+)/$', ajax_gauge_count, name='combo-ajax-gauge-count'),
@ -24,4 +27,9 @@ urlpatterns = [
re_path(
r'^dataviz/graph/(?P<cell>[\w_-]+)/export/$', dataviz_graph_export, name='combo-dataviz-graph-export'
),
path(
'api/dataviz/graph/<int:cell_id>/<filter_id>/ajax-choices',
manager_required(dataviz_choices),
name='combo-dataviz-choices',
),
]

View File

@ -15,12 +15,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import unicodedata
import pyexcel_ods
from django.core import signing
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.http import FileResponse, Http404, HttpResponse, HttpResponseBadRequest
from django.http import FileResponse, Http404, HttpResponse, HttpResponseBadRequest, JsonResponse
from django.shortcuts import render, reverse
from django.template import TemplateSyntaxError, VariableDoesNotExist
from django.utils.translation import gettext_lazy as _
@ -31,7 +32,7 @@ from requests.exceptions import HTTPError
from combo.utils import NothingInCacheException, get_templated_url, requests
from .forms import ChartNgExportForm, ChartNgPartialForm
from .forms import ChartFiltersMixin, ChartNgExportForm, ChartNgPartialForm, Choice
from .models import ChartNgCell, Gauge, MissingVariable, UnsupportedDataSet
@ -189,3 +190,58 @@ class DatavizGraphExportView(SingleObjectMixin, FormView):
dataviz_graph_export = DatavizGraphExportView.as_view()
class DatavizChoicesView(DetailView):
model = ChartNgCell
pk_url_kwarg = 'cell_id'
def dispatch(self, *args, **kwargs):
self.cell = self.get_object()
filter_id = self.kwargs.get('filter_id')
for filter_ in self.cell.available_filters:
if filter_['id'] == filter_id:
self.filter = filter_
break
else:
raise Http404()
return super().dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
search_term = request.GET.get('term', '')
search_term = unicodedata.normalize('NFKC', search_term).casefold()
try:
page_number = int(request.GET.get('page', 1))
except ValueError:
page_number = 1
initial = self.cell.filter_params.get(self.filter['id'], self.filter.get('default'))
objects = ChartFiltersMixin.get_filter_options(self.cell, self.filter, initial)
objects = [x for x in objects if search_term in unicodedata.normalize('NFKC', str(x)).casefold()]
return JsonResponse(
{
'results': self.format_results(objects, (page_number - 1) * 10, page_number * 10),
'pagination': {'more': bool(len(objects) >= page_number * 10)},
}
)
def format_results(self, objects, start_index, end_index):
page_objects = objects[start_index:end_index]
if start_index > 0:
last_displayed_group = objects[start_index - 1].group
for option in page_objects:
if option.group == last_displayed_group:
option.group = None
return [
{'text': group, 'children': [{'id': k, 'text': v} for k, v in choices]}
for group, choices in Choice.get_field_choices(page_objects)
]
dataviz_choices = DatavizChoicesView.as_view()

View File

@ -0,0 +1,63 @@
# combo - content management system
# Copyright (C) 2014-2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.forms.widgets import Select, SelectMultiple
from django.urls import reverse
class Select2WidgetMixin:
class Media:
js = 'xstatic/select2.min.js'
css = {'all': ('xstatic/select2.min.css',)}
def __init__(self, cell, filter_id, choices, initial):
from .forms import Choice
attrs = {}
if self.enable_select2(choices):
attrs['data-autocomplete'] = 'true'
attrs['lang'] = settings.LANGUAGE_CODE
attrs['data-select2-url'] = reverse(
'combo-dataviz-choices', kwargs={'cell_id': cell.pk, 'filter_id': filter_id}
)
choices = self.filter_choices(choices, initial)
super().__init__(choices=Choice.get_field_choices(choices), attrs=attrs)
def enable_select2(self, choices):
return True
class Select2Widget(Select2WidgetMixin, Select):
min_choices = 20
@staticmethod
def filter_choices(choices, initial):
return [x for x in choices if x.id == initial]
def enable_select2(self, choices):
return bool(len(choices) > self.min_choices)
class MultipleSelect2Widget(Select2WidgetMixin, SelectMultiple):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['multiple'] = 'multiple'
@staticmethod
def filter_choices(choices, initial):
return [x for x in choices if x.id in (initial or [])]

View File

@ -1924,46 +1924,30 @@ def test_chartng_cell_manager_new_api(app, admin_user, new_api_statistics):
filter_multiple_stat = Statistic.objects.get(slug='filter-multiple')
resp.form[field_prefix + 'statistic'] = filter_multiple_stat.pk
manager_submit_cell(resp.form)
assert field_prefix + 'color$add_element' in resp.form.fields
assert field_prefix + 'color$remove_element' in resp.form.fields
resp.form[field_prefix + 'color'].select(text='Blue')
manager_submit_cell(resp.form)
cell.refresh_from_db()
assert cell.filter_params == {'color': ['blue']}
assert resp.form[field_prefix + 'color'].options == []
cell.filter_params = {'color': ['blue', 'green']}
cell.save()
resp = app.get('/manage/pages/%s/' % page.id)
assert resp.form.get(field_prefix + 'color', 0).value == 'blue'
assert resp.form.get(field_prefix + 'color', 1).value == 'green'
resp.form.get(field_prefix + 'color', 0).select(text='Red')
resp.form[field_prefix + 'color'].force_value(['blue', 'green'])
manager_submit_cell(resp.form)
assert resp.form[field_prefix + 'color'].value == ['green', 'blue']
assert resp.form[field_prefix + 'color'].options == [('green', True, 'Green'), ('blue', True, 'Blue')]
cell.refresh_from_db()
assert cell.filter_params == {'color': ['red', 'green']}
assert cell.filter_params == {'color': ['blue', 'green']}
color_filter = next(x for x in cell.statistic.filters if x['id'] == 'color')
color_filter['options'] = [{'id': 'black', 'label': 'Black'}, {'id': 'green', 'label': 'Green'}]
cell.statistic.save()
resp = app.get('/manage/pages/%s/' % page.id)
assert resp.form.get(field_prefix + 'color', 0).value == 'red'
assert resp.form.get(field_prefix + 'color', 1).value == 'green'
assert resp.form.get(field_prefix + 'color', 0).options == [
('', False, '---------'),
('black', False, 'Black'),
('green', False, 'Green'),
('red', True, 'red (unavailable)'),
assert resp.form[field_prefix + 'color'].value == ['green', 'blue']
assert resp.form[field_prefix + 'color'].options == [
('green', True, 'Green'),
('blue', True, 'blue (unavailable)'),
]
resp.form.get(field_prefix + 'color', 0).select(text='Green')
resp.form[field_prefix + 'color'].select_multiple(texts=[])
manager_submit_cell(resp.form)
cell.refresh_from_db()
assert cell.filter_params == {'color': ['green']}
resp.form[field_prefix + 'color'] = ''
manager_submit_cell(resp.form)
assert resp.form[field_prefix + 'color'].value == ''
assert resp.form[field_prefix + 'color'].value is None
cell.refresh_from_db()
assert cell.get_filter_params() == {}
@ -2256,14 +2240,6 @@ def test_chartng_cell_manager_new_api_page_variables(app, admin_user, new_api_st
time_interval_field = resp.form[field_prefix + 'time_interval']
assert [x[0] for x in time_interval_field.options] == ['day', 'month', 'year', 'week', 'weekday']
# no variables allowed for multiple choice field
cell.statistic = Statistic.objects.get(slug='filter-multiple')
cell.save()
resp = app.get('/manage/pages/%s/' % page.id)
color_field = resp.form[field_prefix + 'color']
assert [x[0] for x in color_field.options] == ['', 'red', 'green', 'blue']
def test_chartng_cell_manager_new_api_tabs(app, admin_user):
page = Page.objects.create(title='One', slug='index')
@ -3597,3 +3573,104 @@ def test_update_available_statistics(app, settings, caplog, freezer):
update_available_statistics()
assert caplog.records
assert caplog.messages == ['statistics from "Connection" have not been available for more than 48 hours.']
def test_chartng_cell_select2_choices(app, admin_user, new_api_statistics):
page = Page.objects.create(title='One', slug='index', extra_variables={'foo': 'bar'})
cell = ChartNgCell(page=page, order=1, placeholder='content')
cell.statistic = Statistic.objects.get(slug='option-groups')
cell.save()
app = login(app)
resp = app.get('/manage/pages/%s/' % page.id)
assert not resp.pyquery('select#id_cdataviz_chartngcell-%s-form' % cell.id).attr('data-select2-url')
# add choices to enable select2
form_filter = next(x for x in cell.statistic.filters if x['id'] == 'form')
form_filter['options'].append(
['Category C', [{'id': 'test-%s' % i, 'label': 'test %s' % i} for i in range(20)]]
)
cell.statistic.save()
resp = app.get('/manage/pages/%s/' % page.id)
field_prefix = 'cdataviz_chartngcell-%s-' % cell.id
assert resp.form[field_prefix + 'form'].options == []
resp.form[field_prefix + 'form'].force_value('test-0')
manager_submit_cell(resp.form)
assert resp.form[field_prefix + 'form'].options == [('test-0', True, 'test 0')]
url = resp.pyquery('select#id_cdataviz_chartngcell-%s-form' % cell.id).attr('data-select2-url')
resp = app.get(url)
assert resp.json['results'] == [
{'children': [{'id': '', 'text': '---------'}, {'id': 'all', 'text': 'All'}], 'text': None},
{'children': [{'id': 'test', 'text': 'Test'}], 'text': 'Category A'},
{'children': [{'id': 'test-2', 'text': 'test 2'}], 'text': 'Category B'},
{
'children': [
{'id': 'test-0', 'text': 'test 0'},
{'id': 'test-1', 'text': 'test 1'},
{'id': 'test-2', 'text': 'test 2'},
{'id': 'test-3', 'text': 'test 3'},
{'id': 'test-4', 'text': 'test 4'},
{'id': 'test-5', 'text': 'test 5'},
],
'text': 'Category C',
},
]
assert resp.json['pagination']['more'] is True
resp = app.get(url + '?page=2')
assert len(resp.json['results']) == 1
assert resp.json['results'][0]['text'] is None
assert len(resp.json['results'][0]['children']) == 10
assert resp.json['results'][0]['children'][0]['id'] == 'test-6'
assert resp.json['results'][0]['children'][-1]['id'] == 'test-15'
assert resp.json['pagination']['more'] is True
resp = app.get(url + '?page=3')
assert resp.json['results'] == [
{
'children': [
{'id': 'test-16', 'text': 'test 16'},
{'id': 'test-17', 'text': 'test 17'},
{'id': 'test-18', 'text': 'test 18'},
{'id': 'test-19', 'text': 'test 19'},
],
'text': None,
},
{'children': [{'id': 'variable:foo', 'text': 'foo'}], 'text': 'Page variables'},
]
assert resp.json['pagination']['more'] is False
resp = app.get(url + '?term=test 2')
assert resp.json['results'] == [
{'children': [{'id': 'test-2', 'text': 'test 2'}], 'text': 'Category B'},
{'children': [{'id': 'test-2', 'text': 'test 2'}], 'text': 'Category C'},
]
assert resp.json['pagination']['more'] is False
# no variables allowed for multiple choice field
cell.statistic = Statistic.objects.get(slug='filter-multiple')
cell.save()
resp = app.get('/manage/pages/%s/' % page.id)
url = resp.pyquery('select#id_cdataviz_chartngcell-%s-color' % cell.id).attr('data-select2-url')
resp = app.get(url)
assert resp.json == {
'pagination': {'more': False},
'results': [
{
'children': [
{'id': 'red', 'text': 'Red'},
{'id': 'green', 'text': 'Green'},
{'id': 'blue', 'text': 'Blue'},
],
'text': None,
}
],
}
# unknown filter
resp = app.get('/api/dataviz/graph/%s/unknown/ajax-choices' % cell.id, status=404)