Bugzilla – Attachment 147931 Details for
Bug 30719
ILL should provide the ability to create batch requests
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 30719: Batch ILL requests
Bug-30719-Batch-ILL-requests.patch (text/plain), 89.83 KB, created by
Pedro Amorim
on 2023-03-08 14:20:13 UTC
(
hide
)
Description:
Bug 30719: Batch ILL requests
Filename:
MIME Type:
Creator:
Pedro Amorim
Created:
2023-03-08 14:20:13 UTC
Size:
89.83 KB
patch
obsolete
>From 87d56f02a480580ad0b6f892106c4ac1bc6599e0 Mon Sep 17 00:00:00 2001 >From: Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >Date: Fri, 10 Jun 2022 14:32:15 +0100 >Subject: [PATCH] Bug 30719: Batch ILL requests > >- 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 > >Signed-off-by: Pedro Amorim <pedro.amorim@ptfs-europe.com> >--- > 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 | 3 +- > .../prog/en/includes/ill-toolbar.inc | 19 +- > .../prog/en/modules/ill/ill-requests.tt | 54 + > .../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, 2084 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 > >diff --git a/Koha/Illrequest.pm b/Koha/Illrequest.pm >index a62f3946ae..f29b4ae32d 100644 >--- a/Koha/Illrequest.pm >+++ b/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 >diff --git a/Koha/REST/V1/Illrequests.pm b/Koha/REST/V1/Illrequests.pm >index e3fa0a2363..05486a1e4e 100644 >--- a/Koha/REST/V1/Illrequests.pm >+++ b/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; >diff --git a/admin/columns_settings.yml b/admin/columns_settings.yml >index 8967079401..f3060b26ac 100644 >--- a/admin/columns_settings.yml >+++ b/admin/columns_settings.yml >@@ -829,6 +829,8 @@ modules: > columns: > - > columnname: illrequest_id >+ - >+ columnname: batch > - > columnname: metadata_author > - >diff --git a/api/v1/swagger/definitions/illrequest.yaml b/api/v1/swagger/definitions/illrequest.yaml >new file mode 100644 >index 0000000000..1d7be94537 >--- /dev/null >+++ b/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 >\ No newline at end of file >diff --git a/api/v1/swagger/definitions/illrequests.yaml b/api/v1/swagger/definitions/illrequests.yaml >new file mode 100644 >index 0000000000..3bbcd52ec4 >--- /dev/null >+++ b/api/v1/swagger/definitions/illrequests.yaml >@@ -0,0 +1,5 @@ >+--- >+type: array >+items: >+ $ref: "illrequest.yaml" >+additionalProperties: false >diff --git a/api/v1/swagger/paths/illrequests.yaml b/api/v1/swagger/paths/illrequests.yaml >index 92725f08b1..3dbf887088 100644 >--- a/api/v1/swagger/paths/illrequests.yaml >+++ b/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" >diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml >index 8e411f985c..3bfa96128e 100644 >--- a/api/v1/swagger/swagger.yaml >+++ b/api/v1/swagger/swagger.yaml >@@ -56,6 +56,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: >diff --git a/ill/ill-requests.pl b/ill/ill-requests.pl >index fc7da41710..62601aadb6 100755 >--- a/ill/ill-requests.pl >+++ b/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' >+ }); >+} >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >index fd0b6531b3..140860c1b0 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >@@ -3816,6 +3816,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; > } >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc >new file mode 100644 >index 0000000000..070ffaffc4 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal-strings.inc >@@ -0,0 +1,36 @@ >+<!-- ill-batch-table-strings.inc --> >+<script> >+ var ill_batch_add = _("Add new batch"); >+ var ill_batch_update = _("Update batch"); >+ var ill_batch_none = _("None"); >+ var ill_batch_retrieving_metadata = _("Retrieving metadata"); >+ var ill_batch_api_fail = _("Unable to retrieve batch details"); >+ var ill_batch_api_request_fail = _("Unable to create local request"); >+ var ill_batch_requests_api_fail = _("Unable to retrieve batch requests"); >+ var ill_batch_unknown = _("Unknown"); >+ var ill_batch_doi = _("DOI"); >+ var ill_batch_pmid = _("PubMed ID"); >+ var ill_populate_waiting = _("Retrieving..."); >+ var ill_populate_failed = _("Failed to retrieve"); >+ var ill_button_remove = _("Remove"); >+ var ill_batch_create_api_fail = _("Unable to create batch request"); >+ var ill_batch_update_api_fail = _("Unable to updatecreate batch request"); >+ var ill_batch_item_remove = _("Are you sure you want to remove this item from the batch"); >+ var ill_batch_metadata_more = _("More"); >+ var ill_batch_metadata_less = _("Less"); >+ var ill_batch_available_via = _("Available via"); >+ var ill_batch_metadata = { >+ 'doi': _("DOI"), >+ 'pmid': _("PubMed ID"), >+ 'issn': _("ISSN"), >+ 'title': _("Title"), >+ 'year': _("Year"), >+ 'issue': _("Issue"), >+ 'pages': _("Pages"), >+ 'publisher': _("Publisher"), >+ 'article_title': _("Article title"), >+ 'article_author': _("Article author"), >+ 'volume': _("Volume") >+ }; >+</script> >+<!-- / ill-batch-table-strings.inc --> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc >new file mode 100644 >index 0000000000..c3556fb06b >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-modal.inc >@@ -0,0 +1,92 @@ >+<div id="ill-batch-details" style="display:none"></div> >+<div id="ill-batch-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="ill-batch-modal-label" aria-hidden="true"> >+ <div class="modal-dialog modal-lg"> >+ <div class="modal-content"> >+ <div class="modal-header"> >+ <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">Ã</button> >+ <h3 id="ill-batch-modal-label"></h3> >+ </div> >+ <div class="modal-body"> >+ <div id="batch-form"> >+ <form id="new-batch-form"> >+ <fieldset class="rows"> >+ <legend id="legend">Batch details</legend> >+ <ol> >+ <li id="batch_name"> >+ <label class="required" for="name">Batch name:</label> >+ <input type="text" autocomplete="off" name="name" id="name" type="text" >+ value="" /> >+ </li> >+ <li id="batch_patron"> >+ <label class="required" for="cardnumber">Card number, username or surname:</label> >+ <input type="text" autocomplete="off" name="cardnumber" id="cardnumber" type="text" value="" /> >+ <span id="patron_link"></span> >+ </li> >+ <li id="batch_branch"> >+ <label class="required" for="branchcode">Library:</label> >+ <select id="branchcode" name="branchcode"> >+ [% PROCESS options_for_libraries libraries => Branches.all( selected => branchcode ) %] >+ </select> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset id="add_batch_items" class="rows" style="display:none"> >+ <legend id="legend">Add batch items</legend> >+ <div id="textarea-metadata"> >+ <div id="supported">Supported identifiers: <span id="supported_identifiers"></span></div> >+ <div id="row_count">Row count: <span id="row_count_value"></span></div> >+ </div> >+ <div id="textarea-errors" style="display:none" class="error"> >+ <div id="duplicates" style="display:none">The following duplicates were found, these have been de-duplicated: <span id="dupelist"></span></div> >+ <div id="badidentifiers" style="display:none">The following unknown identifiers were found, it was not possible to establish their type: <span id="badids"></span></div> >+ </div> >+ <ol> >+ <li> >+ <textarea id="identifiers_input" placeholder="Enter identifiers, one per line"></textarea> >+ </li> >+ <li id="process-button"> >+ <button id="process_button" disabled type="button">Process identifiers</button> >+ </li> >+ </ol> >+ </fieldset> >+ </form> >+ <div id="create-progress" class="alert alert-info" role="alert" style="display:none"> >+ <span id="progress-label"><strong></strong></span> - >+ Items processed: <span id="processed_count">0</span> out of <span id="processed_total">0</span>. >+ Items failed: <span id="processed_failed">0</span>. >+ <div class="progress"> >+ <div id="processed_progress_bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width: 2em;"> >+ 0% >+ </div> >+ </div> >+ </div> >+ <div id="create-requests" style="display:none"> >+ <button id="create-requests-button" type="button" class="btn btn-xs btn-success">Add items to batch</button> >+ </div> >+ <table id="identifier-table" style="display:none"> >+ <thead> >+ <tr id="identifier-table-header"> >+ <th scope="col">Identifier type</th> >+ <th scope="col">Identifier value</th> >+ <th scope="col">Metadata</th> >+ <th scope="col">Request ID</th> >+ <th scope="col"></th> >+ </tr> >+ </thead> >+ <tbody id="identifier-table-body"> >+ </tbody> >+ </table> >+ </div> >+ </div> >+ <div class="modal-footer"> >+ <div id="lhs"> >+ <button class="btn btn-default" data-dismiss="modal" aria-hidden="false">Close</button> >+ </div> >+ <div id="rhs"> >+ <button id="button_create_batch" class="btn btn-default" aria-hidden="true" disabled>Continue</button> >+ <a id="button_finish" disabled type="button" class="btn btn-default" aria-hidden="true">Finish and view batch</a> >+ </div> >+ </div> >+ </div> >+ </div> >+</div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-table-strings.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-table-strings.inc >new file mode 100644 >index 0000000000..2a8d4051e5 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch-table-strings.inc >@@ -0,0 +1,9 @@ >+<!-- ill-batch-table-strings.inc --> >+<script> >+ var ill_batch_borrower_details = _("View borrower details"); >+ var ill_batch_edit = _("Edit"); >+ var ill_batch_delete = _("Delete"); >+ var ill_batch_confirm_delete = _("Are you sure you want to delete this batch? All attached requests will be detached."); >+ var ill_batch_delete_fail = _("Unable to delete batch"); >+</script> >+<!-- / ill-batch-table-strings.inc --> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch.inc >new file mode 100644 >index 0000000000..429de84596 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-batch.inc >@@ -0,0 +1,20 @@ >+[% IF query_type == "batch_list" %] >+<div> >+ <table id="ill-batch-requests"> >+ <thead> >+ <tr id="ill-batch-header"> >+ <th scope="col">Batch ID</th> >+ <th scope="col">Name</th> >+ <th scope="col">Number of requests</th> >+ <th scope="col">Patron</th> >+ <th scope="col">Branch</th> >+ <th scope="col"></th> >+ </tr> >+ </thead> >+ <tbody id="ill-batch-body"> >+ </tbody> >+ </table> >+</div> >+[% ELSIF query_type == "batch_create" %] >+ >+[% END %] >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table.inc >index 755e6e550b..901fd52e57 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-list-table.inc >@@ -6,6 +6,7 @@ > <thead> > <tr id="illview-header"> > <th scope="col">Request ID</th> >+ <th scope="col">Batch</th> > <th scope="col">Author</th> > <th scope="col">Title</th> > <th scope="col">Article title</th> >@@ -38,4 +39,4 @@ > </thead> > <tbody id="illview-body"> > </tbody> >-</table> >+</table> >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc >index 03fec59ce1..94e45c4e65 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/ill-toolbar.inc >@@ -1,6 +1,6 @@ > [% USE Koha %] > [% IF Koha.Preference('ILLModule ') && CAN_user_ill %] >- <div id="toolbar" class="btn-toolbar"> >+ <div id="toolbar" class="btn-toolbar ill-toolbar"> > [% IF backends_available %] > [% IF backends.size > 1 %] > <div class="dropdown btn-group"> >@@ -32,5 +32,22 @@ > <i class="fa fa-list"></i> List requests > </a> > [% END %] >+ [% IF have_batch.size > 0 %] >+ <div id="ill-batch"> >+ <div class="dropdown btn-group"> >+ <button class="btn btn-default dropdown-toggle" type="button" id="ill-batch-backend-dropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> >+ <i class="fa fa-plus"></i> New ILL batch request <span class="caret"></span> >+ </button> >+ <ul class="dropdown-menu" aria-labelledby="ill-batch-backend-dropdown"> >+ [% FOREACH backend IN have_batch %] >+ <li><a href="#" role="button" onclick="window.openBatchModal(null, '[% backend | html %]')">[% backend | html %]</a></li> >+ [% END %] >+ </ul> >+ </div> >+ <a class="btn btn-default" type="button" href="/cgi-bin/koha/ill/ill-requests.pl?method=batch_list" %]"> >+ <i class="fa fa-tasks"></i> Batch requests</span> >+ </a> >+ </div> >+ [% END %] > </div> > [% END %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt >index 5f4cff6bfc..7599e3337a 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/ill/ill-requests.tt >@@ -120,6 +120,7 @@ > <div class="dialog message">ILL module configuration problem. Take a look at the <a href="/cgi-bin/koha/about.pl#sysinfo">about page</a></div> > [% ELSE %] > [% INCLUDE 'ill-toolbar.inc' %] >+ [% INCLUDE 'ill-batch-modal.inc' %] > > [% IF whole.error %] > <h1>Error performing operation</h1> >@@ -433,6 +434,23 @@ > [% END %] > </select> > </li> >+ [% IF batches.count > 0 %] >+ <li class="batch"> >+ <label class="batch_label">Batch:</label> >+ <select id="batch_id" name="batch_id"> >+ <option value=""> >+ [% FOREACH batch IN batches %] >+ [% IF batch.id == request.batch_id %] >+ <option value="[% batch.id | html %]" selected> >+ [% ELSE %] >+ <option value="[% batch.id | html %]"> >+ [% END %] >+ [% batch.name | html %] >+ </option> >+ [% END %] >+ </select> >+ </li> >+ [% END %] > <li class="updated"> > <label class="updated">Last updated:</label> > [% request.updated | $KohaDates with_hours => 1 %] >@@ -646,6 +664,14 @@ > [% END %] > [% END %] > </li> >+ [% IF request.batch > 0 %] >+ <li class="batch"> >+ <span class="label batch">Batch:</span> >+ <a href="/cgi-bin/koha/ill/ill-requests.pl?batch_id=[% request.batch.id | html %]"> >+ [% request.batch.name | html %] >+ </a> >+ </li> >+ [% END %] > <li class="updated"> > <span class="label updated">Last updated:</span> > [% request.updated | $KohaDates with_hours => 1 %] >@@ -777,9 +803,20 @@ > > [% ELSIF query_type == 'illlist' %] > <!-- illlist --> >+<<<<<<< HEAD > <h1>View ILL requests</h1> > <div id="results" class="page-section"> > <h2>Details for all requests</h2> >+======= >+ <h1> >+ View ILL requests >+ [% IF batch %] >+ for batch "[% batch.name | html %]" >+ [% END %] >+ </h1> >+ <div id="results"> >+ <h3>Details for all requests</h3> >+>>>>>>> Bug 30719: Batch ILL requests > [% INCLUDE 'ill-list-table.inc' %] > </div> <!-- /#results --> > [% ELSIF query_type == 'availability' %] >@@ -816,6 +853,8 @@ > [% INCLUDE 'ill-availability-table.inc' service=service %] > [% END %] > </div> >+ [% ELSIF query_type == 'batch_list' || query_type == 'batch_create' %] >+ [% INCLUDE 'ill-batch.inc' %] > [% ELSE %] > <!-- Custom Backend Action --> > [% PROCESS $whole.template %] >@@ -833,6 +872,16 @@ > [% INCLUDE 'columns_settings.inc' %] > [% INCLUDE 'calendar.inc' %] > [% INCLUDE 'select2.inc' %] >+ [% IF metadata_enrichment_services %] >+ <script> >+ var metadata_enrichment_services = [% metadata_enrichment_services | $raw %]; >+ </script> >+ [% END %] >+ [% IF batch_availability_services %] >+ <script> >+ var batch_availability_services = [% batch_availability_services | $raw %]; >+ </script> >+ [% END %] > <script> > var prefilters = '[% prefilters | $raw %]'; > // Set column settings >@@ -857,7 +906,12 @@ > }); > </script> > [% INCLUDE 'ill-list-table-strings.inc' %] >+ [% INCLUDE 'ill-batch-table-strings.inc' %] >+ [% INCLUDE 'ill-batch-modal-strings.inc' %] > [% Asset.js("js/ill-list-table.js") | $raw %] >+ [% Asset.js("js/ill-batch.js") | $raw %] >+ [% Asset.js("js/ill-batch-table.js") | $raw %] >+ [% Asset.js("js/ill-batch-modal.js") | $raw %] > [% IF (query_type == 'availability' || query_type == 'generic_confirm') && Koha.Preference('ILLCheckAvailability') %] > [% Asset.js("js/ill-availability.js") | $raw %] > [% END %] >diff --git a/koha-tmpl/intranet-tmpl/prog/js/ill-batch-modal.js b/koha-tmpl/intranet-tmpl/prog/js/ill-batch-modal.js >new file mode 100644 >index 0000000000..d09d660915 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/ill-batch-modal.js >@@ -0,0 +1,993 @@ >+(function () { >+ window.addEventListener('load', onload); >+ >+ // Delay between API requests >+ var debounceDelay = 1000; >+ >+ // Elements we work frequently with >+ var textarea = document.getElementById("identifiers_input"); >+ var nameInput = document.getElementById("name"); >+ var cardnumberInput = document.getElementById("cardnumber"); >+ var branchcodeSelect = document.getElementById("branchcode"); >+ var processButton = document.getElementById("process_button"); >+ var createButton = document.getElementById("button_create_batch"); >+ var finishButton = document.getElementById("button_finish"); >+ var batchItemsDisplay = document.getElementById("add_batch_items"); >+ var createProgressTotal = document.getElementById("processed_total"); >+ var createProgressCount = document.getElementById("processed_count"); >+ var createProgressFailed = document.getElementById("processed_failed"); >+ var createProgressBar = document.getElementById("processed_progress_bar"); >+ var identifierTable = document.getElementById('identifier-table'); >+ var createRequestsButton = document.getElementById('create-requests-button'); >+ >+ >+ // We need a data structure keyed on identifier type, which tells us how to parse that >+ // identifier type and what services can get its metadata. We receive an array of >+ // available services >+ var supportedIdentifiers = {}; >+ metadata_enrichment_services.forEach(function (service) { >+ // Iterate the identifiers that this service supports >+ Object.keys(service.identifiers_supported).forEach(function (idType) { >+ if (!supportedIdentifiers[idType]) { >+ supportedIdentifiers[idType] = []; >+ } >+ supportedIdentifiers[idType].push(service); >+ }); >+ }); >+ >+ // An object for when we're creating a new batch >+ var emptyBatch = { >+ name: '', >+ backend: null, >+ cardnumber: '', >+ branchcode: '' >+ }; >+ >+ // The object that holds the batch we're working with >+ // It's a proxy so we can update portions of the UI >+ // upon changes >+ var batch = new Proxy( >+ { data: {} }, >+ { >+ get: function (obj, prop) { >+ return obj[prop]; >+ }, >+ set: function (obj, prop, value) { >+ obj[prop] = value; >+ manageBatchItemsDisplay(); >+ updateBatchInputs(); >+ setFinishButton(); >+ disableCardnumberInput(); >+ displayPatronName(); >+ } >+ } >+ ); >+ >+ // The object that holds the contents of the table >+ // It's a proxy so we can make it automatically redraw the >+ // table upon changes >+ var tableContent = new Proxy( >+ { data: [] }, >+ { >+ get: function (obj, prop) { >+ return obj[prop]; >+ }, >+ set: function (obj, prop, value) { >+ obj[prop] = value; >+ updateTable(); >+ updateRowCount(); >+ updateProcessTotals(); >+ checkAvailability(); >+ } >+ } >+ ); >+ >+ var progressTotals = new Proxy( >+ { >+ data: {} >+ }, >+ { >+ get: function (obj, prop) { >+ return obj[prop]; >+ }, >+ set: function (obj, prop, value) { >+ obj[prop] = value; >+ showCreateRequestsButton(); >+ } >+ } >+ ); >+ >+ // Keep track of submission API calls that are in progress >+ // so we don't duplicate them >+ var submissionSent = {}; >+ >+ // Keep track of availability API calls that are in progress >+ // so we don't duplicate them >+ var availabilitySent = {}; >+ >+ // The datatable >+ var table; >+ var tableEl = document.getElementById('identifier-table'); >+ >+ // The element that potentially holds the ID of the batch >+ // we're working with >+ var idEl = document.getElementById('ill-batch-details'); >+ var batchId = null; >+ var backend = null; >+ >+ function onload() { >+ $('#ill-batch-modal').on('shown.bs.modal', function () { >+ init(); >+ patronAutocomplete(); >+ batchInputsEventListeners(); >+ createButtonEventListener(); >+ createRequestsButtonEventListener(); >+ moreLessEventListener(); >+ removeRowEventListener(); >+ }); >+ $('#ill-batch-modal').on('hidden.bs.modal', function () { >+ // Reset our state when we close the modal >+ delete idEl.dataset.batchId; >+ delete idEl.dataset.backend; >+ batchId = null; >+ tableEl.style.display = 'none'; >+ tableContent.data = []; >+ progressTotals.data = { >+ total: 0, >+ count: 0, >+ failed: 0 >+ }; >+ textarea.value = ''; >+ batch.data = {}; >+ // Remove event listeners we created >+ removeEventListeners(); >+ }); >+ }; >+ >+ function init() { >+ batchId = idEl.dataset.batchId; >+ backend = idEl.dataset.backend; >+ emptyBatch.backend = backend; >+ progressTotals.data = { >+ total: 0, >+ count: 0, >+ failed: 0 >+ }; >+ if (batchId) { >+ fetchBatch(); >+ setModalHeading(true); >+ } else { >+ batch.data = emptyBatch; >+ setModalHeading(); >+ } >+ finishButtonEventListener(); >+ processButtonEventListener(); >+ identifierTextareaEventListener(); >+ displaySupportedIdentifiers(); >+ createButtonEventListener(); >+ updateRowCount(); >+ }; >+ >+ function initPostCreate() { >+ disableCreateButton(); >+ }; >+ >+ function setFinishButton() { >+ if (batch.data.patron) { >+ finishButton.removeAttribute('disabled'); >+ } >+ }; >+ >+ function setModalHeading(isUpdate) { >+ var heading = document.getElementById('ill-batch-modal-label'); >+ heading.textContent = isUpdate ? ill_batch_update : ill_batch_add; >+ } >+ >+ // Identify items that have metadata and therefore can have a local request >+ // created, and do so >+ function requestRequestable() { >+ createRequestsButton.setAttribute('disabled', true); >+ var toCheck = tableContent.data; >+ toCheck.forEach(function (row) { >+ if ( >+ !row.requestId && >+ Object.keys(row.metadata).length > 0 && >+ !submissionSent[row.value] >+ ) { >+ submissionSent[row.value] = 1; >+ makeLocalSubmission(row.value, row.metadata); >+ } >+ }); >+ }; >+ >+ // Identify items that can have their availability checked, and do it >+ function checkAvailability() { >+ // Only proceed if we've got services that can check availability >+ if (!batch_availability_services || batch_availability_services.length === 0) return; >+ var toCheck = tableContent.data; >+ toCheck.forEach(function (row) { >+ if ( >+ !row.url && >+ Object.keys(row.metadata).length > 0 && >+ !availabilitySent[row.value] >+ ) { >+ availabilitySent[row.value] = 1; >+ getAvailability(row.value, row.metadata); >+ } >+ }); >+ }; >+ >+ // Check availability services for immediate availability, if found, >+ // create a link in the table linking to the item >+ function getAvailability(identifier, metadata) { >+ // Prep the metadata for passing to the availability plugins >+ var prepped = encodeURIComponent(base64EncodeUnicode(JSON.stringify(metadata))); >+ for (i = 0; i < batch_availability_services.length; i++) { >+ var service = batch_availability_services[i]; >+ window.doApiRequest( >+ service.endpoint + prepped >+ ) >+ .then(function (response) { >+ return response.json(); >+ }) >+ .then(function (data) { >+ if (data.results.search_results && data.results.search_results.length > 0) { >+ var result = data.results.search_results[0]; >+ tableContent.data = tableContent.data.map(function (row) { >+ if (row.value === identifier) { >+ row.url = result.url; >+ row.availabilitySupplier = service.name; >+ } >+ return row; >+ }); >+ } >+ }); >+ } >+ }; >+ >+ // Help btoa with > 8 bit strings >+ // Shamelessly grabbed from: https://www.base64encoder.io/javascript/ >+ function base64EncodeUnicode(str) { >+ // First we escape the string using encodeURIComponent to get the UTF-8 encoding of the characters, >+ // then we convert the percent encodings into raw bytes, and finally feed it to btoa() function. >+ utf8Bytes = encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { >+ return String.fromCharCode('0x' + p1); >+ }); >+ >+ return btoa(utf8Bytes); >+ }; >+ >+ // Create a local submission and update our local state >+ // upon success >+ function makeLocalSubmission(identifier, metadata) { >+ var payload = { >+ batch_id: batchId, >+ backend: batch.data.backend, >+ borrowernumber: batch.data.patron.borrowernumber, >+ branchcode: batch.data.branchcode, >+ metadata: metadata >+ }; >+ window.doCreateSubmission(payload) >+ .then(function (response) { >+ return response.json(); >+ }) >+ .then(function (data) { >+ tableContent.data = tableContent.data.map(function (row) { >+ if (row.value === identifier) { >+ row.requestId = data.illrequest_id; >+ } >+ return row; >+ }); >+ }) >+ .catch(function () { >+ window.handleApiError(ill_batch_api_request_fail); >+ }); >+ }; >+ >+ function updateProcessTotals() { >+ var init = { >+ total: 0, >+ count: 0, >+ failed: 0 >+ }; >+ progressTotals.data = init; >+ var toUpdate = progressTotals.data; >+ tableContent.data.forEach(function (row) { >+ toUpdate.total++; >+ if (Object.keys(row.metadata).length > 0 || row.failed.length > 0) { >+ toUpdate.count++; >+ } >+ if (Object.keys(row.failed).length > 0) { >+ toUpdate.failed++; >+ } >+ }); >+ createProgressTotal.innerHTML = toUpdate.total; >+ createProgressCount.innerHTML = toUpdate.count; >+ createProgressFailed.innerHTML = toUpdate.failed; >+ var percentDone = Math.ceil((toUpdate.count / toUpdate.total) * 100); >+ createProgressBar.setAttribute('aria-valuenow', percentDone); >+ createProgressBar.innerHTML = percentDone + '%'; >+ createProgressBar.style.width = percentDone + '%'; >+ progressTotals.data = toUpdate; >+ }; >+ >+ function displayPatronName() { >+ var span = document.getElementById('patron_link'); >+ if (batch.data.patron) { >+ var link = createPatronLink(); >+ span.appendChild(link); >+ } else { >+ if (span.children.length > 0) { >+ span.removeChild(span.firstChild); >+ } >+ } >+ }; >+ >+ function removeEventListeners() { >+ textarea.removeEventListener('paste', processButtonState); >+ textarea.removeEventListener('keyup', processButtonState); >+ processButton.removeEventListener('click', processIdentifiers); >+ nameInput.removeEventListener('keyup', createButtonState); >+ cardnumberInput.removeEventListener('keyup', createButtonState); >+ branchcodeSelect.removeEventListener('change', createButtonState); >+ createButton.removeEventListener('click', createBatch); >+ identifierTable.removeEventListener('click', toggleMetadata); >+ identifierTable.removeEventListener('click', removeRow); >+ createRequestsButton.remove('click', requestRequestable); >+ }; >+ >+ function finishButtonEventListener() { >+ finishButton.addEventListener('click', doFinish); >+ }; >+ >+ function identifierTextareaEventListener() { >+ textarea.addEventListener('paste', textareaUpdate); >+ textarea.addEventListener('keyup', textareaUpdate); >+ }; >+ >+ function processButtonEventListener() { >+ processButton.addEventListener('click', processIdentifiers); >+ }; >+ >+ function createRequestsButtonEventListener() { >+ createRequestsButton.addEventListener('click', requestRequestable); >+ }; >+ >+ function createButtonEventListener() { >+ createButton.addEventListener('click', createBatch); >+ }; >+ >+ function batchInputsEventListeners() { >+ nameInput.addEventListener('keyup', createButtonState); >+ cardnumberInput.addEventListener('keyup', createButtonState); >+ branchcodeSelect.addEventListener('change', createButtonState); >+ }; >+ >+ function moreLessEventListener() { >+ identifierTable.addEventListener('click', toggleMetadata); >+ }; >+ >+ function removeRowEventListener() { >+ identifierTable.addEventListener('click', removeRow); >+ }; >+ >+ function textareaUpdate() { >+ processButtonState(); >+ updateRowCount(); >+ }; >+ >+ function processButtonState() { >+ if (textarea.value.length > 0) { >+ processButton.removeAttribute('disabled'); >+ } else { >+ processButton.setAttribute('disabled', 1); >+ } >+ }; >+ >+ function disableCardnumberInput() { >+ if (batch.data.patron) { >+ cardnumberInput.setAttribute('disabled', true); >+ } else { >+ cardnumberInput.removeAttribute('disabled'); >+ } >+ }; >+ >+ function createButtonState() { >+ if ( >+ nameInput.value.length > 0 && >+ cardnumberInput.value.length > 0 && >+ branchcodeSelect.selectedOptions.length === 1 >+ ) { >+ createButton.removeAttribute('disabled'); >+ createButton.setAttribute('display', 'inline-block'); >+ } else { >+ createButton.setAttribute('disabled', 1); >+ createButton.setAttribute('display', 'none'); >+ } >+ }; >+ >+ function doFinish() { >+ updateBatch() >+ .then(function () { >+ $('#ill-batch-modal').modal({ show: false }); >+ location.href = '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + batch.data.id; >+ }); >+ }; >+ >+ // Get the batch >+ function fetchBatch() { >+ window.doBatchApiRequest("/" + batchId) >+ .then(function (response) { >+ return response.json(); >+ }) >+ .then(function (jsoned) { >+ batch.data = { >+ id: jsoned.id, >+ name: jsoned.name, >+ backend: jsoned.backend, >+ cardnumber: jsoned.cardnumber, >+ branchcode: jsoned.branchcode >+ } >+ return jsoned; >+ }) >+ .then(function (data) { >+ batch.data = data; >+ }) >+ .catch(function () { >+ window.handleApiError(ill_batch_api_fail); >+ }); >+ >+ }; >+ >+ function createBatch() { >+ var selectedBranchcode = branchcodeSelect.selectedOptions[0].value; >+ return doBatchApiRequest('', { >+ method: 'POST', >+ headers: { >+ 'Content-type': 'application/json' >+ }, >+ body: JSON.stringify({ >+ name: nameInput.value, >+ backend: backend, >+ cardnumber: cardnumberInput.value, >+ branchcode: selectedBranchcode >+ }) >+ }) >+ .then(function (response) { >+ return response.json(); >+ }) >+ .then(function (body) { >+ batchId = body.id; >+ batch.data = { >+ id: body.id, >+ name: body.name, >+ backend: body.backend, >+ cardnumber: body.patron.cardnumber, >+ branchcode: body.branchcode, >+ patron: body.patron >+ }; >+ initPostCreate(); >+ }) >+ .catch(function () { >+ handleApiError(ill_batch_create_api_fail); >+ }); >+ }; >+ >+ function updateBatch() { >+ var selectedBranchcode = branchcodeSelect.selectedOptions[0].value; >+ return doBatchApiRequest('/' + batch.data.id, { >+ method: 'PUT', >+ headers: { >+ 'Content-type': 'application/json' >+ }, >+ body: JSON.stringify({ >+ name: nameInput.value, >+ backend: batch.data.backend, >+ cardnumber: batch.data.patron.cardnumber, >+ branchcode: selectedBranchcode >+ }) >+ }) >+ .catch(function () { >+ handleApiError(ill_batch_update_api_fail); >+ }); >+ }; >+ >+ function displaySupportedIdentifiers() { >+ var names = Object.keys(supportedIdentifiers).map(function (identifier) { >+ return window['ill_batch_' + identifier]; >+ }); >+ var displayEl = document.getElementById('supported_identifiers'); >+ displayEl.textContent = names.length > 0 ? names.join(', ') : ill_batch_none; >+ } >+ >+ function updateRowCount() { >+ var textEl = document.getElementById('row_count_value'); >+ var val = textarea.value.trim(); >+ var cnt = 0; >+ if (val.length > 0) { >+ cnt = val.split(/\n/).length; >+ } >+ textEl.textContent = cnt; >+ } >+ >+ function showProgress() { >+ var el = document.getElementById('create-progress'); >+ el.style.display = 'block'; >+ } >+ >+ function showCreateRequestsButton() { >+ var data = progressTotals.data; >+ var el = document.getElementById('create-requests'); >+ el.style.display = (data.total > 0 && data.count === data.total) ? 'flex' : 'none'; >+ } >+ >+ async function processIdentifiers() { >+ var content = textarea.value; >+ hideErrors(); >+ if (content.length === 0) return; >+ >+ disableProcessButton(); >+ var label = document.getElementById('progress-label').firstChild; >+ label.innerHTML = ill_batch_retrieving_metadata; >+ showProgress(); >+ >+ // Errors encountered when processing >+ var processErrors = {}; >+ >+ // Prepare the content, including trimming each row >+ var contentArr = content.split(/\n/); >+ var trimmed = contentArr.map(function (row) { >+ return row.trim(); >+ }); >+ >+ var parsed = []; >+ >+ trimmed.forEach(function (identifier) { >+ var match = identifyIdentifier(identifier); >+ // If this identifier is not identifiable or >+ // looks like more than one type, we can't be sure >+ // what it is >+ if (match.length != 1) { >+ parsed.push({ >+ type: 'unknown', >+ value: identifier >+ }); >+ } else { >+ parsed.push(match[0]); >+ } >+ }); >+ >+ var unknownIdentifiers = parsed >+ .filter(function (parse) { >+ if (parse.type == 'unknown') { >+ return parse; >+ } >+ }) >+ .map(function (filtered) { >+ return filtered.value; >+ }); >+ >+ if (unknownIdentifiers.length > 0) { >+ processErrors.badidentifiers = { >+ element: 'badids', >+ values: unknownIdentifiers.join(', ') >+ }; >+ }; >+ >+ // Deduping >+ var deduped = []; >+ var dupes = {}; >+ parsed.forEach(function (row) { >+ var value = row.value; >+ var alreadyInDeduped = deduped.filter(function (d) { >+ return d.value === value; >+ }); >+ if (alreadyInDeduped.length > 0 && !dupes[value]) { >+ dupes[value] = 1; >+ } else if (alreadyInDeduped.length === 0) { >+ row.metadata = {}; >+ row.failed = {}; >+ row.requestId = null; >+ deduped.push(row); >+ } >+ }); >+ // Update duplicate error if dupes were found >+ if (Object.keys(dupes).length > 0) { >+ processErrors.duplicates = { >+ element: 'dupelist', >+ values: Object.keys(dupes).join(', ') >+ }; >+ } >+ >+ // Display any errors >+ displayErrors(processErrors); >+ >+ // Now build and display the table >+ if (!table) { >+ buildTable(); >+ } >+ >+ // We may be appending new values to an existing table, >+ // in which case, ensure we don't create duplicates >+ var tabIdentifiers = tableContent.data.map(function (tabId) { >+ return tabId.value; >+ }); >+ var notInTable = deduped.filter(function (ded) { >+ if (!tabIdentifiers.includes(ded.value)) { >+ return ded; >+ } >+ }); >+ if (notInTable.length > 0) { >+ tableContent.data = tableContent.data.concat(notInTable); >+ } >+ >+ // Populate metadata for those records that need it >+ var newData = tableContent.data; >+ for (var i = 0; i < tableContent.data.length; i++) { >+ var row = tableContent.data[i]; >+ // Skip rows that don't need populating >+ if ( >+ Object.keys(tableContent.data[i].metadata).length > 0 || >+ Object.keys(tableContent.data[i].failed).length > 0 >+ ) continue; >+ var identifier = { type: row.type, value: row.value }; >+ try { >+ var populated = await populateMetadata(identifier); >+ row.metadata = populated.results.result || {}; >+ } catch (e) { >+ row.failed = ill_populate_failed; >+ } >+ newData[i] = row; >+ tableContent.data = newData; >+ } >+ } >+ >+ function disableProcessButton() { >+ processButton.setAttribute('disabled', true); >+ } >+ >+ function disableCreateButton() { >+ createButton.setAttribute('disabled', true); >+ } >+ >+ function disableRemoveRowButtons() { >+ var buttons = document.getElementsByClassName('remove-row'); >+ for (var button of buttons) { >+ button.setAttribute('disabled', true); >+ } >+ } >+ >+ async function populateMetadata(identifier) { >+ // All services that support this identifier type >+ var services = supportedIdentifiers[identifier.type]; >+ // Check each service and use the first results we get, if any >+ for (var i = 0; i < services.length; i++) { >+ var service = services[i]; >+ var endpoint = '/api/v1/contrib/' + service.api_namespace + service.search_endpoint + '?' + identifier.type + '=' + identifier.value; >+ var metadata = await getMetadata(endpoint); >+ if (metadata.errors.length === 0) { >+ var parsed = await parseMetadata(metadata, service); >+ if (parsed.errors.length > 0) { >+ throw Error(metadata.errors.map(function (error) { >+ return error.message; >+ }).join(', ')); >+ } >+ return parsed; >+ } >+ } >+ }; >+ >+ async function getMetadata(endpoint) { >+ var response = await debounce(doApiRequest)(endpoint); >+ return response.json(); >+ }; >+ >+ async function parseMetadata(metadata, service) { >+ var endpoint = '/api/v1/contrib/' + service.api_namespace + service.ill_parse_endpoint; >+ var response = await doApiRequest(endpoint, { >+ method: 'POST', >+ headers: { >+ 'Content-type': 'application/json' >+ }, >+ body: JSON.stringify(metadata) >+ }); >+ return response.json(); >+ } >+ >+ // A render function for identifier type >+ function createIdentifierType(data) { >+ return window['ill_batch_' + data]; >+ }; >+ >+ // Get an item's title >+ function getTitle(meta) { >+ if (meta.article_title && meta.article_title.length > 0) { >+ return { >+ prop: 'article_title', >+ value: meta.article_title >+ }; >+ } else if (meta.title && meta.title.length > 0) { >+ return { >+ prop: 'title', >+ value: meta.title >+ }; >+ } >+ }; >+ >+ // Create a metadata row >+ function createMetadataRow(data, meta, prop) { >+ if (!meta[prop]) return; >+ >+ var div = document.createElement('div'); >+ div.classList.add('metadata-row'); >+ var label = document.createElement('span'); >+ label.classList.add('metadata-label'); >+ label.innerText = ill_batch_metadata[prop] + ': '; >+ >+ // Add a link to the availability URL if appropriate >+ var value; >+ if (!data.url) { >+ value = document.createElement('span'); >+ } else { >+ value = document.createElement('a'); >+ value.setAttribute('href', data.url); >+ value.setAttribute('target', '_blank'); >+ value.setAttribute('title', ill_batch_available_via + ' ' + data.availabilitySupplier); >+ } >+ value.classList.add('metadata-value'); >+ value.innerText = meta[prop]; >+ div.appendChild(label); >+ div.appendChild(value); >+ >+ return div; >+ } >+ >+ // A render function for displaying metadata >+ function createMetadata(x, y, data) { >+ // If the fetch failed >+ if (data.failed.length > 0) { >+ return data.failed; >+ } >+ >+ // If we've not yet got any metadata back >+ if (Object.keys(data.metadata).length === 0) { >+ return ill_populate_waiting; >+ } >+ >+ var core = ['doi', 'pmid', 'issn', 'title', 'year', 'issue', 'pages', 'publisher', 'article_title', 'article_author', 'volume']; >+ var meta = data.metadata; >+ >+ var container = document.createElement('div'); >+ container.classList.add('metadata-container'); >+ >+ // Create the title row >+ var title = getTitle(meta); >+ if (title) { >+ // Remove the title element from the props >+ // we're about to iterate >+ core = core.filter(function (i) { >+ return i !== title.prop; >+ }); >+ var titleRow = createMetadataRow(data, meta, title.prop); >+ container.appendChild(titleRow); >+ } >+ >+ var remainder = document.createElement('div'); >+ remainder.classList.add('metadata-remainder'); >+ remainder.style.display = 'none'; >+ // Create the remaining rows >+ core.sort().forEach(function (prop) { >+ var div = createMetadataRow(data, meta, prop); >+ if (div) { >+ remainder.appendChild(div); >+ } >+ }); >+ container.appendChild(remainder); >+ >+ // Add a more/less toggle >+ var firstField = container.firstChild; >+ var moreLess = document.createElement('div'); >+ moreLess.classList.add('more-less'); >+ var moreLessLink = document.createElement('a'); >+ moreLessLink.setAttribute('href', '#'); >+ moreLessLink.classList.add('more-less-link'); >+ moreLessLink.innerText = ' [' + ill_batch_metadata_more + ']'; >+ moreLess.appendChild(moreLessLink); >+ firstField.appendChild(moreLess); >+ >+ return container.outerHTML; >+ }; >+ >+ function removeRow(ev) { >+ if (ev.target.className.includes('remove-row')) { >+ if (!confirm(ill_batch_item_remove)) return; >+ // Find the parent row >+ var ancestor = ev.target.closest('tr'); >+ var identifier = ancestor.querySelector('.identifier').innerText; >+ tableContent.data = tableContent.data.filter(function (row) { >+ return row.value !== identifier; >+ }); >+ } >+ } >+ >+ function toggleMetadata(ev) { >+ if (ev.target.className === 'more-less-link') { >+ // Find the element we need to show >+ var ancestor = ev.target.closest('.metadata-container'); >+ var meta = ancestor.querySelector('.metadata-remainder'); >+ >+ // Display or hide based on its current state >+ var display = window.getComputedStyle(meta).display; >+ >+ meta.style.display = display === 'block' ? 'none' : 'block'; >+ >+ // Update the More / Less text >+ ev.target.innerText = ' [ ' + (display === 'none' ? ill_batch_metadata_less : ill_batch_metadata_more) + ' ]'; >+ } >+ } >+ >+ // A render function for the link to a request ID >+ function createRequestId(x, y, data) { >+ return data.requestId || '-'; >+ } >+ >+ function buildTable(identifiers) { >+ table = KohaTable('identifier-table', { >+ processing: true, >+ deferRender: true, >+ ordering: false, >+ paging: false, >+ searching: false, >+ autoWidth: false, >+ columns: [ >+ { >+ data: 'type', >+ width: '13%', >+ render: createIdentifierType >+ }, >+ { >+ data: 'value', >+ width: '25%', >+ className: 'identifier' >+ }, >+ { >+ data: 'metadata', >+ render: createMetadata >+ }, >+ { >+ data: 'requestId', >+ width: '13%', >+ render: createRequestId >+ }, >+ { >+ width: '18%', >+ render: createActions, >+ className: 'action-column' >+ } >+ ], >+ createdRow: function (row, data) { >+ if (data.failed.length > 0) { >+ row.classList.add('fetch-failed'); >+ } >+ } >+ }); >+ } >+ >+ function createActions(x, y, data) { >+ return '<button type="button"' + (data.requestId ? ' disabled' : '') + ' class="btn btn-xs btn-danger remove-row">' + ill_button_remove + '</button>'; >+ } >+ >+ // Redraw the table >+ function updateTable() { >+ if (!table) return; >+ tableEl.style.display = tableContent.data.length > 0 ? 'table' : 'none'; >+ tableEl.style.width = '100%'; >+ table.api() >+ .clear() >+ .rows.add(tableContent.data) >+ .draw(); >+ }; >+ >+ function identifyIdentifier(identifier) { >+ var matches = []; >+ >+ // Iterate our available services to see if any can identify this identifier >+ Object.keys(supportedIdentifiers).forEach(function (identifierType) { >+ // Since all the services supporting this identifier type should use the same >+ // regex to identify it, we can just use the first >+ var service = supportedIdentifiers[identifierType][0]; >+ var regex = new RegExp(service.identifiers_supported[identifierType].regex); >+ var match = identifier.match(regex); >+ if (match && match.groups && match.groups.identifier) { >+ matches.push({ >+ type: identifierType, >+ value: match.groups.identifier >+ }); >+ } >+ }); >+ return matches; >+ } >+ >+ function displayErrors(errors) { >+ var keys = Object.keys(errors); >+ if (keys.length > 0) { >+ keys.forEach(function (key) { >+ var el = document.getElementById(errors[key].element); >+ el.textContent = errors[key].values; >+ el.style.display = 'inline'; >+ var container = document.getElementById(key); >+ container.style.display = 'block'; >+ }); >+ var el = document.getElementById('textarea-errors'); >+ el.style.display = 'flex'; >+ } >+ } >+ >+ function hideErrors() { >+ var dupelist = document.getElementById('dupelist'); >+ var badids = document.getElementById('badids'); >+ dupelist.textContent = ''; >+ dupelist.parentElement.style.display = 'none'; >+ badids.textContent = ''; >+ badids.parentElement.style.display = 'none'; >+ var tae = document.getElementById('textarea-errors'); >+ tae.style.display = 'none'; >+ } >+ >+ function manageBatchItemsDisplay() { >+ batchItemsDisplay.style.display = batch.data.id ? 'block' : 'none' >+ }; >+ >+ function updateBatchInputs() { >+ nameInput.value = batch.data.name || ''; >+ cardnumberInput.value = batch.data.cardnumber || ''; >+ branchcodeSelect.value = batch.data.branchcode || ''; >+ } >+ >+ function debounce(func) { >+ var timeout; >+ return function (...args) { >+ return new Promise(function (resolve) { >+ if (timeout) { >+ clearTimeout(timeout); >+ } >+ timeout = setTimeout(function () { >+ return resolve(func(...args)); >+ }, debounceDelay); >+ }); >+ } >+ } >+ >+ function patronAutocomplete() { >+ // Add autocomplete for patron selection >+ $('#batch-form #cardnumber').autocomplete({ >+ appendTo: '#batch-form', >+ source: "/cgi-bin/koha/circ/ysearch.pl", >+ minLength: 3, >+ select: function (event, ui) { >+ var field = ui.item.cardnumber; >+ $('#batch-form #cardnumber').val(field) >+ return false; >+ } >+ }) >+ .data("ui-autocomplete")._renderItem = function (ul, item) { >+ return $("<li></li>") >+ .data("ui-autocomplete-item", item) >+ .append("<a>" + item.surname + ", " + item.firstname + " (" + item.cardnumber + ") <small>" + item.address + " " + item.city + " " + item.zipcode + " " + item.country + "</small></a>") >+ .appendTo(ul); >+ }; >+ }; >+ >+ function createPatronLink() { >+ if (!batch.data.patron) return; >+ var patron = batch.data.patron; >+ var a = document.createElement('a'); >+ var href = '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + patron.borrowernumber; >+ var text = patron.surname + ' (' + patron.cardnumber + ')'; >+ a.setAttribute('title', ill_borrower_details); >+ a.setAttribute('href', href); >+ a.textContent = text; >+ return a; >+ }; >+ >+})(); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/ill-batch-table.js b/koha-tmpl/intranet-tmpl/prog/js/ill-batch-table.js >new file mode 100644 >index 0000000000..502965677f >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/ill-batch-table.js >@@ -0,0 +1,205 @@ >+(function () { >+ var table; >+ var batchesProxy; >+ >+ window.addEventListener('load', onload); >+ >+ function onload() { >+ // Only proceed if appropriate >+ if (!document.getElementById('ill-batch-requests')) return; >+ >+ // A proxy that will give us some element of reactivity to >+ // changes in our list of batches >+ batchesProxy = new Proxy( >+ { data: [] }, >+ { >+ get: function (obj, prop) { >+ return obj[prop]; >+ }, >+ set: function (obj, prop, value) { >+ obj[prop] = value; >+ updateTable(); >+ } >+ } >+ ); >+ >+ // Initialise the Datatable, binding it to our proxy object >+ table = initTable(); >+ >+ // Do the initial data population >+ window.doBatchApiRequest() >+ .then(function (response) { >+ return response.json(); >+ }) >+ .then(function (data) { >+ batchesProxy.data = data; >+ }); >+ >+ // Clean up any event listeners we added >+ window.addEventListener('beforeunload', removeEventListeners); >+ }; >+ >+ // Initialise the Datatable >+ var initTable = function () { >+ return KohaTable("ill-batch-requests", { >+ data: batchesProxy.data, >+ columns: [ >+ { >+ data: 'id', >+ width: '10%' >+ }, >+ { >+ data: 'name', >+ render: createName, >+ width: '30%' >+ }, >+ { >+ data: 'requests_count', >+ width: '10%' >+ }, >+ { >+ data: 'patron', >+ render: createPatronLink, >+ width: '20%' >+ }, >+ { >+ data: 'branch', >+ render: createBranch, >+ width: '20%' >+ }, >+ { >+ render: createActions, >+ width: '10%', >+ orderable: false >+ } >+ ], >+ processing: true, >+ deferRender: true, >+ drawCallback: addEventListeners >+ }); >+ } >+ >+ // A render function for branch name >+ var createBranch = function (data) { >+ return data.branchname; >+ } >+ >+ // A render function for batch name >+ var createName = function (x, y, data) { >+ var a = document.createElement('a'); >+ a.setAttribute('href', '/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + data.id); >+ a.setAttribute('title', data.name); >+ a.textContent = data.name; >+ return a.outerHTML; >+ }; >+ >+ // A render function for our patron link >+ var createPatronLink = function (data) { >+ var link = document.createElement('a'); >+ link.setAttribute('title', ill_batch_borrower_details); >+ link.setAttribute('href', '/cgi-bin/koha/members/moremember.pl?borrowernumber=' + data.borrowernumber); >+ var displayText = [data.firstname, data.surname].join(' ') + ' ( ' + data.cardnumber + ' )'; >+ link.appendChild(document.createTextNode(displayText)); >+ >+ return link.outerHTML; >+ }; >+ >+ // A render function for our row action buttons >+ var createActions = function (data, type, row) { >+ var div = document.createElement('div'); >+ div.setAttribute('class', 'action-buttons'); >+ >+ var editButton = document.createElement('button'); >+ editButton.setAttribute('type', 'button'); >+ editButton.setAttribute('class', 'editButton btn btn-xs btn-default'); >+ editButton.setAttribute('data-batch-id', row.id); >+ editButton.appendChild(document.createTextNode(ill_batch_edit)); >+ >+ var deleteButton = document.createElement('button'); >+ deleteButton.setAttribute('type', 'button'); >+ deleteButton.setAttribute('class', 'deleteButton btn btn-xs btn-danger'); >+ deleteButton.setAttribute('data-batch-id', row.id); >+ deleteButton.appendChild(document.createTextNode(ill_batch_delete)); >+ >+ div.appendChild(editButton); >+ div.appendChild(deleteButton); >+ >+ return div.outerHTML; >+ }; >+ >+ // Add event listeners to our row action buttons >+ var addEventListeners = function () { >+ var del = document.querySelectorAll('.deleteButton'); >+ del.forEach(function (el) { >+ el.addEventListener('click', handleDeleteClick); >+ }); >+ >+ var edit = document.querySelectorAll('.editButton'); >+ edit.forEach(function (elEdit) { >+ elEdit.addEventListener('click', handleEditClick); >+ }); >+ }; >+ >+ // Remove all added event listeners >+ var removeEventListeners = function () { >+ var del = document.querySelectorAll('.deleteButton'); >+ del.forEach(function (el) { >+ el.removeEventListener('click', handleDeleteClick); >+ }); >+ window.removeEventListener('load', onload); >+ window.removeEventListener('beforeunload', removeEventListeners); >+ }; >+ >+ // Handle "Delete" clicks >+ var handleDeleteClick = function(e) { >+ var el = e.srcElement; >+ if (confirm(ill_batch_confirm_delete)) { >+ deleteBatch(el); >+ } >+ }; >+ >+ // Handle "Edit" clicks >+ var handleEditClick = function(e) { >+ var el = e.srcElement; >+ var id = el.dataset.batchId; >+ window.openBatchModal(id); >+ }; >+ >+ // Delete a batch >+ // - Make the API call >+ // - Handle errors >+ // - Update our proxy data >+ var deleteBatch = function (el) { >+ var id = el.dataset.batchId; >+ doBatchApiRequest( >+ '/' + id, >+ { method: 'DELETE' } >+ ) >+ .then(function (response) { >+ if (!response.ok) { >+ window.handleApiError(ill_batch_delete_fail); >+ } else { >+ removeBatch(el.dataset.batchId); >+ } >+ }) >+ .catch(function (response) { >+ window.handleApiError(ill_batch_delete_fail); >+ }) >+ }; >+ >+ // Remove a batch from our proxy data >+ var removeBatch = function(id) { >+ batchesProxy.data = batchesProxy.data.filter(function (batch) { >+ return batch.id != id; >+ }); >+ }; >+ >+ // Redraw the table >+ var updateTable = function () { >+ table.api() >+ .clear() >+ .rows.add(batchesProxy.data) >+ .draw(); >+ }; >+ >+})(); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/ill-batch.js b/koha-tmpl/intranet-tmpl/prog/js/ill-batch.js >new file mode 100644 >index 0000000000..37d4c2d3dd >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/ill-batch.js >@@ -0,0 +1,49 @@ >+(function () { >+ // Enable the modal to be opened from anywhere >+ // If we're working with an existing batch, set the ID so the >+ // modal can access it >+ window.openBatchModal = function (id, backend) { >+ var idEl = document.getElementById('ill-batch-details'); >+ idEl.dataset.backend = backend; >+ if (id) { >+ idEl.dataset.batchId = id; >+ } >+ $('#ill-batch-modal').modal({ show: true }); >+ }; >+ >+ // Make a batch API call, returning the resulting promise >+ window.doBatchApiRequest = function (url, options) { >+ var batchListApi = '/api/v1/illbatches'; >+ var fullUrl = batchListApi + (url ? url : ''); >+ return doApiRequest(fullUrl, options); >+ }; >+ >+ // Make a "create local ILL submission" call >+ window.doCreateSubmission = function (body, options) { >+ options = Object.assign( >+ options || {}, >+ { >+ headers: { >+ 'Content-Type': 'application/json' >+ }, >+ method: 'POST', >+ body: JSON.stringify(body) >+ } >+ ); >+ return doApiRequest( >+ '/api/v1/illrequests', >+ options >+ ) >+ } >+ >+ // Make an API call, returning the resulting promise >+ window.doApiRequest = function (url, options) { >+ return fetch(url, options); >+ }; >+ >+ // Display an API error >+ window.handleApiError = function (error) { >+ alert(error); >+ }; >+ >+})(); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/ill-list-table.js b/koha-tmpl/intranet-tmpl/prog/js/ill-list-table.js >index d6e92cc517..245941c5d6 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/ill-list-table.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/ill-list-table.js >@@ -221,6 +221,12 @@ $(document).ready(function() { > return row.id_prefix + row.illrequest_id; > }; > >+ // Render function for batch >+ var createBatch = function (data, type, row) { >+ if (!row.batch) return; >+ return '<a href="/cgi-bin/koha/ill/ill-requests.pl?batch_id=' + row.batch_id + '">' + row.batch.name + '</a>'; >+ }; >+ > // Render function for type > var createType = function(data, type, row) { > if (!row.hasOwnProperty('metadata_Type') || !row.metadata_Type) { >@@ -291,6 +297,10 @@ $(document).ready(function() { > func: createActionLink, > skipSanitize: true > }, >+ batch: { >+ func: createBatch, >+ skipSanitize: true >+ }, > illrequest_id: { > func: createRequestId > }, >@@ -383,7 +393,10 @@ $(document).ready(function() { > ( > // ILL list requests page > window.location.href.match(/ill\/ill-requests\.pl/) && >- window.location.search.length == 0 >+ ( >+ window.location.search.length == 0 || >+ /borrowernumber|batch_id/.test(window.location.search) >+ ) > ) || > // Patron profile page > window.location.href.match(/members\/ill-requests\.pl/) >@@ -391,7 +404,7 @@ $(document).ready(function() { > var ajax = $.ajax( > '/api/v1/illrequests?embed=metadata,patron,capabilities,library,status_alias,comments,requested_partners' > + filterParam >- ).done(function() { >+ ).done(function () { > var data = JSON.parse(ajax.responseText); > // Make a copy, we'll be removing columns next and need > // to be able to refer to data that has been removed >diff --git a/t/db_dependent/api/v1/illrequests.t b/t/db_dependent/api/v1/illrequests.t >index 31c220c724..368a612144 100755 >--- a/t/db_dependent/api/v1/illrequests.t >+++ b/t/db_dependent/api/v1/illrequests.t >@@ -17,7 +17,7 @@ > > use Modern::Perl; > >-use Test::More tests => 1; >+use Test::More tests => 2; > > use Test::MockModule; > use Test::MockObject; >@@ -173,6 +173,90 @@ subtest 'list() tests' => sub { > $schema->storage->txn_rollback; > }; > >+subtest 'add() tests' => sub { >+ >+ plan tests => 2; >+ >+ $schema->storage->txn_begin; >+ >+ # create an authorized user >+ my $patron = $builder->build_object({ >+ class => 'Koha::Patrons', >+ value => { flags => 2 ** 22 } # 22 => ill >+ }); >+ my $password = 'thePassword123'; >+ $patron->set_password({ password => $password, skip_validation => 1 }); >+ my $userid = $patron->userid; >+ >+ my $library = $builder->build_object( { class => 'Koha::Libraries' } ); >+ >+ # Create an ILL request >+ my $illrequest = $builder->build_object( >+ { >+ class => 'Koha::Illrequests', >+ value => { >+ backend => 'Mock', >+ branchcode => $library->branchcode, >+ borrowernumber => $patron->borrowernumber, >+ status => 'STATUS1', >+ } >+ } >+ ); >+ >+ # Mock ILLBackend (as object) >+ my $backend = Test::MockObject->new; >+ $backend->set_isa('Koha::Illbackends::Mock'); >+ $backend->set_always('name', 'Mock'); >+ $backend->set_always('capabilities', sub { >+ return $illrequest; >+ } ); >+ $backend->mock( >+ 'metadata', >+ sub { >+ my ( $self, $rq ) = @_; >+ return { >+ ID => $rq->illrequest_id, >+ Title => $rq->patron->borrowernumber >+ } >+ } >+ ); >+ $backend->mock( >+ 'status_graph', sub {}, >+ ); >+ >+ # Mock Koha::Illrequest::load_backend (to load Mocked Backend) >+ my $illreqmodule = Test::MockModule->new('Koha::Illrequest'); >+ $illreqmodule->mock( 'load_backend', >+ sub { my $self = shift; $self->{_my_backend} = $backend; return $self } >+ ); >+ >+ $schema->storage->txn_begin; >+ >+ Koha::Illrequests->search->delete; >+ >+ my $body = { >+ backend => 'Mock', >+ borrowernumber => $patron->borrowernumber, >+ branchcode => $library->branchcode, >+ metadata => { >+ article_author => "Jessop, E. G.", >+ article_title => "Sleep", >+ issn => "0957-4832", >+ issue => "2", >+ pages => "89-90", >+ publisher => "OXFORD UNIVERSITY PRESS", >+ title => "Journal of public health medicine.", >+ year => "2001" >+ } >+ }; >+ >+ ## Authorized user test >+ $t->post_ok( "//$userid:$password@/api/v1/illrequests" => json => $body) >+ ->status_is(201); >+ >+ $schema->storage->txn_rollback; >+}; >+ > sub add_formatted { > my $req = shift; > my @format_dates = ( 'placed', 'updated', 'completed' ); >-- >2.30.2
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 30719
:
144281
|
144285
|
144286
|
144287
|
144288
|
144289
|
144290
|
144291
|
144292
|
144293
|
144294
|
144295
|
144296
|
144297
|
144298
|
144299
|
144300
|
144301
|
144302
|
145760
|
145869
|
145870
|
145876
|
145934
|
146152
|
147658
|
147929
|
147930
|
147931
|
147932
|
147933
|
147934
|
147952
|
147953
|
147954
|
147955
|
147956
|
151152
|
151153
|
151154
|
151155
|
151156
|
151284
|
151285
|
151286
|
151287
|
151288
|
151303
|
151304
|
151305
|
151306
|
151307
|
151308
|
151309
|
151310
|
151311
|
151312
|
151513
|
151514
|
151515
|
151516
|
151517
|
151519
|
151520
|
151521
|
151522
|
151523
|
151571
|
151572
|
151573
|
151574
|
151575
|
151611
|
151612
|
151613
|
151614
|
151615
|
151804
|
151805
|
151806
|
151807
|
151808
|
151809
|
154046
|
154047
|
154048
|
154049
|
154050
|
154051
|
154052
|
155421
|
155422
|
155423
|
155424
|
155425
|
155426
|
155427
|
155428
|
155429
|
155430
|
155431
|
155432
|
155433
|
155434
|
155435
|
156556
|
156748
|
156749
|
156752
|
156783
|
156785
|
156811
|
156812
|
156827
|
156828