View | Details | Raw Unified | Return to bug 14367
Collapse All | Expand All

(-)a/C4/Biblio.pm (-7 / +23 lines)
Lines 85-90 use MARC::File::USMARC; Link Here
85
use MARC::File::XML;
85
use MARC::File::XML;
86
use POSIX qw(strftime);
86
use POSIX qw(strftime);
87
use Module::Load::Conditional qw(can_load);
87
use Module::Load::Conditional qw(can_load);
88
use JSON qw(encode_json decode_json);
88
89
89
use C4::Koha;
90
use C4::Koha;
90
use C4::Log;    # logaction
91
use C4::Log;    # logaction
Lines 106-115 use Koha::SearchEngine; Link Here
106
use Koha::SearchEngine::Indexer;
107
use Koha::SearchEngine::Indexer;
107
use Koha::Libraries;
108
use Koha::Libraries;
108
use Koha::Util::MARC;
109
use Koha::Util::MARC;
110
use Koha::MetadataRecord::History;
109
111
110
use vars qw($debug $cgi_debug);
112
use vars qw($debug $cgi_debug);
111
113
112
113
=head1 NAME
114
=head1 NAME
114
115
115
C4::Biblio - cataloging management functions
116
C4::Biblio - cataloging management functions
Lines 284-291 sub AddBiblio { Link Here
284
            # update MARC subfield that stores biblioitems.cn_sort
285
            # update MARC subfield that stores biblioitems.cn_sort
285
            _koha_marc_update_biblioitem_cn_sort( $record, $olddata, $frameworkcode );
286
            _koha_marc_update_biblioitem_cn_sort( $record, $olddata, $frameworkcode );
286
287
287
            # now add the record
288
            # now add the record (history parameter is undef: no history yet)
288
            ModBiblioMarc( $record, $biblionumber ) unless $defer_marc_save;
289
            ModBiblioMarc( $record, $biblionumber, undef ) unless $defer_marc_save;
289
290
290
            # update OAI-PMH sets
291
            # update OAI-PMH sets
291
            if(C4::Context->preference("OAI-PMH:AutoUpdateSets")) {
292
            if(C4::Context->preference("OAI-PMH:AutoUpdateSets")) {
Lines 365-373 sub ModBiblio { Link Here
365
    # update biblionumber and biblioitemnumber in MARC
366
    # update biblionumber and biblioitemnumber in MARC
366
    # FIXME - this is assuming a 1 to 1 relationship between
367
    # FIXME - this is assuming a 1 to 1 relationship between
367
    # biblios and biblioitems
368
    # biblios and biblioitems
368
    my $sth = $dbh->prepare("select biblioitemnumber from biblioitems where biblionumber=?");
369
    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=?");
369
    $sth->execute($biblionumber);
370
    $sth->execute($biblionumber);
370
    my ($biblioitemnumber) = $sth->fetchrow;
371
    my ($biblioitemnumber, $history) = $sth->fetchrow;
371
    $sth->finish();
372
    $sth->finish();
372
    _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
373
    _koha_marc_update_bib_ids( $record, $frameworkcode, $biblionumber, $biblioitemnumber );
373
374
Lines 378-384 sub ModBiblio { Link Here
378
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
379
    _koha_marc_update_biblioitem_cn_sort( $record, $oldbiblio, $frameworkcode );
379
380
380
    # update the MARC record (that now contains biblio and items) with the new record data
381
    # update the MARC record (that now contains biblio and items) with the new record data
381
    &ModBiblioMarc( $record, $biblionumber );
382
    &ModBiblioMarc( $record, $biblionumber, $history );
382
383
383
    # modify the other koha tables
384
    # modify the other koha tables
384
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
385
    _koha_modify_biblio( $dbh, $oldbiblio, $frameworkcode );
Lines 3007-3013 Function exported, but should NOT be used, unless you really know what you're do Link Here
3007
sub ModBiblioMarc {
3008
sub ModBiblioMarc {
3008
    # pass the MARC::Record to this function, and it will create the records in
3009
    # pass the MARC::Record to this function, and it will create the records in
3009
    # the marcxml field
3010
    # the marcxml field
3010
    my ( $record, $biblionumber ) = @_;
3011
    my ( $record, $biblionumber, $history ) = @_;
3011
    if ( !$record ) {
3012
    if ( !$record ) {
3012
        carp 'ModBiblioMarc passed an undefined record';
3013
        carp 'ModBiblioMarc passed an undefined record';
3013
        return;
3014
        return;
Lines 3075-3080 sub ModBiblioMarc { Link Here
3075
    my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
3076
    my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
3076
    $indexer->index_records( $biblionumber, "specialUpdate", "biblioserver" );
3077
    $indexer->index_records( $biblionumber, "specialUpdate", "biblioserver" );
3077
3078
3079
    # Decode JSON history, create new record, update and re-encode to JSON.
3080
    my $historylength = 10;
3081
    eval {
3082
        $history = decode_json($history);
3083
        1;
3084
    } or do {
3085
        $history = undef;
3086
    };
3087
    my $newrecord = HistoryRecordNew($record);
3088
    my $newhistory = HistoryUpdate($newrecord, $historylength, $history);
3089
    $newhistory = encode_json($newhistory);
3090
3091
    $m_rs->update({ metadata => $record->as_xml_record($encoding), history => $newhistory });
3092
3093
    ModZebra( $biblionumber, "specialUpdate", "biblioserver", $record );
3078
    return $biblionumber;
3094
    return $biblionumber;
3079
}
3095
}
3080
3096
(-)a/Koha/MetadataRecord/History.pm (+261 lines)
Line 0 Link Here
1
package Koha::MetadataRecord::History;
2
3
# Copyright 2016 Aleisha Amohia <aleisha@catalyst.net.nz>
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use strict;
21
use warnings;
22
23
use C4::Charset;
24
use JSON qw(decode_json);
25
26
use vars qw(@ISA @EXPORT);
27
28
BEGIN {
29
    require Exporter;
30
    @ISA = qw( Exporter );
31
32
    push @EXPORT, qw(
33
        &GetMarcBiblioHistory
34
        &HistoryUpdate
35
        &HistoryParse
36
        &HistoryMapUsers
37
        &HistoryBorrowerDetails
38
        &HistoryRecordNew
39
    );
40
 }
41
42
=head1 NAME
43
44
Koha::MetadataRecord::History - tracking changes to MARC records
45
46
=head1 DESCRIPTION
47
48
Koha::MetadataRecord::History not only tracks changes to MARC records and presents them in a log, but also allows users to
49
roll them back, either in batch to a previous point in the history or separately for each field (so you can roll back changes
50
to a field without changing fields that may have been changed after this one).
51
52
=head2 GetMarcBiblioHistory
53
54
    my ($record, $history) = GetMarcBiblio($biblionumber, [$embeditems]);
55
56
Returns MARC::Record representing bib identified by C<$biblionumber>.  If no bib exists, returns undef.
57
C<$biblionumber>.  If no bib exists, returns undef.
58
C<$embeditems>.  If set to true, items data are included.
59
The MARC record contains biblio data, and items data if $embeditems is set to true.
60
61
=cut
62
63
sub GetMarcBiblioHistory {
64
    my $biblionumber = shift;
65
    my $embeditems   = shift || 0;
66
    if (not defined $biblionumber) {
67
        warn "GetMarcBiblio called with undefined biblionumber";
68
        return;
69
    }
70
    my $dbh = C4::Context->dbh;
71
    my $sth = $dbh->prepare("SELECT metadata,history FROM biblio_metadata WHERE biblionumber=? ");
72
    $sth->execute($biblionumber);
73
    my $row = $sth->fetchrow_hashref;
74
    my $marcxml = StripNonXmlChars( $row->{'metadata'} );
75
    MARC::File::XML->default_record_format( C4::Context->preference('marcflavour') );
76
    my $record = MARC::Record->new();
77
    my ($history, $borrowers) = HistoryParse( $row->{'history'} );
78
    $borrowers = HistoryBorrowerDetails( $borrowers );
79
    $history = HistoryMapUsers( $history, $borrowers );
80
    if ($marcxml) {
81
        $record = eval { MARC::Record::new_from_xml( $marcxml, "utf8", C4::Context->preference('marcflavour') ) };
82
        if ($@) { warn " problem with :$biblionumber : $@ \n$marcxml"; }
83
            return unless $record;
84
            C4::Biblio::_koha_marc_update_bib_ids($record, '', $biblionumber, $biblionumber);
85
            C4::Biblio::EmbedItemsInMarcBiblio($record, $biblionumber) if ($embeditems);
86
            return ($record, $history);
87
    } else {
88
        return;
89
    }
90
}
91
92
=head2 HistoryParse
93
94
Input format (JSON): [ { datetime, borrowernumber, marcxml } ]
95
Output format: { tag } { subfield } [ { borrowernumber, data, datetime } ]
96
97
=cut
98
99
sub HistoryParse {
100
    my $json = shift;
101
    if (!defined $json) {
102
        warn "HistoryParse: Missing JSON parameter";
103
        return;
104
    }
105
    my $input = decode_json($json);
106
    # Reindex the dataset; order all values by datetime under each tag name.
107
    my $borrowers = { };
108
    my $output = { };
109
    for my $item (@{$input}) {
110
        my $datetime = $item->{datetime};
111
        $borrowers->{ $item->{borrowernumber }} = undef;
112
        my $record = MARC::File::XML::decode($item->{marcxml});
113
        for my $field ($record->fields()) {
114
            my $tag = $field->tag();
115
            $output->{$tag} = {} unless defined $output->{$tag};
116
            for my $subfield ($field->subfields()) {
117
                # Build hash ref structure.
118
                my $subfield_tag = @{$subfield}[0];
119
                $output->{$tag}->{$subfield_tag} = []
120
                unless defined $output->{$tag}->{$subfield_tag};
121
                push( @{ $output->{$tag}->{$subfield_tag} }, HistoryParsedRecordNew(
122
                    @{ $subfield }[1], $item->{borrowernumber}, $datetime));
123
                }
124
            }
125
        }
126
    return ( $output, $borrowers );
127
}
128
129
=head2 HistoryBorrowerDetails
130
131
Take a hashref with borrowernumbers as keys, returns new hash with the
132
borrowernumber as key to hash containing firstname, surname and title.
133
134
=cut
135
136
sub HistoryBorrowerDetails {
137
    my $borrowernumbers = shift;
138
    my $dbh = C4::Context->dbh;
139
    my $output = { };
140
    my @numbers = keys %{$borrowernumbers};
141
    if (@numbers > 0) {
142
        # We now need to look up the numeric user id's and map to name and title strings.
143
        my $sth = $dbh->prepare(qq{SELECT
144
            borrowernumber, title, firstname, surname
145
            FROM borrowers
146
            WHERE borrowernumber IN}
147
            . '(' . join(',', ('?') x @numbers) . ')');
148
        $sth->execute(@numbers)
149
            or die ('BorrowerDetails: Unable to execute SELECT statement');
150
        while (my $row = $sth->fetchrow_hashref()) {
151
            $output->{ $row->{borrowernumber} } = { };
152
            $output->{ $row->{borrowernumber} }->{title}     = $row->{title};
153
            $output->{ $row->{borrowernumber} }->{firstname} = $row->{firstname};
154
            $output->{ $row->{borrowernumber} }->{surname}   = $row->{surname};
155
        }
156
        $sth->finish();
157
    }
158
    return $output;
159
}
160
161
=head2 HistoryMapUsers
162
163
=cut
164
165
sub HistoryMapUsers {
166
    my ($history, $borrowers) = @_;
167
    my $mappedhistory = { };
168
    for my $tag (keys %{ $history }) {
169
        $mappedhistory->{ $tag } = { };
170
        for my $field (keys %{ $history->{ $tag } }) {
171
            $mappedhistory->{ $tag }->{ $field } = [ ];
172
            for my $entry (@{ $history->{ $tag }->{ $field } }) {
173
                # C4::Dates does not allow me to print time, only dates.
174
                #my $newtime = C4::Dates->new( $history->{ $tag }->{ $field }->{ $time }->{timestamp}, 'iso' )->output('syspref');
175
                my $newitem = { };
176
                my $user = $borrowers->{ $entry->{borrowernumber} };
177
178
                # Maybe change this to work on original instead of copy?
179
                $newitem->{title}          = $user->{title};
180
                $newitem->{firstname}      = $user->{firstname};
181
                $newitem->{surname}        = $user->{surname};
182
                $newitem->{borrowernumber} = $entry->{borrowernumber};
183
                $newitem->{data}           = $entry->{data};
184
                $newitem->{datetime}       = $entry->{datetime};
185
186
                push ( @{ $mappedhistory->{ $tag }->{ $field } } , $newitem);
187
            }
188
        }
189
    }
190
    return $mappedhistory;
191
}
192
193
=head2 HistoryParsedRecordNew
194
195
=cut
196
197
sub HistoryParsedRecordNew {
198
    my ($value, $borrowernumber, $datetime) = @_;
199
    my $newrecord = {
200
        data           => $value,
201
        borrowernumber => $borrowernumber,
202
        datetime       => $datetime,
203
    };
204
    return $newrecord;
205
}
206
207
=head2 HistoryRecordNew
208
209
=cut
210
211
sub HistoryRecordNew {
212
    my ( $record ) = @_;
213
    my $encoding = C4::Context->preference("marcflavour");
214
    my $hash_record = {
215
        borrowernumber => C4::Context->userenv()->{number},
216
        datetime       => POSIX::strftime('%Y-%m-%d %T', localtime(time)),
217
        marcxml        => $record->as_xml_record($encoding),
218
    };
219
    return $hash_record;
220
}
221
222
=head2 HistoryUpdate
223
224
=cut
225
226
sub HistoryUpdate {
227
    my ( $record, $limit, $collection ) = @_;
228
    if ( !$record ) {
229
        warn "HistoryUpdate: Called with undefined record";
230
        return;
231
    }
232
    if ( ref($record) ne 'HASH' ) {
233
        warn "HistoryUpdate: Record parameter is not a hashref";
234
        return;
235
    }
236
    $limit = 10 unless defined $limit;
237
    # We need something to append history to, even if there is none.
238
    $collection = [] unless defined $collection;
239
    # Truncate history to $histsize - 1, need space for new entry.
240
    splice(@{$collection}, $limit - 1);
241
    push(@{$collection}, $record);
242
    # Sort the resulting array, newest entries first.
243
    # Removed; used to be integer timestamps. Is now MySQL date strings.
244
    #@{$collection} = sort { $a->{timestamp} < $b->{timestamp} } @{$collection};
245
    @{$collection} = sort { $a->{datetime} lt $b->{datetime} } @{$collection};
246
    return $collection;
247
}
248
249
1;
250
251
__END__
252
253
=head1 AUTHOR
254
255
Koha Development Team <http://koha-community.org/>
256
257
Paul POULAIN paul.poulain@free.fr
258
259
Joshua Ferraro jmf@liblime.com
260
261
=cut
(-)a/cataloguing/addbiblio.pl (-20 / +31 lines)
Lines 28-34 use C4::Biblio; Link Here
28
use C4::Search;
28
use C4::Search;
29
use C4::AuthoritiesMarc;
29
use C4::AuthoritiesMarc;
30
use C4::Context;
30
use C4::Context;
31
use MARC::Record;
32
use C4::Log;
31
use C4::Log;
33
use C4::Koha;
32
use C4::Koha;
34
use C4::ClassSource;
33
use C4::ClassSource;
Lines 36-48 use C4::ImportBatch; Link Here
36
use C4::Charset;
35
use C4::Charset;
37
use Koha::BiblioFrameworks;
36
use Koha::BiblioFrameworks;
38
use Koha::DateUtils;
37
use Koha::DateUtils;
39
40
use Koha::ItemTypes;
38
use Koha::ItemTypes;
39
use Koha::MetadataRecord::History;
41
use Koha::Libraries;
40
use Koha::Libraries;
42
43
use Koha::BiblioFrameworks;
44
45
use Date::Calc qw(Today);
41
use Date::Calc qw(Today);
42
use MARC::Record;
46
use MARC::File::USMARC;
43
use MARC::File::USMARC;
47
use MARC::File::XML;
44
use MARC::File::XML;
48
use URI::Escape;
45
use URI::Escape;
Lines 274-283 sub GetMandatoryFieldZ3950 { Link Here
274
=cut
271
=cut
275
272
276
sub create_input {
273
sub create_input {
277
    my ( $tag, $subfield, $value, $index_tag, $tabloop, $rec, $authorised_values_sth,$cgi ) = @_;
274
278
    
275
    my ( $tag, $subfield, $value, $index_tag, $tabloop, $rec, $authorised_values_sth,$cgi, $history ) = @_;
276
279
    my $index_subfield = CreateKey(); # create a specifique key for each subfield
277
    my $index_subfield = CreateKey(); # create a specifique key for each subfield
280
278
279
    my $taghistory = $history->{$tag}->{$subfield};
280
281
    $value =~ s/"/&quot;/g;
282
281
    # if there is no value provided but a default value in parameters, get it
283
    # if there is no value provided but a default value in parameters, get it
282
    if ( $value eq '' ) {
284
    if ( $value eq '' ) {
283
        $value = $tagslib->{$tag}->{$subfield}->{defaultvalue} // q{};
285
        $value = $tagslib->{$tag}->{$subfield}->{defaultvalue} // q{};
Lines 295-301 sub create_input { Link Here
295
        # And <<USER>> with surname (?)
297
        # And <<USER>> with surname (?)
296
        my $username=(C4::Context->userenv?C4::Context->userenv->{'surname'}:"superlibrarian");
298
        my $username=(C4::Context->userenv?C4::Context->userenv->{'surname'}:"superlibrarian");
297
        $value=~s/<<USER>>/$username/g;
299
        $value=~s/<<USER>>/$username/g;
298
    
300
299
    }
301
    }
300
    my $dbh = C4::Context->dbh;
302
    my $dbh = C4::Context->dbh;
301
303
Lines 318-323 sub create_input { Link Here
318
        value          => $value,
320
        value          => $value,
319
        maxlength      => $tagslib->{$tag}->{$subfield}->{maxlength},
321
        maxlength      => $tagslib->{$tag}->{$subfield}->{maxlength},
320
        random         => CreateKey(),
322
        random         => CreateKey(),
323
        history        => $taghistory,
321
    );
324
    );
322
325
323
    if(exists $mandatory_z3950->{$tag.$subfield}){
326
    if(exists $mandatory_z3950->{$tag.$subfield}){
Lines 375-380 sub create_input { Link Here
375
            maxlength => $subfield_data{maxlength},
378
            maxlength => $subfield_data{maxlength},
376
            readonly  => ($is_readonly) ? 1 : 0,
379
            readonly  => ($is_readonly) ? 1 : 0,
377
            authtype  => $tagslib->{$tag}->{$subfield}->{authtypecode},
380
            authtype  => $tagslib->{$tag}->{$subfield}->{authtypecode},
381
            history   => $taghistory,
378
        };
382
        };
379
383
380
    # it's a plugin field
384
    # it's a plugin field
Lines 397-402 sub create_input { Link Here
397
                javascript     => $plugin->javascript,
401
                javascript     => $plugin->javascript,
398
                plugin         => $plugin->name,
402
                plugin         => $plugin->name,
399
                noclick        => $plugin->noclick,
403
                noclick        => $plugin->noclick,
404
                history        => $taghistory,
400
            };
405
            };
401
        } else {
406
        } else {
402
            warn $plugin->errstr;
407
            warn $plugin->errstr;
Lines 409-414 sub create_input { Link Here
409
                size      => 67,
414
                size      => 67,
410
                maxlength => $subfield_data{maxlength},
415
                maxlength => $subfield_data{maxlength},
411
                readonly  => 0,
416
                readonly  => 0,
417
                history   => $taghistory,
412
            };
418
            };
413
        }
419
        }
414
420
Lines 421-426 sub create_input { Link Here
421
            value     => $value,
427
            value     => $value,
422
            size      => 67,
428
            size      => 67,
423
            maxlength => $subfield_data{maxlength},
429
            maxlength => $subfield_data{maxlength},
430
            history   => $taghistory,
424
        };
431
        };
425
432
426
    }
433
    }
Lines 441-446 sub create_input { Link Here
441
                id        => $subfield_data{id},
448
                id        => $subfield_data{id},
442
                name      => $subfield_data{id},
449
                name      => $subfield_data{id},
443
                value     => $value,
450
                value     => $value,
451
                history   => $taghistory,
444
            };
452
            };
445
453
446
        }
454
        }
Lines 453-458 sub create_input { Link Here
453
                size      => 67,
461
                size      => 67,
454
                maxlength => $subfield_data{maxlength},
462
                maxlength => $subfield_data{maxlength},
455
                readonly  => 0,
463
                readonly  => 0,
464
                history   => $taghistory,
456
            };
465
            };
457
466
458
        }
