]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
LP#1570072: update hold notification methods upon preference changes
[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 OpenSRF::Utils::Cache;
11 use Digest::MD5 qw(md5_hex);
12 use Data::Dumper;
13 $Data::Dumper::Indent = 0;
14 use DateTime;
15 use DateTime::Format::ISO8601;
16 my $U = 'OpenILS::Application::AppUtils';
17 use List::MoreUtils qw/uniq/;
18
19 sub prepare_extended_user_info {
20     my $self = shift;
21     my @extra_flesh = @_;
22     my $e = $self->editor;
23
24     # are we already in a transaction?
25     my $local_xact = !$e->{xact_id};
26     $e->xact_begin if $local_xact;
27
28     # keep the original user object so we can restore
29     # login-specific data (e.g. workstation)
30     my $usr = $self->ctx->{user};
31
32     $self->ctx->{user} = $self->editor->retrieve_actor_user([
33         $self->ctx->{user}->id,
34         {
35             flesh => 1,
36             flesh_fields => {
37                 au => [qw/card home_ou addresses ident_type billing_address waiver_entries/, @extra_flesh]
38                 # ...
39             }
40         }
41     ]);
42
43     $e->rollback if $local_xact;
44
45     $self->ctx->{user}->wsid($usr->wsid);
46     $self->ctx->{user}->ws_ou($usr->ws_ou);
47
48     # discard replaced (negative-id) addresses.
49     $self->ctx->{user}->addresses([
50         grep {$_->id > 0} @{$self->ctx->{user}->addresses} ]);
51
52     return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR
53         unless $self->ctx->{user};
54
55     return;
56 }
57
58 # Given an event returned by a failed attempt to create a hold, do we have
59 # permission to override?  XXX Should the permission check be scoped to a
60 # given org_unit context?
61 sub test_could_override {
62     my ($self, $event) = @_;
63
64     return 0 unless $event;
65     return 1 if $self->editor->allowed($event->{textcode} . ".override");
66     return 1 if $event->{"fail_part"} and
67         $self->editor->allowed($event->{"fail_part"} . ".override");
68     return 0;
69 }
70
71 # Find out whether we care that local copies are available
72 sub local_avail_concern {
73     my ($self, $hold_target, $hold_type, $pickup_lib) = @_;
74
75     my $would_block = $self->ctx->{get_org_setting}->
76         ($pickup_lib, "circ.holds.hold_has_copy_at.block");
77     my $would_alert = (
78         $self->ctx->{get_org_setting}->
79             ($pickup_lib, "circ.holds.hold_has_copy_at.alert") and
80                 not $self->cgi->param("override")
81     ) unless $would_block;
82
83     if ($would_block or $would_alert) {
84         my $args = {
85             "hold_target" => $hold_target,
86             "hold_type" => $hold_type,
87             "org_unit" => $pickup_lib
88         };
89         my $local_avail = $U->simplereq(
90             "open-ils.circ",
91             "open-ils.circ.hold.has_copy_at", $self->editor->authtoken, $args
92         );
93         $logger->info(
94             "copy availability information for " . Dumper($args) .
95             " is " . Dumper($local_avail)
96         );
97         if (%$local_avail) { # if hash not empty
98             $self->ctx->{hold_copy_available} = $local_avail;
99             return ($would_block, $would_alert);
100         }
101     }
102
103     return (0, 0);
104 }
105
106 # context additions:
107 #   user : au object, fleshed
108 sub load_myopac_prefs {
109     my $self = shift;
110     my $cgi = $self->cgi;
111     my $e = $self->editor;
112     my $pending_addr = $cgi->param('pending_addr');
113     my $replace_addr = $cgi->param('replace_addr');
114     my $delete_pending = $cgi->param('delete_pending');
115
116     $self->prepare_extended_user_info;
117     my $user = $self->ctx->{user};
118
119     my $lock_usernames = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.lock_usernames');
120     if(defined($lock_usernames) and $lock_usernames == 1) {
121         # Policy says no username changes
122         $self->ctx->{username_change_disallowed} = 1;
123     } else {
124         my $username_unlimit = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.unlimit_usernames');
125         if(!$username_unlimit) {
126             my $regex_check = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.barcode_regex');
127             if(!$regex_check) {
128                 # Default is "starts with a number"
129                 $regex_check = '^\d+';
130             }
131             # You already have a username?
132             if($regex_check and $self->ctx->{user}->usrname !~ /$regex_check/) {
133                 $self->ctx->{username_change_disallowed} = 1;
134             }
135         }
136     }
137
138     return Apache2::Const::OK unless
139         $pending_addr or $replace_addr or $delete_pending;
140
141     my @form_fields = qw/address_type street1 street2 city county state country post_code/;
142
143     my $paddr;
144     if( $pending_addr ) { # update an existing pending address
145
146         ($paddr) = grep { $_->id == $pending_addr } @{$user->addresses};
147         return Apache2::Const::HTTP_BAD_REQUEST unless $paddr;
148         $paddr->$_( $cgi->param($_) ) for @form_fields;
149
150     } elsif( $replace_addr ) { # create a new pending address for 'replace_addr'
151
152         $paddr = Fieldmapper::actor::user_address->new;
153         $paddr->isnew(1);
154         $paddr->usr($user->id);
155         $paddr->pending('t');
156         $paddr->replaces($replace_addr);
157         $paddr->$_( $cgi->param($_) ) for @form_fields;
158
159     } elsif( $delete_pending ) {
160         $paddr = $e->retrieve_actor_user_address($delete_pending);
161         return Apache2::Const::HTTP_BAD_REQUEST unless
162             $paddr and $paddr->usr == $user->id and $U->is_true($paddr->pending);
163         $paddr->isdeleted(1);
164     }
165
166     my $resp = $U->simplereq(
167         'open-ils.actor',
168         'open-ils.actor.user.address.pending.cud',
169         $e->authtoken, $paddr);
170
171     if( $U->event_code($resp) ) {
172         $logger->error("Error updating pending address: $resp");
173         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
174     }
175
176     # in light of these changes, re-fetch latest data
177     $e->xact_begin;
178     $self->prepare_extended_user_info;
179     $e->rollback;
180
181     return Apache2::Const::OK;
182 }
183
184 sub load_myopac_prefs_notify {
185     my $self = shift;
186     my $e = $self->editor;
187
188
189     my $stat = $self->_load_user_with_prefs;
190     return $stat if $stat;
191
192     my $user_prefs = $self->fetch_optin_prefs;
193     $user_prefs = $self->update_optin_prefs($user_prefs)
194         if $self->cgi->request_method eq 'POST';
195
196     $self->ctx->{opt_in_settings} = $user_prefs;
197
198     return Apache2::Const::OK
199         unless $self->cgi->request_method eq 'POST';
200
201     my %settings;
202     my $set_map = $self->ctx->{user_setting_map};
203
204     foreach my $key (qw/
205         opac.default_phone
206         opac.default_sms_notify
207     /) {
208         my $val = $self->cgi->param($key);
209         $settings{$key}= $val unless $$set_map{$key} eq $val;
210     }
211
212     my $key = 'opac.default_sms_carrier';
213     my $val = $self->cgi->param('sms_carrier');
214     $settings{$key}= $val unless $$set_map{$key} eq $val;
215
216     $key = 'opac.hold_notify';
217     my @notify_methods = ();
218     if ($self->cgi->param($key . ".email") eq 'on') {
219         push @notify_methods, "email";
220     }
221     if ($self->cgi->param($key . ".phone") eq 'on') {
222         push @notify_methods, "phone";
223     }
224     if ($self->cgi->param($key . ".sms") eq 'on') {
225         push @notify_methods, "sms";
226     }
227     $val = join("|",@notify_methods);
228     $settings{$key}= $val unless $$set_map{$key} eq $val;
229
230     # Send the modified settings off to be saved
231     $U->simplereq(
232         'open-ils.actor',
233         'open-ils.actor.patron.settings.update',
234         $self->editor->authtoken, undef, \%settings);
235
236     # re-fetch user prefs
237     $self->ctx->{updated_user_settings} = \%settings;
238
239     # update holds: check if any changes affect any holds
240     my @llchgs = $self->_parse_prefs_notify_hold_related();
241     my @ffectedChgs;
242
243     if ( $self->cgi->param('hasHoldsChanges') ) {
244         # propagate pref_notify changes to holds
245         for my $chset (@llchgs){
246             # FIXME is this still needed?
247         }
248     
249     }
250     else {
251         my $holds = $U->simplereq('open-ils.circ', 'open-ils.circ.holds.retrieve.by_usr.with_notify',
252             $e->authtoken, $e->requestor->id);
253
254         if (@$holds > 0) {
255
256             my $default_phone_changes = {};
257             my $sms_changes           = {};
258             my $new_phone;
259             my $new_carrier;
260             my $new_sms;
261             for my $chset (@llchgs) {
262                 next if scalar(@$chset) < 3;
263                 my ($old, $new, $field) = @$chset;
264
265                 my $bool = $field =~ /_notify/ ? 1 : 0;
266
267                 # find holds that would change
268                 my $affected = [];
269                 foreach my $hold (@$holds) {
270                     if ($field eq 'email_notify') {
271                         my $curr = $hold->{$field} eq 't' ? 'true' : 'false';
272                         push @$affected, $hold if $curr ne $new;
273                     } elsif ($field eq 'default_phone') {
274                         my $old_phone = $hold->{phone_notify} // '';
275                         $new_phone = $new // '';
276                         push @{ $default_phone_changes->{ $old_phone } }, $hold->{id}
277                             if $old_phone ne $new_phone;
278                     } elsif ($field eq 'phone_notify') {
279                         my $curr = ($hold->{$field} // '' ne '') ? 'true' : 'false';
280                         push @$affected, $hold if $curr ne $new;
281                     } elsif ($field eq 'sms_notify') {
282                         my $curr = ($hold->{$field} // '' ne '') ? 'true' : 'false';
283                         push @$affected, $hold if $curr ne $new;
284                     } elsif ($field eq 'sms_info') {
285                         my $old_carrier = $hold->{'sms_carrier'} // '';
286                         my $old_sms = $hold->{'sms_notify'} // '';
287                         $new_carrier = $new->{carrier} // '';
288                         $new_sms = $new->{sms} // '';
289                         if (!($old_carrier eq $new_carrier && $old_sms eq $new_sms)) {
290                             push @{ $sms_changes->{ join("\t", $old_carrier, $old_sms) } }, $hold->{id};
291                         }
292                     }
293                 }
294
295                 # append affected array to chset
296                 if (scalar(@$affected) > 0){
297                     push(@$chset, [ map { $_->{id} } @$affected ]);
298                     push(@ffectedChgs, $chset);
299                 }
300             }
301
302
303             foreach my $old_phone (keys %$default_phone_changes) {
304                 push(@ffectedChgs, [ $old_phone, $new_phone, 'default_phone', $default_phone_changes->{$old_phone} ]);
305             }
306             foreach my $old_sms_info (keys %$sms_changes) {
307                 my ($old_carrier, $old_sms) = split /\t/, $old_sms_info;
308                 push(@ffectedChgs, [
309                                         { carrier => $old_carrier, sms => $old_sms },
310                                         { carrier => $new_carrier, sms => $new_sms },
311                                         'sms_info',
312                                         $sms_changes->{$old_sms_info}
313                                    ]);
314             }
315
316             if ( scalar(@ffectedChgs) ){
317                 $self->ctx->{affectedChgs} = \@ffectedChgs;
318             }
319         }
320     }
321
322     return $self->_load_user_with_prefs || Apache2::Const::OK;
323 }
324
325 sub _parse_prefs_notify_hold_related {
326
327     my $self = shift;
328     my $for_update = shift;
329
330     # create an array of change arrays
331     my @chgs;
332
333     my @phone_notify = $self->cgi->multi_param('phone_notify[]');
334     push(@chgs, \@phone_notify) if scalar(@phone_notify);
335
336     my $turning_on_phone_notify  = !$for_update &&
337                                    scalar(@phone_notify) &&
338                                    $phone_notify[1] eq 'true';
339     my $turning_off_phone_notify = !$for_update &&
340                                    scalar(@phone_notify) &&
341                                    $phone_notify[1] eq 'false';
342
343     my $changing_default_phone = 0;
344     if (!$turning_off_phone_notify) {
345         my @default_phone = $self->cgi->multi_param('default_phone[]');
346         if ($for_update) {
347             while (scalar(@default_phone) > 0) {
348                 my $chg = [ splice(@default_phone, 0, 4) ];
349                 if (scalar(@default_phone) > 0 && $default_phone[0] eq 'on') {
350                     push @$chg, shift(@default_phone);
351                     push(@chgs, $chg);
352                     $changing_default_phone = 1;
353                 }
354             }
355         } else {
356             if (scalar(@default_phone)) {
357                 push @chgs, \@default_phone;
358                 $changing_default_phone = 1;
359             }
360         }
361     }
362
363     if ($turning_on_phone_notify && $changing_default_phone) {
364         # we don't need to have both the phone_notify and default_phone
365         # changes; the latter will suffice
366         @chgs = grep { $_->[2] ne 'phone_notify' } @chgs;
367     } elsif ($turning_on_phone_notify && !$changing_default_phone) {
368         # replace the phone_notify change with a default_phone change
369         @chgs = grep { $_->[2] ne 'phone_notify' } @chgs;
370         my $default_phone = $self->cgi->param('opac.default_phone'); # we assume this is set
371         push @chgs, [ '', $default_phone, 'default_phone' ];
372     }
373
374     # on to SMS
375     # ... since both carrier and number are needed to send an SMS notifcation,
376     # we need to treat the pair as a unit
377     my @sms_notify = $self->cgi->multi_param('sms_notify[]');
378     push(@chgs, \@sms_notify) if scalar(@sms_notify);
379
380     my $turning_on_sms_notify  = !$for_update &&
381                                    scalar(@sms_notify) &&
382                                    $sms_notify[1] eq 'true';
383     my $turning_off_sms_notify = !$for_update &&
384                                    scalar(@sms_notify) &&
385                                    $sms_notify[1] eq 'false';
386
387     my $changing_sms_info = 0;
388     if (!$turning_off_sms_notify) {
389         my @sms_carrier = $self->cgi->multi_param('default_sms_carrier_id[]');
390         my @sms = $self->cgi->multi_param('default_sms[]');
391
392         if (scalar(@sms) || scalar(@sms_carrier)) {
393             my $new_carrier = scalar(@sms_carrier) ? $sms_carrier[1] : $self->cgi->param('sms_carrier');
394             my $new_sms = scalar(@sms) ? $sms[1] : $self->cgi->param('opac.default_sms_notify');
395             push @chgs, [
396                             { carrier => '', sms => '' },
397                             { carrier => $new_carrier, sms => $new_sms },
398                             'sms_info'
399                         ];
400            $changing_sms_info = 1;
401         }
402     }
403
404     my @sms_info = $self->cgi->multi_param('sms_info[]'); # only sent by confirmation page
405     if (scalar(@sms_info)) {
406         while (scalar(@sms_info) > 0) {
407             my $chg = [ splice(@sms_info, 0, 4) ];
408             if (scalar(@sms_info) > 0 && $sms_info[0] eq 'on') {
409                 push @$chg, shift(@sms_info);
410                 my ($carrier, $sms) = split /,/, $chg->[0], -1;
411                 $chg->[0] = { carrier => $carrier, sms => $sms };
412                 ($carrier, $sms) = split /,/, $chg->[1], -1;
413                 $chg->[1] = { carrier => $carrier, sms => $sms };
414                 push(@chgs, $chg);
415                 $changing_sms_info = 1;
416             }
417         }
418     }
419
420     if ($turning_on_sms_notify && $changing_sms_info) {
421         # we don't need to have both the sms_notify and sms_info
422         # changes; the latter will suffice
423         @chgs = grep { $_->[2] ne 'sms_notify' } @chgs;
424     } elsif ($turning_on_sms_notify && !$changing_sms_info) {
425         # replace the sms_notify change with a sms_info change
426         @chgs = grep { $_->[2] ne 'sms_notify' } @chgs;
427         my $sms_info = {
428             carrier => $self->cgi->param('sms_carrier'),
429             sms     => $self->cgi->param('opac.default_sms_notify'),
430         };
431         push @chgs, [ { carrier => '', sms => ''}, $sms_info, 'sms_info' ];
432     }
433
434     my @email_notify = $self->cgi->multi_param('email_notify[]');
435     push(@chgs, \@email_notify) if scalar(@email_notify);
436
437     if ($for_update) {
438         # if we're updating, keep only the ones that have been
439         # explicitly checked by the user
440         @chgs = grep { scalar(@$_) == 5 && $_->[4] eq 'on' } @chgs;
441     }
442     return @chgs;
443 }
444
445 sub load_myopac_prefs_notify_changed_holds {
446     my $self = shift;
447     my $e = $self->editor;
448
449     my $hasChanges = $self->cgi->param('hasHoldsChanges');
450     
451     return $self->_load_user_with_prefs || Apache2::Const::OK unless $hasChanges;
452
453     my @ll = $self->_parse_prefs_notify_hold_related(1);
454
455     my @updates;
456     for my $chset (@ll){
457         my ($old, $new, $type, $holdids, $doit) = @$chset;
458         next if $doit ne 'on';
459         
460         # parse string list into array list
461         my @holdids = split(',', $holdids);
462         
463         if ($type =~ /_notify/){
464             # translate true/false string into 1/0
465             $old = $old eq 'true' ? 1 : 0;
466             $new = $new eq 'true' ? 1 : 0;
467         }
468
469         my $update;
470         if ($type eq 'sms_info') {
471             if ($new->{carrier} eq '' && $new->{sms} eq '') {
472                 # clear SMS number first to avoid check contrainst issue
473                 $update = $U->simplereq('open-ils.circ', "open-ils.circ.holds.batch_update_holds_by_notify",
474                     $e->authtoken, $e->requestor->id, [@holdids], $old->{sms}, $new->{sms}, 'default_sms');
475                 push (@updates, $update) if (scalar(@$update) > 0);
476                 $update = $U->simplereq('open-ils.circ', "open-ils.circ.holds.batch_update_holds_by_notify",
477                     $e->authtoken, $e->requestor->id, [@holdids], $old->{carrier}, $new->{carrier}, 'default_sms_carrier_id');
478                 push (@updates, $update) if (scalar(@$update) > 0);
479             } else {
480                 $update = $U->simplereq('open-ils.circ', "open-ils.circ.holds.batch_update_holds_by_notify",
481                     $e->authtoken, $e->requestor->id, [@holdids], $old->{carrier}, $new->{carrier}, 'default_sms_carrier_id');
482                 push (@updates, $update) if (scalar(@$update) > 0);
483                 $update = $U->simplereq('open-ils.circ', "open-ils.circ.holds.batch_update_holds_by_notify",
484                     $e->authtoken, $e->requestor->id, [@holdids], $old->{sms}, $new->{sms}, 'default_sms');
485                 push (@updates, $update) if (scalar(@$update) > 0);
486             }
487         } else {
488             $update = $U->simplereq('open-ils.circ', "open-ils.circ.holds.batch_update_holds_by_notify",
489                 $e->authtoken, $e->requestor->id, [@holdids], $old, $new, $type);
490
491             # append affected array to chset
492             if (scalar(@$update) > 0){
493                 push(@updates, $update);
494             }
495         }
496     }
497
498     $self->ctx->{'updated'} = \@updates;
499
500     return $self->_load_user_with_prefs || Apache2::Const::OK;
501
502 }
503
504 sub fetch_optin_prefs {
505     my $self = shift;
506     my $e = $self->editor;
507
508     # fetch all of the opt-in settings the user has access to
509     # XXX: user's should in theory have options to opt-in to notices
510     # for remote locations, but that opens the door for a large
511     # set of generally un-used opt-ins.. needs discussion
512     my $opt_ins =  $U->simplereq(
513         'open-ils.actor',
514         'open-ils.actor.event_def.opt_in.settings.atomic',
515         $e->authtoken, $e->requestor->home_ou);
516
517     # some opt-ins are staff-only
518     $opt_ins = [ grep { $U->is_true($_->opac_visible) } @$opt_ins ];
519
520     # fetch user setting values for each of the opt-in settings
521     my $user_set = $U->simplereq(
522         'open-ils.actor',
523         'open-ils.actor.patron.settings.retrieve',
524         $e->authtoken,
525         $e->requestor->id,
526         [map {$_->name} @$opt_ins]
527     );
528
529     return [map { {cust => $_, value => $user_set->{$_->name} } } @$opt_ins];
530 }
531
532 sub load_myopac_messages {
533     my $self = shift;
534     my $e = $self->editor;
535     my $ctx = $self->ctx;
536     my $cgi = $self->cgi;
537
538     my $limit  = $cgi->param('limit') || 20;
539     my $offset = $cgi->param('offset') || 0;
540
541     my $pcrud = OpenSRF::AppSession->create('open-ils.pcrud');
542     $pcrud->connect();
543
544     my $action = $cgi->param('action') || '';
545     if ($action) {
546         my ($changed, $failed) = $self->_handle_message_action($pcrud, $action);
547         if ($changed > 0 || $failed > 0) {
548             $ctx->{message_update_action} = $action;
549             $ctx->{message_update_changed} = $changed;
550             $ctx->{message_update_failed} = $failed;
551             $self->update_dashboard_stats();
552         }
553     }
554
555     my $single = $cgi->param('single') || 0;
556     my $id = $cgi->param('message_id');
557
558     my $messages;
559     my $fetch_all = 1;
560     if (!$action && $single && $id) {
561         $messages = $self->_fetch_and_mark_read_single_message($pcrud, $id);
562         if (scalar(@$messages) == 1) {
563             $ctx->{display_single_message} = 1;
564             $ctx->{patron_message_id} = $id;
565             $fetch_all = 0;
566         }
567     }
568
569     if ($fetch_all) {
570         # fetch all the messages
571         ($ctx->{patron_messages_count}, $messages) =
572             $self->_fetch_user_messages($pcrud, $offset, $limit);
573     }
574
575     $pcrud->kill_me;
576
577     foreach my $aum (@$messages) {
578
579         push @{ $ctx->{patron_messages} }, {
580             id          => $aum->id,
581             title       => $aum->title,
582             message     => $aum->message,
583             create_date => $aum->create_date,
584             is_read     => defined($aum->read_date) ? 1 : 0,
585             library     => $aum->sending_lib->name,
586         };
587     }
588
589     $ctx->{patron_messages_limit} = $limit;
590     $ctx->{patron_messages_offset} = $offset;
591
592     return Apache2::Const::OK;
593 }
594
595 sub _fetch_and_mark_read_single_message {
596     my $self = shift;
597     my $pcrud = shift;
598     my $id = shift;
599
600     $pcrud->request('open-ils.pcrud.transaction.begin', $self->editor->authtoken)->gather(1);
601     my $messages = $pcrud->request(
602         'open-ils.pcrud.search.auml.atomic',
603         $self->editor->authtoken,
604         {
605             usr     => $self->editor->requestor->id,
606             deleted => 'f',
607             id      => $id,
608         },
609         {
610             flesh => 1,
611             flesh_fields => { auml => ['sending_lib'] },
612         }
613     )->gather(1);
614     if (@$messages) {
615         $messages->[0]->read_date('now');
616         $pcrud->request(
617             'open-ils.pcrud.update.auml',
618             $self->editor->authtoken,
619             $messages->[0]
620         )->gather(1);
621     }
622     $pcrud->request('open-ils.pcrud.transaction.commit', $self->editor->authtoken)->gather(1);
623
624     $self->update_dashboard_stats();
625
626     return $messages;
627 }
628
629 sub _fetch_user_messages {
630     my $self = shift;
631     my $pcrud = shift;
632     my $offset = shift;
633     my $limit = shift;
634
635     my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
636
637     my $all_messages = $pcrud->request(
638         'open-ils.pcrud.id_list.auml.atomic',
639         $self->editor->authtoken,
640         {
641             usr     => $self->editor->requestor->id,
642             deleted => 'f'
643         },
644         {}
645     )->gather(1);
646
647     my $messages = $pcrud->request(
648         'open-ils.pcrud.search.auml.atomic',
649         $self->editor->authtoken,
650         {
651             usr     => $self->editor->requestor->id,
652             deleted => 'f'
653         },
654         {
655             flesh => 1,
656             flesh_fields => { auml => ['sending_lib'] },
657             order_by => { auml => 'create_date DESC' },
658             %paging
659         }
660     )->gather(1);
661
662     return scalar(@$all_messages), $messages;
663 }
664
665 sub _handle_message_action {
666     my $self = shift;
667     my $pcrud = shift;
668     my $action = shift;
669     my $cgi = $self->cgi;
670
671     my @ids = $cgi->param('message_id');
672     return (0, 0) unless @ids;
673
674     my $changed = 0;
675     my $failed = 0;
676     $pcrud->request('open-ils.pcrud.transaction.begin', $self->editor->authtoken)->gather(1);
677     for my $id (@ids) {
678         my $aum = $pcrud->request(
679             'open-ils.pcrud.retrieve.auml',
680             $self->editor->authtoken,
681             $id
682         )->gather(1);
683         next unless $aum;
684         if      ($action eq 'mark_read') {
685             $aum->read_date('now');
686         } elsif ($action eq 'mark_unread') {
687             $aum->clear_read_date();
688         } elsif ($action eq 'mark_deleted') {
689             $aum->deleted('t');
690         }
691         $pcrud->request('open-ils.pcrud.update.auml', $self->editor->authtoken, $aum)->gather(1) ?
692             $changed++ :
693             $failed++;
694     }
695     if ($failed) {
696         $pcrud->request('open-ils.pcrud.transaction.rollback', $self->editor->authtoken)->gather(1);
697         $changed = 0;
698         $failed = scalar(@ids);
699     } else {
700         $pcrud->request('open-ils.pcrud.transaction.commit', $self->editor->authtoken)->gather(1);
701     }
702     return ($changed, $failed);
703 }
704
705 sub _load_lists_and_settings {
706     my $self = shift;
707     my $e = $self->editor;
708     my $stat = $self->_load_user_with_prefs;
709     unless ($stat) {
710         my $exclude = 0;
711         my $setting_map = $self->ctx->{user_setting_map};
712         $exclude = $$setting_map{'opac.default_list'} if ($$setting_map{'opac.default_list'});
713         $self->ctx->{bookbags} = $e->search_container_biblio_record_entry_bucket(
714             [
715                 {owner => $self->ctx->{user}->id, btype => 'bookbag', id => {'<>' => $exclude}}, {
716                     order_by => {cbreb => 'name'},
717                     limit => $self->cgi->param('limit') || 10,
718                     offset => $self->cgi->param('offset') || 0
719                 }
720             ]
721         );
722         # We also want a total count of the user's bookbags.
723         my $q = {
724             'select' => { 'cbreb' => [ { 'column' => 'id', 'transform' => 'count', 'aggregate' => 'true', 'alias' => 'count' } ] },
725             'from' => 'cbreb',
726             'where' => { 'btype' => 'bookbag', 'owner' => $self->ctx->{user}->id }
727         };
728         my $r = $e->json_query($q);
729         $self->ctx->{bookbag_count} = $r->[0]->{'count'};
730         # Someone has requested that we use the default list's name
731         # rather than "Default List."
732         if ($exclude) {
733             $q = {
734                 'select' => {'cbreb' => ['name']},
735                 'from' => 'cbreb',
736                 'where' => {'id' => $exclude}
737             };
738             $r = $e->json_query($q);
739             $self->ctx->{default_bookbag} = $r->[0]->{'name'};
740         }
741     } else {
742         return $stat;
743     }
744     return undef;
745 }
746
747 sub update_optin_prefs {
748     my $self = shift;
749     my $user_prefs = shift;
750     my $e = $self->editor;
751     my @settings = $self->cgi->param('setting');
752     my %newsets;
753
754     # apply now-true settings
755     for my $applied (@settings) {
756         # see if setting is already applied to this user
757         next if grep { $_->{cust}->name eq $applied and $_->{value} } @$user_prefs;
758         $newsets{$applied} = OpenSRF::Utils::JSON->true;
759     }
760
761     # remove now-false settings
762     for my $pref (grep { $_->{value} } @$user_prefs) {
763         $newsets{$pref->{cust}->name} = undef
764             unless grep { $_ eq $pref->{cust}->name } @settings;
765     }
766
767     $U->simplereq(
768         'open-ils.actor',
769         'open-ils.actor.patron.settings.update',
770         $e->authtoken, $e->requestor->id, \%newsets);
771
772     # update the local prefs to match reality
773     for my $pref (@$user_prefs) {
774         $pref->{value} = $newsets{$pref->{cust}->name}
775             if exists $newsets{$pref->{cust}->name};
776     }
777
778     return $user_prefs;
779 }
780
781 sub _load_user_with_prefs {
782     my $self = shift;
783     my $stat = $self->prepare_extended_user_info('settings');
784     return $stat if $stat; # not-OK
785
786     $self->ctx->{user_setting_map} = {
787         map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) }
788             @{$self->ctx->{user}->settings}
789     };
790
791     return undef;
792 }
793
794 sub _get_bookbag_sort_params {
795     my ($self, $param_name) = @_;
796
797     # The interface that feeds this cgi parameter will provide a single
798     # argument for a QP sort filter, and potentially a modifier after a period.
799     # In practice this means the "sort" parameter will be something like
800     # "titlesort" or "authorsort.descending".
801     my $sorter = $self->cgi->param($param_name) || "";
802     my $modifier;
803     if ($sorter) {
804         $sorter =~ s/^(.*?)\.(.*)/$1/;
805         $modifier = $2 || undef;
806     }
807
808     return ($sorter, $modifier);
809 }
810
811 sub _prepare_bookbag_container_query {
812     my ($self, $container_id, $sorter, $modifier) = @_;
813
814     return sprintf(
815         "container(bre,bookbag,%d,%s)%s%s",
816         $container_id, $self->editor->authtoken,
817         ($sorter ? " sort($sorter)" : ""),
818         ($modifier ? "#$modifier" : "")
819     );
820 }
821
822 sub _prepare_anonlist_sorting_query {
823     my ($self, $list, $sorter, $modifier) = @_;
824
825     return sprintf(
826         "record_list(%s)%s%s",
827         join(",", @$list),
828         ($sorter ? " sort($sorter)" : ""),
829         ($modifier ? "#$modifier" : "")
830     );
831 }
832
833
834 sub load_myopac_prefs_settings {
835     my $self = shift;
836
837     my @user_prefs = qw/
838         opac.hits_per_page
839         opac.default_search_location
840         opac.default_pickup_location
841         opac.temporary_list_no_warn
842     /;
843
844     my $stat = $self->_load_user_with_prefs;
845     return $stat if $stat;
846
847     # if behind-desk holds are supported and the user
848     # setting which controls the value is opac-visible,
849     # add the setting to the list of settings to manage.
850     # note: this logic may need to be changed later to
851     # check whether behind-the-desk holds are supported
852     # anywhere the patron may select as a pickup lib.
853     my $e = $self->editor;
854     my $bdous = $self->ctx->{get_org_setting}->(
855         $e->requestor->home_ou,
856         'circ.holds.behind_desk_pickup_supported');
857
858     if ($bdous) {
859         my $setting =
860             $e->retrieve_config_usr_setting_type(
861                 'circ.holds_behind_desk');
862
863         if ($U->is_true($setting->opac_visible)) {
864             push(@user_prefs, 'circ.holds_behind_desk');
865             $self->ctx->{behind_desk_supported} = 1;
866         }
867     }
868
869     my $use_privacy_waiver = $self->ctx->{get_org_setting}->(
870         $e->requestor->home_ou, 'circ.privacy_waiver');
871
872     return Apache2::Const::OK
873         unless $self->cgi->request_method eq 'POST';
874
875     # some setting values from the form don't match the
876     # required value/format for the db, so they have to be
877     # individually translated.
878
879     my %settings;
880     my $set_map = $self->ctx->{user_setting_map};
881
882     foreach my $key (@user_prefs) {
883         my $val = $self->cgi->param($key);
884         $settings{$key}= $val unless $$set_map{$key} eq $val;
885     }
886
887     # Used by the settings update form when warning on history delete.
888     my $clear_circ_history = 0;
889     my $clear_hold_history = 0;
890
891     # true if we need to show the warning on next page load.
892     my $hist_warning_needed = 0;
893     my $hist_clear_confirmed = $self->cgi->param('history_delete_confirmed');
894
895     my $now = DateTime->now->strftime('%F');
896     foreach my $key (
897             qw/history.circ.retention_start history.hold.retention_start/) {
898
899         my $val = $self->cgi->param($key);
900         if($val and $val eq 'on') {
901             # Set the start time to 'now' unless a start time already exists for the user
902             $settings{$key} = $now unless $$set_map{$key};
903
904         } else {
905
906             next unless $$set_map{$key}; # nothing to do
907
908             $clear_circ_history = 1 if $key =~ /circ/;
909             $clear_hold_history = 1 if $key =~ /hold/;
910
911             if (!$hist_clear_confirmed) {
912                 # when clearing circ history, only warn if history data exists.
913
914                 if ($clear_circ_history) {
915
916                     if ($self->fetch_user_circ_history(0, 1)->[0]) {
917                         $hist_warning_needed = 1;
918                         next; # no history updates while confirmation pending
919                     }
920
921                 } else {
922
923                     my $one_hold = $e->json_query({
924                         select => {
925                             au => [{
926                                 column => 'id',
927                                 transform => 'action.usr_visible_holds',
928                                 result_field => 'id'
929                             }]
930                         },
931                         from => 'au',
932                         where => {id => $e->requestor->id},
933                         limit => 1
934                     })->[0];
935
936                     if ($one_hold) {
937                         $hist_warning_needed = 1;
938                         next; # no history updates while confirmation pending
939                     }
940                 }
941             }
942
943             $settings{$key} = undef;
944
945             if ($key eq 'history.circ.retention_start') {
946                 # delete existing circulation history data.
947                 $U->simplereq(
948                     'open-ils.actor',
949                     'open-ils.actor.history.circ.clear',
950                     $self->editor->authtoken);
951             }
952         }
953     }
954
955     # Warn patrons before clearing circ/hold history
956     if ($hist_warning_needed) {
957         $self->ctx->{clear_circ_history} = $clear_circ_history;
958         $self->ctx->{clear_hold_history} = $clear_hold_history;
959         $self->ctx->{confirm_history_delete} = 1;
960     }
961
962     # Send the modified settings off to be saved
963     $U->simplereq(
964         'open-ils.actor',
965         'open-ils.actor.patron.settings.update',
966         $self->editor->authtoken, undef, \%settings);
967
968     $self->ctx->{updated_user_settings} = \%settings;
969
970     if ($use_privacy_waiver) {
971         my %waiver;
972         my $saved_entries = ();
973         my @waiver_types = qw/place_holds pickup_holds checkout_items view_history/;
974
975         # initialize our waiver hash with waiver IDs from hidden input
976         # (this ensures that we capture entries with no checked boxes)
977         foreach my $waiver_row_id ($self->cgi->param("waiver_id")) {
978             $waiver{$waiver_row_id} = {};
979         }
980
981         # process our waiver checkboxes into a hash, keyed by waiver ID
982         # (a new entry, if any, has id = 'new')
983         foreach my $waiver_type (@waiver_types) {
984             if ($self->cgi->param("waiver_$waiver_type")) {
985                 foreach my $waiver_id ($self->cgi->param("waiver_$waiver_type")) {
986                     # ensure this waiver exists in our hash
987                     $waiver{$waiver_id} = {} if !$waiver{$waiver_id};
988                     $waiver{$waiver_id}->{$waiver_type} = 1;
989                 }
990             }
991         }
992
993         foreach my $k (keys %waiver) {
994             my $w = $waiver{$k};
995             # get name from textbox
996             $w->{name} = $self->cgi->param("waiver_name_$k");
997             $w->{id} = $k;
998             foreach (@waiver_types) {
999                 $w->{$_} = 0 unless ($w->{$_});
1000             }
1001             push @$saved_entries, $w;
1002         }
1003
1004         # update patron privacy waiver entries
1005         $U->simplereq(
1006             'open-ils.actor',
1007             'open-ils.actor.patron.privacy_waiver.update',
1008             $self->editor->authtoken, undef, $saved_entries);
1009
1010         $self->ctx->{updated_waiver_entries} = $saved_entries;
1011     }
1012
1013     # re-fetch user prefs
1014     return $self->_load_user_with_prefs || Apache2::Const::OK;
1015 }
1016
1017 sub load_myopac_prefs_my_lists {
1018     my $self = shift;
1019
1020     my @user_prefs = qw/
1021         opac.lists_per_page
1022         opac.list_items_per_page
1023     /;
1024
1025     my $stat = $self->_load_user_with_prefs;
1026     return $stat if $stat;
1027
1028     return Apache2::Const::OK
1029         unless $self->cgi->request_method eq 'POST';
1030
1031     my %settings;
1032     my $set_map = $self->ctx->{user_setting_map};
1033
1034     foreach my $key (@user_prefs) {
1035         my $val = $self->cgi->param($key);
1036         $settings{$key}= $val unless $$set_map{$key} eq $val;
1037     }
1038
1039     if (keys %settings) { # we found a different setting value
1040         # Send the modified settings off to be saved
1041         $U->simplereq(
1042             'open-ils.actor',
1043             'open-ils.actor.patron.settings.update',
1044             $self->editor->authtoken, undef, \%settings);
1045
1046         # re-fetch user prefs
1047         $self->ctx->{updated_user_settings} = \%settings;
1048         $stat = $self->_load_user_with_prefs;
1049     }
1050
1051     return $stat || Apache2::Const::OK;
1052 }
1053
1054 sub fetch_user_holds {
1055     my $self = shift;
1056     my $hold_ids = shift;
1057     my $ids_only = shift;
1058     my $flesh = shift;
1059     my $available = shift;
1060     my $limit = shift;
1061     my $offset = shift;
1062
1063     my $e = $self->editor;
1064     my $all_ids; # to be used below.
1065
1066     if(!$hold_ids) {
1067         my $circ = OpenSRF::AppSession->create('open-ils.circ');
1068
1069         $hold_ids = $circ->request(
1070             'open-ils.circ.holds.id_list.retrieve.authoritative',
1071             $e->authtoken,
1072             $e->requestor->id,
1073             $available
1074         )->gather(1);
1075         $circ->kill_me;
1076
1077         $all_ids = $hold_ids;
1078         $hold_ids = [ grep { defined $_ } @$hold_ids[$offset..($offset + $limit - 1)] ] if $limit or $offset;
1079
1080     } else {
1081         $all_ids = $hold_ids;
1082     }
1083
1084     return { ids => $hold_ids, all_ids => $all_ids } if $ids_only or @$hold_ids == 0;
1085
1086     my $args = {
1087         suppress_notices => 1,
1088         suppress_transits => 1,
1089         suppress_mvr => 1,
1090         suppress_patron_details => 1
1091     };
1092
1093     # ----------------------------------------------------------------
1094     # Collect holds in batches of $batch_size for faster retrieval
1095
1096     my $batch_size = 8;
1097     my $batch_idx = 0;
1098     my $mk_req_batch = sub {
1099         my @ses;
1100         my $top_idx = $batch_idx + $batch_size;
1101         while($batch_idx < $top_idx) {
1102             my $hold_id = $hold_ids->[$batch_idx++];
1103             last unless $hold_id;
1104             my $ses = OpenSRF::AppSession->create('open-ils.circ');
1105             my $req = $ses->request(
1106                 'open-ils.circ.hold.details.retrieve',
1107                 $e->authtoken, $hold_id, $args);
1108             push(@ses, {ses => $ses, req => $req});
1109         }
1110         return @ses;
1111     };
1112
1113     my $first = 1;
1114     my(@collected, @holds, @ses);
1115
1116     while(1) {
1117         @ses = $mk_req_batch->() if $first;
1118         last if $first and not @ses;
1119
1120         if(@collected) {
1121             while(my $blob = pop(@collected)) {
1122                 my @data;
1123
1124                 # in the holds edit UI, we need to know what formats and
1125                 # languages the user selected for this hold, plus what
1126                 # formats/langs are available on the MR as a whole.
1127                 if ($blob->{hold}{hold}->hold_type eq 'M') {
1128                     my $hold = $blob->{hold}->{hold};
1129
1130                     # for MR, fetch the combined MR unapi blob
1131                     (undef, @data) = $self->get_records_and_facets(
1132                         [$hold->target], undef, {flesh => '{mra}', metarecord => 1});
1133
1134                     my $filter_org = $U->org_unit_ancestor_at_depth(
1135                         $hold->selection_ou,
1136                         $hold->selection_depth);
1137
1138                     my $filter_data = $U->simplereq(
1139                         'open-ils.circ',
1140                         'open-ils.circ.mmr.holds.filters.authoritative.atomic',
1141                         $hold->target, $filter_org, [$hold->id]
1142                     );
1143
1144                     $blob->{metarecord_filters} =
1145                         $filter_data->[0]->{metarecord};
1146                     $blob->{metarecord_selected_filters} =
1147                         $filter_data->[1]->{hold};
1148                 } else {
1149
1150                     (undef, @data) = $self->get_records_and_facets(
1151                         [$blob->{hold}->{bre_id}], undef, {flesh => '{mra}'}
1152                     );
1153                 }
1154
1155                 $blob->{marc_xml} = $data[0]->{marc_xml};
1156                 push(@holds, $blob);
1157             }
1158         }
1159
1160         for my $req_data (@ses) {
1161             push(@collected, {hold => $req_data->{req}->gather(1)});
1162             $req_data->{ses}->kill_me;
1163         }
1164
1165         @ses = $mk_req_batch->();
1166         last unless @collected or @ses;
1167         $first = 0;
1168     }
1169
1170     # put the holds back into the original server sort order
1171     my @sorted;
1172     for my $id (@$hold_ids) {
1173         push @sorted, grep { $_->{hold}->{hold}->id == $id } @holds;
1174     }
1175
1176     return { holds => \@sorted, ids => $hold_ids, all_ids => $all_ids };
1177 }
1178
1179 sub handle_hold_update {
1180     my $self = shift;
1181     my $action = shift;
1182     my $hold_ids = shift;
1183     my $e = $self->editor;
1184     my $url;
1185
1186     my @hold_ids = ($hold_ids) ? @$hold_ids : $self->cgi->param('hold_id'); # for non-_all actions
1187     @hold_ids = @{$self->fetch_user_holds(undef, 1)->{ids}} if $action =~ /_all/;
1188
1189     my $circ = OpenSRF::AppSession->create('open-ils.circ');
1190
1191     if($action =~ /cancel/) {
1192
1193         for my $hold_id (@hold_ids) {
1194             my $resp = $circ->request(
1195                 'open-ils.circ.hold.cancel', $e->authtoken, $hold_id, 6 )->gather(1); # 6 == patron-cancelled-via-opac
1196         }
1197
1198     } elsif ($action =~ /activate|suspend/) {
1199
1200         my $vlist = [];
1201         for my $hold_id (@hold_ids) {
1202             my $vals = {id => $hold_id};
1203
1204             if($action =~ /activate/) {
1205                 $vals->{frozen} = 'f';
1206                 $vals->{thaw_date} = undef;
1207
1208             } elsif($action =~ /suspend/) {
1209                 $vals->{frozen} = 't';
1210                 # $vals->{thaw_date} = TODO;
1211             }
1212             push(@$vlist, $vals);
1213         }
1214
1215         my $resp = $circ->request('open-ils.circ.hold.update.batch.atomic', $e->authtoken, undef, $vlist)->gather(1);
1216         $self->ctx->{hold_suspend_post_capture} = 1 if
1217             grep {$U->event_equals($_, 'HOLD_SUSPEND_AFTER_CAPTURE')} @$resp;
1218
1219     } elsif ($action eq 'edit') {
1220
1221         my @vals = map {
1222             my $val = {"id" => $_};
1223             $val->{"frozen"} = $self->cgi->param("frozen");
1224             $val->{"pickup_lib"} = $self->cgi->param("pickup_lib");
1225             $val->{"email_notify"} = $self->cgi->param("email_notify") ? 1 : 0;
1226             $val->{"phone_notify"} = $self->cgi->param("phone_notify");
1227             $val->{"sms_notify"} = $self->cgi->param("sms_notify");
1228             $val->{"sms_carrier"} = int($self->cgi->param("sms_carrier")) if $val->{"sms_notify"};
1229
1230             for my $field (qw/expire_time thaw_date/) {
1231                 # XXX TODO make this support other date formats, not just
1232                 # MM/DD/YYYY.
1233                 next unless $self->cgi->param($field) =~
1234                     m:^(\d{2})/(\d{2})/(\d{4})$:;
1235                 $val->{$field} = "$3-$1-$2";
1236             }
1237
1238             $val->{holdable_formats} = # no-op for non-MR holds
1239                 $self->compile_holdable_formats(undef, $_);
1240
1241             $val;
1242         } @hold_ids;
1243
1244         $circ->request(
1245             'open-ils.circ.hold.update.batch.atomic',
1246             $e->authtoken, undef, \@vals
1247         )->gather(1);   # LFW XXX test for failure
1248         $url = $self->ctx->{proto} . '://' . $self->ctx->{hostname} . $self->ctx->{opac_root} . '/myopac/holds';
1249         foreach my $param (('loc', 'qtype', 'query')) {
1250             if ($self->cgi->param($param)) {
1251                 my @vals = $self->cgi->param($param);
1252                 $url .= ";$param=" . uri_escape_utf8($_) foreach @vals;
1253             }
1254         }
1255     }
1256
1257     $circ->kill_me;
1258     return defined($url) ? $self->generic_redirect($url) : undef;
1259 }
1260
1261 sub load_myopac_holds {
1262     my $self = shift;
1263     my $e = $self->editor;
1264     my $ctx = $self->ctx;
1265
1266     my $limit = $self->cgi->param('limit') || 15;
1267     my $offset = $self->cgi->param('offset') || 0;
1268     my $action = $self->cgi->param('action') || '';
1269     my $hold_id = $self->cgi->param('hid');
1270     my $available = int($self->cgi->param('available') || 0);
1271
1272     my $hold_handle_result;
1273     $hold_handle_result = $self->handle_hold_update($action) if $action;
1274
1275     my $holds_object;
1276     if ($self->cgi->param('sort') ne "") {
1277         $holds_object = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available);
1278     }
1279     else {
1280         $holds_object = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available, $limit, $offset);
1281     }
1282
1283     if($holds_object->{holds}) {
1284         $ctx->{holds} = $holds_object->{holds};
1285     }
1286     $ctx->{holds_ids} = $holds_object->{all_ids};
1287     $ctx->{holds_limit} = $limit;
1288     $ctx->{holds_offset} = $offset;
1289
1290     return defined($hold_handle_result) ? $hold_handle_result : Apache2::Const::OK;
1291 }
1292
1293 my $data_filler;
1294
1295 sub load_place_hold {
1296     my $self = shift;
1297     my $ctx = $self->ctx;
1298     my $gos = $ctx->{get_org_setting};
1299     my $e = $self->editor;
1300     my $cgi = $self->cgi;
1301
1302     $self->ctx->{page} = 'place_hold';
1303     my @targets = uniq $cgi->param('hold_target');
1304     my @parts = $cgi->param('part');
1305
1306     $ctx->{hold_type} = $cgi->param('hold_type');
1307     $ctx->{default_pickup_lib} = $e->requestor->home_ou; # unless changed below
1308     $ctx->{email_notify} = $cgi->param('email_notify');
1309     if ($cgi->param('phone_notify_checkbox')) {
1310         $ctx->{phone_notify} = $cgi->param('phone_notify');
1311     }
1312     if ($cgi->param('sms_notify_checkbox')) {
1313         $ctx->{sms_notify} = $cgi->param('sms_notify');
1314         $ctx->{sms_carrier} = $cgi->param('sms_carrier');
1315     }
1316
1317     return $self->generic_redirect unless @targets;
1318
1319     # Check for multiple hold placement via the num_copies widget.
1320     my $num_copies = int($cgi->param('num_copies')); # if undefined, we get 0.
1321     if ($num_copies > 1) {
1322         # Only if we have 1 hold target and no parts.
1323         if (scalar(@targets) == 1 && !$parts[0]) {
1324             # Also, only for M and T holds.
1325             if ($ctx->{hold_type} eq 'M' || $ctx->{hold_type} eq 'T') {
1326                 # Add the extra holds to @targets. NOTE: We start with
1327                 # 1 and go to < $num_copies to account for the
1328                 # existing target.
1329                 for (my $i = 1; $i < $num_copies; $i++) {
1330                     push(@targets, $targets[0]);
1331                 }
1332             }
1333         }
1334     }
1335
1336     $logger->info("Looking at hold_type: " . $ctx->{hold_type} . " and targets: @targets");
1337
1338     $ctx->{staff_recipient} = $self->editor->retrieve_actor_user([
1339         $e->requestor->id,
1340         {
1341             flesh => 1,
1342             flesh_fields => {
1343                 au => ['settings', 'card']
1344             }
1345         }
1346     ]) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
1347     my $user_setting_map = {
1348         map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) }
1349             @{
1350                 $ctx->{staff_recipient}->settings
1351             }
1352     };
1353     $ctx->{user_setting_map} = $user_setting_map;
1354
1355     my $default_notify = (defined $$user_setting_map{'opac.hold_notify'} ? $$user_setting_map{'opac.hold_notify'} : 'email:phone');
1356     if ($default_notify =~ /email/) {
1357         $ctx->{default_email_notify} = 'checked';
1358     } else {
1359         $ctx->{default_email_notify} = '';
1360     }
1361     if ($default_notify =~ /phone/) {
1362         $ctx->{default_phone_notify} = 'checked';
1363     } else {
1364         $ctx->{default_phone_notify} = '';
1365     }
1366     if ($default_notify =~ /sms/) {
1367         $ctx->{default_sms_notify} = 'checked';
1368     } else {
1369         $ctx->{default_sms_notify} = '';
1370     }
1371     if ($cgi->param('hold_suspend')) {
1372         $ctx->{frozen} = 1;
1373         # TODO: Make this support other date formats, not just mm/dd/yyyy.
1374         # We should use a date input type on the forms once it is supported by Firefox.
1375         # I didn't do that now because it is not available in a general release.
1376         if ($cgi->param('thaw_date') =~ m:^(\d{2})/(\d{2})/(\d{4})$:){
1377             eval {
1378                 my $dt = DateTime::Format::ISO8601->parse_datetime("$3-$1-$2");
1379                 $ctx->{thaw_date} = $dt->ymd;
1380             };
1381             if ($@) {
1382                 $logger->warn("ignoring invalid thaw_date when placing hold request");
1383             }
1384         }
1385     }
1386
1387
1388     # If we have a default pickup location, grab it
1389     if ($$user_setting_map{'opac.default_pickup_location'}) {
1390         $ctx->{default_pickup_lib} = $$user_setting_map{'opac.default_pickup_location'};
1391     }
1392
1393     my $request_lib = $e->requestor->ws_ou;
1394     my @hold_data;
1395     $ctx->{hold_data} = \@hold_data;
1396
1397     $data_filler = sub {
1398         my $hdata = shift;
1399         if ($ctx->{email_notify}) { $hdata->{email_notify} = $ctx->{email_notify}; }
1400         if ($ctx->{phone_notify}) { $hdata->{phone_notify} = $ctx->{phone_notify}; }
1401         if ($ctx->{sms_notify}) { $hdata->{sms_notify} = $ctx->{sms_notify}; }
1402         if ($ctx->{sms_carrier}) { $hdata->{sms_carrier} = $ctx->{sms_carrier}; }
1403         if ($ctx->{frozen}) { $hdata->{frozen} = 1; }
1404         if ($ctx->{thaw_date}) { $hdata->{thaw_date} = $ctx->{thaw_date}; }
1405         return $hdata;
1406     };
1407
1408     my $type_dispatch = {
1409         M => sub {
1410             # target metarecords
1411             my $mrecs = $e->batch_retrieve_metabib_metarecord([
1412                 \@targets,
1413                 {flesh => 1, flesh_fields => {mmr => ['master_record']}}],
1414                 {substream => 1}
1415             );
1416
1417             for my $id (@targets) {
1418                 my ($mr) = grep {$_->id eq $id} @$mrecs;
1419
1420                 my $ou_id = $cgi->param('pickup_lib') || $self->ctx->{search_ou};
1421                 my $filter_data = $U->simplereq(
1422                     'open-ils.circ',
1423                     'open-ils.circ.mmr.holds.filters.authoritative', $mr->id, $ou_id);
1424
1425                 my $holdable_formats =
1426                     $self->compile_holdable_formats($mr->id);
1427
1428                 push(@hold_data, $data_filler->({
1429                     target => $mr,
1430                     record => $mr->master_record,
1431                     holdable_formats => $holdable_formats,
1432                     metarecord_filters => $filter_data->{metarecord}
1433                 }));
1434             }
1435         },
1436         T => sub {
1437             my $recs = $e->batch_retrieve_biblio_record_entry(
1438                 [\@targets,  {flesh => 1, flesh_fields => {bre => ['metarecord']}}],
1439                 {substream => 1}
1440             );
1441
1442             for my $id (@targets) { # force back into the correct order
1443                 my ($rec) = grep {$_->id eq $id} @$recs;
1444
1445                 # NOTE: if tpac ever supports locked-down pickup libs,
1446                 # we'll need to pass a pickup_lib param along with the
1447                 # record to filter the set of monographic parts.
1448                 my $parts = $U->simplereq(
1449                     'open-ils.search',
1450                     'open-ils.search.biblio.record_hold_parts',
1451                     {record => $rec->id}
1452                 );
1453
1454                 # T holds on records that have parts are OK, but if the record has
1455                 # no non-part copies, the hold will ultimately fail.  When that
1456                 # happens, require the user to select a part.
1457                 my $part_required = 0;
1458                 if (@$parts) {
1459                     my $np_copies = $e->json_query({
1460                         select => { acp => [{column => 'id', transform => 'count', alias => 'count'}]},
1461                         from => {acp => {acn => {}, acpm => {type => 'left'}}},
1462                         where => {
1463                             '+acp' => {deleted => 'f'},
1464                             '+acn' => {deleted => 'f', record => $rec->id},
1465                             '+acpm' => {id => undef}
1466                         }
1467                     });
1468                     $part_required = 1 if $np_copies->[0]->{count} == 0;
1469                 }
1470
1471                 push(@hold_data, $data_filler->({
1472                     target => $rec,
1473                     record => $rec,
1474                     parts => $parts,
1475                     part_required => $part_required
1476                 }));
1477             }
1478         },
1479         V => sub {
1480             my $vols = $e->batch_retrieve_asset_call_number([
1481                 \@targets, {
1482                     "flesh" => 1,
1483                     "flesh_fields" => {"acn" => ["record"]}
1484                 }
1485             ], {substream => 1});
1486
1487             for my $id (@targets) {
1488                 my ($vol) = grep {$_->id eq $id} @$vols;
1489                 push(@hold_data, $data_filler->({target => $vol, record => $vol->record}));
1490             }
1491         },
1492         C => sub {
1493             my $copies = $e->batch_retrieve_asset_copy([
1494                 \@targets, {
1495                     "flesh" => 2,
1496                     "flesh_fields" => {
1497                         "acn" => ["record"],
1498                         "acp" => ["call_number"]
1499                     }
1500                 }
1501             ], {substream => 1});
1502
1503             for my $id (@targets) {
1504                 my ($copy) = grep {$_->id eq $id} @$copies;
1505                 push(@hold_data, $data_filler->({target => $copy, record => $copy->call_number->record}));
1506             }
1507         },
1508         I => sub {
1509             my $isses = $e->batch_retrieve_serial_issuance([
1510                 \@targets, {
1511                     "flesh" => 2,
1512                     "flesh_fields" => {
1513                         "siss" => ["subscription"], "ssub" => ["record_entry"]
1514                     }
1515                 }
1516             ], {substream => 1});
1517
1518             for my $id (@targets) {
1519                 my ($iss) = grep {$_->id eq $id} @$isses;
1520                 push(@hold_data, $data_filler->({target => $iss, record => $iss->subscription->record_entry}));
1521             }
1522         }
1523         # ...
1524
1525     }->{$ctx->{hold_type}}->();
1526
1527     # caller sent bad target IDs or the wrong hold type
1528     return Apache2::Const::HTTP_BAD_REQUEST unless @hold_data;
1529
1530     # generate the MARC xml for each record
1531     $_->{marc_xml} = XML::LibXML->new->parse_string($_->{record}->marc) for @hold_data;
1532
1533     my $pickup_lib = $cgi->param('pickup_lib');
1534     # no pickup lib means no holds placement
1535     return Apache2::Const::OK unless $pickup_lib;
1536
1537     $ctx->{hold_attempt_made} = 1;
1538
1539     # Give the original CGI params back to the user in case they
1540     # want to try to override something.
1541     $ctx->{orig_params} = $cgi->Vars;
1542     delete $ctx->{orig_params}{submit};
1543     delete $ctx->{orig_params}{hold_target};
1544     delete $ctx->{orig_params}{part};
1545
1546     my $usr = $e->requestor->id;
1547
1548     if ($ctx->{is_staff} and !$cgi->param("hold_usr_is_requestor")) {
1549         # find the real hold target
1550
1551         $usr = $U->simplereq(
1552             'open-ils.actor',
1553             "open-ils.actor.user.retrieve_id_by_barcode_or_username",
1554             $e->authtoken, $cgi->param("hold_usr"));
1555
1556         if (defined $U->event_code($usr)) {
1557             $ctx->{hold_failed} = 1;
1558             $ctx->{hold_failed_event} = $usr;
1559         }
1560     }
1561
1562     # target_id is the true target_id for holds placement.
1563     # needed for attempt_hold_placement()
1564     # With the exception of P-type holds, target_id == target->id.
1565     $_->{target_id} = $_->{target}->id for @hold_data;
1566
1567     if ($ctx->{hold_type} eq 'T') {
1568
1569         # Much like quantum wave-particles, P-type holds pop into
1570         # and out of existence at the user's whim.  For our purposes,
1571         # we treat such holds as T(itle) holds with a selected_part
1572         # designation.  When the time comes to pass the hold information
1573         # off for holds possibility testing and placement, make it look
1574         # like a real P-type hold.
1575         my (@p_holds, @t_holds);
1576
1577         # Now that we have the num_copies field for mutliple title and
1578         # metarecord hold placement, the number of holds and parts
1579         # arrays can get out of sync.  We only want to parse out parts
1580         # if the numbers are equal.
1581         if ($#hold_data == $#parts) {
1582             for my $idx (0..$#parts) {
1583                 my $hdata = $hold_data[$idx];
1584                 if (my $part = $parts[$idx]) {
1585                     $hdata->{target_id} = $part;
1586                     $hdata->{selected_part} = $part;
1587                     push(@p_holds, $hdata);
1588                 } else {
1589                     push(@t_holds, $hdata);
1590                 }
1591             }
1592         } else {
1593             @t_holds = @hold_data;
1594         }
1595
1596         $self->apache->log->warn("$#parts : @t_holds");
1597
1598         $self->attempt_hold_placement($usr, $pickup_lib, 'P', @p_holds) if @p_holds;
1599         $self->attempt_hold_placement($usr, $pickup_lib, 'T', @t_holds) if @t_holds;
1600
1601     } else {
1602         $self->attempt_hold_placement($usr, $pickup_lib, $ctx->{hold_type}, @hold_data);
1603     }
1604
1605     # NOTE: we are leaving the staff-placed patron barcode cookie
1606     # in place.  Otherwise, it's not possible to place more than
1607     # one hold for the patron within a staff/patron session.  This
1608     # does leave the barcode to linger longer than is ideal, but
1609     # normal staff work flow will cause the cookie to be replaced
1610     # with each new patron anyway.
1611     # TODO: See about getting the staff client to clear the cookie
1612
1613     # return to the place_hold page so the results of the hold
1614     # placement attempt can be reported to the user
1615     return Apache2::Const::OK;
1616 }
1617
1618 sub attempt_hold_placement {
1619     my ($self, $usr, $pickup_lib, $hold_type, @hold_data) = @_;
1620     my $cgi = $self->cgi;
1621     my $ctx = $self->ctx;
1622     my $e = $self->editor;
1623
1624     # First see if we should warn/block for any holds that
1625     # might have locally available items.
1626     for my $hdata (@hold_data) {
1627         my ($local_block, $local_alert) = $self->local_avail_concern(
1628             $hdata->{target_id}, $hold_type, $pickup_lib);
1629
1630         if ($local_block) {
1631             $hdata->{hold_failed} = 1;
1632             $hdata->{hold_local_block} = 1;
1633         } elsif ($local_alert) {
1634             $hdata->{hold_failed} = 1;
1635             $hdata->{hold_local_alert} = 1;
1636         }
1637     }
1638
1639     my $method = 'open-ils.circ.holds.test_and_create.batch';
1640
1641     if ($cgi->param('override')) {
1642         $method .= '.override';
1643
1644     } elsif (!$ctx->{is_staff})  {
1645
1646         $method .= '.override' if $self->ctx->{get_org_setting}->(
1647             $e->requestor->home_ou, "opac.patron.auto_overide_hold_events");
1648     }
1649
1650     my @create_targets = map {$_->{target_id}} (grep { !$_->{hold_failed} } @hold_data);
1651
1652
1653     if(@create_targets) {
1654
1655         # holdable formats may be different for each MR hold.
1656         # map each set to the ID of the target.
1657         my $holdable_formats = {};
1658         if ($hold_type eq 'M') {
1659             $holdable_formats->{$_->{target_id}} =
1660                 $_->{holdable_formats} for @hold_data;
1661         }
1662
1663         my $bses = OpenSRF::AppSession->create('open-ils.circ');
1664         my $breq = $bses->request(
1665             $method,
1666             $e->authtoken,
1667             $data_filler->({
1668                 patronid => $usr,
1669                 pickup_lib => $pickup_lib,
1670                 hold_type => $hold_type,
1671                 holdable_formats_map => $holdable_formats,
1672             }),
1673             \@create_targets
1674         );
1675
1676         while (my $resp = $breq->recv) {
1677
1678             $resp = $resp->content;
1679             $logger->info('batch hold placement result: ' . OpenSRF::Utils::JSON->perl2JSON($resp));
1680
1681             if ($U->event_code($resp)) {
1682                 $ctx->{general_hold_error} = $resp;
1683                 last;
1684             }
1685
1686             # Skip those that had the hold_success or hold_failed fields set for duplicate holds placement.
1687             my ($hdata) = grep {$_->{target_id} eq $resp->{target} && !($_->{hold_failed} || $_->{hold_success})} @hold_data;
1688             my $result = $resp->{result};
1689
1690             if ($U->event_code($result)) {
1691                 # e.g. permission denied
1692                 $hdata->{hold_failed} = 1;
1693                 $hdata->{hold_failed_event} = $result;
1694
1695             } else {
1696
1697                 if(not ref $result and $result > 0) {
1698                     # successul hold returns the hold ID
1699
1700                     $hdata->{hold_success} = $result;
1701
1702                 } else {
1703                     # hold-specific failure event
1704                     $hdata->{hold_failed} = 1;
1705
1706                     if (ref $result eq 'HASH') {
1707                         $hdata->{hold_failed_event} = $result->{last_event};
1708
1709                         if ($result->{age_protected_copy}) {
1710                             my %temp = %{$hdata->{hold_failed_event}};
1711                             my $theTextcode = $temp{"textcode"};
1712                             $theTextcode.=".override";
1713                             $hdata->{could_override} = $self->editor->allowed( $theTextcode );
1714                             $hdata->{age_protect} = 1;
1715                         } else {
1716                             $hdata->{could_override} = $result->{place_unfillable} ||
1717                                 $self->test_could_override($hdata->{hold_failed_event});
1718                         }
1719                     } elsif (ref $result eq 'ARRAY') {
1720                         $hdata->{hold_failed_event} = $result->[0];
1721
1722                         if ($result->[3]) { # age_protect_only
1723                             my %temp = %{$hdata->{hold_failed_event}};
1724                             my $theTextcode = $temp{"textcode"};
1725                             $theTextcode.=".override";
1726                             $hdata->{could_override} = $self->editor->allowed( $theTextcode );
1727                             $hdata->{age_protect} = 1;
1728                         } else {
1729                             $hdata->{could_override} = $result->[4] || # place_unfillable
1730                                 $self->test_could_override($hdata->{hold_failed_event});
1731                         }
1732                     }
1733                 }
1734             }
1735         }
1736
1737         $bses->kill_me;
1738     }
1739
1740     if ($self->cgi->param('clear_cart')) {
1741         $self->clear_anon_cache;
1742     }
1743 }
1744
1745 # pull the selected formats and languages for metarecord holds
1746 # from the CGI params and map them into the JSON holdable
1747 # formats...er, format.
1748 # if no metarecord is provided, we'll pull it from the target
1749 # of the provided hold.
1750 sub compile_holdable_formats {
1751     my ($self, $mr_id, $hold_id) = @_;
1752     my $e = $self->editor;
1753     my $cgi = $self->cgi;
1754
1755     # exit early if not needed
1756     return undef unless
1757         grep /metarecord_formats_|metarecord_langs_/,
1758         $cgi->param;
1759
1760     # CGI params are based on the MR id, since during hold placement
1761     # we have no old ID.  During hold edit, map the hold ID back to
1762     # the metarecod target.
1763     $mr_id =
1764         $e->retrieve_action_hold_request($hold_id)->target
1765         unless $mr_id;
1766
1767     my $format_attr = $self->ctx->{get_cgf}->(
1768         'opac.metarecord.holds.format_attr');
1769
1770     if (!$format_attr) {
1771         $logger->error("Missing config.global_flag: ".
1772             "opac.metarecord.holds.format_attr!");
1773         return "";
1774     }
1775
1776     $format_attr = $format_attr->value;
1777
1778     # during hold placement or edit submission, the user selects
1779     # which of the available formats/langs are acceptable.
1780     # Capture those here as the holdable_formats for the MR hold.
1781     my @selected_formats = $cgi->param("metarecord_formats_$mr_id");
1782     my @selected_langs = $cgi->param("metarecord_langs_$mr_id");
1783
1784     # map the selected attrs into the JSON holdable_formats structure
1785     my $blob = {};
1786     if (@selected_formats) {
1787         $blob->{0} = [
1788             map { {_attr => $format_attr, _val => $_} }
1789             @selected_formats
1790         ];
1791     }
1792     if (@selected_langs) {
1793         $blob->{1} = [
1794             map { {_attr => 'item_lang', _val => $_} }
1795             @selected_langs
1796         ];
1797     }
1798
1799     return OpenSRF::Utils::JSON->perl2JSON($blob);
1800 }
1801
1802 sub fetch_user_circs {
1803     my $self = shift;
1804     my $flesh = shift; # flesh bib data, etc.
1805     my $circ_ids = shift;
1806     my $limit = shift;
1807     my $offset = shift;
1808
1809     my $e = $self->editor;
1810
1811     my @circ_ids;
1812
1813     if($circ_ids) {
1814         @circ_ids = @$circ_ids;
1815
1816     } else {
1817
1818         my $query = {
1819             select => {circ => ['id']},
1820             from => 'circ',
1821             where => {
1822                 '+circ' => {
1823                     usr => $e->requestor->id,
1824                     checkin_time => undef,
1825                     '-or' => [
1826                         {stop_fines => undef},
1827                         {stop_fines => {'not in' => ['LOST','CLAIMSRETURNED','LONGOVERDUE']}}
1828                     ],
1829                 }
1830             },
1831             order_by => {circ => ['due_date']}
1832         };
1833
1834         $query->{limit} = $limit if $limit;
1835         $query->{offset} = $offset if $offset;
1836
1837         my $ids = $e->json_query($query);
1838         @circ_ids = map {$_->{id}} @$ids;
1839     }
1840
1841     return [] unless @circ_ids;
1842
1843     my $qflesh = {
1844         flesh => 3,
1845         flesh_fields => {
1846             circ => ['target_copy'],
1847             acp => ['call_number'],
1848             acn => ['record','owning_lib']
1849         }
1850     };
1851
1852     $e->xact_begin;
1853     my $circs = $e->search_action_circulation(
1854         [{id => \@circ_ids}, ($flesh) ? $qflesh : {}], {substream => 1});
1855
1856     my @circs;
1857     for my $circ (@$circs) {
1858         push(@circs, {
1859             circ => $circ,
1860             marc_xml => ($flesh and $circ->target_copy->call_number->id != -1) ?
1861                 XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc) :
1862                 undef  # pre-cat copy, use the dummy title/author instead
1863         });
1864     }
1865     $e->rollback;
1866
1867     # make sure the final list is in the correct order
1868     my @sorted_circs;
1869     for my $id (@circ_ids) {
1870         push(
1871             @sorted_circs,
1872             (grep { $_->{circ}->id == $id } @circs)
1873         );
1874     }
1875
1876     return \@sorted_circs;
1877 }
1878
1879
1880 sub handle_circ_renew {
1881     my $self = shift;
1882     my $action = shift;
1883     my $ctx = $self->ctx;
1884
1885     my @renew_ids = $self->cgi->param('circ');
1886
1887     my $circs = $self->fetch_user_circs(0, ($action eq 'renew') ? [@renew_ids] : undef);
1888
1889     # TODO: fire off renewal calls in batches to speed things up
1890     my @responses;
1891     for my $circ (@$circs) {
1892
1893         my $evt = $U->simplereq(
1894             'open-ils.circ',
1895             'open-ils.circ.renew',
1896             $self->editor->authtoken,
1897             {
1898                 patron_id => $self->editor->requestor->id,
1899                 copy_id => $circ->{circ}->target_copy,
1900                 opac_renewal => 1
1901             }
1902         );
1903
1904         # TODO return these, then insert them into the circ data
1905         # blob that is shoved into the template for each circ
1906         # so the template won't have to match them
1907         push(@responses, {copy => $circ->{circ}->target_copy, evt => $evt});
1908     }
1909
1910     return @responses;
1911 }
1912
1913 sub load_myopac_circs {
1914     my $self = shift;
1915     my $e = $self->editor;
1916     my $ctx = $self->ctx;
1917
1918     $ctx->{circs} = [];
1919     my $limit = $self->cgi->param('limit') || 0; # 0 == unlimited
1920     my $offset = $self->cgi->param('offset') || 0;
1921     my $action = $self->cgi->param('action') || '';
1922
1923     # perform the renewal first if necessary
1924     my @results = $self->handle_circ_renew($action) if $action =~ /renew/;
1925
1926     $ctx->{circs} = $self->fetch_user_circs(1, undef, $limit, $offset);
1927
1928     my $success_renewals = 0;
1929     my $failed_renewals = 0;
1930     for my $data (@{$ctx->{circs}}) {
1931         my ($resp) = grep { $_->{copy} == $data->{circ}->target_copy->id } @results;
1932
1933         if($resp) {
1934             my $evt = ref($resp->{evt}) eq 'ARRAY' ? $resp->{evt}->[0] : $resp->{evt};
1935
1936             # extract the fail_part, if present, from the event payload;
1937             # since # the payload is an acp object in some cases,
1938             # blindly looking for a # 'fail_part' key in the template can
1939             # break things
1940             $evt->{fail_part} = (ref($evt->{payload}) eq 'HASH' && exists $evt->{payload}->{fail_part}) ?
1941                 $evt->{payload}->{fail_part} :
1942                 '';
1943
1944             $data->{renewal_response} = $evt;
1945             $success_renewals++ if $evt->{textcode} eq 'SUCCESS';
1946             $failed_renewals++ if $evt->{textcode} ne 'SUCCESS';
1947         }
1948     }
1949
1950     $ctx->{success_renewals} = $success_renewals;
1951     $ctx->{failed_renewals} = $failed_renewals;
1952
1953     return Apache2::Const::OK;
1954 }
1955
1956 sub load_myopac_circ_history {
1957     my $self = shift;
1958     my $e = $self->editor;
1959     my $ctx = $self->ctx;
1960     my $limit = $self->cgi->param('limit') || 15;
1961     my $offset = $self->cgi->param('offset') || 0;
1962     my $action = $self->cgi->param('action') || '';
1963
1964     my $circ_handle_result;
1965     $circ_handle_result = $self->handle_circ_update($action) if $action;
1966
1967     $ctx->{circ_history_limit} = $limit;
1968     $ctx->{circ_history_offset} = $offset;
1969
1970     # Defer limitation to circ_history.tt2 when sorting
1971     if ($self->cgi->param('sort')) {
1972         $limit = undef;
1973         $offset = undef;
1974     }
1975
1976     $ctx->{circs} = $self->fetch_user_circ_history(1, $limit, $offset);
1977     return Apache2::Const::OK;
1978 }
1979
1980 # if 'flesh' is set, copy data etc. is loaded and the return value is
1981 # a hash of 'circ' and 'marc_xml'.  Othwerwise, it's just a list of
1982 # auch objects.
1983 sub fetch_user_circ_history {
1984     my ($self, $flesh, $limit, $offset) = @_;
1985     my $e = $self->editor;
1986
1987     my %limits = ();
1988     $limits{offset} = $offset if defined $offset;
1989     $limits{limit} = $limit if defined $limit;
1990
1991     my %flesh_ops = (
1992         flesh => 3,
1993         flesh_fields => {
1994             auch => ['target_copy','source_circ'],
1995             acp => ['call_number'],
1996             acn => ['record']
1997         },
1998     );
1999
2000     $e->xact_begin;
2001     my $circs = $e->search_action_user_circ_history(
2002         [
2003             {usr => $e->requestor->id},
2004             {   # order newest to oldest by default
2005                 order_by => {auch => 'xact_start DESC'},
2006                 $flesh ? %flesh_ops : (),
2007                 %limits
2008             }
2009         ],
2010         {substream => 1}
2011     );
2012     $e->rollback;
2013
2014     return $circs unless $flesh;
2015
2016     $e->xact_begin;
2017     my @circs;
2018     my %unapi_cache = ();
2019     for my $circ (@$circs) {
2020         if ($circ->target_copy->call_number->id == -1) {
2021             push(@circs, {
2022                 circ => $circ,
2023                 marc_xml => undef # pre-cat copy, use the dummy title/author instead
2024             });
2025             next;
2026         }
2027         my $bre_id = $circ->target_copy->call_number->record->id;
2028         my $unapi;
2029         if (exists $unapi_cache{$bre_id}) {
2030             $unapi = $unapi_cache{$bre_id};
2031         } else {
2032             my $result = $e->json_query({
2033                 from => [
2034                     'unapi.bre', $bre_id, 'marcxml','record','{mra}', undef, undef, undef
2035                 ]
2036             });
2037             if ($result) {
2038                 $unapi_cache{$bre_id} = $unapi = XML::LibXML->new->parse_string($result->[0]->{'unapi.bre'});
2039             }
2040         }
2041         if ($unapi) {
2042             push(@circs, {
2043                 circ => $circ,
2044                 marc_xml => $unapi
2045             });
2046         } else {
2047             push(@circs, {
2048                 circ => $circ,
2049                 marc_xml => undef # failed, but try to go on
2050             });
2051         }
2052     }
2053     $e->rollback;
2054
2055     return \@circs;
2056 }
2057
2058 sub handle_circ_update {
2059     my $self     = shift;
2060     my $action   = shift;
2061     my $circ_ids = shift;
2062
2063     $circ_ids //= [$self->cgi->param('circ_id')];
2064
2065     if ($action =~ /delete/) {
2066         my $options = {
2067             circ_ids => $circ_ids,
2068         };
2069
2070         $U->simplereq(
2071             'open-ils.actor',
2072             'open-ils.actor.history.circ.clear',
2073             $self->editor->authtoken,
2074             $options
2075         );
2076     }
2077
2078     return;
2079 }
2080
2081 # TODO: action.usr_visible_holds does not return cancelled holds.  Should it?
2082 sub load_myopac_hold_history {
2083     my $self = shift;
2084     my $e = $self->editor;
2085     my $ctx = $self->ctx;
2086     my $limit = $self->cgi->param('limit') || 15;
2087     my $offset = $self->cgi->param('offset') || 0;
2088     $ctx->{hold_history_limit} = $limit;
2089     $ctx->{hold_history_offset} = $offset;
2090
2091     my $hold_ids = $e->json_query({
2092         select => {
2093             au => [{
2094                 column => 'id',
2095                 transform => 'action.usr_visible_holds',
2096                 result_field => 'id'
2097             }]
2098         },
2099         from => 'au',
2100         where => {id => $e->requestor->id}
2101     });
2102
2103     my $holds_object = $self->fetch_user_holds([map { $_->{id} } @$hold_ids], 0, 1, 0, $limit, $offset);
2104     if($holds_object->{holds}) {
2105         $ctx->{holds} = $holds_object->{holds};
2106     }
2107     $ctx->{hold_history_ids} = $holds_object->{all_ids};
2108
2109     return Apache2::Const::OK;
2110 }
2111
2112 sub load_myopac_payment_form {
2113     my $self = shift;
2114     my $r;
2115
2116     $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact'), $self->cgi->param('xact_misc')]) and return $r;
2117     $r = $self->prepare_extended_user_info and return $r;
2118
2119     return Apache2::Const::OK;
2120 }
2121
2122 # TODO: add other filter options as params/configs/etc.
2123 sub load_myopac_payments {
2124     my $self = shift;
2125     my $limit = $self->cgi->param('limit') || 20;
2126     my $offset = $self->cgi->param('offset') || 0;
2127     my $e = $self->editor;
2128
2129     $self->ctx->{payment_history_limit} = $limit;
2130     $self->ctx->{payment_history_offset} = $offset;
2131
2132     my $args = {};
2133     $args->{limit} = $limit if $limit;
2134     $args->{offset} = $offset if $offset;
2135
2136     if (my $max_age = $self->ctx->{get_org_setting}->(
2137         $e->requestor->home_ou, "opac.payment_history_age_limit"
2138     )) {
2139         my $min_ts = DateTime->now(
2140             "time_zone" => DateTime::TimeZone->new("name" => "local"),
2141         )->subtract("seconds" => interval_to_seconds($max_age))->iso8601();
2142
2143         $logger->info("XXX min_ts: $min_ts");
2144         $args->{"where"} = {"payment_ts" => {">=" => $min_ts}};
2145     }
2146
2147     $self->ctx->{payments} = $U->simplereq(
2148         'open-ils.actor',
2149         'open-ils.actor.user.payments.retrieve.atomic',
2150         $e->authtoken, $e->requestor->id, $args);
2151
2152     return Apache2::Const::OK;
2153 }
2154
2155 # 1. caches the form parameters
2156 # 2. loads the credit card payment "Processing..." page
2157 sub load_myopac_pay_init {
2158     my $self = shift;
2159     my $cache = OpenSRF::Utils::Cache->new('global');
2160
2161     my @payment_xacts = ($self->cgi->param('xact'), $self->cgi->param('xact_misc'));
2162
2163     if (!@payment_xacts) {
2164         # for consistency with load_myopac_payment_form() and
2165         # to preserve backwards compatibility, if no xacts are
2166         # selected, assume all (applicable) transactions are wanted.
2167         my $stat = $self->prepare_fines(undef, undef, [$self->cgi->param('xact'), $self->cgi->param('xact_misc')]);
2168         return $stat if $stat;
2169         @payment_xacts =
2170             map { $_->{xact}->id } (
2171                 @{$self->ctx->{fines}->{circulation}},
2172                 @{$self->ctx->{fines}->{grocery}}
2173         );
2174     }
2175
2176     return $self->generic_redirect unless @payment_xacts;
2177
2178     my $cc_args = {"where_process" => 1};
2179
2180     $cc_args->{$_} = $self->cgi->param($_) for (qw/
2181         number cvv2 expire_year expire_month billing_first
2182         billing_last billing_address billing_city billing_state
2183         billing_zip stripe_token
2184     /);
2185
2186     my $cache_args = {
2187         cc_args => $cc_args,
2188         user => $self->ctx->{user}->id,
2189         xacts => \@payment_xacts
2190     };
2191
2192     # generate a temporary cache token and cache the form data
2193     my $token = md5_hex($$ . time() . rand());
2194     $cache->put_cache($token, $cache_args, 30);
2195
2196     $logger->info("tpac caching payment info with token $token and xacts [@payment_xacts]");
2197
2198     # after we render the processing page, we quickly redirect to submit
2199     # the actual payment.  The refresh url contains the payment token.
2200     # It also contains the list of xact IDs, which allows us to clear the
2201     # cache at the earliest possible time while leaving a trace of which
2202     # transactions we were processing, so the UI can bring the user back
2203     # to the payment form w/ the same xacts if the payment fails.
2204
2205     my $refresh = "1; url=main_pay/$token?xact=" . pop(@payment_xacts);
2206     $refresh .= ";xact=$_" for @payment_xacts;
2207     $self->ctx->{refresh} = $refresh;
2208
2209     return Apache2::Const::OK;
2210 }
2211
2212 # retrieve the cached CC payment info and send off for processing
2213 sub load_myopac_pay {
2214     my $self = shift;
2215     my $token = $self->ctx->{page_args}->[0];
2216     return Apache2::Const::HTTP_BAD_REQUEST unless $token;
2217
2218     my $cache = OpenSRF::Utils::Cache->new('global');
2219     my $cache_args = $cache->get_cache($token);
2220     $cache->delete_cache($token);
2221
2222     # this page is loaded immediately after the token is created.
2223     # if the cached data is not there, it's because of an invalid
2224     # token (or cache failure) and not because of a timeout.
2225     return Apache2::Const::HTTP_BAD_REQUEST unless $cache_args;
2226
2227     my @payment_xacts = @{$cache_args->{xacts}};
2228     my $cc_args = $cache_args->{cc_args};
2229
2230     # as an added security check, verify the user submitting
2231     # the form is the same as the user whose data was cached
2232     return Apache2::Const::HTTP_BAD_REQUEST unless
2233         $cache_args->{user} == $self->ctx->{user}->id;
2234
2235     $logger->info("tpac paying fines with token $token and xacts [@payment_xacts]");
2236
2237     my $r;
2238     $r = $self->prepare_fines(undef, undef, \@payment_xacts) and return $r;
2239
2240     # balance_owed is computed specifically from the fines we're paying
2241     if ($self->ctx->{fines}->{balance_owed} <= 0) {
2242         $logger->info("tpac can't pay non-positive balance. xacts selected: [@payment_xacts]");
2243         return Apache2::Const::HTTP_BAD_REQUEST;
2244     }
2245
2246     my $args = {
2247         "cc_args" => $cc_args,
2248         "userid" => $self->ctx->{user}->id,
2249         "payment_type" => "credit_card_payment",
2250         "payments" => $self->prepare_fines_for_payment  # should be safe after self->prepare_fines
2251     };
2252
2253     my $resp = $U->simplereq("open-ils.circ", "open-ils.circ.money.payment",
2254         $self->editor->authtoken, $args, $self->ctx->{user}->last_xact_id
2255     );
2256
2257     $self->ctx->{"payment_response"} = $resp;
2258
2259     unless ($resp->{"textcode"}) {
2260         $self->ctx->{printable_receipt} = $U->simplereq(
2261         "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
2262         $self->editor->authtoken, $resp->{payments}
2263         );
2264     }
2265
2266     return Apache2::Const::OK;
2267 }
2268
2269 sub load_myopac_receipt_print {
2270     my $self = shift;
2271
2272     $self->ctx->{printable_receipt} = $U->simplereq(
2273     "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
2274     $self->editor->authtoken, [$self->cgi->param("payment")]
2275     );
2276
2277     return Apache2::Const::OK;
2278 }
2279
2280 sub load_myopac_receipt_email {
2281     my $self = shift;
2282
2283     # The following ML method doesn't actually check whether the user in
2284     # question has an email address, so we do.
2285     if ($self->ctx->{user}->email) {
2286         $self->ctx->{email_receipt_result} = $U->simplereq(
2287         "open-ils.circ", "open-ils.circ.money.payment_receipt.email",
2288         $self->editor->authtoken, [$self->cgi->param("payment")]
2289         );
2290     } else {
2291         $self->ctx->{email_receipt_result} =
2292             new OpenILS::Event("PATRON_NO_EMAIL_ADDRESS");
2293     }
2294
2295     return Apache2::Const::OK;
2296 }
2297
2298 sub prepare_fines {
2299     my ($self, $limit, $offset, $id_list) = @_;
2300
2301     # XXX TODO: check for failure after various network calls
2302
2303     # It may be unclear, but this result structure lumps circulation and
2304     # reservation fines together, and keeps grocery fines separate.
2305     $self->ctx->{"fines"} = {
2306         "circulation" => [],
2307         "grocery" => [],
2308         "total_paid" => 0,
2309         "total_owed" => 0,
2310         "balance_owed" => 0
2311     };
2312
2313     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2314
2315     # TODO: This should really be a ML call, but the existing calls
2316     # return an excessive amount of data and don't offer streaming
2317
2318     my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
2319
2320     my $req = $cstore->request(
2321         'open-ils.cstore.direct.money.open_billable_transaction_summary.search',
2322         {
2323             usr => $self->editor->requestor->id,
2324             balance_owed => {'!=' => 0},
2325             ($id_list && @$id_list ? ("id" => $id_list) : ()),
2326         },
2327         {
2328             flesh => 4,
2329             flesh_fields => {
2330                 mobts => [qw/grocery circulation reservation/],
2331                 bresv => ['target_resource_type'],
2332                 brt => ['record'],
2333                 mg => ['billings'],
2334                 mb => ['btype'],
2335                 circ => ['target_copy'],
2336                 acp => ['call_number'],
2337                 acn => ['record']
2338             },
2339             order_by => { mobts => 'xact_start' },
2340             %paging
2341         }
2342     );
2343
2344     # Collect $$ amounts from each transaction for summing below.
2345     my (@paid_amounts, @owed_amounts, @balance_amounts);
2346
2347     while(my $resp = $req->recv) {
2348         my $mobts = $resp->content;
2349         my $circ = $mobts->circulation;
2350
2351         my $last_billing;
2352         if($mobts->grocery) {
2353             my @billings = sort { $a->billing_ts cmp $b->billing_ts } @{$mobts->grocery->billings};
2354             $last_billing = pop(@billings);
2355         }
2356
2357         push(@paid_amounts, $mobts->total_paid);
2358         push(@owed_amounts, $mobts->total_owed);
2359         push(@balance_amounts, $mobts->balance_owed);
2360
2361         my $marc_xml = undef;
2362         if ($mobts->xact_type eq 'reservation' and
2363             $mobts->reservation->target_resource_type->record) {
2364             $marc_xml = XML::LibXML->new->parse_string(
2365                 $mobts->reservation->target_resource_type->record->marc
2366             );
2367         } elsif ($mobts->xact_type eq 'circulation' and
2368             $circ->target_copy->call_number->id != -1) {
2369             $marc_xml = XML::LibXML->new->parse_string(
2370                 $circ->target_copy->call_number->record->marc
2371             );
2372         }
2373
2374         push(
2375             @{$self->ctx->{"fines"}->{$mobts->grocery ? "grocery" : "circulation"}},
2376             {
2377                 xact => $mobts,
2378                 last_grocery_billing => $last_billing,
2379                 marc_xml => $marc_xml
2380             }
2381         );
2382     }
2383
2384     $cstore->kill_me;
2385
2386     $self->ctx->{"fines"}->{total_paid}   = $U->fpsum(@paid_amounts);
2387     $self->ctx->{"fines"}->{total_owed}   = $U->fpsum(@owed_amounts);
2388     $self->ctx->{"fines"}->{balance_owed} = $U->fpsum(@balance_amounts);
2389
2390     return;
2391 }
2392
2393 sub prepare_fines_for_payment {
2394     # This assumes $self->prepare_fines has already been run
2395     my ($self) = @_;
2396
2397     my @results = ();
2398     if ($self->ctx->{fines}) {
2399         push @results, [$_->{xact}->id, $_->{xact}->balance_owed] foreach (
2400             @{$self->ctx->{fines}->{circulation}},
2401             @{$self->ctx->{fines}->{grocery}}
2402         );
2403     }
2404
2405     return \@results;
2406 }
2407
2408 sub load_myopac_main {
2409     my $self = shift;
2410     my $limit = $self->cgi->param('limit') || 0;
2411     my $offset = $self->cgi->param('offset') || 0;
2412     $self->ctx->{search_ou} = $self->_get_search_lib();
2413     $self->ctx->{user}->notes(
2414         $self->editor->search_actor_usr_note({
2415             usr => $self->ctx->{user}->id,
2416             pub => 't'
2417         })
2418     );
2419     return $self->prepare_fines($limit, $offset) || Apache2::Const::OK;
2420 }
2421
2422 sub load_myopac_update_email {
2423     my $self = shift;
2424     my $e = $self->editor;
2425     my $ctx = $self->ctx;
2426     my $email = $self->cgi->param('email') || '';
2427     my $current_pw = $self->cgi->param('current_pw') || '';
2428
2429     # needed for most up-to-date email address
2430     if (my $r = $self->prepare_extended_user_info) { return $r };
2431
2432     return Apache2::Const::OK
2433         unless $self->cgi->request_method eq 'POST';
2434
2435     unless($email =~ /.+\@.+\..+/) { # TODO better regex?
2436         $ctx->{invalid_email} = $email;
2437         return Apache2::Const::OK;
2438     }
2439
2440     my $stat = $U->simplereq(
2441         'open-ils.actor',
2442         'open-ils.actor.user.email.update',
2443         $e->authtoken, $email, $current_pw);
2444
2445     if($U->event_equals($stat, 'INCORRECT_PASSWORD')) {
2446         $ctx->{password_incorrect} = 1;
2447         return Apache2::Const::OK;
2448     }
2449
2450     unless ($self->cgi->param("redirect_to")) {
2451         my $url = $self->apache->unparsed_uri;
2452         $url =~ s/update_email/prefs/;
2453
2454         return $self->generic_redirect($url);
2455     }
2456
2457     return $self->generic_redirect;
2458 }
2459
2460 sub load_myopac_update_username {
2461     my $self = shift;
2462     my $e = $self->editor;
2463     my $ctx = $self->ctx;
2464     my $username = $self->cgi->param('username') || '';
2465     my $current_pw = $self->cgi->param('current_pw') || '';
2466
2467     $self->prepare_extended_user_info;
2468
2469     my $allow_change = 1;
2470     my $regex_check;
2471     my $lock_usernames = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.lock_usernames');
2472     if(defined($lock_usernames) and $lock_usernames == 1) {
2473         # Policy says no username changes
2474         $allow_change = 0;
2475     } else {
2476         # We want this further down.
2477         $regex_check = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.barcode_regex');
2478         my $username_unlimit = $self->ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.unlimit_usernames');
2479         if(!$username_unlimit) {
2480             if(!$regex_check) {
2481                 # Default is "starts with a number"
2482                 $regex_check = '^\d+';
2483             }
2484             # You already have a username?
2485             if($regex_check and $self->ctx->{user}->usrname !~ /$regex_check/) {
2486                 $allow_change = 0;
2487             }
2488         }
2489     }
2490     if(!$allow_change) {
2491         my $url = $self->apache->unparsed_uri;
2492         $url =~ s/update_username/prefs/;
2493
2494         return $self->generic_redirect($url);
2495     }
2496
2497     return Apache2::Const::OK
2498         unless $self->cgi->request_method eq 'POST';
2499
2500     unless($username and $username !~ /\s/) { # any other username restrictions?
2501         $ctx->{invalid_username} = $username;
2502         return Apache2::Const::OK;
2503     }
2504
2505     # New username can't look like a barcode if we have a barcode regex
2506     if($regex_check and $username =~ /$regex_check/) {
2507         $ctx->{invalid_username} = $username;
2508         return Apache2::Const::OK;
2509     }
2510
2511     # New username has to look like a username if we have a username regex
2512     $regex_check = $ctx->{get_org_setting}->($e->requestor->home_ou, 'opac.username_regex');
2513     if($regex_check and $username !~ /$regex_check/) {
2514         $ctx->{invalid_username} = $username;
2515         return Apache2::Const::OK;
2516     }
2517
2518     if($username ne $e->requestor->usrname) {
2519
2520         my $evt = $U->simplereq(
2521             'open-ils.actor',
2522             'open-ils.actor.user.username.update',
2523             $e->authtoken, $username, $current_pw);
2524
2525         if($U->event_equals($evt, 'INCORRECT_PASSWORD')) {
2526             $ctx->{password_incorrect} = 1;
2527             return Apache2::Const::OK;
2528         }
2529
2530         if($U->event_equals($evt, 'USERNAME_EXISTS')) {
2531             $ctx->{username_exists} = $username;
2532             return Apache2::Const::OK;
2533         }
2534     }
2535
2536     my $url = $self->apache->unparsed_uri;
2537     $url =~ s/update_username/prefs/;
2538
2539     return $self->generic_redirect($url);
2540 }
2541
2542 sub load_myopac_update_password {
2543     my $self = shift;
2544     my $e = $self->editor;
2545     my $ctx = $self->ctx;
2546
2547     return Apache2::Const::OK
2548         unless $self->cgi->request_method eq 'POST';
2549
2550     my $current_pw = $self->cgi->param('current_pw') || '';
2551     my $new_pw = $self->cgi->param('new_pw') || '';
2552     my $new_pw2 = $self->cgi->param('new_pw2') || '';
2553
2554     unless($new_pw eq $new_pw2) {
2555         $ctx->{password_nomatch} = 1;
2556         return Apache2::Const::OK;
2557     }
2558
2559     my $pw_regex = $ctx->{get_org_setting}->($e->requestor->home_ou, 'global.password_regex');
2560
2561     if(!$pw_regex) {
2562         # This regex duplicates the JSPac's default "digit, letter, and 7 characters" rule
2563         $pw_regex = '(?=.*\d+.*)(?=.*[A-Za-z]+.*).{7,}';
2564     }
2565
2566     if($pw_regex and $new_pw !~ /$pw_regex/) {
2567         $ctx->{password_invalid} = 1;
2568         return Apache2::Const::OK;
2569     }
2570
2571     my $evt = $U->simplereq(
2572         'open-ils.actor',
2573         'open-ils.actor.user.password.update',
2574         $e->authtoken, $new_pw, $current_pw);
2575
2576
2577     if($U->event_equals($evt, 'INCORRECT_PASSWORD')) {
2578         $ctx->{password_incorrect} = 1;
2579         return Apache2::Const::OK;
2580     }
2581
2582     my $url = $self->apache->unparsed_uri;
2583     $url =~ s/update_password/prefs/;
2584
2585     return $self->generic_redirect($url);
2586 }
2587
2588 sub _update_bookbag_metadata {
2589     my ($self, $bookbag) = @_;
2590
2591     $bookbag->name($self->cgi->param("name"));
2592     $bookbag->description($self->cgi->param("description"));
2593
2594     return 1 if $self->editor->update_container_biblio_record_entry_bucket($bookbag);
2595     return 0;
2596 }
2597
2598 sub _get_lists_per_page {
2599     my $self = shift;
2600
2601     if($self->editor->requestor) {
2602         $self->timelog("Checking for opac.lists_per_page preference");
2603         # See if the user has a lists per page preference
2604         my $ipp = $self->editor->search_actor_user_setting({
2605             usr => $self->editor->requestor->id,
2606             name => 'opac.lists_per_page'
2607         })->[0];
2608         $self->timelog("Got opac.lists_per_page preference");
2609         return OpenSRF::Utils::JSON->JSON2perl($ipp->value) if $ipp;
2610     }
2611     return 10; # default
2612 }
2613
2614 sub _get_items_per_page {
2615     my $self = shift;
2616
2617     if($self->editor->requestor) {
2618         $self->timelog("Checking for opac.list_items_per_page preference");
2619         # See if the user has a list items per page preference
2620         my $ipp = $self->editor->search_actor_user_setting({
2621             usr => $self->editor->requestor->id,
2622             name => 'opac.list_items_per_page'
2623         })->[0];
2624         $self->timelog("Got opac.list_items_per_page preference");
2625         return OpenSRF::Utils::JSON->JSON2perl($ipp->value) if $ipp;
2626     }
2627     return 10; # default
2628 }
2629
2630 sub load_myopac_bookbags {
2631     my $self = shift;
2632     my $e = $self->editor;
2633     my $ctx = $self->ctx;
2634     my $limit = $self->_get_lists_per_page || 10;
2635     my $offset = $self->cgi->param('offset') || 0;
2636
2637     $ctx->{bookbags_limit} = $limit;
2638     $ctx->{bookbags_offset} = $offset;
2639
2640     # for list item pagination
2641     my $item_limit = $self->_get_items_per_page;
2642     my $item_page = $self->cgi->param('item_page') || 1;
2643     my $item_offset = ($item_page - 1) * $item_limit;
2644     $ctx->{bookbags_item_page} = $item_page;
2645
2646     my ($sorter, $modifier) = $self->_get_bookbag_sort_params("sort");
2647     $e->xact_begin; # replication...
2648
2649     my $rv = $self->load_mylist;
2650     unless($rv eq Apache2::Const::OK) {
2651         $e->rollback;
2652         return $rv;
2653     }
2654
2655     $ctx->{bookbags} = $e->search_container_biblio_record_entry_bucket(
2656         [
2657             {owner => $e->requestor->id, btype => 'bookbag'}, {
2658                 order_by => {cbreb => 'name'},
2659                 limit => $limit,
2660                 offset => $offset
2661             }
2662         ],
2663         {substream => 1}
2664     );
2665
2666     if(!$ctx->{bookbags}) {
2667         $e->rollback;
2668         return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
2669     }
2670
2671     # We load the user prefs to get their default bookbag.
2672     $self->_load_user_with_prefs;
2673
2674     # We also want a total count of the user's bookbags.
2675     my $q = {
2676         'select' => { 'cbreb' => [ { 'column' => 'id', 'transform' => 'count', 'aggregate' => 'true', 'alias' => 'count' } ] },
2677         'from' => 'cbreb',
2678         'where' => { 'btype' => 'bookbag', 'owner' => $self->ctx->{user}->id }
2679     };
2680     my $r = $e->json_query($q);
2681     $ctx->{bookbag_count} = $r->[0]->{'count'};
2682
2683     # If the user wants a specific bookbag's items, load them.
2684
2685     if ($self->cgi->param("bbid")) {
2686         my ($bookbag) =
2687             grep { $_->id eq $self->cgi->param("bbid") } @{$ctx->{bookbags}};
2688
2689         if ($bookbag) {
2690             my $query = $self->_prepare_bookbag_container_query(
2691                 $bookbag->id, $sorter, $modifier
2692             );
2693
2694             # Calculate total count of the items in selected bookbag.
2695             # This total includes record entries that have no assets available.
2696             my $bb_search_results = $U->simplereq(
2697                 "open-ils.search", "open-ils.search.biblio.multiclass.query",
2698                 {"limit" => 1, "offset" => 0}, $query
2699             ); # we only need the count, so do the actual search with limit=1
2700
2701             if ($bb_search_results) {
2702                 $ctx->{bb_item_count} = $bb_search_results->{count};
2703             } else {
2704                 $logger->warn("search failed in load_myopac_bookbags()");
2705                 $ctx->{bb_item_count} = 0; # fallback value
2706             }
2707
2708             #calculate page count
2709             $ctx->{bb_page_count} = int ((($ctx->{bb_item_count} - 1) / $item_limit) + 1);
2710
2711             if ( ($self->cgi->param("action") || '') eq "editmeta") {
2712                 if (!$self->_update_bookbag_metadata($bookbag))  {
2713                     $e->rollback;
2714                     return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
2715                 } else {
2716                     $e->commit;
2717                     my $url = $self->ctx->{opac_root} . '/myopac/lists?bbid=' .
2718                         $bookbag->id;
2719
2720                     foreach my $param (('loc', 'qtype', 'query', 'sort', 'offset', 'limit')) {
2721                         if ($self->cgi->param($param)) {
2722                             my @vals = $self->cgi->param($param);
2723                             $url .= ";$param=" . uri_escape_utf8($_) foreach @vals;
2724                         }
2725                     }
2726
2727                     return $self->generic_redirect($url);
2728                 }
2729             }
2730
2731             # we're done with our CStoreEditor.  Rollback here so
2732             # later calls don't cause a timeout, resulting in a
2733             # transaction rollback under the covers.
2734             $e->rollback;
2735
2736
2737             # For list items pagination
2738             my $args = {
2739                 "limit" => $item_limit,
2740                 "offset" => $item_offset
2741             };
2742
2743             my $items = $U->bib_container_items_via_search($bookbag->id, $query, $args)
2744                 or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
2745
2746             # capture pref_ou for callnumber filter/display
2747             $ctx->{pref_ou} = $self->_get_pref_lib() || $ctx->{search_ou};
2748
2749             # search for local callnumbers for display
2750             my $focus_ou = $ctx->{physical_loc} || $ctx->{pref_ou};
2751
2752             my (undef, @recs) = $self->get_records_and_facets(
2753                 [ map {$_->target_biblio_record_entry->id} @$items ],
2754                 undef,
2755                 {
2756                     flesh => '{mra,holdings_xml,acp,exclude_invisible_acn}',
2757                     flesh_depth => 1,
2758                     site => $ctx->{get_aou}->($focus_ou)->shortname,
2759                     pref_lib => $ctx->{pref_ou}
2760                 }
2761             );
2762
2763             $ctx->{bookbags_marc_xml}{$_->{id}} = $_->{marc_xml} for @recs;
2764
2765             $bookbag->items($items);
2766         }
2767     }
2768
2769     # If we have add_rec, we got here from the "Add to new list"
2770     # or "See all" popmenu items.
2771     if (my $add_rec = $self->cgi->param('add_rec')) {
2772         $self->ctx->{add_rec} = $add_rec;
2773         # But not in the staff client, 'cause that breaks things.
2774         unless ($self->ctx->{is_staff}) {
2775             # allow caller to provide the where_from in cases where
2776             # the referer is an intermediate error page
2777             if ($self->cgi->param('where_from')) {
2778                 $self->ctx->{where_from} = $self->cgi->param('where_from');
2779             } else {
2780                 $self->ctx->{where_from} = $self->ctx->{referer};
2781                 if ( my $anchor = $self->cgi->param('anchor') ) {
2782                     $self->ctx->{where_from} =~ s/#.*|$/#$anchor/;
2783                 }
2784             }
2785         }
2786     }
2787
2788     # this rollback may be a dupe, but that's OK because
2789     # cstoreditor ignores dupe rollbacks
2790     $e->rollback;
2791
2792     return Apache2::Const::OK;
2793 }
2794
2795
2796 # actions are create, delete, show, hide, rename, add_rec, delete_item, place_hold, print, email
2797 # CGI is action, list=list_id, add_rec/record=bre_id, del_item=bucket_item_id, name=new_bucket_name
2798 sub load_myopac_bookbag_update {
2799     my ($self, $action, $list_id, @hold_recs) = @_;
2800     my $e = $self->editor;
2801     my $cgi = $self->cgi;
2802
2803     # save_notes is effectively another action, but is passed in a separate
2804     # CGI parameter for what are really just layout reasons.
2805     $action = 'save_notes' if $cgi->param('save_notes');
2806     $action ||= $cgi->param('action');
2807
2808     $list_id ||= $cgi->param('list') || $cgi->param('bbid');
2809
2810     my @add_rec = $cgi->param('add_rec') || $cgi->param('record');
2811     my @selected_item = $cgi->param('selected_item');
2812     my $shared = $cgi->param('shared');
2813     my $move_cart = $cgi->param('move_cart');
2814     my $name = $cgi->param('name');
2815     my $description = $cgi->param('description');
2816     my $success = 0;
2817     my $list;
2818
2819     # bail out if user is attempting an action that requires
2820     # that at least one list item be selected
2821     if ((scalar(@selected_item) == 0) && (scalar(@hold_recs) == 0) &&
2822         ($action eq 'place_hold' || $action eq 'print' ||
2823          $action eq 'email' || $action eq 'del_item')) {
2824         my $url = $self->ctx->{referer};
2825         $url .= ($url =~ /\?/ ? '&' : '?') . 'list_none_selected=1' unless $url =~ /list_none_selected/;
2826         return $self->generic_redirect($url);
2827     }
2828
2829     # This url intentionally leaves off the edit_notes parameter, but
2830     # may need to add some back in for paging.
2831
2832     my $url = $self->ctx->{proto} . "://" . $self->ctx->{hostname} .
2833         $self->ctx->{opac_root} . "/myopac/lists?";
2834
2835     foreach my $param (('loc', 'qtype', 'query', 'sort')) {
2836         if ($cgi->param($param)) {
2837             my @vals = $cgi->param($param);
2838             $url .= ";$param=" . uri_escape_utf8($_) foreach @vals;
2839         }
2840     }
2841
2842     if ($action eq 'create') {
2843
2844         if ($name) {
2845             $list = Fieldmapper::container::biblio_record_entry_bucket->new;
2846             $list->name($name);
2847             $list->description($description);
2848             $list->owner($e->requestor->id);
2849             $list->btype('bookbag');
2850             $list->pub($shared ? 't' : 'f');
2851             $success = $U->simplereq('open-ils.actor',
2852                 'open-ils.actor.container.create', $e->authtoken, 'biblio', $list);
2853             if (ref($success) ne 'HASH') {
2854                 $list_id = (ref($success)) ? $success->id : $success;
2855                 if (scalar @add_rec) {
2856                     foreach my $add_rec (@add_rec) {
2857                         my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2858                         $item->bucket($list_id);
2859                         $item->target_biblio_record_entry($add_rec);
2860                         $success = $U->simplereq('open-ils.actor',
2861                                                 'open-ils.actor.container.item.create', $e->authtoken, 'biblio', $item);
2862                         last unless $success;
2863                     }
2864                 }
2865                 if ($move_cart) {
2866                     my ($cache_key, $list) = $self->fetch_mylist(0, 1);
2867                     foreach my $add_rec (@$list) {
2868                         my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2869                         $item->bucket($list_id);
2870                         $item->target_biblio_record_entry($add_rec);
2871                         $success = $U->simplereq('open-ils.actor',
2872                                                 'open-ils.actor.container.item.create', $e->authtoken, 'biblio', $item);
2873                         last unless $success;
2874                     }
2875                     $self->clear_anon_cache;
2876                 }
2877             }
2878             $url = $cgi->param('where_from') if ($success && $cgi->param('where_from'));
2879
2880         } else { # no name
2881             $self->ctx->{bucket_failure_noname} = 1;
2882         }
2883
2884     } elsif($action eq 'place_hold') {
2885
2886         # @hold_recs comes from anon lists redirect; selected_items comes from existing buckets
2887         my $from_basket = scalar(@hold_recs);
2888         unless (@hold_recs) {
2889             if (@selected_item) {
2890                 my $items = $e->search_container_biblio_record_entry_bucket_item({id => \@selected_item});
2891                 @hold_recs = map { $_->target_biblio_record_entry } @$items;
2892             }
2893         }
2894
2895         return Apache2::Const::OK unless @hold_recs;
2896         $logger->info("placing holds from list page on: @hold_recs");
2897
2898         my $url = $self->ctx->{opac_root} . '/place_hold?hold_type=T';
2899         $url .= ';hold_target=' . $_ for @hold_recs;
2900         $url .= ';from_basket=1' if $from_basket;
2901         foreach my $param (('loc', 'qtype', 'query')) {
2902             if ($cgi->param($param)) {
2903                 my @vals = $cgi->param($param);
2904                 $url .= ";$param=" . uri_escape_utf8($_) foreach @vals;
2905             }
2906         }
2907         return $self->generic_redirect($url);
2908
2909     } elsif ($action eq 'print') {
2910         my $temp_cache_key = $self->_stash_record_list_in_anon_cache(@selected_item);
2911         return $self->load_mylist_print($temp_cache_key);
2912     } elsif ($action eq 'email') {
2913         my $temp_cache_key = $self->_stash_record_list_in_anon_cache(@selected_item);
2914         return $self->load_mylist_email($temp_cache_key);
2915     } else {
2916
2917         $list = $e->retrieve_container_biblio_record_entry_bucket($list_id);
2918
2919         return Apache2::Const::HTTP_BAD_REQUEST unless
2920             $list and $list->owner == $e->requestor->id;
2921     }
2922
2923     if($action eq 'delete') {
2924         $success = $U->simplereq('open-ils.actor',
2925             'open-ils.actor.container.full_delete', $e->authtoken, 'biblio', $list_id);
2926         if ($success) {
2927             # We check to see if we're deleting the user's default list.
2928             $self->_load_user_with_prefs;
2929             my $settings_map = $self->ctx->{user_setting_map};
2930             if ($$settings_map{'opac.default_list'} == $list_id) {
2931                 # We unset the user's opac.default_list setting.
2932                 $success = $U->simplereq(
2933                     'open-ils.actor',
2934                     'open-ils.actor.patron.settings.update',
2935                     $e->authtoken,
2936                     $e->requestor->id,
2937                     { 'opac.default_list' => 0 }
2938                 );
2939             }
2940         }
2941     } elsif($action eq 'show') {
2942         unless($U->is_true($list->pub)) {
2943             $list->pub('t');
2944             $success = $U->simplereq('open-ils.actor',
2945                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
2946         }
2947
2948     } elsif($action eq 'hide') {
2949         if($U->is_true($list->pub)) {
2950             $list->pub('f');
2951             $success = $U->simplereq('open-ils.actor',
2952                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
2953         }
2954
2955     } elsif($action eq 'rename') {
2956         if($name) {
2957             $list->name($name);
2958             $success = $U->simplereq('open-ils.actor',
2959                 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
2960         }
2961
2962     } elsif($action eq 'add_rec') {
2963         foreach my $add_rec (@add_rec) {
2964             my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2965             $item->bucket($list_id);
2966             $item->target_biblio_record_entry($add_rec);
2967             $success = $U->simplereq('open-ils.actor',
2968                 'open-ils.actor.container.item.create', $e->authtoken, 'biblio', $item);
2969             last unless $success;
2970         }
2971         # Redirect back where we came from if we have an anchor parameter:
2972         if ( my $anchor = $cgi->param('anchor') && !$self->ctx->{is_staff}) {
2973             $url = $self->ctx->{referer};
2974             $url =~ s/#.*|$/#$anchor/;
2975         } elsif ($cgi->param('where_from')) {
2976             # Or, if we have a "where_from" parameter.
2977             $url = $cgi->param('where_from');
2978         }
2979     } elsif ($action eq 'del_item') {
2980         foreach (@selected_item) {
2981             $success = $U->simplereq(
2982                 'open-ils.actor',
2983                 'open-ils.actor.container.item.delete', $e->authtoken, 'biblio', $_
2984             );
2985             last unless $success;
2986         }
2987     } elsif ($action eq 'save_notes') {
2988         $success = $self->update_bookbag_item_notes;
2989         $url .= "&bbid=" . uri_escape_utf8($cgi->param("bbid")) if $cgi->param("bbid");
2990     } elsif ($action eq 'make_default') {
2991         $success = $U->simplereq(
2992             'open-ils.actor',
2993             'open-ils.actor.patron.settings.update',
2994             $e->authtoken,
2995             $list->owner,
2996             { 'opac.default_list' => $list_id }
2997         );
2998     } elsif ($action eq 'remove_default') {
2999         $success = $U->simplereq(
3000             'open-ils.actor',
3001             'open-ils.actor.patron.settings.update',
3002             $e->authtoken,
3003             $list->owner,
3004             { 'opac.default_list' => 0 }
3005         );
3006     }
3007
3008     return $self->generic_redirect($url) if $success;
3009
3010     $self->ctx->{where_from} = $cgi->param('where_from');
3011     $self->ctx->{bucket_action} = $action;
3012     $self->ctx->{bucket_action_failed} = 1;
3013     return Apache2::Const::OK;
3014 }
3015
3016 sub update_bookbag_item_notes {
3017     my ($self) = @_;
3018     my $e = $self->editor;
3019
3020     my @note_keys = grep /^note-\d+/, keys(%{$self->cgi->Vars});
3021     my @item_keys = grep /^item-\d+/, keys(%{$self->cgi->Vars});
3022
3023     # We're going to leverage an API call that's already been written to check
3024     # permissions appropriately.
3025
3026     my $a = create OpenSRF::AppSession("open-ils.actor");
3027     my $method = "open-ils.actor.container.item_note.cud";
3028
3029     for my $note_key (@note_keys) {
3030         my $note;
3031
3032         my $id = ($note_key =~ /(\d+)/)[0];
3033
3034         if (!($note =
3035             $e->retrieve_container_biblio_record_entry_bucket_item_note($id))) {
3036             my $event = $e->die_event;
3037             $self->apache->log->warn(
3038                 "error retrieving cbrebin id $id, got event " .
3039                 $event->{textcode}
3040             );
3041             $a->kill_me;
3042             $self->ctx->{bucket_action_event} = $event;
3043             return;
3044         }
3045
3046         if (length($self->cgi->param($note_key))) {
3047             $note->ischanged(1);
3048             $note->note($self->cgi->param($note_key));
3049         } else {
3050             $note->isdeleted(1);
3051         }
3052
3053         my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1);
3054
3055         if (defined $U->event_code($r)) {
3056             $self->apache->log->warn(
3057                 "attempt to modify cbrebin " . $note->id .
3058                 " returned event " .  $r->{textcode}
3059             );
3060             $e->rollback;
3061             $a->kill_me;
3062             $self->ctx->{bucket_action_event} = $r;
3063             return;
3064         }
3065     }
3066
3067     for my $item_key (@item_keys) {
3068         my $id = int(($item_key =~ /(\d+)/)[0]);
3069         my $text = $self->cgi->param($item_key);
3070
3071         chomp $text;
3072         next unless length $text;
3073
3074         my $note = new Fieldmapper::container::biblio_record_entry_bucket_item_note;
3075         $note->isnew(1);
3076         $note->item($id);
3077         $note->note($text);
3078
3079         my $r = $a->request($method, $e->authtoken, "biblio", $note)->gather(1);
3080
3081         if (defined $U->event_code($r)) {
3082             $self->apache->log->warn(
3083                 "attempt to create cbrebin for item " . $note->item .
3084                 " returned event " .  $r->{textcode}
3085             );
3086             $e->rollback;
3087             $a->kill_me;
3088             $self->ctx->{bucket_action_event} = $r;
3089             return;
3090         }
3091     }
3092
3093     $a->kill_me;
3094     return 1;   # success
3095 }
3096
3097 sub load_myopac_bookbag_print {
3098     my ($self) = @_;
3099
3100     my $id = int($self->cgi->param("list"));
3101
3102     my ($sorter, $modifier) = $self->_get_bookbag_sort_params("sort");
3103
3104     my $item_search =
3105         $self->_prepare_bookbag_container_query($id, $sorter, $modifier);
3106
3107     my $bbag;
3108
3109     # Get the bookbag object itself, assuming we're allowed to.
3110     if ($self->editor->allowed("VIEW_CONTAINER")) {
3111
3112         $bbag = $self->editor->retrieve_container_biblio_record_entry_bucket($id) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
3113     } else {
3114         my $bookbags = $self->editor->search_container_biblio_record_entry_bucket(
3115             {
3116                 "id" => $id,
3117                 "-or" => {
3118                     "owner" => $self->editor->requestor->id,
3119                     "pub" => "t"
3120                 }
3121             }
3122         ) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
3123
3124         $bbag = pop @$bookbags;
3125     }
3126
3127     # If we have a bookbag we're allowed to look at, issue the A/T event
3128     # to get CSV, passing as a user param that search query we built before.
3129     if ($bbag) {
3130         $self->ctx->{csv} = $U->fire_object_event(
3131             undef, "container.biblio_record_entry_bucket.csv",
3132             $bbag, $self->editor->requestor->home_ou,
3133             undef, {"item_search" => $item_search}
3134         );
3135     }
3136
3137     # Create a reasonable filename and set the content disposition to
3138     # provoke browser download dialogs.
3139     (my $filename = $bbag->id . $bbag->name) =~ s/[^a-z0-9_ -]//gi;
3140
3141     return $self->set_file_download_headers("$filename.csv");
3142 }
3143
3144 sub load_myopac_circ_history_export {
3145     my $self = shift;
3146     my $e = $self->editor;
3147     my $filename = $self->cgi->param('filename') || 'circ_history.csv';
3148
3149     my $circs = $self->fetch_user_circ_history(1);
3150
3151     $self->ctx->{csv}->{circs} = $circs;
3152     return $self->set_file_download_headers($filename, 'text/csv; encoding=UTF-8');
3153
3154 }
3155
3156 sub load_myopac_reservations {
3157     my $self = shift;
3158     my $e = $self->editor;
3159     my $ctx = $self->ctx;
3160
3161     my $upcoming = $U->simplereq("open-ils.booking", "open-ils.booking.reservations.upcoming_reservation_list_by_user",
3162         $e->authtoken, undef
3163     );
3164
3165     $ctx->{reservations} = $upcoming;
3166     return Apache2::Const::OK;
3167
3168 }
3169
3170 sub load_password_reset {
3171     my $self = shift;
3172     my $cgi = $self->cgi;
3173     my $ctx = $self->ctx;
3174     my $barcode = $cgi->param('barcode');
3175     my $username = $cgi->param('username');
3176     my $email = $cgi->param('email');
3177     my $pwd1 = $cgi->param('pwd1');
3178     my $pwd2 = $cgi->param('pwd2');
3179     my $uuid = $ctx->{page_args}->[0];
3180
3181     if ($uuid) {
3182
3183         $logger->info("patron password reset with uuid $uuid");
3184
3185         if ($pwd1 and $pwd2) {
3186
3187             if ($pwd1 eq $pwd2) {
3188
3189                 my $response = $U->simplereq(
3190                     'open-ils.actor',
3191                     'open-ils.actor.patron.password_reset.commit',
3192                     $uuid, $pwd1);
3193
3194                 $logger->info("patron password reset response " . Dumper($response));
3195
3196                 if ($U->event_code($response)) { # non-success event
3197
3198                     my $code = $response->{textcode};
3199
3200                     if ($code eq 'PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST') {
3201                         $ctx->{pwreset} = {style => 'error', status => 'NOT_ACTIVE'};
3202                     }
3203
3204                     if ($code eq 'PATRON_PASSWORD_WAS_NOT_STRONG') {
3205                         $ctx->{pwreset} = {style => 'error', status => 'NOT_STRONG'};
3206                     }
3207
3208                 } else { # success
3209
3210                     $ctx->{pwreset} = {style => 'success', status => 'SUCCESS'};
3211                 }
3212
3213             } else { # passwords not equal
3214
3215                 $ctx->{pwreset} = {style => 'error', status => 'NO_MATCH'};
3216             }
3217
3218         } else { # 2 password values needed
3219
3220             $ctx->{pwreset} = {status => 'TWO_PASSWORDS'};
3221         }
3222
3223     } elsif ($barcode or $username) {
3224
3225         my @params = $barcode ? ('barcode', $barcode) : ('username', $username);
3226         push(@params, $email) if $email;
3227
3228         $U->simplereq(
3229             'open-ils.actor',
3230             'open-ils.actor.patron.password_reset.request', @params);
3231
3232         $ctx->{pwreset} = {status => 'REQUEST_SUCCESS'};
3233     }
3234
3235     $logger->info("patron password reset resulted in " . Dumper($ctx->{pwreset}));
3236     return Apache2::Const::OK;
3237 }
3238
3239 1;