1 package OpenILS::Application::Actor::Container;
2 use base 'OpenILS::Application';
3 use strict; use warnings;
4 use OpenILS::Application::AppUtils;
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;
15 my $apputils = "OpenILS::Application::AppUtils";
17 my $logger = "OpenSRF::Utils::Logger";
19 sub initialize { return 1; }
21 my $svc = 'open-ils.cstore';
22 my $meth = 'open-ils.cstore.direct.container';
32 $batch_perm{'biblio'} = ['UPDATE_MARC'];
33 $batch_perm{'callnumber'} = ['UPDATE_VOLUME'];
34 $batch_perm{'copy'} = ['UPDATE_COPY'];
35 $batch_perm{'user'} = ['UPDATE_USER'];
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";
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";
47 $itypes{'biblio'} = "biblio_record_entry";
48 $itypes{'callnumber'} = "asset_call_number";
49 $itypes{'copy'} = "asset_copy";
50 $itypes{'user'} = "actor_user";
52 $ttypes{'biblio'} = "biblio_record_entry";
53 $ttypes{'callnumber'} = "call_number";
54 $ttypes{'copy'} = "copy";
55 $ttypes{'user'} = "user";
57 $htypes{'biblio'} = "bre";
58 $htypes{'callnumber'} = "acn";
59 $htypes{'copy'} = "acp";
60 $htypes{'user'} = "au";
62 $table{'biblio'} = "biblio.record_entry";
63 $table{'callnumber'} = "asset.call_number";
64 $table{'copy'} = "asset.copy";
65 $table{'user'} = "actor.usr";
67 #$qtypes{'biblio'} = 0
68 #$qtypes{'callnumber'} = 0;
76 return $buckets unless ($buckets && $buckets->[0]);
77 return [ sort { $a->name cmp $b->name } @$buckets ];
80 __PACKAGE__->register_method(
81 method => "bucket_retrieve_all",
82 api_name => "open-ils.actor.container.all.retrieve_by_user",
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.
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;
96 if($e->requestor->id ne $user_id) {
97 return $e->event unless $e->allowed('VIEW_CONTAINER');
101 for my $type (keys %ctypes) {
102 my $meth = "search_" . $ctypes{$type};
103 $buckets{$type} = _sort_buckets($e->$meth({owner => $user_id}));
109 __PACKAGE__->register_method(
110 method => "bucket_flesh",
111 api_name => "open-ils.actor.container.flesh",
116 __PACKAGE__->register_method(
117 method => "bucket_flesh_pub",
118 api_name => "open-ils.actor.container.public.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);
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);
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;
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)
152 my $fmclass = $bkt->class_name . "i";
153 $meth = 'search_' . $ctypes{$class} . '_item';
156 {bucket => $bucket_id},
157 { order_by => {$fmclass => "pos"},
159 flesh_fields => {$fmclass => ['notes']}
168 __PACKAGE__->register_method(
169 method => "item_note_cud",
170 api_name => "open-ils.actor.container.item_note.cud",
175 my($self, $conn, $auth, $class, $note) = @_;
177 return new OpenILS::Event("BAD_PARAMS") unless
178 $note->class_name =~ /bucket_item_note$/;
180 my $e = new_editor(authtoken => $auth, xact => 1);
181 return $e->die_event unless $e->checkauth;
183 my $meat = $ctypes{$class} . "_item_note";
184 my $meth = "retrieve_$meat";
186 my $item_meat = $ctypes{$class} . "_item";
187 my $item_meth = "retrieve_$item_meat";
189 my $nhint = $Fieldmapper::fieldmap->{$note->class_name}->{hint};
190 (my $ihint = $nhint) =~ s/n$//og;
192 my ($db_note, $item);
197 $item = $e->$item_meth([
199 flesh => 1, flesh_fields => {$ihint => ["bucket"]}
201 ]) or return $e->die_event;
203 $db_note = $e->$meth([
211 ]) or return $e->die_event;
213 $item = $db_note->item;
216 if($item->bucket->owner ne $e->requestor->id) {
217 return $e->die_event unless $e->allowed("UPDATE_CONTAINER");
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);
228 __PACKAGE__->register_method(
229 method => "bucket_retrieve_class",
230 api_name => "open-ils.actor.container.retrieve_by_class",
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
239 If bucketOwnerId is not defined, the authtoken is used as the
241 If requestor ID is different than bucketOwnerId, requestor must have
242 VIEW_CONTAINER permissions.
245 sub bucket_retrieve_class {
246 my( $self, $client, $authtoken, $userid, $class, $type ) = @_;
248 my( $staff, $user, $evt ) =
249 $apputils->checkses_requestor( $authtoken, $userid, 'VIEW_CONTAINER' );
252 $userid = $staff->id unless $userid;
254 $logger->debug("User " . $staff->id .
255 " retrieving buckets for user $userid [class=$class, type=$type]");
257 my $meth = $types{$class} . ".search.atomic";
261 $buckets = $apputils->simplereq( $svc,
262 $meth, { owner => $userid, btype => $type } );
264 $logger->debug("Grabbing buckets by class $class: $svc : $meth : {owner => $userid}");
265 $buckets = $apputils->simplereq( $svc, $meth, { owner => $userid } );
268 return _sort_buckets($buckets);
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
282 my( $self, $client, $authtoken, $class, $bucket ) = @_;
284 my $e = new_editor(xact=>1, authtoken=>$authtoken);
285 return $e->event unless $e->checkauth;
287 if( $bucket->owner ne $e->requestor->id ) {
288 return $e->event unless
289 $e->allowed('CREATE_CONTAINER');
292 return $e->event unless
293 $e->allowed('CREATE_MY_CONTAINER');
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};
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);
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);
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);
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);
332 __PACKAGE__->register_method(
333 method => "item_create",
334 api_name => "open-ils.actor.container.item.create",
337 Adds one or more items to an existing container
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'},
346 desc => 'The ID of the newly created item(s). In batch context, an array of IDs is returned'
353 my( $self, $client, $authtoken, $class, $item, $dupe_check ) = @_;
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];
359 my ( $bucket, $evt ) =
360 $apputils->fetch_container_e($e, $items->[0]->bucket, $class);
363 if( $bucket->owner ne $e->requestor->id ) {
364 return $e->die_event unless
365 $e->allowed('CREATE_CONTAINER_ITEM');
368 # return $e->event unless
369 # $e->allowed('CREATE_CONTAINER_ITEM'); # new perm here?
372 for my $one_item (@$items) {
377 if( $class eq 'copy' ) {
380 $e->search_container_copy_bucket_item(
381 {bucket => $one_item->bucket, target_copy => $one_item->target_copy}
384 return $e->die_event unless
385 $stat = $e->create_container_copy_bucket_item($one_item);
388 if( $class eq 'callnumber' ) {
391 $e->search_container_call_number_bucket_item(
392 {bucket => $one_item->bucket, target_call_number => $one_item->target_call_number}
395 return $e->die_event unless
396 $stat = $e->create_container_call_number_bucket_item($one_item);
399 if( $class eq 'biblio' ) {
402 $e->search_container_biblio_record_entry_bucket_item(
403 {bucket => $one_item->bucket, target_biblio_record_entry => $one_item->target_biblio_record_entry}
406 return $e->die_event unless
407 $stat = $e->create_container_biblio_record_entry_bucket_item($one_item);
410 if( $class eq 'user') {
413 $e->search_container_user_bucket_item(
414 {bucket => $one_item->bucket, target_user => $one_item->target_user}
417 return $e->die_event unless
418 $stat = $e->create_container_user_bucket_item($one_item);
424 # CStoreEeditor inserts the id (pkey) on newly created objects
425 return [ map { $_->id } @$items ] if ref $item eq 'ARRAY';
429 __PACKAGE__->register_method(
430 method => 'batch_add_items',
431 api_name => 'open-ils.actor.container.item.create.batch',
433 max_bundle_count => 1,
435 desc => 'Add items to a bucket',
437 {desc => 'Auth token', type => 'string'},
440 Can be "copy", "call_number", "biblio_record_entry", or "user"'/,
442 {desc => 'Bucket ID', type => 'number'},
444 Item target identifiers. E.g. for record buckets,
445 the identifier would be the bib record id/,
450 desc => 'Stream of new item Identifiers',
456 sub batch_add_items {
457 my ($self, $client, $auth, $bucket_class, $bucket_id, $target_ids) = @_;
459 my $e = new_editor(authtoken => $auth, xact => 1);
460 return $e->die_event unless $e->checkauth;
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}";
467 my $bucket = $e->$retrieve($bucket_id) or return $e->die_event;
469 if ($bucket->owner ne $e->requestor->id) {
470 return $e->die_event unless $e->allowed('CREATE_CONTAINER_ITEM');
473 for my $target_id (@$target_ids) {
475 my $item = $constructor->new;
476 $item->bucket($bucket_id);
477 $item->$column($target_id);
479 return $e->die_event unless $e->$create($item);
480 $client->respond($target_id);
487 __PACKAGE__->register_method(
488 method => 'batch_delete_items',
489 api_name => 'open-ils.actor.container.item.delete.batch',
491 max_bundle_count => 1,
493 desc => 'Remove items from a bucket',
495 {desc => 'Auth token', type => 'string'},
498 Can be "copy", "call_number", "biblio_record_entry", or "user"'/,
501 Item target identifiers. E.g. for record buckets,
502 the identifier would be the bib record id/,
507 desc => 'Stream of new removed target IDs',
513 sub batch_delete_items {
514 my ($self, $client, $auth, $bucket_class, $bucket_id, $target_ids) = @_;
516 my $e = new_editor(authtoken => $auth, xact => 1);
517 return $e->die_event unless $e->checkauth;
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}";
524 my $bucket = $e->$retrieve($bucket_id) or return $e->die_event;
526 if ($bucket->owner ne $e->requestor->id) {
527 return $e->die_event unless $e->allowed('DELETE_CONTAINER_ITEM');
530 for my $target_id (@$target_ids) {
532 my $item = $e->$search({bucket => $bucket_id, $column => $target_id})->[0];
535 return $e->die_event unless $e->$delete($item);
536 $client->respond($target_id);
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)
554 my( $self, $client, $authtoken, $class, $itemid ) = @_;
556 my $e = new_editor(xact=>1, authtoken=>$authtoken);
557 return $e->event unless $e->checkauth;
559 my $ret = __item_delete($e, $class, $itemid);
560 $e->commit unless $U->event_code($ret);
565 my( $e, $class, $itemid ) = @_;
566 my( $bucket, $item, $evt);
568 ( $item, $evt ) = $U->fetch_container_item_e( $e, $itemid, $class );
571 ( $bucket, $evt ) = $U->fetch_container_e($e, $item->bucket, $class);
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);
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);
586 return $e->event unless
587 $stat = $e->delete_container_copy_bucket_item($item);
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);
595 return $e->event unless
596 $stat = $e->delete_container_call_number_bucket_item($item);
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);
604 return $e->event unless
605 $stat = $e->delete_container_biblio_record_entry_bucket_item($item);
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);
613 return $e->event unless
614 $stat = $e->delete_container_user_bucket_item($item);
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",
628 my( $self, $client, $authtoken, $class, $containerId ) = @_;
629 my( $container, $evt);
631 my $e = new_editor(xact=>1, authtoken=>$authtoken);
632 return $e->event unless $e->checkauth;
634 ( $container, $evt ) = $apputils->fetch_container_e($e, $containerId, $class);
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);
645 my @s = ({bucket => $containerId}, {idlist=>1});
647 if( $class eq 'copy' ) {
648 $items = $e->search_container_copy_bucket_item(@s);
651 if( $class eq 'callnumber' ) {
652 $items = $e->search_container_call_number_bucket_item(@s);
655 if( $class eq 'biblio' ) {
656 $items = $e->search_container_biblio_record_entry_bucket_item(@s);
659 if( $class eq 'user') {
660 $items = $e->search_container_user_bucket_item(@s);
663 __item_delete($e, $class, $_) for @$items;
666 if( $class eq 'copy' ) {
667 return $e->event unless
668 $stat = $e->delete_container_copy_bucket($container);
671 if( $class eq 'callnumber' ) {
672 return $e->event unless
673 $stat = $e->delete_container_call_number_bucket($container);
676 if( $class eq 'biblio' ) {
677 return $e->event unless
678 $stat = $e->delete_container_biblio_record_entry_bucket($container);
681 if( $class eq 'user') {
682 return $e->event unless
683 $stat = $e->delete_container_user_bucket($container);
690 __PACKAGE__->register_method(
691 method => 'container_update',
692 api_name => 'open-ils.actor.container.update',
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
702 sub container_update {
703 my( $self, $conn, $authtoken, $class, $container ) = @_;
705 my $e = new_editor(xact=>1, authtoken=>$authtoken);
706 return $e->event unless $e->checkauth;
708 my ( $dbcontainer, $evt ) = $U->fetch_container_e($e, $container->id, $class);
711 if( $e->requestor->id ne $container->owner ) {
712 return $e->event unless $e->allowed('UPDATE_CONTAINER');
716 if( $class eq 'copy' ) {
717 return $e->event unless
718 $stat = $e->update_container_copy_bucket($container);
721 if( $class eq 'callnumber' ) {
722 return $e->event unless
723 $stat = $e->update_container_call_number_bucket($container);
726 if( $class eq 'biblio' ) {
727 return $e->event unless
728 $stat = $e->update_container_biblio_record_entry_bucket($container);
731 if( $class eq 'user') {
732 return $e->event unless
733 $stat = $e->update_container_user_bucket($container);
742 __PACKAGE__->register_method(
743 method => "anon_cache",
744 api_name => "open-ils.actor.anon_cache.set_value",
747 Sets a value in the anon web cache. If the session key is
748 undefined, one will be automatically generated.
751 {desc => 'Session key', type => 'string'},
753 desc => q/Field name. The name of the field in this cache session whose value to set/,
757 desc => q/The cached value. This can be any type of object (hash, array, string, etc.)/,
762 desc => 'session key on success, undef on error',
768 __PACKAGE__->register_method(
769 method => "anon_cache",
770 api_name => "open-ils.actor.anon_cache.get_value",
773 Returns the cached data at the specified field within the specified cache session.
776 {desc => 'Session key', type => 'string'},
778 desc => q/Field name. The name of the field in this cache session whose value to set/,
783 desc => 'cached value on success, undef on error',
789 __PACKAGE__->register_method(
790 method => "anon_cache",
791 api_name => "open-ils.actor.anon_cache.delete_session",
794 Deletes a cache session.
797 {desc => 'Session key', type => 'string'},
800 desc => 'Session key',
807 my($self, $conn, $ses_key, $field_key, $value) = @_;
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
814 if($self->api_name =~ /delete_session/) {
816 return $cache->delete_cache($ses_key);
818 } elsif( $self->api_name =~ /set_value/ ) {
820 $ses_key = md5_hex(time . rand($$)) unless $ses_key;
821 my $blob = $cache->get_cache($ses_key) || {};
822 $blob->{$field_key} = $value;
824 length(OpenSRF::Utils::JSON->perl2JSON($blob)) > $cache_size; # bytes, characters, whatever ;)
825 $cache->put_cache($ses_key, $blob, $cache_timeout);
830 my $blob = $cache->get_cache($ses_key) or return undef;
831 return $blob if (!defined($field_key));
832 return $blob->{$field_key};
836 sub batch_statcat_apply {
843 # $changes is a hashref that looks like:
845 # remove => [ qw/ stat cat ids to remove / ],
846 # apply => { $statcat_id => $value_string, ... }
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');
859 my $meth = 'retrieve_' . $ctypes{$class};
860 my $bkt = $e->$meth($c_id) or return $e->die_event;
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)
871 $meth = 'search_' . $ctypes{$class} . '_item';
872 my $contents = $e->$meth({bucket => $c_id});
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) {
879 $meth = 'retrieve_' . $itypes{$class};
880 my $field = 'target_'.$ttypes{$class};
881 my $obj = $e->$meth($item->$field);
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;
890 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
895 my @users = map { $_->target_user } @$contents;
896 $max = scalar(@users) * scalar(@{$changes->{remove}});
898 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' });
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);
905 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' })
906 unless ($count % $chunk);
911 $max = scalar(@users) * scalar(keys %{$changes->{apply}});
913 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' });
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 });
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);
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);
932 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' })
933 unless ($count % $chunk);
939 return { stage => 'COMPLETE' };
942 __PACKAGE__->register_method(
943 method => "batch_statcat_apply",
944 api_name => "open-ils.actor.container.user.batch_statcat_apply",
947 home_ou => 'UPDATE_USER', # field -> perm means "test this perm with field as context OU", both old and new
949 fields => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
951 desc => 'Edits allowed fields on users in a bucket',
953 desc => 'Session key', type => 'string',
954 desc => 'User container id',
955 desc => 'Hash of statcats to apply or remove', type => 'hash',
958 desc => 'Object with the structure { stage => "stage string", max => max_for_stage, count => count_in_stage }',
970 my $main_fsg = shift;
976 my $class = $self->{ctype} or return undef;
978 my $e = new_editor(xact=>1, authtoken=>$ses);
979 return $e->die_event unless $e->checkauth;
981 for my $bp (@{$batch_perm{$class}}) {
982 return { stage => 'COMPLETE' } unless $e->allowed($bp);
985 $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
986 return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
988 my $meth = 'retrieve_' . $ctypes{$class};
989 my $bkt = $e->$meth($c_id) or return $e->die_event;
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)
1000 $main_fsg = $e->retrieve_action_fieldset_group($main_fsg);
1001 return { stage => 'COMPLETE', error => 'No field set group' } unless $main_fsg;
1003 my $rbg = $e->retrieve_action_fieldset_group($main_fsg->rollback_group);
1004 return { stage => 'COMPLETE', error => 'No rollback field set group' } unless $rbg;
1006 my $fieldsets = $e->search_action_fieldset({fieldset_group => $rbg->id});
1007 $max = scalar(@$fieldsets);
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'};
1019 stage => 'APPLY_EDITS',
1020 error => $res ? "Could not apply fieldset ".$fs->id.": $res" : undef
1024 $main_fsg->rollback_time('now');
1025 $e->update_action_fieldset_group($main_fsg);
1029 return { stage => 'COMPLETE' };
1031 __PACKAGE__->register_method(
1032 method => "apply_rollback",
1033 max_bundle_count => 1,
1034 api_name => "open-ils.actor.container.user.apply_rollback",
1037 desc => 'Applys rollback of a fieldset group to users in a bucket',
1039 { desc => 'Session key', type => 'string' },
1040 { desc => 'User container id', type => 'number' },
1041 { desc => 'Main (non-rollback) fieldset group' },
1044 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
1056 my $edit_name = shift;
1063 my $class = $self->{ctype} or return undef;
1065 my $e = new_editor(xact=>1, authtoken=>$ses);
1066 return $e->die_event unless $e->checkauth;
1068 for my $bp (@{$batch_perm{$class}}) {
1069 return { stage => 'COMPLETE' } unless $e->allowed($bp);
1072 $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
1073 return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
1075 my $meth = 'retrieve_' . $ctypes{$class};
1076 my $bkt = $e->$meth($c_id) or return $e->die_event;
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)
1087 $meth = 'search_' . $ctypes{$class} . '_item';
1088 my $contents = $e->$meth({bucket => $c_id});
1091 $max = scalar(@$contents) if ($self->{perms});
1092 $max += scalar(@$contents) if ($self->{base_perm});
1095 if ($self->{base_perm}) {
1096 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1097 for my $item (@$contents) {
1099 $meth = 'retrieve_' . $itypes{$class};
1100 my $field = 'target_'.$ttypes{$class};
1101 my $obj = $$obj_cache{$item->$field} = $e->$meth($item->$field);
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;
1116 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1120 if ($self->{perms}) {
1121 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1122 for my $item (@$contents) {
1124 $meth = 'retrieve_' . $itypes{$class};
1125 my $field = 'target_'.$ttypes{$class};
1126 my $obj = $$obj_cache{$item->$field} || $e->$meth($item->$field);
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 '*') {
1137 $pwhere = $obj->$pwhere;
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;
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;
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;
1168 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1173 $client->respond({ ord => $stage++, stage => 'FIELDSET_GROUP_CREATE' });
1174 my $fsgroup = Fieldmapper::action::fieldset_group->new;
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);
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);
1195 my @keys = keys %$edits;
1196 $max = int(scalar(@keys));
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}});
1203 my $fs_cv = Fieldmapper::action::fieldset_col_val->new;
1205 $fs_cv->fieldset($fieldset->id);
1207 $fs_cv->val($$edits{$key});
1208 $e->create_action_fieldset_col_val($fs_cv);
1210 $client->respond({ ord => $stage, count=> $count, max => $max, stage => 'FIELDSET_EDITS_CREATE' });
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
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'};
1228 $qstore->disconnect;
1230 return { fieldset_group => $fsgroup->id, stage => 'COMPLETE', error => $res };
1233 __PACKAGE__->register_method(
1234 method => "batch_edit",
1235 max_bundle_count => 1,
1236 api_name => "open-ils.actor.container.user.batch_edit",
1238 base_perm => { home_ou => 'UPDATE_USER' },
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);
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
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
1254 fields => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
1256 desc => 'Edits allowed fields on users in a bucket',
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' },
1264 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
1270 __PACKAGE__->register_method(
1271 method => "batch_edit",
1272 api_name => "open-ils.actor.container.user.batch_delete",
1276 t => { 'DELETE_USER UPDATE_USER' => 'home_ou' },
1277 f => { 'UPDATE_USER' => 'home_ou' }
1280 fields => [ qw/deleted/ ],
1282 desc => 'Deletes users in a bucket',
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' },
1291 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',