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