Bug 17170: Add API route for SearchFilters

This adds the API routes and tests

Sponsored-by: Sponsored by: Round Rock Public Library [https://www.roundrocktexas.gov/departments/library/]

Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
This commit is contained in:
Nick Clemens 2021-09-16 15:54:19 +00:00 committed by Tomas Cohen Arazi
parent b306b1d4ca
commit adf252d96c
Signed by: tomascohen
GPG key ID: 0A272EA1B2F3C15F
7 changed files with 846 additions and 0 deletions

View file

@ -0,0 +1,153 @@
package Koha::REST::V1::SearchFilter;
# 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::SearchFilters;
use Try::Tiny qw( catch try );
=head1 Name
Koha::REST::V1::SearchFilters
=head1 API
=head2 Methods
=head3 list
Controller function that handles listing Koha::SearchFilter objects
=cut
sub list {
my $c = shift->openapi->valid_input or return;
return try {
my $filters_set = Koha::SearchFilters->search({});
my $filters = $c->objects->search( $filters_set );
return $c->render(
status => 200,
openapi => $filters
);
}
catch {
$c->unhandled_exception($_);
};
}
=head3 get
Controller function that handles retrieving a single Koha::AdvancedEditorMacro
=cut
sub get {
my $c = shift->openapi->valid_input or return;
my $filter = Koha::SearchFilters->find({
id => $c->validation->param('search_filter_id'),
});
unless ($filter) {
return $c->render( status => 404,
openapi => { error => "Search filter not found" } );
}
return $c->render( status => 200, openapi => $filter->to_api );
}
=head3 add
Controller function that handles adding a new Koha::SearchFilter object
=cut
sub add {
my $c = shift->openapi->valid_input or return;
return try {
my $filter = Koha::SearchFilter->new_from_api( $c->validation->param('body') );
$filter->store->discard_changes;
$c->res->headers->location( $c->req->url->to_string . '/' . $filter->id );
return $c->render(
status => 201,
openapi => $filter->to_api
);
}
catch {
if ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
return $c->render(
status => 409,
openapi => { error => $_->error, conflict => $_->duplicate_id }
);
}
$c->unhandled_exception($_);
};
}
=head3 update
Controller function that handles updating a Koha::SearchFilter object
=cut
sub update {
my $c = shift->openapi->valid_input or return;
my $filter = Koha::SearchFilters->find( $c->validation->param('search_filter_id') );
if ( not defined $filter ) {
return $c->render( status => 404,
openapi => { error => "Object not found" } );
}
return try {
my $params = $c->req->json;
$filter->set_from_api( $params );
$filter->store->discard_changes;
return $c->render( status => 200, openapi => $filter->to_api );
}
catch {
$c->unhandled_exception($_);
};
}
=head3 delete
Controller function that handles deleting a Koha::SearchFilter object
=cut
sub delete {
my $c = shift->openapi->valid_input or return;
my $filter = Koha::SearchFilters->find( $c->validation->param('search_filter_id') );
if ( not defined $filter ) {
return $c->render( status => 404,
openapi => { error => "Object not found" } );
}
return try {
$filter->delete;
return $c->render( status => 204, openapi => q{} );
}
catch {
$c->unhandled_exception($_);
};
}
1;

View file