467
        }
Lines 478-488 sub format_indicator { Link Here
478
}
487
}
479
488
480
sub build_tabs {
489
sub build_tabs {
481
    my ( $template, $record, $dbh, $encoding,$input ) = @_;
490
    my ( $template, $record, $dbh, $encoding, $input, $history ) = @_;
482
491
483
    # fill arrays
492
    # fill arrays
484
    my @loop_data = ();
493
    my @loop_data = ();
485
    my $tag;
494
    #my $tag;
486
495
487
    my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
496
    my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
488
    my $query = "SELECT authorised_value, lib
497
    my $query = "SELECT authorised_value, lib
Lines 538-544 sub build_tabs { Link Here
538
		}
547
		}
539
		# loop through each field
548
		# loop through each field
540
                foreach my $field (@fields) {
549
                foreach my $field (@fields) {
541
                    
542
                    my @subfields_data;
550
                    my @subfields_data;
543
                    if ( $tag < 10 ) {
551
                    if ( $tag < 10 ) {
544
                        my ( $value, $subfield );
552
                        my ( $value, $subfield );
Lines 558-564 sub build_tabs { Link Here
558
                            @subfields_data,
566
                            @subfields_data,
559
                            &create_input(
567
                            &create_input(
560
                                $tag, $subfield, $value, $index_tag, $tabloop, $record,
568
                                $tag, $subfield, $value, $index_tag, $tabloop, $record,
561
                                $authorised_values_sth,$input
569
                                $authorised_values_sth,$input, $history
562
                            )
570
                            )
563
                        );
571
                        );
564
                    }
572
                    }
Lines 573-579 sub build_tabs { Link Here
573
                                @subfields_data,
581
                                @subfields_data,
574
                                &create_input(
582
                                &create_input(
575
                                    $tag, $subfield, $value, $index_tag, $tabloop,
583
                                    $tag, $subfield, $value, $index_tag, $tabloop,
576
                                    $record, $authorised_values_sth,$input
584
                                    $record, $authorised_values_sth,$input, $history
577
                                )
585
                                )
578
                            );
586
                            );
579
                        }
