From 96a0479258c6cb2238e2d929815c346697a9fcf2 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Fri, 2 May 2025 17:06:30 +0100 Subject: [PATCH] Bug 39545: (follow-up) Apply same logic to Koha::Biblio This patch updates the Koha::Biblio->generate_marc_host_field method to more closely match the updated logic in C4::Biblio::prep_marc_host. We change a number of things including: 1) Constructinging the 773 subfield generation from a hash to an array to preserve field order. 2) Updating main entry handling for 100/110/111 fields to clone the field and then remove relevant subfields as apposed to fetching only a limited set of subfields. 3) Add handling to generate subfield 7 content depending on LDR and Main Entry information 4) Improve handling for title subfield construction from 245 to respect non-filing characters and remove trailing punctuation 5) Remove trailing punctuation from subfield b when generated from 250 6) Remove generation of subfield s from the 240 field entirely 7) Remove trailing punctuation from subfield d when generated from 260 8) Add generation of subfield k from 800-830 fields --- Koha/Biblio.pm | 193 +++++++++++++++++---------------- t/db_dependent/Koha/Biblio.t | 201 ++++++++++++++++++++++++++--------- 2 files changed, 257 insertions(+), 137 deletions(-) diff --git a/Koha/Biblio.pm b/Koha/Biblio.pm index 487500de110..6a5b70ae459 100644 --- a/Koha/Biblio.pm +++ b/Koha/Biblio.pm @@ -1921,149 +1921,164 @@ sub generate_marc_host_field { my $marcflavour = C4::Context->preference('marcflavour'); my $marc_host = $self->metadata->record; - my %sfd; + + my @sfd; # array of subfields to preserve order my $host_field; my $link_field; if ( $marcflavour eq 'MARC21' ) { - # Author - if ( $host_field = $marc_host->field('100') || $marc_host->field('110') || $marc_host->field('111') ) { - my $s = $host_field->as_string('ab'); - if ($s) { - $sfd{a} = $s; + # Attempt to clone the main entry (100, 110, or 111) + my $main_entry = $marc_host->field('100') || $marc_host->field('110') || $marc_host->field('111'); + $main_entry = $main_entry ? $main_entry->clone : undef; + + # Clean unwanted subfields based on tag type + if ($main_entry) { + if ( $main_entry->tag eq '111' ) { + $main_entry->delete_subfield( code => qr/[94j]/ ); + } else { + $main_entry->delete_subfield( code => qr/[94e]/ ); } } - # Edition - if ( $host_field = $marc_host->field('250') ) { - my $s = $host_field->as_string('ab'); - if ($s) { - $sfd{b} = $s; + # Construct subfield 7 from leader and main entry tag + my $s7 = "nn" . substr( $marc_host->leader, 6, 2 ); + if ($main_entry) { + my $c1 = 'n'; + if ( $main_entry->tag =~ /^1[01]/ ) { + $c1 = $main_entry->indicator('1'); + $c1 = $main_entry->tag eq '100' ? 1 : 2 unless $c1 =~ /\d/; } + my $c0 = + ( $main_entry->tag eq '100' ) ? 'p' + : ( $main_entry->tag eq '110' ) ? 'c' + : ( $main_entry->tag eq '111' ) ? 'm' + : 'u'; + substr( $s7, 0, 2, $c0 . $c1 ); } + push @sfd, '7' => $s7; - # Publication - if ( $host_field = $marc_host->field('260') ) { - my $s = $host_field->as_string('abc'); - if ($s) { - $sfd{d} = $s; - } + # Subfield a - cleaned main entry string + if ($main_entry) { + my $a = $main_entry->as_string; + $a =~ s/\.$// unless $a =~ /\b[a-z]{1,2}\.$/i; + push @sfd, 'a' => $a; } - # Uniform title - if ( $host_field = $marc_host->field('240') ) { - my $s = $host_field->as_string('a'); - if ($s) { - $sfd{s} = $s; - } + # Subfield t - title from 245, cleaned + if ( my $f245 = $marc_host->field('245') ) { + my $f245c = $f245->clone; + $f245c->delete_subfield( code => 'c' ); + my $t = $f245c->as_string; + $t =~ s/[\s\/\\.]+$//; + my $nonfiling = $f245c->indicator('2') // 0; + $t = ucfirst substr( $t, $nonfiling ); + push @sfd, 't' => $t; } - # Title - if ( $host_field = $marc_host->field('245') ) { - my $s = $host_field->as_string('ab'); - if ($s) { - $sfd{t} = $s; - } + # Subfield b - edition from 250 + if ( my $f250 = $marc_host->field('250') ) { + my $b = $f250->as_string; + $b =~ s/\.$//; + push @sfd, 'b' => $b; } - # ISSN - if ( $host_field = $marc_host->field('022') ) { - my $s = $host_field->as_string('a'); - if ($s) { - $sfd{x} = $s; - } + # Subfield d - publication info from 260 + if ( my $f260 = $marc_host->field('260') ) { + my $d = $f260->as_string('abc'); + $d =~ s/\.$//; + push @sfd, 'd' => $d; } - # ISBN - if ( $host_field = $marc_host->field('020') ) { - my $s = $host_field->as_string('a'); - if ($s) { - $sfd{z} = $s; - } + # Subfield k - host info from 800-830 fields + for my $f ( $marc_host->field('8[013][01]') ) { + my $k = $f->as_string('abcdnjltnp'); + $k .= ', ISSN ' . $f->subfield('x') if $f->subfield('x'); + $k .= ' ; ' . $f->subfield('v') if $f->subfield('v'); + push @sfd, 'k' => $k; } - if ( C4::Context->preference('UseControlNumber') ) { - # Control number - if ( $host_field = $marc_host->field('001') ) { - $sfd{w} = $host_field->data(); - } + # Subfield x - ISSN from 022 + for my $f ( $marc_host->field('022') ) { + push @sfd, 'x' => $f->subfield('a') if $f->subfield('a'); + } + + # Subfield z - ISBN from 020 + for my $f ( $marc_host->field('020') ) { + push @sfd, 'z' => $f->subfield('a') if $f->subfield('a'); + } - # Control number identifier - if ( $host_field = $marc_host->field('003') ) { - $sfd{w} = '(' . $host_field->data() . ')' . $sfd{w}; + # Subfield w - control number (001 and optionally 003) + if ( C4::Context->preference('UseControlNumber') ) { + if ( my $f001 = $marc_host->field('001') ) { + my $w = $f001->data; + if ( my $f003 = $marc_host->field('003') ) { + $w = '(' . $f003->data . ')' . $w; + } + push @sfd, 'w' => $w; } } - $link_field = MARC::Field->new( 773, '0', ' ', %sfd ); + + # Construct 773 link field + $link_field = MARC::Field->new( 773, '0', ' ', @sfd ); + } elsif ( $marcflavour eq 'UNIMARC' ) { - # Author + # Author (700/710/720) if ( $host_field = $marc_host->field('700') || $marc_host->field('710') || $marc_host->field('720') ) { my $s = $host_field->as_string('ab'); - if ($s) { - $sfd{a} = $s; - } + push @sfd, 'a' => $s if $s; + } + + # Title (200) + if ( $host_field = $marc_host->field('200') ) { + my $s = $host_field->as_string('a'); + push @sfd, 't' => $s if $s; } - # Place of publication + # Place of publication (210$a) if ( $host_field = $marc_host->field('210') ) { my $s = $host_field->as_string('a'); - if ($s) { - $sfd{c} = $s; - } + push @sfd, 'c' => $s if $s; } - # Date of publication + # Date of publication (210$d) if ( $host_field = $marc_host->field('210') ) { my $s = $host_field->as_string('d'); - if ($s) { - $sfd{d} = $s; - } + push @sfd, 'd' => $s if $s; } - # Edition statement + # Edition statement (205) if ( $host_field = $marc_host->field('205') ) { - my $s = $host_field->as_string(); - if ($s) { - $sfd{e} = $s; - } + my $s = $host_field->as_string; + push @sfd, 'e' => $s if $s; } - # Title - if ( $host_field = $marc_host->field('200') ) { - my $s = $host_field->as_string('a'); - if ($s) { - $sfd{t} = $s; - } - } - - #URL + # URL (856$u) if ( $host_field = $marc_host->field('856') ) { my $s = $host_field->as_string('u'); - if ($s) { - $sfd{u} = $s; - } + push @sfd, 'u' => $s if $s; } - # ISSN + # ISSN (011$a) if ( $host_field = $marc_host->field('011') ) { my $s = $host_field->as_string('a'); - if ($s) { - $sfd{x} = $s; - } + push @sfd, 'x' => $s if $s; } - # ISBN + # ISBN (010$a) if ( $host_field = $marc_host->field('010') ) { my $s = $host_field->as_string('a'); - if ($s) { - $sfd{y} = $s; - } + push @sfd, 'y' => $s if $s; } + + # Control number (001) if ( $host_field = $marc_host->field('001') ) { - $sfd{0} = $host_field->data(); + push @sfd, '0' => $host_field->data(); } - $link_field = MARC::Field->new( 461, '0', ' ', %sfd ); + + # Construct 461 link field + $link_field = MARC::Field->new( 461, '0', ' ', @sfd ); } return $link_field; diff --git a/t/db_dependent/Koha/Biblio.t b/t/db_dependent/Koha/Biblio.t index 0cd7491d152..072baf7cd22 100755 --- a/t/db_dependent/Koha/Biblio.t +++ b/t/db_dependent/Koha/Biblio.t @@ -999,76 +999,181 @@ subtest 'get_volumes_query' => sub { }; subtest 'generate_marc_host_field' => sub { - plan tests => 22; + plan tests => 33; $schema->storage->txn_begin; + # Set up MARC21 tests t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' ); - my $biblio = $builder->build_sample_biblio(); - my $record = $biblio->metadata->record; + # 1. Complete MARC21 record test + my $record = MARC::Record->new(); + $record->leader('00000nam a22000007a 4500'); $record->append_fields( - MARC::Field->new( '001', '1234' ), - MARC::Field->new( '003', 'FIRST' ), - MARC::Field->new( '240', '', '', a => 'A uniform title' ), - MARC::Field->new( '260', '', '', a => 'Publication' ), - MARC::Field->new( '250', '', '', a => 'Edition a', b => 'Edition b' ), - MARC::Field->new( '022', '', '', a => '0317-8471' ), + MARC::Field->new( '001', '12345' ), + MARC::Field->new( '003', 'NB' ), + MARC::Field->new( '020', '', '', 'a' => '978-3-16-148410-0' ), + MARC::Field->new( '022', '', '', 'a' => '1234-5678' ), + MARC::Field->new( '100', '1', '', 'a' => 'Smith, John', 'e' => 'author', '9' => 'xyz', '4' => 'aut' ), + MARC::Field->new( '245', '1', '0', 'a' => 'The Title', 'b' => 'Subtitle', 'c' => 'John Smith' ), + MARC::Field->new( '250', '', '', 'a' => '2nd edition', 'b' => 'revised' ), + MARC::Field->new( '260', '', '', 'a' => 'New York', 'b' => 'Publisher', 'c' => '2023' ), + MARC::Field->new( '830', '', '', 'a' => 'Series Title', 'v' => 'vol. 2', 'x' => '2345-6789' ) ); - C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); - $biblio = Koha::Biblios->find( $biblio->biblionumber ); + my ($biblio_id) = AddBiblio( $record, qw{} ); + my $biblio = Koha::Biblios->find($biblio_id); - t::lib::Mocks::mock_preference( 'UseControlNumber', '0' ); + # Test MARC21 with UseControlNumber off + t::lib::Mocks::mock_preference( 'UseControlNumber', 0 ); my $link = $biblio->generate_marc_host_field(); - is( ref($link), 'MARC::Field', "->generate_marc_host_field returns a MARC::Field object" ); - is( $link->tag, '773', "MARC::Field->tag returns '773' when marcflavour is 'MARC21" ); - is( $link->subfield('a'), 'Some boring author', 'MARC::Field->subfield(a) returns content from 100ab' ); - is( $link->subfield('b'), 'Edition a Edition b', 'MARC::Field->subfield(b) returns content from 250ab' ); - is( $link->subfield('d'), 'Publication', 'MARC::Field->subfield(c) returns content from 260abc' ); - is( $link->subfield('s'), 'A uniform title', 'MARC::Field->subfield(s) returns content from 240a' ); - is( $link->subfield('t'), 'Some boring read', 'MARC::Field->subfield(s) returns content from 245ab' ); - is( $link->subfield('x'), '0317-8471', 'MARC::Field->subfield(s) returns content from 022a' ); - is( $link->subfield('z'), undef, 'MARC::Field->subfield(s) returns undef when 020a is empty' ); - is( $link->subfield('w'), undef, 'MARC::Field->subfield(w) returns undef when "UseControlNumber" is disabled' ); + # Test standard MARC21 field + is( ref($link), 'MARC::Field', 'Returns a MARC::Field object' ); + is( $link->tag(), '773', 'Field tag is 773 for MARC21' ); + is( $link->indicator(1), '0', 'First indicator is 0' ); + is( $link->indicator(2), ' ', 'Second indicator is blank' ); + + # Check all subfields + is( $link->subfield('7'), 'p1am', 'Subfield 7 correctly formed' ); + is( $link->subfield('a'), 'Smith, John', 'Subfield a contains author from 100a' ); + is( + $link->subfield('t'), 'The Title Subtitle', + 'Subfield t contains title without trailing punctuation from 245ab' + ); + is( $link->subfield('b'), '2nd edition revised', 'Subfield b contains edition info from 250ab' ); + is( $link->subfield('d'), 'New York Publisher 2023', 'Subfield d contains publication info from 260abc' ); + is( $link->subfield('k'), 'Series Title, ISSN 2345-6789 ; vol. 2', 'Subfield k contains series info from 830' ); + is( $link->subfield('x'), '1234-5678', 'Subfield x contains ISSN from 022a' ); + is( $link->subfield('z'), '978-3-16-148410-0', 'Subfield z contains ISBN from 020a' ); + is( $link->subfield('w'), undef, 'Subfield w is undefined when UseControlNumber is disabled' ); + # Test with UseControlNumber enabled t::lib::Mocks::mock_preference( 'UseControlNumber', '1' ); $link = $biblio->generate_marc_host_field(); is( - $link->subfield('w'), '(FIRST)1234', - 'MARC::Field->subfield(w) returns content from 003 and 001 when "UseControlNumber" is enabled' + $link->subfield('w'), '(NB)12345', + 'Subfield w contains control number with source when UseControlNumber is enabled' ); - # UNIMARC tests - t::lib::Mocks::mock_preference( 'marcflavour', 'UNIMARC' ); + # 245 punctuation handling tests + # Trailing slash + $record->field('245')->update( a => 'A title /', b => '', c => '', 'ind2' => '0' ); + ($biblio_id) = AddBiblio( $record, qw{} ); + $biblio = Koha::Biblios->find($biblio_id); + $link = $biblio->generate_marc_host_field(); + is( $link->subfield('t'), 'A title', "Trailing slash is removed from 245a" ); - $biblio = $builder->build_sample_biblio(); - $record = $biblio->metadata->record; - $record->append_fields( - MARC::Field->new( '001', '1234' ), - MARC::Field->new( '700', '', '', a => 'A nice author' ), - MARC::Field->new( '210', '', '', a => 'A publication', d => 'A date' ), - MARC::Field->new( '205', '', '', a => "Fun things" ), - MARC::Field->new( '856', '', '', u => 'http://myurl.com/' ), - MARC::Field->new( '011', '', '', a => '0317-8471' ), - MARC::Field->new( '545', '', '', a => 'Invisible on OPAC' ), + # Trailing period + $record->field('245')->update( a => 'Another title.', 'ind2' => '0' ); + ($biblio_id) = AddBiblio( $record, qw{} ); + $biblio = Koha::Biblios->find($biblio_id); + $link = $biblio->generate_marc_host_field(); + is( $link->subfield('t'), 'Another title', "Trailing period is removed from 245a" ); + + # Offset from indicator 2 = 4 + $record->field('245')->update( a => 'The offset title', 'ind2' => '4' ); + ($biblio_id) = AddBiblio( $record, qw{} ); + $biblio = Koha::Biblios->find($biblio_id); + $link = $biblio->generate_marc_host_field(); + is( $link->subfield('t'), 'Offset title', "Title offset applied from indicator 2" ); + + # Capitalization after offset + $record->field('245')->update( a => 'the capital test', 'ind2' => '0' ); + ($biblio_id) = AddBiblio( $record, qw{} ); + $biblio = Koha::Biblios->find($biblio_id); + $link = $biblio->generate_marc_host_field(); + is( $link->subfield('t'), 'The capital test', "Title is capitalized after indicator offset" ); + + # 2. Test MARC21 with corporate author (110) + my $record_corporate = MARC::Record->new(); + $record_corporate->leader('00000nam a22000007a 4500'); + $record_corporate->append_fields( + MARC::Field->new( '110', '2', '', 'a' => 'Corporate Author', 'e' => 'sponsor', '9' => 'xyz', '4' => 'spn' ), + MARC::Field->new( '245', '1', '0', 'a' => 'The Title' ) ); - C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); + ($biblio_id) = AddBiblio( $record_corporate, qw{} ); + $biblio = Koha::Biblios->find($biblio_id); + + $link = $biblio->generate_marc_host_field(); + is( $link->subfield('7'), 'c2am', 'Subfield 7 correctly formed for corporate author' ); + is( $link->subfield('a'), 'Corporate Author', 'Subfield a contains corporate author' ); + + # 3. Test MARC21 with meeting name (111) + my $record_meeting = MARC::Record->new(); + $record_meeting->leader('00000nam a22000007a 4500'); + $record_meeting->append_fields( + MARC::Field->new( '111', '2', '', 'a' => 'Conference Name', 'j' => 'relator', '9' => 'xyz', '4' => 'spn' ), + MARC::Field->new( '245', '1', '0', 'a' => 'The Title' ) + ); + ($biblio_id) = AddBiblio( $record_meeting, qw{} ); + $biblio = Koha::Biblios->find($biblio_id); + + $link = $biblio->generate_marc_host_field(); + is( $link->subfield('7'), 'm2am', 'Subfield 7 correctly formed for meeting name' ); + + # 4. Test MARC21 with minimal record + my $record_minimal = MARC::Record->new(); + $record_minimal->leader('00000nam a22000007a 4500'); + $record_minimal->append_fields( MARC::Field->new( '245', '0', '0', 'a' => 'Title Only' ) ); + ($biblio_id) = AddBiblio( $record_minimal, qw{} ); + $biblio = Koha::Biblios->find($biblio_id); + + $link = $biblio->generate_marc_host_field(); + is( $link->subfield('7'), 'nnam', 'Subfield 7 correctly formed with no main entry' ); + + # 5. Test UNIMARC + t::lib::Mocks::mock_preference( 'marcflavour', 'UNIMARC' ); + $biblio = $builder->build_sample_biblio(); + my $record_unimarc = MARC::Record->new(); + $record_unimarc->append_fields( + MARC::Field->new( '001', '54321' ), + MARC::Field->new( '010', '', '', 'a' => '978-0-12-345678-9' ), + MARC::Field->new( '011', '', '', 'a' => '2345-6789' ), + MARC::Field->new( '200', '', '', 'a' => 'UNIMARC Title' ), + MARC::Field->new( '205', '', '', 'a' => 'Third edition' ), + MARC::Field->new( '210', '', '', 'a' => 'Paris', 'd' => '2023' ), + MARC::Field->new( '700', '', '', 'a' => 'Doe', 'b' => 'Jane' ), + MARC::Field->new( '856', '', '', 'u' => 'http://example.com' ) + ); + ($biblio_id) = AddBiblio( $record_unimarc, qw{} ); + $biblio = Koha::Biblios->find($biblio_id); + + $link = $biblio->generate_marc_host_field(); + + is( ref($link), 'MARC::Field', 'Returns a MARC::Field object for UNIMARC' ); + is( $link->tag(), '461', 'Field tag is 461 for UNIMARC' ); + is( $link->indicator(1), '0', 'First indicator is 0 for UNIMARC' ); + is( $link->indicator(2), ' ', 'Second indicator is blank for UNIMARC' ); + + # Check UNIMARC subfields + is( $link->subfield('a'), 'Doe Jane', 'Subfield a contains author for UNIMARC' ); + is( $link->subfield('t'), 'UNIMARC Title', 'Subfield t contains title for UNIMARC' ); + is( $link->subfield('c'), 'Paris', 'Subfield c contains place of publication for UNIMARC' ); + is( $link->subfield('d'), '2023', 'Subfield d contains date of publication for UNIMARC' ); + is( $link->subfield('0'), '54321', 'Subfield 0 contains control number for UNIMARC' ); + + # 6. Test UNIMARC with different author types + my $record_unimarc_corporate = MARC::Record->new(); + $record_unimarc_corporate->append_fields( + MARC::Field->new( '710', '', '', 'a' => 'Corporate', 'b' => 'Department' ), + MARC::Field->new( '200', '', '', 'a' => 'Title' ) + ); + C4::Biblio::ModBiblio( $record_unimarc_corporate, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); $link = $biblio->generate_marc_host_field(); + is( $link->subfield('a'), 'Corporate Department', 'Subfield a contains corporate author for UNIMARC' ); + + my $record_unimarc_family = MARC::Record->new(); + $record_unimarc_family->append_fields( + MARC::Field->new( '720', '', '', 'a' => 'Family', 'b' => 'Name' ), + MARC::Field->new( '200', '', '', 'a' => 'Title' ) + ); + C4::Biblio::ModBiblio( $record_unimarc_family, $biblio->biblionumber ); + $biblio = Koha::Biblios->find( $biblio->biblionumber ); - is( ref($link), 'MARC::Field', "->generate_marc_host_field returns a MARC::Field object" ); - is( $link->tag, '461', "MARC::Field->tag returns '461' when marcflavour is 'UNIMARC" ); - is( $link->subfield('a'), 'A nice author', 'MARC::Field->subfield(a) returns content from 700ab' ); - is( $link->subfield('c'), 'A publication', 'MARC::Field->subfield(b) returns content from 210a' ); - is( $link->subfield('d'), 'A date', 'MARC::Field->subfield(c) returns content from 210d' ); - is( $link->subfield('e'), 'Fun things', 'MARC::Field->subfield(s) returns content from 205' ); - is( $link->subfield('t'), 'Some boring read', 'MARC::Field->subfield(s) returns content from 200a' ); - is( $link->subfield('u'), 'http://myurl.com/', 'MARC::Field->subfield(s) returns content from 856u' ); - is( $link->subfield('x'), '0317-8471', 'MARC::Field->subfield(s) returns content from 011a' ); - is( $link->subfield('y'), undef, 'MARC::Field->subfield(w) returns undef if 010a is empty' ); - is( $link->subfield('0'), '1234', 'MARC::Field->subfield(0) returns content from 001' ); + $link = $biblio->generate_marc_host_field(); + is( $link->subfield('a'), 'Family Name', 'Subfield a contains family name for UNIMARC' ); $schema->storage->txn_rollback; t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' ); -- 2.49.0