From 16fa800837360a14ac7735d8f17e2d5d39169e8f Mon Sep 17 00:00:00 2001 From: Matt Blenkinsop Date: Tue, 21 Oct 2025 10:56:32 +0100 Subject: [PATCH] Bug 40192: Add additional fields support to ERM titles This patch extends the additional fields framework to ERM e-holdings titles, allowing libraries to configure custom metadata fields for managing electronic resources. Implementation details: - Extended Koha::ERM::EHoldings::Title and ::Titles with AdditionalFields mixin for object-level support - Added 'erm_titles' table mapping to AdditionalField.pm for API serialization - Updated OpenAPI schema definitions to include extended_attributes in erm_eholdings_title.yaml and expose extended_attribute_types endpoint - Enhanced REST API controller to handle additional fields in title creation/updates - Updated admin interface to allow configuring additional fields for the 'title' table name - Modified Vue.js EHoldingsLocalTitleResource component to display and manage custom fields This enables institutions to track custom metadata (e.g., license details, administrative notes, local identifiers) alongside standard ERM title information. Test plan: 1. Configure additional fields: - Go to Administration > Additional fields - Click "New field" and select table name "title" - Create a text field: Name="License notes", Searchable=Yes - Create an authorized value field: Name="Access level", AV category=LOC (or any available category) - Save both fields 2. Test with local ERM titles: - Navigate to E-resource management > Local titles - Create a new local title or edit an existing one - Verify the "Additional fields" section appears with your configured fields - Fill in "License notes" with test text - Select a value for "Access level" - Save the title 3. Verify persistence: - Reopen the title you just edited - Confirm both additional field values are displayed correctly - Edit one field value and save - Confirm the update persists Signed-off-by: Michaela Signed-off-by: Martin Renvoize --- Koha/AdditionalField.pm | 1 + Koha/ERM/EHoldings/Title.pm | 2 +- Koha/ERM/EHoldings/Titles.pm | 2 +- Koha/REST/V1/ERM/EHoldings/Titles/Local.pm | 9 ++++++ Koha/REST/V1/ExtendedAttributeTypes.pm | 1 + Koha/Schema/Result/ErmEholdingsTitle.pm | 32 +++++++++++++++++++ .../definitions/erm_eholdings_title.yaml | 10 ++++++ .../swagger/paths/erm_eholdings_titles.yaml | 4 +++ .../paths/extended_attribute_types.yaml | 1 + .../en/modules/admin/additional-fields.tt | 3 +- .../prog/js/fetch/erm-api-client.js | 3 +- .../ERM/EHoldingsLocalTitleResource.vue | 4 ++- 12 files changed, 67 insertions(+), 5 deletions(-) diff --git a/Koha/AdditionalField.pm b/Koha/AdditionalField.pm index ead7d3e4fb5..bf5e5c08146 100644 --- a/Koha/AdditionalField.pm +++ b/Koha/AdditionalField.pm @@ -64,6 +64,7 @@ sub to_api { 'erm_packages' => 'package', 'aqorders' => 'order', 'aqbooksellers:vendor' => 'vendor', + 'erm_titles' => 'title' }; my $json = $self->SUPER::to_api($params); diff --git a/Koha/ERM/EHoldings/Title.pm b/Koha/ERM/EHoldings/Title.pm index 6192b75588f..3a07b41460a 100644 --- a/Koha/ERM/EHoldings/Title.pm +++ b/Koha/ERM/EHoldings/Title.pm @@ -22,7 +22,7 @@ use MIME::Base64 qw( decode_base64 ); use Koha::Database; -use base qw(Koha::Object); +use base qw(Koha::Object::Mixin::AdditionalFields Koha::Object); use C4::Biblio qw( AddBiblio TransformKohaToMarc GetMarcFromKohaField ); diff --git a/Koha/ERM/EHoldings/Titles.pm b/Koha/ERM/EHoldings/Titles.pm index 5b914345bd1..cd3390f9e8a 100644 --- a/Koha/ERM/EHoldings/Titles.pm +++ b/Koha/ERM/EHoldings/Titles.pm @@ -21,7 +21,7 @@ use Koha::Database; use Koha::ERM::EHoldings::Title; -use base qw(Koha::Objects); +use base qw(Koha::Objects::Mixin::AdditionalFields Koha::Objects); =head1 NAME diff --git a/Koha/REST/V1/ERM/EHoldings/Titles/Local.pm b/Koha/REST/V1/ERM/EHoldings/Titles/Local.pm index 10e556980b1..9041cc43a77 100644 --- a/Koha/REST/V1/ERM/EHoldings/Titles/Local.pm +++ b/Koha/REST/V1/ERM/EHoldings/Titles/Local.pm @@ -90,12 +90,17 @@ sub add { my $resources = delete $body->{resources} // []; my $create_linked_biblio = delete $body->{create_linked_biblio} // 0; + my $extended_attributes = delete $body->{extended_attributes} // []; my $title = Koha::ERM::EHoldings::Title->new_from_api($body) ->store( { create_linked_biblio => $create_linked_biblio } ); $title->resources($resources); + my @extended_attributes = + map { { 'id' => $_->{field_id}, 'value' => $_->{value} } } @{$extended_attributes}; + $title->extended_attributes( \@extended_attributes ); + $c->res->headers->location( $c->req->url->to_string . '/' . $title->title_id ); return $c->render( status => 201, @@ -152,10 +157,14 @@ sub update { my $resources = delete $body->{resources} // []; my $create_linked_biblio = delete $body->{create_linked_biblio} // 0; + my $extended_attributes = delete $body->{extended_attributes} // []; $title->set_from_api($body)->store( { create_linked_biblio => $create_linked_biblio } ); $title->resources($resources); + my @extended_attributes = + map { { 'id' => $_->{field_id}, 'value' => $_->{value} } } @{$extended_attributes}; + $title->extended_attributes( \@extended_attributes ); return $c->render( status => 200, diff --git a/Koha/REST/V1/ExtendedAttributeTypes.pm b/Koha/REST/V1/ExtendedAttributeTypes.pm index 5893e51d325..782880210e6 100644 --- a/Koha/REST/V1/ExtendedAttributeTypes.pm +++ b/Koha/REST/V1/ExtendedAttributeTypes.pm @@ -55,6 +55,7 @@ sub list { package => 'erm_packages', order => 'aqorders', vendor => 'aqbooksellers:vendor', + title => 'erm_titles' }; return try { diff --git a/Koha/Schema/Result/ErmEholdingsTitle.pm b/Koha/Schema/Result/ErmEholdingsTitle.pm index d8b2ef732b1..a9049e616a7 100644 --- a/Koha/Schema/Result/ErmEholdingsTitle.pm +++ b/Koha/Schema/Result/ErmEholdingsTitle.pm @@ -297,6 +297,38 @@ __PACKAGE__->has_many( # Created by DBIx::Class::Schema::Loader v0.07049 @ 2023-09-13 11:06:45 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:SOFpN45kBTi/qvW5OBnYDA +__PACKAGE__->has_many( + "additional_field_values", + "Koha::Schema::Result::AdditionalFieldValue", + sub { + my ($args) = @_; + + return { + "$args->{foreign_alias}.record_id" => { -ident => "$args->{self_alias}.title_id" }, + + "$args->{foreign_alias}.field_id" => + { -in => \'(SELECT id FROM additional_fields WHERE tablename="erm_titles")' }, + }; + }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +__PACKAGE__->has_many( + "extended_attributes", + "Koha::Schema::Result::AdditionalFieldValue", + sub { + my ($args) = @_; + + return { + "$args->{foreign_alias}.record_id" => { -ident => "$args->{self_alias}.title_id" }, + + "$args->{foreign_alias}.field_id" => + { -in => \'(SELECT id FROM additional_fields WHERE tablename="erm_titles")' }, + }; + }, + { cascade_copy => 0, cascade_delete => 0 }, +); + =head2 koha_objects_class Missing POD for koha_objects_class. diff --git a/api/v1/swagger/definitions/erm_eholdings_title.yaml b/api/v1/swagger/definitions/erm_eholdings_title.yaml index 6bbadd61e4b..c1eae480a32 100644 --- a/api/v1/swagger/definitions/erm_eholdings_title.yaml +++ b/api/v1/swagger/definitions/erm_eholdings_title.yaml @@ -173,6 +173,16 @@ properties: type: - boolean - "null" + extended_attributes: + description: Related additional field values + type: + - array + items: + $ref: extended_attribute_value.yaml + _strings: + type: + - object + - "null" additionalProperties: false required: diff --git a/api/v1/swagger/paths/erm_eholdings_titles.yaml b/api/v1/swagger/paths/erm_eholdings_titles.yaml index 87e037b5b3a..f68eaeb8539 100644 --- a/api/v1/swagger/paths/erm_eholdings_titles.yaml +++ b/api/v1/swagger/paths/erm_eholdings_titles.yaml @@ -153,6 +153,8 @@ type: string enum: - resources.package + - extended_attributes + - +strings collectionFormat: csv - $ref: "../swagger.yaml#/parameters/match" - $ref: "../swagger.yaml#/parameters/order_by" @@ -278,6 +280,8 @@ enum: - resources - resources.package + - extended_attributes + - +strings collectionFormat: csv responses: 200: diff --git a/api/v1/swagger/paths/extended_attribute_types.yaml b/api/v1/swagger/paths/extended_attribute_types.yaml index 4b7b56d71b7..675a80a2506 100644 --- a/api/v1/swagger/paths/extended_attribute_types.yaml +++ b/api/v1/swagger/paths/extended_attribute_types.yaml @@ -24,6 +24,7 @@ - order - subscription - vendor + - title - $ref: "../swagger.yaml#/parameters/match" - $ref: "../swagger.yaml#/parameters/order_by" - $ref: "../swagger.yaml#/parameters/page" diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/additional-fields.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/additional-fields.tt index 78ddf642e7e..3dbb35ca67c 100755 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/additional-fields.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/additional-fields.tt @@ -30,7 +30,7 @@ [% marcfield_tables = ['subscription', 'aqorders'] %] -[% searchable_tables = ['subscription', 'aqbasket', 'aqinvoices', 'erm_licenses', 'erm_agreements', 'erm_packages', 'aqbooksellers:vendor'] %] +[% searchable_tables = ['subscription', 'aqbasket', 'aqinvoices', 'erm_licenses', 'erm_agreements', 'erm_packages', 'aqbooksellers:vendor', 'erm_titles'] %] [% show_marcfield = marcfield_tables.grep('^' _ tablename _ '$').size ? 1 : 0 %] [% show_searchable = searchable_tables.grep('^' _ tablename _ '$').size ? 1 : 0 %] @@ -146,6 +146,7 @@ [% WRAPPER table_option value="erm_agreements" %]Agreements[% END %] [% WRAPPER table_option value="erm_licenses" %]Licenses[% END %] [% WRAPPER table_option value="erm_packages" %]Packages[% END %] + [% WRAPPER table_option value="erm_titles" %]Titles[% END %] [% END %] [% IF CAN_user_updatecharges_remaining_permissions || CAN_user_parameters_manage_accounts %] diff --git a/koha-tmpl/intranet-tmpl/prog/js/fetch/erm-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/fetch/erm-api-client.js index b618e0d672b..ef1737f4afc 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/fetch/erm-api-client.js +++ b/koha-tmpl/intranet-tmpl/prog/js/fetch/erm-api-client.js @@ -155,7 +155,8 @@ export class ERMAPIClient { this.httpClient.get({ endpoint: "eholdings/local/titles/" + id, headers: { - "x-koha-embed": "resources,resources.package", + "x-koha-embed": + "resources,resources.package,extended_attributes,+strings", }, }), delete: id => diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/EHoldingsLocalTitleResource.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/EHoldingsLocalTitleResource.vue index c81a0fb31d4..8a1cf7d0557 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/EHoldingsLocalTitleResource.vue +++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/EHoldingsLocalTitleResource.vue @@ -68,6 +68,7 @@ export default { props, additionalToolbarButtons, moduleStore: "ERMStore", + extendedAttributesResourceType: "title", resourceAttrs: [ { name: "publication_title", @@ -440,7 +441,7 @@ export default { const tableOptions = { url: baseResource.getResourceTableUrl(), options: { - embed: "resources.package", + embed: "resources.package,extended_attributes,+strings", searchCols: [ { search: defaults.publication_title }, null, @@ -488,6 +489,7 @@ export default { let title_id = title.title_id; delete title.title_id; delete title.biblio_id; + delete title._strings; // Cannot use the map/keepAttrs because of the reserved keyword 'package' title.resources.forEach(function (e) { -- 2.51.1