From cfcf59f83568528add1b133ba96d6e6c62ca63e0 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 & Catalyst IT Test plan: * Run updatedatabase.pl * Log into Staff interface, search for a title, choose 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. * Clicking a record in the history log should revert that field to the clicked value (a previous value) * Run prove t/db_dependent/BiblioHistory.t and confirm all tests pass --- C4/Biblio.pm | 33 ++- Koha/MetadataRecord/History.pm | 261 +++++++++++++++++++++ cataloguing/addbiblio.pl | 50 ++-- installer/data/mysql/atomicupdate/add_history.sql | 1 + koha-tmpl/intranet-tmpl/prog/css/addbiblio.css | 21 +- .../prog/en/modules/cataloguing/addbiblio.tt | 36 ++- 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 ++++++++++++++++++++ 9 files changed, 635 insertions(+), 33 deletions(-) create mode 100644 Koha/MetadataRecord/History.pm 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 7fb5c9a..7519fb2 100644 --- a/C4/Biblio.pm +++ b/C4/Biblio.pm @@ -88,6 +88,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 @@ -107,10 +108,10 @@ use Koha::ItemTypes; 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 @@ -226,8 +227,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")) { @@ -296,9 +297,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 ); @@ -309,7 +310,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 ); @@ -3273,8 +3274,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; @@ -3345,6 +3346,22 @@ sub ModBiblioMarc { $m_rs->metadata( $record->as_xml_record($encoding) ); $m_rs->store; + # 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; } diff --git a/Koha/MetadataRecord/History.pm b/Koha/MetadataRecord/History.pm new file mode 100644 index 0000000..2a4e04a --- /dev/null +++ b/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 diff --git a/cataloguing/addbiblio.pl b/cataloguing/addbiblio.pl index c4ed49d..8e0e9f6 100755 --- a/cataloguing/addbiblio.pl +++ b/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; @@ -271,10 +268,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{}; @@ -290,7 +292,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; @@ -312,6 +314,7 @@ sub create_input { value => $value, maxlength => $tagslib->{$tag}->{$subfield}->{maxlength}, random => CreateKey(), + history => $taghistory, ); if(exists $mandatory_z3950->{$tag.$subfield}){ @@ -367,6 +370,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 @@ -388,6 +392,7 @@ sub create_input { maxlength => $subfield_data{maxlength}, javascript => $plugin->javascript, noclick => $plugin->noclick, + history => $taghistory, }; } else { warn $plugin->errstr; @@ -400,6 +405,7 @@ sub create_input { size => 67, maxlength => $subfield_data{maxlength}, readonly => 0, + history => $taghistory, }; } @@ -412,6 +418,7 @@ sub create_input { value => $value, size => 67, maxlength => $subfield_data{maxlength}, + history => $taghistory, }; } @@ -432,6 +439,7 @@ sub create_input { id => $subfield_data{id}, name => $subfield_data{id}, value => $value, + history => $taghistory, }; } @@ -444,6 +452,7 @@ sub create_input { size => 67, maxlength => $subfield_data{maxlength}, readonly => 0, + history => $taghistory, }; } @@ -469,11 +478,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 @@ -489,7 +498,6 @@ sub build_tabs { my @BIG_LOOP; my %seen; my @tab_data; # all tags to display - foreach my $used ( @$usedTagsLib ){ push @tab_data,$used->{tagfield} if not $seen{$used->{tagfield}}; $seen{$used->{tagfield}}++; @@ -526,7 +534,6 @@ sub build_tabs { } # loop through each field foreach my $field (@fields) { - my @subfields_data; if ( $tag < 10 ) { my ( $value, $subfield ); @@ -546,7 +553,7 @@ sub build_tabs { @subfields_data, &create_input( $tag, $subfield, $value, $index_tag, $tabloop, $record, - $authorised_values_sth,$input + $authorised_values_sth,$input, $history ) ); } @@ -561,7 +568,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 ) ); } @@ -589,7 +596,7 @@ sub build_tabs { @subfields_data, &create_input( $tag, $subfield, '', $index_tag, $tabloop, $record, - $authorised_values_sth,$input + $authorised_values_sth,$input, $history ) ); } @@ -635,11 +642,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 ) ); } @@ -656,7 +664,7 @@ sub build_tabs { tagfirstsubfield => $subfields_data[0], fixedfield => $tag < 10?1:0, ); - + push @loop_data, \%tag_data ; } } @@ -755,6 +763,7 @@ $mandatory_z3950 = GetMandatoryFieldZ3950($frameworkcode); # -- Global my $record = -1; +my $history = undef; my $encoding = ""; my ( $biblionumbertagfield, @@ -765,7 +774,7 @@ my ( ); if (($biblionumber) && !($breedingid)){ - $record = GetMarcBiblio({ biblionumber => $biblionumber }); + ($record, $history) = GetMarcBiblioHistory($biblionumber); } if ($breedingid) { ( $record, $encoding ) = MARCfindbreeding( $breedingid ) ; @@ -894,7 +903,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, @@ -941,7 +950,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/css/addbiblio.css b/koha-tmpl/intranet-tmpl/prog/css/addbiblio.css index 5670a23..8d56a88 100644 --- a/koha-tmpl/intranet-tmpl/prog/css/addbiblio.css +++ b/koha-tmpl/intranet-tmpl/prog/css/addbiblio.css @@ -17,6 +17,11 @@ text-decoration : none; } +.buttonHistory { + font-weight : bold; + text-decoration : none; +} + a.expandfield { text-decoration : none; } @@ -126,6 +131,7 @@ 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; @@ -167,7 +173,6 @@ tbody tr.active td { width: 15em; } - @media (min-width: 768px) { div.subfield_line label { width: 20em; @@ -195,3 +200,17 @@ tbody tr.active td { width: 16em; } } + +.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/modules/cataloguing/addbiblio.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt index 41326ab..81b362d 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt @@ -19,6 +19,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(); @@ -680,10 +690,11 @@ function Changefwk() { [% IF ( mv.readonly == 1 ) %] [% ELSE %] - + [% END %] + [% IF ( mv.authtype ) %] - Tag editor + Tag editor [% END %] [% ELSIF ( mv.type == 'text_complex' ) %] @@ -691,7 +702,7 @@ function Changefwk() { [% IF mv.noclick %] [% ELSE %] - Tag editor + Tag editor [% END %] [% mv.javascript | $raw %] @@ -721,8 +732,25 @@ function Changefwk() { [% 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 var divs = original.getElementsByTagName('div'); diff --git a/t/db_dependent/BiblioHistory.t b/t/db_dependent/BiblioHistory.t new file mode 100644 index 0000000..c1a2c48 --- /dev/null +++ b/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 ); -- 2.1.4