From b191a45d7c3c6ed7556f32def7f218f758b571c1 Mon Sep 17 00:00:00 2001 From: Galen Charlton Date: Wed, 12 Jun 2019 17:58:21 -0400 Subject: [PATCH] LP#1832897: business logic for carousels This patch adds various methods in open-ils.actor and open-ils.storage to manipulate carousels. It also adds a server-side script, refresh_carousels.srfsh, and an example crontab entry. The new methods are: * open-ils.actor.carousel.retrieve_by_org * open-ils.actor.carousel.retrieve_manual_by_staff * open-ils.actor.carousel.refresh * open-ils.actor.carousel.create.from_bucket * open-ils.storage.container.refresh_from_carousel * open-ils.storage.carousel.refresh_all Signed-off-by: Galen Charlton Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg --- Open-ILS/examples/crontab.example | 3 + Open-ILS/src/Makefile.am | 2 + .../perlmods/lib/OpenILS/Application/Actor.pm | 1 + .../lib/OpenILS/Application/Actor/Carousel.pm | 215 +++++++++++++++++ .../Application/Storage/CDBI/container.pm | 2 +- .../Storage/Publisher/container.pm | 222 ++++++++++++++++++ .../support-scripts/refresh_carousels.srfsh | 2 + 7 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Carousel.pm create mode 100644 Open-ILS/src/support-scripts/refresh_carousels.srfsh diff --git a/Open-ILS/examples/crontab.example b/Open-ILS/examples/crontab.example index 35144cd871..30f62a5916 100644 --- a/Open-ILS/examples/crontab.example +++ b/Open-ILS/examples/crontab.example @@ -61,6 +61,9 @@ EG_BIN_DIR = /openils/bin # Run the hard due date updater 2 3 * * * . ~/.bashrc && $EG_BIN_DIR/update_hard_due_dates.srfsh +# Run the carousel updater +5 3 * * * . ~/.bashrc && $EG_BIN_DIR/refresh_carousels.srfsh + # Run the credit card number clearing script #5 4 * * * . ~/.bashrc && $EG_BIN_DIR/clear_cc_number.srfsh diff --git a/Open-ILS/src/Makefile.am b/Open-ILS/src/Makefile.am index 9420028df4..7f7954e1ca 100644 --- a/Open-ILS/src/Makefile.am +++ b/Open-ILS/src/Makefile.am @@ -65,6 +65,7 @@ core_scripts = $(examples)/oils_ctl.sh \ $(supportscr)/reshelving_complete.srfsh \ $(supportscr)/clear_expired_circ_history.srfsh \ $(supportscr)/update_hard_due_dates.srfsh \ + $(supportscr)/refresh_carousels.srfsh \ $(supportscr)/juv_to_adult.srfsh \ $(supportscr)/thaw_expired_frozen_holds.srfsh \ $(supportscr)/long-overdue-status-update.pl \ @@ -276,6 +277,7 @@ ilscore-install: sed -i 's|BINDIR|@bindir@|g' '$(DESTDIR)@bindir@/reshelving_complete.srfsh' sed -i 's|BINDIR|@bindir@|g' '$(DESTDIR)@bindir@/clear_expired_circ_history.srfsh' sed -i 's|BINDIR|@bindir@|g' '$(DESTDIR)@bindir@/update_hard_due_dates.srfsh' + sed -i 's|BINDIR|@bindir@|g' '$(DESTDIR)@bindir@/refresh_carousels.srfsh' sed -i 's|BINDIR|@bindir@|g' '$(DESTDIR)@bindir@/juv_to_adult.srfsh' sed -i 's|BINDIR|@bindir@|g' '$(DESTDIR)@bindir@/long-overdue-status-update.pl' sed -i 's|SYSCONFDIR|@sysconfdir@|g' '$(DESTDIR)@bindir@/long-overdue-status-update.pl' diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm index 219f611b6b..d04160fe73 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm @@ -26,6 +26,7 @@ use DateTime; use DateTime::Format::ISO8601; use OpenILS::Const qw/:const/; +use OpenILS::Application::Actor::Carousel; use OpenILS::Application::Actor::Container; use OpenILS::Application::Actor::ClosedDates; use OpenILS::Application::Actor::UserGroups; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Carousel.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Carousel.pm new file mode 100644 index 0000000000..0b64841d4d --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Carousel.pm @@ -0,0 +1,215 @@ +package OpenILS::Application::Actor::Carousel; +use base 'OpenILS::Application'; +use strict; use warnings; +use OpenILS::Application::AppUtils; +use OpenILS::Perm; +use Data::Dumper; +use OpenSRF::EX qw(:try); +use OpenILS::Utils::Fieldmapper; +use OpenILS::Utils::CStoreEditor qw/:funcs/; +use OpenSRF::Utils::SettingsClient; +use OpenSRF::Utils::Cache; +use Digest::MD5 qw(md5_hex); +use OpenSRF::Utils::JSON; + +my $apputils = "OpenILS::Application::AppUtils"; +my $U = $apputils; +my $logger = "OpenSRF::Utils::Logger"; + +sub initialize { return 1; } + +__PACKAGE__->register_method( + method => "retrieve_carousels_at_org", + api_name => "open-ils.actor.carousel.retrieve_by_org", + authoritative => 1, + notes => <<" NOTES"); + Retrieves the IDs and override names of all carousels visible + at the specified org unit sorted by their sequence number at + that library + PARAMS(OrgId) + NOTES + +sub retrieve_carousels_at_org { + my($self, $client, $org_id) = @_; + my $e = new_editor(); + + my $carousels = $e->json_query({ + select => { ccou => ['carousel','override_name','seq'] }, + distinct => 'true', + from => { ccou => 'cc' } , + where => { + '+ccou' => { org_unit => $org_id }, + '+cc' => { active => 't' } + }, + order_by => { + 'ccou' => ['seq'] + } + }); + + return $carousels; +} + +__PACKAGE__->register_method( + method => "retrieve_manual_carousels_for_staff", + api_name => "open-ils.actor.carousel.retrieve_manual_by_staff", + authoritative => 1, + notes => <<" NOTES"); + Retrieves the IDs, buckets, and names of all manually-maintained + carousels visible at any of the staff members working + locations. + PARAMS(authtoken) + NOTES + +sub retrieve_manual_carousels_for_staff { + my($self, $client, $auth) = @_; + my $e = new_editor(authtoken => $auth); + return $e->die_event unless $e->checkauth; + + my $orgs = []; + if ($e->requestor->super_user eq 't') { + # super users can act/see at all OUs + my $ous = $e->json_query({ + select => { aou => ['id'] }, + from => 'aou' + }); + $orgs = [ map { $_->{id} } @$ous ]; + } else { + my $ous = $e->json_query({ + select => { puwoum => ['work_ou'] }, + from => 'puwoum', + where => { + '+puwoum' => { usr => $e->requestor->id } + } + }); + $orgs = [ map { $_->{work_ou} } @$ous ]; + } + + my $carousels = $e->json_query({ + select => { cc => ['id','name','bucket'] }, + distinct => 'true', + from => { cc => 'ccou' }, + where => { + '+ccou' => { org_unit => $orgs }, + '+cc' => { type => 1, active => 't' }, # FIXME + }, + order_by => { + 'cc' => ['name'] + } + }); + + return $carousels; +} + +__PACKAGE__->register_method( + method => "refresh_carousel", + api_name => "open-ils.actor.carousel.refresh", + authoritative => 1, + notes => <<" NOTES"); + Refreshes the specified carousel + PARAMS(authtoken, carousel_id) + NOTES + +sub refresh_carousel { + my ($self, $client, $auth, $carousel_id) = @_; + + my $e = new_editor(authtoken => $auth); + return $e->event unless $e->checkauth; + return $e->event unless $e->allowed('REFRESH_CAROUSEL'); + + my $carousel; + $carousel = $e->retrieve_container_carousel($carousel_id) or return $e->event; + + return $e->event unless $e->allowed('REFRESH_CAROUSEL', $carousel->owner, $carousel); + + my $ctype; + $ctype = $e->retrieve_config_carousel_type($carousel->type) or return $e->event; + return new OpenILS::Event('CANNOT_REFRESH_MANUAL_CAROUSEL') unless $ctype->automatic eq 't'; + + my $orgs = []; + my $locs = []; + if (defined($carousel->owning_lib_filter)) { + my $ou_filter = $carousel->owning_lib_filter; + $ou_filter =~ s/[{}]//g; + @$orgs = split /,/, $ou_filter; + } + if (defined($carousel->copy_location_filter)) { + my $loc_filter = $carousel->copy_location_filter; + $loc_filter =~ s/[{}]//g; + @$locs = split /,/, $loc_filter; + } + + my $num_updated = $U->simplereq( + 'open-ils.storage', + 'open-ils.storage.container.refresh_from_carousel', + $carousel->bucket, + $carousel->type, + $carousel->age_filter, + $orgs, + $locs, + $carousel->max_items, + ); + + $carousel->last_refresh_time('now'); + $e->xact_begin; + $e->update_container_carousel($carousel) or return $e->event; + $e->xact_commit or return $e->event; + + return $num_updated; +} + +__PACKAGE__->register_method( + method => "add_carousel_from_bucket", + api_name => "open-ils.actor.carousel.create.from_bucket", + authoritative => 1, + notes => <<" NOTES"); + Creates new carousel and its container by copying the + contents of an existing bucket. + PARAMS(authtoken, carousel_name, bucket_id) + NOTES + +sub add_carousel_from_bucket { + my ($self, $client, $auth, $carousel_name, $bucket_id) = @_; + + my $e = new_editor(authtoken => $auth); + return $e->event unless $e->checkauth; + return $e->event unless $e->allowed('ADMIN_CAROUSEL'); + + $e->xact_begin; + + my $carousel = Fieldmapper::container::carousel->new; + $carousel->name($carousel_name); + $carousel->type(1); # manual + $carousel->owner($e->requestor->ws_ou); + $carousel->creator($e->requestor->id); + $carousel->editor($e->requestor->id); + $e->create_container_carousel($carousel) or return $e->event; + + # and the bucket + my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new; + $bucket->owner($e->requestor->id); + $bucket->name('System-created bucket for carousel ' . $carousel->id . ' copied from bucket ' . $bucket_id); + $bucket->btype('carousel'); + $bucket->pub('t'); + $bucket->owning_lib($e->requestor->ws_ou); + $e->create_container_biblio_record_entry_bucket($bucket) or return $e->event; + + # link it to the container; + $carousel = $e->retrieve_container_carousel($carousel->id) or return $e->event; + $carousel->bucket($bucket->id); + $e->update_container_carousel($carousel) or return $e->event; + + # and fill it + my $entries = $e->search_container_biblio_record_entry_bucket_item({ bucket => $bucket_id }); + foreach my $entry (@$entries) { + $entry->clear_id; + $entry->bucket($bucket->id); + $entry->create_time('now'); + $e->create_container_biblio_record_entry_bucket_item($entry) or return $e->event; + } + + $e->xact_commit or return $e->event; + + return $carousel->id; +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/container.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/container.pm index c26d7ae5f0..46d20720e1 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/container.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/container.pm @@ -50,7 +50,7 @@ use base qw/container/; container::biblio_record_entry_bucket_item->table( 'container_biblio_record_entry_bucket_item' ); container::biblio_record_entry_bucket_item->columns( Primary => qw/id/ ); -container::biblio_record_entry_bucket_item->columns( Essential => qw/bucket target_biblio_record_entry/ ); +container::biblio_record_entry_bucket_item->columns( Essential => qw/bucket target_biblio_record_entry pos/ ); #------------------------------------------------------------------------------- package container::call_number_bucket; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/container.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/container.pm index fe60ee6336..a06fd1dcd3 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/container.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/container.pm @@ -1,6 +1,228 @@ package OpenILS::Application::Storage::Publisher::container; use base qw/OpenILS::Application::Storage/; +use vars qw/$VERSION/; +use OpenSRF::EX qw/:try/; +use OpenSRF::Utils::Logger qw/:level :logger/; +use OpenILS::Utils::CStoreEditor; #use OpenILS::Application::Storage::CDBI::config; +my $new_items_query = q( + WITH c_attr AS (SELECT c_attrs::query_int AS vis_test FROM asset.patron_default_visibility_mask() x) + SELECT acn.record AS bib + FROM asset.call_number acn + JOIN asset.copy acp ON (acp.call_number = acn.id) + JOIN asset.copy_location acpl ON (acp.location = acpl.id) + JOIN config.copy_status ccs ON (acp.status = ccs.id) + , c_attr + WHERE acn.owning_lib IN (ORG_LIST) + AND acp.circ_lib IN (ORG_LIST) + AND acp.holdable + AND acp.circulate + AND ccs.holdable + AND acpl.holdable + AND acpl.circulate + AND acp.active_date > NOW() - ?::INTERVAL + AND (EXISTS (SELECT 1 FROM asset.copy_vis_attr_cache WHERE record = acn.record AND vis_attr_vector @@ c_attr.vis_test)) + AND (NOT EXISTS (SELECT 1 FROM metabib.record_attr_vector_list WHERE source = acn.record AND vlist @@ metabib.compile_composite_attr(' {"1":[{"_val":"s","_attr":"bib_level"}]}')::query_int)) + GROUP BY acn.record + ORDER BY MIN(AGE(acp.active_date)) + LIMIT ? +); +my $recently_returned_query = q( +WITH c_attr AS (SELECT c_attrs::query_int AS vis_test FROM asset.patron_default_visibility_mask() x) + SELECT acn.record AS bib + FROM asset.call_number acn + JOIN asset.copy acp ON (acp.call_number = acn.id) + JOIN asset.copy_location acpl ON (acp.location = acpl.id) + JOIN config.copy_status ccs ON (acp.status = ccs.id) + JOIN action.circulation circ ON (circ.target_copy = acp.id) + , c_attr + WHERE acn.owning_lib IN (ORG_LIST) + AND acp.circ_lib IN (ORG_LIST) + AND acp.holdable + AND acp.circulate + AND ccs.holdable + AND acpl.holdable + AND acpl.circulate + AND circ.checkin_time > NOW() - ?::INTERVAL + AND circ.checkin_time IS NOT NULL + AND (EXISTS (SELECT 1 FROM asset.copy_vis_attr_cache WHERE record = acn.record AND vis_attr_vector @@ c_attr.vis_test)) + AND (NOT EXISTS (SELECT 1 FROM metabib.record_attr_vector_list WHERE source = acn.record AND vlist @@ metabib.compile_composite_attr(' {"1":[{"_val":"s","_attr":"bib_level"}]}')::query_int)) + GROUP BY acn.record + ORDER BY MIN(AGE(circ.checkin_time)) + LIMIT ? +); +my $top_circs_query = q( + WITH c_attr AS (SELECT c_attrs::query_int AS vis_test FROM asset.patron_default_visibility_mask() x) + SELECT acn.record AS bib + FROM asset.call_number acn + JOIN asset.copy acp ON (acp.call_number = acn.id) + JOIN asset.copy_location acpl ON (acp.location = acpl.id) + JOIN config.copy_status ccs ON (acp.status = ccs.id) + JOIN action.circulation circ ON (circ.target_copy = acp.id) + , c_attr + WHERE acn.owning_lib IN (ORG_LIST) + AND acp.circ_lib IN (ORG_LIST) + AND acp.holdable + AND acp.circulate + AND ccs.holdable + AND acpl.holdable + AND acpl.circulate + AND circ.xact_start > NOW() - ?::INTERVAL + AND (EXISTS (SELECT 1 FROM asset.copy_vis_attr_cache WHERE record = acn.record AND vis_attr_vector @@ c_attr.vis_test)) + AND (NOT EXISTS (SELECT 1 FROM metabib.record_attr_vector_list WHERE source = acn.record AND vlist @@ metabib.compile_composite_attr(' {"1":[{"_val":"s","_attr":"bib_level"}]}')::query_int)) + GROUP BY acn.record + ORDER BY COUNT(circ.id) DESC + LIMIT ? +); +my $new_by_loc_query = q( + WITH c_attr AS (SELECT c_attrs::query_int AS vis_test FROM asset.patron_default_visibility_mask() x) + SELECT acn.record AS bib + FROM asset.call_number acn + JOIN asset.copy acp ON (acp.call_number = acn.id) + JOIN asset.copy_location acpl ON (acp.location = acpl.id) + JOIN config.copy_status ccs ON (acp.status = ccs.id) + , c_attr + WHERE acn.owning_lib IN (ORG_LIST) + AND acp.circ_lib IN (ORG_LIST) + AND acp.active_date > NOW() - ?::INTERVAL + AND acp.location IN (LOC_LIST) + AND acp.holdable + AND acp.circulate + AND ccs.holdable + AND acpl.holdable + AND acpl.circulate + AND (EXISTS (SELECT 1 FROM asset.copy_vis_attr_cache WHERE record = acn.record AND vis_attr_vector @@ c_attr.vis_test)) + AND (NOT EXISTS (SELECT 1 FROM metabib.record_attr_vector_list WHERE source = acn.record AND vlist @@ metabib.compile_composite_attr(' {"1":[{"_val":"s","_attr":"bib_level"}]}')::query_int)) + GROUP BY acn.record + ORDER BY MIN(AGE(acp.active_date)) + LIMIT ? +); + +my %TYPE_QUERY_MAP = ( + 2 => $new_items_query, + 3 => $recently_returned_query, + 4 => $top_circs_query, + 5 => $new_by_loc_query, +); + +sub refresh_container_from_carousel_definition { + my $self = shift; + my $client = shift; + my $bucket = shift; + my $carousel_type = shift; + my $age = shift // '15 days'; + my $libs = shift // []; + my $locs = shift // []; + my $limit = shift // 50; + + my $e = OpenILS::Utils::CStoreEditor->new; + my $ctype = $e->retrieve_config_carousel_type($carousel_type) or return $e->die_event; + $e->disconnect; + + unless (exists($TYPE_QUERY_MAP{$carousel_type})) { + $logger->error("Carousel for bucket $bucket is misconfigured; type $carousel_type is not recognized"); + return 0; + } + + my $query = $TYPE_QUERY_MAP{$carousel_type}; + + if ($ctype->filter_by_copy_owning_lib eq 't') { + if (scalar(@$libs) < 1) { + $logger->error("Carousel for bucket $bucket is misconfigured; owning library filter expected but none specified"); + return 0; + } + my $org_placeholders = join(',', map { '?' } @$libs); + $query =~ s/ORG_LIST/$org_placeholders/g; + } else { + $libs = []; # we'll ignore any superflous supplied values + } + + if ($ctype->filter_by_copy_location eq 't') { + if (scalar(@$locs) < 1) { + $logger->error("Carousel for bucket $bucket is misconfigured; copy location filter expected but none specified"); + return 0; + } + my $loc_placeholders = join(',', map { '?' } @$locs); + $query =~ s/LOC_LIST/$loc_placeholders/g; + } else { + $locs = []; # we'll ignore any superflous supplied values + } + + my $sth = container::biblio_record_entry_bucket_item->db_Main->prepare_cached($query); + + $sth->execute(@$libs, @$libs, $age, @$locs, $limit); + my @bibs = (); + while (my $row = $sth->fetchrow_hashref ) { + push @bibs, $row->{bib}; + } + container::biblio_record_entry_bucket_item->search( bucket => $bucket )->delete_all; + my $i = 0; + foreach my $bib (@bibs) { + container::biblio_record_entry_bucket_item->create({ bucket => $bucket, target_biblio_record_entry => $bib, pos => $i++ }); + } + return scalar(@bibs); +} + +__PACKAGE__->register_method( + api_name => 'open-ils.storage.container.refresh_from_carousel', + method => 'refresh_container_from_carousel_definition', + api_level => 1, + cachable => 1, +); + +sub refresh_all_carousels { + my $self = shift; + my $client = shift; + + my $e = OpenILS::Utils::CStoreEditor->new; + + my $automatic_types = $e->search_config_carousel_type({ automatic => 't' }); + my $carousels = $e->search_container_carousel({ type => [ map { $_->id } @$automatic_types ], active => 't' }); + + my $meth = $self->method_lookup('open-ils.storage.container.refresh_from_carousel'); + + foreach my $carousel (@$carousels) { + + my $orgs = []; + my $locs = []; + if (defined($carousel->owning_lib_filter)) { + my $ou_filter = $carousel->owning_lib_filter; + $ou_filter =~ s/[{}]//g; + @$orgs = split /,/, $ou_filter; + } + if (defined($carousel->copy_location_filter)) { + my $loc_filter = $carousel->copy_location_filter; + $loc_filter =~ s/[{}]//g; + @$locs = split /,/, $loc_filter; + } + + my @res = $meth->run($carousel->bucket, $carousel->type, $carousel->age_filter, $orgs, $locs, $carousel->max_items); + my $ct = scalar(@res) ? $res[0] : 0; + + $e->xact_begin; + $carousel->last_refresh_time('now'); + $e->update_container_carousel($carousel); + $e->xact_commit; + + $client->respond({ + carousel => $carousel->id, + bucket => $carousel->bucket, + updated => $ct + }); + + } + $e->disconnect; + return undef; +} + +__PACKAGE__->register_method( + api_name => 'open-ils.storage.carousel.refresh_all', + method => 'refresh_all_carousels', + api_level => 1, + stream => 1, + cachable => 1, +); + 1; diff --git a/Open-ILS/src/support-scripts/refresh_carousels.srfsh b/Open-ILS/src/support-scripts/refresh_carousels.srfsh new file mode 100644 index 0000000000..8abc1f2fe7 --- /dev/null +++ b/Open-ILS/src/support-scripts/refresh_carousels.srfsh @@ -0,0 +1,2 @@ +#!BINDIR/srfsh +request open-ils.storage open-ils.storage.carousel.refresh_all -- 2.43.2