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:
Andrew Pashkin 2015-04-19 17:20:29 +03:00 committed by Felipe Monteiro
parent e96b4c98e2
commit e096fbfa3c
6 changed files with 243 additions and 1 deletions

View 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},
)

View 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

View File

@ -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);
}
}
});

View File

@ -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>

View File

@ -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 }}" />

View File

@ -16,4 +16,4 @@ semantic-version>=2.3.1 # BSD
# message extraction
Babel!=2.4.0,>=2.3.4 # BSD
django-babel>=0.5.1 # BSD
django-babel>=0.5.1 # BSD