Bug 37893: SIP accounts patron attributes
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com> Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
parent
a0680c94f0
commit
48b845f0eb
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses>.
|
||||
|
||||
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;
|
||||
|
|
@ -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 <http://www.gnu.org/licenses>.
|
||||
|
||||
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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Koha::Schema::Result::SipAccountPatronAttribute>
|
||||
|
||||
=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
|
||||
|
|
|
|||
|
|
@ -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<sip_account_patron_attributes>
|
||||
|
||||
=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</sip_account_patron_attribute_id>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->set_primary_key("sip_account_patron_attribute_id");
|
||||
|
||||
=head1 UNIQUE CONSTRAINTS
|
||||
|
||||
=head2 C<sip_account>
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</sip_account_patron_attribute_id>
|
||||
|
||||
=item * L</sip_account_id>
|
||||
|
||||
=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<Koha::Schema::Result::SipAccount>
|
||||
|
||||
=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;
|
||||
|
|
@ -189,6 +189,11 @@ properties:
|
|||
type:
|
||||
- object
|
||||
- 'null'
|
||||
patron_attributes:
|
||||
description: Related patron_attributes embed
|
||||
type:
|
||||
- array
|
||||
- 'null'
|
||||
terminator:
|
||||
description: terminator
|
||||
type:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
--
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ export class SIP2APIClient {
|
|||
get: id =>
|
||||
this.get({
|
||||
endpoint: "accounts/" + id,
|
||||
headers: {
|
||||
"x-koha-embed": "patron_attributes",
|
||||
},
|
||||
}),
|
||||
getAll: params =>
|
||||
this.getAll({
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue