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 $e->allowed('VIEW_CONTAINER', $owner->home_ou);
149 my $fmclass = $bkt->class_name . "i";
150 $meth = 'search_' . $ctypes{$class} . '_item';
153 {bucket => $bucket_id},
154 { order_by => {$fmclass => "pos"},
156 flesh_fields => {$fmclass => ['notes']}
165 __PACKAGE__->register_method(
166 method => "item_note_cud",
167 api_name => "open-ils.actor.container.item_note.cud",
172 my($self, $conn, $auth, $class, $note) = @_;
174 return new OpenILS::Event("BAD_PARAMS") unless
175 $note->class_name =~ /bucket_item_note$/;
177 my $e = new_editor(authtoken => $auth, xact => 1);
178 return $e->die_event unless $e->checkauth;
180 my $meat = $ctypes{$class} . "_item_note";
181 my $meth = "retrieve_$meat";
183 my $item_meat = $ctypes{$class} . "_item";
184 my $item_meth = "retrieve_$item_meat";
186 my $nhint = $Fieldmapper::fieldmap->{$note->class_name}->{hint};
187 (my $ihint = $nhint) =~ s/n$//og;
189 my ($db_note, $item);
194 $item = $e->$item_meth([
196 flesh => 1, flesh_fields => {$ihint => ["bucket"]}
198 ]) or return $e->die_event;
200 $db_note = $e->$meth([
208 ]) or return $e->die_event;
210 $item = $db_note->item;
213 if($item->bucket->owner ne $e->requestor->id) {
214 return $e->die_event unless $e->allowed("UPDATE_CONTAINER");
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);
225 __PACKAGE__->register_method(
226 method => "bucket_retrieve_class",
227 api_name => "open-ils.actor.container.retrieve_by_class",
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
236 If bucketOwnerId is not defined, the authtoken is used as the
238 If requestor ID is different than bucketOwnerId, requestor must have
239 VIEW_CONTAINER permissions.
242 sub bucket_retrieve_class {
243 my( $self, $client, $authtoken, $userid, $class, $type ) = @_;
245 my( $staff, $user, $evt ) =
246 $apputils->checkses_requestor( $authtoken, $userid, 'VIEW_CONTAINER' );
249 $logger->debug("User " . $staff->id .
250 " retrieving buckets for user $userid [class=$class, type=$type]");
252 my $meth = $types{$class} . ".search.atomic";
256 $buckets = $apputils->simplereq( $svc,
257 $meth, { owner => $userid, btype => $type } );
259 $logger->debug("Grabbing buckets by class $class: $svc : $meth : {owner => $userid}");
260 $buckets = $apputils->simplereq( $svc, $meth, { owner => $userid } );
263 return _sort_buckets($buckets);
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
277 my( $self, $client, $authtoken, $class, $bucket ) = @_;
279 my $e = new_editor(xact=>1, authtoken=>$authtoken);
280 return $e->event unless $e->checkauth;
282 if( $bucket->owner ne $e->requestor->id ) {
283 return $e->event unless
284 $e->allowed('CREATE_CONTAINER');
287 return $e->event unless
288 $e->allowed('CREATE_MY_CONTAINER');
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};
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);
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);
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);
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);
327 __PACKAGE__->register_method(
328 method => "item_create",
329 api_name => "open-ils.actor.container.item.create",
332 Adds one or more items to an existing container
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'},
340 desc => 'The ID of the newly created item(s). In batch context, an array of IDs is returned'
347 my( $self, $client, $authtoken, $class, $item ) = @_;
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];
353 my ( $bucket, $evt ) =
354 $apputils->fetch_container_e($e, $items->[0]->bucket, $class);
357 if( $bucket->owner ne $e->requestor->id ) {
358 return $e->die_event unless
359 $e->allowed('CREATE_CONTAINER_ITEM');
362 # return $e->event unless
363 # $e->allowed('CREATE_CONTAINER_ITEM'); # new perm here?
366 for my $one_item (@$items) {
371 if( $class eq 'copy' ) {
372 return $e->die_event unless
373 $stat = $e->create_container_copy_bucket_item($one_item);
376 if( $class eq 'callnumber' ) {
377 return $e->die_event unless
378 $stat = $e->create_container_call_number_bucket_item($one_item);
381 if( $class eq 'biblio' ) {
382 return $e->die_event unless
383 $stat = $e->create_container_biblio_record_entry_bucket_item($one_item);
386 if( $class eq 'user') {
387 return $e->die_event unless
388 $stat = $e->create_container_user_bucket_item($one_item);
394 # CStoreEeditor inserts the id (pkey) on newly created objects
395 return [ map { $_->id } @$items ] if ref $item eq 'ARRAY';
399 __PACKAGE__->register_method(
400 method => 'batch_add_items',
401 api_name => 'open-ils.actor.container.item.create.batch',
403 max_bundle_count => 1,
405 desc => 'Add items to a bucket',
407 {desc => 'Auth token', type => 'string'},
410 Can be "copy", "call_number", "biblio_record_entry", or "user"'/,
412 {desc => 'Bucket ID', type => 'number'},
414 Item target identifiers. E.g. for record buckets,
415 the identifier would be the bib record id/,
420 desc => 'Stream of new item Identifiers',
426 sub batch_add_items {
427 my ($self, $client, $auth, $bucket_class, $bucket_id, $target_ids) = @_;
429 my $e = new_editor(authtoken => $auth, xact => 1);
430 return $e->die_event unless $e->checkauth;
432 my $constructor = "Fieldmapper::container::${bucket_class}_bucket_item";
433 my $create = "create_container_${bucket_class}_bucket_item";
434 my $retrieve = "retrieve_container_${bucket_class}_bucket";
435 my $column = "target_${bucket_class}";
437 my $bucket = $e->$retrieve($bucket_id) or return $e->die_event;
439 if ($bucket->owner ne $e->requestor->id) {
440 return $e->die_event unless $e->allowed('CREATE_CONTAINER_ITEM');
443 for my $target_id (@$target_ids) {
445 my $item = $constructor->new;
446 $item->bucket($bucket_id);
447 $item->$column($target_id);
449 return $e->die_event unless $e->$create($item);
450 $client->respond($target_id);
457 __PACKAGE__->register_method(
458 method => 'batch_delete_items',
459 api_name => 'open-ils.actor.container.item.delete.batch',
461 max_bundle_count => 1,
463 desc => 'Remove items from a bucket',
465 {desc => 'Auth token', type => 'string'},
468 Can be "copy", "call_number", "biblio_record_entry", or "user"'/,
471 Item target identifiers. E.g. for record buckets,
472 the identifier would be the bib record id/,
477 desc => 'Stream of new removed target IDs',
483 sub batch_delete_items {
484 my ($self, $client, $auth, $bucket_class, $bucket_id, $target_ids) = @_;
486 my $e = new_editor(authtoken => $auth, xact => 1);
487 return $e->die_event unless $e->checkauth;
489 my $delete = "delete_container_${bucket_class}_bucket_item";
490 my $search = "search_container_${bucket_class}_bucket_item";
491 my $retrieve = "retrieve_container_${bucket_class}_bucket";
492 my $column = "target_${bucket_class}";
494 my $bucket = $e->$retrieve($bucket_id) or return $e->die_event;
496 if ($bucket->owner ne $e->requestor->id) {
497 return $e->die_event unless $e->allowed('DELETE_CONTAINER_ITEM');
500 for my $target_id (@$target_ids) {
502 my $item = $e->$search({bucket => $bucket_id, $column => $target_id})->[0];
505 return $e->die_event unless $e->$delete($item);
506 $client->respond($target_id);
516 __PACKAGE__->register_method(
517 method => "item_delete",
518 api_name => "open-ils.actor.container.item.delete",
519 notes => <<" NOTES");
520 PARAMS(authtoken, class, itemId)
524 my( $self, $client, $authtoken, $class, $itemid ) = @_;
526 my $e = new_editor(xact=>1, authtoken=>$authtoken);
527 return $e->event unless $e->checkauth;
529 my $ret = __item_delete($e, $class, $itemid);
530 $e->commit unless $U->event_code($ret);
535 my( $e, $class, $itemid ) = @_;
536 my( $bucket, $item, $evt);
538 ( $item, $evt ) = $U->fetch_container_item_e( $e, $itemid, $class );
541 ( $bucket, $evt ) = $U->fetch_container_e($e, $item->bucket, $class);
544 if( $bucket->owner ne $e->requestor->id ) {
545 my $owner = $e->retrieve_actor_user($bucket->owner)
546 or return $e->die_event;
547 return $e->event unless $e->allowed('DELETE_CONTAINER_ITEM', $owner->home_ou);
551 if( $class eq 'copy' ) {
552 for my $note (@{$e->search_container_copy_bucket_item_note({item => $item->id})}) {
553 return $e->event unless
554 $e->delete_container_copy_bucket_item_note($note);
556 return $e->event unless
557 $stat = $e->delete_container_copy_bucket_item($item);
560 if( $class eq 'callnumber' ) {
561 for my $note (@{$e->search_container_call_number_bucket_item_note({item => $item->id})}) {
562 return $e->event unless
563 $e->delete_container_call_number_bucket_item_note($note);
565 return $e->event unless
566 $stat = $e->delete_container_call_number_bucket_item($item);
569 if( $class eq 'biblio' ) {
570 for my $note (@{$e->search_container_biblio_record_entry_bucket_item_note({item => $item->id})}) {
571 return $e->event unless
572 $e->delete_container_biblio_record_entry_bucket_item_note($note);
574 return $e->event unless
575 $stat = $e->delete_container_biblio_record_entry_bucket_item($item);
578 if( $class eq 'user') {
579 for my $note (@{$e->search_container_user_bucket_item_note({item => $item->id})}) {
580 return $e->event unless
581 $e->delete_container_user_bucket_item_note($note);
583 return $e->event unless
584 $stat = $e->delete_container_user_bucket_item($item);
591 __PACKAGE__->register_method(
592 method => 'full_delete',
593 api_name => 'open-ils.actor.container.full_delete',
594 notes => "Complety removes a container including all attached items",
598 my( $self, $client, $authtoken, $class, $containerId ) = @_;
599 my( $container, $evt);
601 my $e = new_editor(xact=>1, authtoken=>$authtoken);
602 return $e->event unless $e->checkauth;
604 ( $container, $evt ) = $apputils->fetch_container_e($e, $containerId, $class);
607 if( $container->owner ne $e->requestor->id ) {
608 my $owner = $e->retrieve_actor_user($container->owner)
609 or return $e->die_event;
610 return $e->event unless $e->allowed('DELETE_CONTAINER', $owner->home_ou);
615 my @s = ({bucket => $containerId}, {idlist=>1});
617 if( $class eq 'copy' ) {
618 $items = $e->search_container_copy_bucket_item(@s);
621 if( $class eq 'callnumber' ) {
622 $items = $e->search_container_call_number_bucket_item(@s);
625 if( $class eq 'biblio' ) {
626 $items = $e->search_container_biblio_record_entry_bucket_item(@s);
629 if( $class eq 'user') {
630 $items = $e->search_container_user_bucket_item(@s);
633 __item_delete($e, $class, $_) for @$items;
636 if( $class eq 'copy' ) {
637 return $e->event unless
638 $stat = $e->delete_container_copy_bucket($container);
641 if( $class eq 'callnumber' ) {
642 return $e->event unless
643 $stat = $e->delete_container_call_number_bucket($container);
646 if( $class eq 'biblio' ) {
647 return $e->event unless
648 $stat = $e->delete_container_biblio_record_entry_bucket($container);
651 if( $class eq 'user') {
652 return $e->event unless
653 $stat = $e->delete_container_user_bucket($container);
660 __PACKAGE__->register_method(
661 method => 'container_update',
662 api_name => 'open-ils.actor.container.update',
664 Updates the given container item.
665 @param authtoken The login session key
666 @param class The container class
667 @param container The container item
668 @return true on success, 0 on no update, Event on error
672 sub container_update {
673 my( $self, $conn, $authtoken, $class, $container ) = @_;
675 my $e = new_editor(xact=>1, authtoken=>$authtoken);
676 return $e->event unless $e->checkauth;
678 my ( $dbcontainer, $evt ) = $U->fetch_container_e($e, $container->id, $class);
681 if( $e->requestor->id ne $container->owner ) {
682 return $e->event unless $e->allowed('UPDATE_CONTAINER');
686 if( $class eq 'copy' ) {
687 return $e->event unless
688 $stat = $e->update_container_copy_bucket($container);
691 if( $class eq 'callnumber' ) {
692 return $e->event unless
693 $stat = $e->update_container_call_number_bucket($container);
696 if( $class eq 'biblio' ) {
697 return $e->event unless
698 $stat = $e->update_container_biblio_record_entry_bucket($container);
701 if( $class eq 'user') {
702 return $e->event unless
703 $stat = $e->update_container_user_bucket($container);
712 __PACKAGE__->register_method(
713 method => "anon_cache",
714 api_name => "open-ils.actor.anon_cache.set_value",
717 Sets a value in the anon web cache. If the session key is
718 undefined, one will be automatically generated.
721 {desc => 'Session key', type => 'string'},
723 desc => q/Field name. The name of the field in this cache session whose value to set/,
727 desc => q/The cached value. This can be any type of object (hash, array, string, etc.)/,
732 desc => 'session key on success, undef on error',
738 __PACKAGE__->register_method(
739 method => "anon_cache",
740 api_name => "open-ils.actor.anon_cache.get_value",
743 Returns the cached data at the specified field within the specified cache session.
746 {desc => 'Session key', type => 'string'},
748 desc => q/Field name. The name of the field in this cache session whose value to set/,
753 desc => 'cached value on success, undef on error',
759 __PACKAGE__->register_method(
760 method => "anon_cache",
761 api_name => "open-ils.actor.anon_cache.delete_session",
764 Deletes a cache session.
767 {desc => 'Session key', type => 'string'},
770 desc => 'Session key',
777 my($self, $conn, $ses_key, $field_key, $value) = @_;
779 my $sc = OpenSRF::Utils::SettingsClient->new;
780 my $cache = OpenSRF::Utils::Cache->new('anon');
781 my $cache_timeout = $sc->config_value(cache => anon => 'max_cache_time') || 1800; # 30 minutes
782 my $cache_size = $sc->config_value(cache => anon => 'max_cache_size') || 102400; # 100k
784 if($self->api_name =~ /delete_session/) {
786 return $cache->delete_cache($ses_key);
788 } elsif( $self->api_name =~ /set_value/ ) {
790 $ses_key = md5_hex(time . rand($$)) unless $ses_key;
791 my $blob = $cache->get_cache($ses_key) || {};
792 $blob->{$field_key} = $value;
794 length(OpenSRF::Utils::JSON->perl2JSON($blob)) > $cache_size; # bytes, characters, whatever ;)
795 $cache->put_cache($ses_key, $blob, $cache_timeout);
800 my $blob = $cache->get_cache($ses_key) or return undef;
801 return $blob if (!defined($field_key));
802 return $blob->{$field_key};
806 sub batch_statcat_apply {
813 # $changes is a hashref that looks like:
815 # remove => [ qw/ stat cat ids to remove / ],
816 # apply => { $statcat_id => $value_string, ... }
824 my $e = new_editor(xact=>1, authtoken=>$ses);
825 return $e->die_event unless $e->checkauth;
826 $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
827 return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
829 my $meth = 'retrieve_' . $ctypes{$class};
830 my $bkt = $e->$meth($c_id) or return $e->die_event;
832 unless($bkt->owner eq $e->requestor->id) {
833 $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
834 my $owner = $e->retrieve_actor_user($bkt->owner)
835 or return $e->die_event;
836 return $e->die_event unless (
837 $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
841 $meth = 'search_' . $ctypes{$class} . '_item';
842 my $contents = $e->$meth({bucket => $c_id});
844 if ($self->{perms}) {
845 $max = scalar(@$contents);
846 $client->respond({ ord => $stage, max => $max, count => 0, stage => 'ITEM_PERM_CHECK' });
847 for my $item (@$contents) {
849 $meth = 'retrieve_' . $itypes{$class};
850 my $field = 'target_'.$ttypes{$class};
851 my $obj = $e->$meth($item->$field);
853 for my $perm_field (keys %{$self->{perms}}) {
854 my $perm_def = $self->{perms}->{$perm_field};
855 my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
856 for my $p (@$pwhat) {
857 $e->allowed($p, $obj->$pwhere) or return $e->die_event;
860 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
865 my @users = map { $_->target_user } @$contents;
866 $max = scalar(@users) * scalar(@{$changes->{remove}});
868 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' });
870 my $chunk = int($max / 10) || 1;
871 my $to_remove = $e->search_actor_stat_cat_entry_user_map({ target_usr => \@users, stat_cat => $changes->{remove} });
872 for my $t (@$to_remove) {
873 $e->delete_actor_stat_cat_entry_user_map($t);
875 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' })
876 unless ($count % $chunk);
881 $max = scalar(@users) * scalar(keys %{$changes->{apply}});
883 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' });
885 $chunk = int($max / 10) || 1;
886 for my $item (@$contents) {
887 for my $astatcat (keys %{$changes->{apply}}) {
888 my $new_value = $changes->{apply}->{$astatcat};
889 my $to_change = $e->search_actor_stat_cat_entry_user_map({ target_usr => $item->target_user, stat_cat => $astatcat });
891 $to_change = $$to_change[0];
892 $to_change->stat_cat_entry($new_value);
893 $e->update_actor_stat_cat_entry_user_map($to_change);
895 $to_change = new Fieldmapper::actor::stat_cat_entry_user_map;
896 $to_change->stat_cat_entry($new_value);
897 $to_change->stat_cat($astatcat);
898 $to_change->target_usr($item->target_user);
899 $e->create_actor_stat_cat_entry_user_map($to_change);
902 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' })
903 unless ($count % $chunk);
909 return { stage => 'COMPLETE' };
912 __PACKAGE__->register_method(
913 method => "batch_statcat_apply",
914 api_name => "open-ils.actor.container.user.batch_statcat_apply",
917 home_ou => 'UPDATE_USER', # field -> perm means "test this perm with field as context OU", both old and new
919 fields => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
921 desc => 'Edits allowed fields on users in a bucket',
923 desc => 'Session key', type => 'string',
924 desc => 'User container id',
925 desc => 'Hash of statcats to apply or remove', type => 'hash',
928 desc => 'Object with the structure { stage => "stage string", max => max_for_stage, count => count_in_stage }',
940 my $main_fsg = shift;
946 my $class = $self->{ctype} or return undef;
948 my $e = new_editor(xact=>1, authtoken=>$ses);
949 return $e->die_event unless $e->checkauth;
951 for my $bp (@{$batch_perm{$class}}) {
952 return { stage => 'COMPLETE' } unless $e->allowed($bp);
955 $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
956 return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
958 my $meth = 'retrieve_' . $ctypes{$class};
959 my $bkt = $e->$meth($c_id) or return $e->die_event;
961 unless($bkt->owner eq $e->requestor->id) {
962 $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
963 my $owner = $e->retrieve_actor_user($bkt->owner)
964 or return $e->die_event;
965 return $e->die_event unless (
966 $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
970 $main_fsg = $e->retrieve_action_fieldset_group($main_fsg);
971 return { stage => 'COMPLETE', error => 'No field set group' } unless $main_fsg;
973 my $rbg = $e->retrieve_action_fieldset_group($main_fsg->rollback_group);
974 return { stage => 'COMPLETE', error => 'No rollback field set group' } unless $rbg;
976 my $fieldsets = $e->search_action_fieldset({fieldset_group => $rbg->id});
977 $max = scalar(@$fieldsets);
979 $client->respond({ ord => $stage, max => $max, count => 0, stage => 'APPLY_EDITS' });
980 for my $fs (@$fieldsets) {
981 my $res = $e->json_query({
982 from => ['action.apply_fieldset', $fs->id, $table{$class}, 'id', undef]
983 })->[0]->{'action.apply_fieldset'};
989 stage => 'APPLY_EDITS',
990 error => $res ? "Could not apply fieldset ".$fs->id.": $res" : undef
994 $main_fsg->rollback_time('now');
995 $e->update_action_fieldset_group($main_fsg);
999 return { stage => 'COMPLETE' };
1001 __PACKAGE__->register_method(
1002 method => "apply_rollback",
1003 max_bundle_count => 1,
1004 api_name => "open-ils.actor.container.user.apply_rollback",
1007 desc => 'Applys rollback of a fieldset group to users in a bucket',
1009 { desc => 'Session key', type => 'string' },
1010 { desc => 'User container id', type => 'number' },
1011 { desc => 'Main (non-rollback) fieldset group' },
1014 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
1026 my $edit_name = shift;
1033 my $class = $self->{ctype} or return undef;
1035 my $e = new_editor(xact=>1, authtoken=>$ses);
1036 return $e->die_event unless $e->checkauth;
1038 for my $bp (@{$batch_perm{$class}}) {
1039 return { stage => 'COMPLETE' } unless $e->allowed($bp);
1042 $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
1043 return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
1045 my $meth = 'retrieve_' . $ctypes{$class};
1046 my $bkt = $e->$meth($c_id) or return $e->die_event;
1048 unless($bkt->owner eq $e->requestor->id) {
1049 $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
1050 my $owner = $e->retrieve_actor_user($bkt->owner)
1051 or return $e->die_event;
1052 return $e->die_event unless (
1053 $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
1057 $meth = 'search_' . $ctypes{$class} . '_item';
1058 my $contents = $e->$meth({bucket => $c_id});
1061 $max = scalar(@$contents) if ($self->{perms});
1062 $max += scalar(@$contents) if ($self->{base_perm});
1065 if ($self->{base_perm}) {
1066 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1067 for my $item (@$contents) {
1069 $meth = 'retrieve_' . $itypes{$class};
1070 my $field = 'target_'.$ttypes{$class};
1071 my $obj = $$obj_cache{$item->$field} = $e->$meth($item->$field);
1073 for my $perm_field (keys %{$self->{base_perm}}) {
1074 my $perm_def = $self->{base_perm}->{$perm_field};
1075 my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
1076 for my $p (@$pwhat) {
1077 $e->allowed($p, $obj->$pwhere) or return $e->die_event;
1078 if ($$edits{$pwhere}) {
1079 $e->allowed($p, $$edits{$pwhere}) or do {
1080 $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
1081 return $e->die_event;
1086 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1090 if ($self->{perms}) {
1091 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1092 for my $item (@$contents) {
1094 $meth = 'retrieve_' . $itypes{$class};
1095 my $field = 'target_'.$ttypes{$class};
1096 my $obj = $$obj_cache{$item->$field} || $e->$meth($item->$field);
1098 for my $perm_field (keys %{$self->{perms}}) {
1099 my $perm_def = $self->{perms}->{$perm_field};
1100 if (ref($perm_def) eq 'HASH') { # we care about specific values being set
1101 for my $perm_value (keys %$perm_def) {
1102 if (exists $$edits{$perm_field} && $$edits{$perm_field} eq $perm_value) { # check permission
1103 while (my ($pwhat,$pwhere) = each %{$$perm_def{$perm_value}}) {
1104 if ($pwhere eq '*') {
1107 $pwhere = $obj->$pwhere;
1109 $pwhat = [ split / /, $pwhat ];
1110 for my $p (@$pwhat) {
1111 $e->allowed($p, $pwhere) or do {
1112 $pwhere ||= "everywhere";
1113 $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
1114 return $e->die_event;
1120 } elsif (ref($perm_def) eq 'CODE') { # we need to run the code on old and new, and pass both tests
1121 if (exists $$edits{$perm_field}) {
1122 $perm_def->($e, $obj->$perm_field) or return $e->die_event;
1123 $perm_def->($e, $$edits{$perm_field}) or return $e->die_event;
1125 } else { # we're checking an ou field
1126 my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
1127 if ($$edits{$pwhere}) {
1128 for my $p (@$pwhat) {
1129 $e->allowed($p, $obj->$pwhere) or return $e->die_event;
1130 $e->allowed($p, $$edits{$pwhere}) or do {
1131 $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
1132 return $e->die_event;
1138 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1143 $client->respond({ ord => $stage++, stage => 'FIELDSET_GROUP_CREATE' });
1144 my $fsgroup = Fieldmapper::action::fieldset_group->new;
1146 $fsgroup->name($edit_name);
1147 $fsgroup->creator($e->requestor->id);
1148 $fsgroup->owning_lib($e->requestor->ws_ou);
1149 $fsgroup->container($c_id);
1150 $fsgroup->container_type($ttypes{$class});
1151 $fsgroup = $e->create_action_fieldset_group($fsgroup);
1153 $client->respond({ ord => $stage++, stage => 'FIELDSET_CREATE' });
1154 my $fieldset = Fieldmapper::action::fieldset->new;
1155 $fieldset->isnew(1);
1156 $fieldset->fieldset_group($fsgroup->id);
1157 $fieldset->owner($e->requestor->id);
1158 $fieldset->owning_lib($e->requestor->ws_ou);
1159 $fieldset->status('PENDING');
1160 $fieldset->classname($htypes{$class});
1161 $fieldset->name($edit_name . ' batch group fieldset');
1162 $fieldset->stored_query($qtypes{$class});
1163 $fieldset = $e->create_action_fieldset($fieldset);
1165 my @keys = keys %$edits;
1166 $max = scalar(@keys);
1168 $client->respond({ ord => $stage, count=> $count, max => $max, stage => 'FIELDSET_EDITS_CREATE' });
1169 for my $key (@keys) {
1170 if ($self->{fields}) { # restrict edits to registered fields
1171 next unless (grep { $_ eq $key } @{$self->{fields}});
1173 my $fs_cv = Fieldmapper::action::fieldset_col_val->new;
1175 $fs_cv->fieldset($fieldset->id);
1177 $fs_cv->val($$edits{$key});
1178 $e->create_action_fieldset_col_val($fs_cv);
1180 $client->respond({ ord => $stage, count=> $count, max => $max, stage => 'FIELDSET_EDITS_CREATE' });
1183 $client->respond({ ord => ++$stage, stage => 'CONSTRUCT_QUERY' });
1184 my $qstore = OpenSRF::AppSession->connect('open-ils.qstore');
1185 my $prep = $qstore->request('open-ils.qstore.prepare', $fieldset->stored_query)->gather(1);
1186 my $token = $prep->{token};
1187 $qstore->request('open-ils.qstore.bind_param', $token, {bucket => $c_id})->gather(1);
1188 my $sql = $qstore->request('open-ils.qstore.sql', $token)->gather(1);
1189 $sql =~ s/\n\s*/ /g; # normalize the string
1190 $sql =~ s/;\s*//g; # kill trailing semicolon
1192 $client->respond({ ord => ++$stage, stage => 'APPLY_EDITS' });
1193 my $res = $e->json_query({
1194 from => ['action.apply_fieldset', $fieldset->id, $table{$class}, 'id', $sql]
1195 })->[0]->{'action.apply_fieldset'};
1198 $qstore->disconnect;
1200 return { fieldset_group => $fsgroup->id, stage => 'COMPLETE', error => $res };
1203 __PACKAGE__->register_method(
1204 method => "batch_edit",
1205 max_bundle_count => 1,
1206 api_name => "open-ils.actor.container.user.batch_edit",
1208 base_perm => { home_ou => 'UPDATE_USER' },
1211 my ($e, $group) = @_;
1212 my $g = $e->retrieve_permission_grp_tree($group);
1213 if (my $p = $g->application_perm()) {
1214 return $e->allowed($p);
1217 }, # code ref is run with params (editor,value), for both old and new value
1218 # home_ou => 'UPDATE_USER', # field -> perm means "test this perm with field as context OU", both old and new
1220 t => { BAR_PATRON => 'home_ou' },
1221 f => { UNBAR_PATRON => 'home_ou' }
1222 } # field -> struct means "if field getting value "key" check -> perm -> at context org, both old and new
1224 fields => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
1226 desc => 'Edits allowed fields on users in a bucket',
1228 { desc => 'Session key', type => 'string' },
1229 { desc => 'User container id', type => 'number' },
1230 { desc => 'Batch edit name', type => 'string' },
1231 { desc => 'Edit hash, key is column, value is new value to apply', type => 'hash' },
1234 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
1240 __PACKAGE__->register_method(
1241 method => "batch_edit",
1242 api_name => "open-ils.actor.container.user.batch_delete",
1246 t => { 'DELETE_USER UPDATE_USER' => 'home_ou' },
1247 f => { 'UPDATE_USER' => 'home_ou' }
1250 fields => [ qw/deleted/ ],
1252 desc => 'Deletes users in a bucket',
1254 { desc => 'Session key', type => 'string' },
1255 { desc => 'User container id', type => 'number' },
1256 { desc => 'Batch delete name', type => 'string' },
1257 { desc => 'Edit delete, key is "deleted", value is new value to apply ("t")', type => 'hash' },
1261 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',