@ -0,0 +1,65 @@
---
account_line:
$ref: definitions/account_line.yaml
advancededitormacro:
$ref: definitions/advancededitormacro.yaml
allows_renewal:
$ref: definitions/allows_renewal.yaml
basket:
$ref: definitions/basket.yaml
cashup:
$ref: definitions/cashup.yaml
checkout:
$ref: definitions/checkout.yaml
checkouts:
$ref: definitions/checkouts.yaml
circ-rule-kind:
$ref: definitions/circ-rule-kind.yaml
city:
$ref: definitions/city.yaml
error:
$ref: definitions/error.yaml
fund:
$ref: definitions/fund.yaml
hold:
$ref: definitions/hold.yaml
holds:
$ref: definitions/holds.yaml
ill_backend:
$ref: definitions/ill_backend.yaml
ill_backends:
$ref: definitions/ill_backends.yaml
import_batch_profile:
$ref: definitions/import_batch_profile.yaml
import_batch_profiles:
$ref: definitions/import_batch_profiles.yaml
invoice:
$ref: definitions/invoice.yaml
item:
$ref: definitions/item.yaml
library:
$ref: definitions/library.yaml
order:
$ref: definitions/order.yaml
patron:
$ref: definitions/patron.yaml
patron_account_credit:
$ref: definitions/patron_account_credit.yaml
patron_balance:
$ref: definitions/patron_balance.yaml
patron_extended_attribute:
$ref: definitions/patron_extended_attribute.yaml
quote:
$ref: definitions/quote.yaml
return_claim:
$ref: definitions/return_claim.yaml
smtp_server:
$ref: definitions/smtp_server.yaml
suggestion:
$ref: definitions/suggestion.yaml
search_filter:
$ref: definitions/search_filter.yaml
transfer_limit:
$ref: definitions/transfer_limit.yaml
vendor:
$ref: definitions/vendor.yaml

View file

@ -0,0 +1,33 @@
---
type: object
properties:
search_filter_id:
type: integer
description: internally assigned search filter identifier
readOnly: true
name:
description: filter name
type: string
filter_query:
description: filter query part
type:
- string
- 'null'
filter_limits:
description: filter limits part
type:
- string
- 'null'
opac:
description: visible on opac
type:
- boolean
- 'null'
staff_client:
description: visible in staff client
type:
- boolean
- 'null'
additionalProperties: false
required:
- name

View file

