Bug 29016: Convert log viewer to ajax datatable

The script viewlog.pl it's very slow because it retrieves all action logs at the same time.
It should use KohaTables with pagination/number max of returning entries like other Koha tables (API + AJAX wrapper).

Test Plan:
1) Apply this patch
2) Rebuild swagger file
3) Restart all the things!
4) Browse to /cgi-bin/koha/tools/viewlog.pl
5) Test functionality, functionality should be no different!
   * Limit by librarian
   * Limit by modules
   * Limit by actions
   * Object search string
   * Info search string
   * Limit by interface
   * Limit by from date
   * Limit by to date
   * Limit by from and to date
   * Test "Compare" checkboxes ( only two should be selectable at once )
   * Test column sorting
   * Test sharable link
   * Test output to a file
   * Test columns show/hide

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Kyle M Hall 2026-03-26 11:58:26 -04:00 committed by Lucas Gass
parent 4edc85cf29
commit c861e2dafe
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
3 changed files with 582 additions and 423 deletions

View File

@ -5,6 +5,7 @@
[% USE Branches %]
[% USE KohaDates %]
[% USE TablesSettings %]
[% USE To %]
[% PROCESS 'i18n.inc' %]
[% SET footerjs = 1 %]
[% INCLUDE 'doc-head-open.inc' %]
@ -272,207 +273,28 @@
</fieldset>
</form>
[% IF ( do_it ) %]
[% IF ( total ) %]
<h3>Log entries</h3>
<div id="toolbar" class="btn-toolbar sticky">
<a href="#" id="select_none" class="btn btn-link disabled"><i class="fa fa-times"></i> Check none</a>
<button class="btn btn-link disabled compare_link"><i class="fa fa-columns"></i> Compare selected</button>
</div>
<div class="page-section">
<table id="logst">
<thead>
<tr>
<th>Date</th>
<th>Librarian</th>
<th>Module</th>
<th>Action</th>
<th>Object</th>
<th>Info</th>
<th>Interface</th>
<th>Diff</th>
</tr>
</thead>
<tbody>
[% FOREACH loopro IN looprow %]
<tr>
<td data-order="[% loopro.timestamp | html %]">[% loopro.timestamp | $KohaDates with_hours=1 %]</td>
<td>
[% IF loopro.librarian %]
<a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% loopro.librarian.borrowernumber | uri %]" title="display detail for this librarian.">
[% IF ( loopro.librarian.firstname ) || ( loopro.librarian.surname ) %][% loopro.librarian.firstname | html %] [% loopro.librarian.surname | html %] ([% loopro.librarian.borrowernumber | html %])[% ELSE %][% loopro.librarian.borrowernumber | html %][% END %]</a
>
[% ELSE %]
[% loopro.user | html %]
[% END %]
</td>
<td>[% PROCESS translate_log_module module=loopro.module %]</td>
<td>[% PROCESS translate_log_action action=loopro.action %]</td>
<td>
[% IF ( loopro.module == 'MEMBERS' ) || ( loopro.module == 'CIRCULATION' ) || ( loopro.module == 'FINES' ) || ( loopro.module == 'APIKEYS' ) %]
[% IF loopro.patron %]
[% IF ( loopro.module == 'APIKEYS' ) %]
<a href="/cgi-bin/koha/members/apikeys.pl?patron_id=[% loopro.object | uri %]" title="View API keys for this patron">
API keys for [% loopro.patron.firstname | html %] [% loopro.patron.surname | html %] ([% loopro.object | html %])
</a>
[% ELSE %]
<a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% loopro.object | uri %]" title="Display member details.">
[% IF ( loopro.object ) %][% IF ( loopro.patron.firstname ) || ( loopro.patron.surname ) %]
[% loopro.patron.firstname | html %]
[% loopro.patron.surname | html %]
([% loopro.object | html %])
[% ELSE %]
<span>Member</span> [% loopro.object | html %]
[% END %][% END %]
</a>
[% END %]
[% ELSE %]
[% IF ( loopro.module == 'APIKEYS' ) %]
API keys for patron [% loopro.object | html %]
[% ELSE %]
[% loopro.object | html %]
[% END %]
[% END %]
[% ELSIF ( loopro.module == 'CATALOGUING' ) %]
[% IF ( loopro.info.substr(0, 4) == 'item' ) %]
<a
href="/cgi-bin/koha/catalogue/moredetail.pl?item=[% loopro.object | uri %]&amp;biblionumber=[% loopro.biblionumber | uri %]&amp;bi=[% loopro.biblioitemnumber | uri %]#item[% loopro.object | uri %]"
>Item [% loopro.object | html %]</a
>
from
<a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% loopro.biblionumber | uri %]" title="Display detail for this bibliographic record"
>Bibliographic record [% loopro.biblionumber | html %]</a
>
[% ELSIF ( loopro.info.substr(0, 6) == 'biblio' ) %]
<a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% loopro.object | uri %]" title="Display detail for this bibliographic record">Bibliographic record [% loopro.object | html %]</a>
[% ELSE %]
[% loopro.object | html %]
[% END %]
[% ELSIF ( loopro.module == 'SERIAL' ) %]
<a href="/cgi-bin/koha/serials/subscription-detail.pl?subscriptionid=[% loopro.object | uri %]">Subscription [% loopro.object | html %] </a>
[% ELSIF ( loopro.module == 'AUTHORITIES' ) %]
<a href="/cgi-bin/koha/authorities/detail.pl?authid=[% loopro.object | uri %]" title="Display detail for this authority">Authority [% loopro.object | html %]</a>
[% ELSIF ( loopro.module == 'NOTICES' ) %]
[% IF ( loopro.notice ) %]
<a
href="/cgi-bin/koha/tools/letter.pl?op=add_form&branchcode=[% loopro.notice.branchcode | uri %]&module=[% loopro.notice.module | uri %]&code=[% loopro.notice.code | uri %]#lang_[% loopro.notice.lang | uri %]"
>Notice [% loopro.notice.title | html %] ([% loopro.notice.code | html %])</a
>
[% ELSE %]
[% loopro.object | html %]
[% END %]
[% ELSIF ( loopro.module == 'RECALLS' ) %]
[% IF loopro.recall.item_level %]
Item-level recall on
<a href="/cgi-bin/koha/catalogue/moredetail.pl?item=[% loopro.recall.item_id | uri %]&amp;biblionumber=[% loopro.recall.biblio_id | uri %]&amp;bi=[% loopro.recall.item.biblioitemnumber | uri %]"
>item</a
>
[% ELSE %]
Bibliographic record-level recall on <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% loopro.recall.biblio_id | uri %]">biblio</a>
[% END %]
[% ELSIF ( loopro.module == 'ACQUISITIONS' && loopro.action == 'ACQUISITION ORDER' && loopro.object ) %]
[% IF ( CAN_user_acquisition_order_manage ) %]
Basket <a href="/cgi-bin/koha/acqui/basket.pl?basketno=[% loopro.object | uri %]">[% loopro.object | html %]</a>
[% ELSE %]
Basket [% loopro.object | html %]
[% END %]
[% ELSIF ( loopro.module == "SUGGESTION" ) %]
<a href="/cgi-bin/koha/suggestion/suggestion.pl?suggestionid=[% loopro.object | uri %]&op=show">[% loopro.object | html %]</a>
[% ELSE %]
[% loopro.object | html %]
[% END %]
</td>
<td>
[% IF ( loopro.module == 'CIRCULATION' && loopro.object_found ) %]
<a
href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% loopro.info | uri %]&amp;biblionumber=[% loopro.biblionumber | uri %]&amp;bi=[% loopro.biblioitemnumber | uri %]#item[% loopro.info | uri %]"
title="Display detail for this item"
>Item [% loopro.barcode | html %]</a
>
[% ELSIF ( loopro.module == 'CIRCULATION' && loopro.json_found ) %]
<div class="loginfo" id="loginfo[% loopro.action_id | html %]">
<div>
<a
href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% loopro.json.itemnumber | uri %]&amp;biblionumber=[% loopro.json.biblionumber | uri %]&amp;bi=[% loopro.json.biblioitemnumber | uri %]#item[% loopro.json.itemnumber | uri %]"
title="Display detail for this item"
>Item [% loopro.json.barcode | html %] [% loopro.json.branchcode | html %]</a
>
</div>
[% FOREACH force IN loopro.json.forced %]
<div class="forced"> [% PROCESS translate_log_override override=force %] </div>
[% END %]
[% FOREACH confirmation IN loopro.json.confirmations %]
<div class="confirmed"> [% PROCESS translate_log_override override=confirmation %] </div>
[% END %]
</div>
[% ELSIF ( loopro.module == 'HOLDS' && loopro.json_found ) %]
<div class="loginfo" id="loginfo[% loopro.action_id | html %]">
<div>
<a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% loopro.json.biblionumber | uri %]" title="Display holds for this record"
>Hold [% loopro.json.hold | html %] on biblio [% loopro.json.biblionumber | html %]</a
>
[% IF loopro.json.itemnumber %]
-
<a
href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% loopro.json.itemnumber | uri %]&amp;biblionumber=[% loopro.json.biblionumber | uri %]#item[% loopro.json.itemnumber | uri %]"
title="Display detail for this item"
>Item [% loopro.json.itemnumber | html %]</a
>
[% END %]
</div>
[% FOREACH force IN loopro.json.forced %]
<div class="forced"> [% PROCESS translate_log_override override=force %] </div>
[% END %]
[% FOREACH confirmation IN loopro.json.confirmations %]
<div class="confirmed"> [% PROCESS translate_log_override override=confirmation %] </div>
[% END %]
</div>
[% ELSIF loopro.module == "SYSTEMPREFERENCE" || loopro.module == "REPORTS" || loopro.module == "NEWS" %]
<div class="loginfo" id="loginfo[% loopro.action_id | html %]">[% loopro.info | trim | html %]</div>
<div class="compare_info" id="compare_info[% loopro.action_id | html %]">
<!-- prettier-ignore-start -->
<label>
[% SET split_info = loopro.info.split(' \| ') %]
<input type="checkbox" name="diff" id="action_id[% loopro.action_id | html %]" data-actionid="[% loopro.action_id | html %]" data-filter="[% FOREACH info IN split_info %][% IF loop.first %][% info | html %][% END %][% END %]" class="compare" /> Compare</label>
<!-- prettier-ignore-end -->
</div>
[% ELSIF loopro.module == "NOTICES" %]
<div class="loginfo" id="loginfo[% loopro.action_id | html %]">[% loopro.info | trim | html %]</div>
<div class="compare_info" id="compare_info[% loopro.action_id | html %]">
<label
><input
type="checkbox"
name="diff"
id="action_id[% loopro.action_id | html %]"
data-actionid="[% loopro.action_id | html %]"
data-filter="[% IF loopro.notice %][% loopro.notice.title | html %][% ELSE %][% loopro.object | html %][% END %]"
class="compare"
/>
Compare</label
>
</div>
[% ELSE %]
<div class="loginfo" id="loginfo[% loopro.action_id | html %]"> [% loopro.info | html %] </div>
[% END %]
</td>
<td>[% PROCESS translate_log_interface log_interface=loopro.interface %]</td>
<td class="diff-col">[% loopro.diff | html %]</td>
</tr>
[% END %]
</tbody>
</table>
</div>
<!-- /.page-section -->
[% ELSE %]
<div class="alert alert-warning">
[% IF ( CATALOGUING ) %]
No log found for <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% object | url %]">Bibliographic record [% object | html %]</a>.
[% ELSIF ( MEMBERS ) %]
No log found for <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% object | url %]">[% INCLUDE 'patron-title.inc' %]</a>.
[% ELSE %]
No log found.
[% END %]
</div>
[% END %]
<h3>Log entries</h3>
<div id="toolbar" class="btn-toolbar sticky">
<a href="#" id="select_none" class="btn btn-link disabled"><i class="fa fa-times"></i> Check none</a>
<button class="btn btn-link disabled compare_link"><i class="fa fa-columns"></i> Compare selected</button>
</div>
<div class="page-section">
<table id="logst">
<thead>
<tr>
<th>Date</th>
<th>Librarian</th>
<th>Module</th>
<th>Action</th>
<th>Object</th>
<th>Info</th>
<th>Interface</th>
<th>Diff</th>
</tr>
</thead>
</table>
</div>
<!-- /.page-section -->
[% END %]
[% END %]
@ -519,6 +341,20 @@
<script nonce="[% Koha.CSPNonce | $raw %]" data-tt-allowed="true">
var table_settings = [% TablesSettings.GetTableSettings('tools', 'logviewer', 'logst', 'json') | $raw %];
var CAN_user_parameters_manage_sysprefs = "[% CAN_user_parameters_manage_sysprefs | html %]";
var CAN_user_acquisition_order_manage = "[% CAN_user_acquisition_order_manage | html %]";
var do_it = [% do_it || 0 | html %];
[% IF ( do_it ) %]
var search_params = {
user: "[% user | html %]",
modules: [% To.json(modules || []) | $raw %],
actions: [% To.json(actions || []) | $raw %],
interfaces: [% To.json(interfaces || []) | $raw %],
object: "[% object | html %]",
info: "[% info | html %]",
datefrom: "[% datefrom | html %]",
dateto: "[% dateto | html %]",
};
[% END %]
</script>
[% Asset.js("js/viewlog.js") | $raw %]
[% END %]

