1 package OpenILS::WWW::EGCatLoader;
2 use strict; use warnings;
6 use Digest::MD5 qw(md5_hex);
7 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
8 use OpenSRF::AppSession;
9 use OpenSRF::EX qw/:try/;
10 use OpenSRF::Utils qw/:datetime/;
11 use OpenSRF::Utils::JSON;
12 use OpenSRF::Utils::Logger qw/$logger/;
13 use OpenILS::Application::AppUtils;
14 use OpenILS::Utils::CStoreEditor qw/:funcs/;
15 use OpenILS::Utils::Fieldmapper;
16 use DateTime::Format::ISO8601;
17 my $U = 'OpenILS::Application::AppUtils';
19 my %cache; # proc-level cache
21 sub _icon_by_mattype { # XXX This is KCLS specific stuff that needs to be
26 "a" => "media_book.jpg",
27 "b" => "media_magazines.jpg",
28 "c" => "media_printedmusic.jpg",
29 "d" => "media_microform.jpg",
30 "e" => "media_equipment.jpg",
31 "f" => "media_films.jpg",
33 "h" => "media_dvd.jpg",
34 "i" => "media_bookoncassette.jpg",
35 "j" => "media_musiccd.jpg",
36 "k" => "media_musiccassette.jpg",
37 "l" => "media_musicrecord.jpg",
38 "m" => "media_software.jpg",
39 "n" => "media_bookoncd.jpg",
40 "o" => "media_kit.jpg",
41 "p" => "media_newspaper.jpg",
42 "q" => "media_largeprint.jpg",
43 "r" => "media_3dobject.jpg",
44 "s" => "media_slide.jpg",
45 "t" => "media_online.jpg",
46 "u" => "media_eaudio.jpg",
47 "v" => "media_ebooktext.jpg",
48 "w" => "media_eaudio.jpg",
49 "x" => "media_downloadmusic.jpg",
50 "y" => "media_downloadvideo.jpg",
51 "z" => "media_map.jpg",
52 "2" => "media_cassettewithbook.jpg",
53 "5" => "media_cdwithbook.jpg"
56 return $type_map{$mattype};
60 my($class, $apache, $ctx) = @_;
62 my $self = bless({}, ref($class) || $class);
64 $self->apache($apache);
68 OpenILS::Utils::CStoreEditor->init; # just in case
69 $self->editor(new_editor());
75 # current Apache2::RequestRec;
77 my($self, $apache) = @_;
78 $self->{apache} = $apache if $apache;
79 return $self->{apache};
82 # runtime / template context
85 $self->{ctx} = $ctx if $ctx;
91 my($self, $editor) = @_;
92 $self->{editor} = $editor if $editor;
93 return $self->{editor};
99 $self->{cgi} = $cgi if $cgi;
104 # load common data, then load page data
109 my $stat = $self->load_common;
110 return $stat unless $stat == Apache2::Const::OK;
112 my $path = $self->apache->path_info;
114 return $self->load_home if $path =~ /opac\/home/;
115 return $self->load_login if $path =~ /opac\/login/;
116 return $self->load_logout if $path =~ /opac\/logout/;
117 return $self->load_rresults if $path =~ /opac\/results/;
118 return $self->load_record if $path =~ /opac\/record/;
120 # ----------------------------------------------------------------
121 # These pages require authentication
122 # ----------------------------------------------------------------
123 unless($self->cgi->https and $self->editor->requestor) {
124 # If a secure resource is requested insecurely, redirect to the login page
125 my $url = 'https://' . $self->apache->hostname . $self->ctx->{base_path} . "/opac/login";
126 $self->apache->print($self->cgi->redirect(-url => $url));
127 return Apache2::Const::REDIRECT;
130 return $self->load_place_hold if $path =~ /opac\/place_hold/;
131 return $self->load_myopac_holds if $path =~ /opac\/myopac\/holds/;
132 return $self->load_myopac_circs if $path =~ /opac\/myopac\/circs/;
133 return $self->load_myopac_fines if $path =~ /opac\/myopac\/fines/;
134 return $self->load_myopac if $path =~ /opac\/myopac/;
135 # ----------------------------------------------------------------
137 return Apache2::Const::OK;
140 # general purpose utility functions added to the environment
143 my $e = $self->editor;
144 my $ctx = $self->ctx;
146 $cache{map} = {}; # public object maps
147 $cache{list} = {}; # public object lists
149 # fetch-on-demand-and-cache subs for commonly used public data
150 my @public_classes = qw/ccs aout cifm citm clm/;
152 for my $hint (@public_classes) {
155 $Fieldmapper::fieldmap->{$_}->{hint} eq $hint
156 } keys %{ $Fieldmapper::fieldmap };
158 my $ident_field = $Fieldmapper::fieldmap->{$class}->{identity};
160 $class =~ s/Fieldmapper:://o;
164 my $list_key = $hint . '_list';
165 my $find_key = "find_$hint";
167 $ctx->{$list_key} = sub {
168 my $method = "retrieve_all_$class";
169 $cache{list}{$hint} = $e->$method() unless $cache{list}{$hint};
170 return $cache{list}{$hint};
173 $cache{map}{$hint} = {};
175 $ctx->{$find_key} = sub {
177 return $cache{map}{$hint}{$id} if $cache{map}{$hint}{$id};
178 ($cache{map}{$hint}{$id}) = grep { $_->$ident_field eq $id } @{$ctx->{$list_key}->()};
179 return $cache{map}{$hint}{$id};
184 $ctx->{aou_tree} = sub {
186 # fetch the org unit tree
187 unless($cache{aou_tree}) {
188 my $tree = $e->search_actor_org_unit([
189 { parent_ou => undef},
191 flesh_fields => {aou => ['children']},
192 order_by => {aou => 'name'}
196 # flesh the org unit type for each org unit
197 # and simultaneously set the id => aou map cache
201 $node->ou_type( $ctx->{find_aout}->($node->ou_type) );
202 $cache{map}{aou}{$node->id} = $node;
203 flesh_aout($_, $ctx) foreach @{$node->children};
205 flesh_aout($tree, $ctx);
207 $cache{aou_tree} = $tree;
210 return $cache{aou_tree};
213 # Add a special handler for the tree-shaped org unit cache
214 $cache{map}{aou} = {};
215 $ctx->{find_aou} = sub {
217 $ctx->{aou_tree}->(); # force the org tree to load
218 return $cache{map}{aou}{$org_id};
221 # turns an ISO date into something TT can understand
222 $ctx->{parse_datetime} = sub {
224 $date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date));
226 "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
240 # user_status : hash of user circ numbers
244 my $e = $self->editor;
245 my $ctx = $self->ctx;
247 $ctx->{referer} = $self->cgi->referer;
249 if($e->authtoken($self->cgi->cookie('ses'))) {
253 $ctx->{authtoken} = $e->authtoken;
254 $ctx->{user} = $e->requestor;
255 $ctx->{user_stats} = $U->simplereq(
257 'open-ils.actor.user.opac.vital_stats',
258 $e->authtoken, $e->requestor->id);
262 return $self->load_logout;
266 return Apache2::Const::OK;
271 $self->ctx->{page} = 'home';
272 return Apache2::Const::OK;
278 my $cgi = $self->cgi;
280 $self->ctx->{page} = 'login';
282 my $username = $cgi->param('username');
283 my $password = $cgi->param('password');
285 return Apache2::Const::OK unless $username and $password;
287 my $seed = $U->simplereq(
289 'open-ils.auth.authenticate.init',
292 my $response = $U->simplereq(
294 'open-ils.auth.authenticate.complete',
295 { username => $username,
296 password => md5_hex($seed . md5_hex($password)),
301 # XXX check event, redirect as necessary
303 my $home = $self->apache->unparsed_uri;
304 $home =~ s/\/login/\/home/;
306 $self->apache->print(
308 -url => $cgi->param('redirect_to') || $home,
309 -cookie => $cgi->cookie(
313 -value => $response->{payload}->{authtoken},
314 -expires => CORE::time + $response->{payload}->{authtime}
319 return Apache2::Const::REDIRECT;
325 my $url = 'http://' . $self->apache->hostname . $self->ctx->{base_path} . "/opac/home";
327 $self->apache->print(
328 $self->cgi->redirect(
330 -cookie => $self->cgi->cookie(
339 return Apache2::Const::REDIRECT;
345 # records : list of bre's and copy-count objects
348 my $cgi = $self->cgi;
349 my $ctx = $self->ctx;
350 my $e = $self->editor;
352 $ctx->{page} = 'rresult';
353 my $page = $cgi->param('page') || 0;
354 my $facet = $cgi->param('facet');
355 my $query = $cgi->param('query');
356 my $limit = $cgi->param('limit') || 10; # XXX user settings
357 my $args = {limit => $limit, offset => $page * $limit};
358 $query = "$query $facet" if $facet;
362 $results = $U->simplereq(
364 'open-ils.search.biblio.multiclass.query.staff',
369 $logger->error("multiclass search error: $err");
370 $results = {count => 0, ids => []};
373 my $rec_ids = [map { $_->[0] } @{$results->{ids}}];
375 $ctx->{records} = [];
376 $ctx->{search_facets} = {};
377 $ctx->{page_size} = $limit;
378 $ctx->{hit_count} = $results->{count};
380 return Apache2::Const::OK if @$rec_ids == 0;
382 my $cstore1 = OpenSRF::AppSession->create('open-ils.cstore');
383 my $bre_req = $cstore1->request(
384 'open-ils.cstore.direct.biblio.record_entry.search', {id => $rec_ids});
386 my $search = OpenSRF::AppSession->create('open-ils.search');
387 my $facet_req = $search->request('open-ils.search.facet_cache.retrieve', $results->{facet_key}, 10);
389 unless($cache{cmf}) {
390 $cache{cmf} = $e->search_config_metabib_field({id => {'!=' => undef}});
391 $ctx->{metabib_field} = $cache{cmf};
392 #$cache{cmc} = $e->search_config_metabib_class({name => {'!=' => undef}});
393 #$ctx->{metabib_class} = $cache{cmc};
397 while(my $resp = $bre_req->recv) {
398 my $bre = $resp->content;
400 # XXX farm out to multiple cstore sessions before loop, then collect after
401 my $copy_counts = $e->json_query(
402 {from => ['asset.record_copy_count', 1, $bre->id, 0]})->[0];
407 marc_xml => XML::LibXML->new->parse_string($bre->marc),
408 copy_counts => $copy_counts
415 # shove recs into context in search results order
416 for my $rec_id (@$rec_ids) {
419 grep { $_->{bre}->id == $rec_id } @data
423 my $facets = $facet_req->gather(1);
425 for my $cmf_id (keys %$facets) { # quick-n-dirty
426 my ($cmf) = grep { $_->id eq $cmf_id } @{$cache{cmf}};
427 $facets->{$cmf_id} = {cmf => $cmf, data => $facets->{$cmf_id}};
429 $ctx->{search_facets} = $facets;
431 return Apache2::Const::OK;
435 # record : bre object
438 $self->ctx->{page} = 'record';
440 my $rec_id = $self->ctx->{page_args}->[0]
441 or return Apache2::Const::HTTP_BAD_REQUEST;
443 $self->ctx->{record} = $self->editor->retrieve_biblio_record_entry([
448 bre => ['call_numbers'],
449 acn => ['copies'] # limit, paging, etc.
454 $self->ctx->{marc_xml} = XML::LibXML->new->parse_string($self->ctx->{record}->marc);
456 return Apache2::Const::OK;
460 # user : au object, fleshed
463 $self->ctx->{page} = 'myopac';
465 $self->ctx->{user} = $self->editor->retrieve_actor_user([
466 $self->ctx->{user}->id,
476 return Apache2::Const::OK;
480 sub fetch_user_holds {
482 my $hold_ids = shift;
483 my $ids_only = shift;
488 my $e = $self->editor;
490 my $circ = OpenSRF::AppSession->create('open-ils.circ');
494 $hold_ids = $circ->request(
495 'open-ils.circ.holds.id_list.retrieve.authoritative',
500 $hold_ids = [ grep { defined $_ } @$hold_ids[$offset..($offset + $limit - 1)] ] if $limit or $offset;
504 return $hold_ids if $ids_only or @$hold_ids == 0;
507 suppress_notices => 1,
508 suppress_transits => 1,
510 suppress_patron_details => 1,
511 include_bre => $flesh ? 1 : 0
514 # ----------------------------------------------------------------
515 # batch version for testing; initial test show 40% speed
516 # savings on larger sets (>20) of holds.
517 # ----------------------------------------------------------------
520 my $mk_req_batch = sub {
522 my $top_idx = $batch_idx + $batch_size;
523 while($batch_idx < $top_idx) {
524 my $hold_id = $hold_ids->[$batch_idx++];
525 last unless $hold_id;
526 $self->apache->log->warn("fetching hold $hold_id");
527 my $ses = OpenSRF::AppSession->create('open-ils.circ');
528 my $req = $ses->request(
529 'open-ils.circ.hold.details.retrieve',
530 $e->authtoken, $hold_id, $args);
531 push(@ses, {ses => $ses, req => $req});
541 @ses = $mk_req_batch->() if $first;
542 last if $first and not @ses;
544 while(my $blob = pop(@collected)) {
545 $blob->{marc_xml} = XML::LibXML->new->parse_string($blob->{hold}->{bre}->marc) if $flesh;
549 for my $req_data (@ses) {
550 push(@collected, {hold => $req_data->{req}->gather(1)});
551 $self->apache->log->warn("fetched a hold");
552 $req_data->{ses}->kill_me;
554 @ses = $mk_req_batch->();
558 # ----------------------------------------------------------------
561 my $req = $circ->request(
562 # TODO .authoritative version is chewing up cstores
563 # 'open-ils.circ.hold.details.batch.retrieve.authoritative',
564 'open-ils.circ.hold.details.batch.retrieve',
565 $e->authtoken, $hold_ids, $args
569 while(my $resp = $req->recv) {
570 my $hold = $resp->content;
573 marc_xml => ($flesh) ? XML::LibXML->new->parse_string($hold->{bre}->marc) : undef
583 sub handle_hold_update {
586 my $e = $self->editor;
589 my @hold_ids = $self->cgi->param('hold_id'); # for non-_all actions
590 @hold_ids = @{$self->fetch_user_holds(undef, 1)} if $action =~ /_all/;
592 my $circ = OpenSRF::AppSession->create('open-ils.circ');
594 if($action =~ /cancel/) {
596 for my $hold_id (@hold_ids) {
597 my $resp = $circ->request(
598 'open-ils.circ.hold.cancel', $e->authtoken, $hold_id, 6 )->gather(1); # 6 == patron-cancelled-via-opac
604 for my $hold_id (@hold_ids) {
605 my $vals = {id => $hold_id};
607 if($action =~ /activate/) {
608 $vals->{frozen} = 'f';
609 $vals->{thaw_date} = undef;
611 } elsif($action =~ /suspend/) {
612 $vals->{frozen} = 't';
613 # $vals->{thaw_date} = TODO;
615 push(@$vlist, $vals);
618 $circ->request('open-ils.circ.hold.update.batch.atomic', $e->authtoken, undef, $vlist)->gather(1);
625 sub load_myopac_holds {
627 my $e = $self->editor;
628 my $ctx = $self->ctx;
631 my $limit = $self->cgi->param('limit') || 0;
632 my $offset = $self->cgi->param('offset') || 0;
633 my $action = $self->cgi->param('action') || '';
635 $self->handle_hold_update($action) if $action;
637 $ctx->{holds} = $self->fetch_user_holds(undef, 0, 1, $limit, $offset);
639 $ctx->{"icon_by_mattype"} = \&_icon_by_mattype;
641 return Apache2::Const::OK;
644 sub load_place_hold {
646 my $ctx = $self->ctx;
647 my $e = $self->editor;
648 my $cgi = $self->cgi;
649 $self->ctx->{page} = 'place_hold';
651 $ctx->{hold_target} = $cgi->param('hold_target');
652 $ctx->{hold_type} = $cgi->param('hold_type');
653 $ctx->{default_pickup_lib} = $e->requestor->home_ou; # XXX staff
655 if($ctx->{hold_type} eq 'T') {
656 $ctx->{record} = $e->retrieve_biblio_record_entry($ctx->{hold_target});
660 $ctx->{marc_xml} = XML::LibXML->new->parse_string($ctx->{record}->marc);
662 if(my $pickup_lib = $cgi->param('pickup_lib')) {
665 patronid => $e->requestor->id,
666 titleid => $ctx->{hold_target}, # XXX
667 pickup_lib => $pickup_lib,
671 my $allowed = $U->simplereq(
673 'open-ils.circ.title_hold.is_possible',
677 if($allowed->{success} == 1) {
678 my $hold = Fieldmapper::action::hold_request->new;
680 $hold->pickup_lib($pickup_lib);
681 $hold->requestor($e->requestor->id);
682 $hold->usr($e->requestor->id); # XXX staff
683 $hold->target($ctx->{hold_target});
684 $hold->hold_type($ctx->{hold_type});
685 # frozen, expired, etc..
687 my $stat = $U->simplereq(
689 'open-ils.circ.holds.create',
693 if($stat and $stat > 0) {
695 # if successful, return the user to the requesting page
696 $self->apache->log->info("Redirecting back to " . $cgi->param('redirect_to'));
697 $self->apache->print($cgi->redirect(-url => $cgi->param('redirect_to')));
698 return Apache2::Const::REDIRECT;
702 $ctx->{hold_failed} = 1; # XXX process the events, etc
707 $self->apache->log->warn('hold permit result ' . OpenSRF::Utils::JSON->perl2JSON($allowed));
710 return Apache2::Const::OK;
714 sub fetch_user_circs {
716 my $flesh = shift; # flesh bib data, etc.
717 my $circ_ids = shift;
721 my $e = $self->editor;
726 @circ_ids = @$circ_ids;
730 my $circ_data = $U->simplereq(
732 'open-ils.actor.user.checked_out',
737 @circ_ids = ( @{$circ_data->{overdue}}, @{$circ_data->{out}} );
739 if($limit or $offset) {
740 @circ_ids = grep { defined $_ } @circ_ids[0..($offset + $limit - 1)];
744 return [] unless @circ_ids;
746 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
751 circ => ['target_copy'],
752 acp => ['call_number'],
758 my $circs = $e->search_action_circulation(
759 [{id => \@circ_ids}, ($flesh) ? $qflesh : {}], {substream => 1});
762 for my $circ (@$circs) {
765 marc_xml => ($flesh and $circ->target_copy->call_number->id != -1) ?
766 XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc) :
767 undef # pre-cat copy, use the dummy title/author instead
772 # make sure the final list is in the correct order
774 for my $id (@circ_ids) {
777 (grep { $_->{circ}->id == $id } @circs)
781 return \@sorted_circs;
785 sub handle_circ_renew {
788 my $ctx = $self->ctx;
790 my @renew_ids = $self->cgi->param('circ');
792 my $circs = $self->fetch_user_circs(0, ($action eq 'renew') ? [@renew_ids] : undef);
794 # TODO: fire off renewal calls in batches to speed things up
796 for my $circ (@$circs) {
798 my $evt = $U->simplereq(
800 'open-ils.circ.renew',
801 $self->editor->authtoken,
803 patron_id => $self->editor->requestor->id,
804 copy_id => $circ->{circ}->target_copy,
809 # TODO return these, then insert them into the circ data
810 # blob that is shoved into the template for each circ
811 # so the template won't have to match them
812 push(@responses, {copy => $circ->{circ}->target_copy, evt => $evt});
819 sub load_myopac_circs {
821 my $e = $self->editor;
822 my $ctx = $self->ctx;
825 my $limit = $self->cgi->param('limit') || 0; # 0 == unlimited
826 my $offset = $self->cgi->param('offset') || 0;
827 my $action = $self->cgi->param('action') || '';
829 # perform the renewal first if necessary
830 my @results = $self->handle_circ_renew($action) if $action =~ /renew/;
832 $ctx->{circs} = $self->fetch_user_circs(1, undef, $limit, $offset);
834 my $success_renewals = 0;
835 my $failed_renewals = 0;
836 for my $data (@{$ctx->{circs}}) {
837 my ($resp) = grep { $_->{copy} == $data->{circ}->target_copy->id } @results;
840 my $evt = ref($resp->{evt}) eq 'ARRAY' ? $resp->{evt}->[0] : $resp->{evt};
841 $data->{renewal_response} = $evt;
842 $success_renewals++ if $evt->{textcode} eq 'SUCCESS';
843 $failed_renewals++ if $evt->{textcode} ne 'SUCCESS';
847 $ctx->{success_renewals} = $success_renewals;
848 $ctx->{failed_renewals} = $failed_renewals;
850 return Apache2::Const::OK;
853 sub load_myopac_fines {
855 my $e = $self->editor;
856 my $ctx = $self->ctx;
865 my $limit = $self->cgi->param('limit') || 0;
866 my $offset = $self->cgi->param('offset') || 0;
868 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
870 # TODO: This should really be a ML call, but the existing calls
871 # return an excessive amount of data and don't offer streaming
873 my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
875 my $req = $cstore->request(
876 'open-ils.cstore.direct.money.open_billable_transaction_summary.search',
878 usr => $e->requestor->id,
879 balance_owed => {'!=' => 0}
884 mobts => ['circulation', 'grocery'],
887 circ => ['target_copy'],
888 acp => ['call_number'],
891 order_by => { mobts => 'xact_start' },
896 while(my $resp = $req->recv) {
897 my $mobts = $resp->content;
898 my $circ = $mobts->circulation;
901 if($mobts->grocery) {
902 my @billings = sort { $a->billing_ts cmp $b->billing_ts } @{$mobts->grocery->billings};
903 $last_billing = pop(@billings);
906 # XXX TODO switch to some money-safe non-fp library for math
907 $ctx->{"fines"}->{$_} += $mobts->$_ for (
908 qw/total_paid total_owed balance_owed/
912 @{$ctx->{"fines"}->{$mobts->grocery ? "grocery" : "circulation"}},
915 last_grocery_billing => $last_billing,
916 marc_xml => ($mobts->xact_type ne 'circulation' or $circ->target_copy->call_number->id == -1) ?
918 XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc),
923 return Apache2::Const::OK;