dataviz: add new widget to select multiple filter values (#74061)
gitea/combo/pipeline/head This commit looks good Details

This commit is contained in:
Valentin Deniaud 2023-02-01 17:54:27 +01:00 committed by Gitea
parent 378f4d6b53
commit 337259dc5a
11 changed files with 206 additions and 14 deletions

1
.gitignore vendored
View File

@ -14,6 +14,7 @@ combo/apps/maps/static/css/combo.map.css.map
combo/apps/pwa/static/css/combo.manager.pwa.css
combo/apps/pwa/static/css/combo.manager.pwa.css.map
combo/apps/family/static/css/combo.weekly_agenda.css
combo/apps/dataviz/static/css/combo.multiselectwidget.css
combo/manager/static/css/combo.manager.css
data/themes/gadjo/static/css/agent-portal.css
data/themes/gadjo/static/css/agent-portal.css.map

View File

@ -3,7 +3,7 @@ recursive-include combo/locale *.po *.mo
# static
recursive-include combo/apps/lingo/static *.css *.js *.ico *.gif *.png *.jpg
recursive-include combo/apps/dataviz/static *.css *.js *.ico *.gif *.png *.jpg
recursive-include combo/apps/dataviz/static *.css *.js *.ico *.gif *.png *.jpg *.scss
recursive-include combo/apps/dashboard/static *.js
recursive-include combo/apps/family/static *.css *.scss *.js
recursive-include combo/apps/gallery/static *.js

View File

@ -29,6 +29,7 @@ from django.utils.translation import gettext_lazy as _
from combo.utils import cache_during_request, requests, spooler
from .models import ChartCell, ChartFiltersCell, ChartNgCell
from .widgets import MultiSelectWidget
class ChartForm(forms.ModelForm):
@ -93,7 +94,7 @@ class ChartFiltersMixin:
required = filter_.get('required', False)
multiple = filter_.get('multiple')
if not required and not multiple:
if not required:
choices_to_complete.insert(0, BLANK_CHOICE_DASH[0])
extra_variables = cell.page.get_extra_variables_keys()
@ -117,8 +118,13 @@ class ChartFiltersMixin:
choices.append((_('Page variables'), variable_choices))
field_class = forms.MultipleChoiceField if multiple else forms.ChoiceField
widget_class = MultiSelectWidget if multiple else forms.Select
fields[filter_id] = field_class(
label=filter_['label'], choices=choices, required=required, initial=initial
label=filter_['label'],
choices=choices,
required=required,
initial=initial,
widget=widget_class,
)
if filter_.get('deprecated'):
fields[filter_id].label += ' (%s)' % _('deprecated')

View File

@ -757,6 +757,10 @@ class ChartFiltersCell(CellBase):
class Meta:
verbose_name = _('Filters')
class Media:
js = ('js/combo.multiselectwidget.js',)
css = {'all': ('css/combo.multiselectwidget.css',)}
@classmethod
def is_enabled(cls):
return settings.CHART_FILTERS_CELL_ENABLED and settings.STATISTICS_PROVIDERS

View File

@ -0,0 +1,31 @@
.combo-multi-select-widget--field {
margin-bottom: 0.2em;
}
.combo-multi-select-widget--select-button-container {
display: flex;
gap: 0.5em;
}
.combo-multi-select-widget--field select {
min-width: 0;
}
.combo-multi-select-widget--field button {
margin-top: auto;
margin-bottom: auto;
}
.combo-multi-select-widget--field:first-of-type button.combo-multi-select-widget--button-remove {
display: none;
}
button.combo-multi-select-widget--button-add::before {
content: "\f067"; /* plus */
font-family: FontAwesome;
}
button.combo-multi-select-widget--button-remove::before {
content: "\f068"; /* minus */
font-family: FontAwesome;
}

View File

@ -0,0 +1,69 @@
const multiSelectWidget = (function () {
const add_row = function(widget) {
event.preventDefault();
/* get last row node */
const rows = widget.querySelectorAll('.combo-multi-select-widget--field');
const $last_row = $(rows).last();
/* clone the row */
const $new_row = $last_row.clone();
/* set new label and ids */
const row_label = widget.dataset.rowLabel;
const new_label = row_label + ' ' + rows.length;
$new_row.find('label').text(new_label);
const row_id = widget.dataset.rowId;
const new_id = row_id + '_' + rows.length;
$new_row.find('label').attr('for', new_id);
$new_row.find('select').attr('id', new_id);
/* add new row after the last row */
$last_row.after($new_row);
$('.combo-multi-select-widget--button-remove', $new_row).click(remove_row);
}
const remove_row = function(event) {
event.preventDefault();
var $field = $(this).parents('.content');
var $row = $(this).parents('.combo-multi-select-widget--field');
$row.remove();
$field.change();
}
const init = function(cell) {
const widgets = cell.querySelectorAll('.combo-multi-select-widget');
if (!widgets.length) return;
widgets.forEach(function(widget){
const deletBtn = widget.querySelectorAll('.combo-multi-select-widget--button-remove');
const addBtn = widget.querySelectorAll('.combo-multi-select-widget--button-add');
$(addBtn).off('click');
$(addBtn).click( () => add_row(widget) );
$(deletBtn).off('click');
$(deletBtn).click(remove_row);
});
}
return {
init
}
})();
$(function() {
$('.cell').each(function(i, cell) {
multiSelectWidget.init(cell);
});
$(document).on('combo:cell-loaded', function(e, cell) {
multiSelectWidget.init(cell);
});
$('.cell').on('combo:cellform-reloaded', function() {
multiSelectWidget.init(this);
});
});

View File

@ -32,7 +32,7 @@
}
}).change();
$('select, input', 'div#panel-dataviz_chartngcell-{{ cell.pk }}-general').change(function() {
$('div#panel-dataviz_chartngcell-{{ cell.pk }}-general div.content').change(function() {
$('div#cell-dataviz_chartngcell-{{ cell.pk }} button.save').click();
});

View File

@ -0,0 +1,17 @@
{% load i18n %}
<div class="combo-multi-select-widget" data-row-id="{{ widget.name }}" data-row-label="{% trans "Value" %}">
<div class="combo-multi-select-widget--fields" role="group">
{% for widget in widget.subwidgets %}
<div class="combo-multi-select-widget--field">
<label for="{{ widget.name }}_{{ forloop.counter }}" class="sr-only">{% trans "Value" %} {{ forloop.counter }}</label>
<div class="combo-multi-select-widget--select-button-container">
{% include widget.template_name %}
<button type="button" name="{{ widget.name }}$remove_element" class="combo-multi-select-widget--button-remove" title="{% trans "Remove" %}" aria-label="{% trans "Remove value" %} {{ forloop.counter }}"></button>
</div>
</div>
{% endfor %}
</div>
<button type="button" name="{{ widget.name }}$add_element" class="combo-multi-select-widget--button-add" title="{% trans "Add" %}" aria-label="{% trans "Add" %}"></button>
</div>

View File

@ -0,0 +1,43 @@
import django
from django import forms
class MultiSelectWidget(forms.MultiWidget):
template_name = 'combo/widgets/multiselectwidget.html'
class Media:
js = ('js/combo.multiselectwidget.js',)
css = {'all': ('css/combo.multiselectwidget.css',)}
def __init__(self, attrs=None):
self.attrs = attrs
widgets = [forms.Select(attrs=attrs)]
super().__init__(widgets, attrs)
def get_context(self, name, value, attrs):
if not isinstance(value, list):
value = [value]
self.widgets = []
for _ in range(max(len(value), 1)):
self.widgets.append(forms.Select(attrs=self.attrs, choices=self.choices))
# all subwidgets must have the same name
if django.VERSION >= (3, 1):
self.widgets_names = [''] * len(self.widgets)
return super().get_context(name, value, attrs)
else:
context = super().get_context(name, value, attrs)
subwidgets = context['widget']['subwidgets']
for widget in subwidgets:
widget['name'] = widget['name'].rsplit('_', 1)[0]
return context
def decompress(self, value):
return value or []
def value_from_datadict(self, data, files, name):
values = [x for x in data.getlist(name) if x]
# remove duplicates while keeping order
return list(dict.fromkeys(values))

View File

@ -5,6 +5,7 @@
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/combo.manager.css" %}"/>
<link rel="stylesheet" type="text/css" media="all" href="{% static "xstatic/leaflet.css" %}"/>
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/combo.map.css" %}"/>
<link rel="stylesheet" type="text/css" media="all" href="{% static "css/combo.multiselectwidget.css" %}"/>
{% endblock %}
{% block page-title %}{% firstof site_title "Combo" %}{% endblock %}
{% block site-title %}{% firstof site_title "Combo" %}{% endblock %}
@ -34,5 +35,6 @@
<script src="{% static "js/combo.manager.js" %}"></script>
<script src="{% static "xstatic/leaflet.js" %}"></script>
<script src="{% static "js/combo.map.js" %}"></script>
<script src="{% static "js/combo.multiselectwidget.js" %}"></script>
<script src="{% url "javascript-catalog" %}"></script>
{% endblock %}

View File

@ -1584,27 +1584,46 @@ 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)
resp.form[field_prefix + 'color'].select_multiple(texts=['Blue', 'Green'])
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)
assert resp.form[field_prefix + 'color'].value == ['green', 'blue']
cell.refresh_from_db()
assert cell.filter_params == {'color': ['green', 'blue']}
assert cell.filter_params == {'color': ['blue']}
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')
manager_submit_cell(resp.form)
cell.refresh_from_db()
assert cell.filter_params == {'color': ['red', '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[field_prefix + 'color'].value == ['green', 'blue']
assert resp.form[field_prefix + 'color'].options == [
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', True, 'Green'),
('blue', True, 'blue (unavailable)'),
('green', False, 'Green'),
('red', True, 'red (unavailable)'),
]
resp.form[field_prefix + 'color'].select_multiple(texts=[])
resp.form.get(field_prefix + 'color', 0).select(text='Green')
manager_submit_cell(resp.form)
assert resp.form[field_prefix + 'color'].value is None
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 == ''
cell.refresh_from_db()
assert cell.get_filter_params() == {}
@ -1884,7 +1903,7 @@ def test_chartng_cell_manager_new_api_page_variables(app, admin_user, new_api_st
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']
assert [x[0] for x in color_field.options] == ['', 'red', 'green', 'blue']
def test_chartng_cell_manager_new_api_tabs(app, admin_user):