View File

@ -150,58 +150,355 @@ function untickAll(section) {
}
function limitCheckboxes() {
var checkboxes = $(".compare");
var limit = 2;
var compare_link =
'<a href="#" class="btn btn-link compare_link"><i class="fa fa-columns"></i> ' +
__("View comparison") +
"</a>";
checkboxes.each(function () {
$(this).on("change", function () {
var checked = [];
checkboxes.each(function () {
if ($(this).prop("checked")) {
checked.push($(this).data("actionid"));
}
});
if (checked.length > 0) {
$("#select_none").removeClass("disabled");
} else {
$("#select_none").addClass("disabled");
$("#logst").DataTable().search("").draw();
}
if (checked.length == 1) {
$("#logst").DataTable().search($(this).data("filter")).draw();
humanMsg.displayAlert(
__("Showing results for %s").format($(this).data("filter"))
);
}
if (checked.length == 2) {
$("#compare_info" + checked[0]).prepend(compare_link);
$("#compare_info" + checked[1]).prepend(compare_link);
$("button.compare_link").removeClass("disabled");
} else if (checked.length > limit) {
humanMsg.displayAlert(
__("You can select maximum of two checkboxes")
);
$(this).prop("checked", false);
} else if (checked.length < limit) {
$("a.compare_link").remove();
$("button.compare_link").addClass("disabled");
$(document).on("change", ".compare", function () {
var checked = [];
$(".compare").each(function () {
if ($(this).prop("checked")) {
checked.push($(this).data("actionid"));
}
});
if (checked.length > 0) {
$("#select_none").removeClass("disabled");
} else {
$("#select_none").addClass("disabled");
}
if (checked.length == 2) {
$("a.compare_link").remove();
$("#compare_info" + checked[0]).prepend(compare_link);
$("#compare_info" + checked[1]).prepend(compare_link);
$("button.compare_link").removeClass("disabled");
} else if (checked.length > limit) {
humanMsg.displayAlert(
__("You can select maximum of two checkboxes")
);
$(this).prop("checked", false);
} else if (checked.length < limit) {
$("a.compare_link").remove();
$("button.compare_link").addClass("disabled");
}
});
}
var module_labels = {
APIKEYS: __("API keys"),
AUTH: __("Authentication"),
CATALOGUING: __("Cataloging"),
AUTHORITIES: __("Authorities"),
MEMBERS: __("Patrons"),
ACQUISITIONS: __("Acquisitions"),
SERIAL: __("Serials"),
HOLDS: __("Holds"),
ILL: __("Interlibrary loans"),
CIRCULATION: __("Circulation"),
CLAIMS: __("Claims"),
FINES: __("Fines"),
SYSTEMPREFERENCE: __("System preferences"),
CRONJOBS: __("Cron jobs"),
REPORTS: __("Reports"),
SEARCHENGINE: __("Search engine"),
NOTICES: __("Notices"),
NEWS: __("Additional content"),
RECALLS: __("Recalls"),
SUGGESTION: __("Suggestions"),
TRANSFERS: __("Transfers"),
};
var action_labels = {
ACTIVATE: __("Activate"),
ADD: __("Add"),
DELETE: __("Delete"),
MODIFY: __("Modify"),
REVOKE: __("Revoke"),
ISSUE: __("Checkout"),
RETURN: __("Return"),
CREATE: __("Create"),
CANCEL: __("Cancel"),
FILL: __("Fill"),
RESUME: __("Resume"),
SUSPEND: __("Suspend"),
RENEW: __("Renew"),
RENEWAL: __("Renew"),
"CHANGE PASS": __("Change password"),
"RESET PASS": __("Reset password"),
ADDCIRCMESSAGE: __("Add circulation message"),
MODCIRCMESSAGE: __("Modify circulation message"),
DELCIRCMESSAGE: __("Delete circulation message"),
STATUS_CHANGE: __("Change ILL request status"),
PATRON_NOTICE: __("ILL notice sent to patron"),
ADD_BASKET: __("Create an acquisitions basket"),
MODIFY_BASKET: __("Modify an acquisitions basket"),
MODIFY_BASKET_HEADER: __("Modify an acquisitions basket header"),
MODIFY_BASKET_USERS: __("Modify an acquisitions basket's users"),
CLOSE_BASKET: __("Close an acquisitions basket"),
APPROVE_BASKET: __("Approve an acquisitions basket"),
REOPEN_BASKET: __("Reopen an acquisitions basket"),
CANCEL_ORDER: __("Cancel an order"),
CREATE_ORDER: __("Create an order"),
MODIFY_ORDER: __("Modify an order"),
CREATE_INVOICE_ADJUSTMENT: __("Create an invoice adjustment"),
UPDATE_INVOICE_ADJUSTMENT: __("Modify an invoice adjustment"),
DELETE_INVOICE_ADJUSTMENT: __("Delete an invoice adjustment"),
RECEIVE_ORDER: __("Receive an order"),
MODIFY_BUDGET: __("Modify a budget"),
CREATE_FUND: __("Create a fund"),
MODIFY_FUND: __("Modify a fund"),
DELETE_FUND: __("Delete a fund"),
CREATE_RESTRICTION: __("Create restriction"),
MODIFY_RESTRICTION: __("Modify restriction"),
DELETE_RESTRICTION: __("Delete restriction"),
Run: __("Run"),
End: __("End"),
PATRON_MERGE: __("Merge"),
EDIT_MAPPINGS: __("Edit mappings"),
RESET_MAPPINGS: __("Reset mappings"),
"SERIAL CLAIM": __("Serial claim"),
"ACQUISITION CLAIM": __("Acquisition claim"),
"ACQUISITION ORDER": __("Acquisition order"),
OVERDUE: __("Overdue"),
EXPIRE: __("Expire"),
MODIFY_CARDNUMBER: __("Modify cardnumber"),
"RESET 2FA": __("Reset 2FA"),
};
var interface_labels = {
INTRANET: __("Staff interface"),
OPAC: __("OPAC"),
SIP: __("SIP"),
COMMANDLINE: __("Command-line"),
API: __("REST API"),
CRON: __("Cron job"),
UPGRADE: __("Upgrade"),
};
function escapeHtml(str) {
if (str == null) return "";
return $("<span>").text(String(str)).html();
}
function renderObject(data, type, row) {
var mod = row.module;
var obj = row.object;
if (obj == null) return "";
if (
mod == "MEMBERS" ||
mod == "CIRCULATION" ||
mod == "FINES" ||
mod == "APIKEYS"
) {
if (mod == "APIKEYS") {
return (
'<a href="/cgi-bin/koha/members/apikeys.pl?patron_id=' +
encodeURIComponent(obj) +
'">' +
__("API keys for patron %s").format(escapeHtml(obj)) +
"</a>"
);
}
return (
'<a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=' +
encodeURIComponent(obj) +
'">' +
escapeHtml(obj) +
"</a>"
);
}
if (mod == "CATALOGUING") {
var info = row.info || "";
if (info.substr(0, 6) == "biblio") {
return (
'<a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=' +
encodeURIComponent(obj) +
'">Bibliographic record ' +
escapeHtml(obj) +
"</a>"
);
}
return escapeHtml(obj);
}
if (mod == "SERIAL") {
return (
'<a href="/cgi-bin/koha/serials/subscription-detail.pl?subscriptionid=' +
encodeURIComponent(obj) +
'">Subscription ' +
escapeHtml(obj) +
"</a>"
);
}
if (mod == "AUTHORITIES") {
return (
'<a href="/cgi-bin/koha/authorities/detail.pl?authid=' +
encodeURIComponent(obj) +
'">Authority ' +
escapeHtml(obj) +
"</a>"
);
}
if (mod == "NOTICES") {
return escapeHtml(obj);
}
if (mod == "ACQUISITIONS" && row.action == "ACQUISITION ORDER" && obj) {
if (CAN_user_acquisition_order_manage) {
return (
'Basket <a href="/cgi-bin/koha/acqui/basket.pl?basketno=' +
encodeURIComponent(obj) +
'">' +
escapeHtml(obj) +
"</a>"
);
}
return "Basket " + escapeHtml(obj);
}
if (mod == "SUGGESTION") {
return (
'<a href="/cgi-bin/koha/suggestion/suggestion.pl?suggestionid=' +
encodeURIComponent(obj) +
'&op=show">' +
escapeHtml(obj) +
"</a>"
);
}
return escapeHtml(obj);
}
function renderInfo(data, type, row) {
var mod = row.module;
var info = row.info;
var action_id = row.action_id;
if (info == null) return "";
if (mod == "SYSTEMPREFERENCE" || mod == "REPORTS" || mod == "NEWS") {
var split_info = info.split(" | ");
var filter_value = escapeHtml(split_info[0]);
return (
'<div class="loginfo" id="loginfo' +
escapeHtml(action_id) +
'">' +
escapeHtml(info) +
"</div>" +
'<div class="compare_info" id="compare_info' +
escapeHtml(action_id) +
'"><label><input type="checkbox" name="diff" id="action_id' +
escapeHtml(action_id) +
'" data-actionid="' +
escapeHtml(action_id) +
'" data-filter="' +
filter_value +
'" class="compare" /> Compare</label></div>'
);
}
if (mod == "NOTICES") {
return (
'<div class="loginfo" id="loginfo' +
escapeHtml(action_id) +
'">' +
escapeHtml(info) +
"</div>" +
'<div class="compare_info" id="compare_info' +
escapeHtml(action_id) +
'"><label><input type="checkbox" name="diff" id="action_id' +
escapeHtml(action_id) +
'" data-actionid="' +
escapeHtml(action_id) +
'" data-filter="' +
escapeHtml(row.object || "") +
'" class="compare" /> Compare</label></div>'
);
}
var rendered = renderRecordInfo(info);
var body = rendered !== null ? rendered : escapeHtml(info);
return (
'<div class="loginfo" id="loginfo' +
escapeHtml(action_id) +
'">' +
body +
"</div>"
);
}
function buildApiFilters(params) {
var filters = {};
if (params.user) {
filters.user = params.user;
}
if (params.object) {
filters.object = params.object;
}
if (params.info) {
filters.info = { like: "%" + params.info + "%" };
}
if (params.modules && params.modules.length > 0) {
var non_empty = params.modules.filter(function (m) {
return m !== "";
});
if (non_empty.length > 0) {
filters.module = non_empty;
}
}
if (params.actions && params.actions.length > 0) {
var non_empty_actions = params.actions.filter(function (a) {
return a !== "";
});
if (non_empty_actions.length > 0) {
// Circulation uses RENEWAL, but Patrons uses RENEW
if (non_empty_actions.indexOf("RENEW") !== -1) {
non_empty_actions.push("RENEWAL");
}
filters.action = non_empty_actions;
}
}
if (params.interfaces && params.interfaces.length > 0) {
var non_empty_ifaces = params.interfaces.filter(function (i) {
return i !== "";
});
if (non_empty_ifaces.length > 0) {
filters.interface = non_empty_ifaces;
}
}
// Flatpickr always submits dates in YYYY-MM-DD format (dateFormat: "Y-m-d")
if (params.datefrom && params.dateto) {
filters.timestamp = {
">=": params.datefrom + "T00:00:00Z",
"<=": params.dateto + "T23:59:59Z",
};
} else if (params.datefrom) {
filters.timestamp = {
">=": params.datefrom + "T00:00:00Z",
};
} else if (params.dateto) {
filters.timestamp = {
"<=": params.dateto + "T23:59:59Z",
};
}
return filters;
}
$(document).ready(function () {
limitCheckboxes();
if ($(".compare_info").length == 0) {
/* Remove toolbar if there are no system preference
entries to compare */
$("#toolbar").remove();
}
if ($('input[type="checkbox"][name="modules"]:checked').length == 0) {
tickAll("modules");
}
@ -259,14 +556,110 @@ $(document).ready(function () {
}
});
var logst = $("#logst").kohaTable(
{
autoWidth: false,
order: [[0, "desc"]],
pagingType: "full",
},
table_settings
);
if (do_it && typeof search_params !== "undefined") {
var api_filters = buildApiFilters(search_params);
$("#logst").kohaTable(
{
ajax: {
url: "/api/v1/action_logs",
},
order: [[0, "desc"]],
pagingType: "full",
autoWidth: false,
columns: [
{
data: "timestamp",
title: __("Date"),
searchable: true,
orderable: true,
render: function (data, type, row) {
if (type == "sort" || type == "type") {
return data;
}
return $datetime(data);
},
},
{
data: "user",
title: __("Librarian"),
searchable: true,
orderable: true,
render: function (data, type, row) {
if (data) {
return (
'<a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=' +
encodeURIComponent(data) +
'">' +
escapeHtml(data) +
"</a>"
);
}
return escapeHtml(data);
},
},
{
data: "module",
title: __("Module"),
searchable: true,
orderable: true,
render: function (data, type, row) {
return module_labels[data] || escapeHtml(data);
},
},
{
data: "action",
title: __("Action"),
searchable: true,
orderable: true,
render: function (data, type, row) {
return action_labels[data] || escapeHtml(data);
},
},
{
data: "object",
title: __("Object"),
searchable: true,
orderable: true,
render: renderObject,
},
{
data: "info",
title: __("Info"),
searchable: true,
orderable: false,
render: renderInfo,
},
{
data: "interface",
title: __("Interface"),
searchable: true,
orderable: true,
render: function (data, type, row) {
if (!data) return "";
return (
interface_labels[data.toUpperCase()] ||
escapeHtml(data)
);
},
},
{
data: "diff",
title: __("Diff"),
searchable: false,
orderable: false,
render: function (data, type, row) {
if (!data) return "";
return renderStructDiff(data);
},
},
],
},
table_settings,
0,
api_filters
);
}
$("body").on("click", ".compare_link", function (e) {
e.preventDefault();
@ -291,20 +684,6 @@ $(document).ready(function () {
$(".compare:checked").prop("checked", false).change();
});
$("td.diff-col").each(function () {
var raw = $(this).text().trim();
if (raw) {
$(this).html(renderStructDiff(raw));
}
});
$("div.loginfo").each(function () {
var raw = $(this).text().trim();
var rendered = renderRecordInfo(raw);
if (rendered) {
$(this).html(rendered);
}
});
patron_autocomplete($("#user"), {
"on-select-callback": function (event, ui) {
$("#user").val(ui.item.patron_id);

View File

@ -89,171 +89,11 @@ $template->param(
);
if ($do_it) {
my $dtf = Koha::Database->new->schema->storage->datetime_parser;
my %search_params;
if ( $datefrom and $dateto ) {
my $dateto_endday = dt_from_string($dateto);
$dateto_endday->set( # We set last second of day to see all log from that day
hour => 23,
minute => 59,
second => 59
);
$search_params{'timestamp'} = {
-between => [
$dtf->format_datetime( dt_from_string($datefrom) ),
$dtf->format_datetime($dateto_endday),
]
};
} elsif ($datefrom) {
$search_params{'timestamp'} = { '>=' => $dtf->format_datetime( dt_from_string($datefrom) ) };
} elsif ($dateto) {
my $dateto_endday = dt_from_string($dateto);
$dateto_endday->set( # We set last second of day to see all log from that day
hour => 23,
minute => 59,
second => 59
);
$search_params{'timestamp'} = { '<=' => $dtf->format_datetime($dateto_endday) };
}
# Circulation uses RENEWAL, but Patrons uses RENEW, this helps to find both
if ( grep { $_ eq 'RENEW' } @actions ) {
push @actions, 'RENEWAL';
}
$search_params{user} = $user if $user;
$search_params{module} = { -in => [@modules] } if ( defined $modules[0] and $modules[0] ne '' );
$search_params{action} = { -in => [@actions] } if ( defined $actions[0] && $actions[0] ne '' );
$search_params{interface} = { -in => [@interfaces] } if ( defined $interfaces[0] && $interfaces[0] ne '' );
if ( @modules == 1 && $object_type eq 'biblio' ) {
# Handle 'Modification log' from cataloguing
my $biblio = Koha::Biblios->find($object);
my @itemnumbers = $biblio->items->get_column('itemnumber');
$search_params{'-or'} = [
{ -and => { object => $object, info => { -like => 'biblio%' } } },
{ -and => { object => \@itemnumbers, info => { -like => 'item%' } } },
];
$template->param( biblio => $biblio );
} else {
$search_params{info} = { -like => '%' . $info . '%' } if $info;
$search_params{object} = $object if $object;
}
my @logs = Koha::ActionLogs->search( \%search_params )->as_list;
my @data;
foreach my $log (@logs) {
my $result = $log->unblessed;
# Init additional columns for CSV export
$result->{'biblionumber'} = q{};
$result->{'biblioitemnumber'} = q{};
$result->{'barcode'} = q{};
if ( defined $log->info && substr( $log->info, 0, 4 ) eq 'item' ) {
# get item information so we can create a working link
my $itemnumber = $log->object;
my $item = Koha::Items->find($itemnumber);
if ($item) {
$result->{'object_found'} = 1;
$result->{'biblionumber'} = $item->biblionumber;
$result->{'biblioitemnumber'} = $item->biblionumber;
$result->{'barcode'} = $item->barcode;
}
}
if ( $log->module eq "CIRCULATION" ) {
my $info = $log->info;
my $decoded;
my $is_json = eval {
$decoded = decode_json($info);
1;
};
if ( $is_json && ref($decoded) ) {
$result->{'json_found'} = 1;
my $item = Koha::Items->find( $decoded->{itemnumber} );
if ($item) {
$decoded->{biblionumber} = $item->biblionumber;
$decoded->{biblioitemnumber} = $item->biblionumber;
$decoded->{barcode} = $item->barcode;
}
$result->{'json'} = $decoded;
} else {
my $item = Koha::Items->find($info);
if ($item) {
$result->{'object_found'} = 1;
$result->{'biblionumber'} = $item->biblionumber;
$result->{'biblioitemnumber'} = $item->biblionumber;
$result->{'barcode'} = $item->barcode;
}
}
}
#always add firstname and surname for librarian/user
if ( $log->user ) {
my $patron = Koha::Patrons->find( $log->user );
if ( $patron && $output eq 'screen' ) {
$result->{librarian} = $patron;
}
}
#add firstname and surname for borrower, when using the CIRCULATION, MEMBERS, FINES, APIKEYS
if ( $log->module eq "CIRCULATION"
|| $log->module eq "MEMBERS"
|| $log->module eq "FINES"
|| $log->module eq "APIKEYS" )
{
if ( $log->object ) {
my $patron = Koha::Patrons->find( $log->object );
if ( $patron && $output eq 'screen' ) {
$result->{patron} = $patron;
}
}
}
if ( $log->module eq 'NOTICES' ) {
if ( $log->object ) {
my $notice = Koha::Notice::Templates->find( { id => $log->object } );
if ( $notice && $output eq 'screen' ) {
$result->{notice} = $notice->unblessed;
}
}
}
# get recall information
if ( $log->module eq "RECALLS" ) {
if ( $log->object ) {
my $recall = Koha::Recalls->find( $log->object );
if ( $recall && $output eq 'screen' ) {
$result->{recall} = $recall;
}
}
}
if ( $result->{module} eq 'MEMBERS' ) {
my $json_engine = JSON->new->utf8->canonical(1)->pretty(1);
if ( $result->{info} && $result->{info} =~ /^[{\[]/ ) {
my $perl_info = $json_engine->decode( $result->{info} );
$result->{info} = $json_engine->encode($perl_info);
}
if ( $result->{diff} && $result->{diff} =~ /^[{\[]/ ) {
my $perl_diff = $json_engine->decode( $result->{diff} );
$result->{diff} = $json_engine->encode($perl_diff);
}
}
push @data, $result;
}
if ( $output eq "screen" ) {
# Printing results to screen
# Screen output: pass search params to template, DataTable loads via REST API
$template->param(
logview => 1,
total => scalar @data,
looprow => \@data,
do_it => 1,
datefrom => $datefrom,
dateto => $dateto,
@ -272,6 +112,110 @@ if ($do_it) {
output_html_with_http_headers $input, $cookie, $template->output;
} else {
# CSV export: fetch and enrich data server-side
my $dtf = Koha::Database->new->schema->storage->datetime_parser;
my %search_params;
if ( $datefrom and $dateto ) {
my $dateto_endday = dt_from_string($dateto);
$dateto_endday->set( # We set last second of day to see all log from that day
hour => 23,
minute => 59,
second => 59
);
$search_params{'timestamp'} = {
-between => [
$dtf->format_datetime( dt_from_string($datefrom) ),
$dtf->format_datetime($dateto_endday),
]
};
} elsif ($datefrom) {
$search_params{'timestamp'} = { '>=' => $dtf->format_datetime( dt_from_string($datefrom) ) };
} elsif ($dateto) {
my $dateto_endday = dt_from_string($dateto);
$dateto_endday->set( # We set last second of day to see all log from that day
hour => 23,
minute => 59,
second => 59
);
$search_params{'timestamp'} = { '<=' => $dtf->format_datetime($dateto_endday) };
}
# Circulation uses RENEWAL, but Patrons uses RENEW, this helps to find both
if ( grep { $_ eq 'RENEW' } @actions ) {
push @actions, 'RENEWAL';
}
$search_params{user} = $user if $user;
$search_params{module} = { -in => [@modules] } if ( defined $modules[0] and $modules[0] ne '' );
$search_params{action} = { -in => [@actions] } if ( defined $actions[0] && $actions[0] ne '' );
$search_params{interface} = { -in => [@interfaces] } if ( defined $interfaces[0] && $interfaces[0] ne '' );
if ( @modules == 1 && $object_type eq 'biblio' ) {
# Handle 'Modification log' from cataloguing
my $biblio = Koha::Biblios->find($object);
my @itemnumbers = $biblio->items->get_column('itemnumber');
$search_params{'-or'} = [
{ -and => { object => $object, info => { -like => 'biblio%' } } },
{ -and => { object => \@itemnumbers, info => { -like => 'item%' } } },
];
} else {
$search_params{info} = { -like => '%' . $info . '%' } if $info;
$search_params{object} = $object if $object;
}
my @logs = Koha::ActionLogs->search( \%search_params )->as_list;
my @data;
foreach my $log (@logs) {
my $result = $log->unblessed;
# Init additional columns for CSV export
$result->{'biblionumber'} = q{};
$result->{'biblioitemnumber'} = q{};
$result->{'barcode'} = q{};
if ( defined $log->info && substr( $log->info, 0, 4 ) eq 'item' ) {
# get item information so we can create a working link
my $itemnumber = $log->object;
my $item = Koha::Items->find($itemnumber);
if ($item) {
$result->{'biblionumber'} = $item->biblionumber;
$result->{'biblioitemnumber'} = $item->biblionumber;
$result->{'barcode'} = $item->barcode;
}
}
if ( $log->module eq "CIRCULATION" ) {
my $info = $log->info;
my $decoded;
my $is_json = eval {
$decoded = decode_json($info);
1;
};
if ( $is_json && ref($decoded) ) {
my $item = Koha::Items->find( $decoded->{itemnumber} );
if ($item) {
$decoded->{biblionumber} = $item->biblionumber;
$decoded->{biblioitemnumber} = $item->biblionumber;
$decoded->{barcode} = $item->barcode;
}
} else {
my $item = Koha::Items->find($info);
if ($item) {
$result->{'biblionumber'} = $item->biblionumber;
$result->{'biblioitemnumber'} = $item->biblionumber;
$result->{'barcode'} = $item->barcode;
}
}
}
push @data, $result;
}
# Printing to a csv file
my $content = q{};
if (@data) {