587
                        }
Lines 601-607 sub build_tabs { Link Here
601
                            @subfields_data,
609
                            @subfields_data,
602
                            &create_input(
610
                            &create_input(
603
                                $tag, $subfield, '', $index_tag, $tabloop, $record,
611
                                $tag, $subfield, '', $index_tag, $tabloop, $record,
604
                                $authorised_values_sth,$input
612
                                $authorised_values_sth,$input, $history
605
                            )
613
                            )
606
                        );
614
                        );
607
                    }
615
                    }
Lines 648-658 sub build_tabs { Link Here
648
                           # always include in the form regardless of the hidden setting - bug 2206
656
                           # always include in the form regardless of the hidden setting - bug 2206
649
                    next
657
                    next
650
                      if ( $tagslib->{$tag}->{$subfield}->{tab} ne $tabloop );
658
                      if ( $tagslib->{$tag}->{$subfield}->{tab} ne $tabloop );
659
651
			push(
660
			push(
652
                        @subfields_data,
661
                        @subfields_data,
653
                        &create_input(
662
                        &create_input(
654
                            $tag, $subfield, '', $index_tag, $tabloop, $record,
663
                            $tag, $subfield, '', $index_tag, $tabloop, $record,
655
                            $authorised_values_sth,$input
664
                            $authorised_values_sth,$input, $history
656
                        )
665
                        )
657
                    );
666
                    );
658
                }
