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