@@ -, +, @@ 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 | 34 ++- Koha/MetadataRecord/History.pm | 261 +++++++++++++++++++++ cataloguing/addbiblio.pl | 49 ++-- koha-tmpl/intranet-tmpl/prog/css/addbiblio.css | 23 ++ .../prog/en/modules/cataloguing/addbiblio.tt | 38 ++- koha-tmpl/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 | 256 ++++++++++++++++++++ 8 files changed, 640 insertions(+), 31 deletions(-) create mode 100644 Koha/MetadataRecord/History.pm 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 @@ -85,6 +85,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; # logaction @@ -105,10 +106,10 @@ use Koha::Plugins; use Koha::SearchEngine; use Koha::Libraries; use Koha::Util::MARC; +use Koha::MetadataRecord::History; use vars qw($debug $cgi_debug); - =head1 NAME C4::Biblio - cataloging management functions @@ -224,8 +225,8 @@ sub AddBiblio { # update MARC subfield that stores biblioitems.cn_sort _koha_marc_update_biblioitem_cn_sort( $record, $olddata, $frameworkcode ); - # now add the record - ModBiblioMarc( $record, $biblionumber, $frameworkcode ) unless $defer_marc_save; + # now add the record (history parameter is undef: no history yet]) + ModBiblioMarc( $record, $biblionumber, $frameworkcode, undef ) unless $defer_marc_save; # update OAI-PMH sets if(C4::Context->preference("OAI-PMH:AutoUpdateSets")) { @@ -300,9 +301,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 biblioitemnumber,history from biblioitems where biblionumber=?"); $sth->execute($biblionumber); - my ($biblioitemnumber) = $sth->fetchrow; + my ($biblioitemnumber, $history) = $sth->fetchrow; $sth->finish(); _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber ); @@ -313,7 +314,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, $frameworkcode ); + &ModBiblioMarc( $record, $biblionumber, $frameworkcode, $history ); # modify the other koha tables _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode ); @@ -3075,8 +3076,8 @@ Function exported, but should NOT be used, unless you really know what you're do sub ModBiblioMarc { # pass the MARC::Record to this function, and it will create the records in - # the marcxml field - my ( $record, $biblionumber, $frameworkcode ) = @_; + # the marc field + my ( $record, $biblionumber, $frameworkcode, $history ) = @_; if ( !$record ) { carp 'ModBiblioMarc passed an undefined record'; return; @@ -3147,8 +3148,23 @@ sub ModBiblioMarc { $m_rs->metadata( $record->as_xml_record($encoding) ); $m_rs->store; - ModZebra( $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); + + $sth = $dbh->prepare("UPDATE biblioitems SET marc=?,marcxml=?,history=? WHERE biblionumber=?"); + $sth->execute( $record->as_usmarc(), $record->as_xml_record($encoding), $newhistory, $biblionumber ); + $sth->finish; + 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; +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 marcxml,history FROM biblioitems WHERE biblionumber=? "); + $sth->execute($biblionumber); + my $row = $sth->fetchrow_hashref; + my $marcxml = StripNonXmlChars( $row->{'marcxml'} ); + 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 undef; + } + 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 @@ -28,7 +28,6 @@ use C4::Biblio; use C4::Search; use C4::AuthoritiesMarc; use C4::Context; -use MARC::Record; use C4::Log; use C4::Koha; use C4::ClassSource; @@ -36,13 +35,11 @@ use C4::ImportBatch; use C4::Charset; use Koha::BiblioFrameworks; use Koha::DateUtils; - use Koha::ItemTypes; +use Koha::MetadataRecord::History; use Koha::Libraries; - -use Koha::BiblioFrameworks; - use Date::Calc qw(Today); +use MARC::Record; use MARC::File::USMARC; use MARC::File::XML; use URI::Escape; @@ -274,10 +271,15 @@ sub GetMandatoryFieldZ3950 { =cut sub create_input { - my ( $tag, $subfield, $value, $index_tag, $tabloop, $rec, $authorised_values_sth,$cgi ) = @_; - + + my ( $tag, $subfield, $value, $index_tag, $tabloop, $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; + # if there is no value provided but a default value in parameters, get it if ( $value eq '' ) { $value = $tagslib->{$tag}->{$subfield}->{defaultvalue} // q{}; @@ -295,7 +297,7 @@ sub create_input { # And <> with surname (?) my $username=(C4::Context->userenv?C4::Context->userenv->{'surname'}:"superlibrarian"); $value=~s/<>/$username/g; - + } my $dbh = C4::Context->dbh; @@ -318,6 +320,7 @@ sub create_input { value => $value, maxlength => $tagslib->{$tag}->{$subfield}->{maxlength}, random => CreateKey(), + history => $taghistory, ); if(exists $mandatory_z3950->{$tag.$subfield}){ @@ -375,6 +378,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 @@ -397,6 +401,7 @@ sub create_input { javascript => $plugin->javascript, plugin => $plugin->name, noclick => $plugin->noclick, + history => $taghistory, }; } else { warn $plugin->errstr; @@ -409,6 +414,7 @@ sub create_input { size => 67, maxlength => $subfield_data{maxlength}, readonly => 0, + history => $taghistory, }; } @@ -421,6 +427,7 @@ sub create_input { value => $value, size => 67, maxlength => $subfield_data{maxlength}, + history => $taghistory, }; } @@ -441,6 +448,7 @@ sub create_input { id => $subfield_data{id}, name => $subfield_data{id}, value => $value, + history => $taghistory, }; } @@ -453,6 +461,7 @@ sub create_input { size => 67, maxlength => $subfield_data{maxlength}, readonly => 0, + history => $taghistory, }; } @@ -478,11 +487,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 @@ -538,7 +547,6 @@ sub build_tabs { } # loop through each field foreach my $field (@fields) { - my @subfields_data; if ( $tag < 10 ) { my ( $value, $subfield ); @@ -558,7 +566,7 @@ sub build_tabs { @subfields_data, &create_input( $tag, $subfield, $value, $index_tag, $tabloop, $record, - $authorised_values_sth,$input + $authorised_values_sth,$input, $history ) ); } @@ -573,7 +581,7 @@ sub build_tabs { @subfields_data, &create_input( $tag, $subfield, $value, $index_tag, $tabloop, - $record, $authorised_values_sth,$input + $record, $authorised_values_sth,$input, $history ) ); } @@ -601,7 +609,7 @@ sub build_tabs { @subfields_data, &create_input( $tag, $subfield, '', $index_tag, $tabloop, $record, - $authorised_values_sth,$input + $authorised_values_sth,$input, $history ) ); } @@ -648,11 +656,12 @@ sub build_tabs { # always include in the form regardless of the hidden setting - bug 2206 next if ( $tagslib->{$tag}->{$subfield}->{tab} ne $tabloop ); + push( @subfields_data, &create_input( $tag, $subfield, '', $index_tag, $tabloop, $record, - $authorised_values_sth,$input + $authorised_values_sth,$input, $history ) ); } @@ -670,7 +679,7 @@ sub build_tabs { tagfirstsubfield => $subfields_data[0], fixedfield => $tag < 10?1:0, ); - + push @loop_data, \%tag_data ; } } @@ -772,6 +781,7 @@ $mandatory_z3950 = GetMandatoryFieldZ3950($frameworkcode); # -- Global my $record = -1; +my $history = undef; my $encoding = ""; my ( $biblionumbertagfield, @@ -782,7 +792,7 @@ my ( ); if (($biblionumber) && !($breedingid)){ - $record = GetMarcBiblio({ biblionumber => $biblionumber }); + ($record, $history) = GetMarcBiblioHistory($biblionumber); } if ($breedingid) { ( $record, $encoding ) = MARCfindbreeding( $breedingid ) ; @@ -911,7 +921,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, @@ -958,7 +968,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/koha-tmpl/intranet-tmpl/prog/css/addbiblio.css +++ a/koha-tmpl/intranet-tmpl/prog/css/addbiblio.css @@ -80,6 +80,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; } @@ -195,6 +200,9 @@ a.tagnum { .linktools a:hover { background-color: #FFC; } .subfield_controls { 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; } @@ -222,6 +230,7 @@ tbody tr.active td { width: 100%; z-index: 1000; } + #loading div { background : transparent url(../img/loading.gif) top left no-repeat; font-size : 175%; @@ -353,3 +362,17 @@ tbody tr.active td { .ui-autocomplete { z-index: 2000; } + +.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 @@ -22,6 +22,16 @@ var Sticky; $(document).ready(function() { + $(".input_marceditor").click(function(this){ + historyToggle($(this).attr('id') + '_history'); + return false; + }); + + $(".tag_editor").click(function(this){ + openAuth(this.parentNode.parentNode.getElementsByTagName('input')[1].id, $(this).attr('id'), 'biblio'); + return false; + }); + [% IF bib_doesnt_exist %] $("#addbibliotabs").hide(); $("#toolbar").hide(); @@ -832,7 +842,7 @@ [% END %] [% IF ( mv.authtype ) %] - Tag editor + Tag editor [% END %] [% ELSIF ( mv.type == 'text_complex' ) %] @@ -841,9 +851,9 @@ [% ELSE %] [% IF mv.plugin == "upload.pl" %] - Upload + Upload [% ELSE %] - Tag editor + Tag editor [% END %] [% END %] @@ -882,6 +892,28 @@ [% END %] + + [% IF ( subfield_loo.history ) %] + + + History + + + + + + + + + [% FOREACH hist IN subfield_loo.history %] + + + + + + [% END %] +
ValueDate
[% hist.data %][% hist.datetime %][% hist.title %] [% hist.firstname %] [% hist.surname %]
+ [% END %] [% END # /FOREACH subfield_loop %] --- a/koha-tmpl/intranet-tmpl/prog/js/cataloging.js +++ a/koha-tmpl/intranet-tmpl/prog/js/cataloging.js @@ -57,6 +57,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,256 @@ +#!/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( + { + source => 'Biblio', + value => { + branchcode => $branch->{branchcode}, + }, + } +); + +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 $biblioitem1 = $builder->build( + { + source => 'Biblioitem', + value => { + marcxml => $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 $biblioitem2 = $builder->build( + { + source => 'Biblioitem', + value => { + marcxml => $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 ); --