Browse Source
This patchset adds the display of all matches found during import to the import management screen A staff member with the permission to manage batches will be able to select for any individual record which match, or none, should be used during import To test: 1 - Import a batch of records or export existing records from your catalog 2 - Import the file (again) and select a matching rule that will find matches 3 - Note that you now have radio buttons allowing you to select a record, or none 4 - Test scenarios: I - When 'Action if matching record found' is 'Ignore' a - Imported record ignored if match is selected b - 'Action if no match found' followed if no match is selected (Ignore matches) II - When 'Action if matching record found' is 'Replace' a - The chosen record is the one overlayed (you can edit the chosen record before importing to confirm) b - 'Action if no match found' followed if no match is selected (Ignore matches) III - When 'Action if matching record found' is 'Add incoming record' a - Record is added regardless of matches 5 - Confirm 'Diff' 'View' links work as expected 6 - Confirm that after records are imported the radio buttons to choose are disabled Signed-off-by: Andrew Fuerste-Henry <andrew@bywatersolutions.com> Bug 22785: API files Signed-off-by: Ben Daeuber <bdaeuber@cityoffargo.com> Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de> Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com> Signed-off-by: Fridolin Somers <fridolin.somers@biblibre.com>rmain2205
16 changed files with 756 additions and 67 deletions
@ -0,0 +1,106 @@ |
|||
package Koha::REST::V1::ImportRecordMatches; |
|||
|
|||
# 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 Mojo::Base 'Mojolicious::Controller'; |
|||
|
|||
use Koha::Import::Record::Matches; |
|||
|
|||
use Try::Tiny; |
|||
|
|||
=head1 API |
|||
|
|||
=head2 Methods |
|||
|
|||
=cut |
|||
|
|||
=head3 unset_chosen |
|||
|
|||
Method that handles unselecting all chosen matches for an import record |
|||
|
|||
DELETE /api/v1/import/{import_batch_id}/records/{import_record_id}/matches/chosen |
|||
|
|||
=cut |
|||
|
|||
sub unset_chosen { |
|||
my $c = shift->openapi->valid_input or return; |
|||
|
|||
my $import_record_id = $c->validation->param('import_record_id'); |
|||
my $matches = Koha::Import::Record::Matches->search({ |
|||
import_record_id => $import_record_id, |
|||
}); |
|||
unless ($matches) { |
|||
return $c->render( |
|||
status => 404, |
|||
openapi => { error => "No matches not found" } |
|||
); |
|||
} |
|||
return try { |
|||
$matches->update({ chosen => 0 }); |
|||
return $c->render( status => 204, openapi => $matches ); |
|||
} |
|||
catch { |
|||
$c->unhandled_exception($_); |
|||
}; |
|||
} |
|||
|
|||
=head3 set_chosen |
|||
|
|||
Method that handles modifying if a Koha::Import::Record::Match object has been chosen for overlay |
|||
|
|||
PUT /api/v1/import/{import_batch_id}/records/{import_record_id}/matches/chosen |
|||
|
|||
Body should contain the condidate_match_id to chose |
|||
|
|||
=cut |
|||
|
|||
sub set_chosen { |
|||
my $c = shift->openapi->valid_input or return; |
|||
|
|||
my $import_record_id = $c->validation->param('import_record_id'); |
|||
my $body = $c->validation->param('body'); |
|||
my $candidate_match_id = $body->{'candidate_match_id'}; |
|||
|
|||
my $match = Koha::Import::Record::Matches->find({ |
|||
import_record_id => $import_record_id, |
|||
candidate_match_id => $candidate_match_id |
|||
}); |
|||
|
|||
unless ($match) { |
|||
return $c->render( |
|||
status => 404, |
|||
openapi => { error => "Match not found" } |
|||
); |
|||
} |
|||
|
|||
return try { |
|||
my $matches = Koha::Import::Record::Matches->search({ |
|||
import_record_id => $import_record_id, |
|||
chosen => 1 |
|||
}); |
|||
$matches->update({ chosen => 0}) if $matches; |
|||
$match->set_from_api({ chosen => JSON::true }); |
|||
$match->store; |
|||
return $c->render( status => 200, openapi => $match ); |
|||
} |
|||
catch { |
|||
$c->unhandled_exception($_); |
|||
}; |
|||
} |
|||
|
|||
1; |
@ -0,0 +1,21 @@ |
|||
{ |
|||
"type": "object", |
|||
"properties": { |
|||
"import_record_id": { |
|||
"type": "integer", |
|||
"description": "Internal import record identifier" |
|||
}, |
|||
"candidate_match_id": { |
|||
"type": "integer", |
|||
"description": "Internal import record match candidate identifier" |
|||
}, |
|||
"chosen": { |
|||
"type": "boolean", |
|||
"description": "Whether match has been chosen for overlay" |
|||
}, |
|||
"score": { |
|||
"type": "integer", |
|||
"description": "Ranking value for this match calculated by the matching rules" |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,16 @@ |
|||
--- |
|||
type: object |
|||
properties: |
|||
import_record_id: |
|||
type: integer |
|||
description: Internal import record identifier |
|||
candidate_match_id: |
|||
type: integer |
|||
description: Internal import record match candidate identifier |
|||
chosen: |
|||
type: boolean |
|||
description: Whether match has been chosen for overlay |
|||
score: |
|||
type: integer |
|||
description: Ranking value for this match calculated by the matching rules |
|||
additionalProperties: false |
@ -0,0 +1,16 @@ |
|||
{ |
|||
"import_record_id_pp": { |
|||
"name": "import_record_id", |
|||
"in": "path", |
|||
"description": "Internal import_record identifier", |
|||
"required": true, |
|||
"type": "integer" |
|||
}, |
|||
"candidate_match_id_pp": { |
|||
"name": "candidate_match_id", |
|||
"in": "path", |
|||
"description": "Internal import_record_match_candidate identifier", |
|||
"required": true, |
|||
"type": "integer" |
|||
} |
|||
} |
@ -0,0 +1,13 @@ |
|||
--- |
|||
import_record_id_pp: |
|||
name: import_record_id |
|||
in: path |
|||
description: Internal import_record identifier |
|||
required: true |
|||
type: integer |
|||
candidate_match_id_pp: |
|||
name: candidate_match_id |
|||
in: path |
|||
description: Internal import_record_match_candidate identifier |
|||
required: true |
|||
type: integer |
@ -0,0 +1,144 @@ |
|||
{ |
|||
"/import/{import_batch_id}/records/{import_record_id}/matches/chosen": { |
|||
"put": { |
|||
"x-mojo-to": "ImportRecordMatches#set_chosen", |
|||
"operationId": "setChosen", |
|||
"tags": ["import_record_matches"], |
|||
"parameters": [{ |
|||
"name": "import_batch_id", |
|||
"in": "path", |
|||
"required": true, |
|||
"description": "An import_batch ID", |
|||
"type": "integer" |
|||
}, { |
|||
"name": "import_record_id", |
|||
"in": "path", |
|||
"required": true, |
|||
"description": "An import_record ID", |
|||
"type": "integer" |
|||
}, { |
|||
"name": "body", |
|||
"in": "body", |
|||
"description": "A JSON object containing fields to modify", |
|||
"required": true, |
|||
"schema": { |
|||
"type": "object", |
|||
"properties": { |
|||
"candidate_match_id": { |
|||
"description": "Candudate match to choose", |
|||
"type": "integer" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
], |
|||
"consumes": ["application/json"], |
|||
"produces": ["application/json"], |
|||
"responses": { |
|||
"200": { |
|||
"description": "Match updated" |
|||
}, |
|||
"400": { |
|||
"description": "Missing or wrong parameters", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"401": { |
|||
"description": "Authentication required", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"403": { |
|||
"description": "Match management not allowed", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"404": { |
|||
"description": "Import record match not found", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"500": { |
|||
"description": "Internal server error", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"503": { |
|||
"description": "Under maintenance", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
} |
|||
}, |
|||
"x-koha-authorization": { |
|||
"permissions": { |
|||
"tools": "manage_staged_marc" |
|||
} |
|||
} |
|||
}, |
|||
"delete": { |
|||
"x-mojo-to": "ImportRecordMatches#unset_chosen", |
|||
"operationId": "unsetChosen", |
|||
"tags": ["import_record_matches"], |
|||
"parameters": [{ |
|||
"name": "import_batch_id", |
|||
"in": "path", |
|||
"required": true, |
|||
"description": "An import_batch ID", |
|||
"type": "integer" |
|||
}, { |
|||
"name": "import_record_id", |
|||
"in": "path", |
|||
"required": true, |
|||
"description": "An import_record ID", |
|||
"type": "integer" |
|||
}], |
|||
"produces": ["application/json"], |
|||
"responses": { |
|||
"204": { |
|||
"description": "Matches unchosen" |
|||
}, |
|||
"401": { |
|||
"description": "Authentication required", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"403": { |
|||
"description": "Match management not allowed", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"404": { |
|||
"description": "Import record matches not found", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"500": { |
|||
"description": "Internal server error", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
}, |
|||
"503": { |
|||
"description": "Under maintenance", |
|||
"schema": { |
|||
"$ref": "../definitions.json#/error" |
|||
} |
|||
} |
|||
}, |
|||
"x-koha-authorization": { |
|||
"permissions": { |
|||
"tools": "manage_staged_marc" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,106 @@ |
|||
--- |
|||
"/import/{import_batch_id}/records/{import_record_id}/matches/chosen": |
|||
put: |
|||
x-mojo-to: ImportRecordMatches#set_chosen |
|||
operationId: setChosen |
|||
tags: |
|||
- import_record_matches |
|||
parameters: |
|||
- name: import_batch_id |
|||
in: path |
|||
required: true |
|||
description: An import_batch ID |
|||
type: integer |
|||
- name: import_record_id |
|||
in: path |
|||
required: true |
|||
description: An import_record ID |
|||
type: integer |
|||
- name: body |
|||
in: body |
|||
description: A JSON object containing fields to modify |
|||
required: true |
|||
schema: |
|||
type: object |
|||
properties: |
|||
candidate_match_id: |
|||
description: Candidate match to choose |
|||
type: integer |
|||
consumes: |
|||
- application/json |
|||
produces: |
|||
- application/json |
|||
responses: |
|||
"200": |
|||
description: Match updated |
|||
"400": |
|||
description: Missing or wrong parameters |
|||
schema: |
|||
$ref: "../swagger.yaml#/definitions/error" |
|||
"401": |
|||
description: Authentication required |
|||
schema: |
|||
$ref: "../swagger.yaml#/definitions/error" |
|||
"403": |
|||
description: Match management not allowed |
|||
schema: |
|||
$ref: "../swagger.yaml#/definitions/error" |
|||
"404": |
|||
description: Import record match not found |
|||
schema: |
|||
$ref: "../swagger.yaml#/definitions/error" |
|||
"500": |
|||
description: Internal server error |
|||
schema: |
|||
$ref: "../swagger.yaml#/definitions/error" |
|||
"503": |
|||
description: Under maintenance |
|||
schema: |
|||
$ref: "../swagger.yaml#/definitions/error" |
|||
x-koha-authorization: |
|||
permissions: |
|||
tools: manage_staged_marc |
|||
delete: |
|||
x-mojo-to: ImportRecordMatches#unset_chosen |
|||
operationId: unsetChosen |
|||
tags: |
|||
- import_record_matches |
|||
parameters: |
|||
- name: import_batch_id |
|||
in: path |
|||
required: true |
|||
description: An import_batch ID |
|||
type: integer |
|||
- name: import_record_id |
|||
in: path |
|||
required: true |
|||
description: An import_record ID |
|||
type: integer |
|||
produces: |
|||
- application/json |
|||
responses: |
|||
"204": |
|||
description: Matches unchosen |
|||
"401": |
|||
description: Authentication required |
|||
schema: |
|||
$ref: "../swagger.yaml#/definitions/error" |
|||
"403": |
|||
description: Match management not allowed |
|||
schema: |
|||
$ref: "../swagger.yaml#/definitions/error" |
|||
"404": |
|||
description: Import record matches not found |
|||
schema: |
|||
$ref: "../swagger.yaml#/definitions/error" |
|||
"500": |
|||
description: Internal server error |
|||
schema: |
|||
$ref: "../swagger.yaml#/definitions/error" |
|||
"503": |
|||
description: Under maintenance |
|||
schema: |
|||
$ref: "../swagger.yaml#/definitions/error" |
|||
x-koha-authorization: |
|||
permissions: |
|||
tools: manage_staged_marc |
@ -0,0 +1,16 @@ |
|||
use Modern::Perl; |
|||
|
|||
return { |
|||
bug_number => "22785", |
|||
description => "Add chosen column to import_record_matches", |
|||
up => sub { |
|||
my ($args) = @_; |
|||
my ($dbh, $out) = @$args{qw(dbh out)}; |
|||
unless( column_exists('import_record_matches','chosen') ){ |
|||
$dbh->do(q{ |
|||
ALTER TABLE import_record_matches ADD COLUMN chosen TINYINT null DEFAULT null AFTER score |
|||
}); |
|||
say $out "chosen column added to import_record_matches"; |
|||
} |
|||
}, |
|||
} |
@ -0,0 +1,167 @@ |
|||
#!/usr/bin/env perl |
|||
|
|||
# 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 Test::More tests => 1; |
|||
use Test::Mojo; |
|||
use Test::Warn; |
|||
|
|||
use t::lib::TestBuilder; |
|||
use t::lib::Mocks; |
|||
|
|||
use C4::Auth; |
|||
use Koha::Import::Record::Matches; |
|||
|
|||
my $schema = Koha::Database->new->schema; |
|||
my $builder = t::lib::TestBuilder->new; |
|||
|
|||
# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling |
|||
# this affects the other REST api tests |
|||
t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); |
|||
|
|||
my $remote_address = '127.0.0.1'; |
|||
my $t = Test::Mojo->new('Koha::REST::V1'); |
|||
|
|||
subtest 'import record matches tests' => sub { |
|||
|
|||
plan tests => 13; |
|||
|
|||
$schema->storage->txn_begin; |
|||
|
|||
my ( $unauthorized_borrowernumber, $unauthorized_session_id ) = |
|||
create_user_and_session( { authorized => 0 } ); |
|||
my ( $authorized_borrowernumber, $authorized_session_id ) = |
|||
create_user_and_session( { authorized => 1 } ); |
|||
|
|||
my $match_1 = $builder->build_object({ |
|||
class => 'Koha::Import::Record::Matches', |
|||
value => { |
|||
chosen => 0, |
|||
} |
|||
}); |
|||
my $match_2 = $builder->build_object({ |
|||
class => 'Koha::Import::Record::Matches', |
|||
value => { |
|||
import_record_id => $match_1->import_record_id, |
|||
chosen => 1, |
|||
} |
|||
}); |
|||
my $del_match = $builder->build_object({ class => 'Koha::Import::Record::Matches' }); |
|||
my $del_import_batch_id = $del_match->import_record->import_batch_id; |
|||
my $del_match_id = $del_match->import_record_id; |
|||
|
|||
# Unauthorized attempt to update |
|||
my $tx = $t->ua->build_tx( |
|||
PUT => "/api/v1/import/".$match_1->import_record->import_batch_id."/records/".$match_1->import_record_id."/matches/chosen"=> |
|||
json => { |
|||
candidate_match_id => $match_1->candidate_match_id |
|||
} |
|||
); |
|||
$tx->req->cookies( |
|||
{ name => 'CGISESSID', value => $unauthorized_session_id } ); |
|||
$tx->req->env( { REMOTE_ADDR => $remote_address } ); |
|||
$t->request_ok($tx)->status_is(403); |
|||
|
|||
# Invalid attempt to allow match on a non-existent record |
|||
$tx = $t->ua->build_tx( |
|||
PUT => "/api/v1/import/".$del_import_batch_id."/records/".$del_match_id."/matches/chosen" => |
|||
json => { |
|||
candidate_match_id => $match_1->candidate_match_id |
|||
} |
|||
); |
|||
|
|||
$tx->req->cookies( |
|||
{ name => 'CGISESSID', value => $authorized_session_id } ); |
|||
$tx->req->env( { REMOTE_ADDR => $remote_address } ); |
|||
$del_match->delete(); |
|||
$t->request_ok($tx)->status_is(404) |
|||
->json_is( '/error' => "Match not found" ); |
|||
|
|||
# Valid, authorised update |
|||
$tx = $t->ua->build_tx( |
|||
PUT => "/api/v1/import/".$match_1->import_record->import_batch_id."/records/".$match_1->import_record_id."/matches/chosen" => |
|||
json => { |
|||
candidate_match_id => $match_1->candidate_match_id |
|||
} |
|||
); |
|||
$tx->req->cookies( |
|||
{ name => 'CGISESSID', value => $authorized_session_id } ); |
|||
$tx->req->env( { REMOTE_ADDR => $remote_address } ); |
|||
$t->request_ok($tx)->status_is(200); |
|||
|
|||
$match_1->discard_changes; |
|||
$match_2->discard_changes; |
|||
ok( $match_1->chosen,"Match 1 is correctly set to chosen"); |
|||
ok( !$match_2->chosen,"Match 2 correctly unset when match 1 is set"); |
|||
|
|||
# Valid unsetting |
|||
$tx = $t->ua->build_tx( |
|||
DELETE => "/api/v1/import/".$match_1->import_record->import_batch_id."/records/".$match_1->import_record_id."/matches/chosen" => |
|||
json => { |
|||
} |
|||
); |
|||
$tx->req->cookies( |
|||
{ name => 'CGISESSID', value => $authorized_session_id } ); |
|||
$tx->req->env( { REMOTE_ADDR => $remote_address } ); |
|||
$t->request_ok($tx)->status_is(204); |
|||
|
|||
$match_1->discard_changes; |
|||
$match_2->discard_changes; |
|||
ok( !$match_1->chosen,"Match 1 is correctly unset to chosen"); |
|||
ok( !$match_2->chosen,"Match 2 is correctly unset to chosen"); |
|||
|
|||
$schema->storage->txn_rollback; |
|||
}; |
|||
|
|||
sub create_user_and_session { |
|||
|
|||
my $args = shift; |
|||
my $dbh = C4::Context->dbh; |
|||
|
|||
my $user = $builder->build( |
|||
{ |
|||
source => 'Borrower', |
|||
value => { |
|||
flags => 0 |
|||
} |
|||
} |
|||
); |
|||
|
|||
# Create a session for the authorized user |
|||
my $session = C4::Auth::get_session(''); |
|||
$session->param( 'number', $user->{borrowernumber} ); |
|||
$session->param( 'id', $user->{userid} ); |
|||
$session->param( 'ip', '127.0.0.1' ); |
|||
$session->param( 'lasttime', time() ); |
|||
$session->flush; |
|||
|
|||
if ( $args->{authorized} ) { |
|||
$builder->build({ |
|||
source => 'UserPermission', |
|||
value => { |
|||
borrowernumber => $user->{borrowernumber}, |
|||
module_bit => 13, |
|||
code => 'manage_staged_marc', |
|||
} |
|||
}); |
|||
} |
|||
|
|||
return ( $user->{borrowernumber}, $session->id ); |
|||
} |
|||
|
|||
1; |
Loading…
Reference in new issue