Merge "Horizon Checkboxes are now themeable."
This commit is contained in:
commit
721c963b9d
@ -209,6 +209,7 @@ full use of the Bootstrap theme architecture.
|
|||||||
* Login_
|
* Login_
|
||||||
* Tabs_
|
* Tabs_
|
||||||
* Alerts_
|
* Alerts_
|
||||||
|
* Checkboxes_
|
||||||
|
|
||||||
Step 1
|
Step 1
|
||||||
------
|
------
|
||||||
@ -321,6 +322,13 @@ Alerts
|
|||||||
Alerts use the basic Bootstrap brand colors. See **Colors** section of your
|
Alerts use the basic Bootstrap brand colors. See **Colors** section of your
|
||||||
variables file for specifics.
|
variables file for specifics.
|
||||||
|
|
||||||
|
Checkboxes
|
||||||
|
----------
|
||||||
|
|
||||||
|
Horizon uses icon fonts to represent checkboxes. In order to customize
|
||||||
|
this, you simply need to override the standard scss. For an example of
|
||||||
|
this, see themes/material/static/horizon/components/_checkboxes.scss
|
||||||
|
|
||||||
Bootswatch and Material Design
|
Bootswatch and Material Design
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
|
@ -33,6 +33,8 @@ from horizon.forms.fields import IPv4 # noqa
|
|||||||
from horizon.forms.fields import IPv6 # noqa
|
from horizon.forms.fields import IPv6 # noqa
|
||||||
from horizon.forms.fields import MultiIPField # noqa
|
from horizon.forms.fields import MultiIPField # noqa
|
||||||
from horizon.forms.fields import SelectWidget # noqa
|
from horizon.forms.fields import SelectWidget # noqa
|
||||||
|
from horizon.forms.fields import ThemableCheckboxInput # noqa
|
||||||
|
from horizon.forms.fields import ThemableCheckboxSelectMultiple # noqa
|
||||||
from horizon.forms.views import ModalFormMixin # noqa
|
from horizon.forms.views import ModalFormMixin # noqa
|
||||||
from horizon.forms.views import ModalFormView # noqa
|
from horizon.forms.views import ModalFormView # noqa
|
||||||
|
|
||||||
@ -45,6 +47,8 @@ __all__ = [
|
|||||||
"ModalFormMixin",
|
"ModalFormMixin",
|
||||||
"DynamicTypedChoiceField",
|
"DynamicTypedChoiceField",
|
||||||
"DynamicChoiceField",
|
"DynamicChoiceField",
|
||||||
|
"ThemableCheckboxInput",
|
||||||
|
"ThemableCheckboxSelectMultiple",
|
||||||
"IPField",
|
"IPField",
|
||||||
"IPv4",
|
"IPv4",
|
||||||
"IPv6",
|
"IPv6",
|
||||||
|
@ -16,6 +16,7 @@ import re
|
|||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
import six
|
import six
|
||||||
|
import uuid
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError # noqa
|
from django.core.exceptions import ValidationError # noqa
|
||||||
from django.core import urlresolvers
|
from django.core import urlresolvers
|
||||||
@ -253,3 +254,44 @@ class DynamicChoiceField(fields.ChoiceField):
|
|||||||
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
|
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
|
||||||
"""Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``."""
|
"""Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableCheckboxInput(widgets.CheckboxInput):
|
||||||
|
"""A subclass of the ``Checkbox`` widget which renders extra markup to
|
||||||
|
allow a custom checkbox experience.
|
||||||
|
"""
|
||||||
|
def render(self, name, value, attrs=None):
|
||||||
|
label_for = attrs.get('id', '')
|
||||||
|
|
||||||
|
if not label_for:
|
||||||
|
attrs['id'] = uuid.uuid4()
|
||||||
|
label_for = attrs['id']
|
||||||
|
|
||||||
|
return html.format_html(
|
||||||
|
u'<div class="themable-checkbox">{}<label for="{}"></label></div>',
|
||||||
|
super(ThemableCheckboxInput, self).render(name, value, attrs),
|
||||||
|
label_for
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableCheckboxChoiceInput(widgets.CheckboxChoiceInput):
|
||||||
|
def render(self, name=None, value=None, attrs=None, choices=()):
|
||||||
|
if self.id_for_label:
|
||||||
|
label_for = html.format_html(' for="{}"', self.id_for_label)
|
||||||
|
else:
|
||||||
|
label_for = ''
|
||||||
|
attrs = dict(self.attrs, **attrs) if attrs else self.attrs
|
||||||
|
return html.format_html(
|
||||||
|
u'<div class="themable-checkbox">{}<label{}>' +
|
||||||
|
u'<span>{}</span></label></div>',
|
||||||
|
self.tag(attrs), label_for, self.choice_label
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableCheckboxFieldRenderer(widgets.CheckboxFieldRenderer):
|
||||||
|
choice_input_class = ThemableCheckboxChoiceInput
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableCheckboxSelectMultiple(widgets.CheckboxSelectMultiple):
|
||||||
|
renderer = ThemableCheckboxFieldRenderer
|
||||||
|
_empty_value = []
|
||||||
|
@ -89,9 +89,12 @@ horizon.datatables = {
|
|||||||
|
|
||||||
// Only replace row if the html content has changed
|
// Only replace row if the html content has changed
|
||||||
if($new_row.html() !== $row.html()) {
|
if($new_row.html() !== $row.html()) {
|
||||||
if($row.find('.table-row-multi-select:checkbox').is(':checked')) {
|
|
||||||
|
// Directly accessing the checked property of the element
|
||||||
|
// is MUCH faster than using jQuery's helper method
|
||||||
|
if($row.find('.table-row-multi-select')[0].checked) {
|
||||||
// Preserve the checkbox if it's already clicked
|
// Preserve the checkbox if it's already clicked
|
||||||
$new_row.find('.table-row-multi-select:checkbox').prop('checked', true);
|
$new_row.find('.table-row-multi-select').prop('checked', true);
|
||||||
}
|
}
|
||||||
$row.replaceWith($new_row);
|
$row.replaceWith($new_row);
|
||||||
// Reset tablesorter's data cache.
|
// Reset tablesorter's data cache.
|
||||||
@ -151,37 +154,50 @@ horizon.datatables = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
validate_button: function ($form) {
|
validate_button: function ($form, disable_button) {
|
||||||
// Enable or disable table batch action buttons based on row selection.
|
// Enable or disable table batch action buttons based on row selection.
|
||||||
$form = $form || $(".table_wrapper > form");
|
$form = $form || $(".table_wrapper > form");
|
||||||
$form.each(function () {
|
$form.each(function () {
|
||||||
var $this = $(this);
|
var $this = $(this);
|
||||||
var checkboxes = $this.find(".table-row-multi-select:checkbox");
|
var $action_buttons = $this.find('.table_actions button[data-batch-action="true"]');
|
||||||
var action_buttons = $this.find('.table_actions button[data-batch-action="true"]');
|
if (typeof disable_button == undefined) {
|
||||||
action_buttons.toggleClass("disabled", !checkboxes.filter(":checked").length);
|
disable_button = $this.find(".table-row-multi-select").filter(":checked").length > 0;
|
||||||
|
}
|
||||||
|
$action_buttons.toggleClass("disabled", disable_button);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize_checkboxes_behavior: function() {
|
initialize_checkboxes_behavior: function() {
|
||||||
// Bind the "select all" checkbox action.
|
// Bind the "select all" checkbox action.
|
||||||
$('div.table_wrapper, #modal_wrapper').on('click', 'table thead .multi_select_column .table-row-multi-select:checkbox', function() {
|
$('.table_wrapper, #modal_wrapper')
|
||||||
var $this = $(this),
|
.on('change', '.table-row-multi-select', function() {
|
||||||
$table = $this.closest('table'),
|
var $this = $(this);
|
||||||
is_checked = $this.prop('checked'),
|
var $table = $this.closest('table');
|
||||||
checkboxes = $table.find('tbody .table-row-multi-select:visible:checkbox');
|
var is_checked = $this.prop('checked');
|
||||||
checkboxes.prop('checked', is_checked);
|
|
||||||
});
|
if ($this.hasClass('multi-select-header')) {
|
||||||
// Change "select all" checkbox behavior while any checkbox is checked/unchecked.
|
|
||||||
$("div.table_wrapper, #modal_wrapper").on("click", 'table tbody .table-row-multi-select:checkbox', function () {
|
// Only select / deselect the visible rows
|
||||||
var $table = $(this).closest('table');
|
$table.find('tbody tr:visible .table-row-multi-select')
|
||||||
var $multi_select_checkbox = $table.find('thead .multi_select_column .table-row-multi-select:checkbox');
|
.prop('checked', is_checked);
|
||||||
var any_unchecked = $table.find("tbody .table-row-multi-select:checkbox").not(":checked");
|
|
||||||
$multi_select_checkbox.prop('checked', any_unchecked.length === 0);
|
} else {
|
||||||
});
|
|
||||||
// Enable/disable table batch action buttons when row selection changes.
|
// Find the master checkbox
|
||||||
$("div.table_wrapper, #modal_wrapper").on("click", '.table-row-multi-select:checkbox', function () {
|
var $multi_select_checkbox = $table.find('.multi-select-header');
|
||||||
horizon.datatables.validate_button($(this).closest("form"));
|
|
||||||
});
|
// Determine if there are any unchecked checkboxes in the table
|
||||||
|
var $checkboxes = $table.find('tbody .table-row-multi-select');
|
||||||
|
var not_checked = $checkboxes.not(':checked').length;
|
||||||
|
is_checked = $checkboxes.length != not_checked;
|
||||||
|
|
||||||
|
// If there are none, then check the master checkbox
|
||||||
|
$multi_select_checkbox.prop('checked', not_checked == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass in whether it should be visible, no point in doing this twice
|
||||||
|
horizon.datatables.validate_button($this.closest('form'), !is_checked);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize_table_tooltips: function() {
|
initialize_table_tooltips: function() {
|
||||||
@ -229,7 +245,7 @@ horizon.datatables.confirm = function (action) {
|
|||||||
var actions_div = $(action).closest("div");
|
var actions_div = $(action).closest("div");
|
||||||
if(actions_div.hasClass("table_actions") || actions_div.hasClass("table_actions_menu")) {
|
if(actions_div.hasClass("table_actions") || actions_div.hasClass("table_actions_menu")) {
|
||||||
// One or more checkboxes selected
|
// One or more checkboxes selected
|
||||||
$("#"+closest_table_id+" tr[data-display]").has(".table-row-multi-select:checkbox:checked").each(function() {
|
$("#"+closest_table_id+" tr[data-display]").has(".table-row-multi-select:checked").each(function() {
|
||||||
name_array.push(" \"" + $(this).attr("data-display") + "\"");
|
name_array.push(" \"" + $(this).attr("data-display") + "\"");
|
||||||
});
|
});
|
||||||
name_array.join(", ");
|
name_array.join(", ");
|
||||||
@ -480,11 +496,30 @@ horizon.datatables.set_table_sorting = function (parent) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
horizon.datatables.add_table_checkboxes = function(parent) {
|
horizon.datatables.add_table_checkboxes = function($parent) {
|
||||||
$(parent).find('table thead .multi_select_column').each(function(index, thead) {
|
$($parent).find('table thead .multi_select_column').each(function() {
|
||||||
if (!$(thead).find('.table-row-multi-select:checkbox').length &&
|
var $thead = $(this);
|
||||||
$(thead).parents('table').find('tbody .table-row-multi-select:checkbox').length) {
|
if (!$thead.find('.table-row-multi-select').length &&
|
||||||
$(thead).append('<input type="checkbox" class="table-row-multi-select">');
|
$thead.parents('table').find('tbody .table-row-multi-select').length) {
|
||||||
|
|
||||||
|
// Build up the themable checkbox
|
||||||
|
var $container = $(document.createElement('div'))
|
||||||
|
.addClass('themable-checkbox');
|
||||||
|
|
||||||
|
// Create the input checkbox
|
||||||
|
var $input = $(document.createElement('input'))
|
||||||
|
.attr('type', 'checkbox')
|
||||||
|
.addClass('table-row-multi-select multi-select-header')
|
||||||
|
.uniqueId()
|
||||||
|
.appendTo($container);
|
||||||
|
|
||||||
|
// Create the label
|
||||||
|
$(document.createElement('label'))
|
||||||
|
.attr('for', $input.attr('id'))
|
||||||
|
.appendTo($container);
|
||||||
|
|
||||||
|
// Append to the thead last, for speed
|
||||||
|
$thead.append($container);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -576,10 +611,11 @@ horizon.addInitFunction(horizon.datatables.init = function() {
|
|||||||
horizon.datatables.initialize_table_tooltips();
|
horizon.datatables.initialize_table_tooltips();
|
||||||
|
|
||||||
// Trigger run-once setup scripts for tables.
|
// Trigger run-once setup scripts for tables.
|
||||||
horizon.datatables.add_table_checkboxes($('body'));
|
var $body = $('body');
|
||||||
horizon.datatables.set_table_sorting($('body'));
|
horizon.datatables.add_table_checkboxes($body);
|
||||||
horizon.datatables.set_table_query_filter($('body'));
|
horizon.datatables.set_table_sorting($body);
|
||||||
horizon.datatables.set_table_fixed_filter($('body'));
|
horizon.datatables.set_table_query_filter($body);
|
||||||
|
horizon.datatables.set_table_fixed_filter($body);
|
||||||
horizon.datatables.disable_actions_on_submit();
|
horizon.datatables.disable_actions_on_submit();
|
||||||
|
|
||||||
// Also apply on tables in modal views.
|
// Also apply on tables in modal views.
|
||||||
|
@ -37,6 +37,7 @@ import six
|
|||||||
|
|
||||||
from horizon import conf
|
from horizon import conf
|
||||||
from horizon import exceptions
|
from horizon import exceptions
|
||||||
|
from horizon.forms import ThemableCheckboxInput
|
||||||
from horizon import messages
|
from horizon import messages
|
||||||
from horizon.tables.actions import FilterAction # noqa
|
from horizon.tables.actions import FilterAction # noqa
|
||||||
from horizon.tables.actions import LinkAction # noqa
|
from horizon.tables.actions import LinkAction # noqa
|
||||||
@ -672,7 +673,7 @@ class Cell(html.HTMLElement):
|
|||||||
if column.auto == "multi_select":
|
if column.auto == "multi_select":
|
||||||
data = ""
|
data = ""
|
||||||
if row.can_be_selected(datum):
|
if row.can_be_selected(datum):
|
||||||
widget = forms.CheckboxInput(check_test=lambda value: False)
|
widget = ThemableCheckboxInput(check_test=lambda value: False)
|
||||||
# Convert value to string to avoid accidental type conversion
|
# Convert value to string to avoid accidental type conversion
|
||||||
data = widget.render('object_ids',
|
data = widget.render('object_ids',
|
||||||
six.text_type(table.get_object_id(datum)),
|
six.text_type(table.get_object_id(datum)),
|
||||||
|
@ -3,27 +3,7 @@
|
|||||||
<div class="form-group{% if field.errors %} has-error{% endif %} {{ field.css_classes }}">
|
<div class="form-group{% if field.errors %} has-error{% endif %} {{ field.css_classes }}">
|
||||||
{% if field|is_checkbox %}
|
{% if field|is_checkbox %}
|
||||||
<div class="{{ classes.single_value }}">
|
<div class="{{ classes.single_value }}">
|
||||||
<div class="checkbox">
|
{% include 'horizon/common/fields/_themable_checkbox.html' %}
|
||||||
{% if field.auto_id %}
|
|
||||||
<label
|
|
||||||
{% if field.field.required and form.required_css_class %}
|
|
||||||
class="{{ form.required_css_class }}"
|
|
||||||
{% endif %}>
|
|
||||||
{{ field }}
|
|
||||||
<span>{{ field.label }}</span>
|
|
||||||
{% if field.field.required %}{% include "horizon/common/_form_field_required.html" %}{% endif %}
|
|
||||||
{% if field.help_text %}
|
|
||||||
<span class="help-icon" data-toggle="tooltip"
|
|
||||||
data-placement="top" title="{{ field.help_text|safe }}">
|
|
||||||
<span class="fa fa-question-circle"></span>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</label>
|
|
||||||
{% endif %}
|
|
||||||
{% for error in field.errors %}
|
|
||||||
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% elif field|is_radio %}
|
{% elif field|is_radio %}
|
||||||
{% if field.auto_id %}
|
{% if field.auto_id %}
|
||||||
|
@ -3,7 +3,13 @@
|
|||||||
<div class="form-group{% if field.errors %} has-error{% endif %} {{ field.css_classes }}">
|
<div class="form-group{% if field.errors %} has-error{% endif %} {{ field.css_classes }}">
|
||||||
<label class="control-label col-sm-3 {% if field.field.required %}{{ form.required_css_class }}{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
<label class="control-label col-sm-3 {% if field.field.required %}{{ form.required_css_class }}{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||||
<div class="col-sm-9 {{ classes.value }} {{ field|wrapper_classes }}">
|
<div class="col-sm-9 {{ classes.value }} {{ field|wrapper_classes }}">
|
||||||
{{ field|add_bootstrap_class }}
|
{% if field|is_checkbox %}
|
||||||
|
{% with is_vertical=1 %}
|
||||||
|
{% include 'horizon/common/fields/_themable_checkbox.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
{{ field|add_bootstrap_class }}
|
||||||
|
{% endif %}
|
||||||
{% for error in field.errors %}
|
{% for error in field.errors %}
|
||||||
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
<div class="themable-checkbox">
|
||||||
|
{% if field.auto_id %}
|
||||||
|
{{ field }}
|
||||||
|
<label
|
||||||
|
{% if field.field.required and form.required_css_class %}
|
||||||
|
class="{{ form.required_css_class }}"
|
||||||
|
{% endif %}
|
||||||
|
for="{{ field.auto_id }}">
|
||||||
|
{% if not is_vertical %}
|
||||||
|
<span>{{ field.label }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.field.required %}{% include "horizon/common/_form_field_required.html" %}{% endif %}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<span class="help-icon" data-toggle="tooltip"
|
||||||
|
data-placement="top" title="{{ field.help_text|safe }}">
|
||||||
|
<span class="fa fa-question-circle"></span>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
@ -13,6 +13,7 @@
|
|||||||
import django.forms
|
import django.forms
|
||||||
from django import template as django_template
|
from django import template as django_template
|
||||||
|
|
||||||
|
|
||||||
register = django_template.Library()
|
register = django_template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
window.WEBROOT = '{{ WEBROOT }}';
|
window.WEBROOT = '{{ WEBROOT }}';
|
||||||
</script>
|
</script>
|
||||||
<script src='{{ STATIC_URL }}horizon/lib/jquery/jquery.js' type='text/javascript' charset="utf-8"></script>
|
<script src='{{ STATIC_URL }}horizon/lib/jquery/jquery.js' type='text/javascript' charset="utf-8"></script>
|
||||||
|
<script src='{{ STATIC_URL }}horizon/lib/jquery-ui/ui/jquery-ui.js' type='text/javascript' charset="utf-8"></script>
|
||||||
<script src='{{ STATIC_URL }}horizon/lib/jquery/jquery-migrate.js' type='text/javascript' charset="utf-8"></script>
|
<script src='{{ STATIC_URL }}horizon/lib/jquery/jquery-migrate.js' type='text/javascript' charset="utf-8"></script>
|
||||||
<script src="{{ STATIC_URL }}horizon/lib/jquery/jquery.tablesorter.js"></script>
|
<script src="{{ STATIC_URL }}horizon/lib/jquery/jquery.tablesorter.js"></script>
|
||||||
<script src="{{ STATIC_URL }}horizon/lib/angular/angular.js" type="text/javascript" charset="utf-8"></script>
|
<script src="{{ STATIC_URL }}horizon/lib/angular/angular.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
@ -338,10 +338,10 @@ class GetUserHomeTests(BaseHorizonTests):
|
|||||||
conf.HORIZON_CONFIG._setup()
|
conf.HORIZON_CONFIG._setup()
|
||||||
|
|
||||||
def test_using_callable(self):
|
def test_using_callable(self):
|
||||||
def fancy_user_fnc(user):
|
def themable_user_fnc(user):
|
||||||
return user.username.upper()
|
return user.username.upper()
|
||||||
|
|
||||||
settings.HORIZON_CONFIG['user_home'] = fancy_user_fnc
|
settings.HORIZON_CONFIG['user_home'] = themable_user_fnc
|
||||||
conf.HORIZON_CONFIG._setup()
|
conf.HORIZON_CONFIG._setup()
|
||||||
|
|
||||||
self.assertEqual(self.test_user.username.upper(),
|
self.assertEqual(self.test_user.username.upper(),
|
||||||
|
@ -37,7 +37,7 @@ class LazyLoadedTabsTests(test.SeleniumTestCase):
|
|||||||
table_selector = 'div.tab-content > div#{0} > div.table_wrapper'.format(
|
table_selector = 'div.tab-content > div#{0} > div.table_wrapper'.format(
|
||||||
tab_id)
|
tab_id)
|
||||||
button_selector = 'button#lazy_puppies__action_delete'
|
button_selector = 'button#lazy_puppies__action_delete'
|
||||||
checkbox_selector = 'td.multi_select_column > input[type=checkbox]'
|
checkbox_selector = 'td.multi_select_column input[type=checkbox]'
|
||||||
select_all_selector = 'th.multi_select_column input[type=checkbox]'
|
select_all_selector = 'th.multi_select_column input[type=checkbox]'
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -527,6 +527,10 @@
|
|||||||
<input type="checkbox"> Checkbox
|
<input type="checkbox"> Checkbox
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="themable-checkbox">
|
||||||
|
<input type="checkbox" id="themable-checkbox">
|
||||||
|
<label for="themable-checkbox" translate>Checkbox</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -338,7 +338,7 @@ class AddRouterToFirewall(RouterInsertionFormBase):
|
|||||||
router_ids = forms.MultipleChoiceField(
|
router_ids = forms.MultipleChoiceField(
|
||||||
label=_("Add Routers"),
|
label=_("Add Routers"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.CheckboxSelectMultiple(),
|
widget=forms.ThemableCheckboxSelectMultiple(),
|
||||||
help_text=_("Add selected router(s) to the firewall."))
|
help_text=_("Add selected router(s) to the firewall."))
|
||||||
|
|
||||||
failure_url = 'horizon:project:firewalls:index'
|
failure_url = 'horizon:project:firewalls:index'
|
||||||
@ -363,7 +363,7 @@ class RemoveRouterFromFirewall(RouterInsertionFormBase):
|
|||||||
router_ids = forms.MultipleChoiceField(
|
router_ids = forms.MultipleChoiceField(
|
||||||
label=_("Associated Routers"),
|
label=_("Associated Routers"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.CheckboxSelectMultiple(),
|
widget=forms.ThemableCheckboxSelectMultiple(),
|
||||||
help_text=_("Unselect the router(s) to be removed from firewall."))
|
help_text=_("Unselect the router(s) to be removed from firewall."))
|
||||||
|
|
||||||
failure_url = 'horizon:project:firewalls:index'
|
failure_url = 'horizon:project:firewalls:index'
|
||||||
|
@ -164,7 +164,7 @@ class SelectRulesAction(workflows.Action):
|
|||||||
rule = forms.MultipleChoiceField(
|
rule = forms.MultipleChoiceField(
|
||||||
label=_("Rules"),
|
label=_("Rules"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.CheckboxSelectMultiple(),
|
widget=forms.ThemableCheckboxSelectMultiple(),
|
||||||
help_text=_("Create a policy with selected rules."))
|
help_text=_("Create a policy with selected rules."))
|
||||||
|
|
||||||
class Meta(object):
|
class Meta(object):
|
||||||
@ -206,7 +206,7 @@ class SelectRoutersAction(workflows.Action):
|
|||||||
router = forms.MultipleChoiceField(
|
router = forms.MultipleChoiceField(
|
||||||
label=_("Routers"),
|
label=_("Routers"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.CheckboxSelectMultiple(),
|
widget=forms.ThemableCheckboxSelectMultiple(),
|
||||||
help_text=_("Create a firewall with selected routers."))
|
help_text=_("Create a firewall with selected routers."))
|
||||||
|
|
||||||
class Meta(object):
|
class Meta(object):
|
||||||
|
@ -1617,11 +1617,11 @@ class InstanceTests(helpers.TestCase):
|
|||||||
else:
|
else:
|
||||||
self.assertNotContains(res, boot_from_image_field_label)
|
self.assertNotContains(res, boot_from_image_field_label)
|
||||||
|
|
||||||
checked_label = '<label for="id_network_0"><input checked="checked"'
|
checked_box = '<input checked="checked" id="id_network_0"'
|
||||||
if only_one_network:
|
if only_one_network:
|
||||||
self.assertContains(res, checked_label)
|
self.assertContains(res, checked_box)
|
||||||
else:
|
else:
|
||||||
self.assertNotContains(res, checked_label)
|
self.assertNotContains(res, checked_box)
|
||||||
|
|
||||||
disk_config_field_label = 'Disk Partition'
|
disk_config_field_label = 'Disk Partition'
|
||||||
if disk_config:
|
if disk_config:
|
||||||
|
@ -551,12 +551,13 @@ class SetAccessControlsAction(workflows.Action):
|
|||||||
label=_("Confirm Admin Password"),
|
label=_("Confirm Admin Password"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.PasswordInput(render_value=False))
|
widget=forms.PasswordInput(render_value=False))
|
||||||
groups = forms.MultipleChoiceField(label=_("Security Groups"),
|
groups = forms.MultipleChoiceField(
|
||||||
required=False,
|
label=_("Security Groups"),
|
||||||
initial=["default"],
|
required=False,
|
||||||
widget=forms.CheckboxSelectMultiple(),
|
initial=["default"],
|
||||||
help_text=_("Launch instance in these "
|
widget=forms.ThemableCheckboxSelectMultiple(),
|
||||||
"security groups."))
|
help_text=_("Launch instance in these "
|
||||||
|
"security groups."))
|
||||||
|
|
||||||
class Meta(object):
|
class Meta(object):
|
||||||
name = _("Access & Security")
|
name = _("Access & Security")
|
||||||
@ -701,14 +702,15 @@ class PostCreationStep(workflows.Step):
|
|||||||
|
|
||||||
|
|
||||||
class SetNetworkAction(workflows.Action):
|
class SetNetworkAction(workflows.Action):
|
||||||
network = forms.MultipleChoiceField(label=_("Networks"),
|
network = forms.MultipleChoiceField(
|
||||||
widget=forms.CheckboxSelectMultiple(),
|
label=_("Networks"),
|
||||||
error_messages={
|
widget=forms.ThemableCheckboxSelectMultiple(),
|
||||||
'required': _(
|
error_messages={
|
||||||
"At least one network must"
|
'required': _(
|
||||||
" be specified.")},
|
"At least one network must"
|
||||||
help_text=_("Launch instance with"
|
" be specified.")},
|
||||||
" these networks"))
|
help_text=_("Launch instance with"
|
||||||
|
" these networks"))
|
||||||
if api.neutron.is_port_profiles_supported():
|
if api.neutron.is_port_profiles_supported():
|
||||||
widget = None
|
widget = None
|
||||||
else:
|
else:
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
@import '/horizon/lib/font-awesome/scss/variables';
|
||||||
|
@import '/horizon/lib/font-awesome/scss/mixins';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Checkboxes
|
||||||
|
// This will ONLY work when the label's 'for' attribute
|
||||||
|
// shares the input[type=checkbox]'s 'id' value
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
.themable-checkbox {
|
||||||
|
|
||||||
|
// Hide the real checkbox
|
||||||
|
input[type=checkbox] {
|
||||||
|
display:none;
|
||||||
|
|
||||||
|
// The checkbox - Unchecked
|
||||||
|
& + label {
|
||||||
|
margin-bottom: 0; // remove the Bootstrap margin
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
@include fa-icon();
|
||||||
|
content: $fa-var-square-o;
|
||||||
|
width: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
padding-left: $padding-small-vertical;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The checkbox - Checked
|
||||||
|
&:checked + label:before {
|
||||||
|
content: $fa-var-check-square-o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@
|
|||||||
// Dashboard Components
|
// Dashboard Components
|
||||||
@import "components/bar_charts";
|
@import "components/bar_charts";
|
||||||
@import "components/charts";
|
@import "components/charts";
|
||||||
|
@import "components/checkboxes";
|
||||||
@import "components/datepicker";
|
@import "components/datepicker";
|
||||||
@import "components/forms";
|
@import "components/forms";
|
||||||
@import "components/help_panel";
|
@import "components/help_panel";
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
import re
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from selenium.webdriver.common import by
|
from selenium.webdriver.common import by
|
||||||
@ -94,17 +96,31 @@ class CheckBoxMixin(object):
|
|||||||
if self.is_marked():
|
if self.is_marked():
|
||||||
self.element.click()
|
self.element.click()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Themable checkboxes use a <label> with font-awesome icon as a
|
||||||
|
control element while the <input> widget is hidden. Still the name
|
||||||
|
needs to be extracted from <label>. Attribute "for" is used for that,
|
||||||
|
since it mirrors <input> "id" which in turn is derived from <input>'s
|
||||||
|
name.
|
||||||
|
"""
|
||||||
|
for_attribute = self.element.get_attribute('for')
|
||||||
|
indirect_name = re.search(r'id_(.*)', for_attribute)
|
||||||
|
return (indirect_name and indirect_name.group(1)) or None
|
||||||
|
|
||||||
class CheckBoxFormFieldRegion(BaseFormFieldRegion, CheckBoxMixin):
|
|
||||||
|
class CheckBoxFormFieldRegion(CheckBoxMixin, BaseFormFieldRegion):
|
||||||
"""Checkbox field."""
|
"""Checkbox field."""
|
||||||
|
|
||||||
_element_locator_str_suffix = 'label > input[type=checkbox]'
|
_element_locator_str_suffix = \
|
||||||
|
'.themable-checkbox input[type=checkbox] + label'
|
||||||
|
|
||||||
|
|
||||||
class ProjectPageCheckBoxFormFieldRegion(BaseFormFieldRegion, CheckBoxMixin):
|
class ProjectPageCheckBoxFormFieldRegion(CheckBoxMixin, BaseFormFieldRegion):
|
||||||
"""Checkbox field for Project-page."""
|
"""Checkbox field for Project-page."""
|
||||||
|
|
||||||
_element_locator_str_suffix = 'div > input[type=checkbox]'
|
_element_locator_str_suffix = \
|
||||||
|
'div > .themable-checkbox input[type=checkbox] + label'
|
||||||
|
|
||||||
|
|
||||||
class ChooseFileFormFieldRegion(BaseFormFieldRegion):
|
class ChooseFileFormFieldRegion(BaseFormFieldRegion):
|
||||||
|
@ -24,7 +24,10 @@ class RowRegion(baseregion.BaseRegion):
|
|||||||
"""Classic table row."""
|
"""Classic table row."""
|
||||||
|
|
||||||
_cell_locator = (by.By.CSS_SELECTOR, 'td.%s' % NORMAL_COLUMN_CLASS)
|
_cell_locator = (by.By.CSS_SELECTOR, 'td.%s' % NORMAL_COLUMN_CLASS)
|
||||||
_row_checkbox_locator = (by.By.CSS_SELECTOR, 'td > input')
|
_row_checkbox_locator = (
|
||||||
|
by.By.CSS_SELECTOR,
|
||||||
|
'td .themable-checkbox [type="checkbox"] + label'
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, driver, conf, src_elem, column_names):
|
def __init__(self, driver, conf, src_elem, column_names):
|
||||||
self.column_names = column_names
|
self.column_names = column_names
|
||||||
|
@ -36,6 +36,10 @@ form label {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.help-icon {
|
||||||
|
color: $gray;
|
||||||
|
}
|
||||||
|
|
||||||
// Note (hurgleburgler) : the combination of display: table-cell, width: 100%
|
// Note (hurgleburgler) : the combination of display: table-cell, width: 100%
|
||||||
// and max-width has strange consequences in CSS. This is required to make sure
|
// and max-width has strange consequences in CSS. This is required to make sure
|
||||||
// that the width does not stretch beyond 100%. Please see the answer to
|
// that the width does not stretch beyond 100%. Please see the answer to
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
@import "icons";
|
@import "icons";
|
||||||
@import "components/context_selection";
|
@import "components/context_selection";
|
||||||
@import "components/messages";
|
@import "components/messages";
|
||||||
|
@import "components/checkboxes";
|
||||||
@import "components/hamburger";
|
@import "components/hamburger";
|
||||||
@import "components/magic_search";
|
@import "components/magic_search";
|
||||||
@import "components/navbar";
|
@import "components/navbar";
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// Custom Material Checkboxes
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
@import "/horizon/lib/mdi/scss/icons";
|
||||||
|
.themable-checkbox {
|
||||||
|
|
||||||
|
input[type=checkbox] {
|
||||||
|
|
||||||
|
// The checkbox - Unchecked
|
||||||
|
& + label {
|
||||||
|
@extend .mdi-checkbox-blank-outline;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
@extend .mdi;
|
||||||
|
font-size: $font-size-h5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The checkbox - Checked
|
||||||
|
&:checked + label {
|
||||||
|
@extend .mdi-checkbox-marked;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
color: $brand-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user