]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
TPac: One prefers to download CSV, not to display it
[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     my $lock_usernames = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.lock_usernames');
95     if($lock_usernames == 1) {
96         # Policy says no username changes
97         $self->ctx->{username_change_disallowed} = 1;
98     } else {
99         my $username_unlimit = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.unlimit_usernames');
100         if($username_unlimit != 1) {
101             my $regex_check = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.barcode_regex');
102             if(!$regex_check) {
103                 # Default is "starts with a number"
104                 $regex_check = '^\d+';
105             }
106             # You already have a username?
107             if($regex_check and $self->ctx->{user}->usrname !~ /$regex_check/) {
108                 $self->ctx->{username_change_disallowed} = 1;
109             }
110         }
111     }
112
113     return Apache2::Const::OK unless 
114         $pending_addr or $replace_addr or $delete_pending;
115
116     my @form_fields = qw/address_type street1 street2 city county state country post_code/;
117
118     my $paddr;
119     if( $pending_addr ) { # update an existing pending address
120
121         ($paddr) = grep { $_->id == $pending_addr } @{$user->addresses};
122         return Apache2::Const::HTTP_BAD_REQUEST unless $paddr;
123         $paddr->$_( $cgi->param($_) ) for @form_fields;
124
125     } elsif( $replace_addr ) { # create a new pending address for 'replace_addr'
126
127         $paddr = Fieldmapper::actor::user_address->new;
128         $paddr->isnew(1);
129         $paddr->usr($user->id);
130         $paddr->pending('t');
131         $paddr->replaces($replace_addr);
132         $paddr->$_( $cgi->param($_) ) for @form_fields;
133
134     } elsif( $delete_pending ) {
135         $paddr = $e->retrieve_actor_user_address($delete_pending);
136         return Apache2::Const::HTTP_BAD_REQUEST unless 
137             $paddr and $paddr->usr == $user->id and $U->is_true($paddr->pending);
138         $paddr->isdeleted(1);
139     }
140
141     my $resp = $U->simplereq(
142         'open-ils.actor', 
143         'open-ils.actor.user.address.pending.cud',
144         $e->authtoken, $paddr);
145
146     if( $U->event_code($resp) ) {
147         $logger->error("Error updating pending address: $resp");
148         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
149     }
150
151     # in light of these changes, re-fetch latest data
152     $e->xact_begin; 
153     $self->prepare_extended_user_info;
154     $e->rollback;
155
156     return Apache2::Const::OK;
157 }
158
159 sub load_myopac_prefs_notify {
160     my $self = shift;
161     my $e = $self->editor;
162
163     my $user_prefs = $self->fetch_optin_prefs;
164     $user_prefs = $self->update_optin_prefs($user_prefs)
165         if $self->cgi->request_method eq 'POST';
166
167     $self->ctx->{opt_in_settings} = $user_prefs; 
168
169     return Apache2::Const::OK;
170 }
171
172 sub fetch_optin_prefs {
173     my $self = shift;
174     my $e = $self->editor;
175
176     # fetch all of the opt-in settings the user has access to
177     # XXX: user's should in theory have options to opt-in to notices
178     # for remote locations, but that opens the door for a large
179     # set of generally un-used opt-ins.. needs discussion
180     my $opt_ins =  $U->simplereq(
181         'open-ils.actor',
182         'open-ils.actor.event_def.opt_in.settings.atomic',
183         $e->authtoken, $e->requestor->home_ou);
184
185     # some opt-ins are staff-only
186     $opt_ins = [ grep { $U->is_true($_->opac_visible) } @$opt_ins ];
187
188     # fetch user setting values for each of the opt-in settings
189     my $user_set = $U->simplereq(
190         'open-ils.actor',
191         'open-ils.actor.patron.settings.retrieve',
192         $e->authtoken, 
193         $e->requestor->id, 
194         [map {$_->name} @$opt_ins]
195     );
196
197     return [map { {cust => $_, value => $user_set->{$_->name} } } @$opt_ins];
198 }
199
200 sub update_optin_prefs {
201     my $self = shift;
202     my $user_prefs = shift;
203     my $e = $self->editor;
204     my @settings = $self->cgi->param('setting');
205     my %newsets;
206
207     # apply now-true settings
208     for my $applied (@settings) {
209         # see if setting is already applied to this user
210         next if grep { $_->{cust}->name eq $applied and $_->{value} } @$user_prefs;
211         $newsets{$applied} = OpenSRF::Utils::JSON->true;
212     }
213
214     # remove now-false settings
215     for my $pref (grep { $_->{value} } @$user_prefs) {
216         $newsets{$pref->{cust}->name} = undef 
217             unless grep { $_ eq $pref->{cust}->name } @settings;
218     }
219
220     $U->simplereq(
221         'open-ils.actor',
222         'open-ils.actor.patron.settings.update',
223         $e->authtoken, $e->requestor->id, \%newsets);
224
225     # update the local prefs to match reality
226     for my $pref (@$user_prefs) {
227         $pref->{value} = $newsets{$pref->{cust}->name} 
228             if exists $newsets{$pref->{cust}->name};
229     }
230
231     return $user_prefs;
232 }
233
234 sub _load_user_with_prefs {
235     my $self = shift;
236     my $stat = $self->prepare_extended_user_info('settings');
237     return $stat if $stat; # not-OK
238
239     $self->ctx->{user_setting_map} = {
240         map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) } 
241             @{$self->ctx->{user}->settings}
242     };
243
244     return undef;
245 }
246
247 sub _get_bookbag_sort_params {
248     my ($self) = @_;
249
250     # The interface that feeds this cgi parameter will provide a single
251     # argument for a QP sort filter, and potentially a modifier after a period.
252     # In practice this means the "sort" parameter will be something like
253     # "titlesort" or "authorsort.descending".
254     my $sorter = $self->cgi->param("sort") || "";
255     my $modifier;
256     if ($sorter) {
257         $sorter =~ s/^(.*?)\.(.*)/$1/;
258         $modifier = $2 || undef;
259     }
260
261     return ($sorter, $modifier);
262 }
263
264 sub _prepare_bookbag_container_query {
265     my ($self, $container_id, $sorter, $modifier) = @_;
266
267     return sprintf(
268         "container(bre,bookbag,%d,%s)%s%s",
269         $container_id, $self->editor->authtoken,
270         ($sorter ? " sort($sorter)" : ""),
271         ($modifier ? "#$modifier" : "")
272     );
273 }
274
275 sub load_myopac_prefs_settings {
276     my $self = shift;
277
278     my $stat = $self->_load_user_with_prefs;
279     return $stat if $stat;
280
281     return Apache2::Const::OK
282         unless $self->cgi->request_method eq 'POST';
283
284     # some setting values from the form don't match the 
285     # required value/format for the db, so they have to be 
286     # individually translated.
287
288     my %settings;
289     my $set_map = $self->ctx->{user_setting_map};
290
291     my $key = 'opac.hits_per_page';
292     my $val = $self->cgi->param($key);
293     $settings{$key}= $val unless $$set_map{$key} eq $val;
294
295     my $now = DateTime->now->strftime('%F');
296     for $key (qw/history.circ.retention_start history.hold.retention_start/) {
297         $val = $self->cgi->param($key);
298         if($val and $val eq 'on') {
299             # Set the start time to 'now' unless a start time already exists for the user
300             $settings{$key} = $now unless $$set_map{$key};
301         } else {
302             # clear the start time if one previously existed for the user
303             $settings{$key} = undef if $$set_map{$key};
304         }
305     }
306     
307     # Send the modified settings off to be saved
308     $U->simplereq(
309         'open-ils.actor', 
310         'open-ils.actor.patron.settings.update',
311         $self->editor->authtoken, undef, \%settings);
312
313     # re-fetch user prefs 
314     $self->ctx->{updated_user_settings} = \%settings;
315     return $self->_load_user_with_prefs || Apache2::Const::OK;
316 }
317
318 sub fetch_user_holds {
319     my $self = shift;
320     my $hold_ids = shift;
321     my $ids_only = shift;
322     my $flesh = shift;
323     my $available = shift;
324     my $limit = shift;
325     my $offset = shift;
326
327     my $e = $self->editor;
328
329     if(!$hold_ids) {
330         my $circ = OpenSRF::AppSession->create('open-ils.circ');
331
332         $hold_ids = $circ->request(
333             'open-ils.circ.holds.id_list.retrieve.authoritative', 
334             $e->authtoken, 
335             $e->requestor->id
336         )->gather(1);
337         $circ->kill_me;
338     
339         $hold_ids = [ grep { defined $_ } @$hold_ids[$offset..($offset + $limit - 1)] ] if $limit or $offset;
340     }
341
342
343     return $hold_ids if $ids_only or @$hold_ids == 0;
344
345     my $args = {
346         suppress_notices => 1,
347         suppress_transits => 1,
348         suppress_mvr => 1,
349         suppress_patron_details => 1,
350         include_bre => $flesh ? 1 : 0
351     };
352
353     # ----------------------------------------------------------------
354     # Collect holds in batches of $batch_size for faster retrieval
355
356     my $batch_size = 8;
357     my $batch_idx = 0;
358     my $mk_req_batch = sub {
359         my @ses;
360         my $top_idx = $batch_idx + $batch_size;
361         while($batch_idx < $top_idx) {
362             my $hold_id = $hold_ids->[$batch_idx++];
363             last unless $hold_id;
364             my $ses = OpenSRF::AppSession->create('open-ils.circ');
365             my $req = $ses->request(
366                 'open-ils.circ.hold.details.retrieve', 
367                 $e->authtoken, $hold_id, $args);
368             push(@ses, {ses => $ses, req => $req});
369         }
370         return @ses;
371     };
372
373     my $first = 1;
374     my(@collected, @holds, @ses);
375
376     while(1) {
377         @ses = $mk_req_batch->() if $first;
378         last if $first and not @ses;
379
380         if(@collected) {
381             # If desired by the caller, filter any holds that are not available.
382             if ($available) {
383                 @collected = grep { $_->{hold}->{status} == 4 } @collected;
384             }
385             while(my $blob = pop(@collected)) {
386                 $blob->{marc_xml} = XML::LibXML->new->parse_string($blob->{hold}->{bre}->marc) if $flesh;
387                 push(@holds, $blob);
388             }
389         }
390
391         for my $req_data (@ses) {
392             push(@collected, {hold => $req_data->{req}->gather(1)});
393             $req_data->{ses}->kill_me;
394         }
395
396         @ses = $mk_req_batch->();
397         last unless @collected or @ses;
398         $first = 0;
399     }
400
401     # put the holds back into the original server sort order
402     my @sorted;
403     for my $id (@$hold_ids) {
404         push @sorted, grep { $_->{hold}->{hold}->id == $id } @holds;
405     }
406
407     return \@sorted;
408 }
409
410 sub handle_hold_update {
411     my $self = shift;
412     my $action = shift;
413     my $hold_ids = shift;
414     my $e = $self->editor;
415     my $url;
416
417     my @hold_ids = ($hold_ids) ? @$hold_ids : $self->cgi->param('hold_id'); # for non-_all actions
418     @hold_ids = @{$self->fetch_user_holds(undef, 1)} if $action =~ /_all/;
419
420     my $circ = OpenSRF::AppSession->create('open-ils.circ');
421
422     if($action =~ /cancel/) {
423
424         for my $hold_id (@hold_ids) {
425             my $resp = $circ->request(
426                 'open-ils.circ.hold.cancel', $e->authtoken, $hold_id, 6 )->gather(1); # 6 == patron-cancelled-via-opac
427         }
428
429     } elsif ($action =~ /activate|suspend/) {
430         
431         my $vlist = [];
432         for my $hold_id (@hold_ids) {
433             my $vals = {id => $hold_id};
434
435             if($action =~ /activate/) {
436                 $vals->{frozen} = 'f';
437                 $vals->{thaw_date} = undef;
438
439             } elsif($action =~ /suspend/) {
440                 $vals->{frozen} = 't';
441                 # $vals->{thaw_date} = TODO;
442             }
443             push(@$vlist, $vals);
444         }
445
446         $circ->request('open-ils.circ.hold.update.batch.atomic', $e->authtoken, undef, $vlist)->gather(1);
447     } elsif ($action eq 'edit') {
448
449         my @vals = map {
450             my $val = {"id" => $_};
451             $val->{"frozen"} = $self->cgi->param("frozen");
452             $val->{"pickup_lib"} = $self->cgi->param("pickup_lib");
453
454             for my $field (qw/expire_time thaw_date/) {
455                 # XXX TODO make this support other date formats, not just
456                 # MM/DD/YYYY.
457                 next unless $self->cgi->param($field) =~
458                     m:^(\d{2})/(\d{2})/(\d{4})$:;
459                 $val->{$field} = "$3-$1-$2";
460             }
461             $val;
462         } @hold_ids;
463
464         $circ->request(
465             'open-ils.circ.hold.update.batch.atomic',
466             $e->authtoken, undef, \@vals
467         )->gather(1);   # LFW XXX test for failure
468         $url = 'https://' . $self->apache->hostname . $self->ctx->{opac_root} . '/myopac/holds';
469     }
470
471     $circ->kill_me;
472     return defined($url) ? $self->generic_redirect($url) : undef;
473 }
474
475 sub load_myopac_holds {
476     my $self = shift;
477     my $e = $self->editor;
478     my $ctx = $self->ctx;
479     
480     my $limit = $self->cgi->param('limit') || 0;
481     my $offset = $self->cgi->param('offset') || 0;
482     my $action = $self->cgi->param('action') || '';
483     my $hold_id = $self->cgi->param('id');
484     my $available = int($self->cgi->param('available') || 0);
485
486     my $hold_handle_result;
487     $hold_handle_result = $self->handle_hold_update($action) if $action;
488
489     $ctx->{holds} = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available, $limit, $offset);
490
491     return defined($hold_handle_result) ? $hold_handle_result : Apache2::Const::OK;
492 }
493
494 sub load_place_hold {
495     my $self = shift;
496     my $ctx = $self->ctx;
497     my $gos = $ctx->{get_org_setting};
498     my $e = $self->editor;
499     my $cgi = $self->cgi;
500
501     $self->ctx->{page} = 'place_hold';
502     my @targets = $cgi->param('hold_target');
503     $ctx->{hold_type} = $cgi->param('hold_type');
504     $ctx->{default_pickup_lib} = $e->requestor->home_ou; # unless changed below
505
506     return $self->post_hold_redirect unless @targets;
507
508     $logger->info("Looking at hold targets: @targets");
509
510     # if the staff client provides a patron barcode, fetch the patron
511     if (my $bc = $self->cgi->cookie("patron_barcode")) {
512         $ctx->{patron_recipient} = $U->simplereq(
513             "open-ils.actor", "open-ils.actor.user.fleshed.retrieve_by_barcode",
514             $self->editor->authtoken, $bc
515         ) or return Apache2::Const::HTTP_BAD_REQUEST;
516
517         $ctx->{default_pickup_lib} = $ctx->{patron_recipient}->home_ou;
518     }
519
520     my $request_lib = $e->requestor->ws_ou;
521     my @hold_data;
522     $ctx->{hold_data} = \@hold_data;
523
524     my $type_dispatch = {
525         T => sub {
526             my $recs = $e->batch_retrieve_biblio_record_entry(\@targets, {substream => 1});
527             for my $id (@targets) { # force back into the correct order
528                 my ($rec) = grep {$_->id eq $id} @$recs;
529                 push(@hold_data, {target => $rec, record => $rec});
530             }
531         },
532         V => sub {
533             my $vols = $e->batch_retrieve_asset_call_number([
534                 \@targets, {
535                     "flesh" => 1,
536                     "flesh_fields" => {"acn" => ["record"]}
537                 }
538             ], {substream => 1});
539
540             for my $id (@targets) { 
541                 my ($vol) = grep {$_->id eq $id} @$vols;
542                 push(@hold_data, {target => $vol, record => $vol->record});
543             }
544         },
545         C => sub {
546             my $copies = $e->batch_retrieve_asset_copy([
547                 \@targets, {
548                     "flesh" => 2,
549                     "flesh_fields" => {
550                         "acn" => ["record"],
551                         "acp" => ["call_number"]
552                     }
553                 }
554             ], {substream => 1});
555
556             for my $id (@targets) { 
557                 my ($copy) = grep {$_->id eq $id} @$copies;
558                 push(@hold_data, {target => $copy, record => $copy->call_number->record});
559             }
560         },
561         I => sub {
562             my $isses = $e->batch_retrieve_serial_issuance([
563                 \@targets, {
564                     "flesh" => 2,
565                     "flesh_fields" => {
566                         "siss" => ["subscription"], "ssub" => ["record_entry"]
567                     }
568                 }
569             ], {substream => 1});
570
571             for my $id (@targets) { 
572                 my ($iss) = grep {$_->id eq $id} @$isses;
573                 push(@hold_data, {target => $iss, record => $iss->subscription->record_entry});
574             }
575         }
576         # ...
577
578     }->{$ctx->{hold_type}}->();
579
580     # caller sent bad target IDs or the wrong hold type
581     return Apache2::Const::HTTP_BAD_REQUEST unless @hold_data;
582
583     # generate the MARC xml for each record
584     $_->{marc_xml} = XML::LibXML->new->parse_string($_->{record}->marc) for @hold_data;
585
586     my $pickup_lib = $cgi->param('pickup_lib');
587     # no pickup lib means no holds placement
588     return Apache2::Const::OK unless $pickup_lib;
589
590     $ctx->{hold_attempt_made} = 1;
591
592     # Give the original CGI params back to the user in case they
593     # want to try to override something.
594     $ctx->{orig_params} = $cgi->Vars;
595     delete $ctx->{orig_params}{submit};
596     delete $ctx->{orig_params}{hold_target};
597
598     my $usr = $e->requestor->id;
599
600     if ($ctx->{is_staff} and !$cgi->param("hold_usr_is_requestor")) {
601         # find the real hold target
602
603         $usr = $U->simplereq(
604             'open-ils.actor', 
605             "open-ils.actor.user.retrieve_id_by_barcode_or_username",
606             $e->authtoken, $cgi->param("hold_usr"));
607
608         if (defined $U->event_code($usr)) {
609             $ctx->{hold_failed} = 1;
610             $ctx->{hold_failed_event} = $usr;
611         }
612     }
613
614     # First see if we should warn/block for any holds that 
615     # might have locally available items.
616     for my $hdata (@hold_data) {
617         my ($local_block, $local_alert) = $self->local_avail_concern(
618             $hdata->{target}->id, $ctx->{hold_type}, $pickup_lib);
619     
620         if ($local_block) {
621             $hdata->{hold_failed} = 1;
622             $hdata->{hold_local_block} = 1;
623         } elsif ($local_alert) {
624             $hdata->{hold_failed} = 1;
625             $hdata->{hold_local_alert} = 1;
626         }
627     }
628
629
630     my $method = 'open-ils.circ.holds.test_and_create.batch';
631     $method .= '.override' if $cgi->param('override');
632
633     my @create_targets = map {$_->{target}->id} (grep { !$_->{hold_failed} } @hold_data);
634
635     if(@create_targets) {
636
637         my $bses = OpenSRF::AppSession->create('open-ils.circ');
638         my $breq = $bses->request( 
639             $method, 
640             $e->authtoken, 
641             {   patronid => $usr, 
642                 pickup_lib => $pickup_lib, 
643                 hold_type => $ctx->{hold_type}
644             }, 
645             \@create_targets
646         );
647
648         while (my $resp = $breq->recv) {
649
650             $resp = $resp->content;
651             $logger->info('batch hold placement result: ' . OpenSRF::Utils::JSON->perl2JSON($resp));
652
653             if ($U->event_code($resp)) {
654                 $ctx->{general_hold_error} = $resp;
655                 last;
656             }
657
658             my ($hdata) = grep {$_->{target}->id eq $resp->{target}} @hold_data;
659             my $result = $resp->{result};
660
661             if ($U->event_code($result)) {
662                 # e.g. permission denied
663                 $hdata->{hold_failed} = 1;
664                 $hdata->{hold_failed_event} = $result;
665
666             } else {
667                 
668                 if(not ref $result and $result > 0) {
669                     # successul hold returns the hold ID
670
671                     $hdata->{hold_success} = $result; 
672     
673                 } else {
674                     # hold-specific failure event 
675                     $hdata->{hold_failed} = 1;
676                     $hdata->{hold_failed_event} = $result->{last_event};
677                     $hdata->{could_override} = $self->test_could_override($hdata->{hold_failed_event});
678                 }
679             }
680         }
681
682         $bses->kill_me;
683     }
684
685     # stay on the current page and display the results
686     return Apache2::Const::OK if 
687         (grep {$_->{hold_failed}} @hold_data) or $ctx->{general_hold_error};
688
689     # if successful, do some cleanup and return the 
690     # user to the requesting page.
691
692     return $self->post_hold_redirect;
693 }
694
695 sub post_hold_redirect {
696     my $self = shift;
697     
698     # XXX: Leave the barcode cookie in place.  Otherwise, it's not 
699     # possible to place more than one hold for the patron within 
700     # a staff/patron session.  This does leave the barcode to linger 
701     # longer than is ideal, but normal staff work flow will cause the 
702     # cookie to be replaced with each new patron anyway.
703     # TODO:  See about getting the staff client to clear the cookie
704     return $self->generic_redirect;
705
706     # We also clear the patron_barcode (from the staff client)
707     # cookie at this point (otherwise it haunts the staff user
708     # later). XXX todo make sure this is best; also see that
709     # template when staff mode calls xulG.opac_hold_placed()
710
711     return $self->generic_redirect(
712         undef,
713         $self->cgi->cookie(
714             -name => "patron_barcode",
715             -path => "/",
716             -secure => 1,
717             -value => "",
718             -expires => "-1h"
719         )
720     );
721 }
722
723
724 sub fetch_user_circs {
725     my $self = shift;
726     my $flesh = shift; # flesh bib data, etc.
727     my $circ_ids = shift;
728     my $limit = shift;
729     my $offset = shift;
730
731     my $e = $self->editor;
732
733     my @circ_ids;
734
735     if($circ_ids) {
736         @circ_ids = @$circ_ids;
737
738     } else {
739
740         my $circ_data = $U->simplereq(
741             'open-ils.actor', 
742             'open-ils.actor.user.checked_out',
743             $e->authtoken, 
744             $e->requestor->id
745         );
746
747         @circ_ids =  ( @{$circ_data->{overdue}}, @{$circ_data->{out}} );
748
749         if($limit or $offset) {
750             @circ_ids = grep { defined $_ } @circ_ids[0..($offset + $limit - 1)];
751         }
752     }
753
754     return [] unless @circ_ids;
755
756     my $qflesh = {
757         flesh => 3,
758         flesh_fields => {
759             circ => ['target_copy'],
760             acp => ['call_number'],
761             acn => ['record']
762         }
763     };
764
765     $e->xact_begin;
766     my $circs = $e->search_action_circulation(
767         [{id => \@circ_ids}, ($flesh) ? $qflesh : {}], {substream => 1});
768
769     my @circs;
770     for my $circ (@$circs) {
771         push(@circs, {
772             circ => $circ, 
773             marc_xml => ($flesh and $circ->target_copy->call_number->id != -1) ? 
774                 XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc) : 
775                 undef  # pre-cat copy, use the dummy title/author instead
776         });
777     }
778     $e->xact_rollback;
779
780     # make sure the final list is in the correct order
781     my @sorted_circs;
782     for my $id (@circ_ids) {
783         push(
784             @sorted_circs,
785             (grep { $_->{circ}->id == $id } @circs)
786         );
787     }
788
789     return \@sorted_circs;
790 }
791
792
793 sub handle_circ_renew {
794     my $self = shift;
795     my $action = shift;
796     my $ctx = $self->ctx;
797
798     my @renew_ids = $self->cgi->param('circ');
799
800     my $circs = $self->fetch_user_circs(0, ($action eq 'renew') ? [@renew_ids] : undef);
801
802     # TODO: fire off renewal calls in batches to speed things up
803     my @responses;
804     for my $circ (@$circs) {
805
806         my $evt = $U->simplereq(
807             'open-ils.circ', 
808             'open-ils.circ.renew',
809             $self->editor->authtoken,
810             {
811                 patron_id => $self->editor->requestor->id,
812                 copy_id => $circ->{circ}->target_copy,
813                 opac_renewal => 1
814             }
815         );
816
817         # TODO return these, then insert them into the circ data 
818         # blob that is shoved into the template for each circ
819         # so the template won't have to match them
820         push(@responses, {copy => $circ->{circ}->target_copy, evt => $evt});
821     }
822
823     return @responses;
824 }
825
826
827 sub load_myopac_circs {
828     my $self = shift;
829     my $e = $self->editor;
830     my $ctx = $self->ctx;
831
832     $ctx->{circs} = [];
833     my $limit = $self->cgi->param('limit') || 0; # 0 == unlimited
834     my $offset = $self->cgi->param('offset') || 0;
835     my $action = $self->cgi->param('action') || '';
836
837     # perform the renewal first if necessary
838     my @results = $self->handle_circ_renew($action) if $action =~ /renew/;
839
840     $ctx->{circs} = $self->fetch_user_circs(1, undef, $limit, $offset);
841
842     my $success_renewals = 0;
843     my $failed_renewals = 0;
844     for my $data (@{$ctx->{circs}}) {
845         my ($resp) = grep { $_->{copy} == $data->{circ}->target_copy->id } @results;
846
847         if($resp) {
848             my $evt = ref($resp->{evt}) eq 'ARRAY' ? $resp->{evt}->[0] : $resp->{evt};
849             $data->{renewal_response} = $evt;
850             $success_renewals++ if $evt->{textcode} eq 'SUCCESS';
851             $failed_renewals++ if $evt->{textcode} ne 'SUCCESS';
852         }
853     }
854
855     $ctx->{success_renewals} = $success_renewals;
856     $ctx->{failed_renewals} = $failed_renewals;
857
858     return Apache2::Const::OK;
859 }
860
861 sub load_myopac_circ_history {
862     my $self = shift;
863     my $e = $self->editor;
864     my $ctx = $self->ctx;
865     my $limit = $self->cgi->param('limit') || 15;
866     my $offset = $self->cgi->param('offset') || 0;
867
868     $ctx->{circ_history_limit} = $limit;
869     $ctx->{circ_history_offset} = $offset;
870
871     my $circ_ids = $e->json_query({
872         select => {
873             au => [{
874                 column => 'id', 
875                 transform => 'action.usr_visible_circs', 
876                 result_field => 'id'
877             }]
878         },
879         from => 'au',
880         where => {id => $e->requestor->id}, 
881         limit => $limit,
882         offset => $offset
883     });
884
885     $ctx->{circs} = $self->fetch_user_circs(1, [map { $_->{id} } @$circ_ids]);
886     return Apache2::Const::OK;
887 }
888
889 # TODO: action.usr_visible_holds does not return cancelled holds.  Should it?
890 sub load_myopac_hold_history {
891     my $self = shift;
892     my $e = $self->editor;
893     my $ctx = $self->ctx;
894     my $limit = $self->cgi->param('limit') || 15;
895     my $offset = $self->cgi->param('offset') || 0;
896     $ctx->{hold_history_limit} = $limit;
897     $ctx->{hold_history_offset} = $offset;
898
899     my $hold_ids = $e->json_query({
900         select => {
901             au => [{
902                 column => 'id', 
903                 transform => 'action.usr_visible_holds', 
904                 result_field => 'id'
905             }]
906         },
907         from => 'au',
908         where => {id => $e->requestor->id}, 
909         limit => $limit,
910         offset => $offset
911     });
912
913     $ctx->{holds} = $self->fetch_user_holds([map { $_->{id} } @$hold_ids], 0, 1, 0);
914     return Apache2::Const::OK;
915 }
916
917 sub load_myopac_payment_form {
918     my $self = shift;
919     my $r;
920
921     $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact'), $self->cgi->param('xact_misc')]) and return $r;
922     $r = $self->prepare_extended_user_info and return $r;
923
924     return Apache2::Const::OK;
925 }
926
927 # TODO: add other filter options as params/configs/etc.
928 sub load_myopac_payments {
929     my $self = shift;
930     my $limit = $self->cgi->param('limit') || 20;
931     my $offset = $self->cgi->param('offset') || 0;
932     my $e = $self->editor;
933
934     $self->ctx->{payment_history_limit} = $limit;
935     $self->ctx->{payment_history_offset} = $offset;
936
937     my $args = {};
938     $args->{limit} = $limit if $limit;
939     $args->{offset} = $offset if $offset;
940
941     if (my $max_age = $self->ctx->{get_org_setting}->(
942         $e->requestor->home_ou, "opac.payment_history_age_limit"
943     )) {
944         my $min_ts = DateTime->now(
945             "time_zone" => DateTime::TimeZone->new("name" => "local"),
946         )->subtract("seconds" => interval_to_seconds($max_age))->iso8601();
947         
948         $logger->info("XXX min_ts: $min_ts");
949         $args->{"where"} = {"payment_ts" => {">=" => $min_ts}};
950     }
951
952     $self->ctx->{payments} = $U->simplereq(
953         'open-ils.actor',
954         'open-ils.actor.user.payments.retrieve.atomic',
955         $e->authtoken, $e->requestor->id, $args);
956
957     return Apache2::Const::OK;
958 }
959
960 sub load_myopac_pay {
961     my $self = shift;
962     my $r;
963
964     my @payment_xacts = ($self->cgi->param('xact'), $self->cgi->param('xact_misc'));
965     $logger->info("tpac paying fines for xacts @payment_xacts");
966
967     $r = $self->prepare_fines(undef, undef, \@payment_xacts) and return $r;
968
969     # balance_owed is computed specifically from the fines we're trying
970     # to pay in this case.
971     if ($self->ctx->{fines}->{balance_owed} <= 0) {
972         $self->apache->log->info(
973             sprintf("Can't pay non-positive balance. xacts selected: (%s)",
974                 join(", ", map(int, $self->cgi->param("xact"), $self->cgi->param('xact_misc'))))
975         );
976         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
977     }
978
979     my $cc_args = {"where_process" => 1};
980
981     $cc_args->{$_} = $self->cgi->param($_) for (qw/
982         number cvv2 expire_year expire_month billing_first
983         billing_last billing_address billing_city billing_state
984         billing_zip
985     /);
986
987     my $args = {
988         "cc_args" => $cc_args,
989         "userid" => $self->ctx->{user}->id,
990         "payment_type" => "credit_card_payment",
991         "payments" => $self->prepare_fines_for_payment   # should be safe after self->prepare_fines
992     };
993
994     my $resp = $U->simplereq("open-ils.circ", "open-ils.circ.money.payment",
995         $self->editor->authtoken, $args, $self->ctx->{user}->last_xact_id
996     );
997
998     $self->ctx->{"payment_response"} = $resp;
999
1000     unless ($resp->{"textcode"}) {
1001         $self->ctx->{printable_receipt} = $U->simplereq(
1002            "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
1003            $self->editor->authtoken, $resp->{payments}
1004         );
1005     }
1006
1007     return Apache2::Const::OK;
1008 }
1009
1010 sub load_myopac_receipt_print {
1011     my $self = shift;
1012
1013     $self->ctx->{printable_receipt} = $U->simplereq(
1014        "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
1015        $self->editor->authtoken, [$self->cgi->param("payment")]
1016     );
1017
1018     return Apache2::Const::OK;
1019 }
1020
1021 sub load_myopac_receipt_email {
1022     my $self = shift;
1023
1024     # The following ML method doesn't actually check whether the user in
1025     # question has an email address, so we do.
1026     if ($self->ctx->{user}->email) {
1027         $self->ctx->{email_receipt_result} = $U->simplereq(
1028            "open-ils.circ", "open-ils.circ.money.payment_receipt.email",
1029            $self->editor->authtoken, [$self->cgi->param("payment")]
1030         );
1031     } else {
1032         $self->ctx->{email_receipt_result} =
1033             new OpenILS::Event("PATRON_NO_EMAIL_ADDRESS");
1034     }
1035
1036     return Apache2::Const::OK;
1037 }
1038
1039 sub prepare_fines {
1040     my ($self, $limit, $offset, $id_list) = @_;
1041
1042     # XXX TODO: check for failure after various network calls
1043
1044     # It may be unclear, but this result structure lumps circulation and
1045     # reservation fines together, and keeps grocery fines separate.
1046     $self->ctx->{"fines"} = {
1047         "circulation" => [],
1048         "grocery" => [],
1049         "total_paid" => 0,
1050         "total_owed" => 0,
1051         "balance_owed" => 0
1052     };
1053
1054     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
1055
1056     # TODO: This should really be a ML call, but the existing calls 
1057     # return an excessive amount of data and don't offer streaming
1058
1059     my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
1060
1061     my $req = $cstore->request(
1062         'open-ils.cstore.direct.money.open_billable_transaction_summary.search',
1063         {
1064             usr => $self->editor->requestor->id,
1065             balance_owed => {'!=' => 0},
1066             ($id_list && @$id_list ? ("id" => $id_list) : ()),
1067         },
1068         {
1069             flesh => 4,
1070             flesh_fields => {
1071                 mobts => [qw/grocery circulation reservation/],
1072                 bresv => ['target_resource_type'],
1073                 brt => ['record'],
1074                 mg => ['billings'],
1075                 mb => ['btype'],
1076                 circ => ['target_copy'],
1077                 acp => ['call_number'],
1078                 acn => ['record']
1079             },
1080             order_by => { mobts => 'xact_start' },
1081             %paging
1082         }
1083     );
1084
1085     my @total_keys = qw/total_paid total_owed balance_owed/;
1086     $self->ctx->{"fines"}->{@total_keys} = (0, 0, 0);
1087
1088     while(my $resp = $req->recv) {
1089         my $mobts = $resp->content;
1090         my $circ = $mobts->circulation;
1091
1092         my $last_billing;
1093         if($mobts->grocery) {
1094             my @billings = sort { $a->billing_ts cmp $b->billing_ts } @{$mobts->grocery->billings};
1095             $last_billing = pop(@billings);
1096         }
1097
1098         # XXX TODO confirm that the following, and the later division by 100.0
1099         # to get a floating point representation once again, is sufficiently
1100         # "money-safe" math.
1101         $self->ctx->{"fines"}->{$_} += int($mobts->$_ * 100) for (@total_keys);
1102
1103         my $marc_xml = undef;
1104         if ($mobts->xact_type eq 'reservation' and
1105             $mobts->reservation->target_resource_type->record) {
1106             $marc_xml = XML::LibXML->new->parse_string(
1107                 $mobts->reservation->target_resource_type->record->marc
1108             );
1109         } elsif ($mobts->xact_type eq 'circulation' and
1110             $circ->target_copy->call_number->id != -1) {
1111             $marc_xml = XML::LibXML->new->parse_string(
1112                 $circ->target_copy->call_number->record->marc
1113             );
1114         }
1115
1116         push(
1117             @{$self->ctx->{"fines"}->{$mobts->grocery ? "grocery" : "circulation"}},
1118             {
1119                 xact => $mobts,
1120                 last_grocery_billing => $last_billing,
1121                 marc_xml => $marc_xml
1122             } 
1123         );
1124     }
1125
1126     $cstore->kill_me;
1127
1128     $self->ctx->{"fines"}->{$_} /= 100.0 for (@total_keys);
1129     return;
1130 }
1131
1132 sub prepare_fines_for_payment {
1133     # This assumes $self->prepare_fines has already been run
1134     my ($self) = @_;
1135
1136     my @results = ();
1137     if ($self->ctx->{fines}) {
1138         push @results, [$_->{xact}->id, $_->{xact}->balance_owed] foreach (
1139             @{$self->ctx->{fines}->{circulation}},
1140             @{$self->ctx->{fines}->{grocery}}
1141         );
1142     }
1143
1144     return \@results;
1145 }
1146
1147 sub load_myopac_main {
1148     my $self = shift;
1149     my $limit = $self->cgi->param('limit') || 0;
1150     my $offset = $self->cgi->param('offset') || 0;
1151
1152     return $self->prepare_fines($limit, $offset) || Apache2::Const::OK;
1153 }
1154
1155 sub load_myopac_update_email {
1156     my $self = shift;
1157     my $e = $self->editor;
1158     my $ctx = $self->ctx;
1159     my $email = $self->cgi->param('email') || '';
1160     my $current_pw = $self->cgi->param('current_pw') || '';
1161
1162     # needed for most up-to-date email address
1163     if (my $r = $self->prepare_extended_user_info) { return $r };
1164
1165     return Apache2::Const::OK 
1166         unless $self->cgi->request_method eq 'POST';
1167
1168     unless($email =~ /.+\@.+\..+/) { # TODO better regex?
1169         $ctx->{invalid_email} = $email;
1170         return Apache2::Const::OK;
1171     }
1172
1173     my $stat = $U->simplereq(
1174         'open-ils.actor', 
1175         'open-ils.actor.user.email.update', 
1176         $e->authtoken, $email, $current_pw);
1177
1178     if($U->event_equals($stat, 'INCORRECT_PASSWORD')) {
1179         $ctx->{password_incorrect} = 1;
1180         return Apache2::Const::OK;
1181     }
1182
1183     unless ($self->cgi->param("redirect_to")) {
1184         my $url = $self->apache->unparsed_uri;
1185         $url =~ s/update_email/prefs/;
1186
1187         return $self->generic_redirect($url);
1188     }
1189
1190     return $self->generic_redirect;
1191 }
1192
1193 sub load_myopac_update_username {
1194     my $self = shift;
1195     my $e = $self->editor;
1196     my $ctx = $self->ctx;
1197     my $username = $self->cgi->param('username') || '';
1198     my $current_pw = $self->cgi->param('current_pw') || '';
1199
1200     $self->prepare_extended_user_info;
1201
1202     my $allow_change = 1;
1203     my $regex_check;
1204     my $lock_usernames = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.lock_usernames');
1205     if($lock_usernames == 1) {
1206         # Policy says no username changes
1207         $allow_change = 0;
1208     } else {
1209         # We want this further down.
1210         $regex_check = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.barcode_regex');
1211         my $username_unlimit = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.unlimit_usernames');
1212         if($username_unlimit != 1) {
1213             if(!$regex_check) {
1214                 # Default is "starts with a number"
1215                 $regex_check = '^\d+';
1216             }
1217             # You already have a username?
1218             if($regex_check and $self->ctx->{user}->usrname !~ /$regex_check/) {
1219                 $allow_change = 0;
1220             }
1221         }
1222     }
1223     if(!$allow_change) {
1224         my $url = $self->apache->unparsed_uri;
1225         $url =~ s/update_username/prefs/;
1226
1227         return $self->generic_redirect($url);
1228     }
1229
1230     return Apache2::Const::OK 
1231         unless $self->cgi->request_method eq 'POST';
1232
1233     unless($username and $username !~ /\s/) { # any other username restrictions?
1234         $ctx->{invalid_username} = $username;
1235         return Apache2::Const::OK;
1236     }
1237
1238     # New username can't look like a barcode if we have a barcode regex
1239     if($regex_check and $username =~ /$regex_check/) {
1240         $ctx->{invalid_username} = $username;
1241         return Apache2::Const::OK;
1242     }
1243
1244     # New username has to look like a username if we have a username regex
1245     $regex_check = $ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.username_regex');
1246     if($regex_check and $username !~ /$regex_check/) {
1247         $ctx->{invalid_username} = $username;
1248         return Apache2::Const::OK;
1249     }
1250
1251     if($username ne $e->requestor->usrname) {
1252
1253         my $evt = $U->simplereq(
1254             'open-ils.actor', 
1255             'open-ils.actor.user.username.update', 
1256             $e->authtoken, $username, $current_pw);
1257
1258         if($U->event_equals($evt, 'INCORRECT_PASSWORD')) {
1259             $ctx->{password_incorrect} = 1;
1260             return Apache2::Const::OK;
1261         }
1262
1263         if($U->event_equals($evt, 'USERNAME_EXISTS')) {
1264             $ctx->{username_exists} = $username;
1265             return Apache2::Const::OK;
1266         }
1267     }
1268
1269     my $url = $self->apache->unparsed_uri;
1270     $url =~ s/update_username/prefs/;
1271
1272     return $self->generic_redirect($url);
1273 }
1274
1275 sub load_myopac_update_password {
1276     my $self = shift;
1277     my $e = $self->editor;
1278     my $ctx = $self->ctx;
1279
1280     return Apache2::Const::OK 
1281         unless $self->cgi->request_method eq 'POST';
1282
1283     my $current_pw = $self->cgi->param('current_pw') || '';
1284     my $new_pw = $self->cgi->param('new_pw') || '';
1285     my $new_pw2 = $self->cgi->param('new_pw2') || '';
1286
1287     unless($new_pw eq $new_pw2) {
1288         $ctx->{password_nomatch} = 1;
1289         return Apache2::Const::OK;
1290     }
1291
1292     my $pw_regex = $ctx->{get_org_setting}->($e->requestor->home_ou, 'global.password_regex');
1293
1294     if(!$pw_regex) {
1295         # This regex duplicates the JSPac's default "digit, letter, and 7 characters" rule
1296         $pw_regex = '(?=.*\d+.*)(?=.*[A-Za-z]+.*).{7,}';
1297     }
1298
1299     if($pw_regex and $new_pw !~ /$pw_regex/) {
1300         $ctx->{password_invalid} = 1;
1301         return Apache2::Const::OK;
1302     }
1303
1304     my $evt = $U->simplereq(
1305         'open-ils.actor', 
1306         'open-ils.actor.user.password.update', 
1307         $e->authtoken, $new_pw, $current_pw);
1308
1309
1310     if($U->event_equals($evt, 'INCORRECT_PASSWORD')) {
1311         $ctx->{password_incorrect} = 1;
1312         return Apache2::Const::OK;
1313     }
1314
1315     my $url = $self->apache->unparsed_uri;
1316     $url =~ s/update_password/prefs/;
1317
1318     return $self->generic_redirect($url);
1319 }
1320
1321 sub load_myopac_bookbags {
1322     my $self = shift;
1323     my $e = $self->editor;
1324     my $ctx = $self->ctx;
1325
1326     my ($sorter, $modifier) = $self->_get_bookbag_sort_params;
1327     $e->xact_begin; # replication...
1328
1329     my $rv = $self->load_mylist;
1330     unless($rv eq Apache2::Const::OK) {
1331         $e->rollback;
1332         return $rv;
1333     }
1334
1335     $ctx->{bookbags} = $e->search_container_biblio_record_entry_bucket(
1336         [
1337             {owner => $e->requestor->id, btype => 'bookbag'}, {
1338                 order_by => {cbreb => 'name'},
1339                 limit => $self->cgi->param('limit') || 10,
1340                 offset => $self->cgi->param('offset') || 0
1341             }
1342         ],
1343         {substream => 1}
1344     );
1345
1346     if(!$ctx->{bookbags}) {
1347         $e->rollback;
1348         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1349     }
1350     
1351     # Here is the loop that uses search to find the bib records in each
1352     # bookbag.  XXX This should be parallelized.  Should this be done
1353     # with OpenSRF::MultiSession, or is it enough to use OpenSRF::AppSession
1354     # and call ->request() without calling ->gather() on any of those objects
1355     # until all the requests have been issued?
1356
1357     foreach my $bookbag (@{$ctx->{bookbags}}) {
1358         my $query = $self->_prepare_bookbag_container_query(
1359             $bookbag->id, $sorter, $modifier
1360         );
1361
1362         # XXX we need to limit the number of records per bbag; use third arg
1363         # of bib_container_items_via_search() i think.
1364         my $items = $U->bib_container_items_via_search($bookbag->id, $query)
1365             or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1366
1367         # Maybe save a little memory by creating only one XML::LibXML::Document
1368         # instance for each record, even if record is repeated across bookbags.
1369
1370         foreach my $rec (map { $_->target_biblio_record_entry } @$items) {
1371             next if $ctx->{bookbags_marc_xml}{$rec->id};
1372             $ctx->{bookbags_marc_xml}{$rec->id} =
1373                 (new XML::LibXML)->parse_string($rec->marc);
1374         }
1375
1376         $bookbag->items($items);
1377     }
1378
1379     $e->rollback;
1380     return Apache2::Const::OK;
1381 }
1382
1383
1384 # actions are create, delete, show, hide, rename, add_rec, delete_item, place_hold
1385 # CGI is action, list=list_id, add_rec/record=bre_id, del_item=bucket_item_id, name=new_bucket_name
1386 sub load_myopac_bookbag_update {
1387     my ($self, $action, $list_id, @hold_recs) = @_;
1388     my $e = $self->editor;
1389     my $cgi = $self->cgi;
1390
1391     # save_notes is effectively another action, but is passed in a separate
1392     # CGI parameter for what are really just layout reasons.
1393     $action = 'save_notes' if $cgi->param('save_notes');
1394     $action ||= $cgi->param('action');
1395
1396     $list_id ||= $cgi->param('list');
1397
1398     my @add_rec = $cgi->param('add_rec') || $cgi->param('record');
1399     my @selected_item = $cgi->param('selected_item');
1400     my $shared = $cgi->param('shared');
1401     my $name = $cgi->param('name');
1402     my $description = $cgi->param('description');
1403     my $success = 0;
1404     my $list;
1405
1406     # This url intentionally leaves off the edit_notes parameter, but
1407     # may need to add some back in for paging.
1408
1409     my $url = "https://" . $self->apache->hostname .
1410         $self->ctx->{opac_root} . "/myopac/lists?";
1411
1412     $url .= 'sort=' . uri_escape($cgi->param("sort")) if $cgi->param("sort");
1413
1414     if ($action eq 'create') {
1415         $list = Fieldmapper::container::biblio_record_entry_bucket->new;
1416         $list->name($name);
1417         $list->description($description);
1418         $list->owner($e->requestor->id);
1419         $list->btype('bookbag');
1420         $list->pub($shared ? 't' : 'f');
1421         $success = $U->simplereq('open-ils.actor', 
1422             'open-ils.actor.container.create', $e->authtoken, 'biblio', $list)
1423
1424     } elsif($action eq 'place_hold') {
1425
1426         # @hold_recs comes from anon lists redirect; selected_itesm comes from existing buckets
1427         unless (@hold_recs) {
1428             if (@selected_item) {
1429                 my $items = $e->search_container_biblio_record_entry_bucket_item({id => \@selected_item});
1430                 @hold_recs = map { $_->target_biblio_record_entry } @$items;
1431             }
1432         }
1433                 
1434         return Apache2::Const::OK unless @hold_recs;
1435         $logger->info("placing holds from list page on: @hold_recs");
1436
1437         my $url = $self->ctx->{opac_root} . '/place_hold?hold_type=T';
1438         $url .= ';hold_target=' . $_ for @hold_recs;
1439         return $self->generic_redirect($url);
1440
1441     } else {
1442
1443         $list = $e->retrieve_container_biblio_record_entry_bucket($list_id);
1444
1445         return Apache2::Const::HTTP_BAD_REQUEST unless 
1446             $list and $list->owner == $e->requestor->id;
1447     }
1448
1449     if($action eq 'delete') {
1450         $success = $U->simplereq('open-ils.actor', 
1451             'open-ils.actor.container.full_delete', $e->authtoken, 'biblio', $list_id);
1452
1453     } elsif($action eq 'show') {
1454         unless($U->is_true($list->pub)) {
1455             $list->pub('t');
1456             $success = $U->simplereq('open-ils.actor', 
1457                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1458         }
1459
1460     } elsif($action eq 'hide') {
1461         if($U->is_true($list->pub)) {
1462             $list->pub('f');
1463             $success = $U->simplereq('open-ils.actor', 
1464                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1465         }
1466
1467     } elsif($action eq 'rename') {
1468         if($name) {
1469             $list->name($name);
1470             $success = $U->simplereq('open-ils.actor', 
1471                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
1472         }
1473
1474     } elsif($action eq 'add_rec') {
1475         foreach my $add_rec (@add_rec) {
1476             my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1477             $item->bucket($list_id);
1478             $item->target_biblio_record_entry($add_rec);
1479             $success = $U->simplereq('open-ils.actor', 
1480                 'open-ils.actor.container.item.create', $e->authtoken, 'biblio', $item);
1481             last unless $success;
1482         }
1483
1484     } elsif($action eq 'del_item') {
1485         foreach (@selected_item) {
1486             $success = $U->simplereq(
1487                 'open-ils.actor',
1488                 'open-ils.actor.container.item.delete', $e->authtoken, 'biblio', $_
1489             );
1490             last unless $success;
1491         }
1492     } elsif ($action eq 'save_notes') {
1493         $success = $self->update_bookbag_item_notes;
1494     }
1495
1496     return $self->generic_redirect($url) if $success;
1497
1498     # XXX FIXME Bucket failure doesn't have a page to show the user anything
1499     # right now. User just sees a 404 currently.
1500
1501     $self->ctx->{bucket_action} = $action;
1502     $self->ctx->{bucket_action_failed} = 1;
1503     return Apache2::Const::OK;
1504 }
1505
1506 sub update_bookbag_item_notes {
1507     my ($self) = @_;
1508     my $e = $self->editor;
1509
1510     my @note_keys = grep /^note-\d+/, keys(%{$self->cgi->Vars});
1511     my @item_keys = grep /^item-\d+/, keys(%{$self->cgi->Vars});
1512
1513     # We're going to leverage an API call that's already been written to check
1514     # permissions appropriately.
1515
1516     my $a = create OpenSRF::AppSession("open-ils.actor");
1517     my $method = "open-ils.actor.container.item_note.cud";
1518
1519     for my $note_key (@note_keys) {
1520         my $note;
1521
1522         my $id = ($note_key =~ /(\d+)/)[0];
1523
1524         if (!($note =
1525             $e->retrieve_container_biblio_record_entry_bucket_item_note($id))) {
1526             my $event = $e->die_event;
1527             $self->apache->log->warn(
1528                 "error retrieving cbrebin id $id, got event " .
1529                 $event->{textcode}
1530             );
1531             $a->kill_me;
1532             $self->ctx->{bucket_action_event} = $event;
1533             return;
1534         }
1535
1536         if (length($self->cgi->param($note_key))) {
1537             $note->ischanged(1);
1538             $note->note($self->cgi->param($note_key));
1539         } else {
1540             $note->isdeleted(1);
1541         }
1542
1543         my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1);
1544
1545         if (defined $U->event_code($r)) {
1546             $self->apache->log->warn(
1547                 "attempt to modify cbrebin " . $note->id .
1548                 " returned event " .  $r->{textcode}
1549             );
1550             $e->rollback;
1551             $a->kill_me;
1552             $self->ctx->{bucket_action_event} = $r;
1553             return;
1554         }
1555     }
1556
1557     for my $item_key (@item_keys) {
1558         my $id = int(($item_key =~ /(\d+)/)[0]);
1559         my $text = $self->cgi->param($item_key);
1560
1561         chomp $text;
1562         next unless length $text;
1563
1564         my $note = new Fieldmapper::container::biblio_record_entry_bucket_item_note;
1565         $note->isnew(1);
1566         $note->item($id);
1567         $note->note($text);
1568
1569         my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1);
1570
1571         if (defined $U->event_code($r)) {
1572             $self->apache->log->warn(
1573                 "attempt to create cbrebin for item " . $note->item .
1574                 " returned event " .  $r->{textcode}
1575             );
1576             $e->rollback;
1577             $a->kill_me;
1578             $self->ctx->{bucket_action_event} = $r;
1579             return;
1580         }
1581     }
1582
1583     $a->kill_me;
1584     return 1;   # success
1585 }
1586
1587 sub load_myopac_bookbag_print {
1588     my ($self) = @_;
1589
1590     $self->apache->content_type("text/plain; encoding=utf8");
1591
1592     my $id = int($self->cgi->param("list"));
1593
1594     my ($sorter, $modifier) = $self->_get_bookbag_sort_params;
1595
1596     my $item_search =
1597         $self->_prepare_bookbag_container_query($id, $sorter, $modifier);
1598
1599     my $bbag;
1600
1601     # Get the bookbag object itself, assuming we're allowed to.
1602     if ($self->editor->allowed("VIEW_CONTAINER")) {
1603
1604         $bbag = $self->editor->retrieve_container_biblio_record_entry_bucket($id) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1605     } else {
1606         my $bookbags = $self->editor->search_container_biblio_record_entry_bucket(
1607             {
1608                 "id" => $id,
1609                 "-or" => {
1610                     "owner" => $self->editor->requestor->id,
1611                     "pub" => "t"
1612                 }
1613             }
1614         ) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1615
1616         $bbag = pop @$bookbags;
1617     }
1618
1619     # If we have a bookbag we're allowed to look at, issue the A/T event
1620     # to get CSV, passing as a user param that search query we built before.
1621     if ($bbag) {
1622         $self->ctx->{csv} = $U->fire_object_event(
1623             undef, "container.biblio_record_entry_bucket.csv",
1624             $bbag, $self->editor->requestor->home_ou,
1625             undef, {"item_search" => $item_search}
1626         );
1627     }
1628
1629     # Create a reasonable filename and set the content disposition to
1630     # provoke browser download dialogs.
1631     (my $filename = $bbag->id . $bbag->name) =~ s/[^a-z0-9_ -]//gi;
1632
1633     $self->apache->headers_out->add(
1634         "Content-Disposition",
1635         "attachment;filename=$filename.csv"
1636     );
1637
1638     return Apache2::Const::OK;
1639 }
1640
1641 sub load_password_reset {
1642     my $self = shift;
1643     my $cgi = $self->cgi;
1644     my $ctx = $self->ctx;
1645     my $barcode = $cgi->param('barcode');
1646     my $username = $cgi->param('username');
1647     my $email = $cgi->param('email');
1648     my $pwd1 = $cgi->param('pwd1');
1649     my $pwd2 = $cgi->param('pwd2');
1650     my $uuid = $ctx->{page_args}->[0];
1651
1652     if ($uuid) {
1653
1654         $logger->info("patron password reset with uuid $uuid");
1655
1656         if ($pwd1 and $pwd2) {
1657
1658             if ($pwd1 eq $pwd2) {
1659
1660                 my $response = $U->simplereq(
1661                     'open-ils.actor', 
1662                     'open-ils.actor.patron.password_reset.commit',
1663                     $uuid, $pwd1);
1664
1665                 $logger->info("patron password reset response " . Dumper($response));
1666
1667                 if ($U->event_code($response)) { # non-success event
1668                     
1669                     my $code = $response->{textcode};
1670                     
1671                     if ($code eq 'PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST') {
1672                         $ctx->{pwreset} = {style => 'error', status => 'NOT_ACTIVE'};
1673                     }
1674
1675                     if ($code eq 'PATRON_PASSWORD_WAS_NOT_STRONG') {
1676                         $ctx->{pwreset} = {style => 'error', status => 'NOT_STRONG'};
1677                     }
1678
1679                 } else { # success
1680
1681                     $ctx->{pwreset} = {style => 'success', status => 'SUCCESS'};
1682                 }
1683
1684             } else { # passwords not equal
1685
1686                 $ctx->{pwreset} = {style => 'error', status => 'NO_MATCH'};
1687             }
1688
1689         } else { # 2 password values needed
1690
1691             $ctx->{pwreset} = {status => 'TWO_PASSWORDS'};
1692         }
1693
1694     } elsif ($barcode or $username) {
1695
1696         my @params = $barcode ? ('barcode', $barcode) : ('username', $username);
1697
1698         $U->simplereq(
1699             'open-ils.actor', 
1700             'open-ils.actor.patron.password_reset.request', @params);
1701
1702         $ctx->{pwreset} = {status => 'REQUEST_SUCCESS'};
1703     }
1704
1705     $logger->info("patron password reset resulted in " . Dumper($ctx->{pwreset}));
1706     return Apache2::Const::OK;
1707 }
1708
1709 1;