]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/WWW/EGCatLoader.pm
Merge branch 'master' of git+ssh://yeti.esilibrary.com/home/evergreen/evergreen-equin...
[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 $req = $circ->request(
507         # TODO .authoritative version is chewing up cstores
508         # 'open-ils.circ.hold.details.batch.retrieve.authoritative', 
509         'open-ils.circ.hold.details.batch.retrieve', 
510         $e->authtoken, 
511         $hold_ids,
512         {
513             suppress_notices => 1,
514             suppress_transits => 1,
515             suppress_mvr => 1,
516             suppress_patron_details => 1,
517             include_bre => 1
518         }
519     );
520
521     my @holds;
522     while(my $resp = $req->recv) {
523         my $hold = $resp->content;
524         push(@holds, {
525             hold => $hold,
526             marc_xml => ($flesh) ? XML::LibXML->new->parse_string($hold->{bre}->marc) : undef
527         });
528     }
529
530     $circ->kill_me;
531     return \@holds;
532 }
533
534 sub handle_hold_update {
535     my $self = shift;
536     my $action = shift;
537     my $e = $self->editor;
538
539
540     my @hold_ids = $self->cgi->param('hold_id'); # for non-_all actions
541     @hold_ids = @{$self->fetch_user_holds(undef, 1)} if $action =~ /_all/;
542
543     my $circ = OpenSRF::AppSession->create('open-ils.circ');
544
545     if($action =~ /cancel/) {
546
547         for my $hold_id (@hold_ids) {
548             my $resp = $circ->request(
549                 'open-ils.circ.hold.cancel', $e->authtoken, $hold_id, 6 )->gather(1); # 6 == patron-cancelled-via-opac
550         }
551
552     } else {
553         
554         my $vlist = [];
555         for my $hold_id (@hold_ids) {
556             my $vals = {id => $hold_id};
557
558             if($action =~ /activate/) {
559                 $vals->{frozen} = 'f';
560                 $vals->{thaw_date} = undef;
561
562             } elsif($action =~ /suspend/) {
563                 $vals->{frozen} = 't';
564                 # $vals->{thaw_date} = TODO;
565             }
566             push(@$vlist, $vals);
567         }
568
569         $circ->request('open-ils.circ.hold.update.batch.atomic', $e->authtoken, undef, $vlist)->gather(1);
570     }
571
572     $circ->kill_me;
573     return undef;
574 }
575
576 sub load_myopac_holds {
577     my $self = shift;
578     my $e = $self->editor;
579     my $ctx = $self->ctx;
580     
581
582     my $limit = $self->cgi->param('limit') || 0;
583     my $offset = $self->cgi->param('offset') || 0;
584     my $action = $self->cgi->param('action') || '';
585
586     $self->handle_hold_update($action) if $action;
587
588     $ctx->{holds} = $self->fetch_user_holds(undef, 0, 1, $limit, $offset);
589
590     $ctx->{"icon_by_mattype"} = \&_icon_by_mattype;
591
592     return Apache2::Const::OK;
593 }
594
595 sub load_place_hold {
596     my $self = shift;
597     my $ctx = $self->ctx;
598     my $e = $self->editor;
599     my $cgi = $self->cgi;
600     $self->ctx->{page} = 'place_hold';
601
602     $ctx->{hold_target} = $cgi->param('hold_target');
603     $ctx->{hold_type} = $cgi->param('hold_type');
604     $ctx->{default_pickup_lib} = $e->requestor->home_ou; # XXX staff
605
606     if($ctx->{hold_type} eq 'T') {
607         $ctx->{record} = $e->retrieve_biblio_record_entry($ctx->{hold_target});
608     }
609     # ...
610
611     $ctx->{marc_xml} = XML::LibXML->new->parse_string($ctx->{record}->marc);
612
613     if(my $pickup_lib = $cgi->param('pickup_lib')) {
614
615         my $args = {
616             patronid => $e->requestor->id,
617             titleid => $ctx->{hold_target}, # XXX
618             pickup_lib => $pickup_lib,
619             depth => 0, # XXX
620         };
621
622         my $allowed = $U->simplereq(
623             'open-ils.circ',
624             'open-ils.circ.title_hold.is_possible',
625             $e->authtoken, $args
626         );
627
628         if($allowed->{success} == 1) {
629             my $hold = Fieldmapper::action::hold_request->new;
630
631             $hold->pickup_lib($pickup_lib);
632             $hold->requestor($e->requestor->id);
633             $hold->usr($e->requestor->id); # XXX staff
634             $hold->target($ctx->{hold_target});
635             $hold->hold_type($ctx->{hold_type});
636             # frozen, expired, etc..
637
638             my $stat = $U->simplereq(
639                 'open-ils.circ',
640                 'open-ils.circ.holds.create',
641                 $e->authtoken, $hold
642             );
643
644             if($stat and $stat > 0) {
645
646                 # if successful, return the user to the requesting page
647                 $self->apache->log->info("Redirecting back to " . $cgi->param('redirect_to'));
648                 $self->apache->print($cgi->redirect(-url => $cgi->param('redirect_to')));
649                 return Apache2::Const::REDIRECT;
650
651             } else {
652
653                 $ctx->{hold_failed} = 1; # XXX process the events, etc
654             }
655         }
656
657         # hold permit failed
658         $self->apache->log->warn('hold permit result ' . OpenSRF::Utils::JSON->perl2JSON($allowed));
659     }
660
661     return Apache2::Const::OK;
662 }
663
664
665 sub fetch_user_circs {
666     my $self = shift;
667     my $flesh = shift; # flesh bib data, etc.
668     my $circ_ids = shift;
669     my $limit = shift;
670     my $offset = shift;
671
672     my $e = $self->editor;
673
674     my @circ_ids;
675
676     if($circ_ids) {
677         @circ_ids = @$circ_ids;
678
679     } else {
680
681         my $circ_data = $U->simplereq(
682             'open-ils.actor', 
683             'open-ils.actor.user.checked_out',
684             $e->authtoken, 
685             $e->requestor->id
686         );
687
688         @circ_ids =  ( @{$circ_data->{overdue}}, @{$circ_data->{out}} );
689
690         if($limit or $offset) {
691             @circ_ids = grep { defined $_ } @circ_ids[0..($offset + $limit - 1)];
692         }
693     }
694
695     return [] unless @circ_ids;
696
697     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
698
699     my $qflesh = {
700         flesh => 3,
701         flesh_fields => {
702             circ => ['target_copy'],
703             acp => ['call_number'],
704             acn => ['record']
705         }
706     };
707
708     $e->xact_begin;
709     my $circs = $e->search_action_circulation(
710         [{id => \@circ_ids}, ($flesh) ? $qflesh : {}], {substream => 1});
711
712     my @circs;
713     for my $circ (@$circs) {
714         push(@circs, {
715             circ => $circ, 
716             marc_xml => ($flesh and $circ->target_copy->call_number->id != -1) ? 
717                 XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc) : 
718                 undef  # pre-cat copy, use the dummy title/author instead
719         });
720     }
721     $e->xact_rollback;
722
723     # make sure the final list is in the correct order
724     my @sorted_circs;
725     for my $id (@circ_ids) {
726         push(
727             @sorted_circs,
728             (grep { $_->{circ}->id == $id } @circs)
729         );
730     }
731
732     return \@sorted_circs;
733 }
734
735
736 sub handle_circ_renew {
737     my $self = shift;
738     my $action = shift;
739     my $ctx = $self->ctx;
740
741     my @renew_ids = $self->cgi->param('circ');
742
743     my $circs = $self->fetch_user_circs(0, ($action eq 'renew') ? [@renew_ids] : undef);
744
745     # TODO: fire off renewal calls in batches to speed things up
746     my @responses;
747     for my $circ (@$circs) {
748
749         my $evt = $U->simplereq(
750             'open-ils.circ', 
751             'open-ils.circ.renew',
752             $self->editor->authtoken,
753             {
754                 patron_id => $self->editor->requestor->id,
755                 copy_id => $circ->{circ}->target_copy,
756                 opac_renewal => 1
757             }
758         );
759
760         # TODO return these, then insert them into the circ data 
761         # blob that is shoved into the template for each circ
762         # so the template won't have to match them
763         push(@responses, {copy => $circ->{circ}->target_copy, evt => $evt});
764     }
765
766     return @responses;
767 }
768
769
770 sub load_myopac_circs {
771     my $self = shift;
772     my $e = $self->editor;
773     my $ctx = $self->ctx;
774
775     $ctx->{circs} = [];
776     my $limit = $self->cgi->param('limit') || 0; # 0 == unlimited
777     my $offset = $self->cgi->param('offset') || 0;
778     my $action = $self->cgi->param('action') || '';
779
780     # perform the renewal first if necessary
781     my @results = $self->handle_circ_renew($action) if $action =~ /renew/;
782
783     $ctx->{circs} = $self->fetch_user_circs(1, undef, $limit, $offset);
784
785     my $success_renewals = 0;
786     my $failed_renewals = 0;
787     for my $data (@{$ctx->{circs}}) {
788         my ($resp) = grep { $_->{copy} == $data->{circ}->target_copy->id } @results;
789
790         if($resp) {
791             my $evt = ref($resp->{evt}) eq 'ARRAY' ? $resp->{evt}->[0] : $resp->{evt};
792             $data->{renewal_response} = $evt;
793             $success_renewals++ if $evt->{textcode} eq 'SUCCESS';
794             $failed_renewals++ if $evt->{textcode} ne 'SUCCESS';
795         }
796     }
797
798     $ctx->{success_renewals} = $success_renewals;
799     $ctx->{failed_renewals} = $failed_renewals;
800
801     return Apache2::Const::OK;
802 }
803
804 sub load_myopac_fines {
805     my $self = shift;
806     my $e = $self->editor;
807     my $ctx = $self->ctx;
808     $ctx->{"fines"} = {
809         "circulation" => [],
810         "grocery" => [],
811         "total_paid" => 0,
812         "total_owed" => 0,
813         "balance_owed" => 0
814     };
815
816     my $limit = $self->cgi->param('limit') || 0;
817     my $offset = $self->cgi->param('offset') || 0;
818
819     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
820
821     # TODO: This should really be a ML call, but the existing calls 
822     # return an excessive amount of data and don't offer streaming
823
824     my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
825
826     my $req = $cstore->request(
827         'open-ils.cstore.direct.money.open_billable_transaction_summary.search',
828         {
829             usr => $e->requestor->id,
830             balance_owed => {'!=' => 0}
831         },
832         {
833             flesh => 4,
834             flesh_fields => {
835                 mobts => ['circulation', 'grocery'],
836                 mg => ['billings'],
837                 mb => ['btype'],
838                 circ => ['target_copy'],
839                 acp => ['call_number'],
840                 acn => ['record']
841             },
842             order_by => { mobts => 'xact_start' },
843             %paging
844         }
845     );
846
847     while(my $resp = $req->recv) {
848         my $mobts = $resp->content;
849         my $circ = $mobts->circulation;
850
851         my $last_billing;
852         if($mobts->grocery) {
853             my @billings = sort { $a->billing_ts cmp $b->billing_ts } @{$mobts->grocery->billings};
854             $last_billing = pop(@billings);
855         }
856
857         # XXX TODO switch to some money-safe non-fp library for math
858         $ctx->{"fines"}->{$_} += $mobts->$_ for (
859             qw/total_paid total_owed balance_owed/
860         );
861
862         push(
863             @{$ctx->{"fines"}->{$mobts->grocery ? "grocery" : "circulation"}},
864             {
865                 xact => $mobts,
866                 last_grocery_billing => $last_billing,
867                 marc_xml => ($mobts->xact_type ne 'circulation' or $circ->target_copy->call_number->id == -1) ?
868                     undef :
869                     XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc),
870             } 
871         );
872     }
873
874      return Apache2::Const::OK;
875 }       
876
877 1;