@ -0,0 +1,226 @@
---
"/search_filters":
get:
x-mojo-to: SearchFilter#list
operationId: listFilters
tags:
- search_filters
summary: List search filters
produces:
- application/json
parameters:
- name: name
in: query
description: Case insensitive search on filter name
required: false
type: string
- name: filter_query
in: query
description: Search on filter query part
required: false
type: string
- name: filter_limits
in: query
description: Search on filter limits
required: false
type: string
- name: opac
in: query
description: Display in OPAC
required: false
type: boolean
- name: staff_client
in: query
description: Display on staff client
required: false
type: boolean
- $ref: "../swagger.yaml#/parameters/match"
- $ref: "../swagger.yaml#/parameters/order_by"
- $ref: "../swagger.yaml#/parameters/page"
- $ref: "../swagger.yaml#/parameters/per_page"
- $ref: "../swagger.yaml#/parameters/q_param"
- $ref: "../swagger.yaml#/parameters/q_body"
- $ref: "../swagger.yaml#/parameters/q_header"
- $ref: "../swagger.yaml#/parameters/request_id_header"
responses:
'200':
description: A list of search filters
schema:
type: array
items:
$ref: "../swagger.yaml#/definitions/search_filter"
'403':
description: Access forbidden
schema:
$ref: "../swagger.yaml#/definitions/error"
'500':
description: Internal error
schema:
$ref: "../swagger.yaml#/definitions/error"
'503':
description: Under maintenance
schema:
$ref: "../swagger.yaml#/definitions/error"
x-koha-authorization:
permissions:
parameters: manage_search_filters
post:
x-mojo-to: SearchFilter#add
operationId: addSearchFilter
tags:
- search_filters
summary: Add search filter
parameters:
- name: body
in: body
description: A JSON object containing informations about the new search filter
required: true
schema:
$ref: "../swagger.yaml#/definitions/search_filter"
produces:
- application/json
responses:
'201':
description: Search filter added
schema:
$ref: "../swagger.yaml#/definitions/search_filter"
'401':
description: Authentication required
schema:
$ref: "../swagger.yaml#/definitions/error"
'403':
description: Access forbidden
schema:
$ref: "../swagger.yaml#/definitions/error"
"409":
description: Conflict in creating the resource
schema:
$ref: ../swagger.yaml#/definitions/error
'500':
description: Internal error
schema:
$ref: "../swagger.yaml#/definitions/error"
'503':
description: Under maintenance
schema:
$ref: "../swagger.yaml#/definitions/error"
x-koha-authorization:
permissions:
parameters: manage_search_filters
"/search_filters/{search_filter_id}":
get:
x-mojo-to: SearchFilter#get
operationId: getSearchFilter
tags:
- search_filters
summary: Get search filter
parameters:
- $ref: "../swagger.yaml#/parameters/search_filter_id_pp"
produces:
- application/json
responses:
'200':
description: A search filter
schema:
$ref: "../swagger.yaml#/definitions/search_filter"
'403':
description: Access forbidden
schema:
$ref: "../swagger.yaml#/definitions/error"
'404':
description: SearchFilter not found
schema:
$ref: "../swagger.yaml#/definitions/error"
'500':
description: Internal error
schema:
$ref: "../swagger.yaml#/definitions/error"
'503':
description: Under maintenance
schema:
$ref: "../swagger.yaml#/definitions/error"
x-koha-authorization:
permissions:
parameters: manage_search_filters
put:
x-mojo-to: SearchFilter#update
operationId: updateSearchFilter
tags:
- search_filters
summary: Update search filter
parameters:
- $ref: "../swagger.yaml#/parameters/search_filter_id_pp"
- name: body
in: body
description: A search filter object
required: true
schema:
$ref: "../swagger.yaml#/definitions/search_filter"
produces:
- application/json
responses:
'200':
description: An search_filter
schema:
$ref: "../swagger.yaml#/definitions/search_filter"
'401':
description: Authentication required
schema:
$ref: "../swagger.yaml#/definitions/error"
'403':
description: Access forbidden
schema:
$ref: "../swagger.yaml#/definitions/error"
'404':
description: Search filter not found
schema:
$ref: "../swagger.yaml#/definitions/error"
'500':
description: Internal error
schema:
$ref: "../swagger.yaml#/definitions/error"
'503':
description: Under maintenance
schema:
$ref: "../swagger.yaml#/definitions/error"
x-koha-authorization:
permissions:
parameters: manage_search_filters
delete:
x-mojo-to: SearchFilter#delete
operationId: deleteSearchFilter
tags:
- macros
summary: Delete search filter
parameters:
- $ref: "../swagger.yaml#/parameters/search_filter_id_pp"
produces:
- application/json
responses:
'204':
description: Searc filter deleted
schema:
type: string
'401':
description: Authentication required
schema:
$ref: "../swagger.yaml#/definitions/error"
'403':
description: Access forbidden
schema:
$ref: "../swagger.yaml#/definitions/error"
'404':
description: Search filter not found
schema:
$ref: "../swagger.yaml#/definitions/error"
'500':
description: Internal error
schema:
$ref: "../swagger.yaml#/definitions/error"
'503':
description: Under maintenance
schema:
$ref: "../swagger.yaml#/definitions/error"
x-koha-authorization:
permissions:
parameters: manage_search_filters

View file

@ -68,6 +68,8 @@ definitions:
$ref: ./definitions/renewals.yaml
return_claim:
$ref: ./definitions/return_claim.yaml
search_filter:
$ref: ./definitions/search_filter.yaml
smtp_server:
$ref: ./definitions/smtp_server.yaml
suggestion:
@ -97,6 +99,10 @@ paths:
$ref: ./paths/advancededitormacros.yaml#/~1advanced_editor~1macros
/advanced_editor/macros/shared:
$ref: ./paths/advancededitormacros.yaml#/~1advanced_editor~1macros~1shared
/search_filters:
$ref: ./paths/search_filters.yaml#/~1search_filters
"/search_filters/{search_filter_id}":
$ref: "./paths/search_filters.yaml#/~1search_filters~1{search_filter_id}"
"/advanced_editor/macros/shared/{advancededitormacro_id}":
$ref: "./paths/advancededitormacros.yaml#/~1advanced_editor~1macros~1shared~1{advancededitormacro_id}"
"/advanced_editor/macros/{advancededitormacro_id}":
@ -427,6 +433,12 @@ parameters:
name: x-koha-request-id
required: false
type: integer
search_filter_id_pp:
name: search_filter_id
in: path
description: Search filter internal identifier
required: true
type: integer
seen_pp:
description: Item was seen flag
in: query

