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