diff options
Diffstat (limited to 'extensions/EditTable')
7 files changed, 436 insertions, 0 deletions
diff --git a/extensions/EditTable/Config.pm b/extensions/EditTable/Config.pm new file mode 100644 index 000000000..d601951a4 --- /dev/null +++ b/extensions/EditTable/Config.pm @@ -0,0 +1,15 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::EditTable; +use strict; + +use constant NAME => 'EditTable'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; + +__PACKAGE__->NAME; diff --git a/extensions/EditTable/Extension.pm b/extensions/EditTable/Extension.pm new file mode 100644 index 000000000..a10a30e57 --- /dev/null +++ b/extensions/EditTable/Extension.pm @@ -0,0 +1,180 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + + +# this is a quick and dirty table editor, designed to allow admins to quickly +# maintain tables. +# +# each table must be defined via the editable_tables hook +# +# this extension doesn't currently provide any ability to modify or validate +# values. use with caution! + +package Bugzilla::Extension::EditTable; + +use strict; +use warnings; + +use base qw(Bugzilla::Extension); + +use Bugzilla::Error; +use Bugzilla::Hook; +use Bugzilla::Util qw(trick_taint); +use JSON; +use Storable qw(dclone); + +our $VERSION = '1'; + +# definitions for tables which we can edit with the quick-and-dirty editor +# +# $table_name => { +# id_field => name of the "id" field +# order_by => the field to sort rows by (optional, defaults to the id_field) +# blurb => text which describes the table +# group => group required to edit this table (optional, defaults to "admin") +# } +# +# example: +# 'antispam_domain_blocklist' => { +# id_field => 'id', +# order_by => 'domain', +# blurb => 'List of fully qualified domain names to block at account creation time.', +# group => 'can_configure_antispam', +# }, + +sub EDITABLE_TABLES { + my $tables = {}; + Bugzilla::Hook::process("editable_tables", { tables => $tables }); + return $tables; +} + +sub page_before_template { + my ($self, $args) = @_; + my ($vars, $page) = @$args{qw(vars page_id)}; + return unless $page eq 'edit_table.html'; + my $input = Bugzilla->input_params; + + # we only support editing a particular set of tables + my $table_name = $input->{table}; + exists $self->EDITABLE_TABLES()->{$table_name} + || ThrowUserError('edittable_unsupported', { table => $table_name } ); + my $table = $self->EDITABLE_TABLES()->{$table_name}; + my $id_field = $table->{id_field}; + my $order_by = $table->{order_by} || $id_field; + my $group = $table->{group} || 'admin'; + trick_taint($table_name); + + Bugzilla->user->in_group($group) + || ThrowUserError('auth_failure', { group => $group, + action => 'edit', + object => 'tables' }); + + # load columns + my $dbh = Bugzilla->dbh; + my @fields = sort + grep { $_ ne $id_field && $_ ne $order_by; } + $dbh->bz_table_columns($table_name); + if ($order_by ne $id_field) { + unshift @fields, $order_by; + } + + # update table + my $data = $input->{table_data}; + my $edits = []; + if ($data) { + $data = from_json($data)->{data}; + $edits = dclone($data); + eval { + $dbh->bz_start_transaction; + + foreach my $row (@$data) { + map { trick_taint($_) } @$row; + if ($row->[0] eq '-') { + # add + shift @$row; + next unless grep { $_ ne '' } @$row; + my $placeholders = join(',', split(//, '?' x scalar(@fields))); + $dbh->do( + "INSERT INTO $table_name(" . join(',', @fields) . ") " . + "VALUES ($placeholders)", + undef, + @$row + ); + } + elsif ($row->[0] < 0) { + # delete + $dbh->do( + "DELETE FROM $table_name WHERE $id_field=?", + undef, + -$row->[0] + ); + } + else { + # update + my $id = shift @$row; + $dbh->do( + "UPDATE $table_name " . + "SET " . join(',', map { "$_ = ?" } @fields) . " " . + "WHERE $id_field = ?", + undef, + @$row, $id + ); + } + } + + $dbh->bz_commit_transaction; + $vars->{updated} = 1; + $edits = []; + }; + if ($@) { + my $error = $@; + $error =~ s/^DBD::[^:]+::db do failed: //; + $error =~ s/^(.+) \[for Statement ".+$/$1/s; + $vars->{error} = $error; + $dbh->bz_rollback_transaction; + } + } + + # load data from table + unshift @fields, $id_field; + $data = $dbh->selectall_arrayref( + "SELECT " . join(',', @fields) . " FROM $table_name ORDER BY $order_by" + ); + + # we don't support nulls currently + foreach my $row (@$data) { + if (grep { !defined($_) } @$row) { + ThrowUserError('edittable_nulls', { table => $table_name } ); + } + } + + # apply failed edits + foreach my $edit (@$edits) { + if ($edit->[0] eq '-') { + push @$data, $edit; + } + else { + my $id = $edit->[0]; + foreach my $row (@$data) { + if ($row->[0] == $id) { + @$row = @$edit; + last; + } + } + } + } + + $vars->{table_name} = $table_name; + $vars->{blurb} = $table->{blurb}; + $vars->{table_data} = to_json({ + fields => \@fields, + id_field => $id_field, + data => $data, + }); +} + +__PACKAGE__->NAME; diff --git a/extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl b/extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl new file mode 100644 index 000000000..f86fb4c86 --- /dev/null +++ b/extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl @@ -0,0 +1,11 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% IF object == 'tables' %] + tables +[% END %] diff --git a/extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..d87270b98 --- /dev/null +++ b/extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,17 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% IF error == "edittable_unsupported" %] + [% title = "Unsupported Table" %] + You cannot edit the table '[% table FILTER html %]'. + +[% ELSIF error == "edittable_nulls" %] + [% title = "Table Contains NULLs" %] + EditTable cannot edit the table '[% table FILTER html %]' as it contains NULL + values. +[% END %] diff --git a/extensions/EditTable/template/en/default/pages/edit_table.html.tmpl b/extensions/EditTable/template/en/default/pages/edit_table.html.tmpl new file mode 100644 index 000000000..d81291640 --- /dev/null +++ b/extensions/EditTable/template/en/default/pages/edit_table.html.tmpl @@ -0,0 +1,43 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Edit Table" + javascript_urls = [ 'extensions/EditTable/web/js/edit_table.js' ] + style_urls = [ "extensions/EditTable/web/styles/edit_table.css" ] +%] + +<h2>[% table_name FILTER html %]</h2> + +[% IF updated %] + <p id="message">Table [% table_name FILTER html %] updated.</p> +[% ELSIF error %] + <p class="throw_error">[% error FILTER html FILTER html_line_break %]</p> +[% END %] + +<p>[% blurb FILTER html FILTER html_line_break %]</p> + +<div id="edit_table"></div> +<br> +<form method="post" action="page.cgi" enctype="multipart/form-data" + onsubmit="editTable.to_json('table_data')"> +<input type="hidden" name="id" value="edit_table.html"> +<input type="hidden" name="table" value="[% table_name FILTER html %]"> +<input type="hidden" name="table_data" id="table_data"> +<input type="submit" value="Commit Changes" id="commit_btn" class="bz_default_hidden"> +</form> + +<script> + var table_data = [% table_data FILTER none %]; + var editTable = new EditTable('edit_table', table_data); + editTable.render(); +</script> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/EditTable/web/js/edit_table.js b/extensions/EditTable/web/js/edit_table.js new file mode 100644 index 000000000..ae239759b --- /dev/null +++ b/extensions/EditTable/web/js/edit_table.js @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. */ + + +function EditTable(parent_el, table_data) { + this.parent_el = YAHOO.util.Dom.get(parent_el); + this.table_data = table_data; + this.field_count = table_data.fields.length; + if (!JSON) JSON = YAHOO.lang.JSON; + + this.render = function() { + // create table + this.parent_el.innerHTML = ''; + var table = document.createElement('table'); + + // header + var tr = document.createElement('tr'); + for (var i = 0; i < this.field_count; i++) { + var th = document.createElement('th'); + th.appendChild(document.createTextNode(this.table_data.fields[i])); + tr.appendChild(th); + } + var td = document.createElement('td'); + td.innerHTML = ' '; + tr.appendChild(td); + table.appendChild(tr); + + // rows + for (var i = 0; i < table_data.data.length; i++) { + // skip deleted rows + if (this.table_data.data[i][0] < 0) + continue; + var tr = document.createElement('tr'); + for (var j = 0; j < this.field_count; j++) { + var td = document.createElement('td'); + td.appendChild(document.createTextNode(this.table_data.data[i][j])); + tr.appendChild(td); + + if (this.table_data.fields[j] != this.table_data.id_field) { + td.className = 'editable'; + td.contentEditable = true; + YAHOO.util.Event.addListener(td, 'keydown', this._edit_keydown, this); + YAHOO.util.Event.addListener(td, 'blur', this._save, this); + d = td; + } + } + var td = document.createElement('td'); + var a = document.createElement('a'); + a.href = '#'; + a.innerHTML = 'x'; + YAHOO.util.Event.addListener(a, 'click', this._remove_row, this); + td.appendChild(a); + td.className = 'action'; + tr.appendChild(td); + table.appendChild(tr); + } + + this.parent_el.appendChild(table); + + var add_btn = document.createElement('button'); + add_btn.innerHTML = 'Add'; + YAHOO.util.Event.addListener(add_btn, 'click', this._add_row, this); + this.parent_el.appendChild(add_btn); + }, + + this.to_json = function(target) { + YAHOO.util.Dom.get(target).value = JSON.stringify(this.table_data); + }, + + this._add_row = function(event, obj) { + var row = []; + for (var i = 0; i < obj.field_count; i++) { + row.push(obj.table_data.fields[i] == obj.table_data.id_field ? '-' : ''); + } + obj.table_data.data.push(row); + obj.render(); + YAHOO.util.Dom.removeClass('commit_btn', 'bz_default_hidden'); + event.preventDefault(); + }, + + this._remove_row = function(event, obj) { + var row = event.target.parentElement.parentElement.rowIndex - 1; + if (obj.table_data.data[row][0] == '-') { + // removing a newly added row + obj.table_data.data.splice(row, 1); + } + else { + // to remove a db row we set its id to negative + // it'll be skipped by render, and the update script knows which id to delete + obj.table_data.data[row][0] = -obj.table_data.data[row][0]; + } + obj.render(); + YAHOO.util.Dom.removeClass('commit_btn', 'bz_default_hidden'); + event.preventDefault(); + }, + + this._save = function(event, obj) { + var row = event.target.parentElement.rowIndex - 1; + var col = event.target.cellIndex; + var value = event.target.textContent; + if (obj.table_data.data[row][col] != event.target.textContent) { + obj.table_data.data[row][col] = event.target.textContent; + YAHOO.util.Dom.removeClass('commit_btn', 'bz_default_hidden'); + } + }, + + this._revert = function(event, obj) { + var row = event.target.parentElement.rowIndex - 1; + var col = event.target.cellIndex; + event.target.replaceChild( + document.createTextNode(obj.table_data.data[row][col]), + event.target.firstChild + ); + }, + + this._edit_keydown = function(event, obj) { + if (event.keyCode == 13) { + event.preventDefault(); + obj._save(event, obj); + document.activeElement.blur(event.target); + } + else if (event.keyCode == 27) { + event.preventDefault(); + obj._revert(event, obj); + } + } +}; diff --git a/extensions/EditTable/web/styles/edit_table.css b/extensions/EditTable/web/styles/edit_table.css new file mode 100644 index 000000000..0b1c72db6 --- /dev/null +++ b/extensions/EditTable/web/styles/edit_table.css @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. */ + + +#edit_table table { + border-spacing: 0; + border-collapse: collapse; + margin-bottom: 1em; +} + +#edit_table td, #edit_table th { + padding: 5px; +} + +#edit_table th { + background: #ccc; + text-align: left; +} + +#edit_table .editable { + background: #fff; +} + +#edit_table tr:hover { + background: #eee; +} + +#edit_table .action { + display: none; +} + +#edit_table tr:hover .action { + display: block; +} + |