Django Formset support in DataTable
Add FormsetDataTable and FormsetDataTableMixin classes that allow you to use a Django Formset with your DataTable, making it possible to edit the data displayed. Implements: blueprint formset-data-table Change-Id: I636fa3ae9847f1700eae0fb5293fa97683ba2218
This commit is contained in:
parent
fdf920e714
commit
ba1b9007d4
@ -49,6 +49,17 @@ The following options can be defined in a ``Meta`` class inside a
|
||||
.. autoclass:: horizon.tables.base.DataTableOptions
|
||||
:members:
|
||||
|
||||
FormsetDataTable
|
||||
================
|
||||
|
||||
You can integrate the :class:`.DataTable` with a Django Formset using one of following classes:
|
||||
|
||||
.. autoclass:: horizon.tables.formset.FormsetDataTableMixin
|
||||
:members:
|
||||
|
||||
.. autoclass:: horizon.tables.formset.FormsetDataTable
|
||||
:members:
|
||||
|
||||
Table Components
|
||||
================
|
||||
|
||||
|
87
horizon/static/horizon/js/horizon.formset_table.js
Normal file
87
horizon/static/horizon/js/horizon.formset_table.js
Normal file
@ -0,0 +1,87 @@
|
||||
horizon.formset_table = (function () {
|
||||
'use strict';
|
||||
|
||||
var module = {};
|
||||
|
||||
|
||||
// go through the whole table and fix the numbering of rows
|
||||
module.reenumerate_rows = function (table, prefix) {
|
||||
var count = 0;
|
||||
var input_name_re = new RegExp('^' + prefix + '-(\\d+|__prefix__)-');
|
||||
var input_id_re = new RegExp('^id_' + prefix + '-(\\d+|__prefix__)-');
|
||||
|
||||
table.find('tbody tr').each(function () {
|
||||
$(this).find('input').each(function () {
|
||||
var input = $(this);
|
||||
input.attr('name', input.attr('name').replace(
|
||||
input_name_re, prefix + '-' + count + '-'));
|
||||
input.attr('id', input.attr('id').replace(
|
||||
input_id_re, 'id_' + prefix + '-' + count + '-'));
|
||||
});
|
||||
count += 1;
|
||||
});
|
||||
$('#id_' + prefix + '-TOTAL_FORMS').val(count);
|
||||
};
|
||||
|
||||
// mark a row as deleted and hide it
|
||||
module.delete_row = function (e) {
|
||||
$(this).closest('tr').hide();
|
||||
$(this).prev('input[name$="-DELETE"]').attr('checked', true);
|
||||
};
|
||||
|
||||
// replace the "Delete" checkboxes with × for deleting rows
|
||||
module.replace_delete = function (where) {
|
||||
where.find('input[name$="-DELETE"]').hide().after(
|
||||
$('<a href="#" class="close">×</a>').click(module.delete_row)
|
||||
);
|
||||
};
|
||||
|
||||
// add more empty rows in the flavors table
|
||||
module.add_row = function (table, prefix, empty_row_html) {
|
||||
var new_row = $(empty_row_html);
|
||||
module.replace_delete(new_row);
|
||||
table.find('tbody').append(new_row);
|
||||
module.reenumerate_rows(table, prefix);
|
||||
};
|
||||
|
||||
// prepare all the javascript for formset table
|
||||
module.init = function (prefix, empty_row_html, add_label) {
|
||||
|
||||
var table = $('table#' + prefix);
|
||||
|
||||
module.replace_delete(table);
|
||||
|
||||
// if there are extra empty rows, add the button for new rows
|
||||
if (add_label) {
|
||||
var button = $('<a href="#" class="btn btn-small pull-right">' +
|
||||
add_label + '</a>');
|
||||
table.find('tfoot td').append(button);
|
||||
button.click(function () {
|
||||
module.add_row(table, prefix, empty_row_html);
|
||||
});
|
||||
}
|
||||
|
||||
// if the formset is not empty and has no errors,
|
||||
// delete the empty extra rows from the end
|
||||
var initial_forms = +$('#id_' + prefix + '-INITIAL_FORMS').val();
|
||||
var total_forms = +$('#id_' + prefix + '-TOTAL_FORMS').val();
|
||||
|
||||
if (table.find('tbody tr').length > 1 &&
|
||||
table.find('tbody td.error').length === 0 &&
|
||||
total_forms > initial_forms) {
|
||||
table.find('tbody tr').each(function (index) {
|
||||
if (index >= initial_forms) {
|
||||
$(this).remove();
|
||||
}
|
||||
});
|
||||
module.reenumerate_rows(table, prefix);
|
||||
$('#id_' + prefix + '-INITIAL_FORMS').val(
|
||||
$('#id_' + prefix + '-TOTAL_FORMS').val());
|
||||
}
|
||||
|
||||
// enable tooltips
|
||||
table.find('td.error[title]').tooltip();
|
||||
};
|
||||
|
||||
return module;
|
||||
} ());
|
@ -25,6 +25,7 @@ horizon.addInitFunction(function () {
|
||||
jsHintTest('horizon.d3piechart.js', '/static/horizon/js/horizon.d3piechart.js', config);
|
||||
jsHintTest('horizon.firewalls.js', '/static/horizon/js/horizon.firewalls.js', config);
|
||||
jsHintTest('horizon.forms.js', '/static/horizon/js/horizon.forms.js', config);
|
||||
jsHintTest('horizon.formset_table.js', '/static/horizon/js/horizon.formset_table.js', config);
|
||||
jsHintTest('horizon.heattop.js', '/static/horizon/js/horizon.heattop.js', config);
|
||||
jsHintTest('horizon.instances.js', '/static/horizon/js/horizon.instances.js', config);
|
||||
jsHintTest('horizon.js', '/static/horizon/js/horizon.js', config);
|
||||
|
@ -47,3 +47,60 @@ test("Footer count update", function () {
|
||||
horizon.datatables.update_footer_count(table);
|
||||
notEqual(table_count.text().indexOf('5 items'), -1, "Count correct after adding two rows");
|
||||
});
|
||||
|
||||
test("Formset reenumerate rows", function () {
|
||||
var html = $('#formset');
|
||||
var table = html.find('table');
|
||||
var input = table.find('tbody tr#flavors__row__14 input').first();
|
||||
|
||||
input.attr('id', 'id_flavors-3-name');
|
||||
horizon.formset_table.reenumerate_rows(table, 'flavors');
|
||||
equal(input.attr('id'), 'id_flavors-0-name', "Enumerate old rows ids");
|
||||
input.attr('id', 'id_flavors-__prefix__-name');
|
||||
horizon.formset_table.reenumerate_rows(table, 'flavors');
|
||||
equal(input.attr('id'), 'id_flavors-0-name', "Enumerate new rows ids");
|
||||
});
|
||||
|
||||
test("Formset delete row", function () {
|
||||
var html = $('#formset');
|
||||
var table = html.find('table');
|
||||
var row = table.find('tbody tr').first();
|
||||
var input = row.find('input#id_flavors-0-DELETE');
|
||||
|
||||
equal(row.css("display"), 'table-row');
|
||||
equal(input.attr('checked'), undefined);
|
||||
horizon.formset_table.replace_delete(row);
|
||||
var x = input.next('a');
|
||||
horizon.formset_table.delete_row.call(x);
|
||||
equal(row.css("display"), 'none');
|
||||
equal(input.attr('checked'), 'checked');
|
||||
});
|
||||
|
||||
test("Formset add row", function() {
|
||||
var html = $('#formset');
|
||||
var table = html.find('table');
|
||||
var empty_row_html = '<tr><td><input id="id_flavors-__prefix__-name" name="flavors-__prefix__-name"></td></tr>';
|
||||
|
||||
equal(table.find('tbody tr').length, 3);
|
||||
equal(html.find('#id_flavors-TOTAL_FORMS').val(), 3);
|
||||
horizon.formset_table.add_row(table, 'flavors', empty_row_html);
|
||||
equal(table.find('tbody tr').length, 4);
|
||||
equal(table.find('tbody tr:last input').attr('id'), 'id_flavors-3-name');
|
||||
equal(html.find('#id_flavors-TOTAL_FORMS').val(), 4);
|
||||
});
|
||||
|
||||
test("Init formset table", function() {
|
||||
var html = $('#formset');
|
||||
var table = html.find('table');
|
||||
|
||||
horizon.formset_table.init('flavors', '', 'Add row');
|
||||
equal(table.find('tfoot tr a').html(), 'Add row');
|
||||
});
|
||||
|
||||
test("Init formset table -- no add", function() {
|
||||
var html = $('#formset');
|
||||
var table = html.find('table');
|
||||
|
||||
horizon.formset_table.init('flavors', '', '');
|
||||
equal(table.find('tfoot tr a').length, 0);
|
||||
});
|
||||
|
178
horizon/tables/formset.py
Normal file
178
horizon/tables/formset.py
Normal file
@ -0,0 +1,178 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from django import template
|
||||
from django.template import loader
|
||||
from django.utils import datastructures
|
||||
|
||||
from horizon.tables import base as horizon_tables
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FormsetCell(horizon_tables.Cell):
|
||||
"""A DataTable cell that knows about its field from the formset."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FormsetCell, self).__init__(*args, **kwargs)
|
||||
try:
|
||||
self.field = (self.row.form or {})[self.column.name]
|
||||
except KeyError:
|
||||
self.field = None
|
||||
else:
|
||||
if self.field.errors:
|
||||
self.attrs['class'] = (self.attrs.get('class', '') +
|
||||
' error control-group')
|
||||
self.attrs['title'] = ' '.join(
|
||||
unicode(error) for error in self.field.errors)
|
||||
|
||||
|
||||
class FormsetRow(horizon_tables.Row):
|
||||
"""A DataTable row that knows about its form from the formset."""
|
||||
|
||||
template_path = 'horizon/common/_formset_table_row.html'
|
||||
|
||||
def __init__(self, column, datum, form):
|
||||
self.form = form
|
||||
super(FormsetRow, self).__init__(column, datum)
|
||||
if self.cells == []:
|
||||
# We need to be able to handle empty rows, because there may
|
||||
# be extra empty forms in a formset. The original DataTable breaks
|
||||
# on this, because it sets self.cells to [], but later expects a
|
||||
# SortedDict. We just fill self.cells with empty Cells.
|
||||
cells = []
|
||||
for column in self.table.columns.values():
|
||||
cell = self.table._meta.cell_class(None, column, self)
|
||||
cells.append((column.name or column.auto, cell))
|
||||
self.cells = datastructures.SortedDict(cells)
|
||||
|
||||
def render(self):
|
||||
return loader.render_to_string(self.template_path,
|
||||
{"row": self, "form": self.form})
|
||||
|
||||
|
||||
class FormsetDataTableMixin(object):
|
||||
"""A mixin for DataTable to support Django Formsets.
|
||||
|
||||
This works the same as the ``FormsetDataTable`` below, but can be used
|
||||
to add to existing DataTable subclasses.
|
||||
"""
|
||||
formset_class = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FormsetDataTableMixin, self).__init__(*args, **kwargs)
|
||||
self._formset = None
|
||||
|
||||
# Override Meta settings, because we need custom Form and Cell classes,
|
||||
# and also our own template.
|
||||
self._meta.row_class = FormsetRow
|
||||
self._meta.cell_class = FormsetCell
|
||||
self._meta.template = 'horizon/common/_formset_table.html'
|
||||
|
||||
def get_required_columns(self):
|
||||
"""Lists names of columns that have required fields."""
|
||||
required_columns = []
|
||||
if self.formset_class:
|
||||
empty_form = self.get_formset().empty_form
|
||||
for column in self.columns.values():
|
||||
field = empty_form.fields.get(column.name)
|
||||
if field and field.required:
|
||||
required_columns.append(column.name)
|
||||
return required_columns
|
||||
|
||||
def _get_formset_data(self):
|
||||
"""Formats the self.filtered_data in a way suitable for a formset."""
|
||||
data = []
|
||||
for datum in self.filtered_data:
|
||||
form_data = {}
|
||||
for column in self.columns.values():
|
||||
value = column.get_data(datum)
|
||||
form_data[column.name] = value
|
||||
form_data['id'] = self.get_object_id(datum)
|
||||
data.append(form_data)
|
||||
return data
|
||||
|
||||
def get_formset(self):
|
||||
"""Provide the formset corresponding to this DataTable.
|
||||
|
||||
Use this to validate the formset and to get the submitted data back.
|
||||
"""
|
||||
if self._formset is None:
|
||||
self._formset = self.formset_class(
|
||||
self.request.POST or None,
|
||||
initial=self._get_formset_data(),
|
||||
prefix=self._meta.name)
|
||||
return self._formset
|
||||
|
||||
def get_empty_row(self):
|
||||
"""Return a row with no data, for adding at the end of the table."""
|
||||
return self._meta.row_class(self, None, self.get_formset().empty_form)
|
||||
|
||||
def get_rows(self):
|
||||
"""Return the row data for this table broken out by columns.
|
||||
|
||||
The row objects get an additional ``form`` parameter, with the
|
||||
formset form corresponding to that row.
|
||||
"""
|
||||
try:
|
||||
rows = []
|
||||
if self.formset_class is None:
|
||||
formset = []
|
||||
else:
|
||||
formset = self.get_formset()
|
||||
formset.is_valid()
|
||||
for datum, form in itertools.izip_longest(self.filtered_data,
|
||||
formset):
|
||||
row = self._meta.row_class(self, datum, form)
|
||||
if self.get_object_id(datum) == self.current_item_id:
|
||||
self.selected = True
|
||||
row.classes.append('current_selected')
|
||||
rows.append(row)
|
||||
except Exception:
|
||||
# Exceptions can be swallowed at the template level here,
|
||||
# re-raising as a TemplateSyntaxError makes them visible.
|
||||
LOG.exception("Error while rendering table rows.")
|
||||
exc_info = sys.exc_info()
|
||||
raise template.TemplateSyntaxError, exc_info[1], exc_info[2]
|
||||
return rows
|
||||
|
||||
def get_object_id(self, datum):
|
||||
# We need to support ``None`` when there are more forms than data.
|
||||
if datum is None:
|
||||
return None
|
||||
return super(FormsetDataTableMixin, self).get_object_id(datum)
|
||||
|
||||
|
||||
class FormsetDataTable(FormsetDataTableMixin, horizon_tables.DataTable):
|
||||
"""A DataTable with support for Django Formsets.
|
||||
|
||||
Note that :attr:`horizon.tables.DataTableOptions.row_class` and
|
||||
:attr:`horizon.tables.DataTaleOptions.cell_class` are overwritten in this
|
||||
class, so setting them in ``Meta`` has no effect.
|
||||
|
||||
.. attribute:: formset_class
|
||||
|
||||
A classs made with ``django.forms.formsets.formset_factory``
|
||||
containing the definition of the formset to use with this data table.
|
||||
|
||||
The columns that are named the same as the formset fields will be
|
||||
replaced with form widgets in the table. Any hidden fields from the
|
||||
formset will also be included. The fields that are not hidden and
|
||||
don't correspond to any column will not be included in the form.
|
||||
"""
|
@ -30,6 +30,7 @@
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.communication.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.cookies.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.forms.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.formset_table.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.instances.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.messages.js' type='text/javascript' charset='utf-8'></script>
|
||||
<script src='{{ STATIC_URL }}horizon/js/horizon.modals.js' type='text/javascript' charset='utf-8'></script>
|
||||
|
44
horizon/templates/horizon/common/_formset_table.html
Normal file
44
horizon/templates/horizon/common/_formset_table.html
Normal file
@ -0,0 +1,44 @@
|
||||
{% extends 'horizon/common/_data_table.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block table_columns %}
|
||||
{% if not table.is_browser_table %}
|
||||
<tr>
|
||||
{% for column in columns %}
|
||||
<th {{ column.attr_string|safe }}><span
|
||||
{% if column.name in table.get_required_columns %}
|
||||
class="required"
|
||||
{% endif %}
|
||||
>{{ column }}</span></th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endblock table_columns %}
|
||||
|
||||
{% block table %}
|
||||
{% with table.get_formset as formset %}
|
||||
{{ formset.management_form }}
|
||||
{% if formset.non_field_errors %}
|
||||
<div class="alert alert-error">
|
||||
{{ formset.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{{ block.super }}
|
||||
|
||||
<script type="text/javascript">
|
||||
($ || addHorizonLoadEvent)(function () {
|
||||
// prepare the js-enabled parts of the formset data table
|
||||
var prefix = '{{ table.name|escapejs }}';
|
||||
var empty_row_html = '{% filter escapejs %}{% include "horizon/common/_formset_table_row.html" with row=table.get_empty_row %}{% endfilter %}';
|
||||
{% if table.formset_class.extra %}
|
||||
var add_label = '{% filter escapejs %}{% trans "Add a row" %}{% endfilter %}';
|
||||
{% else %}
|
||||
var add_label = '';
|
||||
{% endif %}
|
||||
|
||||
|
||||
horizon.formset_table.init(prefix, empty_row_html, add_label);
|
||||
});
|
||||
</script>
|
||||
{% endblock table %}
|
24
horizon/templates/horizon/common/_formset_table_row.html
Normal file
24
horizon/templates/horizon/common/_formset_table_row.html
Normal file
@ -0,0 +1,24 @@
|
||||
<tr{{ row.attr_string|safe }}>
|
||||
{% for cell in row %}
|
||||
<td{{ cell.attr_string|safe }}>
|
||||
{% if cell.field %}
|
||||
{{ cell.field }}
|
||||
{% else %}
|
||||
{%if cell.wrap_list %}<ul>{% endif %}{{ cell.value }}{%if cell.wrap_list %}</ul>{% endif %}
|
||||
{% endif %}
|
||||
{% if forloop.first %}
|
||||
{% for field in row.form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% for error in field.errors %}
|
||||
<span class="help-inline">{{ field.name }}: {{ error }}</span>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% if row.form.non_field_errors %}
|
||||
<div class="alert alert-error">
|
||||
{{ row.form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
@ -71,6 +71,165 @@
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<div id="formset" class="table_wrapper">
|
||||
<input id="id_flavors-TOTAL_FORMS" name="flavors-TOTAL_FORMS"
|
||||
type="hidden" value="3"><input id="id_flavors-INITIAL_FORMS"
|
||||
name="flavors-INITIAL_FORMS" type="hidden" value="2"><input id=
|
||||
"id_flavors-MAX_NUM_FORMS" name="flavors-MAX_NUM_FORMS" type=
|
||||
"hidden" value="1000">
|
||||
|
||||
<table id="flavors" class=
|
||||
"table table-bordered table-striped datatable">
|
||||
<thead>
|
||||
<tr class='table_caption'>
|
||||
<th class='table_header' colspan='8'>
|
||||
<h3 class='table_title'>Flavors</h3>
|
||||
|
||||
<div class="table_actions clearfix"></div>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th class="sortable normal_column"><span class=
|
||||
"required">Flavor Name</span></th>
|
||||
|
||||
<th class="sortable normal_column"><span class=
|
||||
"required">VCPU</span></th>
|
||||
|
||||
<th class="sortable normal_column"><span class=
|
||||
"required">RAM (MB)</span></th>
|
||||
|
||||
<th class="sortable normal_column"><span class=
|
||||
"required">Root Disk (GB)</span></th>
|
||||
|
||||
<th class="sortable normal_column"><span>Ephemeral Disk
|
||||
(GB)</span></th>
|
||||
|
||||
<th class="sortable normal_column"><span>Swap Disk
|
||||
(MB)</span></th>
|
||||
|
||||
<th class="sortable normal_column"><span>Max.
|
||||
VMs</span></th>
|
||||
|
||||
<th class="sortable normal_column">
|
||||
<span>Delete</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr class="" data-display="yyy.1" id="flavors__row__14">
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input input-small" id="id_flavors-0-name" maxlength="25"
|
||||
name="flavors-0-name" type="text" value="1"> <input id=
|
||||
"id_flavors-0-id" name="flavors-0-id" type="hidden"
|
||||
value="14"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-0-cpu" name=
|
||||
"flavors-0-cpu" type="number" value="1"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-0-memory" name=
|
||||
"flavors-0-memory" type="number" value="1"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-0-storage" name=
|
||||
"flavors-0-storage" type="number" value="1"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id=
|
||||
"id_flavors-0-ephemeral_disk" name=
|
||||
"flavors-0-ephemeral_disk" type="number"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-0-swap_disk"
|
||||
name="flavors-0-swap_disk" type="number"></td>
|
||||
|
||||
<td class="sortable normal_column">-</td>
|
||||
|
||||
<td class="sortable normal_column"><input id=
|
||||
"id_flavors-0-DELETE" name="flavors-0-DELETE" type=
|
||||
"checkbox"></td>
|
||||
</tr>
|
||||
|
||||
<tr class="" data-display="yyy.2" id="flavors__row__15">
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input input-small" id="id_flavors-1-name" maxlength="25"
|
||||
name="flavors-1-name" type="text" value="2"> <input id=
|
||||
"id_flavors-1-id" name="flavors-1-id" type="hidden"
|
||||
value="15"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-1-cpu" name=
|
||||
"flavors-1-cpu" type="number" value="2"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-1-memory" name=
|
||||
"flavors-1-memory" type="number" value="2"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-1-storage" name=
|
||||
"flavors-1-storage" type="number" value="2"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id=
|
||||
"id_flavors-1-ephemeral_disk" name=
|
||||
"flavors-1-ephemeral_disk" type="number"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-1-swap_disk"
|
||||
name="flavors-1-swap_disk" type="number"></td>
|
||||
|
||||
<td class="sortable normal_column">-</td>
|
||||
|
||||
<td class="sortable normal_column"><input id=
|
||||
"id_flavors-1-DELETE" name="flavors-1-DELETE" type=
|
||||
"checkbox"></td>
|
||||
</tr>
|
||||
|
||||
<tr class="current_selected">
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input input-small" id="id_flavors-2-name" maxlength="25"
|
||||
name="flavors-2-name" type="text"> <input id=
|
||||
"id_flavors-2-id" name="flavors-2-id" type="hidden"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-2-cpu" name=
|
||||
"flavors-2-cpu" type="number"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-2-memory" name=
|
||||
"flavors-2-memory" type="number"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-2-storage" name=
|
||||
"flavors-2-storage" type="number"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id=
|
||||
"id_flavors-2-ephemeral_disk" name=
|
||||
"flavors-2-ephemeral_disk" type="number"></td>
|
||||
|
||||
<td class="sortable normal_column"><input class=
|
||||
"input number_input_slim" id="id_flavors-2-swap_disk"
|
||||
name="flavors-2-swap_disk" type="number"></td>
|
||||
|
||||
<td class="sortable normal_column">-</td>
|
||||
|
||||
<td class="sortable normal_column"><input id=
|
||||
"id_flavors-2-DELETE" name="flavors-2-DELETE" type=
|
||||
"checkbox"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="8"><span class="table_count">Displaying 3
|
||||
items</span></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -22,6 +22,7 @@ from django import shortcuts
|
||||
from mox import IsA # noqa
|
||||
|
||||
from horizon import tables
|
||||
from horizon.tables import formset as table_formset
|
||||
from horizon.tables import views as table_views
|
||||
from horizon.test import helpers as test
|
||||
|
||||
@ -1087,3 +1088,33 @@ class DataTableViewTests(test.TestCase):
|
||||
self.assertEqual(context['my_table_table'].__class__, MyTable)
|
||||
self.assertEqual(context['table_with_permissions_table'].__class__,
|
||||
TableWithPermissions)
|
||||
|
||||
|
||||
class FormsetTableTests(test.TestCase):
|
||||
|
||||
def test_populate(self):
|
||||
"""Create a FormsetDataTable and populate it with data."""
|
||||
|
||||
class TableForm(forms.Form):
|
||||
name = forms.CharField()
|
||||
value = forms.IntegerField()
|
||||
|
||||
TableFormset = forms.formsets.formset_factory(TableForm, extra=0)
|
||||
|
||||
class Table(table_formset.FormsetDataTable):
|
||||
formset_class = TableFormset
|
||||
|
||||
name = tables.Column('name')
|
||||
value = tables.Column('value')
|
||||
|
||||
class Meta:
|
||||
name = 'table'
|
||||
|
||||
table = Table(self.request)
|
||||
table.data = TEST_DATA_4
|
||||
formset = table.get_formset()
|
||||
self.assertEqual(len(formset), 2)
|
||||
form = formset[0]
|
||||
form_data = form.initial
|
||||
self.assertEqual(form_data['name'], 'object_1')
|
||||
self.assertEqual(form_data['value'], 2)
|
||||
|
@ -890,6 +890,29 @@ div.table_cell_wrapper {
|
||||
}
|
||||
#external_links li { margin: 0 0 0 15px; }
|
||||
|
||||
|
||||
/* Formset data tables. */
|
||||
|
||||
.datatable {
|
||||
th.narrow {
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 2px 5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
th span.required:after {
|
||||
content: "*";
|
||||
font-weight: bold;
|
||||
line-height: 0;
|
||||
padding-left: 4px;
|
||||
color: #3290c0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Forms */
|
||||
|
||||
form label {
|
||||
|
Loading…
Reference in New Issue
Block a user