From 19a97b141916942c51552dac26bb6e27f2ee5252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Cohen=20Arazi?= Date: Tue, 27 Jan 2026 12:42:28 -0300 Subject: [PATCH] Bug 41714: Add a way to let the authority linker honor frameworks (MARC21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patchset implements a way to make the authority linker follow the cataloguing frameworks when it comes to linked authority type. The current behavior is this for UNIMARC, but in the MARC21 case it just has hardcoded links (e.g. 650 is always linked to `TOPIC_TERM`). This is accomplished by: * Replicating what UNIMARC does, in MARC21 * Moving the code around so both use the same codebase * Adding tests * Adding the `LinkerUseFrameworkAuthTypes` system preference for opting into this new behavior. To test: 1. Launch a fresh KTD instance: $ ktd --proxy --name authorities --search-engine es9 up -d $ ktd --name authorities --wait-ready 120 2. On the staff interface (http://authorities-admin.localhost) open record 344 3. Change the cataloguing framework to the `Default` one and adjust the details so it can be saved. 4. Edit the `Default` framework to unlink the subfield `650$a` from the `TOPIC_TERM` thesaurus. 5. Go back to editing the record => SUCCESS: Field 650$a can be edited freely (normal behavior for unlinked subfields) 6. Add the following headings to repeatable `650$a`: * YERBA * MATE * BOMBILLA * AGUA CALIENTE 7. Create a new Authority type called `CUST_TERM` in `Admin > Authority types` * Make the 'field to copy' be 150 * Export the `TOPIC_TERM` authority type structure into CSV * Import the exported authority type onto `CUST_TERM` => SUCCESS: `TOPIC_TERM` and `CUST_TERM` are the same thing. 8. Add the following authorities of type `CUST_TERM` * YERBA * MATE * BOMBILLA 9. Edit the `Default` framework so `650$a` is now linked to `CUST_TERM` 10. Run: $ ktd --name authorities --shell k$ perl misc/link_bibs_to_authorities.pl --bib-limit "biblionumber=344" --auth-limit "authtypecode='CUST_TERM'" -l -g 650 => FAIL: You see: ``` Bib authority heading linking report ======================================================= Linker module: C4::Linker::Default Run started at: 01/27/26 14:40:42 Run ended at: 01/27/26 14:40:43 Total run time: 276 ms Number of bibs checked: 1 Number of bibs modified: 0 Number of bibs with errors: 0 Number of headings linked: 1 Number of headings unlinked: 4 Number of headings fuzzily linked: 0 ``` This means it kept the currently linked heading, and the other 4 were kept unliked even though they exist on the `CUST_TERM` thesaurus. 11. Apply this patchset 12. Run: k$ updatedatabase k$ restart_all # just in case => SUCCESS: You see a syspref has been added 13. Repeat 10 => SUCCESS: Same result! 14. Set the `LinkerUseFrameworkAuthTypes` system preference to `Do` 15. Repeat 10: => SUCCESS: Yay! You see: ``` Bib authority heading linking report ======================================================= Linker module: C4::Linker::Default Run started at: 01/27/26 14:41:15 Run ended at: 01/27/26 14:41:16 Total run time: 302 ms Number of bibs checked: 1 Number of bibs modified: 1 Number of bibs with errors: 0 Number of headings linked: 4 Number of headings unlinked: 1 Number of headings fuzzily linked: 0 ``` This means *YERBA*, *MATE* and *BOMBILLA* where linked, the existing link was kept [1] and *AGUA CALIENTE* didn't get linked (of course, as it doesn't exist in `CUST_TERM`. 16. Run the tests: k$ prove t/db_dependent/Headin* => SUCCESS: Tests pass! 17. Sign off :-D [1] This really depends on how `LinkerKeepStale` is set, but what I describe is the default behavior. Signed-off-by: Tomás Cohen Arazi --- C4/Heading.pm | 41 +++++++++++++++++++++++++++++++++++++++-- C4/Heading/MARC21.pm | 38 ++++++++++++++++++++++++++++++++++---- C4/Heading/UNIMARC.pm | 12 +++--------- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/C4/Heading.pm b/C4/Heading.pm index 7cf7e8c401..c79a2e739b 100644 --- a/C4/Heading.pm +++ b/C4/Heading.pm @@ -31,6 +31,9 @@ use C4::Context; use Module::Load qw( load ); use List::Util qw( none first ); +# Package-level cache for authority types from frameworks +my %authority_type_cache; + =head1 NAME C4::Heading @@ -71,7 +74,11 @@ sub new_from_field { my $frameworkcode = shift; #FIXME this is not used? my $auth = shift; my $marcflavour = C4::Context->preference('marcflavour'); - my $marc_handler = _marc_format_handler($marcflavour); + + # Enable framework lookup for MARC21 if LinkerUseFrameworkAuthTypes is enabled + my $use_frameworks = $marcflavour eq 'MARC21' + && C4::Context->preference('LinkerUseFrameworkAuthTypes'); + my $marc_handler = _marc_format_handler( $marcflavour, { use_frameworks => $use_frameworks } ); return unless $field; my $tag = $field->tag(); @@ -304,6 +311,35 @@ sub _filter_diacritics { =head1 INTERNAL FUNCTIONS +=head2 _load_authority_types_from_frameworks + +Load authority type mappings from marc_subfield_structure. +Shared by both MARC21 and UNIMARC implementations. +Results are cached at package level. + +=cut + +sub _load_authority_types_from_frameworks { + my $cache_key = shift // 'default'; + + unless ( exists $authority_type_cache{$cache_key} ) { + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare( + "SELECT DISTINCT tagfield, authtypecode + FROM marc_subfield_structure + WHERE frameworkcode = '' AND authtypecode <> ''" + ); + $sth->execute(); + + $authority_type_cache{$cache_key} = {}; + while ( my ( $tag, $auth_type ) = $sth->fetchrow ) { + $authority_type_cache{$cache_key}->{$tag} = $auth_type; + } + } + + return $authority_type_cache{$cache_key}; +} + =head2 _marc_format_handler Returns a C4::Heading::MARC21 or C4::Heading::UNIMARC object @@ -313,9 +349,10 @@ depending on the selected MARC flavour. sub _marc_format_handler { my $marcflavour = uc shift; + my $params = shift // {}; my $pname = "C4::Heading::$marcflavour"; load $pname; - return $pname->new(); + return $pname->new($params); } =head1 AUTHOR diff --git a/C4/Heading/MARC21.pm b/C4/Heading/MARC21.pm index fc7fad7b9c..6976d1931f 100644 --- a/C4/Heading/MARC21.pm +++ b/C4/Heading/MARC21.pm @@ -245,13 +245,40 @@ my %subdivisions = ( =head2 new - my $marc_handler = C4::Heading::MARC21->new(); + my $marc_handler = C4::Heading::MARC21->new(); + my $marc_handler = C4::Heading::MARC21->new( { use_frameworks => 1 } ); + +Creates a new MARC21 heading handler. + +If use_frameworks is true, authority types are loaded from marc_subfield_structure +instead of using the hardcoded defaults. This allows local customization of +authority types per tag via MARC bibliographic frameworks. =cut sub new { - my $class = shift; - return bless {}, $class; + my $class = shift; + my $params = shift // {}; + + my $self = bless {}, $class; + + if ( $params->{use_frameworks} ) { + $self->_load_from_frameworks(); + } + + return $self; +} + +=head2 _load_from_frameworks + +Load authority type mappings from marc_subfield_structure. +Uses shared method from C4::Heading with package-level caching. + +=cut + +sub _load_from_frameworks { + my $self = shift; + $self->{_framework_overrides} = C4::Heading::_load_authority_types_from_frameworks('marc21'); } =head2 valid_heading_tag @@ -331,7 +358,10 @@ sub parse_heading { my $heading_fields = $auth ? {%$auth_heading_fields} : {%$bib_heading_fields}; my $field_info = $heading_fields->{$tag}; - my $auth_type = $field_info->{'auth_type'}; + + # Use framework override if available, otherwise use hardcoded default + my $auth_type = $self->{_framework_overrides}->{$tag} // $field_info->{'auth_type'}; + my $thesaurus = $tag =~ m/6../ ? _get_subject_thesaurus($field) diff --git a/C4/Heading/UNIMARC.pm b/C4/Heading/UNIMARC.pm index 7c2969dce9..9c928359b9 100644 --- a/C4/Heading/UNIMARC.pm +++ b/C4/Heading/UNIMARC.pm @@ -68,17 +68,11 @@ sub new { my $class = shift; unless ( defined $bib_heading_fields ) { - my $dbh = C4::Context->dbh; - my $sth = $dbh->prepare( - "SELECT tagfield, authtypecode - FROM marc_subfield_structure - WHERE frameworkcode = '' AND authtypecode <> ''" - ); - $sth->execute(); + my $types = C4::Heading::_load_authority_types_from_frameworks('unimarc'); $bib_heading_fields = {}; - while ( my ( $tag, $auth_type ) = $sth->fetchrow ) { + foreach my $tag ( keys %$types ) { $bib_heading_fields->{$tag} = { - auth_type => $auth_type, + auth_type => $types->{$tag}, subfields => 'abcdefghjklmnopqrstvxyz', }; } -- 2.50.1 (Apple Git-155)