]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/WWW/EGCatLoader.pm
holds retrieval is a hefty process, created a parallelizer for testing; shows good...
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / WWW / EGCatLoader.pm
1 package OpenILS::WWW::EGCatLoader;
2 use strict; use warnings;
3 use CGI;
4 use XML::LibXML;
5 use URI::Escape;
6 use Digest::MD5 qw(md5_hex);
7 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
8 use OpenSRF::AppSession;
9 use OpenSRF::EX qw/:try/;
10 use OpenSRF::Utils qw/:datetime/;
11 use OpenSRF::Utils::JSON;
12 use OpenSRF::Utils::Logger qw/$logger/;
13 use OpenILS::Application::AppUtils;
14 use OpenILS::Utils::CStoreEditor qw/:funcs/;
15 use OpenILS::Utils::Fieldmapper;
16 use DateTime::Format::ISO8601;
17 my $U = 'OpenILS::Application::AppUtils';
18
19 my %cache; # proc-level cache
20
21 sub _icon_by_mattype {  # XXX This is KCLS specific stuff that needs to be
22                         # genericized later.
23     my $mattype = shift;
24
25     my %type_map = (
26         "a" => "media_book.jpg",
27         "b" => "media_magazines.jpg",
28         "c" => "media_printedmusic.jpg",
29         "d" => "media_microform.jpg",
30         "e" => "media_equipment.jpg",
31         "f" => "media_films.jpg",
32         "g" => "",
33         "h" => "media_dvd.jpg",
34         "i" => "media_bookoncassette.jpg",
35         "j" => "media_musiccd.jpg",
36         "k" => "media_musiccassette.jpg",
37         "l" => "media_musicrecord.jpg",
38         "m" => "media_software.jpg",
39         "n" => "media_bookoncd.jpg",
40         "o" => "media_kit.jpg",
41         "p" => "media_newspaper.jpg",
42         "q" => "media_largeprint.jpg",
43         "r" => "media_3dobject.jpg",
44         "s" => "media_slide.jpg",
45         "t" => "media_online.jpg",
46         "u" => "media_eaudio.jpg",
47         "v" => "media_ebooktext.jpg",
48         "w" => "media_eaudio.jpg",
49         "x" => "media_downloadmusic.jpg",
50         "y" => "media_downloadvideo.jpg",
51         "z" => "media_map.jpg",
52         "2" => "media_cassettewithbook.jpg",
53         "5" => "media_cdwithbook.jpg"
54     );
55
56     return $type_map{$mattype};
57 }
58
59 sub new {
60     my($class, $apache, $ctx) = @_;
61
62     my $self = bless({}, ref($class) || $class);
63
64     $self->apache($apache);
65     $self->ctx($ctx);
66     $self->cgi(CGI->new);
67
68     OpenILS::Utils::CStoreEditor->init; # just in case
69     $self->editor(new_editor());
70
71     return $self;
72 }
73
74
75 # current Apache2::RequestRec;
76 sub apache {
77     my($self, $apache) = @_;
78     $self->{apache} = $apache if $apache;
79     return $self->{apache};
80 }
81
82 # runtime / template context
83 sub ctx {
84     my($self, $ctx) = @_;
85     $self->{ctx} = $ctx if $ctx;
86     return $self->{ctx};
87 }
88
89 # cstore editor
90 sub editor {
91     my($self, $editor) = @_;
92     $self->{editor} = $editor if $editor;
93     return $self->{editor};
94 }
95
96 # CGI handle
97 sub cgi {
98     my($self, $cgi) = @_;
99     $self->{cgi} = $cgi if $cgi;
100     return $self->{cgi};
101 }
102
103
104 # load common data, then load page data
105 sub load {
106     my $self = shift;
107
108     $self->load_helpers;
109     my $stat = $self->load_common;
110     return $stat unless $stat == Apache2::Const::OK;
111
112     my $path = $self->apache->path_info;
113
114     return $self->load_home if $path =~ /opac\/home/;
115     return $self->load_login if $path =~ /opac\/login/;
116     return $self->load_logout if $path =~ /opac\/logout/;
117     return $self->load_rresults if $path =~ /opac\/results/;
118     return $self->load_record if $path =~ /opac\/record/;
119
120     # ----------------------------------------------------------------
121     # These pages require authentication
122     # ----------------------------------------------------------------
123     unless($self->cgi->https and $self->editor->requestor) {
124         # If a secure resource is requested insecurely, redirect to the login page
125         my $url = 'https://' . $self->apache->hostname . $self->ctx->{base_path} . "/opac/login";
126         $self->apache->print($self->cgi->redirect(-url => $url));
127         return Apache2::Const::REDIRECT;
128     }
129
130     return $self->load_place_hold if $path =~ /opac\/place_hold/;
131     return $self->load_myopac_holds if $path =~ /opac\/myopac\/holds/;
132     return $self->load_myopac_circs if $path =~ /opac\/myopac\/circs/;
133     return $self->load_myopac_fines if $path =~ /opac\/myopac\/fines/;
134     return $self->load_myopac if $path =~ /opac\/myopac/;
135     # ----------------------------------------------------------------
136
137     return Apache2::Const::OK;
138 }
139
140 # general purpose utility functions added to the environment
141 sub load_helpers {
142     my $self = shift;
143     my $e = $self->editor;
144     my $ctx = $self->ctx;
145
146     $cache{map} = {}; # public object maps
147     $cache{list} = {}; # public object lists
148
149     # fetch-on-demand-and-cache subs for commonly used public data
150     my @public_classes = qw/ccs aout cifm citm clm/;
151
152     for my $hint (@public_classes) {
153
154         my ($class) = grep {
155             $Fieldmapper::fieldmap->{$_}->{hint} eq $hint
156         } keys %{ $Fieldmapper::fieldmap };
157
158         my $ident_field =  $Fieldmapper::fieldmap->{$class}->{identity};
159
160             $class =~ s/Fieldmapper:://o;
161             $class =~ s/::/_/g;
162
163         # copy statuses
164         my $list_key = $hint . '_list';
165         my $find_key = "find_$hint";
166
167         $ctx->{$list_key} = sub {
168             my $method = "retrieve_all_$class";
169             $cache{list}{$hint} = $e->$method() unless $cache{list}{$hint};
170             return $cache{list}{$hint};
171         };
172     
173         $cache{map}{$hint} = {};
174
175         $ctx->{$find_key} = sub {
176             my $id = shift;
177             return $cache{map}{$hint}{$id} if $cache{map}{$hint}{$id}; 
178             ($cache{map}{$hint}{$id}) = grep { $_->$ident_field eq $id } @{$ctx->{$list_key}->()};
179             return $cache{map}{$hint}{$id};
180         };
181
182     }
183
184     $ctx->{aou_tree} = sub {
185
186         # fetch the org unit tree
187         unless($cache{aou_tree}) {
188             my $tree = $e->search_actor_org_unit([
189                             {   parent_ou => undef},
190                             {   flesh            => -1,
191                                     flesh_fields    => {aou =>  ['children']},
192                                     order_by        => {aou => 'name'}
193                             }
194                     ])->[0];
195
196             # flesh the org unit type for each org unit
197             # and simultaneously set the id => aou map cache
198             sub flesh_aout {
199                 my $node = shift;
200                 my $ctx = shift;
201                 $node->ou_type( $ctx->{find_aout}->($node->ou_type) );
202                 $cache{map}{aou}{$node->id} = $node;
203                 flesh_aout($_, $ctx) foreach @{$node->children};
204             };
205             flesh_aout($tree, $ctx);
206
207             $cache{aou_tree} = $tree;
208         }
209
210         return $cache{aou_tree};
211     };
212
213     # Add a special handler for the tree-shaped org unit cache
214     $cache{map}{aou} = {};
215     $ctx->{find_aou} = sub {
216         my $org_id = shift;
217         $ctx->{aou_tree}->(); # force the org tree to load
218         return $cache{map}{aou}{$org_id};
219     };
220
221     # turns an ISO date into something TT can understand
222     $ctx->{parse_datetime} = sub {
223         my $date = shift;
224         $date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date));
225         return sprintf(
226             "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
227             $date->hour,
228             $date->minute,
229             $date->second,
230             $date->day,
231             $date->month,
232             $date->year
233         );
234     }
235 }
236
237 # context additions: 
238 #   authtoken : string
239 #   user : au object
240 #   user_status : hash of user circ numbers
241 sub load_common {
242     my $self = shift;
243
244     my $e = $self->editor;
245     my $ctx = $self->ctx;
246
247     $ctx->{referer} = $self->cgi->referer;
248
249     if($e->authtoken($self->cgi->cookie('ses'))) {
250
251         if($e->checkauth) {
252
253             $ctx->{authtoken} = $e->authtoken;
254             $ctx->{user} = $e->requestor;
255             $ctx->{user_stats} = $U->simplereq(
256                 'open-ils.actor', 
257                 'open-ils.actor.user.opac.vital_stats', 
258                 $e->authtoken, $e->requestor->id);
259
260         } else {
261
262             return $self->load_logout;
263         }
264     }
265
266     return Apache2::Const::OK;
267 }
268
269 sub load_home {
270     my $self = shift;
271     $self->ctx->{page} = 'home';
272     return Apache2::Const::OK;
273 }
274
275
276 sub load_login {
277     my $self = shift;
278     my $cgi = $self->cgi;
279
280     $self->ctx->{page} = 'login';
281
282     my $username = $cgi->param('username');
283     my $password = $cgi->param('password');
284
285     return Apache2::Const::OK unless $username and $password;
286
287         my $seed = $U->simplereq(
288         'open-ils.auth', 
289                 'open-ils.auth.authenticate.init',
290         $username);
291
292         my $response = $U->simplereq(
293         'open-ils.auth', 
294                 'open-ils.auth.authenticate.complete', 
295                 {       username => $username, 
296                         password => md5_hex($seed . md5_hex($password)), 
297                         type => 'opac' 
298         }
299     );
300
301     # XXX check event, redirect as necessary
302
303     my $home = $self->apache->unparsed_uri;
304     $home =~ s/\/login/\/home/;
305
306     $self->apache->print(
307         $cgi->redirect(
308             -url => $cgi->param('redirect_to') || $home,
309             -cookie => $cgi->cookie(
310                 -name => 'ses',
311                 -path => '/',
312                 -secure => 1,
313                 -value => $response->{payload}->{authtoken},
314                 -expires => CORE::time + $response->{payload}->{authtime}
315             )
316         )
317     );
318
319     return Apache2::Const::REDIRECT;
320 }
321
322 sub load_logout {
323     my $self = shift;
324
325     my $url = 'http://' . $self->apache->hostname . $self->ctx->{base_path} . "/opac/home";
326
327     $self->apache->print(
328         $self->cgi->redirect(
329             -url => $url,
330             -cookie => $self->cgi->cookie(
331                 -name => 'ses',
332                 -path => '/',
333                 -value => '',
334                 -expires => '-1h'
335             )
336         )
337     );
338
339     return Apache2::Const::REDIRECT;
340 }
341
342 # context additions: 
343 #   page_size
344 #   hit_count
345 #   records : list of bre's and copy-count objects
346 sub load_rresults {
347     my $self = shift;
348     my $cgi = $self->cgi;
349     my $ctx = $self->ctx;
350     my $e = $self->editor;
351
352     $ctx->{page} = 'rresult';
353     my $page = $cgi->param('page') || 0;
354     my $facet = $cgi->param('facet');
355     my $query = $cgi->param('query');
356     my $limit = $cgi->param('limit') || 10; # XXX user settings
357     my $args = {limit => $limit, offset => $page * $limit}; 
358     $query = "$query $facet" if $facet;
359     my $results;
360
361     try {
362         $results = $U->simplereq(
363             'open-ils.search',
364             'open-ils.search.biblio.multiclass.query.staff', 
365             $args, $query, 1);
366
367     } catch Error with {
368         my $err = shift;
369         $logger->error("multiclass search error: $err");
370         $results = {count => 0, ids => []};
371     };
372
373     my $rec_ids = [map { $_->[0] } @{$results->{ids}}];
374
375     $ctx->{records} = [];
376     $ctx->{search_facets} = {};
377     $ctx->{page_size} = $limit;
378     $ctx->{hit_count} = $results->{count};
379
380     return Apache2::Const::OK if @$rec_ids == 0;
381
382     my $cstore1 = OpenSRF::AppSession->create('open-ils.cstore');
383     my $bre_req = $cstore1->request(
384         'open-ils.cstore.direct.biblio.record_entry.search', {id => $rec_ids});
385
386     my $search = OpenSRF::AppSession->create('open-ils.search');
387     my $facet_req = $search->request('open-ils.search.facet_cache.retrieve', $results->{facet_key}, 10);
388
389     unless($cache{cmf}) {
390         $cache{cmf} = $e->search_config_metabib_field({id => {'!=' => undef}});
391         $ctx->{metabib_field} = $cache{cmf};
392         #$cache{cmc} = $e->search_config_metabib_class({name => {'!=' => undef}});
393         #$ctx->{metabib_class} = $cache{cmc};
394     }
395
396     my @data;
397     while(my $resp = $bre_req->recv) {
398         my $bre = $resp->content; 
399
400         # XXX farm out to multiple cstore sessions before loop, then collect after
401         my $copy_counts = $e->json_query(
402             {from => ['asset.record_copy_count', 1, $bre->id, 0]})->[0];
403
404         push(@data,
405             {
406                 bre => $bre,
407                 marc_xml => XML::LibXML->new->parse_string($bre->marc),
408                 copy_counts => $copy_counts
409             }
410         );
411     }
412
413     $cstore1->kill_me;
414
415     # shove recs into context in search results order
416     for my $rec_id (@$rec_ids) { 
417         push(
418             @{$ctx->{records}},
419             grep { $_->{bre}->id == $rec_id } @data
420         );
421     }
422
423     my $facets = $facet_req->gather(1);
424
425     for my $cmf_id (keys %$facets) {  # quick-n-dirty
426         my ($cmf) = grep { $_->id eq $cmf_id } @{$cache{cmf}};
427         $facets->{$cmf_id} = {cmf => $cmf, data => $facets->{$cmf_id}};
428     }
429     $ctx->{search_facets} = $facets;
430
431     return Apache2::Const::OK;
432 }
433
434 # context additions: 
435 #   record : bre object
436 sub load_record {
437     my $self = shift;
438     $self->ctx->{page} = 'record';
439
440     my $rec_id = $self->ctx->{page_args}->[0]
441         or return Apache2::Const::HTTP_BAD_REQUEST;
442
443     $self->ctx->{record} = $self->editor->retrieve_biblio_record_entry([
444         $rec_id,
445         {
446             flesh => 2, 
447             flesh_fields => {
448                 bre => ['call_numbers'],
449                 acn => ['copies'] # limit, paging, etc.
450             }
451         }
452     ]);
453
454     $self->ctx->{marc_xml} = XML::LibXML->new->parse_string($self->ctx->{record}->marc);
455
456     return Apache2::Const::OK;
457 }
458
459 # context additions: 
460 #   user : au object, fleshed
461 sub load_myopac {
462     my $self = shift;
463     $self->ctx->{page} = 'myopac';
464
465     $self->ctx->{user} = $self->editor->retrieve_actor_user([
466         $self->ctx->{user}->id,
467         {
468             flesh => 1,
469             flesh_fields => {
470                 au => ['card']
471                 # ...
472             }
473         }
474     ]);
475
476     return Apache2::Const::OK;
477 }
478
479
480 sub fetch_user_holds {
481     my $self = shift;
482     my $hold_ids = shift;
483     my $ids_only = shift;
484     my $flesh = shift;
485     my $limit = shift;
486     my $offset = shift;
487
488     my $e = $self->editor;
489
490     my $circ = OpenSRF::AppSession->create('open-ils.circ');
491
492     if(!$hold_ids) {
493
494         $hold_ids = $circ->request(
495             'open-ils.circ.holds.id_list.retrieve.authoritative', 
496             $e->authtoken, 
497             $e->requestor->id
498         )->gather(1);
499     
500         $hold_ids = [ grep { defined $_ } @$hold_ids[$offset..($offset + $limit - 1)] ] if $limit or $offset;
501     }
502
503
504     return $hold_ids if $ids_only or @$hold_ids == 0;
505
506     my $args = {
507         suppress_notices => 1,
508         suppress_transits => 1,
509         suppress_mvr => 1,
510         suppress_patron_details => 1,
511         include_bre => $flesh ? 1 : 0
512     };
513
514     # ----------------------------------------------------------------
515     # batch version for testing;  initial test show 40% speed 
516     # savings on larger sets (>20) of holds.
517     # ----------------------------------------------------------------
518     my $batch_size = 8;
519     my $batch_idx = 0;
520     my $mk_req_batch = sub {
521         my @ses;
522         my $top_idx = $batch_idx + $batch_size;
523         while($batch_idx < $top_idx) {
524             my $hold_id = $hold_ids->[$batch_idx++];
525             last unless $hold_id;
526             $self->apache->log->warn("fetching hold $hold_id");
527             my $ses = OpenSRF::AppSession->create('open-ils.circ');
528             my $req = $ses->request(
529                 'open-ils.circ.hold.details.retrieve', 
530                 $e->authtoken, $hold_id, $args);
531             push(@ses, {ses => $ses, req => $req});
532         }
533         return @ses;
534     };
535
536     my $first = 1;
537     my @collected;
538     my @holds;
539     my @ses;
540     while(1) {
541         @ses = $mk_req_batch->() if $first;
542         last if $first and not @ses;
543         if(@collected) {
544             while(my $blob = pop(@collected)) {
545                 $blob->{marc_xml} = XML::LibXML->new->parse_string($blob->{hold}->{bre}->marc) if $flesh;
546                 push(@holds, $blob);
547             }
548         }
549         for my $req_data (@ses) {
550             push(@collected, {hold => $req_data->{req}->gather(1)});
551             $self->apache->log->warn("fetched a hold");
552             $req_data->{ses}->kill_me;
553         }
554         @ses = $mk_req_batch->();
555         last unless @ses;
556         $first = 0;
557     }
558     # ----------------------------------------------------------------
559
560 =head
561     my $req = $circ->request(
562         # TODO .authoritative version is chewing up cstores
563         # 'open-ils.circ.hold.details.batch.retrieve.authoritative', 
564         'open-ils.circ.hold.details.batch.retrieve', 
565         $e->authtoken, $hold_ids, $args
566     );
567
568     my @holds;
569     while(my $resp = $req->recv) {
570         my $hold = $resp->content;
571         push(@holds, {
572             hold => $hold,
573             marc_xml => ($flesh) ? XML::LibXML->new->parse_string($hold->{bre}->marc) : undef
574         });
575     }
576
577     $circ->kill_me;
578 =cut
579
580     return \@holds;
581 }
582
583 sub handle_hold_update {
584     my $self = shift;
585     my $action = shift;
586     my $e = $self->editor;
587
588
589     my @hold_ids = $self->cgi->param('hold_id'); # for non-_all actions
590     @hold_ids = @{$self->fetch_user_holds(undef, 1)} if $action =~ /_all/;
591
592     my $circ = OpenSRF::AppSession->create('open-ils.circ');
593
594     if($action =~ /cancel/) {
595
596         for my $hold_id (@hold_ids) {
597             my $resp = $circ->request(
598                 'open-ils.circ.hold.cancel', $e->authtoken, $hold_id, 6 )->gather(1); # 6 == patron-cancelled-via-opac
599         }
600
601     } else {
602         
603         my $vlist = [];
604         for my $hold_id (@hold_ids) {
605             my $vals = {id => $hold_id};
606
607             if($action =~ /activate/) {
608                 $vals->{frozen} = 'f';
609                 $vals->{thaw_date} = undef;
610
611             } elsif($action =~ /suspend/) {
612                 $vals->{frozen} = 't';
613                 # $vals->{thaw_date} = TODO;
614             }
615             push(@$vlist, $vals);
616         }
617
618         $circ->request('open-ils.circ.hold.update.batch.atomic', $e->authtoken, undef, $vlist)->gather(1);
619     }
620
621     $circ->kill_me;
622     return undef;
623 }
624
625 sub load_myopac_holds {
626     my $self = shift;
627     my $e = $self->editor;
628     my $ctx = $self->ctx;
629     
630
631     my $limit = $self->cgi->param('limit') || 0;
632     my $offset = $self->cgi->param('offset') || 0;
633     my $action = $self->cgi->param('action') || '';
634
635     $self->handle_hold_update($action) if $action;
636
637     $ctx->{holds} = $self->fetch_user_holds(undef, 0, 1, $limit, $offset);
638
639     $ctx->{"icon_by_mattype"} = \&_icon_by_mattype;
640
641     return Apache2::Const::OK;
642 }
643
644 sub load_place_hold {
645     my $self = shift;
646     my $ctx = $self->ctx;
647     my $e = $self->editor;
648     my $cgi = $self->cgi;
649     $self->ctx->{page} = 'place_hold';
650
651     $ctx->{hold_target} = $cgi->param('hold_target');
652     $ctx->{hold_type} = $cgi->param('hold_type');
653     $ctx->{default_pickup_lib} = $e->requestor->home_ou; # XXX staff
654
655     if($ctx->{hold_type} eq 'T') {
656         $ctx->{record} = $e->retrieve_biblio_record_entry($ctx->{hold_target});
657     }
658     # ...
659
660     $ctx->{marc_xml} = XML::LibXML->new->parse_string($ctx->{record}->marc);
661
662     if(my $pickup_lib = $cgi->param('pickup_lib')) {
663
664         my $args = {
665             patronid => $e->requestor->id,
666             titleid => $ctx->{hold_target}, # XXX
667             pickup_lib => $pickup_lib,
668             depth => 0, # XXX
669         };
670
671         my $allowed = $U->simplereq(
672             'open-ils.circ',
673             'open-ils.circ.title_hold.is_possible',
674             $e->authtoken, $args
675         );
676
677         if($allowed->{success} == 1) {
678             my $hold = Fieldmapper::action::hold_request->new;
679
680             $hold->pickup_lib($pickup_lib);
681             $hold->requestor($e->requestor->id);
682             $hold->usr($e->requestor->id); # XXX staff
683             $hold->target($ctx->{hold_target});
684             $hold->hold_type($ctx->{hold_type});
685             # frozen, expired, etc..
686
687             my $stat = $U->simplereq(
688                 'open-ils.circ',
689                 'open-ils.circ.holds.create',
690                 $e->authtoken, $hold
691             );
692
693             if($stat and $stat > 0) {
694
695                 # if successful, return the user to the requesting page
696                 $self->apache->log->info("Redirecting back to " . $cgi->param('redirect_to'));
697                 $self->apache->print($cgi->redirect(-url => $cgi->param('redirect_to')));
698                 return Apache2::Const::REDIRECT;
699
700             } else {
701
702                 $ctx->{hold_failed} = 1; # XXX process the events, etc
703             }
704         }
705
706         # hold permit failed
707         $self->apache->log->warn('hold permit result ' . OpenSRF::Utils::JSON->perl2JSON($allowed));
708     }
709
710     return Apache2::Const::OK;
711 }
712
713
714 sub fetch_user_circs {
715     my $self = shift;
716     my $flesh = shift; # flesh bib data, etc.
717     my $circ_ids = shift;
718     my $limit = shift;
719     my $offset = shift;
720
721     my $e = $self->editor;
722
723     my @circ_ids;
724
725     if($circ_ids) {
726         @circ_ids = @$circ_ids;
727
728     } else {
729
730         my $circ_data = $U->simplereq(
731             'open-ils.actor', 
732             'open-ils.actor.user.checked_out',
733             $e->authtoken, 
734             $e->requestor->id
735         );
736
737         @circ_ids =  ( @{$circ_data->{overdue}}, @{$circ_data->{out}} );
738
739         if($limit or $offset) {
740             @circ_ids = grep { defined $_ } @circ_ids[0..($offset + $limit - 1)];
741         }
742     }
743
744     return [] unless @circ_ids;
745
746     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
747
748     my $qflesh = {
749         flesh => 3,
750         flesh_fields => {
751             circ => ['target_copy'],
752             acp => ['call_number'],
753             acn => ['record']
754         }
755     };
756
757     $e->xact_begin;
758     my $circs = $e->search_action_circulation(
759         [{id => \@circ_ids}, ($flesh) ? $qflesh : {}], {substream => 1});
760
761     my @circs;
762     for my $circ (@$circs) {
763         push(@circs, {
764             circ => $circ, 
765             marc_xml => ($flesh and $circ->target_copy->call_number->id != -1) ? 
766                 XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc) : 
767                 undef  # pre-cat copy, use the dummy title/author instead
768         });
769     }
770     $e->xact_rollback;
771
772     # make sure the final list is in the correct order
773     my @sorted_circs;
774     for my $id (@circ_ids) {
775         push(
776             @sorted_circs,
777             (grep { $_->{circ}->id == $id } @circs)
778         );
779     }
780
781     return \@sorted_circs;
782 }
783
784
785 sub handle_circ_renew {
786     my $self = shift;
787     my $action = shift;
788     my $ctx = $self->ctx;
789
790     my @renew_ids = $self->cgi->param('circ');
791
792     my $circs = $self->fetch_user_circs(0, ($action eq 'renew') ? [@renew_ids] : undef);
793
794     # TODO: fire off renewal calls in batches to speed things up
795     my @responses;
796     for my $circ (@$circs) {
797
798         my $evt = $U->simplereq(
799             'open-ils.circ', 
800             'open-ils.circ.renew',
801             $self->editor->authtoken,
802             {
803                 patron_id => $self->editor->requestor->id,
804                 copy_id => $circ->{circ}->target_copy,
805                 opac_renewal => 1
806             }
807         );
808
809         # TODO return these, then insert them into the circ data 
810         # blob that is shoved into the template for each circ
811         # so the template won't have to match them
812         push(@responses, {copy => $circ->{circ}->target_copy, evt => $evt});
813     }
814
815     return @responses;
816 }
817
818
819 sub load_myopac_circs {
820     my $self = shift;
821     my $e = $self->editor;
822     my $ctx = $self->ctx;
823
824     $ctx->{circs} = [];
825     my $limit = $self->cgi->param('limit') || 0; # 0 == unlimited
826     my $offset = $self->cgi->param('offset') || 0;
827     my $action = $self->cgi->param('action') || '';
828
829     # perform the renewal first if necessary
830     my @results = $self->handle_circ_renew($action) if $action =~ /renew/;
831
832     $ctx->{circs} = $self->fetch_user_circs(1, undef, $limit, $offset);
833
834     my $success_renewals = 0;
835     my $failed_renewals = 0;
836     for my $data (@{$ctx->{circs}}) {
837         my ($resp) = grep { $_->{copy} == $data->{circ}->target_copy->id } @results;
838
839         if($resp) {
840             my $evt = ref($resp->{evt}) eq 'ARRAY' ? $resp->{evt}->[0] : $resp->{evt};
841             $data->{renewal_response} = $evt;
842             $success_renewals++ if $evt->{textcode} eq 'SUCCESS';
843             $failed_renewals++ if $evt->{textcode} ne 'SUCCESS';
844         }
845     }
846
847     $ctx->{success_renewals} = $success_renewals;
848     $ctx->{failed_renewals} = $failed_renewals;
849
850     return Apache2::Const::OK;
851 }
852
853 sub load_myopac_fines {
854     my $self = shift;
855     my $e = $self->editor;
856     my $ctx = $self->ctx;
857     $ctx->{"fines"} = {
858         "circulation" => [],
859         "grocery" => [],
860         "total_paid" => 0,
861         "total_owed" => 0,
862         "balance_owed" => 0
863     };
864
865     my $limit = $self->cgi->param('limit') || 0;
866     my $offset = $self->cgi->param('offset') || 0;
867
868     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
869
870     # TODO: This should really be a ML call, but the existing calls 
871     # return an excessive amount of data and don't offer streaming
872
873     my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
874
875     my $req = $cstore->request(
876         'open-ils.cstore.direct.money.open_billable_transaction_summary.search',
877         {
878             usr => $e->requestor->id,
879             balance_owed => {'!=' => 0}
880         },
881         {
882             flesh => 4,
883             flesh_fields => {
884                 mobts => ['circulation', 'grocery'],
885                 mg => ['billings'],
886                 mb => ['btype'],
887                 circ => ['target_copy'],
888                 acp => ['call_number'],
889                 acn => ['record']
890             },
891             order_by => { mobts => 'xact_start' },
892             %paging
893         }
894     );
895
896     while(my $resp = $req->recv) {
897         my $mobts = $resp->content;
898         my $circ = $mobts->circulation;
899
900         my $last_billing;
901         if($mobts->grocery) {
902             my @billings = sort { $a->billing_ts cmp $b->billing_ts } @{$mobts->grocery->billings};
903             $last_billing = pop(@billings);
904         }
905
906         # XXX TODO switch to some money-safe non-fp library for math
907         $ctx->{"fines"}->{$_} += $mobts->$_ for (
908             qw/total_paid total_owed balance_owed/
909         );
910
911         push(
912             @{$ctx->{"fines"}->{$mobts->grocery ? "grocery" : "circulation"}},
913             {
914                 xact => $mobts,
915                 last_grocery_billing => $last_billing,
916                 marc_xml => ($mobts->xact_type ne 'circulation' or $circ->target_copy->call_number->id == -1) ?
917                     undef :
918                     XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc),
919             } 
920         );
921     }
922
923      return Apache2::Const::OK;
924 }       
925
926 1;