# 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
$(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 \
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'
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;
--- /dev/null
+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;
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;
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;
--- /dev/null
+#!BINDIR/srfsh
+request open-ils.storage open-ils.storage.carousel.refresh_all