Skip to content

Commit

Permalink
Merge "Inline Table editing"
Browse files Browse the repository at this point in the history
  • Loading branch information
Jenkins authored and openstack-gerrit committed Dec 4, 2013
2 parents 258e9b4 + c77d907 commit efc88d4
Show file tree
Hide file tree
Showing 15 changed files with 1,415 additions and 21 deletions.
3 changes: 3 additions & 0 deletions doc/source/ref/tables.rst
Expand Up @@ -76,6 +76,9 @@ Actions
.. autoclass:: DeleteAction
:members:

.. autoclass:: UpdateAction
:members:

Class-Based Views
=================

Expand Down
135 changes: 135 additions & 0 deletions doc/source/topics/tables.rst
Expand Up @@ -246,3 +246,138 @@ So it's enough to just import and use them, e.g. ::

# code omitted
filters=(parse_isotime, timesince)


Inline editing
==============

Table cells can be easily upgraded with in-line editing. With use of
django.form.Field, we are able to run validations of the field and correctly
parse the data. The updating process is fully encapsulated into table
functionality, communication with the server goes through AJAX in JSON format.
The javacript wrapper for inline editing allows each table cell that has
in-line editing available to:
#. Refresh itself with new data from the server.
#. Display in edit mod.
#. Send changed data to server.
#. Display validation errors.

There are basically 3 things that need to be defined in the table in order
to enable in-line editing.

Fetching the row data
---------------------

Defining an ``get_data`` method in a class inherited from ``tables.Row``.
This method takes care of fetching the row data. This class has to be then
defined in the table Meta class as ``row_class = UpdateRow``.

Example::

class UpdateRow(tables.Row):
# this method is also used for automatic update of the row
ajax = True

def get_data(self, request, project_id):
# getting all data of all row cells
project_info = api.keystone.tenant_get(request, project_id,
admin=True)
return project_info

Updating changed cell data
--------------------------

Define an ``update_cell`` method in the class inherited from
``tables.UpdateAction``. This method takes care of saving the data of the
table cell. There can be one class for every cell thanks to the
``cell_name`` parameter. This class is then defined in tables column as
``update_action=UpdateCell``, so each column can have its own updating
method.

Example::

class UpdateCell(tables.UpdateAction):
def allowed(self, request, project, cell):
# Determines whether given cell or row will be inline editable
# for signed in user.
return api.keystone.keystone_can_edit_project()

def update_cell(self, request, project_id, cell_name, new_cell_value):
# in-line update project info
try:
project_obj = datum
# updating changed value by new value
setattr(project_obj, cell_name, new_cell_value)

# sending new attributes back to API
api.keystone.tenant_update(
request,
project_id,
name=project_obj.name,
description=project_obj.description,
enabled=project_obj.enabled)

except Conflict:
# Validation error for naming conflict, raised when user
# choose the existing name. We will raise a
# ValidationError, that will be sent back to the client
# browser and shown inside of the table cell.
message = _("This name is already taken.")
raise ValidationError(message)
except:
# Other exception of the API just goes through standard
# channel
exceptions.handle(request, ignore=True)
return False
return True

Defining a form_field for each Column that we want to be in-line edited.
------------------------------------------------------------------------

Form field should be ``django.form.Field`` instance, so we can use django
validations and parsing of the values sent by POST (in example validation
``required=True`` and correct parsing of the checkbox value from the POST
data).

Form field can be also ``django.form.Widget`` class, if we need to just
display the form widget in the table and we don't need Field functionality.

Then connecting ``UpdateRow`` and ``UpdateCell`` classes to the table.

Example::

class TenantsTable(tables.DataTable):
# Adding html text input for inline editing, with required validation.
# HTML form input will have a class attribute tenant-name-input, we
# can define here any HTML attribute we need.
name = tables.Column('name', verbose_name=_('Name'),
form_field=forms.CharField(required=True),
form_field_attributes={'class':'tenant-name-input'},
update_action=UpdateCell)

# Adding html textarea without required validation.
description = tables.Column(lambda obj: getattr(obj, 'description', None),
verbose_name=_('Description'),
form_field=forms.CharField(
widget=forms.Textarea(),
required=False),
update_action=UpdateCell)

# Id will not be inline edited.
id = tables.Column('id', verbose_name=_('Project ID'))

# Adding html checkbox, that will be shown inside of the table cell with
# label
enabled = tables.Column('enabled', verbose_name=_('Enabled'), status=True,
form_field=forms.BooleanField(
label=_('Enabled'),
required=False),
update_action=UpdateCell)

