@@ -, +, @@ - Add API endpoint for ILL request creation - UI for creation of ILL batches, including: - Ability to paste in list of identifier - Auto metadata enrichment for each identifier, using installed third party service API plugins - Auto retrieval of identifier availability, using installed third party service API plugins - Auto creation of local requests within batch - UI for management of existing ILL batches, including adding requests to, and removing requests from, batches - Additional UI to allow users to navigate from requests to their enclosing batches and vice-versa --- Koha/Illrequest.pm | 3 - Koha/REST/V1/Illrequests.pm | 107 +- admin/columns_settings.yml | 2 + api/v1/swagger/definitions/illrequest.yaml | 144 +++ api/v1/swagger/definitions/illrequests.yaml | 5 + api/v1/swagger/paths/illrequests.yaml | 52 + api/v1/swagger/swagger.yaml | 4 + ill/ill-requests.pl | 103 +- .../prog/css/src/staff-global.scss | 106 ++ .../en/includes/ill-batch-modal-strings.inc | 36 + .../prog/en/includes/ill-batch-modal.inc | 92 ++ .../en/includes/ill-batch-table-strings.inc | 9 + .../prog/en/includes/ill-batch.inc | 20 + .../prog/en/includes/ill-list-table.inc | 1 + .../prog/en/includes/ill-toolbar.inc | 19 +- .../prog/en/modules/ill/ill-requests.tt | 50 +- .../intranet-tmpl/prog/js/ill-batch-modal.js | 993 ++++++++++++++++++ .../intranet-tmpl/prog/js/ill-batch-table.js | 205 ++++ koha-tmpl/intranet-tmpl/prog/js/ill-batch.js | 49 + .../intranet-tmpl/prog/js/ill-list-table.js | 17 +- t/db_dependent/api/v1/illrequests.t | 86 +- 21 files changed, 2078 insertions(+), 25 deletions(-) create mode 100644 api/v1/swagger/definitions/illrequest.yaml create mode 100644 api/v1/swagger/definitions/illrequests.yaml create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-table-strings.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/js/ill-batch-modal.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/ill-batch-table.js create mode 100644 koha-tmpl/intranet-tmpl/prog/js/ill-batch.js --- a/Koha/Illrequest.pm +++ a/Koha/Illrequest.pm @@ -156,9 +156,6 @@ sub batch { my ( $self ) = @_; return Koha::Illbatches->find($self->_result->batch_id); -# return Koha::Illbatch->_new_from_dbic( -# scalar $self->_result->batch_id -# ); } =head3 statusalias --- a/Koha/REST/V1/Illrequests.pm +++ a/Koha/REST/V1/Illrequests.pm @@ -22,6 +22,7 @@ use Mojo::Base 'Mojolicious::Controller'; use C4::Context; use Koha::Illrequests; use Koha::Illrequestattributes; +use Koha::Illbatches; use Koha::Libraries; use Koha::Patrons; use Koha::Libraries; @@ -43,8 +44,11 @@ sub list { my $c = shift->openapi->valid_input or return; my $args = $c->req->params->to_hash // {}; - my $output = []; - my @format_dates = ( 'placed', 'updated', 'completed' ); + + # Get the pipe-separated string of hidden ILL statuses + my $hidden_statuses_string = C4::Context->preference('ILLHiddenRequestStatuses') // q{}; + # Turn into arrayref + my $hidden_statuses = [ split /\|/, $hidden_statuses_string ]; # Create a hash where all keys are embedded values # Enables easy checking @@ -55,11 +59,6 @@ sub list { delete $args->{embed}; } - # Get the pipe-separated string of hidden ILL statuses - my $hidden_statuses_string = C4::Context->preference('ILLHiddenRequestStatuses') // q{}; - # Turn into arrayref - my $hidden_statuses = [ split /\|/, $hidden_statuses_string ]; - # Get all requests # If necessary, restrict the resultset my @requests = Koha::Illrequests->search({ @@ -74,6 +73,73 @@ sub list { : () })->as_list; + my $output = _form_request(\@requests, \%embed); + + return $c->render( status => 200, openapi => $output ); +} + +=head3 add + +Adds a new ILL request + +=cut + +sub add { + my $c = shift->openapi->valid_input or return; + + my $body = $c->validation->param('body'); + + return try { + my $request = Koha::Illrequest->new->load_backend( $body->{backend} ); + + my $create_api = $request->_backend->capabilities('create_api'); + + if (!$create_api) { + return $c->render( + status => 405, + openapi => { + errors => [ 'This backend does not allow request creation via API' ] + } + ); + } + + my $create_result = &{$create_api}($body, $request); + my $new_id = $create_result->illrequest_id; + + my @new_req = Koha::Illrequests->search({ + illrequest_id => $new_id + })->as_list; + + my $output = _form_request(\@new_req, { + metadata => 1, + patron => 1, + library => 1, + status_alias => 1, + comments => 1, + requested_partners => 1 + }); + + return $c->render( + status => 201, + openapi => $output->[0] + ); + } catch { + return $c->render( + status => 500, + openapi => { error => 'Unable to create request' } + ) + }; +} + +sub _form_request { + my ($requests_hash, $embed_hash) = @_; + + my @requests = @{$requests_hash}; + my %embed = %{$embed_hash}; + + my $output = []; + my @format_dates = ( 'placed', 'updated', 'completed' ); + my $fetch_backends = {}; foreach my $request (@requests) { $fetch_backends->{ $request->backend } ||= @@ -83,17 +149,19 @@ sub list { # Pre-load the backend object to avoid useless backend lookup/loads @requests = map { $_->_backend( $fetch_backends->{ $_->backend } ); $_ } @requests; - # Identify patrons & branches that + # Identify additional stuff that # we're going to need and get them my $to_fetch = { patrons => {}, branches => {}, - capabilities => {} + capabilities => {}, + batches => {} }; foreach my $req (@requests) { $to_fetch->{patrons}->{$req->borrowernumber} = 1 if $embed{patron}; $to_fetch->{branches}->{$req->branchcode} = 1 if $embed{library}; $to_fetch->{capabilities}->{$req->backend} = 1 if $embed{capabilities}; + $to_fetch->{batches}->{$req->batch_id} = 1 if $req->batch_id; } # Fetch the patrons we need @@ -130,7 +198,17 @@ sub list { } } - # Now we've got all associated users and branches, + # Fetch the batches we need + my $batch_arr = []; + my @batch_ids = keys %{$to_fetch->{batches}}; + if (scalar @batch_ids > 0) { + my $where = { + id => { -in => \@batch_ids } + }; + $batch_arr = Koha::Illbatches->search($where)->unblessed; + } + + # Now we've got all associated stuff # we can augment the request objects my @output = (); foreach my $req(@requests) { @@ -166,6 +244,12 @@ sub list { last; } } + foreach my $b(@{$batch_arr}) { + if ($b->{id} eq $req->batch_id) { + $to_push->{batch} = $b; + last; + } + } if ($embed{metadata}) { my $metadata = Koha::Illrequestattributes->search( { illrequest_id => $req->illrequest_id }, @@ -191,8 +275,7 @@ sub list { } push @output, $to_push; } - - return $c->render( status => 200, openapi => \@output ); + return \@output; } 1; --- a/admin/columns_settings.yml +++ a/admin/columns_settings.yml @@ -802,6 +802,8 @@ modules: columns: - columnname: illrequest_id + - + columnname: batch - columnname: metadata_author - --- a/api/v1/swagger/definitions/illrequest.yaml +++ a/api/v1/swagger/definitions/illrequest.yaml @@ -0,0 +1,144 @@ +--- +type: object +properties: + accessurl: + type: + - string + - "null" + description: A URL for accessing the resource + backend: + type: string + description: Name of the request's ILL supplier + batch_id: + type: + - string + - "null" + description: The ID of the batch the request belongs to + batch: + type: + - object + - "null" + description: The batch the request belongs to + biblio_id: + type: + - string + - "null" + description: The ID of the biblio created from the requested item + borrowernumber: + type: string + description: Borrower number of the patron of the ILL request + branchcode: + type: string + description: The branch code associated with the request + capabilities: + type: + - object + - "null" + description: A list of the valid actions that can be carried out on this request + comments: + type: + - string + - "null" + description: The number of comments this request has associated with it + completed: + type: + - string + - "null" + description: Is this request completed + cost: + type: + - string + - "null" + description: The recorded cost for this request + id_prefix: + type: + - string + - "null" + description: A prefix that should be prepended to the ID of this request during display + illrequest_id: + type: + - string + - "null" + description: The ID of this request + library: + type: + - object + - "null" + description: The library object associated with this request + medium: + type: + - string + - "null" + description: The material type associated with this request + metadata: + type: object + description: Metadata that formed this request + notesopac: + type: + - string + - "null" + description: Notes that have been entered for display in the OPAC + notesstaff: + type: + - string + - "null" + description: Notes that have been entered for display in the staff interface + orderid: + type: + - string + - "null" + description: The supplier's ID for the request + patron: + type: + - object + - "null" + description: The patron associated with this request + placed: + type: + - string + - "null" + description: The timestamp the request was placed + placed_formatted: + type: + - string + - "null" + description: The timestamp the request was placed (formatted for display) + completed_formatted: + type: + - string + - "null" + description: The timestamp the request was completed (formatted for display) + price_paid: + type: + - string + - "null" + description: The amount ultimately paid for the request + replied: + type: + - string + - "null" + description: N/A (Not in use) + requested_partners: + type: + - string + - "null" + description: The email addresses of partners this request been requested from + status: + type: string + description: The request's current status + status_alias: + type: + - string + - "null" + description: The ID of a user defined status for this request + updated: + type: + - string + - "null" + description: The timestamp the request was last updated + updated_formatted: + type: + - string + - "null" + description: The timestamp the request was last updated (formatted for display) +additionalProperties: false --- a/api/v1/swagger/definitions/illrequests.yaml +++ a/api/v1/swagger/definitions/illrequests.yaml @@ -0,0 +1,5 @@ +--- +type: array +items: + $ref: "illrequest.yaml" +additionalProperties: false --- a/api/v1/swagger/paths/illrequests.yaml +++ a/api/v1/swagger/paths/illrequests.yaml @@ -108,6 +108,8 @@ responses: "200": description: A list of ILL requests + schema: + $ref: "../swagger.yaml#/definitions/illrequests" "401": description: Authentication required schema: @@ -134,3 +136,53 @@ x-koha-authorization: permissions: ill: "1" + post: + x-mojo-to: Illrequests#add + operationId: addIllrequest + tags: + - illrequests + summary: Add ILL request + parameters: + - name: body + in: body + description: A JSON object containing informations about the new request + required: true + schema: + $ref: "../swagger.yaml#/definitions/illrequest" + produces: + - application/json + responses: + "201": + description: Request added + schema: + $ref: "../swagger.yaml#/definitions/illrequest" + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "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 resource + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + ill: "1" --- a/api/v1/swagger/swagger.yaml +++ a/api/v1/swagger/swagger.yaml @@ -52,6 +52,10 @@ definitions: $ref: ./definitions/illbatch.yaml illbatches: $ref: ./definitions/illbatches.yaml + illrequest: + $ref: ./definitions/illrequest.yaml + illrequests: + $ref: ./definitions/illrequests.yaml import_batch_profile: $ref: ./definitions/import_batch_profile.yaml import_batch_profiles: --- a/ill/ill-requests.pl +++ a/ill/ill-requests.pl @@ -27,9 +27,11 @@ use Koha::Notice::Templates; use Koha::AuthorisedValues; use Koha::Illcomment; use Koha::Illrequests; +use Koha::Illbatches; use Koha::Illrequest::Availability; use Koha::Libraries; use Koha::Token; +use Koha::Plugins; use Try::Tiny qw( catch try ); use URI::Escape qw( uri_escape_utf8 ); @@ -65,10 +67,27 @@ my $has_branch = $cfg->has_branch; my $backends_available = ( scalar @{$backends} > 0 ); $template->param( backends_available => $backends_available, - has_branch => $has_branch + has_branch => $has_branch, + have_batch => have_batch_backends($backends) ); if ( $backends_available ) { + # Establish what metadata enrichment plugins we have available + my $enrichment_services = get_metadata_enrichment(); + if (scalar @{$enrichment_services} > 0) { + $template->param( + metadata_enrichment_services => encode_json($enrichment_services) + ); + } + # Establish whether we have any availability services that can provide availability + # for the batch identifier types we support + my $batch_availability_services = get_ill_availability($enrichment_services); + if (scalar @{$batch_availability_services} > 0) { + $template->param( + batch_availability_services => encode_json($batch_availability_services) + ); + } + if ( $op eq 'illview' ) { # View the details of an ILL my $request = Koha::Illrequests->find($params->{illrequest_id}); @@ -147,8 +166,8 @@ if ( $backends_available ) { } else { my $backend_result = $request->backend_create($params); $template->param( - whole => $backend_result, - request => $request + whole => $backend_result, + request => $request ); handle_commit_maybe($backend_result, $request); } @@ -215,6 +234,9 @@ if ( $backends_available ) { # We simulate the API for backend requests for uniformity. # So, init: my $request = Koha::Illrequests->find($params->{illrequest_id}); + my $batches = Koha::Illbatches->search(undef, { + order_by => { -asc => 'name' } + }); if ( !$params->{stage} ) { my $backend_result = { error => 0, @@ -227,13 +249,15 @@ if ( $backends_available ) { }; $template->param( whole => $backend_result, - request => $request + request => $request, + batches => $batches ); } else { # Commit: # Save the changes $request->borrowernumber($params->{borrowernumber}); $request->biblio_id($params->{biblio_id}); + $request->batch_id($params->{batch_id}); $request->branchcode($params->{branchcode}); $request->price_paid($params->{price_paid}); $request->notesopac($params->{notesopac}); @@ -365,7 +389,7 @@ if ( $backends_available ) { } elsif ( $op eq 'illlist') { # If we receive a pre-filter, make it available to the template - my $possible_filters = ['borrowernumber']; + my $possible_filters = ['borrowernumber', 'batch_id']; my $active_filters = {}; foreach my $filter(@{$possible_filters}) { if ($params->{$filter}) { @@ -384,6 +408,17 @@ if ( $backends_available ) { $template->param( prefilters => join("&", @tpl_arr) ); + + if ($active_filters->{batch_id}) { + my $batch_id = $active_filters->{batch_id}; + if ($batch_id) { + my $batch = Koha::Illbatches->find($batch_id); + $template->param( + batch => $batch + ); + } + } + } elsif ( $op eq "save_comment" ) { die "Wrong CSRF token" unless Koha::Token->new->check_csrf({ session_id => scalar $cgi->cookie('CGISESSID'), @@ -418,6 +453,9 @@ if ( $backends_available ) { scalar $params->{illrequest_id} . $append ); exit; + } elsif ( $op eq "batch_list" ) { + } elsif ( $op eq "batch_create" ) { + # Batch create } else { my $request = Koha::Illrequests->find($params->{illrequest_id}); my $backend_result = $request->custom_capability($op, $params); @@ -475,3 +513,58 @@ sub redirect_to_list { print $cgi->redirect('/cgi-bin/koha/ill/ill-requests.pl'); exit; } + +# Do any of the available backends provide batch requesting +sub have_batch_backends { + my ( $backends ) = @_; + + my @have_batch = (); + + foreach my $backend(@{$backends}) { + my $can_batch = can_batch($backend); + if ($can_batch) { + push @have_batch, $backend; + } + } + return \@have_batch; +} + +# Does a given backend provide batch requests +sub can_batch { + my ( $backend ) = @_; + my $request = Koha::Illrequest->new->load_backend( $backend ); + return $request->_backend_capability( 'provides_batch_requests' ); +} + +# Get available metadata enrichment plugins +sub get_metadata_enrichment { + my @candidates = Koha::Plugins->new()->GetPlugins({ + method => 'provides_api' + }); + my @services = (); + foreach my $plugin(@candidates) { + my $supported = $plugin->provides_api(); + if ($supported->{type} eq 'search') { + push @services, $supported; + } + } + return \@services; +} + +# Get ILL availability plugins that can help us with the batch identifier types +# we support +sub get_ill_availability { + my ( $services ) = @_; + + my $id_types = {}; + foreach my $service(@{$services}) { + foreach my $id_supported(keys %{$service->{identifiers_supported}}) { + $id_types->{$id_supported} = 1; + } + } + + my $availability = Koha::Illrequest::Availability->new($id_types); + return $availability->get_services({ + ui_context => 'staff' + }); +} --- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss +++ a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss @@ -3769,6 +3769,112 @@ input.renew { } #interlibraryloans { + + .ill-toolbar { + display: flex; + } + + #ill-batch { + flex-grow: 1; + display: flex; + justify-content: flex-end; + gap: 5px; + } + + #ill-batch-requests { + .action-buttons { + display: flex; + gap: 5px; + justify-content: center; + } + } + + #ill-batch-modal { + .modal-footer { + display: flex; + & > * { + flex: 1; + } + #lhs { + text-align: left; + } + } + #create-progress { + margin-top: 17px; + } + .fetch-failed { + background-color: rgba(255,0,0,0.1); + & > * { + background-color: inherit; + } + } + .progress { + margin-bottom: 0; + margin-top: 17px; + } + #create-requests { + display: flex; + justify-content: flex-end; + } + .action-column { + text-align: center; + & > * { + margin-left: 5px; + } + & > *:first-child { + margin-left: 0; + } + } + .metadata-row:not(:first-child) { + margin-top: 0.5em; + } + .metadata-label { + font-weight: 600; + } + .more-less { + text-align: right; + margin: 2px 0; + } + + } + + #batch-form { + legend { + margin-bottom: 2em; + } + textarea { + width: 100%; + min-height: 100px; + padding: 5px; + resize: vertical; + } + #new-batch-form { + display: flex; + gap: 20px; + } + li#process-button { + display: flex; + justify-content: flex-end; + } + #textarea-metadata { + padding: 0 15px; + display: flex; + justify-content: space-between; + } + #textarea-errors { + display: flex; + flex-direction: column; + gap: 10px; + padding: 20px 15px 10px + } + .batch-modal-actions { + text-align: center; + } + fieldset { + border: 2px solid #b9d8d9; + } + } + #dataPreviewLabel { margin: .3em 0; } --- a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc @@ -0,0 +1,36 @@ + + + --- a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc @@ -0,0 +1,92 @@ +
+ --- a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-table-strings.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-table-strings.inc @@ -0,0 +1,9 @@ + + + --- a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch.inc @@ -0,0 +1,20 @@ +[% IF query_type == "batch_list" %] +Batch ID | +Name | +Number of requests | +Patron | +Branch | ++ |
---|