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