From f1ca516b60dc237ead939b21ec4d41c7512a7069 Mon Sep 17 00:00:00 2001 From: Nick Clemens Date: Sat, 15 Aug 2026 15:45:08 +0000 Subject: [PATCH] Bug 39829: Allow custom title in SIP responses This patch adds a custom aj_field_template to the sip accounts This allows libraries to specify a template for the title (AJ) field in sip messages The template allows using item and biblio fields (item provides access to biblio. but we pass both in order to makes the templates simpler To test: 1 - Install patches 2 - updatedatabase, yarn build, restart_all 3 - Setup a sip account, KTD already has a sip patron, term1, you can use that don't add the aj_field_template yet 4 - Find or create a biblio with 245a and 245b like 245$aTitle : $bSubtitle and add item with TESTBARCODE 5 - From the command line: perl misc/sip_cli_emulator.pl -su term1 -sp term1 -l CPL -t CR -a localhost -p 6001 -m item_information --item TESTBARCODE 6 - Confirm AJ field shows "Title :" as expected 7 - Edit sip account, set aj_field_template to: [% biblio.title %] [% biblio.subtitle %] [% item.barcode %] 8 - Repeat the sip test 9 - Confirm you get the response like: Title : Subtitle TESTBARCODE 10 - Sign off! Signed-off-by: Bob Bennhoff Signed-off-by: Paul Derscheid Signed-off-by: Pedro Amorim --- C4/SIP/Sip.pm | 3 +- C4/SIP/Sip/MsgType.pm | 61 ++++++++++++------- api/v1/swagger/definitions/sip2_account.yaml | 5 ++ .../data/mysql/atomicupdate/bug_39829.pl | 25 ++++++++ installer/data/mysql/kohastructure.sql | 1 + .../components/SIP2/SIP2AccountResource.vue | 13 ++++ t/cypress/integration/SIP2/Accounts.ts | 1 + t/db_dependent/Koha/SIP2/Accounts.t | 2 + t/db_dependent/SIP/Message.t | 57 ++++++++++++++++- 9 files changed, 144 insertions(+), 24 deletions(-) create mode 100755 installer/data/mysql/atomicupdate/bug_39829.pl diff --git a/C4/SIP/Sip.pm b/C4/SIP/Sip.pm index b4056b59d0..f8ea568383 100644 --- a/C4/SIP/Sip.pm +++ b/C4/SIP/Sip.pm @@ -97,6 +97,7 @@ sub add_field { ); $value = ''; } + $value =~ s/\r\n|\r|\n/ /g; # CR or LF terminates a sip message # Protect against them in sip text fields @@ -140,7 +141,7 @@ sub maybe_add { } return ( defined($value) && length($value) ) - ? add_field( $fid, $value ) + ? add_field( $fid, $value, $server ) : ''; } diff --git a/C4/SIP/Sip/MsgType.pm b/C4/SIP/Sip/MsgType.pm index 1b8f0f8ca8..7cd3c4606e 100644 --- a/C4/SIP/Sip/MsgType.pm +++ b/C4/SIP/Sip/MsgType.pm @@ -24,7 +24,8 @@ use Koha::Patrons; use Koha::Patron::Attributes; use Koha::Plugins; use Koha::Items; -use Koha::DateUtils qw( output_pref ); +use Koha::DateUtils qw( output_pref ); +use Koha::TemplateUtils qw( process_tt ); use UNIVERSAL::can; @@ -625,10 +626,10 @@ sub handle_checkout { $resp .= timestamp; # Now for the variable fields - $resp .= add_field( FID_INST_ID, $inst, $server ); - $resp .= add_field( FID_PATRON_ID, $patron_id, $server ); - $resp .= add_field( FID_ITEM_ID, $item_id, $server ); - $resp .= add_field( FID_TITLE_ID, $item->title_id, $server ); + $resp .= add_field( FID_INST_ID, $inst, $server ); + $resp .= add_field( FID_PATRON_ID, $patron_id, $server ); + $resp .= add_field( FID_ITEM_ID, $item_id, $server ); + $resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ) ); if ( $item->due_date ) { my $due_date = $account->{format_due_date} @@ -663,7 +664,7 @@ sub handle_checkout { # If the item is valid, provide the title, otherwise # leave it blank - $resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : '', $server ); + $resp .= add_field( FID_TITLE_ID, $item ? _format_title( { item => $item, server => $server } ) : "" ); # Due date is required. Since it didn't get checked out, # it's not due, so leave the date blank @@ -763,7 +764,7 @@ sub handle_checkin { if ($item) { $resp .= add_field( FID_PERM_LOCN, $item->permanent_location, $server ); - $resp .= maybe_add( FID_TITLE_ID, $item->title_id, $server ); + $resp .= maybe_add( FID_TITLE_ID, _format_title( { item => $item, server => $server } ) ); $resp .= $item->build_additional_item_fields_string($server); } else { $resp .= add_field( FID_PERM_LOCN, "", $server ); @@ -1343,8 +1344,8 @@ sub handle_item_information { $resp .= maybe_add( FID_SCREEN_MSG, "Item is damaged", $server ); } - $resp .= add_field( FID_ITEM_ID, $item->id, $server ); - $resp .= add_field( FID_TITLE_ID, $item->title_id, $server ); + $resp .= add_field( FID_ITEM_ID, $item->id, $server ); + $resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ) ); $resp .= maybe_add( FID_MEDIA_TYPE, $item->sip_media_type, $server ); $resp .= maybe_add( FID_PERM_LOCN, $item->permanent_location, $server ); @@ -1429,8 +1430,8 @@ sub handle_item_status_update { $resp .= $status->ok ? '1' : '0'; $resp .= timestamp; - $resp .= add_field( FID_ITEM_ID, $item->id, $server ); - $resp .= add_field( FID_TITLE_ID, $item->title_id, $server ); + $resp .= add_field( FID_ITEM_ID, $item->id, $server ); + $resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ) ); $resp .= maybe_add( FID_ITEM_PROPS, $item->sip_item_properties, $server ); } @@ -1551,10 +1552,10 @@ sub handle_hold { ( $status->expiration_date ) and $resp .= maybe_add( FID_EXPIRATION, timestamp( $status->expiration_date ), $server ); - $resp .= maybe_add( FID_QUEUE_POS, $status->queue_position, $server ); - $resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location, $server ); - $resp .= maybe_add( FID_ITEM_ID, $status->item->id, $server ); - $resp .= maybe_add( FID_TITLE_ID, $status->item->title_id, $server ); + $resp .= maybe_add( FID_QUEUE_POS, $status->queue_position, $server ); + $resp .= maybe_add( FID_PICKUP_LOCN, $status->pickup_location, $server ); + $resp .= maybe_add( FID_ITEM_ID, $status->item->id, $server ); + $resp .= maybe_add( FID_TITLE_ID, _format_title( { item => $status->item, server => $server } ), $server ); } else { # Not ok. still need required fields @@ -1618,9 +1619,9 @@ sub handle_renew { } $resp .= sipbool( desensitize( { status => $status, patron => $patron, server => $server } ) ); $resp .= timestamp; - $resp .= add_field( FID_PATRON_ID, $patron->id, $server ); - $resp .= add_field( FID_ITEM_ID, $item->id, $server ); - $resp .= add_field( FID_TITLE_ID, $item->title_id, $server ); + $resp .= add_field( FID_PATRON_ID, $patron->id, $server ); + $resp .= add_field( FID_ITEM_ID, $item->id, $server ); + $resp .= add_field( FID_TITLE_ID, _format_title( { item => $item, server => $server } ) ); if ( $item->due_date ) { $resp .= add_field( FID_DUE_DATE, timestamp( $item->due_date ), $server ); } else { @@ -1641,10 +1642,13 @@ sub handle_renew { # If we found the patron or the item, the return the ILS # information, otherwise echo back the information we received # from the terminal - $resp .= add_field( FID_PATRON_ID, $patron ? $patron->id : $patron_id, $server ); - $resp .= add_field( FID_ITEM_ID, $item ? $item->id : $item_id, $server ); - $resp .= add_field( FID_TITLE_ID, $item ? $item->title_id : $title_id, $server ); - $resp .= add_field( FID_DUE_DATE, '', $server ); + $resp .= add_field( FID_PATRON_ID, $patron ? $patron->id : $patron_id, $server ); + $resp .= add_field( FID_ITEM_ID, $item ? $item->id : $item_id, $server ); + $resp .= add_field( + FID_TITLE_ID, $item ? _format_title( { item => $item, server => $server } ) : $title_id, + $server + ); + $resp .= add_field( FID_DUE_DATE, '', $server ); } if ( $status->fee_amount ) { @@ -1900,6 +1904,19 @@ sub desensitize { return 1; } +sub _format_title { + my ($params) = @_; + my $server = $params->{server}; + my $item = $params->{item}; + if ( defined $server->{account}->{aj_field_template} && $server->{account}->{aj_field_template} ne '' ) { + return process_tt( + $server->{account}->{aj_field_template}, + { biblio => $item->{object}->biblio, item => $item->{object} } + ); + } else { + return $item->title_id; + } +} 1; __END__ diff --git a/api/v1/swagger/definitions/sip2_account.yaml b/api/v1/swagger/definitions/sip2_account.yaml index 8d18dc7618..f7d6fc6cb1 100644 --- a/api/v1/swagger/definitions/sip2_account.yaml +++ b/api/v1/swagger/definitions/sip2_account.yaml @@ -13,6 +13,11 @@ properties: type: - string - 'null' + aj_field_template: + description: aj field template + type: + - string + - 'null' allow_additional_materials_checkout: description: allow additional materials checkout type: diff --git a/installer/data/mysql/atomicupdate/bug_39829.pl b/installer/data/mysql/atomicupdate/bug_39829.pl new file mode 100755 index 0000000000..f6209ea102 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_39829.pl @@ -0,0 +1,25 @@ +use Modern::Perl; +use Koha::Installer::Output qw(say_warning say_success say_info); + +return { + bug_number => "39829", + description => "Add SIP aj_field_template to allow customizing title info", + up => sub { + my ($args) = @_; + my ( $dbh, $out ) = @$args{qw(dbh out)}; + + unless ( column_exists( 'sip_accounts', 'aj_field_template' ) ) { + $dbh->do( + q{ + ALTER TABLE sip_accounts + ADD COLUMN + `aj_field_template` text NULL + AFTER `ae_field_template` + } + ); + say $out "Added column 'sip_accounts.aj_field_template'"; + } else { + say $out "Column already existed: 'sip_accounts.aj_field_template'"; + } + }, +}; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 422be2a470..649b8d30a5 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -6333,6 +6333,7 @@ CREATE TABLE `sip_accounts` ( `sip_account_id` int(11) NOT NULL AUTO_INCREMENT, `sip_institution_id` int(11) NOT NULL COMMENT 'Foreign key to sip_institutions.sip_institution_id', `ae_field_template` text DEFAULT NULL, + `aj_field_template` text DEFAULT NULL, `allow_additional_materials_checkout` tinyint(1) DEFAULT NULL, `allow_empty_passwords` tinyint(1) DEFAULT NULL, `allow_fields` varchar(255) DEFAULT NULL, diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/SIP2/SIP2AccountResource.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/SIP2/SIP2AccountResource.vue index 457bf595e4..73a6bd2ce8 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/SIP2/SIP2AccountResource.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/SIP2/SIP2AccountResource.vue @@ -957,6 +957,19 @@ export default { "(Hold patron name) is the name of the patron who has placed a hold on the item being checked in. Use Template Toolkit syntax - the patron fields (Koha::Patron) for the hold patron and item fields are available." ), }, + { + name: "aj_field_template", + type: "textarea", + label: "AJ " + __("field template"), + placeholder: "[% biblio.title %]", + hideIn: ["List"], + group: "templates", + toolTip: + "AJ " + + __( + "(Title) is the item's title as sent to the SIP2 client. Use Template Toolkit syntax - the item and biblio fields (Koha::Item, Koha::Biblio) are available." + ), + }, ]; const baseResource = useBaseResource({ diff --git a/t/cypress/integration/SIP2/Accounts.ts b/t/cypress/integration/SIP2/Accounts.ts index 85391c8d7b..e7787d42c3 100644 --- a/t/cypress/integration/SIP2/Accounts.ts +++ b/t/cypress/integration/SIP2/Accounts.ts @@ -3,6 +3,7 @@ import { mount } from "@cypress/vue"; function get_account() { return { ae_field_template: "", + aj_field_template: "", allow_additional_materials_checkout: false, allow_empty_passwords: false, allow_fields: null, diff --git a/t/db_dependent/Koha/SIP2/Accounts.t b/t/db_dependent/Koha/SIP2/Accounts.t index c35e90c988..e9dd81436a 100755 --- a/t/db_dependent/Koha/SIP2/Accounts.t +++ b/t/db_dependent/Koha/SIP2/Accounts.t @@ -260,6 +260,7 @@ subtest 'template fields accept long text' => sub { class => 'Koha::SIP2::Accounts', value => { ae_field_template => $template, + aj_field_template => $template, av_field_template => $template, da_field_template => $template, }, @@ -268,6 +269,7 @@ subtest 'template fields accept long text' => sub { my $stored = $account->get_from_storage; is( $stored->ae_field_template, $template, 'ae_field_template can store large templates' ); + is( $stored->aj_field_template, $template, 'aj_field_template can store large templates' ); is( $stored->av_field_template, $template, 'av_field_template can store large templates' ); is( $stored->da_field_template, $template, 'da_field_template can store large templates' ); diff --git a/t/db_dependent/SIP/Message.t b/t/db_dependent/SIP/Message.t index f0db09403f..1083068a24 100755 --- a/t/db_dependent/SIP/Message.t +++ b/t/db_dependent/SIP/Message.t @@ -22,7 +22,7 @@ use Modern::Perl; use Test::NoWarnings; -use Test::More tests => 23; +use Test::More tests => 24; use Test::Exception; use Test::MockObject; use Test::MockModule; @@ -2009,6 +2009,61 @@ subtest 'Test convert_nonprinting_characters' => sub { $schema->storage->txn_rollback; }; +subtest 'Test _format_title' => sub { + + plan tests => 2; + my $schema = Koha::Database->new->schema; + $schema->storage->txn_begin; + my $builder = t::lib::TestBuilder->new(); + my $branchcode = $builder->build( { source => 'Branch' } )->{branchcode}; + my ( $response, $findpatron ); + my $mocks = create_mocks( \$response, \$findpatron, \$branchcode ); + my $mockILS = $mocks->{ils}; + my $server = { ils => $mockILS, account => {} }; + + my $patron = $builder->build( + { + source => 'Borrower', + value => { + password => hash_password(PATRON_PW), + branchcode => $branchcode, + }, + } + ); + my $biblio = $builder->build_sample_biblio( + { + title => 'Title', + } + ); + $biblio->subtitle("Subtitle")->store; + my $item = $builder->build_sample_item( + { + damaged => 0, + withdrawn => 0, + itemlost => 0, + restricted => 0, + homebranch => $branchcode, + holdingbranch => $branchcode, + biblionumber => $biblio->biblionumber + } + ); + my $sip_item = C4::SIP::ILS::Item->new( $item->barcode ); + + delete $server->{account}->{aj_field_template}; + is( + C4::SIP::Sip::MsgType::_format_title( { item => $sip_item, server => $server } ), "Title", + "When aj_field_template undefined, we get the simple title" + ); + + $server->{account}->{aj_field_template} = "[% biblio.title %] [% biblio.subtitle %] [% item.barcode %]"; + is( + C4::SIP::Sip::MsgType::_format_title( { item => $sip_item, server => $server } ), + "Title Subtitle " . $item->barcode, "When aj_field_template set, we get expected fields" + ); + + $schema->storage->txn_rollback; +}; + # Helper routines sub create_mocks {