dataviz: add new widget to select multiple filter values (#74061)
gitea/combo/pipeline/head This commit looks good
Details
gitea/combo/pipeline/head This commit looks good
Details
This commit is contained in:
parent
378f4d6b53
commit
337259dc5a
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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))
|
|
@ -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 %}
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue