]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
TPac: Default Password Strength Rule
[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     my @payment_xacts = ($self->cgi->param('xact'), $self->cgi->param('xact_misc'));
946     $logger->info("tpac paying fines for xacts @payment_xacts");
947
948     $r = $self->prepare_fines(undef, undef, \@payment_xacts) and return $r;
949
950     # balance_owed is computed specifically from the fines we're trying
951     # to pay in this case.
952     if ($self->ctx->{fines}->{balance_owed} <= 0) {
953         $self->apache->log->info(
954             sprintf("Can't pay non-positive balance. xacts selected: (%s)",
955                 join(", ", map(int, $self->cgi->param("xact"), $self->cgi->param('xact_misc'))))
956         );
957         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
958     }
959
960     my $cc_args = {"where_process" => 1};
961
962     $cc_args->{$_} = $self->cgi->param($_) for (qw/
963         number cvv2 expire_year expire_month billing_first
964         billing_last billing_address billing_city billing_state
965         billing_zip
966     /);
967
968     my $args = {
969         "cc_args" => $cc_args,
970         "userid" => $self->ctx->{user}->id,
971         "payment_type" => "credit_card_payment",
972         "payments" => $self->prepare_fines_for_payment   # should be safe after self->prepare_fines
973     };
974
975     my $resp = $U->simplereq("open-ils.circ", "open-ils.circ.money.payment",
976         $self->editor->authtoken, $args, $self->ctx->{user}->last_xact_id
977     );
978
979     $self->ctx->{"payment_response"} = $resp;
980
981     unless ($resp->{"textcode"}) {
982         $self->ctx->{printable_receipt} = $U->simplereq(
983            "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
984            $self->editor->authtoken, $resp->{payments}
985         );
986     }
987
988     return Apache2::Const::OK;
989 }
990
991 sub load_myopac_receipt_print {
992     my $self = shift;
993
994     $self->ctx->{printable_receipt} = $U->simplereq(
995        "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
996        $self->editor->authtoken, [$self->cgi->param("payment")]
997     );
998
999     return Apache2::Const::OK;
1000 }
1001
1002 sub load_myopac_receipt_email {
1003     my $self = shift;
1004
1005     # The following ML method doesn't actually check whether the user in
1006     # question has an email address, so we do.
1007     if ($self->ctx->{user}->email) {
1008         $self->ctx->{email_receipt_result} = $U->simplereq(
1009            "open-ils.circ", "open-ils.circ.money.payment_receipt.email",
1010            $self->editor->authtoken, [$self->cgi->param("payment")]
1011         );
1012     } else {
1013         $self->ctx->{email_receipt_result} =
1014             new OpenILS::Event("PATRON_NO_EMAIL_ADDRESS");
1015     }
1016
1017     return Apache2::Const::OK;
1018 }
1019
1020 sub prepare_fines {
1021     my ($self, $limit, $offset, $id_list) = @_;
1022
1023     # XXX TODO: check for failure after various network calls
1024
1025     # It may be unclear, but this result structure lumps circulation and
1026     # reservation fines together, and keeps grocery fines separate.
1027     $self->ctx->{"fines"} = {
1028         "circulation" => [],
1029         "grocery" => [],
1030         "total_paid" => 0,
1031         "total_owed" => 0,
1032         "balance_owed" => 0
1033     };
1034
1035     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
1036
1037     # TODO: This should really be a ML call, but the existing calls 
1038     # return an excessive amount of data and don't offer streaming
1039
1040     my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
1041
1042     my $req = $cstore->request(
1043         'open-ils.cstore.direct.money.open_billable_transaction_summary.search',
1044         {
1045             usr => $self->editor->requestor->id,
1046             balance_owed => {'!=' => 0},
1047             ($id_list && @$id_list ? ("id" => $id_list) : ()),
1048         },
1049         {
1050             flesh => 4,
1051             flesh_fields => {
1052                 mobts => [qw/grocery circulation reservation/],
1053                 bresv => ['target_resource_type'],
1054                 brt => ['record'],
1055                 mg => ['billings'],
1056                 mb => ['btype'],
1057                 circ => ['target_copy'],
1058                 acp => ['call_number'],
1059                 acn => ['record']
1060             },
1061             order_by => { mobts => 'xact_start' },
1062             %paging
1063         }
1064     );
1065
1066     my @total_keys = qw/total_paid total_owed balance_owed/;
1067     $self->ctx->{"fines"}->{@total_keys} = (0, 0, 0);
1068
1069     while(my $resp = $req->recv) {
1070         my $mobts = $resp->content;
1071         my $circ = $mobts->circulation;
1072
1073         my $last_billing;
1074         if($mobts->grocery) {
1075             my @billings = sort { $a->billing_ts cmp $b->billing_ts } @{$mobts->grocery->billings};
1076             $last_billing = pop(@billings);
1077         }
1078
1079         # XXX TODO confirm that the following, and the later division by 100.0
1080         # to get a floating point representation once again, is sufficiently
1081         # "money-safe" math.
1082         $self->ctx->{"fines"}->{$_} += int($mobts->$_ * 100) for (@total_keys);
1083
1084         my $marc_xml = undef;
1085         if ($mobts->xact_type eq 'reservation' and
1086             $mobts->reservation->target_resource_type->record) {
1087             $marc_xml = XML::LibXML->new->parse_string(
1088                 $mobts->reservation->target_resource_type->record->marc
1089             );
1090         } elsif ($mobts->xact_type eq 'circulation' and
1091             $circ->target_copy->call_number->id != -1) {
1092             $marc_xml = XML::LibXML->new->parse_string(
1093                 $circ->target_copy->call_number->record->marc
1094             );
1095         }
1096
1097         push(
1098             @{$self->ctx->{"fines"}->{$mobts->grocery ? "grocery" : "circulation"}},
1099             {
1100                 xact => $mobts,
1101                 last_grocery_billing => $last_billing,
1102                 marc_xml => $marc_xml
1103             } 
1104         );
1105     }
1106
1107     $cstore->kill_me;
1108
1109     $self->ctx->{"fines"}->{$_} /= 100.0 for (@total_keys);
1110     return;
1111 }
1112
1113 sub prepare_fines_for_payment {
1114     # This assumes $self->prepare_fines has already been run
1115     my ($self) = @_;
1116
1117     my @results = ();
1118     if ($self->ctx->{fines}) {
1119         push @results, [$_->{xact}->id, $_->{xact}->balance_owed] foreach (
1120             @{$self->ctx->{fines}->{circulation}},
1121             @{$self->ctx->{fines}->{grocery}}
1122         );
1123     }
1124
1125     return \@results;
1126 }
1127
1128 sub load_myopac_main {
1129     my $self = shift;
1130     my $limit = $self->cgi->param('limit') || 0;
1131     my $offset = $self->cgi->param('offset') || 0;
1132
1133     return $self->prepare_fines($limit, $offset) || Apache2::Const::OK;
1134 }
1135
1136 sub load_myopac_update_email {
1137     my $self = shift;
1138     my $e = $self->editor;
1139     my $ctx = $self->ctx;
1140     my $email = $self->cgi->param('email') || '';
1141
1142     # needed for most up-to-date email address
1143     if (my $r = $self->prepare_extended_user_info) { return $r };
1144
1145     return Apache2::Const::OK 
1146         unless $self->cgi->request_method eq 'POST';
1147
1148     unless($email =~ /.+\@.+\..+/) { # TODO better regex?
1149         $ctx->{invalid_email} = $email;
1150         return Apache2::Const::OK;
1151     }
1152
1153     my $stat = $U->simplereq(
1154         'open-ils.actor', 
1155         'open-ils.actor.user.email.update', 
1156         $e->authtoken, $email);
1157
1158     unless ($self->cgi->param("redirect_to")) {
1159         my $url = $self->apache->unparsed_uri;
1160         $url =~ s/update_email/prefs/;
1161
1162         return $self->generic_redirect($url);
1163     }
1164
1165     return $self->generic_redirect;
1166 }
1167
1168 sub load_myopac_update_username {
1169     my $self = shift;
1170     my $e = $self->editor;
1171     my $ctx = $self->ctx;
1172     my $username = $self->cgi->param('username') || '';
1173
1174     return Apache2::Const::OK 
1175         unless $self->cgi->request_method eq 'POST';
1176
1177     unless($username and $username !~ /\s/) { # any other username restrictions?
1178         $ctx->{invalid_username} = $username;
1179         return Apache2::Const::OK;
1180     }
1181
1182     if($username ne $e->requestor->usrname) {
1183
1184         my $evt = $U->simplereq(
1185             'open-ils.actor', 
1186             'open-ils.actor.user.username.update', 
1187             $e->authtoken, $username);
1188
1189         if($U->event_equals($evt, 'USERNAME_EXISTS')) {
1190             $ctx->{username_exists} = $username;
1191             return Apache2::Const::OK;
1192         }
1193     }
1194
1195     my $url = $self->apache->unparsed_uri;
1196     $url =~ s/update_username/prefs/;
1197
1198     return $self->generic_redirect($url);
1199 }
1200
1201 sub load_myopac_update_password {
1202     my $self = shift;
1203     my $e = $self->editor;
1204     my $ctx = $self->ctx;
1205
1206     return Apache2::Const::OK 
1207         unless $self->cgi->request_method eq 'POST';
1208
1209     my $current_pw = $self->cgi->param('current_pw') || '';
1210     my $new_pw = $self->cgi->param('new_pw') || '';
1211     my $new_pw2 = $self->cgi->param('new_pw2') || '';
1212
1213     unless($new_pw eq $new_pw2) {
1214         $ctx->{password_nomatch} = 1;
1215         return Apache2::Const::OK;
1216     }
1217
1218     my $pw_regex = $ctx->{get_org_setting}->($e->requestor->home_ou, 'global.password_regex');
1219
1220     if(!$pw_regex) {
1221         # This regex duplicates the JSPac's default "digit, letter, and 7 characters" rule
1222         $pw_regex = '(?=.*\d+.*)(?=.*[A-Za-z]+.*).{7,}';
1223     }
1224
1225     if($pw_regex and $new_pw !~ /$pw_regex/) {
1226         $ctx->{password_invalid} = 1;
1227         return Apache2::Const::OK;
1228     }
1229
1230     my $evt = $U->simplereq(
1231         'open-ils.actor', 
1232         'open-ils.actor.user.password.update', 
1233         $e->authtoken, $new_pw, $current_pw);
1234
1235
1236     if($U->event_equals($evt, 'INCORRECT_PASSWORD')) {
1237         $ctx->{password_incorrect} = 1;
1238         return Apache2::Const::OK;
1239     }
1240
1241     my $url = $self->apache->unparsed_uri;
1242     $url =~ s/update_password/prefs/;
1243
1244     return $self->generic_redirect($url);
1245 }
1246
1247 sub load_myopac_bookbags {
1248     my $self = shift;
1249     my $e = $self->editor;
1250     my $ctx = $self->ctx;
1251
1252     my ($sorter, $modifier) = $self->_get_bookbag_sort_params;
1253     $e->xact_begin; # replication...
1254
1255     my $rv = $self->load_mylist;
1256     unless($rv eq Apache2::Const::OK) {
1257         $e->rollback;
1258         return $rv;
1259     }
1260
1261     $ctx->{bookbags} = $e->search_container_biblio_record_entry_bucket(
1262         [
1263             {owner => $e->requestor->id, btype => 'bookbag'}, {
1264                 order_by => {cbreb => 'name'},
1265                 limit => $self->cgi->param('limit') || 10,
1266                 offset => $self->cgi->param('offset') || 0
1267             }
1268         ],
1269         {substream => 1}
1270     );
1271
1272     if(!$ctx->{bookbags}) {
1273         $e->rollback;
1274         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1275     }
1276     
1277     # Here is the loop that uses search to find the bib records in each
1278     # bookbag.  XXX This should be parallelized.  Should this be done
1279     # with OpenSRF::MultiSession, or is it enough to use OpenSRF::AppSession
1280     # and call ->request() without calling ->gather() on any of those objects
1281     # until all the requests have been issued?
1282
1283     foreach my $bookbag (@{$ctx->{bookbags}}) {
1284         my $query = $self->_prepare_bookbag_container_query(
1285             $bookbag->id, $sorter, $modifier
1286         );
1287
1288         # XXX we need to limit the number of records per bbag; use third arg
1289         # of bib_container_items_via_search() i think.
1290         my $items = $U->bib_container_items_via_search($bookbag->id, $query)
1291             or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1292
1293         # Maybe save a little memory by creating only one XML::LibXML::Document
1294         # instance for each record, even if record is repeated across bookbags.
1295
1296         foreach my $rec (map { $_->target_biblio_record_entry } @$items) {
1297             next if $ctx->{bookbags_marc_xml}{$rec->id};
1298             $ctx->{bookbags_marc_xml}{$rec->id} =
1299                 (new XML::LibXML)->parse_string($rec->marc);
1300         }
1301
1302         $bookbag->items($items);
1303     }
1304
1305     $e->rollback;
1306     return Apache2::Const::OK;
1307 }
1308
1309
1310 # actions are create, delete, show, hide, rename, add_rec, delete_item, place_hold
1311 # CGI is action, list=list_id, add_rec/record=bre_id, del_item=bucket_item_id, name=new_bucket_name
1312 sub load_myopac_bookbag_update {
1313     my ($self, $action, $list_id, @hold_recs) = @_;
1314     my $e = $self->editor;
1315     my $cgi = $self->cgi;
1316
1317     # save_notes is effectively another action, but is passed in a separate
1318     # CGI parameter for what are really just layout reasons.
1319     $action = 'save_notes' if $cgi->param('save_notes');
1320     $action ||= $cgi->param('action');
1321
1322     $list_id ||= $cgi->param('list');
1323
1324     my @add_rec = $cgi->param('add_rec') || $cgi->param('record');
1325     my @selected_item = $cgi->param('selected_item');
1326     my $shared = $cgi->param('shared');
1327     my $name = $cgi->param('name');
1328     my $description = $cgi->param('description');
1329     my $success = 0;
1330     my $list;
1331
1332     # This url intentionally leaves off the edit_notes parameter, but
1333     # may need to add some back in for paging.
1334
1335     my $url = "https://" . $self->apache->hostname .
1336         $self->ctx->{opac_root} . "/myopac/lists?";
1337
1338     $url .= 'sort=' . uri_escape($cgi->param("sort")) if $cgi->param("sort");
1339
1340     if ($action eq 'create') {
1341         $list = Fieldmapper::container::biblio_record_entry_bucket->new;
1342         $list->name($name);
1343         $list->description($description);
1344         $list->owner($e->requestor->id);
1345         $list->btype('bookbag');
1346         $list->pub($shared ? 't' : 'f');
1347         $success = $U->simplereq('open-ils.actor', 
1348             'open-ils.actor.container.create', $e->authtoken, 'biblio', $list)
1349
1350     } elsif($action eq 'place_hold') {
1351
1352         # @hold_recs comes from anon lists redirect; selected_itesm comes from existing buckets
1353         unless (@hold_recs) {
1354             if (@selected_item) {
1355                 my $items = $e->search_container_biblio_record_entry_bucket_item({id => \@selected_item});
1356                 @hold_recs = map { $_->target_biblio_record_entry } @$items;
1357             }
1358         }
1359                 
1360         return Apache2::Const::OK unless @hold_recs;
1361         $logger->info("placing holds from list page on: @hold_recs");
1362
1363         my $url = $self->ctx->{opac_root} . '/place_hold?hold_type=T';
1364         $url .= ';hold_target=' . $_ for @hold_recs;
1365         return $self->generic_redirect($url);
1366
1367     } else {
1368
1369         $list = $e->retrieve_container_biblio_record_entry_bucket($list_id);
1370
1371         return Apache2::Const::HTTP_BAD_REQUEST unless 
1372             $list and $list->owner == $e->requestor->id;
1373     }
1374
1375     if($action eq 'delete') {
1376         $success = $U->simplereq('open-ils.actor', 
1377             'open-ils.actor.container.full_delete', $e->authtoken, 'biblio', $list_id);
1378
1379     } elsif($action eq 'show') {
1380         unless($U->is_true($list->pub)) {
1381             $list->pub('t');
1382             $success = $U->simplereq('open-ils.actor', 
1383                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1384         }
1385
1386     } elsif($action eq 'hide') {
1387         if($U->is_true($list->pub)) {
1388             $list->pub('f');
1389             $success = $U->simplereq('open-ils.actor', 
1390                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1391         }
1392
1393     } elsif($action eq 'rename') {
1394         if($name) {
1395             $list->name($name);
1396             $success = $U->simplereq('open-ils.actor', 
1397                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1398         }
1399
1400     } elsif($action eq 'add_rec') {
1401         foreach my $add_rec (@add_rec) {
1402             my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1403             $item->bucket($list_id);
1404             $item->target_biblio_record_entry($add_rec);
1405             $success = $U->simplereq('open-ils.actor', 
1406                 'open-ils.actor.container.item.create', $e->authtoken, 'biblio', $item);
1407             last unless $success;
1408         }
1409
1410     } elsif($action eq 'del_item') {
1411         foreach (@selected_item) {
1412             $success = $U->simplereq(
1413                 'open-ils.actor',
1414                 'open-ils.actor.container.item.delete', $e->authtoken, 'biblio', $_
1415             );
1416             last unless $success;
1417         }
1418     } elsif ($action eq 'save_notes') {
1419         $success = $self->update_bookbag_item_notes;
1420     }
1421
1422     return $self->generic_redirect($url) if $success;
1423
1424     # XXX FIXME Bucket failure doesn't have a page to show the user anything
1425     # right now. User just sees a 404 currently.
1426
1427     $self->ctx->{bucket_action} = $action;
1428     $self->ctx->{bucket_action_failed} = 1;
1429     return Apache2::Const::OK;
1430 }
1431
1432 sub update_bookbag_item_notes {
1433     my ($self) = @_;
1434     my $e = $self->editor;
1435
1436     my @note_keys = grep /^note-\d+/, keys(%{$self->cgi->Vars});
1437     my @item_keys = grep /^item-\d+/, keys(%{$self->cgi->Vars});
1438
1439     # We're going to leverage an API call that's already been written to check
1440     # permissions appropriately.
1441
1442     my $a = create OpenSRF::AppSession("open-ils.actor");
1443     my $method = "open-ils.actor.container.item_note.cud";
1444
1445     for my $note_key (@note_keys) {
1446         my $note;
1447
1448         my $id = ($note_key =~ /(\d+)/)[0];
1449
1450         if (!($note =
1451             $e->retrieve_container_biblio_record_entry_bucket_item_note($id))) {
1452             my $event = $e->die_event;
1453             $self->apache->log->warn(
1454                 "error retrieving cbrebin id $id, got event " .
1455                 $event->{textcode}
1456             );
1457             $a->kill_me;
1458             $self->ctx->{bucket_action_event} = $event;
1459             return;
1460         }
1461
1462         if (length($self->cgi->param($note_key))) {
1463             $note->ischanged(1);
1464             $note->note($self->cgi->param($note_key));
1465         } else {
1466             $note->isdeleted(1);
1467         }
1468
1469         my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1);
1470
1471         if (defined $U->event_code($r)) {
1472             $self->apache->log->warn(
1473                 "attempt to modify cbrebin " . $note->id .
1474                 " returned event " .  $r->{textcode}
1475             );
1476             $e->rollback;
1477             $a->kill_me;
1478             $self->ctx->{bucket_action_event} = $r;
1479             return;
1480         }
1481     }
1482
1483     for my $item_key (@item_keys) {
1484         my $id = int(($item_key =~ /(\d+)/)[0]);
1485         my $text = $self->cgi->param($item_key);
1486
1487         chomp $text;
1488         next unless length $text;
1489
1490         my $note = new Fieldmapper::container::biblio_record_entry_bucket_item_note;
1491         $note->isnew(1);
1492         $note->item($id);
1493         $note->note($text);
1494
1495         my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1);
1496
1497         if (defined $U->event_code($r)) {
1498             $self->apache->log->warn(
1499                 "attempt to create cbrebin for item " . $note->item .
1500                 " returned event " .  $r->{textcode}
1501             );
1502             $e->rollback;
1503             $a->kill_me;
1504             $self->ctx->{bucket_action_event} = $r;
1505             return;
1506         }
1507     }
1508
1509     $a->kill_me;
1510     return 1;   # success
1511 }
1512
1513 sub load_myopac_bookbag_print {
1514     my ($self) = @_;
1515
1516     $self->apache->content_type("text/plain; encoding=utf8");
1517
1518     my $id = int($self->cgi->param("list"));
1519
1520     my ($sorter, $modifier) = $self->_get_bookbag_sort_params;
1521
1522     my $item_search =
1523         $self->_prepare_bookbag_container_query($id, $sorter, $modifier);
1524
1525     my $bbag;
1526
1527     # Get the bookbag object itself, assuming we're allowed to.
1528     if ($self->editor->allowed("VIEW_CONTAINER")) {
1529
1530         $bbag = $self->editor->retrieve_container_biblio_record_entry_bucket($id) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1531     } else {
1532         my $bookbags = $self->editor->search_container_biblio_record_entry_bucket(
1533             {
1534                 "id" => $id,
1535                 "-or" => {
1536                     "owner" => $self->editor->requestor->id,
1537                     "pub" => "t"
1538                 }
1539             }
1540         ) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1541
1542         $bbag = pop @$bookbags;
1543     }
1544
1545     # If we have a bookbag we're allowed to look at, issue the A/T event
1546     # to get CSV, passing as a user param that search query we built before.
1547     if ($bbag) {
1548         $self->ctx->{csv} = $U->fire_object_event(
1549             undef, "container.biblio_record_entry_bucket.csv",
1550             $bbag, $self->editor->requestor->home_ou,
1551             undef, {"item_search" => $item_search}
1552         );
1553     }
1554
1555     return Apache2::Const::OK;
1556 }
1557
1558 sub load_password_reset {
1559     my $self = shift;
1560     my $cgi = $self->cgi;
1561     my $ctx = $self->ctx;
1562     my $barcode = $cgi->param('barcode');
1563     my $username = $cgi->param('username');
1564     my $email = $cgi->param('email');
1565     my $pwd1 = $cgi->param('pwd1');
1566     my $pwd2 = $cgi->param('pwd2');
1567     my $uuid = $ctx->{page_args}->[0];
1568
1569     if ($uuid) {
1570
1571         $logger->info("patron password reset with uuid $uuid");
1572
1573         if ($pwd1 and $pwd2) {
1574
1575             if ($pwd1 eq $pwd2) {
1576
1577                 my $response = $U->simplereq(
1578                     'open-ils.actor', 
1579                     'open-ils.actor.patron.password_reset.commit',
1580                     $uuid, $pwd1);
1581
1582                 $logger->info("patron password reset response " . Dumper($response));
1583
1584                 if ($U->event_code($response)) { # non-success event
1585                     
1586                     my $code = $response->{textcode};
1587                     
1588                     if ($code eq 'PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST') {
1589                         $ctx->{pwreset} = {style => 'error', status => 'NOT_ACTIVE'};
1590                     }
1591
1592                     if ($code eq 'PATRON_PASSWORD_WAS_NOT_STRONG') {
1593                         $ctx->{pwreset} = {style => 'error', status => 'NOT_STRONG'};
1594                     }
1595
1596                 } else { # success
1597
1598                     $ctx->{pwreset} = {style => 'success', status => 'SUCCESS'};
1599                 }
1600
1601             } else { # passwords not equal
1602
1603                 $ctx->{pwreset} = {style => 'error', status => 'NO_MATCH'};
1604             }
1605
1606         } else { # 2 password values needed
1607
1608             $ctx->{pwreset} = {status => 'TWO_PASSWORDS'};
1609         }
1610
1611     } elsif ($barcode or $username) {
1612
1613         my @params = $barcode ? ('barcode', $barcode) : ('username', $username);
1614
1615         $U->simplereq(
1616             'open-ils.actor', 
1617             'open-ils.actor.patron.password_reset.request', @params);
1618
1619         $ctx->{pwreset} = {status => 'REQUEST_SUCCESS'};
1620     }
1621
1622     $logger->info("patron password reset resulted in " . Dumper($ctx->{pwreset}));
1623     return Apache2::Const::OK;
1624 }
1625
1626 1;