dataviz, avoir un widget dynamique pour la sélection multiple (#74061) #41
|
@ -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');
|
||||
vdeniaud marked this conversation as resolved
Outdated
|
||||
var $row = $(this).parents('.combo-multi-select-widget--field');
|
||||
$row.remove();
|
||||
$field.change();
|
||||
vdeniaud marked this conversation as resolved
Outdated
vdeniaud
commented
Si plusieurs cellules sont sur la même page, le JS se retrouve inclut et donc exécuté plusieurs fois. Ceci m'a l'air d'être la technique la plus simple pour assurer qu'on ait pas plusieurs handlers attachés aux boutons (sinon un clic va déclencher l'apparition d'autant de lignes qu'il y a de cellules, pas ouf). Si plusieurs cellules sont sur la même page, le JS se retrouve inclut et donc exécuté plusieurs fois. Ceci m'a l'air d'être la technique la plus simple pour assurer qu'on ait pas plusieurs handlers attachés aux boutons (sinon un clic va déclencher l'apparition d'autant de lignes qu'il y a de cellules, pas ouf).
tjund
commented
unbind eet déprécié, utiliser unbind eet déprécié, utiliser `off` à la place. cf commit de proposition
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
{% load i18n %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block cell-content %}
|
||||
<h2>{{ cell.title }}</h2>
|
||||
|
@ -6,7 +6,7 @@
|
|||
<div>
|
||||
{% if form.fields %}
|
||||
<form method='get' enctype='multipart/form-data' id='chart-filters'>
|
||||
{{ form.as_p }}
|
||||
{{ form|with_template }}
|
||||
<div class='buttons'>
|
||||
<button class='submit-button'>{% trans 'Refresh' %}</button>
|
||||
</div>
|
||||
|
@ -44,15 +44,15 @@
|
|||
});
|
||||
}
|
||||
|
||||
start_field = $('#id_filter-time_range_start');
|
||||
end_field = $('#id_filter-time_range_end');
|
||||
start_field = $('#id_filter-time_range_start_p');
|
||||
end_field = $('#id_filter-time_range_end_p');
|
||||
$('#id_filter-time_range').change(function() {
|
||||
if(this.value == 'range') {
|
||||
start_field.parent().show();
|
||||
end_field.parent().show();
|
||||
start_field.show();
|
||||
end_field.show();
|
||||
} else {
|
||||
start_field.parent().hide();
|
||||
end_field.parent().hide();
|
||||
start_field.hide();
|
||||
end_field.hide();
|
||||
}
|
||||
}).change();
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{% load gadjo %}
|
||||
|
||||
<div style="position: relative">
|
||||
{{ form.as_p }}
|
||||
{{ form|with_template }}
|
||||
{% if cell.statistic and cell.chart_type != "table" and cell.chart_type != "table-inverted" %}
|
||||
<div style="position: absolute; right: 0; top: 0; width: 300px; height: 150px">
|
||||
<embed type="image/svg+xml" src="{% url 'combo-dataviz-graph' cell=cell.id %}?width=300&height=150"/>
|
||||
|
@ -9,28 +11,28 @@
|
|||
|
||||
<script>
|
||||
$(function () {
|
||||
start_field = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_start');
|
||||
end_field = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_end');
|
||||
start_field_template = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_start_template');
|
||||
end_field_template = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_end_template');
|
||||
start_field = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_start_p');
|
||||
end_field = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_end_p');
|
||||
start_field_template = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_start_template_p');
|
||||
end_field_template = $('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range_end_template_p');
|
||||
$('#id_cdataviz_chartngcell-{{ cell.pk }}-time_range').change(function() {
|
||||
if(this.value == 'range') {
|
||||
start_field.parent().show();
|
||||
end_field.parent().show();
|
||||
start_field.show();
|
||||
end_field.show();
|
||||
} else {
|
||||
start_field.parent().hide();
|
||||
end_field.parent().hide();
|
||||
start_field.hide();
|
||||
end_field.hide();
|
||||
}
|
||||
if(this.value == 'range-template') {
|
||||
start_field_template.parent().show();
|
||||
end_field_template.parent().show();
|
||||
start_field_template.show();
|
||||
end_field_template.show();
|
||||
} else {
|
||||
start_field_template.parent().hide();
|
||||
end_field_template.parent().hide();
|
||||
start_field_template.hide();
|
||||
end_field_template.hide();
|
||||
}
|
||||
}).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();
|
||||
vdeniaud marked this conversation as resolved
Outdated
vdeniaud
commented
Petit changement de cible de ce sélecteur, sinon les Petit changement de cible de ce sélecteur, sinon les `<select>` ajoutés dynamiquement ne déclenchaient pas le rafraîchissement de la cellule.
|
||||
});
|
||||
|
||||
|
|
|
@ -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() == {}
|
||||
|
||||
|
@ -1822,7 +1841,7 @@ def test_chartng_cell_manager_new_api_dynamic_fields(app, admin_user, new_api_st
|
|||
resp.form[field_prefix + 'statistic'] = statistic.pk
|
||||
resp = app.post(resp.form.action, params=resp.form.submit_fields(), xhr=True)
|
||||
assert 'time_interval' in resp.json['tabs']['general']['form']
|
||||
assert 'This field is required.' not in resp.json['tabs']['general']['form']
|
||||
assert '<div class="error">' not in resp.json['tabs']['general']['form']
|
||||
|
||||
|
||||
@with_httmock(new_api_mock)
|
||||
|
@ -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
C'est nécessaire pour le rafraîchissement auto de la cellule, le fonctionnement étant :
Utiliser un event pour cela t'oblige à ajouter des lignes des codes JS dans les templates où le widget est utilisé. On peut s'en passer, je pousse un commit de proposition