667
                }
Lines 670-676 sub build_tabs { Link Here
670
                        tagfirstsubfield => $subfields_data[0],
679
                        tagfirstsubfield => $subfields_data[0],
671
                        fixedfield       => $tag < 10?1:0,
680
                        fixedfield       => $tag < 10?1:0,
672
                    );
681
                    );
673
                    
682
674
                    push @loop_data, \%tag_data ;
683
                    push @loop_data, \%tag_data ;
675
                }
684
                }
676
            }
685
            }
Lines 772-777 $mandatory_z3950 = GetMandatoryFieldZ3950($frameworkcode); Link Here
772
# -- Global
781
# -- Global
773
782
774
my $record   = -1;
783
my $record   = -1;
784
my $history  = undef;
775
my $encoding = "";
785
my $encoding = "";
776
my (
786
my (
777
	$biblionumbertagfield,
787
	$biblionumbertagfield,
Lines 782-788 my ( Link Here
782
);
792
);
783
793
784
if (($biblionumber) && !($breedingid)){
794
if (($biblionumber) && !($breedingid)){
785
    $record = GetMarcBiblio({ biblionumber => $biblionumber });
795
    ($record, $history) = GetMarcBiblioHistory($biblionumber);
786
}
796
}
787
if ($breedingid) {
797
if ($breedingid) {
788
    ( $record, $encoding ) = MARCfindbreeding( $breedingid ) ;
798
    ( $record, $encoding ) = MARCfindbreeding( $breedingid ) ;
Lines 852-858 if ( $op eq "addbiblio" ) { Link Here
852
        else {
862
        else {
853
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
863
            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
854
        }
864
        }
855
        if ($redirect eq "items" || ($mode ne "popup" && !$is_a_modif && $redirect ne "view" && $redirect ne "just_save")){
865
        if ($redirect eq "items" || (defined $mode && $mode ne "popup" && !$is_a_modif && $redirect ne "view" && $redirect ne "just_save")){
856
	    if ($frameworkcode eq 'FA'){
866
	    if ($frameworkcode eq 'FA'){
857
		print $input->redirect(
867
		print $input->redirect(
858
            '/cgi-bin/koha/cataloguing/additem.pl?'
868
            '/cgi-bin/koha/cataloguing/additem.pl?'
Lines 911-917 if ( $op eq "addbiblio" ) { Link Here
911
        }
921
        }
912
    } else {
922
    } else {
913
    # it may be a duplicate, warn the user and do nothing
923
    # it may be a duplicate, warn the user and do nothing
914
        build_tabs ($template, $record, $dbh,$encoding,$input);
924
        build_tabs ($template, $record, $dbh,$encoding, $input, $history);
915
        $template->param(
925
        $template->param(
916
            biblionumber             => $biblionumber,
926
            biblionumber             => $biblionumber,
917
            biblioitemnumber         => $biblioitemnumber,
927
            biblioitemnumber         => $biblioitemnumber,
Lines 958-964 elsif ( $op eq "delete" ) { Link Here
958
            $record = $urecord;
968
            $record = $urecord;
959
        };
969
        };
960
    }
970
    }
961
    build_tabs( $template, $record, $dbh, $encoding,$input );
971
972
    build_tabs( $template, $record, $dbh, $encoding, $input, $history );
962
    $template->param(
973
    $template->param(
963
        biblionumber             => $biblionumber,
974
        biblionumber             => $biblionumber,
964
        biblionumbertagfield        => $biblionumbertagfield,
975
        biblionumbertagfield        => $biblionumbertagfield,
(-)a/installer/data/mysql/atomicupdate/bug_14367_-_add_history_column.perl (+9 lines)
Line 0 Link Here
1
$DBversion = 'XXX';
2
if( CheckVersion( $DBversion ) ) {
3
    unless( column_exists( 'biblio_metadata', 'history' ) ) {
4
        $dbh->do(q|ALTER TABLE biblio_metadata ADD history longtext default NULL AFTER metadata|);
5
    }
6
7
    SetVersion( $DBversion );
8
    print "Upgrade to $DBversion done (Bug 14367: Add column biblio_metadata.history)\n";
9
}
(-)a/koha-tmpl/intranet-tmpl/prog/css/addbiblio.css (+23 lines)
Lines 80-85 ul li.tag li.subfield_line.ui-sortable-helper::before { Link Here
80
	text-decoration : none;
80
	text-decoration : none;
81
}
81
}
82
82
83
.buttonHistory {
84
	font-weight : bold;
85
	text-decoration : none;
86
}
87
83
a.expandfield {
88
a.expandfield {
84
	text-decoration : none;
89
	text-decoration : none;
85
}
90
}
Lines 232-237 a.tagnum { Link Here
232
.linktools a:hover { background-color: #FFC; }
237
.linktools a:hover { background-color: #FFC; }
233
.subfield_controls { margin: 0 .5em; }
238
.subfield_controls { margin: 0 .5em; }
234
239
240
.readonly { border-width : 1px; border-style: inset; padding-left : 15px; background: #EEE url(../img/locked.png) center left no-repeat; width:29em; }
241
.subfield_history { margin : 0 .5em; float: right; }
242
235
#cataloguing_additem_itemlist {
243
#cataloguing_additem_itemlist {
236
	margin-bottom : 1em;
244
	margin-bottom : 1em;
237
}
245
}
Lines 252-257 tbody tr.active td { Link Here
252
    width: 100%;
260
    width: 100%;
253
    z-index: 1000;
261
    z-index: 1000;
254
}
262
}
263
255
#loading div {
264
#loading div {
256
    background : transparent url(../img/loading.gif) top left no-repeat;
265
    background : transparent url(../img/loading.gif) top left no-repeat;
257
    font-size : 175%;
266
    font-size : 175%;
Lines 442-444 tbody tr.active td { Link Here
442
    border-bottom: 1px solid #b9d8d9;
451
    border-bottom: 1px solid #b9d8d9;
443
    border-radius: 0;
452
    border-radius: 0;
444
}
453
}
454
455
.history_container {
456
    display: none;
457
}
458
459
.history_table {
460
    width: 100%;
461
    display: none;
462
    float: right;
463
    clear: both;
464
    font-size: 75%;
465
    width: 75%;
466
    margin: 0.3em;
467
}
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt (-3 / +52 lines)
Lines 1-6 Link Here
1
[% USE raw %]
1
[% USE raw %]
2
[% USE Asset %]
2
[% USE Asset %]
3
[% USE Koha %]
3
[% USE Koha %]
4
[% USE KohaDates %]
4
[% INCLUDE 'doc-head-open.inc' %]
5
[% INCLUDE 'doc-head-open.inc' %]
5
<title>Koha &rsaquo; Cataloging &rsaquo; [% IF ( biblionumber ) %]Editing [% title | html %] (Record number [% biblionumber | html %])[% ELSE %]Add MARC record[% END %]</title>
6
<title>Koha &rsaquo; Cataloging &rsaquo; [% IF ( biblionumber ) %]Editing [% title | html %] (Record number [% biblionumber | html %])[% ELSE %]Add MARC record[% END %]</title>
6
[% INCLUDE 'doc-head-close.inc' %]
7
[% INCLUDE 'doc-head-close.inc' %]
Lines 22-27 Link Here
22
    var Sticky;
23
    var Sticky;
23
    $(document).ready(function() {
24
    $(document).ready(function() {
24
25
26
        $(".input_marceditor").click(function(){
27
            var tag = $(this).attr('id');
28
            historyToggle(tag + '_history');
29
            return false;
30
        });
31
32
        $(".tag_editor").click(function(){
33
            var tag = $(this).attr('id');
34
            openAuth(this.parentNode.parentNode.getElementsByTagName('input')[1].id, tag, 'biblio');
35
            return false;
36
        });
37
38
        $(".buttonHistory").click(function(){
39
            var tag = $(this).attr('id');
40
            HistoryToggle(tag + '_history');
41
            return false;
42
        });
43
44
        $(".hist_rollback").click(function(){
45
            var tag = $(this).attr('data-tag');
46
            var value = $(this).attr('data-history');
47
            HistoryRollback(tag, value);
48
            return false;
49
        });
50
25
        [% IF bib_doesnt_exist %]
51
        [% IF bib_doesnt_exist %]
26
            $("#addbibliotabs").hide();
52
            $("#addbibliotabs").hide();
27
            $("#toolbar").hide();
53
            $("#toolbar").hide();
Lines 1082-1097 function PopupMARCFieldDoc(field) { Link Here
1082
                                                <div class="subfield_controls">
1108
                                                <div class="subfield_controls">
1083
                                                    [% IF ( mv.type == 'text' ) %]
1109
                                                    [% IF ( mv.type == 'text' ) %]
1084
                                                        [% IF ( mv.authtype ) %]
1110
                                                        [% IF ( mv.authtype ) %]
1085
                                                            <a href="#" class="buttonDot tag_editor" onclick="openAuth(this.parentNode.parentNode.getElementsByTagName('input')[1].id,'[%- mv.authtype | html -%]','biblio'); return false;" tabindex="1" title="Tag editor">Tag editor</a>
1111
                                                            <a id="[%- mv.authtype | html %]" href="#" class="buttonDot tag_editor" onclick="openAuth(this.parentNode.parentNode.getElementsByTagName('input')[1].id,'[%- mv.authtype | html -%]','biblio'); return false;" tabindex="1" title="Tag editor">Tag editor</a>
1086
                                                        [% END %]
1112
                                                        [% END %]
1087
                                                    [% ELSIF ( mv.type == 'text_complex' ) %]
1113
                                                    [% ELSIF ( mv.type == 'text_complex' ) %]
1088
                                                            [% IF mv.noclick %]
1114
                                                            [% IF mv.noclick %]
1089
                                                                <span class="buttonDot tag_editor disabled" tabindex="-1" title="Field autofilled by plugin"></span>
1115
                                                                <span class="buttonDot tag_editor disabled" tabindex="-1" title="Field autofilled by plugin"></span>
1090
                                                            [% ELSE %]
1116
                                                            [% ELSE %]
1091
                                                                [% IF mv.plugin == "upload.pl" %]
1117
                                                                [% IF mv.plugin == "upload.pl" %]
1092
                                                                    <a href="#" id="buttonDot_[% mv.id | html %]" class="tag_editor upload framework_plugin" tabindex="1"><i class="fa fa-upload" aria-hidden="true"></i> Upload</a>
1118
                                                                    <a href="#" id="buttonDot_[% mv.id | html %]" class="buttonDot tag_editor upload framework_plugin" tabindex="2"><i class="fa fa-upload" aria-hidden="true"></i> Upload</a>
1093
                                                                [% ELSE %]
1119
                                                                [% ELSE %]
1094
                                                                    <a href="#" id="buttonDot_[% mv.id | html %]" class="buttonDot tag_editor framework_plugin" tabindex="1" title="Tag editor">Tag editor</a>
1120
                                                                    <a href="#" id="buttonDot_[% mv.id | html %]" class="buttonDot tag_editor framework_plugin" tabindex="2" title="Tag editor">Tag editor</a>
1095
                                                                [% END %]
1121
                                                                [% END %]
1096
                                                            [% END %]
1122
                                                            [% END %]
1097
                                                        </span>
1123
                                                        </span>
Lines 1105-1110 function PopupMARCFieldDoc(field) { Link Here
1105
                                                        </a>
1131
                                                        </a>
1106
                                                    [% END %]
1132
                                                    [% END %]
1107
                                                </div>
1133
                                                </div>
1134
1135
                                                [% IF ( subfield_loo.history ) %]
1136
                                                <span class="subfield_history">
1137
                                                    <a href="#" class="buttonHistory" id="[%- mv.id | html -%]">
1138
                                                        <img src="[% interface | html %]/[% theme | html %]/img/icon-history.png" alt="History" title="Display history for this field" />
1139
                                                    </a>
1140
                                                </span>
1141
1142
                                                <table class="history_table" id="[%- mv.id | html -%]_history" name="[%- mv.name | html -%]_history">
1143
                                                    <tr>
1144
                                                        <th>Value</th>
1145
                                                        <th>Date</th>
1146
                                                        <th>User</th>
1147
                                                    </tr>
1148
                                                    [% FOREACH hist IN subfield_loo.history %]
1149
                                                    <tr>
1150
                                                        <td><a href="#" data-tag='[%- mv.id | html -%]' data-history='[% hist.data | html %]' class='hist_rollback'>[% hist.data | html %]</a></td>
1151
                                                        <td>[% hist.datetime | $KohaDates with_hours = 1 %]</td>
1152
                                                        <td>[% hist.title | html %] [% hist.firstname | html %] [% hist.surname | html %]</td>
1153
                                                    </tr>
1154
                                                    [% END %]
1155
                                                </table>
1156
                                                [% END %]
1108
                                            </li> <!-- /.subfield_line -->
1157
                                            </li> <!-- /.subfield_line -->
1109
                                            <!-- End of the line -->
1158
                                            <!-- End of the line -->
1110
                                        [% END # /FOREACH subfield_loop %]
1159
                                        [% END # /FOREACH subfield_loop %]
(-)a/koha-tmpl/intranet-tmpl/prog/js/cataloging.js (+10 lines)
Lines 57-62 function openAuth(tagsubfieldid,authtype,source) { Link Here
57
    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');
57
    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');
58
}
58
}
59
59
60
function HistoryToggle (tagid)
61
{
62
	$('#' + tagid).toggle();
63
}
64
65
function HistoryRollback (tagid, value)
66
{
67
	$('#' + tagid).val(value);
68
}
69
60
function ExpandField(index) {
70
function ExpandField(index) {
61
    var original = document.getElementById(index); //original <li>
71
    var original = document.getElementById(index); //original <li>
62
    var lis = original.getElementsByTagName('li');
72
    var lis = original.getElementsByTagName('li');
(-)a/t/db_dependent/BiblioHistory.t (-1 / +256 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
use t::lib::TestBuilder;
5
use t::lib::Mocks;
6
use Test::More tests => 6;
7
use POSIX qw(strftime);
8
9
use MARC::Record;
10
use MARC::Field;
11
use JSON qw(encode_json);
12
13
use Koha::MetadataRecord::History;
14
15
BEGIN {
16
    use_ok('C4::Biblio');
17
}
18
19
our $dbh = C4::Context->dbh;
20
$dbh->{AutoCommit} = 0;
21
$dbh->{RaiseError} = 0;
22
23
$dbh->do(q|DELETE FROM issues|);
24
$dbh->do(q|DELETE FROM items|);
25
$dbh->do(q|DELETE FROM borrowers|);
26
$dbh->do(q|DELETE FROM branches|);
27
$dbh->do(q|DELETE FROM categories|);
28
$dbh->do(q|DELETE FROM biblioitems|);
29
30
my $builder = t::lib::TestBuilder->new();
31
32
my $branch = $builder->build(
33
    {
34
        source => 'Branch',
35
    }
36
);
37
38
my $category = $builder->build(
39
    {
40
        source => 'Category',
41
    }
42
);
43
44
my $patron1 = $builder->build(
45
    {
46
        source => 'Borrower',
47
        value  => {
48
            title        => 'Mr/Mrs',
49
            firstname    => 'First name',
50
            surname      => 'Surname',
51
            categorycode => $category->{categorycode},
52
            branchcode   => $branch->{branchcode},
53
        },
54
    }
55
);
56
57
my $patron2 = $builder->build(
58
    {
59
        source => 'Borrower',
60
        value  => {
61
            title        => 'Mr/Mrs 2',
62
            firstname    => 'First name 2',
63
            surname      => 'Surname 2',
64
            categorycode => $category->{categorycode},
65
            branchcode   => $branch->{branchcode},
66
        },
67
    }
68
);
69
70
my $biblio = $builder->build(
71
    {
72
        source => 'Biblio',
73
        value  => {
74
            branchcode => $branch->{branchcode},
75
        },
76
    }
77
);
78
79
C4::Context->_new_userenv('DUMMY_SESSION_ID');
80
C4::Context->set_userenv( $patron1->{borrowernumber},
81
    $patron1->{userid}, 'usercnum', $patron1->{firstname}, $patron1->{surname},
82
    $branch->{branchcode}, 'My library', 0 );
83
84
# Item with no pre-existing history backlog.
85
my $record1 = MARC::Record->new();
86
my $field1  = MARC::Field->new(
87
    245, '1', '0',
88
    'a' => 'First Title',
89
    'c' => 'Some dude'
90
);
91
$record1->append_fields($field1);
92
93
my $record2 = MARC::Record->new();
94
my $field2  = MARC::Field->new(
95
    245, '1', '0',
96
    'a' => 'Second Title',
97
    'c' => 'Former dude'
98
);
99
$record2->append_fields($field2);
100
101
my $biblioitem1 = $builder->build(
102
    {
103
        source => 'Biblioitem',
104
        value  => {
105
            marcxml => $record1->as_xml(),
106
            history => undef,
107
        }
108
    }
109
);
110
111
# items{tagname}{subfield}{timestamp}{borrowernumber|timestamp|marcxml}
112
my $items = [
113
    {
114
        borrowernumber => $patron1->{borrowernumber},
115
        datetime       => '2012-08-12 12:00:10',
116
        marcxml        => $record1->as_xml(),
117
    },
118
    {
119
        borrowernumber => $patron2->{borrowernumber},
120
        datetime       => '2012-08-12 12:00:10',
121
        marcxml        => $record2->as_xml(),
122
    }
123
];
124
125
# Item with previous history entries.
126
my $biblioitem2 = $builder->build(
127
    {
128
        source => 'Biblioitem',
129
        value  => {
130
            marcxml => $record2->as_xml(),
131
            history => encode_json($items),
132
        }
133
    }
134
);
135
136
my $expected_history = {
137
    245 => {
138
        a => [
139
            {
140
                borrowernumber => $patron1->{borrowernumber},
141
                data           => 'First Title',
142
                datetime       => '2012-08-12 12:00:10'
143
            },
144
            {
145
                borrowernumber => $patron2->{borrowernumber},
146
                data           => 'Second Title',
147
                datetime       => '2012-08-12 12:00:10'
148
            },
149
        ],
150
        c => [
151
            {
152
                borrowernumber => $patron1->{borrowernumber},
153
                data           => 'Some dude',
154
                datetime       => '2012-08-12 12:00:10'
155
            },
156
            {
157
                borrowernumber => $patron2->{borrowernumber},
158
                data           => 'Former dude',
159
                datetime       => '2012-08-12 12:00:10'
160
            },
161
        ],
162
    },
163
};
164
165
my $expected_users = {
166
    $patron1->{borrowernumber} => undef,
167
    $patron2->{borrowernumber} => undef,
168
};
169
170
# Tests the decoding and reindexing of the data, without any borrower
171
# information implanted into the history itself.
172
173
my ( $history, $users ) = C4::Biblio::HistoryParse( encode_json($items) );
174
175
is_deeply( $history, $expected_history, 'HistoryParse: Output' );
176
is_deeply( $users,   $expected_users,   'HistoryParse: Users' );
177
178
# Tests the extraction of full borrower details based on the borrowernumber.
179
my $expected_details = {
180
    $patron1->{borrowernumber} => {
181
        firstname => $patron1->{firstname},
182
        surname   => $patron1->{surname},
183
        title     => $patron1->{title},
184
    },
185
    $patron2->{borrowernumber} => {
186
        firstname => $patron2->{firstname},
187
        surname   => $patron2->{surname},
188
        title     => $patron2->{title},
189
    }
190
};
191
192
my $details = HistoryBorrowerDetails($expected_users);
193
is_deeply( $details, $expected_details, 'HistoryBorrowerDetails: Output' );
194
195
{
196
    my $expected_mapped_history = {
197
        245 => {
198
            a => [
199
                {
200
                    borrowernumber => $patron1->{borrowernumber},
201
                    data           => 'First Title',
202
                    firstname      => $patron1->{firstname},
203
                    surname        => $patron1->{surname},
204
                    title          => $patron1->{title},
205
                    datetime       => '2012-08-12 12:00:10'
206
                },
207
                {
208
                    borrowernumber => $patron2->{borrowernumber},
209
                    data           => 'Second Title',
210
                    firstname      => $patron2->{firstname},
211
                    surname        => $patron2->{surname},
212
                    title          => $patron2->{title},
213
                    datetime       => '2012-08-12 12:00:10'
214
                },
215
            ],
216
            c => [
217
                {
218
                    borrowernumber => $patron1->{borrowernumber},
219
                    data           => 'Some dude',
220
                    firstname      => $patron1->{firstname},
221
                    surname        => $patron1->{surname},
222
                    title          => $patron1->{title},
223
                    datetime       => '2012-08-12 12:00:10'
224
                },
225
                {
226
                    borrowernumber => $patron2->{borrowernumber},
227
                    data           => 'Former dude',
228
                    firstname      => $patron2->{firstname},
229
                    surname        => $patron2->{surname},
230
                    title          => $patron2->{title},
231
                    datetime       => '2012-08-12 12:00:10'
232
                },
233
            ]
234
        }
235
    };
236
237
    $history = C4::Biblio::HistoryMapUsers( $expected_history, $details );
238
239
    is_deeply( $history, $expected_mapped_history, 'HistoryMapUsers: Output' );
240
241
  #my $record;
242
  #( $record, $history ) = GetMarcBiblioHistory( $biblioitem2->{biblionumber} );
243
244
    #is_deeply( $record, $record2 );
245
    #is_deeply( $history, $expected_mapped_history );
246
}
247
248
# The C4::Context will provide the expected user borrowernumber.
249
my $record = HistoryRecordNew($record1);
250
251
my $expected_record = {
252
    borrowernumber => C4::Context->userenv()->{number},
253
    marcxml        => $record1->as_xml_record(),
254
    datetime       => POSIX::strftime( '%Y-%m-%d %T', localtime ),
255
};
256
is_deeply( $record, $expected_record );

Return to bug 14367