@@ -, +, @@ a clock face with a encircling arrow; the history icon. to the record, including value change, date/time and user name. clicked value (a previous value) --- C4/Biblio.pm | 25 +- Koha/MetadataRecord/History.pm | 261 ++++++++++++++++++ cataloguing/addbiblio.pl | 42 ++- .../bug_14367_-_add_history_column.pl | 26 ++ installer/data/mysql/kohastructure.sql | 2 + .../intranet-tmpl/prog/css/addbiblio.css | 22 ++ .../prog/en/modules/cataloguing/addbiblio.tt | 54 +++- .../intranet-tmpl/prog/img/icon-history.png | Bin 0 -> 513 bytes koha-tmpl/intranet-tmpl/prog/js/cataloging.js | 10 + t/db_dependent/BiblioHistory.t | 249 +++++++++++++++++ 10 files changed, 670 insertions(+), 21 deletions(-) create mode 100644 Koha/MetadataRecord/History.pm create mode 100644 installer/data/mysql/atomicupdate/bug_14367_-_add_history_column.pl create mode 100644 koha-tmpl/intranet-tmpl/prog/img/icon-history.png create mode 100644 t/db_dependent/BiblioHistory.t --- a/C4/Biblio.pm +++ a/C4/Biblio.pm @@ -84,6 +84,7 @@ use MARC::File::USMARC; use MARC::File::XML; use POSIX qw( strftime ); use Module::Load::Conditional qw( can_load ); +use JSON qw(encode_json decode_json); use C4::Koha; use C4::Log qw( logaction ); # logaction @@ -112,6 +113,7 @@ use Koha::SearchEngine; use Koha::SearchEngine::Indexer; use Koha::Libraries; use Koha::Util::MARC; +use Koha::MetadataRecord::History; =head1 NAME @@ -404,9 +406,9 @@ sub ModBiblio { # update biblionumber and biblioitemnumber in MARC # FIXME - this is assuming a 1 to 1 relationship between # biblios and biblioitems - my $sth = $dbh->prepare("select biblioitemnumber from biblioitems where biblionumber=?"); + my $sth = $dbh->prepare("SELECT biblioitems.biblioitemnumber, biblio_metadata.history FROM biblioitems INNER JOIN biblio_metadata ON biblioitems.biblionumber = biblio_metadata.biblionumber WHERE biblioitems.biblionumber=?"); $sth->execute($biblionumber); - my ($biblioitemnumber) = $sth->fetchrow; + my ($biblioitemnumber, $history) = $sth->fetchrow; $sth->finish(); _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber ); @@ -417,7 +419,7 @@ sub ModBiblio { _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode ); # update the MARC record (that now contains biblio and items) with the new record data - ModBiblioMarc( $record, $biblionumber, { skip_record_index => $skip_record_index } ); + ModBiblioMarc( $record, $biblionumber, { skip_record_index => $skip_record_index, history => $history } ); # modify the other koha tables _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode ); @@ -2716,6 +2718,7 @@ sub ModBiblioMarc { } my $skip_record_index = $params->{skip_record_index} || 0; + my $history = $params->{history} || undef; # Clone record as it gets modified $record = $record->clone(); @@ -2781,6 +2784,22 @@ sub ModBiblioMarc { $indexer->index_records( $biblionumber, "specialUpdate", "biblioserver" ); } + # Decode JSON history, create new record, update and re-encode to JSON. + my $historylength = 10; + eval { + $history = decode_json($history); + 1; + } or do { + $history = undef; + }; + my $newrecord = HistoryRecordNew($record); + my $newhistory = HistoryUpdate($newrecord, $historylength, $history); + $newhistory = encode_json($newhistory); + + $m_rs->update({ metadata => $record->as_xml_record($encoding), history => $newhistory }); + + ModZebra( $biblionumber, "specialUpdate", "biblioserver", $record ); + return $biblionumber; } --- a/Koha/MetadataRecord/History.pm +++ a/Koha/MetadataRecord/History.pm @@ -0,0 +1,261 @@ +package Koha::MetadataRecord::History; + +# Copyright 2016 Aleisha Amohia +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use strict; +use warnings; + +use C4::Charset qw( StripNonXmlChars ); +use JSON qw(decode_json); + +use vars qw(@ISA @EXPORT); + +BEGIN { + require Exporter; + @ISA = qw( Exporter ); + + push @EXPORT, qw( + &GetMarcBiblioHistory + &HistoryUpdate + &HistoryParse + &HistoryMapUsers + &HistoryBorrowerDetails + &HistoryRecordNew + ); + } + +=head1 NAME + +Koha::MetadataRecord::History - tracking changes to MARC records + +=head1 DESCRIPTION + +Koha::MetadataRecord::History not only tracks changes to MARC records and presents them in a log, but also allows users to +roll them back, either in batch to a previous point in the history or separately for each field (so you can roll back changes +to a field without changing fields that may have been changed after this one). + +=head2 GetMarcBiblioHistory + + my ($record, $history) = GetMarcBiblio($biblionumber, [$embeditems]); + +Returns MARC::Record representing bib identified by C<$biblionumber>. If no bib exists, returns undef. +C<$biblionumber>. If no bib exists, returns undef. +C<$embeditems>. If set to true, items data are included. +The MARC record contains biblio data, and items data if $embeditems is set to true. + +=cut + +sub GetMarcBiblioHistory { + my $biblionumber = shift; + my $embeditems = shift || 0; + if (not defined $biblionumber) { + warn "GetMarcBiblio called with undefined biblionumber"; + return; + } + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare("SELECT metadata,history FROM biblio_metadata WHERE biblionumber=? "); + $sth->execute($biblionumber); + my $row = $sth->fetchrow_hashref; + my $marcxml = StripNonXmlChars( $row->{'metadata'} ); + MARC::File::XML->default_record_format( C4::Context->preference('marcflavour') ); + my $record = MARC::Record->new(); + my ($history, $borrowers) = HistoryParse( $row->{'history'} ); + $borrowers = HistoryBorrowerDetails( $borrowers ); + $history = HistoryMapUsers( $history, $borrowers ); + if ($marcxml) { + $record = eval { MARC::Record::new_from_xml( $marcxml, "utf8", C4::Context->preference('marcflavour') ) }; + if ($@) { warn " problem with :$biblionumber : $@ \n$marcxml"; } + return unless $record; + C4::Biblio::_koha_marc_update_bib_ids($record, '', $biblionumber, $biblionumber); + C4::Biblio::EmbedItemsInMarcBiblio($record, $biblionumber) if ($embeditems); + return ($record, $history); + } else { + return; + } +} + +=head2 HistoryParse + +Input format (JSON): [ { datetime, borrowernumber, marcxml } ] +Output format: { tag } { subfield } [ { borrowernumber, data, datetime } ] + +=cut + +sub HistoryParse { + my $json = shift; + if (!defined $json) { + warn "HistoryParse: Missing JSON parameter"; + return; + } + my $input = decode_json($json); + # Reindex the dataset; order all values by datetime under each tag name. + my $borrowers = { }; + my $output = { }; + for my $item (@{$input}) { + my $datetime = $item->{datetime}; + $borrowers->{ $item->{borrowernumber }} = undef; + my $record = MARC::File::XML::decode($item->{marcxml}); + for my $field ($record->fields()) { + my $tag = $field->tag(); + $output->{$tag} = {} unless defined $output->{$tag}; + for my $subfield ($field->subfields()) { + # Build hash ref structure. + my $subfield_tag = @{$subfield}[0]; + $output->{$tag}->{$subfield_tag} = [] + unless defined $output->{$tag}->{$subfield_tag}; + push( @{ $output->{$tag}->{$subfield_tag} }, HistoryParsedRecordNew( + @{ $subfield }[1], $item->{borrowernumber}, $datetime)); + } + } + } + return ( $output, $borrowers ); +} + +=head2 HistoryBorrowerDetails + +Take a hashref with borrowernumbers as keys, returns new hash with the +borrowernumber as key to hash containing firstname, surname and title. + +=cut + +sub HistoryBorrowerDetails { + my $borrowernumbers = shift; + my $dbh = C4::Context->dbh; + my $output = { }; + my @numbers = keys %{$borrowernumbers}; + if (@numbers > 0) { + # We now need to look up the numeric user id's and map to name and title strings. + my $sth = $dbh->prepare(qq{SELECT + borrowernumber, title, firstname, surname + FROM borrowers + WHERE borrowernumber IN} + . '(' . join(',', ('?') x @numbers) . ')'); + $sth->execute(@numbers) + or die ('BorrowerDetails: Unable to execute SELECT statement'); + while (my $row = $sth->fetchrow_hashref()) { + $output->{ $row->{borrowernumber} } = { }; + $output->{ $row->{borrowernumber} }->{title} = $row->{title}; + $output->{ $row->{borrowernumber} }->{firstname} = $row->{firstname}; + $output->{ $row->{borrowernumber} }->{surname} = $row->{surname}; + } + $sth->finish(); + } + return $output; +} + +=head2 HistoryMapUsers + +=cut + +sub HistoryMapUsers { + my ($history, $borrowers) = @_; + my $mappedhistory = { }; + for my $tag (keys %{ $history }) { + $mappedhistory->{ $tag } = { }; + for my $field (keys %{ $history->{ $tag } }) { + $mappedhistory->{ $tag }->{ $field } = [ ]; + for my $entry (@{ $history->{ $tag }->{ $field } }) { + # C4::Dates does not allow me to print time, only dates. + #my $newtime = C4::Dates->new( $history->{ $tag }->{ $field }->{ $time }->{timestamp}, 'iso' )->output('syspref'); + my $newitem = { }; + my $user = $borrowers->{ $entry->{borrowernumber} }; + + # Maybe change this to work on original instead of copy? + $newitem->{title} = $user->{title}; + $newitem->{firstname} = $user->{firstname}; + $newitem->{surname} = $user->{surname}; + $newitem->{borrowernumber} = $entry->{borrowernumber}; + $newitem->{data} = $entry->{data}; + $newitem->{datetime} = $entry->{datetime}; + + push ( @{ $mappedhistory->{ $tag }->{ $field } } , $newitem); + } + } + } + return $mappedhistory; +} + +=head2 HistoryParsedRecordNew + +=cut + +sub HistoryParsedRecordNew { + my ($value, $borrowernumber, $datetime) = @_; + my $newrecord = { + data => $value, + borrowernumber => $borrowernumber, + datetime => $datetime, + }; + return $newrecord; +} + +=head2 HistoryRecordNew + +=cut + +sub HistoryRecordNew { + my ( $record ) = @_; + my $encoding = C4::Context->preference("marcflavour"); + my $hash_record = { + borrowernumber => C4::Context->userenv()->{number}, + datetime => POSIX::strftime('%Y-%m-%d %T', localtime(time)), + marcxml => $record->as_xml_record($encoding), + }; + return $hash_record; +} + +=head2 HistoryUpdate + +=cut + +sub HistoryUpdate { + my ( $record, $limit, $collection ) = @_; + if ( !$record ) { + warn "HistoryUpdate: Called with undefined record"; + return; + } + if ( ref($record) ne 'HASH' ) { + warn "HistoryUpdate: Record parameter is not a hashref"; + return; + } + $limit = 10 unless defined $limit; + # We need something to append history to, even if there is none. + $collection = [] unless defined $collection; + # Truncate history to $histsize - 1, need space for new entry. + splice(@{$collection}, $limit - 1); + push(@{$collection}, $record); + # Sort the resulting array, newest entries first. + # Removed; used to be integer timestamps. Is now MySQL date strings. + #@{$collection} = sort { $a->{timestamp} < $b->{timestamp} } @{$collection}; + @{$collection} = sort { $a->{datetime} lt $b->{datetime} } @{$collection}; + return $collection; +} + +1; + +__END__ + +=head1 AUTHOR + +Koha Development Team + +Paul POULAIN paul.poulain@free.fr + +Joshua Ferraro jmf@liblime.com + +=cut --- a/cataloguing/addbiblio.pl +++ a/cataloguing/addbiblio.pl @@ -47,12 +47,12 @@ use Koha::BiblioFrameworks; use Koha::DateUtils qw( dt_from_string ); use Koha::Biblios; +use C4::Koha; use Koha::ItemTypes; +use Koha::MetadataRecord::History; use Koha::Libraries; - use Koha::BiblioFrameworks; use Koha::Patrons; - use MARC::File::USMARC; use MARC::File::XML; use URI::Escape qw( uri_escape_utf8 ); @@ -288,10 +288,14 @@ sub GetMandatoryFieldZ3950 { =cut sub create_input { - my ( $tag, $subfield, $value, $index_tag, $rec, $authorised_values_sth,$cgi ) = @_; + my ( $tag, $subfield, $value, $index_tag, $rec, $authorised_values_sth,$cgi, $history ) = @_; my $index_subfield = CreateKey(); # create a specifique key for each subfield + my $taghistory = $history->{$tag}->{$subfield}; + + $value =~ s/"/"/g; + # Apply optional framework default value when it is a new record, # or when editing as new (duplicating a record), # or when changing a record's framework, @@ -343,6 +347,7 @@ sub create_input { value => $value, maxlength => $tagslib->{$tag}->{$subfield}->{maxlength}, random => CreateKey(), + history => $taghistory, ); if(exists $mandatory_z3950->{$tag.$subfield}){ @@ -401,6 +406,7 @@ sub create_input { maxlength => $subfield_data{maxlength}, readonly => ($is_readonly) ? 1 : 0, authtype => $tagslib->{$tag}->{$subfield}->{authtypecode}, + history => $taghistory, }; # it's a plugin field @@ -423,6 +429,7 @@ sub create_input { javascript => $plugin->javascript, plugin => $plugin->name, noclick => $plugin->noclick, + history => $taghistory, }; } else { warn $plugin->errstr; @@ -435,6 +442,7 @@ sub create_input { size => 67, maxlength => $subfield_data{maxlength}, readonly => 0, + history => $taghistory, }; } @@ -447,6 +455,7 @@ sub create_input { value => $value, size => 67, maxlength => $subfield_data{maxlength}, + history => $taghistory, }; } @@ -467,6 +476,7 @@ sub create_input { id => $subfield_data{id}, name => $subfield_data{id}, value => $value, + history => $taghistory, }; } @@ -479,6 +489,7 @@ sub create_input { size => 67, maxlength => $subfield_data{maxlength}, readonly => 0, + history => $taghistory, }; } @@ -504,11 +515,11 @@ sub format_indicator { } sub build_tabs { - my ( $template, $record, $dbh, $encoding,$input ) = @_; + my ( $template, $record, $dbh, $encoding, $input, $history ) = @_; # fill arrays my @loop_data = (); - my $tag; + #my $tag; my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : ""; my $query = "SELECT authorised_value, lib @@ -564,7 +575,6 @@ sub build_tabs { } # loop through each field foreach my $field (@fields) { - my @subfields_data; if ( $tag < 10 ) { my ( $value, $subfield ); @@ -584,7 +594,7 @@ sub build_tabs { @subfields_data, &create_input( $tag, $subfield, $value, $index_tag, $record, - $authorised_values_sth,$input + $authorised_values_sth,$input, $history ) ); } @@ -599,7 +609,7 @@ sub build_tabs { @subfields_data, &create_input( $tag, $subfield, $value, $index_tag, - $record, $authorised_values_sth,$input + $record, $authorised_values_sth,$input, $history ) ); } @@ -629,7 +639,7 @@ sub build_tabs { @subfields_data, &create_input( $tag, $subfield, '', $index_tag, $record, - $authorised_values_sth,$input + $authorised_values_sth,$input, $history ) ); } @@ -685,7 +695,7 @@ sub build_tabs { @subfields_data, &create_input( $tag, $subfield->{subfield}, '', $index_tag, $record, - $authorised_values_sth,$input + $authorised_values_sth,$input, $history ) ); } @@ -703,7 +713,7 @@ sub build_tabs { tagfirstsubfield => $subfields_data[0], fixedfield => $tag < 10?1:0, ); - + push @loop_data, \%tag_data ; } } @@ -806,6 +816,7 @@ $mandatory_z3950 = GetMandatoryFieldZ3950($frameworkcode); # -- Global my $record = -1; +my $history = undef; my $encoding = ""; my ( $biblionumbertagfield, @@ -816,7 +827,7 @@ my ( ); if ( $biblio && !$breedingid ) { - $record = $biblio->metadata->record; + ($record, $history) = GetMarcBiblioHistory($biblio->id); } if ($breedingid) { ( $record, $encoding ) = MARCfindbreeding( $breedingid ) ; @@ -902,7 +913,7 @@ if ( $op eq "addbiblio" ) { else { ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode ); } - if ($redirect eq "items" || ($mode ne "popup" && !$is_a_modif && $redirect ne "view" && $redirect ne "just_save")){ + if ($redirect eq "items" || (defined $mode && $mode ne "popup" && !$is_a_modif && $redirect ne "view" && $redirect ne "just_save")){ if ($frameworkcode eq 'FA'){ print $input->redirect( '/cgi-bin/koha/cataloguing/additem.pl?' @@ -961,7 +972,7 @@ if ( $op eq "addbiblio" ) { } } else { # it may be a duplicate, warn the user and do nothing - build_tabs ($template, $record, $dbh,$encoding,$input); + build_tabs ($template, $record, $dbh,$encoding, $input, $history); $template->param( biblionumber => $biblionumber, biblioitemnumber => $biblioitemnumber, @@ -1009,7 +1020,8 @@ elsif ( $op eq "delete" ) { $record = $urecord; }; } - build_tabs( $template, $record, $dbh, $encoding,$input ); + + build_tabs( $template, $record, $dbh, $encoding, $input, $history ); $template->param( biblionumber => $biblionumber, biblionumbertagfield => $biblionumbertagfield, --- a/installer/data/mysql/atomicupdate/bug_14367_-_add_history_column.pl +++ a/installer/data/mysql/atomicupdate/bug_14367_-_add_history_column.pl @@ -0,0 +1,26 @@ +use Modern::Perl; + +return { + bug_number => "14367", + description => "History for MARC records", + up => sub { + my ($args) = @_; + my ($dbh, $out) = @$args{qw(dbh out)}; + + if( !column_exists( 'biblio_metadata', 'history' ) ) { + $dbh->do(q{ + ALTER TABLE biblio_metadata ADD COLUMN `history` longtext DEFAULT NULL AFTER `timestamp` + }); + + say $out "Added column 'biblio_metadata.history'"; + } + + if( !column_exists( 'deletedbiblio_metadata', 'history' ) ) { + $dbh->do(q{ + ALTER TABLE deletedbiblio_metadata ADD COLUMN `history` longtext DEFAULT NULL AFTER `timestamp` + }); + + say $out "Added column 'deletedbiblio_metadata.history'"; + } + }, +}; --- a/installer/data/mysql/kohastructure.sql +++ a/installer/data/mysql/kohastructure.sql @@ -1051,6 +1051,7 @@ CREATE TABLE `biblio_metadata` ( `schema` varchar(16) NOT NULL, `metadata` longtext NOT NULL, `timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + `history` longtext DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `biblio_metadata_uniq_key` (`biblionumber`,`format`,`schema`), KEY `timestamp` (`timestamp`), @@ -2450,6 +2451,7 @@ CREATE TABLE `deletedbiblio_metadata` ( `schema` varchar(16) NOT NULL, `metadata` longtext NOT NULL, `timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + `history` longtext DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `deletedbiblio_metadata_uniq_key` (`biblionumber`,`format`,`schema`), KEY `timestamp` (`timestamp`), --- a/koha-tmpl/intranet-tmpl/prog/css/addbiblio.css +++ a/koha-tmpl/intranet-tmpl/prog/css/addbiblio.css @@ -81,6 +81,11 @@ ul li.tag li.subfield_line.ui-sortable-helper::before { text-decoration: none; } +.buttonHistory { + font-weight : bold; + text-decoration : none; +} + a.expandfield { text-decoration: none; } @@ -287,6 +292,9 @@ fieldset.order_details ol li .label:first-child { margin: 0 .5em; } +.readonly { border-width : 1px; border-style: inset; padding-left : 15px; background: #EEE url(../img/locked.png) center left no-repeat; width:29em; } +.subfield_history { margin : 0 .5em; float: right; } + #cataloguing_additem_itemlist { margin-bottom: 1em; } @@ -514,3 +522,17 @@ tbody tr.active td { font-weight: bold; padding: 0; } + +.history_container { + display: none; +} + +.history_table { + width: 100%; + display: none; + float: right; + clear: both; + font-size: 75%; + width: 75%; + margin: 0.3em; +} --- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt @@ -1,6 +1,7 @@ [% USE raw %] [% USE Asset %] [% USE Koha %] +[% USE KohaDates %] [% INCLUDE 'doc-head-open.inc' %] [% IF ( biblionumber ) %]Editing [% title | html %] (Record number [% biblionumber | html %])[% ELSE %]Add MARC record[% END %] › Cataloging › Koha [% INCLUDE 'doc-head-close.inc' %] @@ -27,6 +28,31 @@ var Sticky; $(document).ready(function() { + $(".input_marceditor").click(function(){ + var tag = $(this).attr('id'); + historyToggle(tag + '_history'); + return false; + }); + + $(".tag_editor").click(function(){ + var tag = $(this).attr('id'); + openAuth(this.parentNode.parentNode.getElementsByTagName('input')[1].id, tag, 'biblio'); + return false; + }); + + $(".buttonHistory").click(function(){ + var tag = $(this).attr('id'); + HistoryToggle(tag + '_history'); + return false; + }); + + $(".hist_rollback").click(function(){ + var tag = $(this).attr('data-tag'); + var value = $(this).attr('data-history'); + HistoryRollback(tag, value); + return false; + }); + [% IF bib_doesnt_exist %] $("#addbibliotabs").hide(); $("#toolbar").hide(); @@ -1171,16 +1197,16 @@ function PopupMARCFieldDoc(field) {
[% IF ( mv.type == 'text' ) %] [% IF ( mv.authtype ) %] - Tag editor + Tag editor [% END %] [% ELSIF ( mv.type == 'text_complex' ) %] [% IF mv.noclick %] [% ELSE %] [% IF mv.plugin == "upload.pl" %] - Upload + Upload [% ELSE %] - Tag editor + Tag editor [% END %] [% END %] @@ -1194,6 +1220,28 @@ function PopupMARCFieldDoc(field) { [% END %]
+ [% IF ( subfield_loo.history ) %] + + + History + + + + + + + + + + [% FOREACH hist IN subfield_loo.history %] + + + + + + [% END %] +
ValueDateUser
[% hist.data | html %][% hist.datetime | $KohaDates with_hours = 1 %][% hist.title | html %] [% hist.firstname | html %] [% hist.surname | html %]
+ [% END %] [% END # /FOREACH subfield_loop %] --- a/koha-tmpl/intranet-tmpl/prog/js/cataloging.js +++ a/koha-tmpl/intranet-tmpl/prog/js/cataloging.js @@ -58,6 +58,16 @@ function openAuth(tagsubfieldid,authtype,source) { window.open("../authorities/auth_finder.pl?source="+source+"&authtypecode="+authtype+"&index="+tagsubfieldid+"&value_mainstr="+encodeURIComponent(mainmainstring)+"&value_main="+encodeURIComponent(mainstring), "_blank",'width=700,height=550,toolbar=false,scrollbars=yes'); } +function HistoryToggle (tagid) +{ + $('#' + tagid).toggle(); +} + +function HistoryRollback (tagid, value) +{ + $('#' + tagid).val(value); +} + function ExpandField(index) { var original = document.getElementById(index); //original
  • var lis = original.getElementsByTagName('li'); --- a/t/db_dependent/BiblioHistory.t +++ a/t/db_dependent/BiblioHistory.t @@ -0,0 +1,249 @@ +#!/usr/bin/perl + +use Modern::Perl; +use t::lib::TestBuilder; +use t::lib::Mocks; +use Test::More tests => 6; +use POSIX qw(strftime); + +use MARC::Record; +use MARC::Field; +use JSON qw(encode_json); + +use Koha::MetadataRecord::History; + +BEGIN { + use_ok('C4::Biblio'); +} + +our $dbh = C4::Context->dbh; +$dbh->{AutoCommit} = 0; +$dbh->{RaiseError} = 0; + +$dbh->do(q|DELETE FROM issues|); +$dbh->do(q|DELETE FROM items|); +$dbh->do(q|DELETE FROM borrowers|); +$dbh->do(q|DELETE FROM branches|); +$dbh->do(q|DELETE FROM categories|); +$dbh->do(q|DELETE FROM biblioitems|); + +my $builder = t::lib::TestBuilder->new(); + +my $branch = $builder->build( + { + source => 'Branch', + } +); + +my $category = $builder->build( + { + source => 'Category', + } +); + +my $patron1 = $builder->build( + { + source => 'Borrower', + value => { + title => 'Mr/Mrs', + firstname => 'First name', + surname => 'Surname', + categorycode => $category->{categorycode}, + branchcode => $branch->{branchcode}, + }, + } +); + +my $patron2 = $builder->build( + { + source => 'Borrower', + value => { + title => 'Mr/Mrs 2', + firstname => 'First name 2', + surname => 'Surname 2', + categorycode => $category->{categorycode}, + branchcode => $branch->{branchcode}, + }, + } +); + +my $biblio = $builder->build_sample_biblio; + +C4::Context->_new_userenv('DUMMY_SESSION_ID'); +C4::Context->set_userenv( $patron1->{borrowernumber}, + $patron1->{userid}, 'usercnum', $patron1->{firstname}, $patron1->{surname}, + $branch->{branchcode}, 'My library', 0 ); + +# Item with no pre-existing history backlog. +my $record1 = MARC::Record->new(); +my $field1 = MARC::Field->new( + 245, '1', '0', + 'a' => 'First Title', + 'c' => 'Some dude' +); +$record1->append_fields($field1); + +my $record2 = MARC::Record->new(); +my $field2 = MARC::Field->new( + 245, '1', '0', + 'a' => 'Second Title', + 'c' => 'Former dude' +); +$record2->append_fields($field2); + +my $biblio_metadata1 = $builder->build( + { + source => 'BiblioMetadata', + value => { + metadata => $record1->as_xml(), + history => undef, + } + } +); + +# items{tagname}{subfield}{timestamp}{borrowernumber|timestamp|marcxml} +my $items = [ + { + borrowernumber => $patron1->{borrowernumber}, + datetime => '2012-08-12 12:00:10', + marcxml => $record1->as_xml(), + }, + { + borrowernumber => $patron2->{borrowernumber}, + datetime => '2012-08-12 12:00:10', + marcxml => $record2->as_xml(), + } +]; + +# Item with previous history entries. +my $biblio_metadata2 = $builder->build( + { + source => 'BiblioMetadata', + value => { + metadata => $record2->as_xml(), + history => encode_json($items), + } + } +); + +my $expected_history = { + 245 => { + a => [ + { + borrowernumber => $patron1->{borrowernumber}, + data => 'First Title', + datetime => '2012-08-12 12:00:10' + }, + { + borrowernumber => $patron2->{borrowernumber}, + data => 'Second Title', + datetime => '2012-08-12 12:00:10' + }, + ], + c => [ + { + borrowernumber => $patron1->{borrowernumber}, + data => 'Some dude', + datetime => '2012-08-12 12:00:10' + }, + { + borrowernumber => $patron2->{borrowernumber}, + data => 'Former dude', + datetime => '2012-08-12 12:00:10' + }, + ], + }, +}; + +my $expected_users = { + $patron1->{borrowernumber} => undef, + $patron2->{borrowernumber} => undef, +}; + +# Tests the decoding and reindexing of the data, without any borrower +# information implanted into the history itself. + +my ( $history, $users ) = C4::Biblio::HistoryParse( encode_json($items) ); + +is_deeply( $history, $expected_history, 'HistoryParse: Output' ); +is_deeply( $users, $expected_users, 'HistoryParse: Users' ); + +# Tests the extraction of full borrower details based on the borrowernumber. +my $expected_details = { + $patron1->{borrowernumber} => { + firstname => $patron1->{firstname}, + surname => $patron1->{surname}, + title => $patron1->{title}, + }, + $patron2->{borrowernumber} => { + firstname => $patron2->{firstname}, + surname => $patron2->{surname}, + title => $patron2->{title}, + } +}; + +my $details = HistoryBorrowerDetails($expected_users); +is_deeply( $details, $expected_details, 'HistoryBorrowerDetails: Output' ); + +{ + my $expected_mapped_history = { + 245 => { + a => [ + { + borrowernumber => $patron1->{borrowernumber}, + data => 'First Title', + firstname => $patron1->{firstname}, + surname => $patron1->{surname}, + title => $patron1->{title}, + datetime => '2012-08-12 12:00:10' + }, + { + borrowernumber => $patron2->{borrowernumber}, + data => 'Second Title', + firstname => $patron2->{firstname}, + surname => $patron2->{surname}, + title => $patron2->{title}, + datetime => '2012-08-12 12:00:10' + }, + ], + c => [ + { + borrowernumber => $patron1->{borrowernumber}, + data => 'Some dude', + firstname => $patron1->{firstname}, + surname => $patron1->{surname}, + title => $patron1->{title}, + datetime => '2012-08-12 12:00:10' + }, + { + borrowernumber => $patron2->{borrowernumber}, + data => 'Former dude', + firstname => $patron2->{firstname}, + surname => $patron2->{surname}, + title => $patron2->{title}, + datetime => '2012-08-12 12:00:10' + }, + ] + } + }; + + $history = C4::Biblio::HistoryMapUsers( $expected_history, $details ); + + is_deeply( $history, $expected_mapped_history, 'HistoryMapUsers: Output' ); + + #my $record; + #( $record, $history ) = GetMarcBiblioHistory( $biblioitem2->{biblionumber} ); + + #is_deeply( $record, $record2 ); + #is_deeply( $history, $expected_mapped_history ); +} + +# The C4::Context will provide the expected user borrowernumber. +my $record = HistoryRecordNew($record1); + +my $expected_record = { + borrowernumber => C4::Context->userenv()->{number}, + marcxml => $record1->as_xml_record(), + datetime => POSIX::strftime( '%Y-%m-%d %T', localtime ), +}; +is_deeply( $record, $expected_record ); --