261 lines
8.3 KiB
Python
261 lines
8.3 KiB
Python
# 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
|
|
|
|
from django.template.loader import get_template
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from horizon.forms import fields
|
|
|
|
"""A custom Horizon Forms Select widget that displays select choices as a table
|
|
|
|
The widgets is meant as an optional replacement for the existing Horizon
|
|
ThemableDynamicSelectWidget which it extends and is compatible with.
|
|
|
|
|
|
Columns
|
|
-------
|
|
Columns are defined by setting the widgets 'column' attribute, which is
|
|
expected to be an iterable of strings, each one corresponding to one column and
|
|
used for that columns heading.
|
|
|
|
|
|
Rows
|
|
----
|
|
Each row corresponds to one choice/select option with a defined value in
|
|
each column.
|
|
|
|
The values displayed in each column are derived using the 'build_columns'
|
|
attribute, which is expected to be a function that:
|
|
|
|
- takes a choice tuple of the form (value, label) as defined
|
|
for the Django SelectField instances as it's only parameter
|
|
- returns an iterable of Strings which are rendered as column
|
|
values for the given choice row in the same order as in the
|
|
iterable
|
|
|
|
The default implementation simply uses the provided value and label as separate
|
|
column values.
|
|
|
|
See the default implementation and example bellow for more details.
|
|
|
|
|
|
Condensed values
|
|
----------------
|
|
To maintain visual consistency, the currently selected value is displayed in
|
|
the 'standard' ThemableDynamicSelectWidget HTML setup. To accommodate this, a
|
|
condensed, single string value is created from the individual columns and
|
|
displayed in the select box.
|
|
|
|
This behavior can be modified by setting the 'condense' attribute. This is
|
|
expected to be a function that:
|
|
|
|
- Takes the column iterable returned by 'build_columns' function
|
|
- Returns a single string representation of the choice
|
|
|
|
By default, the condensed value is created by joining all of the provided
|
|
columns and joining them using commas as a delimiter.
|
|
|
|
See the default implementation and example bellow for more details.
|
|
|
|
|
|
Small screen reactivity
|
|
-----------------------
|
|
Support for small screens (< 768px) is turned on by setting the attribute
|
|
'alternate_xs' to True. When on, a condesned version of the popup table
|
|
us used for small screens, where a single column is used with the condensed
|
|
row values used instead of the full table rows.
|
|
|
|
The 'condense' function described above is used to construct this table.
|
|
|
|
|
|
Example
|
|
-------
|
|
|
|
port_id = forms.ThemableDynamicChoiceField(
|
|
label=_("Ports"),
|
|
widget=TableSelectWidget(
|
|
columns=[
|
|
'ID',
|
|
'Name'
|
|
],
|
|
build_columns=lambda choice: return (choice[1], choice[0]),
|
|
choices=[
|
|
('port 1', 'id1'),
|
|
('port 2', 'id2')
|
|
],
|
|
alternate_xs=True,
|
|
condense=lambda columns: return ",".join(columns)
|
|
)
|
|
)
|
|
|
|
Produces:
|
|
|
|
+------+--------+
|
|
| ID | Name |
|
|
+------+--------+
|
|
| id1 | port 1 |
|
|
| id2 | port 2 |
|
|
+------+--------+
|
|
|
|
on normal screens and
|
|
|
|
+-------------+
|
|
| ID, Name |
|
|
+-------------+
|
|
| id1, port 1 |
|
|
| id2, port 2 |
|
|
+-------------+
|
|
|
|
on xs screens.
|
|
|
|
"""
|
|
|
|
|
|
class TableSelectWidget(fields.ThemableDynamicSelectWidget):
|
|
def __init__(self,
|
|
attrs=None,
|
|
columns=None,
|
|
alternate_xs=False,
|
|
empty_text=_("No options available"),
|
|
other_html=None,
|
|
condense=None,
|
|
build_columns=None, *args, **kwargs
|
|
):
|
|
"""Initializer for TableSelectWidget
|
|
|
|
:param attrs: A { attribute: value } dictionary which is attached to
|
|
the hidden select element; see
|
|
ThemableDynamicSelectWidget for further information
|
|
:param columns: An iterable of column headers/names
|
|
:param alternate_xs: A truth-y value which enables/disables an
|
|
alternate rendering method for small screens
|
|
:param empty_text: The text to be displayed in case no options are
|
|
available
|
|
:param other_html: A method for adding custom HTML to the hidden option
|
|
HTML.
|
|
NOTE: This mimics the behavior of
|
|
ThemableDynamicSelectWidget and is retained to
|
|
maintain compatibility with any related, potential
|
|
functionality
|
|
:param condense: A function callback that produces a condensed label
|
|
for each option
|
|
:param build_columns: A function used to populate the individual
|
|
columns in the pop up table for each option
|
|
"""
|
|
super(TableSelectWidget, self).__init__(attrs, *args, **kwargs)
|
|
self.columns = columns or [_('Label'), _('Value'), 'Nothing']
|
|
|
|
self.alternate_xs = alternate_xs
|
|
self.empty_text = empty_text
|
|
|
|
if other_html:
|
|
self.other_html = other_html
|
|
|
|
if condense:
|
|
self.condense = condense
|
|
|
|
if build_columns:
|
|
self.build_columns = build_columns
|
|
|
|
@staticmethod
|
|
def build_columns(choice):
|
|
"""Default column building method
|
|
|
|
Overwrite this method when initializing this widget or using
|
|
self.fields[name].widget.build_columns in a parent form initialization
|
|
to customize the behavior (see above for details)
|
|
|
|
:param choice:
|
|
:return:
|
|
"""
|
|
return choice
|
|
|
|
@staticmethod
|
|
def condense(choice_columns):
|
|
"""The default condense method
|
|
|
|
Overwrite this method when initializing this widget or using
|
|
self.fields[name].widget.condense in a parent form initialization to
|
|
customize the behavior (see above for details)
|
|
|
|
:param choice_columns:
|
|
:return:
|
|
"""
|
|
return " / ".join([str(c) for c in choice_columns])
|
|
|
|
# Implements the parent 'other_html' construction for compatibility reasons
|
|
# Can be set in initializer to change the behavior as needed
|
|
def other_html(self, choice):
|
|
opt_label = choice[1]
|
|
|
|
other_html = self.transform_option_html_attrs(opt_label)
|
|
data_attr_html = self.get_data_attrs(opt_label)
|
|
|
|
if data_attr_html:
|
|
other_html += ' ' + data_attr_html
|
|
|
|
return other_html
|
|
|
|
def render(self, name, value, attrs=None, choices=None):
|
|
new_choices = []
|
|
initial_value = value
|
|
|
|
choices = choices or []
|
|
|
|
for opt in itertools.chain(self.choices, choices):
|
|
other_html = self.other_html(opt)
|
|
choice_columns = self.build_columns(opt)
|
|
condensed_label = self.condense(choice_columns)
|
|
|
|
built_choice = (
|
|
opt[0], condensed_label, choice_columns, other_html
|
|
)
|
|
|
|
new_choices.append(built_choice)
|
|
|
|
# Initial selection
|
|
if opt[0] == value:
|
|
initial_value = built_choice
|
|
|
|
if not initial_value and new_choices:
|
|
initial_value = new_choices[0]
|
|
|
|
element_id = attrs.pop('id', 'id_%s' % name)
|
|
|
|
# Size of individual columns in terms of the bootstrap grid - used
|
|
# for styling purposes
|
|
column_size = 12 // len(self.columns)
|
|
|
|
# Creates a single string label for all columns for use with small
|
|
# screens
|
|
condensed_headers = self.condense(self.columns)
|
|
|
|
template = get_template('project/firewalls_v2/table_select.html')
|
|
|
|
select_attrs = self.build_attrs(attrs)
|
|
|
|
context = {
|
|
'name': name,
|
|
'options': new_choices,
|
|
'id': element_id,
|
|
'value': value,
|
|
'initial_value': initial_value,
|
|
'select_attrs': select_attrs,
|
|
'column_size': column_size,
|
|
'columns': self.columns,
|
|
'condensed_headers': condensed_headers,
|
|
'alternate_xs': self.alternate_xs,
|
|
'empty_text': self.empty_text
|
|
}
|
|
return template.render(context)
|