1 package OpenILS::WWW::EGCatLoader;
2 use strict; use warnings;
5 use Digest::MD5 qw(md5_hex);
6 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
7 use OpenSRF::AppSession;
8 use OpenSRF::EX qw/:try/;
9 use OpenSRF::Utils qw/:datetime/;
10 use OpenSRF::Utils::JSON;
11 use OpenSRF::Utils::Logger qw/$logger/;
12 use OpenILS::Application::AppUtils;
13 use OpenILS::Utils::CStoreEditor qw/:funcs/;
14 use OpenILS::Utils::Fieldmapper;
15 use DateTime::Format::ISO8601;
16 my $U = 'OpenILS::Application::AppUtils';
18 my %cache; # proc-level cache
21 my($class, $apache, $ctx) = @_;
23 my $self = bless({}, ref($class) || $class);
25 $self->apache($apache);
29 OpenILS::Utils::CStoreEditor->init; # just in case
30 $self->editor(new_editor());
36 # current Apache2::RequestRec;
38 my($self, $apache) = @_;
39 $self->{apache} = $apache if $apache;
40 return $self->{apache};
43 # runtime / template context
46 $self->{ctx} = $ctx if $ctx;
52 my($self, $editor) = @_;
53 $self->{editor} = $editor if $editor;
54 return $self->{editor};
60 $self->{cgi} = $cgi if $cgi;
65 # load common data, then load page data
70 my $stat = $self->load_common;
71 return $stat unless $stat == Apache2::Const::OK;
73 my $path = $self->apache->path_info;
75 return $self->load_home if $path =~ /opac\/home/;
76 return $self->load_login if $path =~ /opac\/login/;
77 return $self->load_logout if $path =~ /opac\/logout/;
78 return $self->load_rresults if $path =~ /opac\/results/;
79 return $self->load_record if $path =~ /opac\/record/;
81 # ----------------------------------------------------------------
82 # These pages require authentication
83 # ----------------------------------------------------------------
84 return Apache2::Const::FORBIDDEN unless $self->cgi->https;
85 return $self->load_logout unless $self->editor->requestor;
87 return $self->load_place_hold if $path =~ /opac\/place_hold/;
88 return $self->load_myopac_holds if $path =~ /opac\/myopac\/holds/;
89 return $self->load_myopac_circs if $path =~ /opac\/myopac\/circs/;
90 return $self->load_myopac_fines if $path =~ /opac\/myopac\/fines/;
91 return $self->load_myopac if $path =~ /opac\/myopac/;
92 # ----------------------------------------------------------------
94 return Apache2::Const::OK;
97 # general purpose utility functions added to the environment
100 my $e = $self->editor;
101 my $ctx = $self->ctx;
103 $cache{map} = {}; # public object maps
104 $cache{list} = {}; # public object lists
106 # fetch-on-demand-and-cache subs for commonly used public data
107 my @public_classes = qw/ccs aout cifm citm clm/;
109 for my $hint (@public_classes) {
112 $Fieldmapper::fieldmap->{$_}->{hint} eq $hint
113 } keys %{ $Fieldmapper::fieldmap };
115 my $ident_field = $Fieldmapper::fieldmap->{$class}->{identity};
117 $class =~ s/Fieldmapper:://o;
121 my $list_key = $hint . '_list';
122 my $find_key = "find_$hint";
124 $ctx->{$list_key} = sub {
125 my $method = "retrieve_all_$class";
126 $cache{list}{$hint} = $e->$method() unless $cache{list}{$hint};
127 return $cache{list}{$hint};
130 $cache{map}{$hint} = {};
132 $ctx->{$find_key} = sub {
134 return $cache{map}{$hint}{$id} if $cache{map}{$hint}{$id};
135 ($cache{map}{$hint}{$id}) = grep { $_->$ident_field eq $id } @{$ctx->{$list_key}->()};
136 return $cache{map}{$hint}{$id};
141 $ctx->{aou_tree} = sub {
143 # fetch the org unit tree
144 unless($cache{aou_tree}) {
145 my $tree = $e->search_actor_org_unit([
146 { parent_ou => undef},
148 flesh_fields => {aou => ['children']},
149 order_by => {aou => 'name'}
153 # flesh the org unit type for each org unit
154 # and simultaneously set the id => aou map cache
158 $node->ou_type( $ctx->{find_aout}->($node->ou_type) );
159 $cache{map}{aou}{$node->id} = $node;
160 flesh_aout($_, $ctx) foreach @{$node->children};
162 flesh_aout($tree, $ctx);
164 $cache{aou_tree} = $tree;
167 return $cache{aou_tree};
170 # Add a special handler for the tree-shaped org unit cache
171 $cache{map}{aou} = {};
172 $ctx->{find_aou} = sub {
174 $ctx->{aou_tree}->(); # force the org tree to load
175 return $cache{map}{aou}{$org_id};
178 # turns an ISO date into something TT can understand
179 $ctx->{parse_datetime} = sub {
181 $date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date));
183 "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
197 # user_status : hash of user circ numbers
201 my $e = $self->editor;
202 my $ctx = $self->ctx;
204 if($e->authtoken($self->cgi->cookie('ses'))) {
208 $ctx->{authtoken} = $e->authtoken;
209 $ctx->{user} = $e->requestor;
210 $ctx->{user_stats} = $U->simplereq(
212 'open-ils.actor.user.opac.vital_stats',
213 $e->authtoken, $e->requestor->id);
217 return $self->load_logout;
221 return Apache2::Const::OK;
226 $self->ctx->{page} = 'home';
227 return Apache2::Const::OK;
233 my $cgi = $self->cgi;
235 $self->ctx->{page} = 'login';
237 my $username = $cgi->param('username');
238 my $password = $cgi->param('password');
240 return Apache2::Const::OK unless $username and $password;
242 my $seed = $U->simplereq(
244 'open-ils.auth.authenticate.init',
247 my $response = $U->simplereq(
249 'open-ils.auth.authenticate.complete',
250 { username => $username,
251 password => md5_hex($seed . md5_hex($password)),
256 # XXX check event, redirect as necessary
258 my $home = $self->apache->unparsed_uri;
259 $home =~ s/\/login/\/home/;
261 $self->apache->print(
263 -url => $cgi->param('origin') || $home,
264 -cookie => $cgi->cookie(
268 -value => $response->{payload}->{authtoken},
269 -expires => CORE::time + $response->{payload}->{authtime}
274 return Apache2::Const::REDIRECT;
280 my $url = 'http://' . $self->apache->hostname . $self->ctx->{base_path} . "/opac/home";
282 $self->apache->print(
283 $self->cgi->redirect(
285 -cookie => $self->cgi->cookie(
294 return Apache2::Const::REDIRECT;
300 # records : list of bre's and copy-count objects
303 my $cgi = $self->cgi;
304 my $ctx = $self->ctx;
305 my $e = $self->editor;
307 $ctx->{page} = 'rresult';
308 my $page = $cgi->param('page') || 0;
309 my $facet = $cgi->param('facet');
310 my $query = $cgi->param('query');
311 my $limit = $cgi->param('limit') || 10; # XXX user settings
312 my $args = {limit => $limit, offset => $page * $limit};
313 $query = "$query $facet" if $facet;
317 $results = $U->simplereq(
319 'open-ils.search.biblio.multiclass.query.staff',
324 $logger->error("multiclass search error: $err");
325 $results = {count => 0, ids => []};
328 my $rec_ids = [map { $_->[0] } @{$results->{ids}}];
330 $ctx->{records} = [];
331 $ctx->{search_facets} = {};
332 $ctx->{page_size} = $limit;
333 $ctx->{hit_count} = $results->{count};
335 return Apache2::Const::OK if @$rec_ids == 0;
337 my $cstore1 = OpenSRF::AppSession->create('open-ils.cstore');
338 my $bre_req = $cstore1->request(
339 'open-ils.cstore.direct.biblio.record_entry.search', {id => $rec_ids});
341 my $search = OpenSRF::AppSession->create('open-ils.search');
342 my $facet_req = $search->request('open-ils.search.facet_cache.retrieve', $results->{facet_key}, 10);
344 unless($cache{cmf}) {
345 $cache{cmf} = $e->search_config_metabib_field({id => {'!=' => undef}});
346 $ctx->{metabib_field} = $cache{cmf};
347 #$cache{cmc} = $e->search_config_metabib_class({name => {'!=' => undef}});
348 #$ctx->{metabib_class} = $cache{cmc};
352 while(my $resp = $bre_req->recv) {
353 my $bre = $resp->content;
355 # XXX farm out to multiple cstore sessions before loop, then collect after
356 my $copy_counts = $e->json_query(
357 {from => ['asset.record_copy_count', 1, $bre->id, 0]})->[0];
362 marc_xml => XML::LibXML->new->parse_string($bre->marc),
363 copy_counts => $copy_counts
370 # shove recs into context in search results order
371 for my $rec_id (@$rec_ids) {
374 grep { $_->{bre}->id == $rec_id } @data
378 my $facets = $facet_req->gather(1);
380 for my $cmf_id (keys %$facets) { # quick-n-dirty
381 my ($cmf) = grep { $_->id eq $cmf_id } @{$cache{cmf}};
382 $facets->{$cmf_id} = {cmf => $cmf, data => $facets->{$cmf_id}};
384 $ctx->{search_facets} = $facets;
386 return Apache2::Const::OK;
390 # record : bre object
393 $self->ctx->{page} = 'record';
395 my $rec_id = $self->ctx->{page_args}->[0]
396 or return Apache2::Const::HTTP_BAD_REQUEST;
398 $self->ctx->{record} = $self->editor->retrieve_biblio_record_entry([
403 bre => ['call_numbers'],
404 acn => ['copies'] # limit, paging, etc.
409 $self->ctx->{marc_xml} = XML::LibXML->new->parse_string($self->ctx->{record}->marc);
411 return Apache2::Const::OK;
415 # user : au object, fleshed
418 $self->ctx->{page} = 'myopac';
420 $self->ctx->{user} = $self->editor->retrieve_actor_user([
421 $self->ctx->{user}->id,
431 return Apache2::Const::OK;
435 sub fetch_user_holds {
437 my $hold_ids = shift;
438 my $ids_only = shift;
443 my $e = $self->editor;
445 my $circ = OpenSRF::AppSession->create('open-ils.circ');
449 $hold_ids = $circ->request(
450 'open-ils.circ.holds.id_list.retrieve.authoritative',
455 $hold_ids = [ grep { defined $_ } @$hold_ids[$offset..($offset + $limit - 1)] ] if $limit or $offset;
459 return $hold_ids if $ids_only or @$hold_ids == 0;
461 my $req = $circ->request(
462 # TODO .authoritative version is chewing up cstores
463 # 'open-ils.circ.hold.details.batch.retrieve.authoritative',
464 'open-ils.circ.hold.details.batch.retrieve',
468 suppress_notices => 1,
469 suppress_transits => 1,
471 suppress_patron_details => 1,
477 while(my $resp = $req->recv) {
478 my $hold = $resp->content;
481 marc_xml => ($flesh) ? XML::LibXML->new->parse_string($hold->{bre}->marc) : undef
489 sub handle_hold_update {
492 my $e = $self->editor;
495 my @hold_ids = $self->cgi->param('hold_id'); # for non-_all actions
496 @hold_ids = @{$self->fetch_user_holds(undef, 1)} if $action =~ /_all/;
498 my $circ = OpenSRF::AppSession->create('open-ils.circ');
500 if($action =~ /cancel/) {
502 for my $hold_id (@hold_ids) {
503 my $resp = $circ->request(
504 'open-ils.circ.hold.cancel', $e->authtoken, $hold_id, 6 )->gather(1); # 6 == patron-cancelled-via-opac
510 for my $hold_id (@hold_ids) {
511 my $vals = {id => $hold_id};
513 if($action =~ /activate/) {
514 $vals->{frozen} = 'f';
515 $vals->{thaw_date} = undef;
517 } elsif($action =~ /suspend/) {
518 $vals->{frozen} = 't';
519 # $vals->{thaw_date} = TODO;
521 push(@$vlist, $vals);
524 $circ->request('open-ils.circ.hold.update.batch.atomic', $e->authtoken, undef, $vlist)->gather(1);
531 sub load_myopac_holds {
533 my $e = $self->editor;
534 my $ctx = $self->ctx;
537 my $limit = $self->cgi->param('limit') || 0;
538 my $offset = $self->cgi->param('offset') || 0;
539 my $action = $self->cgi->param('action') || '';
541 $self->handle_hold_update($action) if $action;
543 $ctx->{holds} = $self->fetch_user_holds(undef, 0, 1, $limit, $offset);
545 return Apache2::Const::OK;
548 sub load_place_hold {
550 my $ctx = $self->ctx;
551 my $e = $self->editor;
552 my $cgi = $self->cgi;
553 $self->ctx->{page} = 'place_hold';
555 $ctx->{hold_target} = $cgi->param('hold_target');
556 $ctx->{hold_type} = $cgi->param('hold_type');
557 $ctx->{default_pickup_lib} = $e->requestor->home_ou; # XXX staff
559 if($ctx->{hold_type} eq 'T') {
560 $ctx->{record} = $e->retrieve_biblio_record_entry($ctx->{hold_target});
564 $ctx->{marc_xml} = XML::LibXML->new->parse_string($ctx->{record}->marc);
566 if(my $pickup_lib = $cgi->param('pickup_lib')) {
569 patronid => $e->requestor->id,
570 titleid => $ctx->{hold_target}, # XXX
571 pickup_lib => $pickup_lib,
575 my $allowed = $U->simplereq(
577 'open-ils.circ.title_hold.is_possible',
581 if($allowed->{success} == 1) {
582 my $hold = Fieldmapper::action::hold_request->new;
584 $hold->pickup_lib($pickup_lib);
585 $hold->requestor($e->requestor->id);
586 $hold->usr($e->requestor->id); # XXX staff
587 $hold->target($ctx->{hold_target});
588 $hold->hold_type($ctx->{hold_type});
589 # frozen, expired, etc..
591 my $stat = $U->simplereq(
593 'open-ils.circ.holds.create',
597 if($stat and $stat > 0) {
599 # if successful, return the user to the requesting page
600 $self->apache->print($cgi->redirect(-url => $cgi->referer));
601 return Apache2::Const::REDIRECT;
604 $ctx->{hold_failed} = 1; # XXX process the events, etc
609 return Apache2::Const::OK;
613 sub fetch_user_circs {
615 my $flesh = shift; # flesh bib data, etc.
616 my $circ_ids = shift;
620 my $e = $self->editor;
625 @circ_ids = @$circ_ids;
629 my $circ_data = $U->simplereq(
631 'open-ils.actor.user.checked_out',
636 @circ_ids = ( @{$circ_data->{overdue}}, @{$circ_data->{out}} );
638 if($limit or $offset) {
639 @circ_ids = grep { defined $_ } @circ_ids[0..($offset + $limit - 1)];
643 return [] unless @circ_ids;
645 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
650 circ => ['target_copy'],
651 acp => ['call_number'],
657 my $circs = $e->search_action_circulation(
658 [{id => \@circ_ids}, ($flesh) ? $qflesh : {}], {substream => 1});
661 for my $circ (@$circs) {
664 marc_xml => ($flesh and $circ->target_copy->call_number->id != -1) ?
665 XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc) :
666 undef # pre-cat copy, use the dummy title/author instead
671 # make sure the final list is in the correct order
673 for my $id (@circ_ids) {
676 (grep { $_->{circ}->id == $id } @circs)
680 return \@sorted_circs;
684 sub handle_circ_renew {
687 my $ctx = $self->ctx;
689 my @renew_ids = $self->cgi->param('circ');
691 my $circs = $self->fetch_user_circs(0, ($action eq 'renew') ? [@renew_ids] : undef);
693 # TODO: fire off renewal calls in batches to speed things up
695 for my $circ (@$circs) {
697 my $evt = $U->simplereq(
699 'open-ils.circ.renew',
700 $self->editor->authtoken,
702 patron_id => $self->editor->requestor->id,
703 copy_id => $circ->{circ}->target_copy,
708 # TODO return these, then insert them into the circ data
709 # blob that is shoved into the template for each circ
710 # so the template won't have to match them
711 push(@responses, {copy => $circ->{circ}->target_copy, evt => $evt});
718 sub load_myopac_circs {
720 my $e = $self->editor;
721 my $ctx = $self->ctx;
724 my $limit = $self->cgi->param('limit') || 0; # 0 == unlimited
725 my $offset = $self->cgi->param('offset') || 0;
726 my $action = $self->cgi->param('action') || '';
728 # perform the renewal first if necessary
729 my @results = $self->handle_circ_renew($action) if $action =~ /renew/;
731 $ctx->{circs} = $self->fetch_user_circs(1, undef, $limit, $offset);
733 my $success_renewals = 0;
734 my $failed_renewals = 0;
735 for my $data (@{$ctx->{circs}}) {
736 my ($resp) = grep { $_->{copy} == $data->{circ}->target_copy->id } @results;
739 my $evt = ref($resp->{evt}) eq 'ARRAY' ? $resp->{evt}->[0] : $resp->{evt};
740 $data->{renewal_response} = $evt;
741 $success_renewals++ if $evt->{textcode} eq 'SUCCESS';
742 $failed_renewals++ if $evt->{textcode} ne 'SUCCESS';
746 $ctx->{success_renewals} = $success_renewals;
747 $ctx->{failed_renewals} = $failed_renewals;
749 return Apache2::Const::OK;
752 sub load_myopac_fines {
754 my $e = $self->editor;
755 my $ctx = $self->ctx;
764 my $limit = $self->cgi->param('limit') || 0;
765 my $offset = $self->cgi->param('offset') || 0;
767 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
769 # TODO: This should really be a ML call, but the existing calls
770 # return an excessive amount of data and don't offer streaming
772 my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
774 my $req = $cstore->request(
775 'open-ils.cstore.direct.money.open_billable_transaction_summary.search',
777 usr => $e->requestor->id,
778 balance_owed => {'!=' => 0}
783 mobts => ['circulation', 'grocery'],
786 circ => ['target_copy'],
787 acp => ['call_number'],
790 order_by => { mobts => 'xact_start' },
795 while(my $resp = $req->recv) {
796 my $mobts = $resp->content;
797 my $circ = $mobts->circulation;
800 if($mobts->grocery) {
801 my @billings = sort { $a->billing_ts cmp $b->billing_ts } @{$mobts->grocery->billings};
802 $last_billing = pop(@billings);
805 # XXX TODO switch to some money-safe non-fp library for math
806 $ctx->{"fines"}->{$_} += $mobts->$_ for (
807 qw/total_paid total_owed balance_owed/
811 @{$ctx->{"fines"}->{$mobts->grocery ? "grocery" : "circulation"}},
814 last_grocery_billing => $last_billing,
815 marc_xml => ($mobts->xact_type ne 'circulation' or $circ->target_copy->call_number->id == -1) ?
817 XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc),
822 return Apache2::Const::OK;