From 8c33a31210e10692976306324e87f9bebf70cabb Mon Sep 17 00:00:00 2001 From: Martin Persson Date: Fri, 19 Jun 2015 21:36:27 +0200 Subject: [PATCH] Bug 14367: Add MARC record history This is a proof-of-concept implementation for adding history support to MARC records. Every time a change is made a complete copy of the record is stored along with the date/time and user identity. The changes are listed under each field in the MARC record editor and can be reverted with a click. The changes are stored as a JSON array in a new column named 'history' in the database. The array is re-read from the database before updating the record to prevent old data lingering in the session from overwriting newer changes made by other users. If we decide to implement this feature it might be better to simply create a new table altogether and link it rather than the clumsy JSON solution. That would eliminate a lot of bulky code that transforms MARC-KOHA-JSON and back while ensuring data integrity. Also, there are plans to add permissions to the MARC records; this likely requires more complex interactions that will scale badly with the current JSON solution. At present, the history is hardcoded to 10 entries. This can easily be made into a syspref. The current implementation should probably be refactored into a 'BiblioHistory' class before deploying. Documentation of the functions/methods are also needed. Icon is ugly and needs to be improved. Sponsored-By: Halland County Library Test plan: * Log into OPAC, search for a title, chose to edit it's MARC record. * Chose a MARC field and modify it, press Save. * Open the MARC editor again for the same title. * Next to the edited field a new icon should appear, looking like a clock face with a encircling arrow; the history icon. * Clicking the icon should open a table showing all changes done to the record, including value change, date/time and user name. --- C4/Biblio.pm | 243 +++++++++++++++++++- cataloguing/addbiblio.pl | 42 ++-- installer/data/mysql/atomicupdate/add_history.sql | 1 + koha-tmpl/intranet-tmpl/prog/en/css/addbiblio.css | 22 +- koha-tmpl/intranet-tmpl/prog/en/js/cataloging.js | 10 + .../prog/en/modules/cataloguing/addbiblio.tt | 29 ++- koha-tmpl/intranet-tmpl/prog/img/icon-history.png | Bin 0 -> 513 bytes t/db_dependent/BiblioHistory.t | 254 +++++++++++++++++++++ 8 files changed, 575 insertions(+), 26 deletions(-) create mode 100644 installer/data/mysql/atomicupdate/add_history.sql create mode 100644 koha-tmpl/intranet-tmpl/prog/img/icon-history.png create mode 100644 t/db_dependent/BiblioHistory.t diff --git a/C4/Biblio.pm b/C4/Biblio.pm index d90094e..23c03d8 100644 --- a/C4/Biblio.pm +++ b/C4/Biblio.pm @@ -29,6 +29,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::Dates qw/format_date/; @@ -78,6 +79,7 @@ BEGIN { &GetMarcISSN &GetMarcSubjects &GetMarcBiblio + &GetMarcBiblioHistory &GetMarcAuthors &GetMarcSeries &GetMarcHosts @@ -139,6 +141,15 @@ BEGIN { &TransformHtmlToXml prepare_host_field ); + + # History functions + push @EXPORT, qw( + &HistoryUpdate + &HistoryParse + &HistoryMapUsers + &HistoryBorrowerDetails + &HistoryRecordNew + ); } =head1 NAME @@ -266,8 +277,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, undef, $biblionumber, $frameworkcode ) unless $defer_marc_save; # update OAI-PMH sets if(C4::Context->preference("OAI-PMH:AutoUpdateSets")) { @@ -332,9 +343,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 ); @@ -345,7 +356,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, $history, $biblionumber, $frameworkcode ); # modify the other koha tables _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode ); @@ -1291,6 +1302,165 @@ sub GetMarcBiblio { } } +=head2 GetMarcBiblioHistory + + my ($record, $history) = GetMarcBiblio($biblionumber, [$embeditems]); + +Returns MARC::Record representing bib identified by +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) { + carp '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; + } +} +# Input format (JSON): [ { datetime, borrowernumber, marcxml } ] +# Output format: { tag } { subfield } [ { borrowernumber, data, datetime } ] +sub HistoryParse +{ + my $json = shift; + + if (!defined $json) { + carp('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 ); +} + +# Take a hashref with borrowernumbers as keys, returns new hash with the +# borrowernumber as key to hash containing firstname, surname and title. +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; +} + +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; +} + +sub HistoryParsedRecordNew +{ + my ($value, $borrowernumber, $datetime) = @_; + my $newrecord = { + data => $value, + borrowernumber => $borrowernumber, + datetime => $datetime, + }; + + return $newrecord; +} + =head2 GetXmlBiblio my $marcxml = GetXmlBiblio($biblionumber); @@ -3361,7 +3531,7 @@ 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 marc field - my ( $record, $biblionumber, $frameworkcode ) = @_; + my ( $record, $history, $biblionumber, $frameworkcode ) = @_; if ( !$record ) { carp 'ModBiblioMarc passed an undefined record'; return; @@ -3407,8 +3577,20 @@ sub ModBiblioMarc { $f005->update(sprintf("%4d%02d%02d%02d%02d%04.1f",@a)) if $f005; } - $sth = $dbh->prepare("UPDATE biblioitems SET marc=?,marcxml=? WHERE biblionumber=?"); - $sth->execute( $record->as_usmarc(), $record->as_xml_record($encoding), $biblionumber ); + # 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" ); return $biblionumber; @@ -3765,6 +3947,51 @@ sub RemoveAllNsb { return $record; } +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; +}; + +sub HistoryUpdate +{ + my ( $record, $limit, $collection ) = @_; + + if ( !$record ) { + carp('HistoryUpdate: Called with undefined record'); + return; + } + + if ( ref($record) ne 'HASH' ) { + carp('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; diff --git a/cataloguing/addbiblio.pl b/cataloguing/addbiblio.pl index 890db0f..5a89436 100755 --- a/cataloguing/addbiblio.pl +++ b/cataloguing/addbiblio.pl @@ -41,6 +41,8 @@ use MARC::File::USMARC; use MARC::File::XML; use URI::Escape; +use Data::Dumper; + if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) { MARC::File::XML->default_record_format('UNIMARC'); } @@ -291,10 +293,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}; + if (defined $taghistory ) { + warn "TAG HIST: $tag/$subfield", Dumper($taghistory); + } $value =~ s/"/"/g; # if there is no value provided but a default value in parameters, get it @@ -332,6 +339,7 @@ sub create_input { value => $value, maxlength => $tagslib->{$tag}->{$subfield}->{maxlength}, random => CreateKey(), + history => $taghistory, ); if(exists $mandatory_z3950->{$tag.$subfield}){ @@ -387,6 +395,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 @@ -408,6 +417,7 @@ sub create_input { maxlength => $subfield_data{maxlength}, javascript => $plugin->javascript, noclick => $plugin->noclick, + history => $taghistory, }; } else { warn $plugin->errstr; @@ -420,6 +430,7 @@ sub create_input { size => 67, maxlength => $subfield_data{maxlength}, readonly => 0, + history => $taghistory, }; } @@ -432,6 +443,7 @@ sub create_input { value => $value, size => 67, maxlength => $subfield_data{maxlength}, + history => $taghistory, }; } @@ -452,6 +464,7 @@ sub create_input { id => $subfield_data{id}, name => $subfield_data{id}, value => $value, + history => $taghistory, }; } @@ -464,6 +477,7 @@ sub create_input { size => 67, maxlength => $subfield_data{maxlength}, readonly => 0, + history => $taghistory, }; } @@ -489,11 +503,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 @@ -509,7 +523,7 @@ sub build_tabs { my @BIG_LOOP; my %seen; my @tab_data; # all tags to display - + #print STDERR "usedTagsLibs: ", Dumper(@$usedTagsLib); foreach my $used ( @$usedTagsLib ){ push @tab_data,$used->{tagfield} if not $seen{$used->{tagfield}}; $seen{$used->{tagfield}}++; @@ -546,7 +560,6 @@ sub build_tabs { } # loop through each field foreach my $field (@fields) { - my @subfields_data; if ( $tag < 10 ) { my ( $value, $subfield ); @@ -566,7 +579,7 @@ sub build_tabs { @subfields_data, &create_input( $tag, $subfield, $value, $index_tag, $tabloop, $record, - $authorised_values_sth,$input + $authorised_values_sth,$input, $history ) ); } @@ -581,7 +594,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 ) ); } @@ -609,7 +622,7 @@ sub build_tabs { @subfields_data, &create_input( $tag, $subfield, '', $index_tag, $tabloop, $record, - $authorised_values_sth,$input + $authorised_values_sth,$input, $history ) ); } @@ -655,11 +668,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 ) ); } @@ -676,7 +690,7 @@ sub build_tabs { tagfirstsubfield => $subfields_data[0], fixedfield => $tag < 10?1:0, ); - + push @loop_data, \%tag_data ; } } @@ -772,6 +786,7 @@ $mandatory_z3950 = GetMandatoryFieldZ3950($frameworkcode); # -- Global my $record = -1; +my $history = undef; my $encoding = ""; my ( $biblionumbertagfield, @@ -783,7 +798,7 @@ my ( ); if (($biblionumber) && !($breedingid)){ - $record = GetMarcBiblio($biblionumber); + ($record, $history) = GetMarcBiblioHistory($biblionumber); } if ($breedingid) { ( $record, $encoding ) = MARCfindbreeding( $breedingid ) ; @@ -917,7 +932,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, @@ -964,7 +979,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, diff --git a/installer/data/mysql/atomicupdate/add_history.sql b/installer/data/mysql/atomicupdate/add_history.sql new file mode 100644 index 0000000..4c5c424 --- /dev/null +++ b/installer/data/mysql/atomicupdate/add_history.sql @@ -0,0 +1 @@ +ALTER TABLE biblioitems ADD COLUMN `history` LONGTEXT DEFAULT NULL AFTER `marcxml`; diff --git a/koha-tmpl/intranet-tmpl/prog/en/css/addbiblio.css b/koha-tmpl/intranet-tmpl/prog/en/css/addbiblio.css index dfd36f5..2ded057 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/css/addbiblio.css +++ b/koha-tmpl/intranet-tmpl/prog/en/css/addbiblio.css @@ -16,6 +16,11 @@ text-decoration : none; } +.buttonHistory { + font-weight : bold; + text-decoration : none; +} + a.expandfield { text-decoration : none; } @@ -127,6 +132,7 @@ a.tagnum { .linktools a:first-child { border-bottom: 1px solid #DDD; } .linktools a:hover { background-color: #FFC; } .subfield_controls { margin : 0 .5em; } +.subfield_history { margin : 0 .5em; float: right; } .readonly { border-width : 1px; border-style: inset; padding-left : 15px; background: #EEE url(../../img/locked.png) center left no-repeat; width:29em; } #cataloguing_additem_itemlist { @@ -170,4 +176,18 @@ tr.active.highlight td { position: absolute; top: 50%; width: 15em; -} \ No newline at end of file +} + +.history_container { + display: none; +} + +.history_table { + width: 100%; + display: none; + float: right; + clear: both; + font-size: 75%; + width: 75%; + margin: 0.3em; +} diff --git a/koha-tmpl/intranet-tmpl/prog/en/js/cataloging.js b/koha-tmpl/intranet-tmpl/prog/en/js/cataloging.js index c23637f..6b4387f 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/js/cataloging.js +++ b/koha-tmpl/intranet-tmpl/prog/en/js/cataloging.js @@ -55,6 +55,16 @@ function openAuth(tagsubfieldid,authtype,source) { newin=window.open("../authorities/auth_finder.pl?source="+source+"&authtypecode="+authtype+"&index="+tagsubfieldid+"&value_mainstr="+encodeURI(mainmainstring)+"&value_main="+encodeURI(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 divs = original.getElementsByTagName('div'); diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt index 3bc00d5..fcf42d5 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt @@ -620,10 +620,13 @@ function Changefwk(FwkList) { [% IF ( mv.readonly == 1 ) %] [% ELSE %] - + [% END %] + [% IF ( mv.authtype ) %] - Tag editor + + Tag editor + [% END %] [% ELSIF ( mv.type == 'text_complex' ) %] @@ -631,7 +634,8 @@ function Changefwk(FwkList) { [% IF mv.noclick %] [% ELSE %] - Tag editor + + Tag editor [% END %] [% mv.javascript %] @@ -661,8 +665,25 @@ function Changefwk(FwkList) { [% END %] - + + [% IF ( subfield_loo.history ) %] + + + History + + + + + + [% FOREACH hist IN subfield_loo.history %] + + + + [% END %] +
ValueDateUser
[% hist.data %][% hist.datetime %][% hist.title %] [% hist.firstname %] [% hist.surname %]
+ [% END %]
+ [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/img/icon-history.png b/koha-tmpl/intranet-tmpl/prog/img/icon-history.png new file mode 100644 index 0000000000000000000000000000000000000000..c65d0c6750c915ec1fc7c47eae52a329d0358404 GIT binary patch literal 513 zcmV+c0{;DpP)JY z0A>feN^JlD010qNS#tmYF*N`HF*N}XOdPos*6keP$;J=l&he8UQs zGxKMSYlg`Yu^&s=fz6fv8opvSGanA{%@DbV+ZAaApGy_Hs`G1^c_||Ps^ECZ++Y>Q z@CGwjz&6~+fkL^FnHM{i>a9Y4fw9bdiA}hJEt&Z#GY=LqxDXM$J0<%Z#>+*o_#MO# z{I0B?!3T74wp&2w%Kj`f-}y(BKEXYl$4n<8#>;cTqlnm8aem=_T~T?ySLJuAgA 6; +use POSIX qw(strftime); + +use MARC::Record; +use MARC::Field; +use JSON qw(encode_json); + +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 ); -- 2.1.4