]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm
e468a0b789006c73cf0041aa032f085282efd11b
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Actor / Container.pm
1 package OpenILS::Application::Actor::Container;
2 use base 'OpenILS::Application';
3 use strict; use warnings;
4 use OpenILS::Application::AppUtils;
5 use OpenILS::Perm;
6 use Data::Dumper;
7 use OpenSRF::EX qw(:try);
8 use OpenILS::Utils::Fieldmapper;
9 use OpenILS::Utils::CStoreEditor qw/:funcs/;
10 use OpenSRF::Utils::SettingsClient;
11 use OpenSRF::Utils::Cache;
12 use Digest::MD5 qw(md5_hex);
13 use OpenSRF::Utils::JSON;
14
15 my $apputils = "OpenILS::Application::AppUtils";
16 my $U = $apputils;
17 my $logger = "OpenSRF::Utils::Logger";
18
19 sub initialize { return 1; }
20
21 my $svc = 'open-ils.cstore';
22 my $meth = 'open-ils.cstore.direct.container';
23 my %types;
24 my %ctypes;
25 my %itypes;
26 my %htypes;
27 my %qtypes;
28 my %ttypes;
29 my %batch_perm;
30 my %table;
31
32 $batch_perm{'biblio'} = ['UPDATE_MARC'];
33 $batch_perm{'callnumber'} = ['UPDATE_VOLUME'];
34 $batch_perm{'copy'} = ['UPDATE_COPY'];
35 $batch_perm{'user'} = ['UPDATE_USER'];
36
37 $types{'biblio'} = "$meth.biblio_record_entry_bucket";
38 $types{'callnumber'} = "$meth.call_number_bucket";
39 $types{'copy'} = "$meth.copy_bucket";
40 $types{'user'} = "$meth.user_bucket";
41
42 $ctypes{'biblio'} = "container_biblio_record_entry_bucket";
43 $ctypes{'callnumber'} = "container_call_number_bucket";
44 $ctypes{'copy'} = "container_copy_bucket";
45 $ctypes{'user'} = "container_user_bucket";
46
47 $itypes{'biblio'} = "biblio_record_entry";
48 $itypes{'callnumber'} = "asset_call_number";
49 $itypes{'copy'} = "asset_copy";
50 $itypes{'user'} = "actor_user";
51
52 $ttypes{'biblio'} = "biblio_record_entry";
53 $ttypes{'callnumber'} = "call_number";
54 $ttypes{'copy'} = "copy";
55 $ttypes{'user'} = "user";
56
57 $htypes{'biblio'} = "bre";
58 $htypes{'callnumber'} = "acn";
59 $htypes{'copy'} = "acp";
60 $htypes{'user'} = "au";
61
62 $table{'biblio'} = "biblio.record_entry";
63 $table{'callnumber'} = "asset.call_number";
64 $table{'copy'} = "asset.copy";
65 $table{'user'} = "actor.usr";
66
67 #$qtypes{'biblio'} = 0 
68 #$qtypes{'callnumber'} = 0;
69 #$qtypes{'copy'} = 0;
70 $qtypes{'user'} = 1;
71
72 my $event;
73
74 sub _sort_buckets {
75     my $buckets = shift;
76     return $buckets unless ($buckets && $buckets->[0]);
77     return [ sort { $a->name cmp $b->name } @$buckets ];
78 }
79
80 __PACKAGE__->register_method(
81     method  => "bucket_retrieve_all",
82     api_name    => "open-ils.actor.container.all.retrieve_by_user",
83     authoritative => 1,
84     notes        => <<"    NOTES");
85         Retrieves all un-fleshed buckets assigned to given user 
86         PARAMS(authtoken, bucketOwnerId)
87         If requestor ID is different than bucketOwnerId, requestor must have
88         VIEW_CONTAINER permissions.
89     NOTES
90
91 sub bucket_retrieve_all {
92     my($self, $client, $auth, $user_id) = @_;
93     my $e = new_editor(authtoken => $auth);
94     return $e->event unless $e->checkauth;
95
96     if($e->requestor->id ne $user_id) {
97         return $e->event unless $e->allowed('VIEW_CONTAINER');
98     }
99     
100     my %buckets;
101     for my $type (keys %ctypes) {
102         my $meth = "search_" . $ctypes{$type};
103         $buckets{$type} = _sort_buckets($e->$meth({owner => $user_id}));
104     }
105
106     return \%buckets;
107 }
108
109 __PACKAGE__->register_method(
110     method  => "bucket_flesh",
111     api_name    => "open-ils.actor.container.flesh",
112     authoritative => 1,
113     argc        => 3, 
114 );
115
116 __PACKAGE__->register_method(
117     method  => "bucket_flesh_pub",
118     api_name    => "open-ils.actor.container.public.flesh",
119     argc        => 3, 
120 );
121
122 sub bucket_flesh {
123     my($self, $conn, $auth, $class, $bucket_id) = @_;
124     my $e = new_editor(authtoken => $auth);
125     return $e->event unless $e->checkauth;
126     return _bucket_flesh($self, $conn, $e, $class, $bucket_id);
127 }
128
129 sub bucket_flesh_pub {
130     my($self, $conn, $class, $bucket_id) = @_;
131     my $e = new_editor();
132     return _bucket_flesh($self, $conn, $e, $class, $bucket_id);
133 }
134
135 sub _bucket_flesh {
136     my($self, $conn, $e, $class, $bucket_id) = @_;
137     my $meth = 'retrieve_' . $ctypes{$class};
138     my $bkt = $e->$meth($bucket_id) or return $e->event;
139
140     unless($U->is_true($bkt->pub)) {
141         return undef if $self->api_name =~ /public/;
142         unless($bkt->owner eq $e->requestor->id) {
143             my $owner = $e->retrieve_actor_user($bkt->owner)
144                 or return $e->die_event;
145             return $e->event unless $e->allowed('VIEW_CONTAINER', $owner->home_ou);
146         }
147     }
148
149     my $fmclass = $bkt->class_name . "i";
150     $meth = 'search_' . $ctypes{$class} . '_item';
151     $bkt->items(
152         $e->$meth(
153             {bucket => $bucket_id}, 
154             {   order_by => {$fmclass => "pos"},
155                 flesh => 1, 
156                 flesh_fields => {$fmclass => ['notes']}
157             }
158         )
159     );
160
161     return $bkt;
162 }
163
164
165 __PACKAGE__->register_method(
166     method  => "item_note_cud",
167     api_name    => "open-ils.actor.container.item_note.cud",
168 );
169
170
171 sub item_note_cud {
172     my($self, $conn, $auth, $class, $note) = @_;
173
174     return new OpenILS::Event("BAD_PARAMS") unless
175         $note->class_name =~ /bucket_item_note$/;
176
177     my $e = new_editor(authtoken => $auth, xact => 1);
178     return $e->die_event unless $e->checkauth;
179
180     my $meat = $ctypes{$class} . "_item_note";
181     my $meth = "retrieve_$meat";
182
183     my $item_meat = $ctypes{$class} . "_item";
184     my $item_meth = "retrieve_$item_meat";
185
186     my $nhint = $Fieldmapper::fieldmap->{$note->class_name}->{hint};
187     (my $ihint = $nhint) =~ s/n$//og;
188
189     my ($db_note, $item);
190
191     if ($note->isnew) {
192         $db_note = $note;
193
194         $item = $e->$item_meth([
195             $note->item, {
196                 flesh => 1, flesh_fields => {$ihint => ["bucket"]}
197             }
198         ]) or return $e->die_event;
199     } else {
200         $db_note = $e->$meth([
201             $note->id, {
202                 flesh => 2,
203                 flesh_fields => {
204                     $nhint => ['item'],
205                     $ihint => ['bucket']
206                 }
207             }
208         ]) or return $e->die_event;
209
210         $item = $db_note->item;
211     }
212
213     if($item->bucket->owner ne $e->requestor->id) {
214         return $e->die_event unless $e->allowed("UPDATE_CONTAINER");
215     }
216
217     $meth = 'create_' . $meat if $note->isnew;
218     $meth = 'update_' . $meat if $note->ischanged;
219     $meth = 'delete_' . $meat if $note->isdeleted;
220     return $e->die_event unless $e->$meth($note);
221     $e->commit;
222 }
223
224
225 __PACKAGE__->register_method(
226     method  => "bucket_retrieve_class",
227     api_name    => "open-ils.actor.container.retrieve_by_class",
228     argc        => 3, 
229     authoritative   => 1, 
230     notes        => <<"    NOTES");
231         Retrieves all un-fleshed buckets by class assigned to given user 
232         PARAMS(authtoken, bucketOwnerId, class [, type])
233         class can be one of "biblio", "callnumber", "copy", "user"
234         The optional "type" parameter allows you to limit the search by 
235         bucket type.  
236         If bucketOwnerId is not defined, the authtoken is used as the
237         bucket owner.
238         If requestor ID is different than bucketOwnerId, requestor must have
239         VIEW_CONTAINER permissions.
240     NOTES
241
242 sub bucket_retrieve_class {
243     my( $self, $client, $authtoken, $userid, $class, $type ) = @_;
244
245     my( $staff, $user, $evt ) = 
246         $apputils->checkses_requestor( $authtoken, $userid, 'VIEW_CONTAINER' );
247     return $evt if $evt;
248
249     $logger->debug("User " . $staff->id . 
250         " retrieving buckets for user $userid [class=$class, type=$type]");
251
252     my $meth = $types{$class} . ".search.atomic";
253     my $buckets;
254
255     if( $type ) {
256         $buckets = $apputils->simplereq( $svc, 
257             $meth, { owner => $userid, btype => $type } );
258     } else {
259         $logger->debug("Grabbing buckets by class $class: $svc : $meth :  {owner => $userid}");
260         $buckets = $apputils->simplereq( $svc, $meth, { owner => $userid } );
261     }
262
263     return _sort_buckets($buckets);
264 }
265
266 __PACKAGE__->register_method(
267     method  => "bucket_create",
268     api_name    => "open-ils.actor.container.create",
269     notes        => <<"    NOTES");
270         Creates a new bucket object.  If requestor is different from
271         bucketOwner, requestor needs CREATE_CONTAINER permissions
272         PARAMS(authtoken, bucketObject);
273         Returns the new bucket object
274     NOTES
275
276 sub bucket_create {
277     my( $self, $client, $authtoken, $class, $bucket ) = @_;
278
279     my $e = new_editor(xact=>1, authtoken=>$authtoken);
280     return $e->event unless $e->checkauth;
281
282     if( $bucket->owner ne $e->requestor->id ) {
283         return $e->event unless
284             $e->allowed('CREATE_CONTAINER');
285
286     } else {
287         return $e->event unless
288             $e->allowed('CREATE_MY_CONTAINER');
289     }
290         
291     $bucket->clear_id;
292
293     my $evt = OpenILS::Event->new('CONTAINER_EXISTS', 
294         payload => [$class, $bucket->owner, $bucket->btype, $bucket->name]);
295     my $search = {name => $bucket->name, owner => $bucket->owner, btype => $bucket->btype};
296
297     my $obj;
298     if( $class eq 'copy' ) {
299         return $evt if $e->search_container_copy_bucket($search)->[0];
300         return $e->event unless
301             $obj = $e->create_container_copy_bucket($bucket);
302     }
303
304     if( $class eq 'callnumber' ) {
305         return $evt if $e->search_container_call_number_bucket($search)->[0];
306         return $e->event unless
307             $obj = $e->create_container_call_number_bucket($bucket);
308     }
309
310     if( $class eq 'biblio' ) {
311         return $evt if $e->search_container_biblio_record_entry_bucket($search)->[0];
312         return $e->event unless
313             $obj = $e->create_container_biblio_record_entry_bucket($bucket);
314     }
315
316     if( $class eq 'user') {
317         return $evt if $e->search_container_user_bucket($search)->[0];
318         return $e->event unless
319             $obj = $e->create_container_user_bucket($bucket);
320     }
321
322     $e->commit;
323     return $obj->id;
324 }
325
326
327 __PACKAGE__->register_method(
328     method  => "item_create",
329     api_name    => "open-ils.actor.container.item.create",
330     signature => {
331         desc => q/
332             Adds one or more items to an existing container
333         /,
334         params => [
335             {desc => 'Authentication token', type => 'string'},
336             {desc => 'Container class.  Can be "copy", "callnumber", "biblio", or "user"', type => 'string'},
337             {desc => 'Item or items.  Can either be a single container item object, or an array of them', type => 'object'},
338         ],
339         return => {
340             desc => 'The ID of the newly created item(s).  In batch context, an array of IDs is returned'
341         }
342     }
343 );
344
345
346 sub item_create {
347     my( $self, $client, $authtoken, $class, $item ) = @_;
348
349     my $e = new_editor(xact=>1, authtoken=>$authtoken);
350     return $e->die_event unless $e->checkauth;
351     my $items = (ref $item eq 'ARRAY') ? $item : [$item];
352
353     my ( $bucket, $evt ) = 
354         $apputils->fetch_container_e($e, $items->[0]->bucket, $class);
355     return $evt if $evt;
356
357     if( $bucket->owner ne $e->requestor->id ) {
358         return $e->die_event unless
359             $e->allowed('CREATE_CONTAINER_ITEM');
360
361     } else {
362 #       return $e->event unless
363 #           $e->allowed('CREATE_CONTAINER_ITEM'); # new perm here?
364     }
365         
366     for my $one_item (@$items) {
367
368         $one_item->clear_id;
369
370         my $stat;
371         if( $class eq 'copy' ) {
372             return $e->die_event unless
373                 $stat = $e->create_container_copy_bucket_item($one_item);
374         }
375
376         if( $class eq 'callnumber' ) {
377             return $e->die_event unless
378                 $stat = $e->create_container_call_number_bucket_item($one_item);
379         }
380
381         if( $class eq 'biblio' ) {
382             return $e->die_event unless
383                 $stat = $e->create_container_biblio_record_entry_bucket_item($one_item);
384         }
385
386         if( $class eq 'user') {
387             return $e->die_event unless
388                 $stat = $e->create_container_user_bucket_item($one_item);
389         }
390     }
391
392     $e->commit;
393
394     # CStoreEeditor inserts the id (pkey) on newly created objects
395     return [ map { $_->id } @$items ] if ref $item eq 'ARRAY';
396     return $item->id; 
397 }
398
399 __PACKAGE__->register_method(
400     method  => 'batch_add_items',
401     api_name    => 'open-ils.actor.container.item.create.batch',
402     stream      => 1,
403     max_bundle_count => 1,
404     signature => {
405         desc => 'Add items to a bucket',
406         params => [
407             {desc => 'Auth token', type => 'string'},
408             {desc => q/
409                 Container class.  
410                 Can be "copy", "call_number", "biblio_record_entry", or "user"'/,
411                 type => 'string'},
412             {desc => 'Bucket ID', type => 'number'},
413             {desc => q/
414                 Item target identifiers.  E.g. for record buckets,
415                 the identifier would be the bib record id/, 
416                 type => 'array'
417             },
418         ],
419         return => {
420             desc => 'Stream of new item Identifiers',
421             type => 'number'
422         }
423     }
424 );
425
426 sub batch_add_items {
427     my ($self, $client, $auth, $bucket_class, $bucket_id, $target_ids) = @_;
428
429     my $e = new_editor(authtoken => $auth, xact => 1);
430     return $e->die_event unless $e->checkauth;
431
432     my $constructor = "Fieldmapper::container::${bucket_class}_bucket_item";
433     my $create = "create_container_${bucket_class}_bucket_item";
434     my $retrieve = "retrieve_container_${bucket_class}_bucket";
435     my $column = "target_${bucket_class}";
436
437     my $bucket = $e->$retrieve($bucket_id) or return $e->die_event;
438
439     if ($bucket->owner ne $e->requestor->id) {
440         return $e->die_event unless $e->allowed('CREATE_CONTAINER_ITEM');
441     }
442
443     for my $target_id (@$target_ids) {
444
445         my $item = $constructor->new;
446         $item->bucket($bucket_id);
447         $item->$column($target_id);
448
449         return $e->die_event unless $e->$create($item);
450         $client->respond($target_id);
451     }
452
453     $e->commit;
454     return undef;
455 }
456
457 __PACKAGE__->register_method(
458     method  => 'batch_delete_items',
459     api_name    => 'open-ils.actor.container.item.delete.batch',
460     stream      => 1,
461     max_bundle_count => 1,
462     signature => {
463         desc => 'Remove items from a bucket',
464         params => [
465             {desc => 'Auth token', type => 'string'},
466             {desc => q/
467                 Container class.  
468                 Can be "copy", "call_number", "biblio_record_entry", or "user"'/,
469                 type => 'string'},
470             {desc => q/
471                 Item target identifiers.  E.g. for record buckets,
472                 the identifier would be the bib record id/, 
473                 type => 'array'
474             }
475         ],
476         return => {
477             desc => 'Stream of new removed target IDs',
478             type => 'number'
479         }
480     }
481 );
482
483 sub batch_delete_items {
484     my ($self, $client, $auth, $bucket_class, $bucket_id, $target_ids) = @_;
485
486     my $e = new_editor(authtoken => $auth, xact => 1);
487     return $e->die_event unless $e->checkauth;
488
489     my $delete = "delete_container_${bucket_class}_bucket_item";
490     my $search = "search_container_${bucket_class}_bucket_item";
491     my $retrieve = "retrieve_container_${bucket_class}_bucket";
492     my $column = "target_${bucket_class}";
493
494     my $bucket = $e->$retrieve($bucket_id) or return $e->die_event;
495
496     if ($bucket->owner ne $e->requestor->id) {
497         return $e->die_event unless $e->allowed('DELETE_CONTAINER_ITEM');
498     }
499
500     for my $target_id (@$target_ids) {
501
502         my $item = $e->$search({bucket => $bucket_id, $column => $target_id})->[0];
503         next unless $item;
504
505         return $e->die_event unless $e->$delete($item);
506         $client->respond($target_id);
507     }
508
509     $e->commit;
510     return undef;
511 }
512
513
514
515
516 __PACKAGE__->register_method(
517     method  => "item_delete",
518     api_name    => "open-ils.actor.container.item.delete",
519     notes        => <<"    NOTES");
520         PARAMS(authtoken, class, itemId)
521     NOTES
522
523 sub item_delete {
524     my( $self, $client, $authtoken, $class, $itemid ) = @_;
525
526     my $e = new_editor(xact=>1, authtoken=>$authtoken);
527     return $e->event unless $e->checkauth;
528
529     my $ret = __item_delete($e, $class, $itemid);
530     $e->commit unless $U->event_code($ret);
531     return $ret;
532 }
533
534 sub __item_delete {
535     my( $e, $class, $itemid ) = @_;
536     my( $bucket, $item, $evt);
537
538     ( $item, $evt ) = $U->fetch_container_item_e( $e, $itemid, $class );
539     return $evt if $evt;
540
541     ( $bucket, $evt ) = $U->fetch_container_e($e, $item->bucket, $class);
542     return $evt if $evt;
543
544     if( $bucket->owner ne $e->requestor->id ) {
545       my $owner = $e->retrieve_actor_user($bucket->owner)
546          or return $e->die_event;
547         return $e->event unless $e->allowed('DELETE_CONTAINER_ITEM', $owner->home_ou);
548     }
549
550     my $stat;
551     if( $class eq 'copy' ) {
552         for my $note (@{$e->search_container_copy_bucket_item_note({item => $item->id})}) {
553             return $e->event unless 
554                 $e->delete_container_copy_bucket_item_note($note);
555         }
556         return $e->event unless
557             $stat = $e->delete_container_copy_bucket_item($item);
558     }
559
560     if( $class eq 'callnumber' ) {
561         for my $note (@{$e->search_container_call_number_bucket_item_note({item => $item->id})}) {
562             return $e->event unless 
563                 $e->delete_container_call_number_bucket_item_note($note);
564         }
565         return $e->event unless
566             $stat = $e->delete_container_call_number_bucket_item($item);
567     }
568
569     if( $class eq 'biblio' ) {
570         for my $note (@{$e->search_container_biblio_record_entry_bucket_item_note({item => $item->id})}) {
571             return $e->event unless 
572                 $e->delete_container_biblio_record_entry_bucket_item_note($note);
573         }
574         return $e->event unless
575             $stat = $e->delete_container_biblio_record_entry_bucket_item($item);
576     }
577
578     if( $class eq 'user') {
579         for my $note (@{$e->search_container_user_bucket_item_note({item => $item->id})}) {
580             return $e->event unless 
581                 $e->delete_container_user_bucket_item_note($note);
582         }
583         return $e->event unless
584             $stat = $e->delete_container_user_bucket_item($item);
585     }
586
587     return $stat;
588 }
589
590
591 __PACKAGE__->register_method(
592     method  => 'full_delete',
593     api_name    => 'open-ils.actor.container.full_delete',
594     notes       => "Complety removes a container including all attached items",
595 );  
596
597 sub full_delete {
598     my( $self, $client, $authtoken, $class, $containerId ) = @_;
599     my( $container, $evt);
600
601     my $e = new_editor(xact=>1, authtoken=>$authtoken);
602     return $e->event unless $e->checkauth;
603
604     ( $container, $evt ) = $apputils->fetch_container_e($e, $containerId, $class);
605     return $evt if $evt;
606
607     if( $container->owner ne $e->requestor->id ) {
608       my $owner = $e->retrieve_actor_user($container->owner)
609          or return $e->die_event;
610         return $e->event unless $e->allowed('DELETE_CONTAINER', $owner->home_ou);
611     }
612
613     my $items; 
614
615     my @s = ({bucket => $containerId}, {idlist=>1});
616
617     if( $class eq 'copy' ) {
618         $items = $e->search_container_copy_bucket_item(@s);
619     }
620
621     if( $class eq 'callnumber' ) {
622         $items = $e->search_container_call_number_bucket_item(@s);
623     }
624
625     if( $class eq 'biblio' ) {
626         $items = $e->search_container_biblio_record_entry_bucket_item(@s);
627     }
628
629     if( $class eq 'user') {
630         $items = $e->search_container_user_bucket_item(@s);
631     }
632
633     __item_delete($e, $class, $_) for @$items;
634
635     my $stat;
636     if( $class eq 'copy' ) {
637         return $e->event unless
638             $stat = $e->delete_container_copy_bucket($container);
639     }
640
641     if( $class eq 'callnumber' ) {
642         return $e->event unless
643             $stat = $e->delete_container_call_number_bucket($container);
644     }
645
646     if( $class eq 'biblio' ) {
647         return $e->event unless
648             $stat = $e->delete_container_biblio_record_entry_bucket($container);
649     }
650
651     if( $class eq 'user') {
652         return $e->event unless
653             $stat = $e->delete_container_user_bucket($container);
654     }
655
656     $e->commit;
657     return $stat;
658 }
659
660 __PACKAGE__->register_method(
661     method      => 'container_update',
662     api_name        => 'open-ils.actor.container.update',
663     signature   => q/
664         Updates the given container item.
665         @param authtoken The login session key
666         @param class The container class
667         @param container The container item
668         @return true on success, 0 on no update, Event on error
669         /
670 );
671
672 sub container_update {
673     my( $self, $conn, $authtoken, $class, $container )  = @_;
674
675     my $e = new_editor(xact=>1, authtoken=>$authtoken);
676     return $e->event unless $e->checkauth;
677
678     my ( $dbcontainer, $evt ) = $U->fetch_container_e($e, $container->id, $class);
679     return $evt if $evt;
680
681     if( $e->requestor->id ne $container->owner ) {
682         return $e->event unless $e->allowed('UPDATE_CONTAINER');
683     }
684
685     my $stat;
686     if( $class eq 'copy' ) {
687         return $e->event unless
688             $stat = $e->update_container_copy_bucket($container);
689     }
690
691     if( $class eq 'callnumber' ) {
692         return $e->event unless
693             $stat = $e->update_container_call_number_bucket($container);
694     }
695
696     if( $class eq 'biblio' ) {
697         return $e->event unless
698             $stat = $e->update_container_biblio_record_entry_bucket($container);
699     }
700
701     if( $class eq 'user') {
702         return $e->event unless
703             $stat = $e->update_container_user_bucket($container);
704     }
705
706     $e->commit;
707     return $stat;
708 }
709
710
711
712 __PACKAGE__->register_method(
713     method  => "anon_cache",
714     api_name    => "open-ils.actor.anon_cache.set_value",
715     signature => {
716         desc => q/
717             Sets a value in the anon web cache.  If the session key is
718             undefined, one will be automatically generated.
719         /,
720         params => [
721             {desc => 'Session key', type => 'string'},
722             {
723                 desc => q/Field name.  The name of the field in this cache session whose value to set/, 
724                 type => 'string'
725             },
726             {
727                 desc => q/The cached value.  This can be any type of object (hash, array, string, etc.)/,
728                 type => 'any'
729             },
730         ],
731         return => {
732             desc => 'session key on success, undef on error',
733             type => 'string'
734         }
735     }
736 );
737
738 __PACKAGE__->register_method(
739     method  => "anon_cache",
740     api_name    => "open-ils.actor.anon_cache.get_value",
741     signature => {
742         desc => q/
743             Returns the cached data at the specified field within the specified cache session.
744         /,
745         params => [
746             {desc => 'Session key', type => 'string'},
747             {
748                 desc => q/Field name.  The name of the field in this cache session whose value to set/, 
749                 type => 'string'
750             },
751         ],
752         return => {
753             desc => 'cached value on success, undef on error',
754             type => 'any'
755         }
756     }
757 );
758
759 __PACKAGE__->register_method(
760     method  => "anon_cache",
761     api_name    => "open-ils.actor.anon_cache.delete_session",
762     signature => {
763         desc => q/
764             Deletes a cache session.
765         /,
766         params => [
767             {desc => 'Session key', type => 'string'},
768         ],
769         return => {
770             desc => 'Session key',
771             type => 'string'
772         }
773     }
774 );
775
776 sub anon_cache {
777     my($self, $conn, $ses_key, $field_key, $value) = @_;
778
779     my $sc = OpenSRF::Utils::SettingsClient->new;
780     my $cache = OpenSRF::Utils::Cache->new('anon');
781     my $cache_timeout = $sc->config_value(cache => anon => 'max_cache_time') || 1800; # 30 minutes
782     my $cache_size = $sc->config_value(cache => anon => 'max_cache_size') || 102400; # 100k
783
784     if($self->api_name =~ /delete_session/) {
785
786        return $cache->delete_cache($ses_key); 
787
788     }  elsif( $self->api_name =~ /set_value/ ) {
789
790         $ses_key = md5_hex(time . rand($$)) unless $ses_key;
791         my $blob = $cache->get_cache($ses_key) || {};
792         $blob->{$field_key} = $value;
793         return undef if 
794             length(OpenSRF::Utils::JSON->perl2JSON($blob)) > $cache_size; # bytes, characters, whatever ;)
795         $cache->put_cache($ses_key, $blob, $cache_timeout);
796         return $ses_key;
797
798     } else {
799
800         my $blob = $cache->get_cache($ses_key) or return undef;
801         return $blob if (!defined($field_key));
802         return $blob->{$field_key};
803     }
804 }
805
806 sub batch_statcat_apply {
807     my $self = shift;
808     my $client = shift;
809     my $ses = shift;
810     my $c_id = shift;
811     my $changes = shift;
812
813     # $changes is a hashref that looks like:
814     #   {
815     #       remove  => [ qw/ stat cat ids to remove / ],
816     #       apply   => { $statcat_id => $value_string, ... }
817     #   }
818
819     my $class = 'user';
820     my $max = 0;
821     my $count = 0;
822     my $stage = 0;
823
824     my $e = new_editor(xact=>1, authtoken=>$ses);
825     return $e->die_event unless $e->checkauth;
826     $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
827     return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
828
829     my $meth = 'retrieve_' . $ctypes{$class};
830     my $bkt = $e->$meth($c_id) or return $e->die_event;
831
832     unless($bkt->owner eq $e->requestor->id) {
833         $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
834         my $owner = $e->retrieve_actor_user($bkt->owner)
835             or return $e->die_event;
836         return $e->die_event unless (
837             $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
838         );
839     }
840
841     $meth = 'search_' . $ctypes{$class} . '_item';
842     my $contents = $e->$meth({bucket => $c_id});
843
844     if ($self->{perms}) {
845         $max = scalar(@$contents);
846         $client->respond({ ord => $stage, max => $max, count => 0, stage => 'ITEM_PERM_CHECK' });
847         for my $item (@$contents) {
848             $count++;
849             $meth = 'retrieve_' . $itypes{$class};
850             my $field = 'target_'.$ttypes{$class};
851             my $obj = $e->$meth($item->$field);
852
853             for my $perm_field (keys %{$self->{perms}}) {
854                 my $perm_def = $self->{perms}->{$perm_field};
855                 my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
856                 for my $p (@$pwhat) {
857                     $e->allowed($p, $obj->$pwhere) or return $e->die_event;
858                 }
859             }
860             $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
861         }
862         $stage++;
863     }
864
865     my @users = map { $_->target_user } @$contents;
866     $max = scalar(@users) * scalar(@{$changes->{remove}});
867     $count = 0;
868     $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' });
869
870     my $chunk = int($max / 10) || 1;
871     my $to_remove = $e->search_actor_stat_cat_entry_user_map({ target_usr => \@users, stat_cat => $changes->{remove} });
872     for my $t (@$to_remove) {
873         $e->delete_actor_stat_cat_entry_user_map($t);
874         $count++;
875         $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' })
876             unless ($count % $chunk);
877     }
878
879     $stage++;
880
881     $max = scalar(@users) * scalar(keys %{$changes->{apply}});
882     $count = 0;
883     $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' });
884
885     $chunk = int($max / 10) || 1;
886     for my $item (@$contents) {
887         for my $astatcat (keys %{$changes->{apply}}) {
888             my $new_value = $changes->{apply}->{$astatcat};
889             my $to_change = $e->search_actor_stat_cat_entry_user_map({ target_usr => $item->target_user, stat_cat => $astatcat });
890             if (@$to_change) {
891                 $to_change = $$to_change[0];
892                 $to_change->stat_cat_entry($new_value);
893                 $e->update_actor_stat_cat_entry_user_map($to_change);
894             } else {
895                 $to_change = new Fieldmapper::actor::stat_cat_entry_user_map;
896                 $to_change->stat_cat_entry($new_value);
897                 $to_change->stat_cat($astatcat);
898                 $to_change->target_usr($item->target_user);
899                 $e->create_actor_stat_cat_entry_user_map($to_change);
900             }
901             $count++;
902             $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' })
903                 unless ($count % $chunk);
904         }
905     }
906
907     $e->commit;
908
909     return { stage => 'COMPLETE' };
910 }
911
912 __PACKAGE__->register_method(
913     method  => "batch_statcat_apply",
914     api_name    => "open-ils.actor.container.user.batch_statcat_apply",
915     ctype       => 'user',
916     perms       => {
917             home_ou     => 'UPDATE_USER', # field -> perm means "test this perm with field as context OU", both old and new
918     },
919     fields      => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
920     signature => {
921         desc => 'Edits allowed fields on users in a bucket',
922         params => [{
923             desc => 'Session key', type => 'string',
924             desc => 'User container id',
925             desc => 'Hash of statcats to apply or remove', type => 'hash',
926         }],
927         return => {
928             desc => 'Object with the structure { stage => "stage string", max => max_for_stage, count => count_in_stage }',
929             type => 'hash'
930         }
931     }
932 );
933
934
935 sub apply_rollback {
936     my $self = shift;
937     my $client = shift;
938     my $ses = shift;
939     my $c_id = shift;
940     my $main_fsg = shift;
941
942     my $max = 0;
943     my $count = 0;
944     my $stage = 0;
945
946     my $class = $self->{ctype} or return undef;
947
948     my $e = new_editor(xact=>1, authtoken=>$ses);
949     return $e->die_event unless $e->checkauth;
950
951     for my $bp (@{$batch_perm{$class}}) {
952         return { stage => 'COMPLETE' } unless $e->allowed($bp);
953     }
954
955     $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
956     return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
957
958     my $meth = 'retrieve_' . $ctypes{$class};
959     my $bkt = $e->$meth($c_id) or return $e->die_event;
960
961     unless($bkt->owner eq $e->requestor->id) {
962         $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
963         my $owner = $e->retrieve_actor_user($bkt->owner)
964             or return $e->die_event;
965         return $e->die_event unless (
966             $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
967         );
968     }
969
970     $main_fsg = $e->retrieve_action_fieldset_group($main_fsg);
971     return { stage => 'COMPLETE', error => 'No field set group' } unless $main_fsg;
972
973     my $rbg = $e->retrieve_action_fieldset_group($main_fsg->rollback_group);
974     return { stage => 'COMPLETE', error => 'No rollback field set group' } unless $rbg;
975
976     my $fieldsets = $e->search_action_fieldset({fieldset_group => $rbg->id});
977     $max = scalar(@$fieldsets);
978
979     $client->respond({ ord => $stage, max => $max, count => 0, stage => 'APPLY_EDITS' });
980     for my $fs (@$fieldsets) {
981         my $res = $e->json_query({
982             from => ['action.apply_fieldset', $fs->id, $table{$class}, 'id', undef]
983         })->[0]->{'action.apply_fieldset'};
984
985         $client->respond({
986             ord => $stage,
987             max => $max,
988             count => ++$count,
989             stage => 'APPLY_EDITS',
990             error => $res ? "Could not apply fieldset ".$fs->id.": $res" : undef
991         });
992     }
993
994     $main_fsg->rollback_time('now');
995     $e->update_action_fieldset_group($main_fsg);
996
997     $e->commit;
998
999     return { stage => 'COMPLETE' };
1000 }
1001 __PACKAGE__->register_method(
1002     method  => "apply_rollback",
1003     max_bundle_count => 1,
1004     api_name    => "open-ils.actor.container.user.apply_rollback",
1005     ctype       => 'user',
1006     signature => {
1007         desc => 'Applys rollback of a fieldset group to users in a bucket',
1008         params => [
1009             { desc => 'Session key', type => 'string' },
1010             { desc => 'User container id', type => 'number' },
1011             { desc => 'Main (non-rollback) fieldset group' },
1012         ],
1013         return => {
1014             desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
1015             type => 'hash'
1016         }
1017     }
1018 );
1019
1020
1021 sub batch_edit {
1022     my $self = shift;
1023     my $client = shift;
1024     my $ses = shift;
1025     my $c_id = shift;
1026     my $edit_name = shift;
1027     my $edits = shift;
1028
1029     my $max = 0;
1030     my $count = 0;
1031     my $stage = 0;
1032
1033     my $class = $self->{ctype} or return undef;
1034
1035     my $e = new_editor(xact=>1, authtoken=>$ses);
1036     return $e->die_event unless $e->checkauth;
1037
1038     for my $bp (@{$batch_perm{$class}}) {
1039         return { stage => 'COMPLETE' } unless $e->allowed($bp);
1040     }
1041
1042     $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
1043     return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
1044
1045     my $meth = 'retrieve_' . $ctypes{$class};
1046     my $bkt = $e->$meth($c_id) or return $e->die_event;
1047
1048     unless($bkt->owner eq $e->requestor->id) {
1049         $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
1050         my $owner = $e->retrieve_actor_user($bkt->owner)
1051             or return $e->die_event;
1052         return $e->die_event unless (
1053             $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
1054         );
1055     }
1056
1057     $meth = 'search_' . $ctypes{$class} . '_item';
1058     my $contents = $e->$meth({bucket => $c_id});
1059
1060     $max = 0;
1061     $max = scalar(@$contents) if ($self->{perms});
1062     $max += scalar(@$contents) if ($self->{base_perm});
1063
1064     my $obj_cache = {};
1065     if ($self->{base_perm}) {
1066         $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1067         for my $item (@$contents) {
1068             $count++;
1069             $meth = 'retrieve_' . $itypes{$class};
1070             my $field = 'target_'.$ttypes{$class};
1071             my $obj = $$obj_cache{$item->$field} = $e->$meth($item->$field);
1072
1073             for my $perm_field (keys %{$self->{base_perm}}) {
1074                 my $perm_def = $self->{base_perm}->{$perm_field};
1075                 my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
1076                 for my $p (@$pwhat) {
1077                     $e->allowed($p, $obj->$pwhere) or return $e->die_event;
1078                     if ($$edits{$pwhere}) {
1079                         $e->allowed($p, $$edits{$pwhere}) or do {
1080                             $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
1081                             return $e->die_event;
1082                         };
1083                     }
1084                 }
1085             }
1086             $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1087         }
1088     }
1089
1090     if ($self->{perms}) {
1091         $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1092         for my $item (@$contents) {
1093             $count++;
1094             $meth = 'retrieve_' . $itypes{$class};
1095             my $field = 'target_'.$ttypes{$class};
1096             my $obj = $$obj_cache{$item->$field} || $e->$meth($item->$field);
1097
1098             for my $perm_field (keys %{$self->{perms}}) {
1099                 my $perm_def = $self->{perms}->{$perm_field};
1100                 if (ref($perm_def) eq 'HASH') { # we care about specific values being set
1101                     for my $perm_value (keys %$perm_def) {
1102                         if (exists $$edits{$perm_field} && $$edits{$perm_field} eq $perm_value) { # check permission
1103                             while (my ($pwhat,$pwhere) = each %{$$perm_def{$perm_value}}) {
1104                                 if ($pwhere eq '*') {
1105                                     $pwhere = undef;
1106                                 } else {
1107                                     $pwhere = $obj->$pwhere;
1108                                 }
1109                                 $pwhat = [ split / /, $pwhat ];
1110                                 for my $p (@$pwhat) {
1111                                     $e->allowed($p, $pwhere) or do {
1112                                         $pwhere ||= "everywhere";
1113                                         $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
1114                                         return $e->die_event;
1115                                     };
1116                                 }
1117                             }
1118                         }
1119                     }
1120                 } elsif (ref($perm_def) eq 'CODE') { # we need to run the code on old and new, and pass both tests
1121                     if (exists $$edits{$perm_field}) {
1122                         $perm_def->($e, $obj->$perm_field) or return $e->die_event;
1123                         $perm_def->($e, $$edits{$perm_field}) or return $e->die_event;
1124                     }
1125                 } else { # we're checking an ou field
1126                     my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
1127                     if ($$edits{$pwhere}) {
1128                         for my $p (@$pwhat) {
1129                             $e->allowed($p, $obj->$pwhere) or return $e->die_event;
1130                             $e->allowed($p, $$edits{$pwhere}) or do {
1131                                 $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
1132                                 return $e->die_event;
1133                             };
1134                         }
1135                     }
1136                 }
1137             }
1138             $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1139         }
1140         $stage++;
1141     }
1142
1143     $client->respond({ ord => $stage++, stage => 'FIELDSET_GROUP_CREATE' });
1144     my $fsgroup = Fieldmapper::action::fieldset_group->new;
1145     $fsgroup->isnew(1);
1146     $fsgroup->name($edit_name);
1147     $fsgroup->creator($e->requestor->id);
1148     $fsgroup->owning_lib($e->requestor->ws_ou);
1149     $fsgroup->container($c_id);
1150     $fsgroup->container_type($ttypes{$class});
1151     $fsgroup = $e->create_action_fieldset_group($fsgroup);
1152
1153     $client->respond({ ord => $stage++, stage => 'FIELDSET_CREATE' });
1154     my $fieldset = Fieldmapper::action::fieldset->new;
1155     $fieldset->isnew(1);
1156     $fieldset->fieldset_group($fsgroup->id);
1157     $fieldset->owner($e->requestor->id);
1158     $fieldset->owning_lib($e->requestor->ws_ou);
1159     $fieldset->status('PENDING');
1160     $fieldset->classname($htypes{$class});
1161     $fieldset->name($edit_name . ' batch group fieldset');
1162     $fieldset->stored_query($qtypes{$class});
1163     $fieldset = $e->create_action_fieldset($fieldset);
1164
1165     my @keys = keys %$edits;
1166     $max = scalar(@keys);
1167     $count = 0;
1168     $client->respond({ ord => $stage, count=> $count, max => $max, stage => 'FIELDSET_EDITS_CREATE' });
1169     for my $key (@keys) {
1170         if ($self->{fields}) { # restrict edits to registered fields
1171             next unless (grep { $_ eq $key } @{$self->{fields}});
1172         }
1173         my $fs_cv = Fieldmapper::action::fieldset_col_val->new;
1174         $fs_cv->isnew(1);
1175         $fs_cv->fieldset($fieldset->id);
1176         $fs_cv->col($key);
1177         $fs_cv->val($$edits{$key});
1178         $e->create_action_fieldset_col_val($fs_cv);
1179         $count++;
1180         $client->respond({ ord => $stage, count=> $count, max => $max, stage => 'FIELDSET_EDITS_CREATE' });
1181     }
1182
1183     $client->respond({ ord => ++$stage, stage => 'CONSTRUCT_QUERY' });
1184     my $qstore = OpenSRF::AppSession->connect('open-ils.qstore');
1185     my $prep = $qstore->request('open-ils.qstore.prepare', $fieldset->stored_query)->gather(1);
1186     my $token = $prep->{token};
1187     $qstore->request('open-ils.qstore.bind_param', $token, {bucket => $c_id})->gather(1);
1188     my $sql = $qstore->request('open-ils.qstore.sql', $token)->gather(1);
1189     $sql =~ s/\n\s*/ /g; # normalize the string
1190     $sql =~ s/;\s*//g; # kill trailing semicolon
1191
1192     $client->respond({ ord => ++$stage, stage => 'APPLY_EDITS' });
1193     my $res = $e->json_query({
1194         from => ['action.apply_fieldset', $fieldset->id, $table{$class}, 'id', $sql]
1195     })->[0]->{'action.apply_fieldset'};
1196
1197     $e->commit;
1198     $qstore->disconnect;
1199
1200     return { fieldset_group => $fsgroup->id, stage => 'COMPLETE', error => $res };
1201 }
1202
1203 __PACKAGE__->register_method(
1204     method  => "batch_edit",
1205     max_bundle_count => 1,
1206     api_name    => "open-ils.actor.container.user.batch_edit",
1207     ctype       => 'user',
1208     base_perm   => { home_ou => 'UPDATE_USER' },
1209     perms       => {
1210             profile => sub {
1211                 my ($e, $group) = @_;
1212                 my $g = $e->retrieve_permission_grp_tree($group);
1213                 if (my $p = $g->application_perm()) {
1214                     return $e->allowed($p);
1215                 }
1216                 return 1;
1217             }, # code ref is run with params (editor,value), for both old and new value
1218             # home_ou => 'UPDATE_USER', # field -> perm means "test this perm with field as context OU", both old and new
1219             barred  => {
1220                     t => { BAR_PATRON => 'home_ou' },
1221                     f => { UNBAR_PATRON => 'home_ou' }
1222             } # field -> struct means "if field getting value "key" check -> perm -> at context org, both old and new
1223     },
1224     fields      => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
1225     signature => {
1226         desc => 'Edits allowed fields on users in a bucket',
1227         params => [
1228             { desc => 'Session key', type => 'string' },
1229             { desc => 'User container id', type => 'number' },
1230             { desc => 'Batch edit name', type => 'string' },
1231             { desc => 'Edit hash, key is column, value is new value to apply', type => 'hash' },
1232         ],
1233         return => {
1234             desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
1235             type => 'hash'
1236         }
1237     }
1238 );
1239
1240 __PACKAGE__->register_method(
1241     method  => "batch_edit",
1242     api_name    => "open-ils.actor.container.user.batch_delete",
1243     ctype       => 'user',
1244     perms       => {
1245             deleted => {
1246                     t => { 'DELETE_USER UPDATE_USER' => 'home_ou' },
1247                     f => { 'UPDATE_USER' => 'home_ou' }
1248             }
1249     },
1250     fields      => [ qw/deleted/ ],
1251     signature => {
1252         desc => 'Deletes users in a bucket',
1253         params => [{
1254             { desc => 'Session key', type => 'string' },
1255             { desc => 'User container id', type => 'number' },
1256             { desc => 'Batch delete name', type => 'string' },
1257             { desc => 'Edit delete, key is "deleted", value is new value to apply ("t")', type => 'hash' },
1258             
1259         }],
1260         return => {
1261             desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
1262             type => 'hash'
1263         }
1264     }
1265 );
1266
1267
1268
1269 1;
1270
1271