1 package OpenILS::WWW::EGCatLoader;
2 use strict; use warnings;
3 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
4 use OpenSRF::Utils::Logger qw/$logger/;
5 use OpenILS::Utils::CStoreEditor qw/:funcs/;
6 use OpenILS::Utils::Fieldmapper;
7 use OpenILS::Application::AppUtils;
9 use OpenSRF::Utils::JSON;
11 $Data::Dumper::Indent = 0;
13 my $U = 'OpenILS::Application::AppUtils';
15 sub prepare_extended_user_info {
19 $self->ctx->{user} = $self->editor->retrieve_actor_user([
20 $self->ctx->{user}->id,
24 au => [qw/card home_ou addresses ident_type billing_address/, @extra_flesh]
28 ]) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
33 # Given an event returned by a failed attempt to create a hold, do we have
34 # permission to override? XXX Should the permission check be scoped to a
35 # given org_unit context?
36 sub test_could_override {
37 my ($self, $event) = @_;
39 return 0 unless $event;
40 return 1 if $self->editor->allowed($event->{textcode} . ".override");
41 return 1 if $event->{"fail_part"} and
42 $self->editor->allowed($event->{"fail_part"} . ".override");
46 # Find out whether we care that local copies are available
47 sub local_avail_concern {
48 my ($self, $hold_target, $hold_type, $pickup_lib) = @_;
50 my $would_block = $self->ctx->{get_org_setting}->
51 ($pickup_lib, "circ.holds.hold_has_copy_at.block");
53 $self->ctx->{get_org_setting}->
54 ($pickup_lib, "circ.holds.hold_has_copy_at.alert") and
55 not $self->cgi->param("override")
56 ) unless $would_block;
58 if ($would_block or $would_alert) {
60 "hold_target" => $hold_target,
61 "hold_type" => $hold_type,
62 "org_unit" => $pickup_lib
64 my $local_avail = $U->simplereq(
66 "open-ils.circ.hold.has_copy_at", $self->editor->authtoken, $args
69 "copy availability information for " . Dumper($args) .
70 " is " . Dumper($local_avail)
72 if (%$local_avail) { # if hash not empty
73 $self->ctx->{hold_copy_available} = $local_avail;
74 return ($would_block, $would_alert);
82 # user : au object, fleshed
83 sub load_myopac_prefs {
86 my $e = $self->editor;
87 my $pending_addr = $cgi->param('pending_addr');
88 my $replace_addr = $cgi->param('replace_addr');
89 my $delete_pending = $cgi->param('delete_pending');
91 $self->prepare_extended_user_info;
92 my $user = $self->ctx->{user};
94 return Apache2::Const::OK unless
95 $pending_addr or $replace_addr or $delete_pending;
97 my @form_fields = qw/address_type street1 street2 city county state country post_code/;
100 if( $pending_addr ) { # update an existing pending address
102 ($paddr) = grep { $_->id == $pending_addr } @{$user->addresses};
103 return Apache2::Const::HTTP_BAD_REQUEST unless $paddr;
104 $paddr->$_( $cgi->param($_) ) for @form_fields;
106 } elsif( $replace_addr ) { # create a new pending address for 'replace_addr'
108 $paddr = Fieldmapper::actor::user_address->new;
110 $paddr->usr($user->id);
111 $paddr->pending('t');
112 $paddr->replaces($replace_addr);
113 $paddr->$_( $cgi->param($_) ) for @form_fields;
115 } elsif( $delete_pending ) {
116 $paddr = $e->retrieve_actor_user_address($delete_pending);
117 return Apache2::Const::HTTP_BAD_REQUEST unless
118 $paddr and $paddr->usr == $user->id and $U->is_true($paddr->pending);
119 $paddr->isdeleted(1);
122 my $resp = $U->simplereq(
124 'open-ils.actor.user.address.pending.cud',
125 $e->authtoken, $paddr);
127 if( $U->event_code($resp) ) {
128 $logger->error("Error updating pending address: $resp");
129 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
132 # in light of these changes, re-fetch latest data
134 $self->prepare_extended_user_info;
137 return Apache2::Const::OK;
140 sub load_myopac_prefs_notify {
142 my $e = $self->editor;
144 my $user_prefs = $self->fetch_optin_prefs;
145 $user_prefs = $self->update_optin_prefs($user_prefs)
146 if $self->cgi->request_method eq 'POST';
148 $self->ctx->{opt_in_settings} = $user_prefs;
150 return Apache2::Const::OK;
153 sub fetch_optin_prefs {
155 my $e = $self->editor;
157 # fetch all of the opt-in settings the user has access to
158 # XXX: user's should in theory have options to opt-in to notices
159 # for remote locations, but that opens the door for a large
160 # set of generally un-used opt-ins.. needs discussion
161 my $opt_ins = $U->simplereq(
163 'open-ils.actor.event_def.opt_in.settings.atomic',
164 $e->authtoken, $e->requestor->home_ou);
166 # some opt-ins are staff-only
167 $opt_ins = [ grep { $U->is_true($_->opac_visible) } @$opt_ins ];
169 # fetch user setting values for each of the opt-in settings
170 my $user_set = $U->simplereq(
172 'open-ils.actor.patron.settings.retrieve',
175 [map {$_->name} @$opt_ins]
178 return [map { {cust => $_, value => $user_set->{$_->name} } } @$opt_ins];
181 sub update_optin_prefs {
183 my $user_prefs = shift;
184 my $e = $self->editor;
185 my @settings = $self->cgi->param('setting');
188 # apply now-true settings
189 for my $applied (@settings) {
190 # see if setting is already applied to this user
191 next if grep { $_->{cust}->name eq $applied and $_->{value} } @$user_prefs;
192 $newsets{$applied} = OpenSRF::Utils::JSON->true;
195 # remove now-false settings
196 for my $pref (grep { $_->{value} } @$user_prefs) {
197 $newsets{$pref->{cust}->name} = undef
198 unless grep { $_ eq $pref->{cust}->name } @settings;
203 'open-ils.actor.patron.settings.update',
204 $e->authtoken, $e->requestor->id, \%newsets);
206 # update the local prefs to match reality
207 for my $pref (@$user_prefs) {
208 $pref->{value} = $newsets{$pref->{cust}->name}
209 if exists $newsets{$pref->{cust}->name};
215 sub _load_user_with_prefs {
217 my $stat = $self->prepare_extended_user_info('settings');
218 return $stat if $stat; # not-OK
220 $self->ctx->{user_setting_map} = {
221 map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) }
222 @{$self->ctx->{user}->settings}
228 sub _get_bookbag_sort_params {
231 # The interface that feeds this cgi parameter will provide a single
232 # argument for a QP sort filter, and potentially a modifier after a period.
233 # In practice this means the "sort" parameter will be something like
234 # "titlesort" or "authorsort.descending".
235 my $sorter = $self->cgi->param("sort") || "";
238 $sorter =~ s/^(.*?)\.(.*)/$1/;
239 $modifier = $2 || undef;
242 return ($sorter, $modifier);
245 sub _prepare_bookbag_container_query {
246 my ($self, $container_id, $sorter, $modifier) = @_;
249 "container(bre,bookbag,%d,%s)%s%s",
250 $container_id, $self->editor->authtoken,
251 ($sorter ? " sort($sorter)" : ""),
252 ($modifier ? "#$modifier" : "")
256 sub load_myopac_prefs_settings {
259 my $stat = $self->_load_user_with_prefs;
260 return $stat if $stat;
262 return Apache2::Const::OK
263 unless $self->cgi->request_method eq 'POST';
265 # some setting values from the form don't match the
266 # required value/format for the db, so they have to be
267 # individually translated.
270 my $set_map = $self->ctx->{user_setting_map};
272 my $key = 'opac.hits_per_page';
273 my $val = $self->cgi->param($key);
274 $settings{$key}= $val unless $$set_map{$key} eq $val;
276 my $now = DateTime->now->strftime('%F');
277 for $key (qw/history.circ.retention_start history.hold.retention_start/) {
278 $val = $self->cgi->param($key);
279 if($val and $val eq 'on') {
280 # Set the start time to 'now' unless a start time already exists for the user
281 $settings{$key} = $now unless $$set_map{$key};
283 # clear the start time if one previously existed for the user
284 $settings{$key} = undef if $$set_map{$key};
288 # Send the modified settings off to be saved
291 'open-ils.actor.patron.settings.update',
292 $self->editor->authtoken, undef, \%settings);
294 # re-fetch user prefs
295 $self->ctx->{updated_user_settings} = \%settings;
296 return $self->_load_user_with_prefs || Apache2::Const::OK;
299 sub fetch_user_holds {
301 my $hold_ids = shift;
302 my $ids_only = shift;
304 my $available = shift;
308 my $e = $self->editor;
311 my $circ = OpenSRF::AppSession->create('open-ils.circ');
313 $hold_ids = $circ->request(
314 'open-ils.circ.holds.id_list.retrieve.authoritative',
320 $hold_ids = [ grep { defined $_ } @$hold_ids[$offset..($offset + $limit - 1)] ] if $limit or $offset;
324 return $hold_ids if $ids_only or @$hold_ids == 0;
327 suppress_notices => 1,
328 suppress_transits => 1,
330 suppress_patron_details => 1,
331 include_bre => $flesh ? 1 : 0
334 # ----------------------------------------------------------------
335 # Collect holds in batches of $batch_size for faster retrieval
339 my $mk_req_batch = sub {
341 my $top_idx = $batch_idx + $batch_size;
342 while($batch_idx < $top_idx) {
343 my $hold_id = $hold_ids->[$batch_idx++];
344 last unless $hold_id;
345 my $ses = OpenSRF::AppSession->create('open-ils.circ');
346 my $req = $ses->request(
347 'open-ils.circ.hold.details.retrieve',
348 $e->authtoken, $hold_id, $args);
349 push(@ses, {ses => $ses, req => $req});
355 my(@collected, @holds, @ses);
358 @ses = $mk_req_batch->() if $first;
359 last if $first and not @ses;
362 # If desired by the caller, filter any holds that are not available.
364 @collected = grep { $_->{hold}->{status} == 4 } @collected;
366 while(my $blob = pop(@collected)) {
367 $blob->{marc_xml} = XML::LibXML->new->parse_string($blob->{hold}->{bre}->marc) if $flesh;
372 for my $req_data (@ses) {
373 push(@collected, {hold => $req_data->{req}->gather(1)});
374 $req_data->{ses}->kill_me;
377 @ses = $mk_req_batch->();
378 last unless @collected or @ses;
382 # put the holds back into the original server sort order
384 for my $id (@$hold_ids) {
385 push @sorted, grep { $_->{hold}->{hold}->id == $id } @holds;
391 sub handle_hold_update {
394 my $hold_ids = shift;
395 my $e = $self->editor;
398 my @hold_ids = ($hold_ids) ? @$hold_ids : $self->cgi->param('hold_id'); # for non-_all actions
399 @hold_ids = @{$self->fetch_user_holds(undef, 1)} if $action =~ /_all/;
401 my $circ = OpenSRF::AppSession->create('open-ils.circ');
403 if($action =~ /cancel/) {
405 for my $hold_id (@hold_ids) {
406 my $resp = $circ->request(
407 'open-ils.circ.hold.cancel', $e->authtoken, $hold_id, 6 )->gather(1); # 6 == patron-cancelled-via-opac
410 } elsif ($action =~ /activate|suspend/) {
413 for my $hold_id (@hold_ids) {
414 my $vals = {id => $hold_id};
416 if($action =~ /activate/) {
417 $vals->{frozen} = 'f';
418 $vals->{thaw_date} = undef;
420 } elsif($action =~ /suspend/) {
421 $vals->{frozen} = 't';
422 # $vals->{thaw_date} = TODO;
424 push(@$vlist, $vals);
427 $circ->request('open-ils.circ.hold.update.batch.atomic', $e->authtoken, undef, $vlist)->gather(1);
428 } elsif ($action eq 'edit') {
431 my $val = {"id" => $_};
432 $val->{"frozen"} = $self->cgi->param("frozen");
433 $val->{"pickup_lib"} = $self->cgi->param("pickup_lib");
435 for my $field (qw/expire_time thaw_date/) {
436 # XXX TODO make this support other date formats, not just
438 next unless $self->cgi->param($field) =~
439 m:^(\d{2})/(\d{2})/(\d{4})$:;
440 $val->{$field} = "$3-$1-$2";
446 'open-ils.circ.hold.update.batch.atomic',
447 $e->authtoken, undef, \@vals
448 )->gather(1); # LFW XXX test for failure
449 $url = 'https://' . $self->apache->hostname . $self->ctx->{opac_root} . '/myopac/holds';
453 return defined($url) ? $self->generic_redirect($url) : undef;
456 sub load_myopac_holds {
458 my $e = $self->editor;
459 my $ctx = $self->ctx;
461 my $limit = $self->cgi->param('limit') || 0;
462 my $offset = $self->cgi->param('offset') || 0;
463 my $action = $self->cgi->param('action') || '';
464 my $hold_id = $self->cgi->param('id');
465 my $available = int($self->cgi->param('available') || 0);
467 my $hold_handle_result;
468 $hold_handle_result = $self->handle_hold_update($action) if $action;
470 $ctx->{holds} = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available, $limit, $offset);
472 return defined($hold_handle_result) ? $hold_handle_result : Apache2::Const::OK;
475 sub load_place_hold {
477 my $ctx = $self->ctx;
478 my $gos = $ctx->{get_org_setting};
479 my $e = $self->editor;
480 my $cgi = $self->cgi;
482 $self->ctx->{page} = 'place_hold';
483 my @targets = $cgi->param('hold_target');
484 $ctx->{hold_type} = $cgi->param('hold_type');
485 $ctx->{default_pickup_lib} = $e->requestor->home_ou; # unless changed below
487 return $self->post_hold_redirect unless @targets;
489 $logger->info("Looking at hold targets: @targets");
491 # if the staff client provides a patron barcode, fetch the patron
492 if (my $bc = $self->cgi->cookie("patron_barcode")) {
493 $ctx->{patron_recipient} = $U->simplereq(
494 "open-ils.actor", "open-ils.actor.user.fleshed.retrieve_by_barcode",
495 $self->editor->authtoken, $bc
496 ) or return Apache2::Const::HTTP_BAD_REQUEST;
498 $ctx->{default_pickup_lib} = $ctx->{patron_recipient}->home_ou;
501 my $request_lib = $e->requestor->ws_ou;
503 $ctx->{hold_data} = \@hold_data;
505 my $type_dispatch = {
507 my $recs = $e->batch_retrieve_biblio_record_entry(\@targets, {substream => 1});
508 for my $id (@targets) { # force back into the correct order
509 my ($rec) = grep {$_->id eq $id} @$recs;
510 push(@hold_data, {target => $rec, record => $rec});
514 my $vols = $e->batch_retrieve_asset_call_number([
517 "flesh_fields" => {"acn" => ["record"]}
519 ], {substream => 1});
521 for my $id (@targets) {
522 my ($vol) = grep {$_->id eq $id} @$vols;
523 push(@hold_data, {target => $vol, record => $vol->record});
527 my $copies = $e->batch_retrieve_asset_copy([
532 "acp" => ["call_number"]
535 ], {substream => 1});
537 for my $id (@targets) {
538 my ($copy) = grep {$_->id eq $id} @$copies;
539 push(@hold_data, {target => $copy, record => $copy->call_number->record});
543 my $isses = $e->batch_retrieve_serial_issuance([
547 "siss" => ["subscription"], "ssub" => ["record_entry"]
550 ], {substream => 1});
552 for my $id (@targets) {
553 my ($iss) = grep {$_->id eq $id} @$isses;
554 push(@hold_data, {target => $iss, record => $iss->subscription->record_entry});
559 }->{$ctx->{hold_type}}->();
561 # caller sent bad target IDs or the wrong hold type
562 return Apache2::Const::HTTP_BAD_REQUEST unless @hold_data;
564 # generate the MARC xml for each record
565 $_->{marc_xml} = XML::LibXML->new->parse_string($_->{record}->marc) for @hold_data;
567 my $pickup_lib = $cgi->param('pickup_lib');
568 # no pickup lib means no holds placement
569 return Apache2::Const::OK unless $pickup_lib;
571 $ctx->{hold_attempt_made} = 1;
573 # Give the original CGI params back to the user in case they
574 # want to try to override something.
575 $ctx->{orig_params} = $cgi->Vars;
576 delete $ctx->{orig_params}{submit};
577 delete $ctx->{orig_params}{hold_target};
579 my $usr = $e->requestor->id;
581 if ($ctx->{is_staff} and !$cgi->param("hold_usr_is_requestor")) {
582 # find the real hold target
584 $usr = $U->simplereq(
586 "open-ils.actor.user.retrieve_id_by_barcode_or_username",
587 $e->authtoken, $cgi->param("hold_usr"));
589 if (defined $U->event_code($usr)) {
590 $ctx->{hold_failed} = 1;
591 $ctx->{hold_failed_event} = $usr;
595 # First see if we should warn/block for any holds that
596 # might have locally available items.
597 for my $hdata (@hold_data) {
598 my ($local_block, $local_alert) = $self->local_avail_concern(
599 $hdata->{target}->id, $ctx->{hold_type}, $pickup_lib);
602 $hdata->{hold_failed} = 1;
603 $hdata->{hold_local_block} = 1;
604 } elsif ($local_alert) {
605 $hdata->{hold_failed} = 1;
606 $hdata->{hold_local_alert} = 1;
611 my $method = 'open-ils.circ.holds.test_and_create.batch';
612 $method .= '.override' if $cgi->param('override');
614 my @create_targets = map {$_->{target}->id} (grep { !$_->{hold_failed} } @hold_data);
616 if(@create_targets) {
618 my $bses = OpenSRF::AppSession->create('open-ils.circ');
619 my $breq = $bses->request(
623 pickup_lib => $pickup_lib,
624 hold_type => $ctx->{hold_type}
629 while (my $resp = $breq->recv) {
631 $resp = $resp->content;
632 $logger->info('batch hold placement result: ' . OpenSRF::Utils::JSON->perl2JSON($resp));
634 if ($U->event_code($resp)) {
635 $ctx->{general_hold_error} = $resp;
639 my ($hdata) = grep {$_->{target}->id eq $resp->{target}} @hold_data;
640 my $result = $resp->{result};
642 if ($U->event_code($result)) {
643 # e.g. permission denied
644 $hdata->{hold_failed} = 1;
645 $hdata->{hold_failed_event} = $result;
649 if(not ref $result and $result > 0) {
650 # successul hold returns the hold ID
652 $hdata->{hold_success} = $result;
655 # hold-specific failure event
656 $hdata->{hold_failed} = 1;
657 $hdata->{hold_failed_event} = $result->{last_event};
658 $hdata->{could_override} = $self->test_could_override($hdata->{hold_failed_event});
666 # stay on the current page and display the results
667 return Apache2::Const::OK if
668 (grep {$_->{hold_failed}} @hold_data) or $ctx->{general_hold_error};
670 # if successful, do some cleanup and return the
671 # user to the requesting page.
673 return $self->post_hold_redirect;
676 sub post_hold_redirect {
679 # XXX: Leave the barcode cookie in place. Otherwise, it's not
680 # possible to place more than one hold for the patron within
681 # a staff/patron session. This does leave the barcode to linger
682 # longer than is ideal, but normal staff work flow will cause the
683 # cookie to be replaced with each new patron anyway.
684 # TODO: See about getting the staff client to clear the cookie
685 return $self->generic_redirect;
687 # We also clear the patron_barcode (from the staff client)
688 # cookie at this point (otherwise it haunts the staff user
689 # later). XXX todo make sure this is best; also see that
690 # template when staff mode calls xulG.opac_hold_placed()
692 return $self->generic_redirect(
695 -name => "patron_barcode",
705 sub fetch_user_circs {
707 my $flesh = shift; # flesh bib data, etc.
708 my $circ_ids = shift;
712 my $e = $self->editor;
717 @circ_ids = @$circ_ids;
721 my $circ_data = $U->simplereq(
723 'open-ils.actor.user.checked_out',
728 @circ_ids = ( @{$circ_data->{overdue}}, @{$circ_data->{out}} );
730 if($limit or $offset) {
731 @circ_ids = grep { defined $_ } @circ_ids[0..($offset + $limit - 1)];
735 return [] unless @circ_ids;
740 circ => ['target_copy'],
741 acp => ['call_number'],
747 my $circs = $e->search_action_circulation(
748 [{id => \@circ_ids}, ($flesh) ? $qflesh : {}], {substream => 1});
751 for my $circ (@$circs) {
754 marc_xml => ($flesh and $circ->target_copy->call_number->id != -1) ?
755 XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc) :
756 undef # pre-cat copy, use the dummy title/author instead
761 # make sure the final list is in the correct order
763 for my $id (@circ_ids) {
766 (grep { $_->{circ}->id == $id } @circs)
770 return \@sorted_circs;
774 sub handle_circ_renew {
777 my $ctx = $self->ctx;
779 my @renew_ids = $self->cgi->param('circ');
781 my $circs = $self->fetch_user_circs(0, ($action eq 'renew') ? [@renew_ids] : undef);
783 # TODO: fire off renewal calls in batches to speed things up
785 for my $circ (@$circs) {
787 my $evt = $U->simplereq(
789 'open-ils.circ.renew',
790 $self->editor->authtoken,
792 patron_id => $self->editor->requestor->id,
793 copy_id => $circ->{circ}->target_copy,
798 # TODO return these, then insert them into the circ data
799 # blob that is shoved into the template for each circ
800 # so the template won't have to match them
801 push(@responses, {copy => $circ->{circ}->target_copy, evt => $evt});
808 sub load_myopac_circs {
810 my $e = $self->editor;
811 my $ctx = $self->ctx;
814 my $limit = $self->cgi->param('limit') || 0; # 0 == unlimited
815 my $offset = $self->cgi->param('offset') || 0;
816 my $action = $self->cgi->param('action') || '';
818 # perform the renewal first if necessary
819 my @results = $self->handle_circ_renew($action) if $action =~ /renew/;
821 $ctx->{circs} = $self->fetch_user_circs(1, undef, $limit, $offset);
823 my $success_renewals = 0;
824 my $failed_renewals = 0;
825 for my $data (@{$ctx->{circs}}) {
826 my ($resp) = grep { $_->{copy} == $data->{circ}->target_copy->id } @results;
829 my $evt = ref($resp->{evt}) eq 'ARRAY' ? $resp->{evt}->[0] : $resp->{evt};
830 $data->{renewal_response} = $evt;
831 $success_renewals++ if $evt->{textcode} eq 'SUCCESS';
832 $failed_renewals++ if $evt->{textcode} ne 'SUCCESS';
836 $ctx->{success_renewals} = $success_renewals;
837 $ctx->{failed_renewals} = $failed_renewals;
839 return Apache2::Const::OK;
842 sub load_myopac_circ_history {
844 my $e = $self->editor;
845 my $ctx = $self->ctx;
846 my $limit = $self->cgi->param('limit') || 15;
847 my $offset = $self->cgi->param('offset') || 0;
849 $ctx->{circ_history_limit} = $limit;
850 $ctx->{circ_history_offset} = $offset;
852 my $circ_ids = $e->json_query({
856 transform => 'action.usr_visible_circs',
861 where => {id => $e->requestor->id},
866 $ctx->{circs} = $self->fetch_user_circs(1, [map { $_->{id} } @$circ_ids]);
867 return Apache2::Const::OK;
870 # TODO: action.usr_visible_holds does not return cancelled holds. Should it?
871 sub load_myopac_hold_history {
873 my $e = $self->editor;
874 my $ctx = $self->ctx;
875 my $limit = $self->cgi->param('limit') || 15;
876 my $offset = $self->cgi->param('offset') || 0;
877 $ctx->{hold_history_limit} = $limit;
878 $ctx->{hold_history_offset} = $offset;
880 my $hold_ids = $e->json_query({
884 transform => 'action.usr_visible_holds',
889 where => {id => $e->requestor->id},
894 $ctx->{holds} = $self->fetch_user_holds([map { $_->{id} } @$hold_ids], 0, 1, 0);
895 return Apache2::Const::OK;
898 sub load_myopac_payment_form {
902 $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact'), $self->cgi->param('xact_misc')]) and return $r;
903 $r = $self->prepare_extended_user_info and return $r;
905 return Apache2::Const::OK;
908 # TODO: add other filter options as params/configs/etc.
909 sub load_myopac_payments {
911 my $limit = $self->cgi->param('limit') || 20;
912 my $offset = $self->cgi->param('offset') || 0;
913 my $e = $self->editor;
915 $self->ctx->{payment_history_limit} = $limit;
916 $self->ctx->{payment_history_offset} = $offset;
919 $args->{limit} = $limit if $limit;
920 $args->{offset} = $offset if $offset;
922 if (my $max_age = $self->ctx->{get_org_setting}->(
923 $e->requestor->home_ou, "opac.payment_history_age_limit"
925 my $min_ts = DateTime->now(
926 "time_zone" => DateTime::TimeZone->new("name" => "local"),
927 )->subtract("seconds" => interval_to_seconds($max_age))->iso8601();
929 $logger->info("XXX min_ts: $min_ts");
930 $args->{"where"} = {"payment_ts" => {">=" => $min_ts}};
933 $self->ctx->{payments} = $U->simplereq(
935 'open-ils.actor.user.payments.retrieve.atomic',
936 $e->authtoken, $e->requestor->id, $args);
938 return Apache2::Const::OK;
941 sub load_myopac_pay {
945 $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact'), $self->cgi->param('xact_misc')]) and
948 # balance_owed is computed specifically from the fines we're trying
949 # to pay in this case.
950 if ($self->ctx->{fines}->{balance_owed} <= 0) {
951 $self->apache->log->info(
952 sprintf("Can't pay non-positive balance. xacts selected: (%s)",
953 join(", ", map(int, $self->cgi->param("xact"), $self->cgi->param('xact_misc'))))
955 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
958 my $cc_args = {"where_process" => 1};
960 $cc_args->{$_} = $self->cgi->param($_) for (qw/
961 number cvv2 expire_year expire_month billing_first
962 billing_last billing_address billing_city billing_state
967 "cc_args" => $cc_args,
968 "userid" => $self->ctx->{user}->id,
969 "payment_type" => "credit_card_payment",
970 "payments" => $self->prepare_fines_for_payment # should be safe after self->prepare_fines
973 my $resp = $U->simplereq("open-ils.circ", "open-ils.circ.money.payment",
974 $self->editor->authtoken, $args, $self->ctx->{user}->last_xact_id
977 $self->ctx->{"payment_response"} = $resp;
979 unless ($resp->{"textcode"}) {
980 $self->ctx->{printable_receipt} = $U->simplereq(
981 "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
982 $self->editor->authtoken, $resp->{payments}
986 return Apache2::Const::OK;
989 sub load_myopac_receipt_print {
992 $self->ctx->{printable_receipt} = $U->simplereq(
993 "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
994 $self->editor->authtoken, [$self->cgi->param("payment")]
997 return Apache2::Const::OK;
1000 sub load_myopac_receipt_email {
1003 # The following ML method doesn't actually check whether the user in
1004 # question has an email address, so we do.
1005 if ($self->ctx->{user}->email) {
1006 $self->ctx->{email_receipt_result} = $U->simplereq(
1007 "open-ils.circ", "open-ils.circ.money.payment_receipt.email",
1008 $self->editor->authtoken, [$self->cgi->param("payment")]
1011 $self->ctx->{email_receipt_result} =
1012 new OpenILS::Event("PATRON_NO_EMAIL_ADDRESS");
1015 return Apache2::Const::OK;
1019 my ($self, $limit, $offset, $id_list) = @_;
1021 # XXX TODO: check for failure after various network calls
1023 # It may be unclear, but this result structure lumps circulation and
1024 # reservation fines together, and keeps grocery fines separate.
1025 $self->ctx->{"fines"} = {
1026 "circulation" => [],
1033 my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
1035 # TODO: This should really be a ML call, but the existing calls
1036 # return an excessive amount of data and don't offer streaming
1038 my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
1040 my $req = $cstore->request(
1041 'open-ils.cstore.direct.money.open_billable_transaction_summary.search',
1043 usr => $self->editor->requestor->id,
1044 balance_owed => {'!=' => 0},
1045 ($id_list && @$id_list ? ("id" => $id_list) : ()),
1050 mobts => [qw/grocery circulation reservation/],
1051 bresv => ['target_resource_type'],
1055 circ => ['target_copy'],
1056 acp => ['call_number'],
1059 order_by => { mobts => 'xact_start' },
1064 my @total_keys = qw/total_paid total_owed balance_owed/;
1065 $self->ctx->{"fines"}->{@total_keys} = (0, 0, 0);
1067 while(my $resp = $req->recv) {
1068 my $mobts = $resp->content;
1069 my $circ = $mobts->circulation;
1072 if($mobts->grocery) {
1073 my @billings = sort { $a->billing_ts cmp $b->billing_ts } @{$mobts->grocery->billings};
1074 $last_billing = pop(@billings);
1077 # XXX TODO confirm that the following, and the later division by 100.0
1078 # to get a floating point representation once again, is sufficiently
1079 # "money-safe" math.
1080 $self->ctx->{"fines"}->{$_} += int($mobts->$_ * 100) for (@total_keys);
1082 my $marc_xml = undef;
1083 if ($mobts->xact_type eq 'reservation' and
1084 $mobts->reservation->target_resource_type->record) {
1085 $marc_xml = XML::LibXML->new->parse_string(
1086 $mobts->reservation->target_resource_type->record->marc
1088 } elsif ($mobts->xact_type eq 'circulation' and
1089 $circ->target_copy->call_number->id != -1) {
1090 $marc_xml = XML::LibXML->new->parse_string(
1091 $circ->target_copy->call_number->record->marc
1096 @{$self->ctx->{"fines"}->{$mobts->grocery ? "grocery" : "circulation"}},
1099 last_grocery_billing => $last_billing,
1100 marc_xml => $marc_xml
1107 $self->ctx->{"fines"}->{$_} /= 100.0 for (@total_keys);
1111 sub prepare_fines_for_payment {
1112 # This assumes $self->prepare_fines has already been run
1116 if ($self->ctx->{fines}) {
1117 push @results, [$_->{xact}->id, $_->{xact}->balance_owed] foreach (
1118 @{$self->ctx->{fines}->{circulation}},
1119 @{$self->ctx->{fines}->{grocery}}
1126 sub load_myopac_main {
1128 my $limit = $self->cgi->param('limit') || 0;
1129 my $offset = $self->cgi->param('offset') || 0;
1131 return $self->prepare_fines($limit, $offset) || Apache2::Const::OK;
1134 sub load_myopac_update_email {
1136 my $e = $self->editor;
1137 my $ctx = $self->ctx;
1138 my $email = $self->cgi->param('email') || '';
1140 # needed for most up-to-date email address
1141 if (my $r = $self->prepare_extended_user_info) { return $r };
1143 return Apache2::Const::OK
1144 unless $self->cgi->request_method eq 'POST';
1146 unless($email =~ /.+\@.+\..+/) { # TODO better regex?
1147 $ctx->{invalid_email} = $email;
1148 return Apache2::Const::OK;
1151 my $stat = $U->simplereq(
1153 'open-ils.actor.user.email.update',
1154 $e->authtoken, $email);
1156 unless ($self->cgi->param("redirect_to")) {
1157 my $url = $self->apache->unparsed_uri;
1158 $url =~ s/update_email/prefs/;
1160 return $self->generic_redirect($url);
1163 return $self->generic_redirect;
1166 sub load_myopac_update_username {
1168 my $e = $self->editor;
1169 my $ctx = $self->ctx;
1170 my $username = $self->cgi->param('username') || '';
1172 return Apache2::Const::OK
1173 unless $self->cgi->request_method eq 'POST';
1175 unless($username and $username !~ /\s/) { # any other username restrictions?
1176 $ctx->{invalid_username} = $username;
1177 return Apache2::Const::OK;
1180 if($username ne $e->requestor->usrname) {
1182 my $evt = $U->simplereq(
1184 'open-ils.actor.user.username.update',
1185 $e->authtoken, $username);
1187 if($U->event_equals($evt, 'USERNAME_EXISTS')) {
1188 $ctx->{username_exists} = $username;
1189 return Apache2::Const::OK;
1193 my $url = $self->apache->unparsed_uri;
1194 $url =~ s/update_username/prefs/;
1196 return $self->generic_redirect($url);
1199 sub load_myopac_update_password {
1201 my $e = $self->editor;
1202 my $ctx = $self->ctx;
1204 return Apache2::Const::OK
1205 unless $self->cgi->request_method eq 'POST';
1207 my $current_pw = $self->cgi->param('current_pw') || '';
1208 my $new_pw = $self->cgi->param('new_pw') || '';
1209 my $new_pw2 = $self->cgi->param('new_pw2') || '';
1211 unless($new_pw eq $new_pw2) {
1212 $ctx->{password_nomatch} = 1;
1213 return Apache2::Const::OK;
1216 my $pw_regex = $ctx->{get_org_setting}->($e->requestor->home_ou, 'global.password_regex');
1218 if($pw_regex and $new_pw !~ /$pw_regex/) {
1219 $ctx->{password_invalid} = 1;
1220 return Apache2::Const::OK;
1223 my $evt = $U->simplereq(
1225 'open-ils.actor.user.password.update',
1226 $e->authtoken, $new_pw, $current_pw);
1229 if($U->event_equals($evt, 'INCORRECT_PASSWORD')) {
1230 $ctx->{password_incorrect} = 1;
1231 return Apache2::Const::OK;
1234 my $url = $self->apache->unparsed_uri;
1235 $url =~ s/update_password/prefs/;
1237 return $self->generic_redirect($url);
1240 sub load_myopac_bookbags {
1242 my $e = $self->editor;
1243 my $ctx = $self->ctx;
1245 my ($sorter, $modifier) = $self->_get_bookbag_sort_params;
1246 $e->xact_begin; # replication...
1248 my $rv = $self->load_mylist;
1249 unless($rv eq Apache2::Const::OK) {
1254 $ctx->{bookbags} = $e->search_container_biblio_record_entry_bucket(
1256 {owner => $e->requestor->id, btype => 'bookbag'}, {
1257 order_by => {cbreb => 'name'},
1258 limit => $self->cgi->param('limit') || 10,
1259 offset => $self->cgi->param('offset') || 0
1265 if(!$ctx->{bookbags}) {
1267 return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1270 # Here is the loop that uses search to find the bib records in each
1271 # bookbag. XXX This should be parallelized. Should this be done
1272 # with OpenSRF::MultiSession, or is it enough to use OpenSRF::AppSession
1273 # and call ->request() without calling ->gather() on any of those objects
1274 # until all the requests have been issued?
1276 foreach my $bookbag (@{$ctx->{bookbags}}) {
1277 my $query = $self->_prepare_bookbag_container_query(
1278 $bookbag->id, $sorter, $modifier
1281 # XXX we need to limit the number of records per bbag; use third arg
1282 # of bib_container_items_via_search() i think.
1283 my $items = $U->bib_container_items_via_search($bookbag->id, $query)
1284 or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1286 # Maybe save a little memory by creating only one XML::LibXML::Document
1287 # instance for each record, even if record is repeated across bookbags.
1289 foreach my $rec (map { $_->target_biblio_record_entry } @$items) {
1290 next if $ctx->{bookbags_marc_xml}{$rec->id};
1291 $ctx->{bookbags_marc_xml}{$rec->id} =
1292 (new XML::LibXML)->parse_string($rec->marc);
1295 $bookbag->items($items);
1299 return Apache2::Const::OK;
1303 # actions are create, delete, show, hide, rename, add_rec, delete_item, place_hold
1304 # CGI is action, list=list_id, add_rec/record=bre_id, del_item=bucket_item_id, name=new_bucket_name
1305 sub load_myopac_bookbag_update {
1306 my ($self, $action, $list_id, @hold_recs) = @_;
1307 my $e = $self->editor;
1308 my $cgi = $self->cgi;
1310 # save_notes is effectively another action, but is passed in a separate
1311 # CGI parameter for what are really just layout reasons.
1312 $action = 'save_notes' if $cgi->param('save_notes');
1313 $action ||= $cgi->param('action');
1315 $list_id ||= $cgi->param('list');
1317 my @add_rec = $cgi->param('add_rec') || $cgi->param('record');
1318 my @selected_item = $cgi->param('selected_item');
1319 my $shared = $cgi->param('shared');
1320 my $name = $cgi->param('name');
1321 my $description = $cgi->param('description');
1325 # This url intentionally leaves off the edit_notes parameter, but
1326 # may need to add some back in for paging.
1328 my $url = "https://" . $self->apache->hostname .
1329 $self->ctx->{opac_root} . "/myopac/lists?";
1331 $url .= 'sort=' . uri_escape($cgi->param("sort")) if $cgi->param("sort");
1333 if ($action eq 'create') {
1334 $list = Fieldmapper::container::biblio_record_entry_bucket->new;
1336 $list->description($description);
1337 $list->owner($e->requestor->id);
1338 $list->btype('bookbag');
1339 $list->pub($shared ? 't' : 'f');
1340 $success = $U->simplereq('open-ils.actor',
1341 'open-ils.actor.container.create', $e->authtoken, 'biblio', $list)
1343 } elsif($action eq 'place_hold') {
1345 # @hold_recs comes from anon lists redirect; selected_itesm comes from existing buckets
1346 unless (@hold_recs) {
1347 if (@selected_item) {
1348 my $items = $e->search_container_biblio_record_entry_bucket_item({id => \@selected_item});
1349 @hold_recs = map { $_->target_biblio_record_entry } @$items;
1353 return Apache2::Const::OK unless @hold_recs;
1354 $logger->info("placing holds from list page on: @hold_recs");
1356 my $url = $self->ctx->{opac_root} . '/place_hold?hold_type=T';
1357 $url .= ';hold_target=' . $_ for @hold_recs;
1358 return $self->generic_redirect($url);
1362 $list = $e->retrieve_container_biblio_record_entry_bucket($list_id);
1364 return Apache2::Const::HTTP_BAD_REQUEST unless
1365 $list and $list->owner == $e->requestor->id;
1368 if($action eq 'delete') {
1369 $success = $U->simplereq('open-ils.actor',
1370 'open-ils.actor.container.full_delete', $e->authtoken, 'biblio', $list_id);
1372 } elsif($action eq 'show') {
1373 unless($U->is_true($list->pub)) {
1375 $success = $U->simplereq('open-ils.actor',
1376 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1379 } elsif($action eq 'hide') {
1380 if($U->is_true($list->pub)) {
1382 $success = $U->simplereq('open-ils.actor',
1383 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1386 } elsif($action eq 'rename') {
1389 $success = $U->simplereq('open-ils.actor',
1390 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1393 } elsif($action eq 'add_rec') {
1394 foreach my $add_rec (@add_rec) {
1395 my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1396 $item->bucket($list_id);
1397 $item->target_biblio_record_entry($add_rec);
1398 $success = $U->simplereq('open-ils.actor',
1399 'open-ils.actor.container.item.create', $e->authtoken, 'biblio', $item);
1400 last unless $success;
1403 } elsif($action eq 'del_item') {
1404 foreach (@selected_item) {
1405 $success = $U->simplereq(
1407 'open-ils.actor.container.item.delete', $e->authtoken, 'biblio', $_
1409 last unless $success;
1411 } elsif ($action eq 'save_notes') {
1412 $success = $self->update_bookbag_item_notes;
1415 return $self->generic_redirect($url) if $success;
1417 # XXX FIXME Bucket failure doesn't have a page to show the user anything
1418 # right now. User just sees a 404 currently.
1420 $self->ctx->{bucket_action} = $action;
1421 $self->ctx->{bucket_action_failed} = 1;
1422 return Apache2::Const::OK;
1425 sub update_bookbag_item_notes {
1427 my $e = $self->editor;
1429 my @note_keys = grep /^note-\d+/, keys(%{$self->cgi->Vars});
1430 my @item_keys = grep /^item-\d+/, keys(%{$self->cgi->Vars});
1432 # We're going to leverage an API call that's already been written to check
1433 # permissions appropriately.
1435 my $a = create OpenSRF::AppSession("open-ils.actor");
1436 my $method = "open-ils.actor.container.item_note.cud";
1438 for my $note_key (@note_keys) {
1441 my $id = ($note_key =~ /(\d+)/)[0];
1444 $e->retrieve_container_biblio_record_entry_bucket_item_note($id))) {
1445 my $event = $e->die_event;
1446 $self->apache->log->warn(
1447 "error retrieving cbrebin id $id, got event " .
1451 $self->ctx->{bucket_action_event} = $event;
1455 if (length($self->cgi->param($note_key))) {
1456 $note->ischanged(1);
1457 $note->note($self->cgi->param($note_key));
1459 $note->isdeleted(1);
1462 my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1);
1464 if (defined $U->event_code($r)) {
1465 $self->apache->log->warn(
1466 "attempt to modify cbrebin " . $note->id .
1467 " returned event " . $r->{textcode}
1471 $self->ctx->{bucket_action_event} = $r;
1476 for my $item_key (@item_keys) {
1477 my $id = int(($item_key =~ /(\d+)/)[0]);
1478 my $text = $self->cgi->param($item_key);
1481 next unless length $text;
1483 my $note = new Fieldmapper::container::biblio_record_entry_bucket_item_note;
1488 my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1);
1490 if (defined $U->event_code($r)) {
1491 $self->apache->log->warn(
1492 "attempt to create cbrebin for item " . $note->item .
1493 " returned event " . $r->{textcode}
1497 $self->ctx->{bucket_action_event} = $r;
1506 sub load_myopac_bookbag_print {
1509 $self->apache->content_type("text/plain; encoding=utf8");
1511 my $id = int($self->cgi->param("list"));
1513 my ($sorter, $modifier) = $self->_get_bookbag_sort_params;
1516 $self->_prepare_bookbag_container_query($id, $sorter, $modifier);
1520 # Get the bookbag object itself, assuming we're allowed to.
1521 if ($self->editor->allowed("VIEW_CONTAINER")) {
1523 $bbag = $self->editor->retrieve_container_biblio_record_entry_bucket($id) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1525 my $bookbags = $self->editor->search_container_biblio_record_entry_bucket(
1529 "owner" => $self->editor->requestor->id,
1533 ) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1535 $bbag = pop @$bookbags;
1538 # If we have a bookbag we're allowed to look at, issue the A/T event
1539 # to get CSV, passing as a user param that search query we built before.
1541 $self->ctx->{csv} = $U->fire_object_event(
1542 undef, "container.biblio_record_entry_bucket.csv",
1543 $bbag, $self->editor->requestor->home_ou,
1544 undef, {"item_search" => $item_search}
1548 return Apache2::Const::OK;
1551 sub load_password_reset {
1553 my $cgi = $self->cgi;
1554 my $ctx = $self->ctx;
1555 my $barcode = $cgi->param('barcode');
1556 my $username = $cgi->param('username');
1557 my $email = $cgi->param('email');
1558 my $pwd1 = $cgi->param('pwd1');
1559 my $pwd2 = $cgi->param('pwd2');
1560 my $uuid = $ctx->{page_args}->[0];
1564 $logger->info("patron password reset with uuid $uuid");
1566 if ($pwd1 and $pwd2) {
1568 if ($pwd1 eq $pwd2) {
1570 my $response = $U->simplereq(
1572 'open-ils.actor.patron.password_reset.commit',
1575 $logger->info("patron password reset response " . Dumper($response));
1577 if ($U->event_code($response)) { # non-success event
1579 my $code = $response->{textcode};
1581 if ($code eq 'PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST') {
1582 $ctx->{pwreset} = {style => 'error', status => 'NOT_ACTIVE'};
1585 if ($code eq 'PATRON_PASSWORD_WAS_NOT_STRONG') {
1586 $ctx->{pwreset} = {style => 'error', status => 'NOT_STRONG'};
1591 $ctx->{pwreset} = {style => 'success', status => 'SUCCESS'};
1594 } else { # passwords not equal
1596 $ctx->{pwreset} = {style => 'error', status => 'NO_MATCH'};
1599 } else { # 2 password values needed
1601 $ctx->{pwreset} = {status => 'TWO_PASSWORDS'};
1604 } elsif ($barcode or $username) {
1606 my @params = $barcode ? ('barcode', $barcode) : ('username', $username);
1610 'open-ils.actor.patron.password_reset.request', @params);
1612 $ctx->{pwreset} = {status => 'REQUEST_SUCCESS'};
1615 $logger->info("patron password reset resulted in " . Dumper($ctx->{pwreset}));
1616 return Apache2::Const::OK;