From 1d0e1817e363def0dff639deacea1356503a1693 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Wed, 27 Jan 2021 11:55:56 -0500 Subject: [PATCH] LP1913458 Bucket Add/Delete Item Operations Batched In the record bucket administration UI, the act of adding or removing items from a bucket are now done in batch to avoid too many parallel requests. These changes include new batch create/delete API calls that can operate on any bucket type, so other UI's can be similarly batched as needed. Signed-off-by: Bill Erickson Signed-off-by: Jeff Davis Signed-off-by: Jane Sandberg --- .../OpenILS/Application/Actor/Container.pm | 115 ++++++++++++++++++ .../ui/default/staff/cat/bucket/record/app.js | 72 ++++++----- 2 files changed, 156 insertions(+), 31 deletions(-) diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm index abeeea2e9b..e468a0b789 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm @@ -396,6 +396,121 @@ sub item_create { return $item->id; } +__PACKAGE__->register_method( + method => 'batch_add_items', + api_name => 'open-ils.actor.container.item.create.batch', + stream => 1, + max_bundle_count => 1, + signature => { + desc => 'Add items to a bucket', + params => [ + {desc => 'Auth token', type => 'string'}, + {desc => q/ + Container class. + Can be "copy", "call_number", "biblio_record_entry", or "user"'/, + type => 'string'}, + {desc => 'Bucket ID', type => 'number'}, + {desc => q/ + Item target identifiers. E.g. for record buckets, + the identifier would be the bib record id/, + type => 'array' + }, + ], + return => { + desc => 'Stream of new item Identifiers', + type => 'number' + } + } +); + +sub batch_add_items { + my ($self, $client, $auth, $bucket_class, $bucket_id, $target_ids) = @_; + + my $e = new_editor(authtoken => $auth, xact => 1); + return $e->die_event unless $e->checkauth; + + my $constructor = "Fieldmapper::container::${bucket_class}_bucket_item"; + my $create = "create_container_${bucket_class}_bucket_item"; + my $retrieve = "retrieve_container_${bucket_class}_bucket"; + my $column = "target_${bucket_class}"; + + my $bucket = $e->$retrieve($bucket_id) or return $e->die_event; + + if ($bucket->owner ne $e->requestor->id) { + return $e->die_event unless $e->allowed('CREATE_CONTAINER_ITEM'); + } + + for my $target_id (@$target_ids) { + + my $item = $constructor->new; + $item->bucket($bucket_id); + $item->$column($target_id); + + return $e->die_event unless $e->$create($item); + $client->respond($target_id); + } + + $e->commit; + return undef; +} + +__PACKAGE__->register_method( + method => 'batch_delete_items', + api_name => 'open-ils.actor.container.item.delete.batch', + stream => 1, + max_bundle_count => 1, + signature => { + desc => 'Remove items from a bucket', + params => [ + {desc => 'Auth token', type => 'string'}, + {desc => q/ + Container class. + Can be "copy", "call_number", "biblio_record_entry", or "user"'/, + type => 'string'}, + {desc => q/ + Item target identifiers. E.g. for record buckets, + the identifier would be the bib record id/, + type => 'array' + } + ], + return => { + desc => 'Stream of new removed target IDs', + type => 'number' + } + } +); + +sub batch_delete_items { + my ($self, $client, $auth, $bucket_class, $bucket_id, $target_ids) = @_; + + my $e = new_editor(authtoken => $auth, xact => 1); + return $e->die_event unless $e->checkauth; + + my $delete = "delete_container_${bucket_class}_bucket_item"; + my $search = "search_container_${bucket_class}_bucket_item"; + my $retrieve = "retrieve_container_${bucket_class}_bucket"; + my $column = "target_${bucket_class}"; + + my $bucket = $e->$retrieve($bucket_id) or return $e->die_event; + + if ($bucket->owner ne $e->requestor->id) { + return $e->die_event unless $e->allowed('DELETE_CONTAINER_ITEM'); + } + + for my $target_id (@$target_ids) { + + my $item = $e->$search({bucket => $bucket_id, $column => $target_id})->[0]; + next unless $item; + + return $e->die_event unless $e->$delete($item); + $client->respond($target_id); + } + + $e->commit; + return undef; +} + + __PACKAGE__->register_method( diff --git a/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js b/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js index cd9b9326d4..0474429f9a 100644 --- a/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js +++ b/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js @@ -284,26 +284,26 @@ function($scope, $location, $q, $timeout, $uibModal, if (recs.length == 0) return; bucketSvc.bucketNeedsRefresh = true; - angular.forEach(recs, - function(rec) { - var item = new egCore.idl.cbrebi(); - item.bucket(bucketSvc.currentBucket.id()); - item.target_biblio_record_entry(rec.id); - egCore.net.request( - 'open-ils.actor', - 'open-ils.actor.container.item.create', - egCore.auth.token(), 'biblio', item - ).then(function(resp) { - - // HACK: add the IDs of the added items so that the size - // of the view list will grow (and update any UI looking at - // the list size). The data stored is inconsistent, but since - // we are forcing a bucket refresh on the next rendering of - // the view pane, the list will be repaired. - bucketSvc.currentBucket.items().push(resp); - }); + var ids = recs.map(function(rec) { return rec.id; }); + + egCore.net.request( + 'open-ils.actor', + 'open-ils.actor.container.item.create.batch', + egCore.auth.token(), 'biblio_record_entry', + bucketSvc.currentBucket.id(), ids + + ).then( + null, // complete + null, // error + function(resp) { + // HACK: add the IDs of the added items so that the size + // of the view list will grow (and update any UI looking at + // the list size). The data stored is inconsistent, but since + // we are forcing a bucket refresh on the next rendering of + // the view pane, the list will be repaired. + bucketSvc.currentBucket.items().push(resp); } - ); + ) } $scope.openCreateBucketDialog = function() { @@ -769,19 +769,29 @@ function($scope, $q , $routeParams, bucketSvc, egCore, $window, } $scope.detachRecords = function(records) { - var promises = []; - angular.forEach(records, function(rec) { - var item = bucketSvc.currentBucket.items().filter( - function(i) { - return (i.target_biblio_record_entry() == rec.id) - } - ); - if (item.length) - promises.push(bucketSvc.detachRecord(item[0].id())); - }); - bucketSvc.bucketNeedsRefresh = true; - return $q.all(promises).then(drawBucket); + + var ids = records.map(function(rec) { return rec.id; }); + + return egCore.net.request( + 'open-ils.actor', + 'open-ils.actor.container.item.delete.batch', + egCore.auth.token(), 'biblio_record_entry', + bucketSvc.currentBucket.id(), ids + + ).then( + null, // complete + null, // error + function(resp) { + // Remove the items as the API responds so the UI can show + // the count of items decreasing. + bucketSvc.currentBucket.items( + bucketSvc.currentBucket.items().filter(function(item) { + return item.target_biblio_record_entry() != resp; + }) + ); + } + ).then(drawBucket); } $scope.moveToPending = function(records) { -- 2.43.2