Tri-state checkbox widget
Tri-state checkbox is the kind of checkbox which has three states instead of two, as the regular checkbox has. Third state is "indeterminate", which means, that the element is in undefined state. Common example of such situation is some parent element, which has some of child elements selected and some not - and so it means that parent element is not selected nor unselect - which in terminology of this changeset called "indeterminate" state. Partially implements bp assign-category-button Depends-On: Ie65a77eab29aa191420fbd630b1d242b59c9ff9f Change-Id: I98116fce362af5eb4c64941f4c442dd81120e762
This commit is contained in:
parent
e96b4c98e2
commit
e096fbfa3c
72
muranodashboard/common/fields.py
Normal file
72
muranodashboard/common/fields.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from muranodashboard.common import widgets
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core import validators
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class TriStateMultipleChoiceField(forms.ChoiceField):
|
||||||
|
"""A multiple choice checkbox field where checkboxes has three states.
|
||||||
|
|
||||||
|
States are:
|
||||||
|
|
||||||
|
- Checked
|
||||||
|
- Unchecked
|
||||||
|
- Indeterminate
|
||||||
|
|
||||||
|
It takes a ``dict`` instance as a value,
|
||||||
|
where keys are internal values from `choices`
|
||||||
|
and values are ones from following (in order respectively to states):
|
||||||
|
|
||||||
|
- True
|
||||||
|
- False
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
widget = widgets.TriStateCheckboxSelectMultiple
|
||||||
|
default_error_messages = {
|
||||||
|
'invalid_choice': _('Select a valid choice. %(value)s is not one '
|
||||||
|
'of the available choices.'),
|
||||||
|
'invalid_value': _('Enter a dict with choices and values. '
|
||||||
|
'Got %(value)s.'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
"""Checks if value, that comes from widget, is a dict."""
|
||||||
|
if value in validators.EMPTY_VALUES:
|
||||||
|
return {}
|
||||||
|
elif not isinstance(value, dict):
|
||||||
|
raise ValidationError(self.error_messages['invalid_value'],
|
||||||
|
code='invalid_value')
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
"""Ensures that value has only allowed values."""
|
||||||
|
if not set(value.keys()) <= {k for k, _ in self.choices}:
|
||||||
|
raise ValidationError(
|
||||||
|
self.error_messages['invalid_choice'],
|
||||||
|
code='invalid_choice',
|
||||||
|
params={'value': value},
|
||||||
|
)
|
||||||
|
elif not (set(value.values()) <=
|
||||||
|
set(widgets.TriStateCheckboxSelectMultiple
|
||||||
|
.VALUES_MAP.values())):
|
||||||
|
raise ValidationError(
|
||||||
|
self.error_messages['invalid_value'],
|
||||||
|
code='invalid_value',
|
||||||
|
params={'value': value},
|
||||||
|
)
|
96
muranodashboard/common/widgets.py
Normal file
96
muranodashboard/common/widgets.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# Copyright (c) 2015 Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import itertools as it
|
||||||
|
|
||||||
|
import floppyforms as floppy
|
||||||
|
|
||||||
|
|
||||||
|
class TriStateCheckboxSelectMultiple(floppy.widgets.Input):
|
||||||
|
"""Renders tri-state multi-selectable checkbox.
|
||||||
|
|
||||||
|
.. note:: Subclassed from ``CheckboxSelectMultiple`` and not from
|
||||||
|
``SelectMultiple`` only to make
|
||||||
|
``horizon.templatetags.form_helpers.is_checkbox`` able to recognize
|
||||||
|
this widget.
|
||||||
|
|
||||||
|
Otherwise template ``horizon/common/_form_field.html`` would render
|
||||||
|
this widget slightly incorrectly.
|
||||||
|
"""
|
||||||
|
template_name = 'common/tri_state_checkbox/base.html'
|
||||||
|
|
||||||
|
VALUES_MAP = {
|
||||||
|
'True': True,
|
||||||
|
'False': False,
|
||||||
|
'None': None
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_context(self, name, value, attrs=None, choices=()):
|
||||||
|
"""Renders html and JavaScript.
|
||||||
|
|
||||||
|
:param value: Dictionary of form
|
||||||
|
Choice => Value (Checked|Uncheckec|Indeterminate)
|
||||||
|
:type value: dict
|
||||||
|
"""
|
||||||
|
context = super(TriStateCheckboxSelectMultiple, self).get_context(
|
||||||
|
name, value, attrs
|
||||||
|
)
|
||||||
|
|
||||||
|
choices = dict(it.chain(self.choices, choices))
|
||||||
|
if value is None:
|
||||||
|
value = dict.fromkeys(choices, False)
|
||||||
|
else:
|
||||||
|
value = dict(dict.fromkeys(choices, False).items() +
|
||||||
|
value.items())
|
||||||
|
|
||||||
|
context['values'] = [
|
||||||
|
(choice, label, value[choice])
|
||||||
|
for choice, label in choices.iteritems()
|
||||||
|
]
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_value(cls, value):
|
||||||
|
"""Converts encoded string with value to Python values."""
|
||||||
|
choice, value = value.split('=')
|
||||||
|
value = cls.VALUES_MAP[value]
|
||||||
|
|
||||||
|
return choice, value
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
"""Expects values in ``"key=False/True/None"`` form."""
|
||||||
|
try:
|
||||||
|
values = data.getlist(name)
|
||||||
|
except AttributeError:
|
||||||
|
if name in data:
|
||||||
|
values = [data[name]]
|
||||||
|
else:
|
||||||
|
values = []
|
||||||
|
|
||||||
|
return dict(map(self.parse_value, values))
|
||||||
|
|
||||||
|
|
||||||
|
class ExtraContextWidgetMixin(object):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ExtraContextWidgetMixin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.extra_context = kwargs.pop('extra_context', {})
|
||||||
|
|
||||||
|
def get_context(self, *args, **kwargs):
|
||||||
|
context = super(ExtraContextWidgetMixin, self).get_context(
|
||||||
|
*args, **kwargs
|
||||||
|
)
|
||||||
|
context.update(self.extra_context)
|
||||||
|
return context
|
@ -0,0 +1,44 @@
|
|||||||
|
$(function() {
|
||||||
|
"use strict";
|
||||||
|
//Updates value of hidden input based on state of visible input
|
||||||
|
function updateValue ($beautyInput, $valueInput) {
|
||||||
|
var value;
|
||||||
|
if ($beautyInput.prop('indeterminate')) {
|
||||||
|
value = 'None';
|
||||||
|
} else if ($beautyInput.prop('checked')) {
|
||||||
|
value = 'True';
|
||||||
|
} else {
|
||||||
|
value = 'False';
|
||||||
|
}
|
||||||
|
$valueInput.val($valueInput.val().split('=')[0] + '=' + value);
|
||||||
|
}
|
||||||
|
function makeUpdater(beautyInput, valueInput) {
|
||||||
|
return function() {
|
||||||
|
updateValue(beautyInput, valueInput);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var i, len, $beautyInput, $valueInput, updater, value, $inputs;
|
||||||
|
|
||||||
|
$inputs = $('[data-tri-state-checkbox=]');
|
||||||
|
|
||||||
|
for (i = 0, len = $inputs.length; i < len; i++) {
|
||||||
|
//Subscribe hidden input to updates of visible input
|
||||||
|
$valueInput = $inputs.eq(i);
|
||||||
|
$beautyInput = $valueInput.prev();
|
||||||
|
updater = makeUpdater($beautyInput, $valueInput);
|
||||||
|
$beautyInput.change(updater);
|
||||||
|
|
||||||
|
//Set initial state of visible input
|
||||||
|
value = $valueInput.val().split('=')[1];
|
||||||
|
if (value === 'True') {
|
||||||
|
$beautyInput.prop('checked', true);
|
||||||
|
$beautyInput.prop('indeterminate', false);
|
||||||
|
} else if (value === 'False') {
|
||||||
|
$beautyInput.prop('checked', false);
|
||||||
|
$beautyInput.prop('indeterminate', false);
|
||||||
|
} else {
|
||||||
|
$beautyInput.prop('checked', false);
|
||||||
|
$beautyInput.prop('indeterminate', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,14 @@
|
|||||||
|
<ul>
|
||||||
|
{% for choice, label, value in values %}
|
||||||
|
<li>
|
||||||
|
{% with id=attrs.id|add:forloop.counter %}
|
||||||
|
{% include 'common/tri_state_checkbox/input.html' %}
|
||||||
|
<label {% if attrs.id %}
|
||||||
|
for="{{ id }}"
|
||||||
|
{% endif %}>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
{% endwith %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
@ -0,0 +1,16 @@
|
|||||||
|
<input type="checkbox"
|
||||||
|
{% for attr, value in attrs.iteritems %}
|
||||||
|
{% if attr != 'name' and attr != 'id' and attr != 'value' %}
|
||||||
|
{{ attr }}="{{ value }}"
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %} />
|
||||||
|
<input type="hidden"
|
||||||
|
data-tri-state-checkbox
|
||||||
|
{% if attrs.id %}
|
||||||
|
id="{{ id }}"
|
||||||
|
{% endif %}
|
||||||
|
{% for attr, value in attrs.iteritems %}
|
||||||
|
{{ attr }}="{{ value }}"
|
||||||
|
{% endfor %}
|
||||||
|
name="{{ name }}"
|
||||||
|
value="{{ choice }}={{ value }}" />
|
Loading…
Reference in New Issue
Block a user