class Meta:
name = "tenants"
verbose_name = _("Projects")
# Connection to UpdateRow, so table can fetch row data based on
# their primary key.
row_class = UpdateRow

26 changes: 13 additions & 13 deletions horizon/static/horizon/js/horizon.tables.js
Expand Up @@ -76,9 +76,9 @@ horizon.datatables = {

// Only replace row if the html content has changed
if($new_row.html() != $row.html()) {
if($row.find(':checkbox').is(':checked')) {
if($row.find('.table-row-multi-select:checkbox').is(':checked')) {
// Preserve the checkbox if it's already clicked
$new_row.find(':checkbox').prop('checked', true);
$new_row.find('.table-row-multi-select:checkbox').prop('checked', true);
}
$row.replaceWith($new_row);
// Reset tablesorter's data cache.
Expand Down Expand Up @@ -112,7 +112,7 @@ horizon.datatables = {
validate_button: function () {
// Disable form button if checkbox are not checked
$("form").each(function (i) {
var checkboxes = $(this).find(":checkbox");
var checkboxes = $(this).find(".table-row-multi-select:checkbox");
if(!checkboxes.length) {
// Do nothing if no checkboxes in this form
return;
Expand Down Expand Up @@ -142,7 +142,7 @@ horizon.datatables.confirm = function (action) {
if ($("#"+closest_table_id+" tr[data-display]").length > 0) {
if($(action).closest("div").hasClass("table_actions")) {
// One or more checkboxes selected
$("#"+closest_table_id+" tr[data-display]").has(":checkbox:checked").each(function() {
$("#"+closest_table_id+" tr[data-display]").has(".table-row-multi-select:checkbox:checked").each(function() {
name_array.push(" \"" + $(this).attr("data-display") + "\"");
});
name_array.join(", ");
Expand Down Expand Up @@ -290,8 +290,8 @@ $(parent).find("table.datatable").each(function () {

horizon.datatables.add_table_checkboxes = function(parent) {
$(parent).find('table thead .multi_select_column').each(function(index, thead) {
if (!$(thead).find(':checkbox').length &&
$(thead).parents('table').find('tbody :checkbox').length) {
if (!$(thead).find('.table-row-multi-select:checkbox').length &&
$(thead).parents('table').find('tbody .table-row-multi-select:checkbox').length) {
$(thead).append('<input type="checkbox">');
}
});
Expand Down Expand Up @@ -377,24 +377,24 @@ horizon.addInitFunction(function() {
horizon.datatables.update_footer_count($(el), 0);
});
// Bind the "select all" checkbox action.
$('div.table_wrapper, #modal_wrapper').on('click', 'table thead .multi_select_column :checkbox', function(evt) {
$('div.table_wrapper, #modal_wrapper').on('click', 'table thead .multi_select_column .table-row-multi-select:checkbox', function(evt) {
var $this = $(this),
$table = $this.closest('table'),
is_checked = $this.prop('checked'),
checkboxes = $table.find('tbody :visible:checkbox');
checkboxes = $table.find('tbody .table-row-multi-select:visible:checkbox');
checkboxes.prop('checked', is_checked);
});
// Change "select all" checkbox behaviour while any checkbox is checked/unchecked.
$("div.table_wrapper, #modal_wrapper").on("click", 'table tbody :checkbox', function (evt) {
$("div.table_wrapper, #modal_wrapper").on("click", 'table tbody .table-row-multi-select:checkbox', function (evt) {
var $table = $(this).closest('table');
var $multi_select_checkbox = $table.find('thead .multi_select_column :checkbox');
var any_unchecked = $table.find("tbody :checkbox").not(":checked");
var $multi_select_checkbox = $table.find('thead .multi_select_column .table-row-multi-select:checkbox');
var any_unchecked = $table.find("tbody .table-row-multi-select:checkbox").not(":checked");
$multi_select_checkbox.prop('checked', any_unchecked.length === 0);
});
// Enable dangerous buttons only if one or more checkbox is checked.
$("div.table_wrapper, #modal_wrapper").on("click", ':checkbox', function (evt) {
$("div.table_wrapper, #modal_wrapper").on("click", '.table-row-multi-select:checkbox', function (evt) {
var $form = $(this).closest("form");
var any_checked = $form.find("tbody :checkbox").is(":checked");
var any_checked = $form.find("tbody .table-row-multi-select:checkbox").is(":checked");
if(any_checked) {
$form.find(".table_actions button.btn-danger").removeClass("disabled");
}else {
Expand Down

0 comments on commit efc88d4

Please sign in to comment.