From e096fbfa3c8019758d03de832f75878fa6c09d84 Mon Sep 17 00:00:00 2001 From: Andrew Pashkin Date: Sun, 19 Apr 2015 17:20:29 +0300 Subject: [PATCH] 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 --- muranodashboard/common/fields.py | 72 ++++++++++++++ muranodashboard/common/widgets.py | 96 +++++++++++++++++++ .../muranodashboard/js/triStateCheckbox.js | 44 +++++++++ .../common/tri_state_checkbox/base.html | 14 +++ .../common/tri_state_checkbox/input.html | 16 ++++ requirements.txt | 2 +- 6 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 muranodashboard/common/fields.py create mode 100644 muranodashboard/common/widgets.py create mode 100644 muranodashboard/static/muranodashboard/js/triStateCheckbox.js create mode 100644 muranodashboard/templates/common/tri_state_checkbox/base.html create mode 100644 muranodashboard/templates/common/tri_state_checkbox/input.html diff --git a/muranodashboard/common/fields.py b/muranodashboard/common/fields.py new file mode 100644 index 000000000..db5bebc02 --- /dev/null +++ b/muranodashboard/common/fields.py @@ -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}, + ) diff --git a/muranodashboard/common/widgets.py b/muranodashboard/common/widgets.py new file mode 100644 index 000000000..dd14d932e --- /dev/null +++ b/muranodashboard/common/widgets.py @@ -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 diff --git a/muranodashboard/static/muranodashboard/js/triStateCheckbox.js b/muranodashboard/static/muranodashboard/js/triStateCheckbox.js new file mode 100644 index 000000000..260ebefab --- /dev/null +++ b/muranodashboard/static/muranodashboard/js/triStateCheckbox.js @@ -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); + } + } +}); diff --git a/muranodashboard/templates/common/tri_state_checkbox/base.html b/muranodashboard/templates/common/tri_state_checkbox/base.html new file mode 100644 index 000000000..1cf210097 --- /dev/null +++ b/muranodashboard/templates/common/tri_state_checkbox/base.html @@ -0,0 +1,14 @@ + diff --git a/muranodashboard/templates/common/tri_state_checkbox/input.html b/muranodashboard/templates/common/tri_state_checkbox/input.html new file mode 100644 index 000000000..ab0e57d24 --- /dev/null +++ b/muranodashboard/templates/common/tri_state_checkbox/input.html @@ -0,0 +1,16 @@ + + diff --git a/requirements.txt b/requirements.txt index 4ef766abf..8484ae7f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file