From 48b845f0eb76c39f7ebcf56fdcd33df88c6f1bd4 Mon Sep 17 00:00:00 2001 From: Pedro Amorim Date: Thu, 30 Jan 2025 12:36:38 -0100 Subject: [PATCH] Bug 37893: SIP accounts patron attributes Signed-off-by: Andrew Fuerste Henry Signed-off-by: Lisette Scheer Signed-off-by: Martin Renvoize Signed-off-by: Lucas Gass --- Koha/REST/V1/SIP2/Accounts.pm | 43 +++--- Koha/SIP2/Account.pm | 35 +++++ Koha/SIP2/Account/PatronAttribute.pm | 59 +++++++++ Koha/SIP2/Account/PatronAttributes.pm | 52 ++++++++ Koha/SIP2/Object.pm | 14 +- Koha/Schema/Result/SipAccount.pm | 15 +++ .../Result/SipAccountPatronAttribute.pm | 123 ++++++++++++++++++ api/v1/swagger/definitions/sip2_account.yaml | 5 + api/v1/swagger/paths/sip2_accounts.yaml | 11 ++ .../data/mysql/atomicupdate/bug_37893.pl | 23 +++- installer/data/mysql/kohastructure.sql | 19 +++ .../prog/js/fetch/sip2-api-client.js | 3 + .../components/SIP2/SIP2AccountResource.vue | 58 +++++++++ 13 files changed, 426 insertions(+), 34 deletions(-) create mode 100644 Koha/SIP2/Account/PatronAttribute.pm create mode 100644 Koha/SIP2/Account/PatronAttributes.pm create mode 100644 Koha/Schema/Result/SipAccountPatronAttribute.pm diff --git a/Koha/REST/V1/SIP2/Accounts.pm b/Koha/REST/V1/SIP2/Accounts.pm index e15afb7778..09ef392170 100644 --- a/Koha/REST/V1/SIP2/Accounts.pm +++ b/Koha/REST/V1/SIP2/Accounts.pm @@ -38,8 +38,7 @@ sub list { return try { my $insitutions = $c->objects->search( Koha::SIP2::Accounts->new ); return $c->render( status => 200, openapi => $insitutions ); - } - catch { + } catch { $c->unhandled_exception($_); }; @@ -64,8 +63,7 @@ sub get { status => 200, openapi => $account ); - } - catch { + } catch { $c->unhandled_exception($_); }; } @@ -85,17 +83,19 @@ sub add { my $body = $c->req->json; - my $account = Koha::SIP2::Account->new_from_api($body)->store; + my $patron_attributes = delete $body->{patron_attributes} // []; - $c->res->headers->location($c->req->url->to_string . '/' . $account->sip_account_id); + my $account = Koha::SIP2::Account->new_from_api($body)->store; + $account->patron_attributes($patron_attributes); + + $c->res->headers->location( $c->req->url->to_string . '/' . $account->sip_account_id ); return $c->render( status => 201, openapi => $c->objects->to_api($account), ); } ); - } - catch { + } catch { my $to_api_mapping = Koha::SIP2::Account->new->to_api_mapping; @@ -105,15 +105,10 @@ sub add { status => 409, openapi => { error => $_->error, conflict => $_->duplicate_id } ); - } - elsif ( $_->isa('Koha::Exceptions::BadParameter') ) { + } elsif ( $_->isa('Koha::Exceptions::BadParameter') ) { return $c->render( status => 400, - openapi => { - error => "Given " - . $to_api_mapping->{ $_->parameter } - . " does not exist" - } + openapi => { error => "Given " . $to_api_mapping->{ $_->parameter } . " does not exist" } ); } } @@ -142,35 +137,33 @@ sub update { my $body = $c->req->json; - $account->set_from_api($body)->store; + my $patron_attributes = delete $body->{patron_attributes} // []; - $c->res->headers->location($c->req->url->to_string . '/' . $account->sip_account_id); + $account->set_from_api($body)->store; + $account->patron_attributes($patron_attributes); + + $c->res->headers->location( $c->req->url->to_string . '/' . $account->sip_account_id ); return $c->render( status => 200, openapi => $c->objects->to_api($account), ); } ); - } - catch { + } catch { my $to_api_mapping = Koha::SIP2::Account->new->to_api_mapping; if ( blessed $_ ) { if ( $_->isa('Koha::Exceptions::BadParameter') ) { return $c->render( status => 400, - openapi => { - error => "Given " - . $to_api_mapping->{ $_->parameter } - . " does not exist" - } + openapi => { error => "Given " . $to_api_mapping->{ $_->parameter } . " does not exist" } ); } } $c->unhandled_exception($_); }; -}; +} =head3 delete diff --git a/Koha/SIP2/Account.pm b/Koha/SIP2/Account.pm index c82fdb0c91..2df9a972e3 100644 --- a/Koha/SIP2/Account.pm +++ b/Koha/SIP2/Account.pm @@ -19,6 +19,7 @@ use Modern::Perl; use base qw(Koha::SIP2::Object Koha::Object); +use Koha::SIP2::Account::PatronAttributes; use Koha::SIP2::Institution; =head1 NAME @@ -68,6 +69,14 @@ sub get_for_config { delete $unblessed->{error_detect}; } + # Add relations + # If a single element, return that, otherwise return a list + + my @patron_attributes = map { $_->get_for_config } @{ $self->patron_attributes->as_list }; + if (@patron_attributes) { + $unblessed->{patron_attribute} = scalar(@patron_attributes) == 1 ? $patron_attributes[0] : \@patron_attributes; + } + return $unblessed; } @@ -84,6 +93,32 @@ sub institution { return Koha::SIP2::Institution->_new_from_dbic($institution_rs); } +=head3 patron_attributes + +Returns the patron attributes for this SIP account + +=cut + +sub patron_attributes { + my ( $self, $patron_attributes ) = @_; + + if ($patron_attributes) { + my $schema = $self->_result->result_source->schema; + $schema->txn_do( + sub { + $self->patron_attributes->delete; + + for my $patron_attribute (@$patron_attributes) { + $self->_result->add_to_sip_account_patron_attributes($patron_attribute); + } + } + ); + } + + my $patron_attributes_rs = $self->_result->sip_account_patron_attributes; + return Koha::SIP2::Account::PatronAttributes->_new_from_dbic($patron_attributes_rs); +} + =head3 type =cut diff --git a/Koha/SIP2/Account/PatronAttribute.pm b/Koha/SIP2/Account/PatronAttribute.pm new file mode 100644 index 0000000000..9ad092d71f --- /dev/null +++ b/Koha/SIP2/Account/PatronAttribute.pm @@ -0,0 +1,59 @@ +package Koha::SIP2::Account::PatronAttribute; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Koha::Database; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::SIP2::Account::PatronAttribute - Koha SIP2 Account Patron Attribute Object class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 get_for_config + +Returns the patron attribute hashref as expected by C4/SIP/Sip/Configuration->new; + +=cut + +sub get_for_config { + my ($self) = @_; + + return { + 'field' => $self->field, + 'code' => $self->code, + }; +} + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'SipAccountPatronAttribute'; +} + +1; diff --git a/Koha/SIP2/Account/PatronAttributes.pm b/Koha/SIP2/Account/PatronAttributes.pm new file mode 100644 index 0000000000..b14e0820bc --- /dev/null +++ b/Koha/SIP2/Account/PatronAttributes.pm @@ -0,0 +1,52 @@ +package Koha::SIP2::Account::PatronAttributes; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Koha::Database; + +use Koha::SIP2::Account::PatronAttribute; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::SIP2::Account::PatronAttributes - Koha SIP2 Account Patron Attributes Object set class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub _type { + return 'SipAccountPatronAttribute'; +} + +=head3 object_class + +=cut + +sub object_class { + return 'Koha::SIP2::Account::PatronAttribute'; +} + +1; diff --git a/Koha/SIP2/Object.pm b/Koha/SIP2/Object.pm index 81ce138f70..860cf6aa2f 100644 --- a/Koha/SIP2/Object.pm +++ b/Koha/SIP2/Object.pm @@ -21,7 +21,7 @@ use base qw(Koha::Object); use DateTime; -use Koha::SIP2::ServerParams; +# use Koha::SIP2::ServerParams; =head1 NAME @@ -57,13 +57,13 @@ sub delete { return $self->SUPER::delete; } -sub _update_config_timestamp { - my $timestamp = DateTime->now; +# sub _update_config_timestamp { +# my $timestamp = DateTime->now; - my $config_timestamp = Koha::SIP2::ServerParams->find( { key => 'config_timestamp' } ) - || Koha::SIP2::ServerParam->new( { key => 'config_timestamp' } ); - $config_timestamp->value( DateTime->now->epoch )->store; -} +# my $config_timestamp = Koha::SIP2::ServerParams->find( { key => 'config_timestamp' } ) +# || Koha::SIP2::ServerParam->new( { key => 'config_timestamp' } ); +# $config_timestamp->value( DateTime->now->epoch )->store; +# } 1; diff --git a/Koha/Schema/Result/SipAccount.pm b/Koha/Schema/Result/SipAccount.pm index 6128b92ccf..6f544d3fb4 100644 --- a/Koha/Schema/Result/SipAccount.pm +++ b/Koha/Schema/Result/SipAccount.pm @@ -346,6 +346,21 @@ __PACKAGE__->add_unique_constraint("account_login_id", ["login_id"]); =head1 RELATIONS +=head2 sip_account_patron_attributes + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "sip_account_patron_attributes", + "Koha::Schema::Result::SipAccountPatronAttribute", + { "foreign.sip_account_id" => "self.sip_account_id" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + =head2 sip_institution Type: belongs_to diff --git a/Koha/Schema/Result/SipAccountPatronAttribute.pm b/Koha/Schema/Result/SipAccountPatronAttribute.pm new file mode 100644 index 0000000000..64aac95308 --- /dev/null +++ b/Koha/Schema/Result/SipAccountPatronAttribute.pm @@ -0,0 +1,123 @@ +use utf8; +package Koha::Schema::Result::SipAccountPatronAttribute; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::SipAccountPatronAttribute + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("sip_account_patron_attributes"); + +=head1 ACCESSORS + +=head2 sip_account_patron_attribute_id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 sip_account_id + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +Foreign key to sip_accounts.sip_account_id + +=head2 field + + data_type: 'varchar' + is_nullable: 0 + size: 80 + +SIP field name e.g. XY + +=head2 code + + data_type: 'varchar' + is_nullable: 0 + size: 80 + +Patron attribute code as in borrower_attribute_types.code + +=cut + +__PACKAGE__->add_columns( + "sip_account_patron_attribute_id", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "sip_account_id", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "field", + { data_type => "varchar", is_nullable => 0, size => 80 }, + "code", + { data_type => "varchar", is_nullable => 0, size => 80 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("sip_account_patron_attribute_id"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint( + "sip_account", + ["sip_account_patron_attribute_id", "sip_account_id"], +); + +=head1 RELATIONS + +=head2 sip_account + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "sip_account", + "Koha::Schema::Result::SipAccount", + { sip_account_id => "sip_account_id" }, + { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-01-30 12:33:46 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:aKRw1tnA6e32AxFjm4g7YQ + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/api/v1/swagger/definitions/sip2_account.yaml b/api/v1/swagger/definitions/sip2_account.yaml index f498cc8420..016c7365ca 100644 --- a/api/v1/swagger/definitions/sip2_account.yaml +++ b/api/v1/swagger/definitions/sip2_account.yaml @@ -189,6 +189,11 @@ properties: type: - object - 'null' + patron_attributes: + description: Related patron_attributes embed + type: + - array + - 'null' terminator: description: terminator type: diff --git a/api/v1/swagger/paths/sip2_accounts.yaml b/api/v1/swagger/paths/sip2_accounts.yaml index 4b871c2df3..8e085a3310 100644 --- a/api/v1/swagger/paths/sip2_accounts.yaml +++ b/api/v1/swagger/paths/sip2_accounts.yaml @@ -23,6 +23,7 @@ type: string enum: - institution + - patron_attributes collectionFormat: csv - $ref: "../swagger.yaml#/parameters/match" - $ref: "../swagger.yaml#/parameters/order_by" @@ -132,6 +133,16 @@ - application/json parameters: - $ref: "../swagger.yaml#/parameters/sip_account_id_pp" + - name: x-koha-embed + in: header + required: false + description: Embed list sent as a request header + type: array + items: + type: string + enum: + - patron_attributes + collectionFormat: csv responses: 200: description: A SIP2 account diff --git a/installer/data/mysql/atomicupdate/bug_37893.pl b/installer/data/mysql/atomicupdate/bug_37893.pl index f33ed6cf1c..1792c620de 100755 --- a/installer/data/mysql/atomicupdate/bug_37893.pl +++ b/installer/data/mysql/atomicupdate/bug_37893.pl @@ -90,8 +90,8 @@ return { my $encoding = $SIPconfig->{accounts}->{$account_key}->{encoding}; my $error_detect; - if ( defined $SIPconfig->{accounts}->{$account_key}->{'error-detect'} ){ - $error_detect = $SIPconfig->{accounts}->{$account_key}->{'error-detect'} eq 'enabled' ? 1 : 0 + if ( defined $SIPconfig->{accounts}->{$account_key}->{'error-detect'} ) { + $error_detect = $SIPconfig->{accounts}->{$account_key}->{'error-detect'} eq 'enabled' ? 1 : 0; } my $format_due_date = $SIPconfig->{accounts}->{$account_key}->{format_due_date}; @@ -156,6 +156,25 @@ return { $show_outstanding_amount, $terminator ); + + my $new_account_id = $dbh->last_insert_id( undef, undef, "sip_accounts", "sip_account_id" ); + + # Accounts patron attributes + my @patron_attributes = + ref $SIPconfig->{accounts}->{$account_key}->{patron_attribute} eq "ARRAY" + ? @{ $SIPconfig->{accounts}->{$account_key}->{patron_attribute} } + : ( $SIPconfig->{accounts}->{$account_key}->{patron_attribute} ); + + my $insert_patron_attributes = $dbh->prepare( + q{INSERT IGNORE INTO sip_account_patron_attributes (sip_account_id, field, code) VALUES (?, ?, ?)}); + + foreach my $patron_attribute (@patron_attributes) { + $insert_patron_attributes->execute( + $new_account_id, + $patron_attribute->{field}, + $patron_attribute->{code} + ) if $patron_attribute; + } } }, }; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index bcee71427a..473f27e3f7 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -6130,6 +6130,25 @@ CREATE TABLE `sip_accounts` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `sip_account_patron_attributes` +-- + +DROP TABLE IF EXISTS `sip_account_patron_attributes`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `sip_account_patron_attributes` ( + `sip_account_patron_attribute_id` int(11) NOT NULL AUTO_INCREMENT, + `sip_account_id` int(11) NOT NULL COMMENT 'Foreign key to sip_accounts.sip_account_id', + `field` varchar(80) NOT NULL COMMENT 'SIP field name e.g. XY', + `code` varchar(80) NOT NULL COMMENT 'Patron attribute code as in borrower_attribute_types.code', + PRIMARY KEY (`sip_account_patron_attribute_id`), + UNIQUE KEY `sip_account` (`sip_account_patron_attribute_id`,`sip_account_id`), + KEY `sip_account_patron_attributes_ibfk_1` (`sip_account_id`), + CONSTRAINT `sip_account_patron_attributes_ibfk_1` FOREIGN KEY (`sip_account_id`) REFERENCES `sip_accounts` (`sip_account_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `sip_institutions` -- diff --git a/koha-tmpl/intranet-tmpl/prog/js/fetch/sip2-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/fetch/sip2-api-client.js index 55586a11bf..6d8dad6f67 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/fetch/sip2-api-client.js +++ b/koha-tmpl/intranet-tmpl/prog/js/fetch/sip2-api-client.js @@ -47,6 +47,9 @@ export class SIP2APIClient { get: id => this.get({ endpoint: "accounts/" + id, + headers: { + "x-koha-embed": "patron_attributes", + }, }), getAll: params => this.getAll({ 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 758a12f561..1e72aecb47 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 @@ -392,6 +392,59 @@ export default { placeholder: "CRLF", group: "Details", }, + { + name: "patron_attributes", + type: "relationshipWidget", + showElement: { + type: "table", + columnData: "patron_attributes", + hidden: account => !!account.patron_attributes?.length, + columns: [ + { + name: __("Field"), + value: "field", + }, + { + name: __("Code"), + value: "code", + }, + ], + }, + group: __("SIP response mappings"), + componentProps: { + resourceRelationships: { + resourceProperty: "patron_attributes", + }, + relationshipStrings: { + nameLowerCase: __("patron attribute"), + nameUpperCase: __("Patron attribute"), + namePlural: __("patron attributes"), + }, + newRelationshipDefaultAttrs: { + type: "object", + value: { + field: null, + code: null, + }, + }, + }, + relationshipFields: [ + { + name: "field", + required: true, + type: "text", + placeholder: "XY", + label: __("Field"), + }, + { + name: "code", + required: true, + type: "text", + placeholder: "CODE", + label: __("Code"), + }, + ], + }, { name: "ae_field_template", type: "textarea", @@ -607,6 +660,11 @@ export default { account.terminator = null; } + account.patron_attributes = account.patron_attributes.map( + ({ account_id, account_patron_attribute_id, ...keepAttrs }) => + keepAttrs + ); + const client = APIClient.sip2; if (sip_account_id) { client.accounts.update(account, sip_account_id).then(