]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
TPac: integrated patron password reset
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / WWW / EGCatLoader / Account.pm
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;
8 use OpenILS::Event;
9 use OpenSRF::Utils::JSON;
10 use Data::Dumper;
11 $Data::Dumper::Indent = 0;
12 use DateTime;
13 my $U = 'OpenILS::Application::AppUtils';
14
15 sub prepare_extended_user_info {
16     my $self = shift;
17     my @extra_flesh = @_;
18
19     $self->ctx->{user} = $self->editor->retrieve_actor_user([
20         $self->ctx->{user}->id,
21         {
22             flesh => 1,
23             flesh_fields => {
24                 au => [qw/card home_ou addresses ident_type billing_address/, @extra_flesh]
25                 # ...
26             }
27         }
28     ]) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
29
30     return;
31 }
32
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) = @_;
38
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");
43     return 0;
44 }
45
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) = @_;
49
50     my $would_block = $self->ctx->{get_org_setting}->
51         ($pickup_lib, "circ.holds.hold_has_copy_at.block");
52     my $would_alert = (
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;
57
58     if ($would_block or $would_alert) {
59         my $args = {
60             "hold_target" => $hold_target,
61             "hold_type" => $hold_type,
62             "org_unit" => $pickup_lib
63         };
64         my $local_avail = $U->simplereq(
65             "open-ils.circ",
66             "open-ils.circ.hold.has_copy_at", $self->editor->authtoken, $args
67         );
68         $logger->info(
69             "copy availability information for " . Dumper($args) .
70             " is " . Dumper($local_avail)
71         );
72         if (%$local_avail) { # if hash not empty
73             $self->ctx->{hold_copy_available} = $local_avail;
74             return ($would_block, $would_alert);
75         }
76     }
77
78     return (0, 0);
79 }
80
81 # context additions: 
82 #   user : au object, fleshed
83 sub load_myopac_prefs {
84     my $self = shift;
85     my $cgi = $self->cgi;
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');
90
91     $self->prepare_extended_user_info;
92     my $user = $self->ctx->{user};
93
94     return Apache2::Const::OK unless 
95         $pending_addr or $replace_addr or $delete_pending;
96
97     my @form_fields = qw/address_type street1 street2 city county state country post_code/;
98
99     my $paddr;
100     if( $pending_addr ) { # update an existing pending address
101
102         ($paddr) = grep { $_->id == $pending_addr } @{$user->addresses};
103         return Apache2::Const::HTTP_BAD_REQUEST unless $paddr;
104         $paddr->$_( $cgi->param($_) ) for @form_fields;
105
106     } elsif( $replace_addr ) { # create a new pending address for 'replace_addr'
107
108         $paddr = Fieldmapper::actor::user_address->new;
109         $paddr->isnew(1);
110         $paddr->usr($user->id);
111         $paddr->pending('t');
112         $paddr->replaces($replace_addr);
113         $paddr->$_( $cgi->param($_) ) for @form_fields;
114
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);
120     }
121
122     my $resp = $U->simplereq(
123         'open-ils.actor', 
124         'open-ils.actor.user.address.pending.cud',
125         $e->authtoken, $paddr);
126
127     if( $U->event_code($resp) ) {
128         $logger->error("Error updating pending address: $resp");
129         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
130     }
131
132     # in light of these changes, re-fetch latest data
133     $e->xact_begin; 
134     $self->prepare_extended_user_info;
135     $e->rollback;
136
137     return Apache2::Const::OK;
138 }
139
140 sub load_myopac_prefs_notify {
141     my $self = shift;
142     my $e = $self->editor;
143
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';
147
148     $self->ctx->{opt_in_settings} = $user_prefs; 
149
150     return Apache2::Const::OK;
151 }
152
153 sub fetch_optin_prefs {
154     my $self = shift;
155     my $e = $self->editor;
156
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(
162         'open-ils.actor',
163         'open-ils.actor.event_def.opt_in.settings.atomic',
164         $e->authtoken, $e->requestor->home_ou);
165
166     # some opt-ins are staff-only
167     $opt_ins = [ grep { $U->is_true($_->opac_visible) } @$opt_ins ];
168
169     # fetch user setting values for each of the opt-in settings
170     my $user_set = $U->simplereq(
171         'open-ils.actor',
172         'open-ils.actor.patron.settings.retrieve',
173         $e->authtoken, 
174         $e->requestor->id, 
175         [map {$_->name} @$opt_ins]
176     );
177
178     return [map { {cust => $_, value => $user_set->{$_->name} } } @$opt_ins];
179 }
180
181 sub update_optin_prefs {
182     my $self = shift;
183     my $user_prefs = shift;
184     my $e = $self->editor;
185     my @settings = $self->cgi->param('setting');
186     my %newsets;
187
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;
193     }
194
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;
199     }
200
201     $U->simplereq(
202         'open-ils.actor',
203         'open-ils.actor.patron.settings.update',
204         $e->authtoken, $e->requestor->id, \%newsets);
205
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};
210     }
211
212     return $user_prefs;
213 }
214
215 sub _load_user_with_prefs {
216     my $self = shift;
217     my $stat = $self->prepare_extended_user_info('settings');
218     return $stat if $stat; # not-OK
219
220     $self->ctx->{user_setting_map} = {
221         map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) } 
222             @{$self->ctx->{user}->settings}
223     };
224
225     return undef;
226 }
227
228 sub _get_bookbag_sort_params {
229     my ($self) = @_;
230
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") || "";
236     my $modifier;
237     if ($sorter) {
238         $sorter =~ s/^(.*?)\.(.*)/$1/;
239         $modifier = $2 || undef;
240     }
241
242     return ($sorter, $modifier);
243 }
244
245 sub _prepare_bookbag_container_query {
246     my ($self, $container_id, $sorter, $modifier) = @_;
247
248     return sprintf(
249         "container(bre,bookbag,%d,%s)%s%s",
250         $container_id, $self->editor->authtoken,
251         ($sorter ? " sort($sorter)" : ""),
252         ($modifier ? "#$modifier" : "")
253     );
254 }
255
256 sub load_myopac_prefs_settings {
257     my $self = shift;
258
259     my $stat = $self->_load_user_with_prefs;
260     return $stat if $stat;
261
262     return Apache2::Const::OK
263         unless $self->cgi->request_method eq 'POST';
264
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.
268
269     my %settings;
270     my $set_map = $self->ctx->{user_setting_map};
271
272     my $key = 'opac.hits_per_page';
273     my $val = $self->cgi->param($key);
274     $settings{$key}= $val unless $$set_map{$key} eq $val;
275
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};
282         } else {
283             # clear the start time if one previously existed for the user
284             $settings{$key} = undef if $$set_map{$key};
285         }
286     }
287     
288     # Send the modified settings off to be saved
289     $U->simplereq(
290         'open-ils.actor', 
291         'open-ils.actor.patron.settings.update',
292         $self->editor->authtoken, undef, \%settings);
293
294     # re-fetch user prefs 
295     $self->ctx->{updated_user_settings} = \%settings;
296     return $self->_load_user_with_prefs || Apache2::Const::OK;
297 }
298
299 sub fetch_user_holds {
300     my $self = shift;
301     my $hold_ids = shift;
302     my $ids_only = shift;
303     my $flesh = shift;
304     my $available = shift;
305     my $limit = shift;
306     my $offset = shift;
307
308     my $e = $self->editor;
309
310     if(!$hold_ids) {
311         my $circ = OpenSRF::AppSession->create('open-ils.circ');
312
313         $hold_ids = $circ->request(
314             'open-ils.circ.holds.id_list.retrieve.authoritative', 
315             $e->authtoken, 
316             $e->requestor->id
317         )->gather(1);
318         $circ->kill_me;
319     
320         $hold_ids = [ grep { defined $_ } @$hold_ids[$offset..($offset + $limit - 1)] ] if $limit or $offset;
321     }
322
323
324     return $hold_ids if $ids_only or @$hold_ids == 0;
325
326     my $args = {
327         suppress_notices => 1,
328         suppress_transits => 1,
329         suppress_mvr => 1,
330         suppress_patron_details => 1,
331         include_bre => $flesh ? 1 : 0
332     };
333
334     # ----------------------------------------------------------------
335     # Collect holds in batches of $batch_size for faster retrieval
336
337     my $batch_size = 8;
338     my $batch_idx = 0;
339     my $mk_req_batch = sub {
340         my @ses;
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});
350         }
351         return @ses;
352     };
353
354     my $first = 1;
355     my(@collected, @holds, @ses);
356
357     while(1) {
358         @ses = $mk_req_batch->() if $first;
359         last if $first and not @ses;
360
361         if(@collected) {
362             # If desired by the caller, filter any holds that are not available.
363             if ($available) {
364                 @collected = grep { $_->{hold}->{status} == 4 } @collected;
365             }
366             while(my $blob = pop(@collected)) {
367                 $blob->{marc_xml} = XML::LibXML->new->parse_string($blob->{hold}->{bre}->marc) if $flesh;
368                 push(@holds, $blob);
369             }
370         }
371
372         for my $req_data (@ses) {
373             push(@collected, {hold => $req_data->{req}->gather(1)});
374             $req_data->{ses}->kill_me;
375         }
376
377         @ses = $mk_req_batch->();
378         last unless @collected or @ses;
379         $first = 0;
380     }
381
382     # put the holds back into the original server sort order
383     my @sorted;
384     for my $id (@$hold_ids) {
385         push @sorted, grep { $_->{hold}->{hold}->id == $id } @holds;
386     }
387
388     return \@sorted;
389 }
390
391 sub handle_hold_update {
392     my $self = shift;
393     my $action = shift;
394     my $hold_ids = shift;
395     my $e = $self->editor;
396     my $url;
397
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/;
400
401     my $circ = OpenSRF::AppSession->create('open-ils.circ');
402
403     if($action =~ /cancel/) {
404
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
408         }
409
410     } elsif ($action =~ /activate|suspend/) {
411         
412         my $vlist = [];
413         for my $hold_id (@hold_ids) {
414             my $vals = {id => $hold_id};
415
416             if($action =~ /activate/) {
417                 $vals->{frozen} = 'f';
418                 $vals->{thaw_date} = undef;
419
420             } elsif($action =~ /suspend/) {
421                 $vals->{frozen} = 't';
422                 # $vals->{thaw_date} = TODO;
423             }
424             push(@$vlist, $vals);
425         }
426
427         $circ->request('open-ils.circ.hold.update.batch.atomic', $e->authtoken, undef, $vlist)->gather(1);
428     } elsif ($action eq 'edit') {
429
430         my @vals = map {
431             my $val = {"id" => $_};
432             $val->{"frozen"} = $self->cgi->param("frozen");
433             $val->{"pickup_lib"} = $self->cgi->param("pickup_lib");
434
435             for my $field (qw/expire_time thaw_date/) {
436                 # XXX TODO make this support other date formats, not just
437                 # MM/DD/YYYY.
438                 next unless $self->cgi->param($field) =~
439                     m:^(\d{2})/(\d{2})/(\d{4})$:;
440                 $val->{$field} = "$3-$1-$2";
441             }
442             $val;
443         } @hold_ids;
444
445         $circ->request(
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';
450     }
451
452     $circ->kill_me;
453     return defined($url) ? $self->generic_redirect($url) : undef;
454 }
455
456 sub load_myopac_holds {
457     my $self = shift;
458     my $e = $self->editor;
459     my $ctx = $self->ctx;
460     
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);
466
467     my $hold_handle_result;
468     $hold_handle_result = $self->handle_hold_update($action) if $action;
469
470     $ctx->{holds} = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available, $limit, $offset);
471
472     return defined($hold_handle_result) ? $hold_handle_result : Apache2::Const::OK;
473 }
474
475 sub load_place_hold {
476     my $self = shift;
477     my $ctx = $self->ctx;
478     my $gos = $ctx->{get_org_setting};
479     my $e = $self->editor;
480     my $cgi = $self->cgi;
481
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
486
487     return $self->post_hold_redirect unless @targets;
488
489     $logger->info("Looking at hold targets: @targets");
490
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;
497
498         $ctx->{default_pickup_lib} = $ctx->{patron_recipient}->home_ou;
499     }
500
501     my $request_lib = $e->requestor->ws_ou;
502     my @hold_data;
503     $ctx->{hold_data} = \@hold_data;
504
505     my $type_dispatch = {
506         T => sub {
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});
511             }
512         },
513         V => sub {
514             my $vols = $e->batch_retrieve_asset_call_number([
515                 \@targets, {
516                     "flesh" => 1,
517                     "flesh_fields" => {"acn" => ["record"]}
518                 }
519             ], {substream => 1});
520
521             for my $id (@targets) { 
522                 my ($vol) = grep {$_->id eq $id} @$vols;
523                 push(@hold_data, {target => $vol, record => $vol->record});
524             }
525         },
526         C => sub {
527             my $copies = $e->batch_retrieve_asset_copy([
528                 \@targets, {
529                     "flesh" => 2,
530                     "flesh_fields" => {
531                         "acn" => ["record"],
532                         "acp" => ["call_number"]
533                     }
534                 }
535             ], {substream => 1});
536
537             for my $id (@targets) { 
538                 my ($copy) = grep {$_->id eq $id} @$copies;
539                 push(@hold_data, {target => $copy, record => $copy->call_number->record});
540             }
541         },
542         I => sub {
543             my $isses = $e->batch_retrieve_serial_issuance([
544                 \@targets, {
545                     "flesh" => 2,
546                     "flesh_fields" => {
547                         "siss" => ["subscription"], "ssub" => ["record_entry"]
548                     }
549                 }
550             ], {substream => 1});
551
552             for my $id (@targets) { 
553                 my ($iss) = grep {$_->id eq $id} @$isses;
554                 push(@hold_data, {target => $iss, record => $iss->subscription->record_entry});
555             }
556         }
557         # ...
558
559     }->{$ctx->{hold_type}}->();
560
561     # caller sent bad target IDs or the wrong hold type
562     return Apache2::Const::HTTP_BAD_REQUEST unless @hold_data;
563
564     # generate the MARC xml for each record
565     $_->{marc_xml} = XML::LibXML->new->parse_string($_->{record}->marc) for @hold_data;
566
567     my $pickup_lib = $cgi->param('pickup_lib');
568     # no pickup lib means no holds placement
569     return Apache2::Const::OK unless $pickup_lib;
570
571     $ctx->{hold_attempt_made} = 1;
572
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};
578
579     my $usr = $e->requestor->id;
580
581     if ($ctx->{is_staff} and !$cgi->param("hold_usr_is_requestor")) {
582         # find the real hold target
583
584         $usr = $U->simplereq(
585             'open-ils.actor', 
586             "open-ils.actor.user.retrieve_id_by_barcode_or_username",
587             $e->authtoken, $cgi->param("hold_usr"));
588
589         if (defined $U->event_code($usr)) {
590             $ctx->{hold_failed} = 1;
591             $ctx->{hold_failed_event} = $usr;
592         }
593     }
594
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);
600     
601         if ($local_block) {
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;
607         }
608     }
609
610
611     my $method = 'open-ils.circ.holds.test_and_create.batch';
612     $method .= '.override' if $cgi->param('override');
613
614     my @create_targets = map {$_->{target}->id} (grep { !$_->{hold_failed} } @hold_data);
615
616     if(@create_targets) {
617
618         my $bses = OpenSRF::AppSession->create('open-ils.circ');
619         my $breq = $bses->request( 
620             $method, 
621             $e->authtoken, 
622             {   patronid => $usr, 
623                 pickup_lib => $pickup_lib, 
624                 hold_type => $ctx->{hold_type}
625             }, 
626             \@create_targets
627         );
628
629         while (my $resp = $breq->recv) {
630
631             $resp = $resp->content;
632             $logger->info('batch hold placement result: ' . OpenSRF::Utils::JSON->perl2JSON($resp));
633
634             if ($U->event_code($resp)) {
635                 $ctx->{general_hold_error} = $resp;
636                 last;
637             }
638
639             my ($hdata) = grep {$_->{target}->id eq $resp->{target}} @hold_data;
640             my $result = $resp->{result};
641
642             if ($U->event_code($result)) {
643                 # e.g. permission denied
644                 $hdata->{hold_failed} = 1;
645                 $hdata->{hold_failed_event} = $result;
646
647             } else {
648                 
649                 if(not ref $result and $result > 0) {
650                     # successul hold returns the hold ID
651
652                     $hdata->{hold_success} = $result; 
653     
654                 } else {
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});
659                 }
660             }
661         }
662
663         $bses->kill_me;
664     }
665
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};
669
670     # if successful, do some cleanup and return the 
671     # user to the requesting page.
672
673     return $self->post_hold_redirect;
674 }
675
676 sub post_hold_redirect {
677     my $self = shift;
678     
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;
686
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()
691
692     return $self->generic_redirect(
693         undef,
694         $self->cgi->cookie(
695             -name => "patron_barcode",
696             -path => "/",
697             -secure => 1,
698             -value => "",
699             -expires => "-1h"
700         )
701     );
702 }
703
704
705 sub fetch_user_circs {
706     my $self = shift;
707     my $flesh = shift; # flesh bib data, etc.
708     my $circ_ids = shift;
709     my $limit = shift;
710     my $offset = shift;
711
712     my $e = $self->editor;
713
714     my @circ_ids;
715
716     if($circ_ids) {
717         @circ_ids = @$circ_ids;
718
719     } else {
720
721         my $circ_data = $U->simplereq(
722             'open-ils.actor', 
723             'open-ils.actor.user.checked_out',
724             $e->authtoken, 
725             $e->requestor->id
726         );
727
728         @circ_ids =  ( @{$circ_data->{overdue}}, @{$circ_data->{out}} );
729
730         if($limit or $offset) {
731             @circ_ids = grep { defined $_ } @circ_ids[0..($offset + $limit - 1)];
732         }
733     }
734
735     return [] unless @circ_ids;
736
737     my $qflesh = {
738         flesh => 3,
739         flesh_fields => {
740             circ => ['target_copy'],
741             acp => ['call_number'],
742             acn => ['record']
743         }
744     };
745
746     $e->xact_begin;
747     my $circs = $e->search_action_circulation(
748         [{id => \@circ_ids}, ($flesh) ? $qflesh : {}], {substream => 1});
749
750     my @circs;
751     for my $circ (@$circs) {
752         push(@circs, {
753             circ => $circ, 
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
757         });
758     }
759     $e->xact_rollback;
760
761     # make sure the final list is in the correct order
762     my @sorted_circs;
763     for my $id (@circ_ids) {
764         push(
765             @sorted_circs,
766             (grep { $_->{circ}->id == $id } @circs)
767         );
768     }
769
770     return \@sorted_circs;
771 }
772
773
774 sub handle_circ_renew {
775     my $self = shift;
776     my $action = shift;
777     my $ctx = $self->ctx;
778
779     my @renew_ids = $self->cgi->param('circ');
780
781     my $circs = $self->fetch_user_circs(0, ($action eq 'renew') ? [@renew_ids] : undef);
782
783     # TODO: fire off renewal calls in batches to speed things up
784     my @responses;
785     for my $circ (@$circs) {
786
787         my $evt = $U->simplereq(
788             'open-ils.circ', 
789             'open-ils.circ.renew',
790             $self->editor->authtoken,
791             {
792                 patron_id => $self->editor->requestor->id,
793                 copy_id => $circ->{circ}->target_copy,
794                 opac_renewal => 1
795             }
796         );
797
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});
802     }
803
804     return @responses;
805 }
806
807
808 sub load_myopac_circs {
809     my $self = shift;
810     my $e = $self->editor;
811     my $ctx = $self->ctx;
812
813     $ctx->{circs} = [];
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') || '';
817
818     # perform the renewal first if necessary
819     my @results = $self->handle_circ_renew($action) if $action =~ /renew/;
820
821     $ctx->{circs} = $self->fetch_user_circs(1, undef, $limit, $offset);
822
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;
827
828         if($resp) {
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';
833         }
834     }
835
836     $ctx->{success_renewals} = $success_renewals;
837     $ctx->{failed_renewals} = $failed_renewals;
838
839     return Apache2::Const::OK;
840 }
841
842 sub load_myopac_circ_history {
843     my $self = shift;
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;
848
849     $ctx->{circ_history_limit} = $limit;
850     $ctx->{circ_history_offset} = $offset;
851
852     my $circ_ids = $e->json_query({
853         select => {
854             au => [{
855                 column => 'id', 
856                 transform => 'action.usr_visible_circs', 
857                 result_field => 'id'
858             }]
859         },
860         from => 'au',
861         where => {id => $e->requestor->id}, 
862         limit => $limit,
863         offset => $offset
864     });
865
866     $ctx->{circs} = $self->fetch_user_circs(1, [map { $_->{id} } @$circ_ids]);
867     return Apache2::Const::OK;
868 }
869
870 # TODO: action.usr_visible_holds does not return cancelled holds.  Should it?
871 sub load_myopac_hold_history {
872     my $self = shift;
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;
879
880     my $hold_ids = $e->json_query({
881         select => {
882             au => [{
883                 column => 'id', 
884                 transform => 'action.usr_visible_holds', 
885                 result_field => 'id'
886             }]
887         },
888         from => 'au',
889         where => {id => $e->requestor->id}, 
890         limit => $limit,
891         offset => $offset
892     });
893
894     $ctx->{holds} = $self->fetch_user_holds([map { $_->{id} } @$hold_ids], 0, 1, 0);
895     return Apache2::Const::OK;
896 }
897
898 sub load_myopac_payment_form {
899     my $self = shift;
900     my $r;
901
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;
904
905     return Apache2::Const::OK;
906 }
907
908 # TODO: add other filter options as params/configs/etc.
909 sub load_myopac_payments {
910     my $self = shift;
911     my $limit = $self->cgi->param('limit') || 20;
912     my $offset = $self->cgi->param('offset') || 0;
913     my $e = $self->editor;
914
915     $self->ctx->{payment_history_limit} = $limit;
916     $self->ctx->{payment_history_offset} = $offset;
917
918     my $args = {};
919     $args->{limit} = $limit if $limit;
920     $args->{offset} = $offset if $offset;
921
922     if (my $max_age = $self->ctx->{get_org_setting}->(
923         $e->requestor->home_ou, "opac.payment_history_age_limit"
924     )) {
925         my $min_ts = DateTime->now(
926             "time_zone" => DateTime::TimeZone->new("name" => "local"),
927         )->subtract("seconds" => interval_to_seconds($max_age))->iso8601();
928         
929         $logger->info("XXX min_ts: $min_ts");
930         $args->{"where"} = {"payment_ts" => {">=" => $min_ts}};
931     }
932
933     $self->ctx->{payments} = $U->simplereq(
934         'open-ils.actor',
935         'open-ils.actor.user.payments.retrieve.atomic',
936         $e->authtoken, $e->requestor->id, $args);
937
938     return Apache2::Const::OK;
939 }
940
941 sub load_myopac_pay {
942     my $self = shift;
943     my $r;
944
945     $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact'), $self->cgi->param('xact_misc')]) and
946         return $r;
947
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'))))
954         );
955         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
956     }
957
958     my $cc_args = {"where_process" => 1};
959
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
963         billing_zip
964     /);
965
966     my $args = {
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
971     };
972
973     my $resp = $U->simplereq("open-ils.circ", "open-ils.circ.money.payment",
974         $self->editor->authtoken, $args, $self->ctx->{user}->last_xact_id
975     );
976
977     $self->ctx->{"payment_response"} = $resp;
978
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}
983         );
984     }
985
986     return Apache2::Const::OK;
987 }
988
989 sub load_myopac_receipt_print {
990     my $self = shift;
991
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")]
995     );
996
997     return Apache2::Const::OK;
998 }
999
1000 sub load_myopac_receipt_email {
1001     my $self = shift;
1002
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")]
1009         );
1010     } else {
1011         $self->ctx->{email_receipt_result} =
1012             new OpenILS::Event("PATRON_NO_EMAIL_ADDRESS");
1013     }
1014
1015     return Apache2::Const::OK;
1016 }
1017
1018 sub prepare_fines {
1019     my ($self, $limit, $offset, $id_list) = @_;
1020
1021     # XXX TODO: check for failure after various network calls
1022
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" => [],
1027         "grocery" => [],
1028         "total_paid" => 0,
1029         "total_owed" => 0,
1030         "balance_owed" => 0
1031     };
1032
1033     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
1034
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
1037
1038     my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
1039
1040     my $req = $cstore->request(
1041         'open-ils.cstore.direct.money.open_billable_transaction_summary.search',
1042         {
1043             usr => $self->editor->requestor->id,
1044             balance_owed => {'!=' => 0},
1045             ($id_list && @$id_list ? ("id" => $id_list) : ()),
1046         },
1047         {
1048             flesh => 4,
1049             flesh_fields => {
1050                 mobts => [qw/grocery circulation reservation/],
1051                 bresv => ['target_resource_type'],
1052                 brt => ['record'],
1053                 mg => ['billings'],
1054                 mb => ['btype'],
1055                 circ => ['target_copy'],
1056                 acp => ['call_number'],
1057                 acn => ['record']
1058             },
1059             order_by => { mobts => 'xact_start' },
1060             %paging
1061         }
1062     );
1063
1064     my @total_keys = qw/total_paid total_owed balance_owed/;
1065     $self->ctx->{"fines"}->{@total_keys} = (0, 0, 0);
1066
1067     while(my $resp = $req->recv) {
1068         my $mobts = $resp->content;
1069         my $circ = $mobts->circulation;
1070
1071         my $last_billing;
1072         if($mobts->grocery) {
1073             my @billings = sort { $a->billing_ts cmp $b->billing_ts } @{$mobts->grocery->billings};
1074             $last_billing = pop(@billings);
1075         }
1076
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);
1081
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
1087             );
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
1092             );
1093         }
1094
1095         push(
1096             @{$self->ctx->{"fines"}->{$mobts->grocery ? "grocery" : "circulation"}},
1097             {
1098                 xact => $mobts,
1099                 last_grocery_billing => $last_billing,
1100                 marc_xml => $marc_xml
1101             } 
1102         );
1103     }
1104
1105     $cstore->kill_me;
1106
1107     $self->ctx->{"fines"}->{$_} /= 100.0 for (@total_keys);
1108     return;
1109 }
1110
1111 sub prepare_fines_for_payment {
1112     # This assumes $self->prepare_fines has already been run
1113     my ($self) = @_;
1114
1115     my @results = ();
1116     if ($self->ctx->{fines}) {
1117         push @results, [$_->{xact}->id, $_->{xact}->balance_owed] foreach (
1118             @{$self->ctx->{fines}->{circulation}},
1119             @{$self->ctx->{fines}->{grocery}}
1120         );
1121     }
1122
1123     return \@results;
1124 }
1125
1126 sub load_myopac_main {
1127     my $self = shift;
1128     my $limit = $self->cgi->param('limit') || 0;
1129     my $offset = $self->cgi->param('offset') || 0;
1130
1131     return $self->prepare_fines($limit, $offset) || Apache2::Const::OK;
1132 }
1133
1134 sub load_myopac_update_email {
1135     my $self = shift;
1136     my $e = $self->editor;
1137     my $ctx = $self->ctx;
1138     my $email = $self->cgi->param('email') || '';
1139
1140     # needed for most up-to-date email address
1141     if (my $r = $self->prepare_extended_user_info) { return $r };
1142
1143     return Apache2::Const::OK 
1144         unless $self->cgi->request_method eq 'POST';
1145
1146     unless($email =~ /.+\@.+\..+/) { # TODO better regex?
1147         $ctx->{invalid_email} = $email;
1148         return Apache2::Const::OK;
1149     }
1150
1151     my $stat = $U->simplereq(
1152         'open-ils.actor', 
1153         'open-ils.actor.user.email.update', 
1154         $e->authtoken, $email);
1155
1156     unless ($self->cgi->param("redirect_to")) {
1157         my $url = $self->apache->unparsed_uri;
1158         $url =~ s/update_email/prefs/;
1159
1160         return $self->generic_redirect($url);
1161     }
1162
1163     return $self->generic_redirect;
1164 }
1165
1166 sub load_myopac_update_username {
1167     my $self = shift;
1168     my $e = $self->editor;
1169     my $ctx = $self->ctx;
1170     my $username = $self->cgi->param('username') || '';
1171
1172     return Apache2::Const::OK 
1173         unless $self->cgi->request_method eq 'POST';
1174
1175     unless($username and $username !~ /\s/) { # any other username restrictions?
1176         $ctx->{invalid_username} = $username;
1177         return Apache2::Const::OK;
1178     }
1179
1180     if($username ne $e->requestor->usrname) {
1181
1182         my $evt = $U->simplereq(
1183             'open-ils.actor', 
1184             'open-ils.actor.user.username.update', 
1185             $e->authtoken, $username);
1186
1187         if($U->event_equals($evt, 'USERNAME_EXISTS')) {
1188             $ctx->{username_exists} = $username;
1189             return Apache2::Const::OK;
1190         }
1191     }
1192
1193     my $url = $self->apache->unparsed_uri;
1194     $url =~ s/update_username/prefs/;
1195
1196     return $self->generic_redirect($url);
1197 }
1198
1199 sub load_myopac_update_password {
1200     my $self = shift;
1201     my $e = $self->editor;
1202     my $ctx = $self->ctx;
1203
1204     return Apache2::Const::OK 
1205         unless $self->cgi->request_method eq 'POST';
1206
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') || '';
1210
1211     unless($new_pw eq $new_pw2) {
1212         $ctx->{password_nomatch} = 1;
1213         return Apache2::Const::OK;
1214     }
1215
1216     my $pw_regex = $ctx->{get_org_setting}->($e->requestor->home_ou, 'global.password_regex');
1217
1218     if($pw_regex and $new_pw !~ /$pw_regex/) {
1219         $ctx->{password_invalid} = 1;
1220         return Apache2::Const::OK;
1221     }
1222
1223     my $evt = $U->simplereq(
1224         'open-ils.actor', 
1225         'open-ils.actor.user.password.update', 
1226         $e->authtoken, $new_pw, $current_pw);
1227
1228
1229     if($U->event_equals($evt, 'INCORRECT_PASSWORD')) {
1230         $ctx->{password_incorrect} = 1;
1231         return Apache2::Const::OK;
1232     }
1233
1234     my $url = $self->apache->unparsed_uri;
1235     $url =~ s/update_password/prefs/;
1236
1237     return $self->generic_redirect($url);
1238 }
1239
1240 sub load_myopac_bookbags {
1241     my $self = shift;
1242     my $e = $self->editor;
1243     my $ctx = $self->ctx;
1244
1245     my ($sorter, $modifier) = $self->_get_bookbag_sort_params;
1246     $e->xact_begin; # replication...
1247
1248     my $rv = $self->load_mylist;
1249     unless($rv eq Apache2::Const::OK) {
1250         $e->rollback;
1251         return $rv;
1252     }
1253
1254     $ctx->{bookbags} = $e->search_container_biblio_record_entry_bucket(
1255         [
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
1260             }
1261         ],
1262         {substream => 1}
1263     );
1264
1265     if(!$ctx->{bookbags}) {
1266         $e->rollback;
1267         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1268     }
1269     
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?
1275
1276     foreach my $bookbag (@{$ctx->{bookbags}}) {
1277         my $query = $self->_prepare_bookbag_container_query(
1278             $bookbag->id, $sorter, $modifier
1279         );
1280
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;
1285
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.
1288
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);
1293         }
1294
1295         $bookbag->items($items);
1296     }
1297
1298     $e->rollback;
1299     return Apache2::Const::OK;
1300 }
1301
1302
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;
1309
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');
1314
1315     $list_id ||= $cgi->param('list');
1316
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');
1322     my $success = 0;
1323     my $list;
1324
1325     # This url intentionally leaves off the edit_notes parameter, but
1326     # may need to add some back in for paging.
1327
1328     my $url = "https://" . $self->apache->hostname .
1329         $self->ctx->{opac_root} . "/myopac/lists?";
1330
1331     $url .= 'sort=' . uri_escape($cgi->param("sort")) if $cgi->param("sort");
1332
1333     if ($action eq 'create') {
1334         $list = Fieldmapper::container::biblio_record_entry_bucket->new;
1335         $list->name($name);
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)
1342
1343     } elsif($action eq 'place_hold') {
1344
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;
1350             }
1351         }
1352                 
1353         return Apache2::Const::OK unless @hold_recs;
1354         $logger->info("placing holds from list page on: @hold_recs");
1355
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);
1359
1360     } else {
1361
1362         $list = $e->retrieve_container_biblio_record_entry_bucket($list_id);
1363
1364         return Apache2::Const::HTTP_BAD_REQUEST unless 
1365             $list and $list->owner == $e->requestor->id;
1366     }
1367
1368     if($action eq 'delete') {
1369         $success = $U->simplereq('open-ils.actor', 
1370             'open-ils.actor.container.full_delete', $e->authtoken, 'biblio', $list_id);
1371
1372     } elsif($action eq 'show') {
1373         unless($U->is_true($list->pub)) {
1374             $list->pub('t');
1375             $success = $U->simplereq('open-ils.actor', 
1376                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1377         }
1378
1379     } elsif($action eq 'hide') {
1380         if($U->is_true($list->pub)) {
1381             $list->pub('f');
1382             $success = $U->simplereq('open-ils.actor', 
1383                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1384         }
1385
1386     } elsif($action eq 'rename') {
1387         if($name) {
1388             $list->name($name);
1389             $success = $U->simplereq('open-ils.actor', 
1390                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1391         }
1392
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;
1401         }
1402
1403     } elsif($action eq 'del_item') {
1404         foreach (@selected_item) {
1405             $success = $U->simplereq(
1406                 'open-ils.actor',
1407                 'open-ils.actor.container.item.delete', $e->authtoken, 'biblio', $_
1408             );
1409             last unless $success;
1410         }
1411     } elsif ($action eq 'save_notes') {
1412         $success = $self->update_bookbag_item_notes;
1413     }
1414
1415     return $self->generic_redirect($url) if $success;
1416
1417     # XXX FIXME Bucket failure doesn't have a page to show the user anything
1418     # right now. User just sees a 404 currently.
1419
1420     $self->ctx->{bucket_action} = $action;
1421     $self->ctx->{bucket_action_failed} = 1;
1422     return Apache2::Const::OK;
1423 }
1424
1425 sub update_bookbag_item_notes {
1426     my ($self) = @_;
1427     my $e = $self->editor;
1428
1429     my @note_keys = grep /^note-\d+/, keys(%{$self->cgi->Vars});
1430     my @item_keys = grep /^item-\d+/, keys(%{$self->cgi->Vars});
1431
1432     # We're going to leverage an API call that's already been written to check
1433     # permissions appropriately.
1434
1435     my $a = create OpenSRF::AppSession("open-ils.actor");
1436     my $method = "open-ils.actor.container.item_note.cud";
1437
1438     for my $note_key (@note_keys) {
1439         my $note;
1440
1441         my $id = ($note_key =~ /(\d+)/)[0];
1442
1443         if (!($note =
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 " .
1448                 $event->{textcode}
1449             );
1450             $a->kill_me;
1451             $self->ctx->{bucket_action_event} = $event;
1452             return;
1453         }
1454
1455         if (length($self->cgi->param($note_key))) {
1456             $note->ischanged(1);
1457             $note->note($self->cgi->param($note_key));
1458         } else {
1459             $note->isdeleted(1);
1460         }
1461
1462         my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1);
1463
1464         if (defined $U->event_code($r)) {
1465             $self->apache->log->warn(
1466                 "attempt to modify cbrebin " . $note->id .
1467                 " returned event " .  $r->{textcode}
1468             );
1469             $e->rollback;
1470             $a->kill_me;
1471             $self->ctx->{bucket_action_event} = $r;
1472             return;
1473         }
1474     }
1475
1476     for my $item_key (@item_keys) {
1477         my $id = int(($item_key =~ /(\d+)/)[0]);
1478         my $text = $self->cgi->param($item_key);
1479
1480         chomp $text;
1481         next unless length $text;
1482
1483         my $note = new Fieldmapper::container::biblio_record_entry_bucket_item_note;
1484         $note->isnew(1);
1485         $note->item($id);
1486         $note->note($text);
1487
1488         my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1);
1489
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}
1494             );
1495             $e->rollback;
1496             $a->kill_me;
1497             $self->ctx->{bucket_action_event} = $r;
1498             return;
1499         }
1500     }
1501
1502     $a->kill_me;
1503     return 1;   # success
1504 }
1505
1506 sub load_myopac_bookbag_print {
1507     my ($self) = @_;
1508
1509     $self->apache->content_type("text/plain; encoding=utf8");
1510
1511     my $id = int($self->cgi->param("list"));
1512
1513     my ($sorter, $modifier) = $self->_get_bookbag_sort_params;
1514
1515     my $item_search =
1516         $self->_prepare_bookbag_container_query($id, $sorter, $modifier);
1517
1518     my $bbag;
1519
1520     # Get the bookbag object itself, assuming we're allowed to.
1521     if ($self->editor->allowed("VIEW_CONTAINER")) {
1522
1523         $bbag = $self->editor->retrieve_container_biblio_record_entry_bucket($id) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1524     } else {
1525         my $bookbags = $self->editor->search_container_biblio_record_entry_bucket(
1526             {
1527                 "id" => $id,
1528                 "-or" => {
1529                     "owner" => $self->editor->requestor->id,
1530                     "pub" => "t"
1531                 }
1532             }
1533         ) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1534
1535         $bbag = pop @$bookbags;
1536     }
1537
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.
1540     if ($bbag) {
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}
1545         );
1546     }
1547
1548     return Apache2::Const::OK;
1549 }
1550
1551 sub load_password_reset {
1552     my $self = shift;
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];
1561
1562     if ($uuid) {
1563
1564         $logger->info("patron password reset with uuid $uuid");
1565
1566         if ($pwd1 and $pwd2) {
1567
1568             if ($pwd1 eq $pwd2) {
1569
1570                 my $response = $U->simplereq(
1571                     'open-ils.actor', 
1572                     'open-ils.actor.patron.password_reset.commit',
1573                     $uuid, $pwd1);
1574
1575                 $logger->info("patron password reset response " . Dumper($response));
1576
1577                 if ($U->event_code($response)) { # non-success event
1578                     
1579                     my $code = $response->{textcode};
1580                     
1581                     if ($code eq 'PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST') {
1582                         $ctx->{pwreset} = {style => 'error', status => 'NOT_ACTIVE'};
1583                     }
1584
1585                     if ($code eq 'PATRON_PASSWORD_WAS_NOT_STRONG') {
1586                         $ctx->{pwreset} = {style => 'error', status => 'NOT_STRONG'};
1587                     }
1588
1589                 } else { # success
1590
1591                     $ctx->{pwreset} = {style => 'success', status => 'SUCCESS'};
1592                 }
1593
1594             } else { # passwords not equal
1595
1596                 $ctx->{pwreset} = {style => 'error', status => 'NO_MATCH'};
1597             }
1598
1599         } else { # 2 password values needed
1600
1601             $ctx->{pwreset} = {style => 'error', status => 'TWO_PASSWORDS'};
1602         }
1603
1604     } elsif ($barcode or $username) {
1605
1606         my @params = $barcode ? ('barcode', $barcode) : ('username', $username);
1607
1608         $U->simplereq(
1609             'open-ils.actor', 
1610             'open-ils.actor.patron.password_reset.request', @params);
1611
1612         $ctx->{pwreset} = {style => 'plain', status => 'REQUEST_SUCCESS'};
1613     }
1614
1615     $logger->info("patron password reset resulted in " . Dumper($ctx->{pwreset}));
1616     return Apache2::Const::OK;
1617 }
1618
1619 1;