View file

@ -0,0 +1,353 @@
#!/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 => 5;
use Test::Mojo;
use Test::Warn;
use t::lib::TestBuilder;
use t::lib::Mocks;
use Koha::SearchFilters;
use Koha::Database;
my $schema = Koha::Database->new->schema;
my $builder = t::lib::TestBuilder->new;
t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
my $t = Test::Mojo->new('Koha::REST::V1');
$schema->storage->txn_begin;
subtest 'list() tests' => sub {
plan tests => 10;
Koha::SearchFilters->search()->delete();
my $patron_1 = $builder->build_object({
class => 'Koha::Patrons',
value => { flags => 3 }
});
my $password = 'thePassword123';
$patron_1->set_password({ password => $password, skip_validation => 1 });
my $userid = $patron_1->userid;
# Create test context
my $search_filter_1 = $builder->build_object({ class => 'Koha::SearchFilters', value =>
{
name => 'Test1',
query => 'kw:this',
limits => 'mc-itype,phr:BK',
opac => 1,
staff_client => 1
}
});
my $search_filter_2 = $builder->build_object({ class => 'Koha::SearchFilters', value =>
{
name => 'Test2',
query => 'kw:that',
limits => 'mc-itype,phr:BK',
opac => 0,
staff_client => 1
}
});
my $search_filter_3 = $builder->build_object({ class => 'Koha::SearchFilters', value =>
{
name => 'Test3',
query => 'kw:any',
limits => 'mc-itype,phr:CD',
opac => 0,
staff_client => 0
}
});
my $search_filter_4 = $builder->build_object({ class => 'Koha::SearchFilters', value =>
{
name => 'Test4',
query => 'kw:some',
limits => 'mc-itype,phr:CD',
opac => 1,
staff_client => 0
}
});
# Make sure we are returned with the correct amount of macros
$t->get_ok( "//$userid:$password@/api/v1/search_filters" )
->status_is( 200, 'SWAGGER3.2.2' )
->json_has('/0/search_filter_id')
->json_has('/1/search_filter_id')
->json_has('/2/search_filter_id')
->json_has('/3/search_filter_id');
subtest 'query parameters' => sub {
plan tests => 12;
$t->get_ok("//$userid:$password@/api/v1/search_filters?name=" . $search_filter_2->name)
->status_is(200)
->json_is( [ $search_filter_2->to_api ] );
$t->get_ok("//$userid:$password@/api/v1/search_filters?name=NotAName")
->status_is(200)
->json_is( [ ] );
$t->get_ok("//$userid:$password@/api/v1/search_filters?filter_query=kw:any")
->status_is(200)
->json_is( [ $search_filter_3->to_api ] );
$t->get_ok("//$userid:$password@/api/v1/search_filters?filter_limits=mc-itype,phr:BK")
->status_is(200)
->json_is( [ $search_filter_1->to_api, $search_filter_2->to_api ] );
};
# Warn on unsupported query parameter
$t->get_ok( "//$userid:$password@/api/v1/search_filters?filter_blah=blah" )
->status_is(400)
->json_is( [{ path => '/query/filter_blah', message => 'Malformed query string'}] );
};
subtest 'get() tests' => sub {
plan tests => 9;
my $patron = $builder->build_object({
class => 'Koha::Patrons',
value => { flags => 3 }
});
my $password = 'thePassword123';
$patron->set_password({ password => $password, skip_validation => 1 });
my $userid = $patron->userid;
my $search_filter_1 = $builder->build_object( { class => 'Koha::SearchFilters' } );
my $search_filter_2 = $builder->build_object( { class => 'Koha::SearchFilters' } );
my $search_filter_3 = $builder->build_object( { class => 'Koha::SearchFilters' } );
$t->get_ok( "//$userid:$password@/api/v1/search_filters/" . $search_filter_1->id )
->status_is( 200, 'Filter retrieved correctly' )
->json_is( $search_filter_1->to_api );
my $non_existent_code = $search_filter_1->id;
$search_filter_1->delete;
$t->get_ok( "//$userid:$password@/api/v1/search_filters/" . $non_existent_code )
->status_is(404)
->json_is( '/error' => 'Search filter not found' );
$patron->flags(4)->store;
$t->get_ok( "//$userid:$password/api/v1/search_filters/" . $search_filter_2->id )
->status_is( 401, 'Cannot search filters without permission' )
->json_is( '/error' => 'Authentication failure.' );
};
subtest 'add() tests' => sub {
plan tests => 17;
my $authorized_patron = $builder->build_object({
class => 'Koha::Patrons',
value => { flags => 0 }
});
$builder->build({
source => 'UserPermission',
value => {
borrowernumber => $authorized_patron->borrowernumber,
module_bit => 3,
code => 'manage_search_filters',
},
});
my $password = 'thePassword123';
$authorized_patron->set_password({ password => $password, skip_validation => 1 });
my $auth_userid = $authorized_patron->userid;
my $unauthorized_patron = $builder->build_object({
class => 'Koha::Patrons',
value => { flags => 0 }
});
$unauthorized_patron->set_password({ password => $password, skip_validation => 1 });
my $unauth_userid = $unauthorized_patron->userid;
my $search_filter = $builder->build_object({ class => 'Koha::SearchFilters' });
my $search_filter_values = $search_filter->to_api;
delete $search_filter_values->{search_filter_id};
$search_filter->delete;
# Unauthorized attempt to write
$t->post_ok( "//$unauth_userid:$password@/api/v1/search_filters" => json => $search_filter_values )
->status_is(403);
# Authorized attempt to write invalid data
my $search_filter_with_invalid_field = { %$search_filter_values };
$search_filter_with_invalid_field->{'coffee_filter'} = 'Chemex';
$t->post_ok( "//$auth_userid:$password@/api/v1/search_filters" => json => $search_filter_with_invalid_field )
->status_is(400)
->json_is(
"/errors" => [
{
message => "Properties not allowed: coffee_filter.",
path => "/body"
}
]
);
# Authorized attempt to write
$t->post_ok( "//$auth_userid:$password@/api/v1/search_filters" => json => $search_filter_values )
->status_is( 201, 'SWAGGER3.2.1' )
->json_has( '/search_filter_id', 'We generated a new id' )
->json_is( '/name' => $search_filter_values->{name}, 'The name matches what we supplied' )
->json_is( '/query' => $search_filter_values->{query}, 'The query matches what we supplied' )
->json_is( '/limits' => $search_filter_values->{limits}, 'The limits match what we supplied' )
->json_is( '/opac' => $search_filter_values->{opac}, 'The limits match what we supplied' )
->json_is( '/staff_client' => $search_filter_values->{staff_client}, 'The limits match what we supplied' )
->header_like( Location => qr|^\/api\/v1\/search_filters\/d*|, 'Correct location' );
# save the library_id
my $search_filter_id = 999;
# Authorized attempt to create with existing id
$search_filter_values->{search_filter_id} = $search_filter_id;
$t->post_ok( "//$auth_userid:$password@/api/v1/search_filters" => json => $search_filter_values )
->status_is(400)
->json_is( '/errors' => [
{
message => "Read-only.",
path => "/body/search_filter_id"
}
]
);
};
subtest 'update() tests' => sub {
plan tests => 15;
my $authorized_patron = $builder->build_object({
class => 'Koha::Patrons',
value => { flags => 0 }
});
$builder->build({
source => 'UserPermission',
value => {
borrowernumber => $authorized_patron->borrowernumber,
module_bit => 3,
code => 'manage_search_filters',
},
});
my $password = 'thePassword123';
$authorized_patron->set_password({ password => $password, skip_validation => 1 });
my $auth_userid = $authorized_patron->userid;
my $unauthorized_patron = $builder->build_object({
class => 'Koha::Patrons',
value => { flags => 0 }
});
$unauthorized_patron->set_password({ password => $password, skip_validation => 1 });
my $unauth_userid = $unauthorized_patron->userid;
my $search_filter = $builder->build_object({ class => 'Koha::SearchFilters' });
my $search_filter_id = $search_filter->id;
my $search_filter_values = $search_filter->to_api;
delete $search_filter_values->{search_filter_id};
# Unauthorized attempt to update
$t->put_ok( "//$unauth_userid:$password@/api/v1/search_filters/$search_filter_id"
=> json => { name => 'New unauthorized name change' } )
->status_is(403);
my $search_filter_update = {
name => "Filter update",
filter_query => "ti:The hobbit",
filter_limits => "mc-ccode:fantasy",
};
my $test = $t->put_ok( "//$auth_userid:$password@/api/v1/search_filters/$search_filter_id" => json => $search_filter_update )
->status_is(200, 'Authorized user can update a macro')
->json_is( '/search_filter_id' => $search_filter_id, 'We get back the id' )
->json_is( '/name' => $search_filter_update->{name}, 'We get back the name' )
->json_is( '/filter_query' => $search_filter_update->{filter_query}, 'We get back our query' )
->json_is( '/filter_limits' => $search_filter_update->{filter_limits}, 'We get back our limits' )
->json_is( '/opac' => 1, 'We get back our opac visibility unchanged' )
->json_is( '/staff_client' => 1, 'We get back our staff client visibility unchanged' );
# Authorized attempt to write invalid data
my $search_filter_with_invalid_field = { %$search_filter_update };
$search_filter_with_invalid_field->{'coffee_filter'} = 'Chemex';
$t->put_ok( "//$auth_userid:$password@/api/v1/search_filters/$search_filter_id" => json => $search_filter_with_invalid_field )
->status_is(400)
->json_is(
"/errors" => [
{
message => "Properties not allowed: coffee_filter.",
path => "/body"
}
]
);
my $non_existent_macro = $builder->build_object({class => 'Koha::SearchFilters'});
my $non_existent_code = $non_existent_macro->id;
$non_existent_macro->delete;
$t->put_ok("//$auth_userid:$password@/api/v1/search_filters/$non_existent_code" => json => $search_filter_update)
->status_is(404);
};
subtest 'delete() tests' => sub {
plan tests => 4;
my $authorized_patron = $builder->build_object({
class => 'Koha::Patrons',
value => { flags => 0 }
});
$builder->build({
source => 'UserPermission',
value => {
borrowernumber => $authorized_patron->borrowernumber,
module_bit => 3,
code => 'manage_search_filters',
},
});
my $password = 'thePassword123';
$authorized_patron->set_password({ password => $password, skip_validation => 1 });
my $auth_userid = $authorized_patron->userid;
my $unauthorized_patron = $builder->build_object({
class => 'Koha::Patrons',
value => { flags => 0 }
});
$unauthorized_patron->set_password({ password => $password, skip_validation => 1 });
my $unauth_userid = $unauthorized_patron->userid;
my $search_filter = $builder->build_object({ class => 'Koha::SearchFilters' });
my $search_filter_2 = $builder->build_object({ class => 'Koha::SearchFilters' });
my $search_filter_id = $search_filter->id;
my $search_filter_2_id = $search_filter_2->id;
# Unauthorized attempt to delete
$t->delete_ok( "//$unauth_userid:$password@/api/v1/search_filters/$search_filter_2_id")
->status_is(403, "Cannot delete search filter without permission");
$t->delete_ok( "//$auth_userid:$password@/api/v1/search_filters/$search_filter_id")
->status_is( 204, 'Can delete search filter with permission');
};
$schema->storage->txn_rollback;

View file

@ -628,6 +628,10 @@ sub _gen_default_values {
status => 'staged',
import_error => undef
},
SearchFilter => {
opac => 1,
staff_client => 1
},
};
}