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 $logger->debug("User " . $staff->id .
253 " retrieving buckets for user $userid [class=$class, type=$type]");
255 my $meth = $types{$class} . ".search.atomic";
259 $buckets = $apputils->simplereq( $svc,
260 $meth, { owner => $userid, btype => $type } );
262 $logger->debug("Grabbing buckets by class $class: $svc : $meth : {owner => $userid}");
263 $buckets = $apputils->simplereq( $svc, $meth, { owner => $userid } );
266 return _sort_buckets($buckets);
269 __PACKAGE__->register_method(
270 method => "bucket_create",
271 api_name => "open-ils.actor.container.create",
272 notes => <<" NOTES");
273 Creates a new bucket object. If requestor is different from
274 bucketOwner, requestor needs CREATE_CONTAINER permissions
275 PARAMS(authtoken, bucketObject);
276 Returns the new bucket object
280 my( $self, $client, $authtoken, $class, $bucket ) = @_;
282 my $e = new_editor(xact=>1, authtoken=>$authtoken);
283 return $e->event unless $e->checkauth;
285 if( $bucket->owner ne $e->requestor->id ) {
286 return $e->event unless
287 $e->allowed('CREATE_CONTAINER');
290 return $e->event unless
291 $e->allowed('CREATE_MY_CONTAINER');
296 my $evt = OpenILS::Event->new('CONTAINER_EXISTS',
297 payload => [$class, $bucket->owner, $bucket->btype, $bucket->name]);
298 my $search = {name => $bucket->name, owner => $bucket->owner, btype => $bucket->btype};
301 if( $class eq 'copy' ) {
302 return $evt if $e->search_container_copy_bucket($search)->[0];
303 return $e->event unless
304 $obj = $e->create_container_copy_bucket($bucket);
307 if( $class eq 'callnumber' ) {
308 return $evt if $e->search_container_call_number_bucket($search)->[0];
309 return $e->event unless
310 $obj = $e->create_container_call_number_bucket($bucket);
313 if( $class eq 'biblio' ) {
314 return $evt if $e->search_container_biblio_record_entry_bucket($search)->[0];
315 return $e->event unless
316 $obj = $e->create_container_biblio_record_entry_bucket($bucket);
319 if( $class eq 'user') {
320 return $evt if $e->search_container_user_bucket($search)->[0];
321 return $e->event unless
322 $obj = $e->create_container_user_bucket($bucket);
330 __PACKAGE__->register_method(
331 method => "item_create",
332 api_name => "open-ils.actor.container.item.create",
335 Adds one or more items to an existing container
338 {desc => 'Authentication token', type => 'string'},
339 {desc => 'Container class. Can be "copy", "callnumber", "biblio", or "user"', type => 'string'},
340 {desc => 'Item or items. Can either be a single container item object, or an array of them', type => 'object'},
341 {desc => 'Duplicate check. Avoid adding an item that is already in a container', type => 'bool'},
344 desc => 'The ID of the newly created item(s). In batch context, an array of IDs is returned'
351 my( $self, $client, $authtoken, $class, $item, $dupe_check ) = @_;
353 my $e = new_editor(xact=>1, authtoken=>$authtoken);
354 return $e->die_event unless $e->checkauth;
355 my $items = (ref $item eq 'ARRAY') ? $item : [$item];
357 my ( $bucket, $evt ) =
358 $apputils->fetch_container_e($e, $items->[0]->bucket, $class);
361 if( $bucket->owner ne $e->requestor->id ) {
362 return $e->die_event unless
363 $e->allowed('CREATE_CONTAINER_ITEM');
366 # return $e->event unless
367 # $e->allowed('CREATE_CONTAINER_ITEM'); # new perm here?
370 for my $one_item (@$items) {
375 if( $class eq 'copy' ) {
378 $e->search_container_copy_bucket_item(
379 {bucket => $one_item->bucket, target_copy => $one_item->target_copy}
382 return $e->die_event unless
383 $stat = $e->create_container_copy_bucket_item($one_item);
386 if( $class eq 'callnumber' ) {
389 $e->search_container_call_number_bucket_item(
390 {bucket => $one_item->bucket, target_call_number => $one_item->target_call_number}
393 return $e->die_event unless
394 $stat = $e->create_container_call_number_bucket_item($one_item);
397 if( $class eq 'biblio' ) {
400 $e->search_container_biblio_record_entry_bucket_item(
401 {bucket => $one_item->bucket, target_biblio_record_entry => $one_item->target_biblio_record_entry}
404 return $e->die_event unless
405 $stat = $e->create_container_biblio_record_entry_bucket_item($one_item);
408 if( $class eq 'user') {
411 $e->search_container_user_bucket_item(
412 {bucket => $one_item->bucket, target_user => $one_item->target_user}
415 return $e->die_event unless
416 $stat = $e->create_container_user_bucket_item($one_item);
422 # CStoreEeditor inserts the id (pkey) on newly created objects
423 return [ map { $_->id } @$items ] if ref $item eq 'ARRAY';
427 __PACKAGE__->register_method(
428 method => 'batch_add_items',
429 api_name => 'open-ils.actor.container.item.create.batch',
431 max_bundle_count => 1,
433 desc => 'Add items to a bucket',
435 {desc => 'Auth token', type => 'string'},
438 Can be "copy", "call_number", "biblio_record_entry", or "user"'/,
440 {desc => 'Bucket ID', type => 'number'},
442 Item target identifiers. E.g. for record buckets,
443 the identifier would be the bib record id/,
448 desc => 'Stream of new item Identifiers',
454 sub batch_add_items {
455 my ($self, $client, $auth, $bucket_class, $bucket_id, $target_ids) = @_;
457 my $e = new_editor(authtoken => $auth, xact => 1);
458 return $e->die_event unless $e->checkauth;
460 my $constructor = "Fieldmapper::container::${bucket_class}_bucket_item";
461 my $create = "create_container_${bucket_class}_bucket_item";
462 my $retrieve = "retrieve_container_${bucket_class}_bucket";
463 my $column = "target_${bucket_class}";
465 my $bucket = $e->$retrieve($bucket_id) or return $e->die_event;
467 if ($bucket->owner ne $e->requestor->id) {
468 return $e->die_event unless $e->allowed('CREATE_CONTAINER_ITEM');
471 for my $target_id (@$target_ids) {
473 my $item = $constructor->new;
474 $item->bucket($bucket_id);
475 $item->$column($target_id);
477 return $e->die_event unless $e->$create($item);
478 $client->respond($target_id);
485 __PACKAGE__->register_method(
486 method => 'batch_delete_items',
487 api_name => 'open-ils.actor.container.item.delete.batch',
489 max_bundle_count => 1,
491 desc => 'Remove items from a bucket',
493 {desc => 'Auth token', type => 'string'},
496 Can be "copy", "call_number", "biblio_record_entry", or "user"'/,
499 Item target identifiers. E.g. for record buckets,
500 the identifier would be the bib record id/,
505 desc => 'Stream of new removed target IDs',
511 sub batch_delete_items {
512 my ($self, $client, $auth, $bucket_class, $bucket_id, $target_ids) = @_;
514 my $e = new_editor(authtoken => $auth, xact => 1);
515 return $e->die_event unless $e->checkauth;
517 my $delete = "delete_container_${bucket_class}_bucket_item";
518 my $search = "search_container_${bucket_class}_bucket_item";
519 my $retrieve = "retrieve_container_${bucket_class}_bucket";
520 my $column = "target_${bucket_class}";
522 my $bucket = $e->$retrieve($bucket_id) or return $e->die_event;
524 if ($bucket->owner ne $e->requestor->id) {
525 return $e->die_event unless $e->allowed('DELETE_CONTAINER_ITEM');
528 for my $target_id (@$target_ids) {
530 my $item = $e->$search({bucket => $bucket_id, $column => $target_id})->[0];
533 return $e->die_event unless $e->$delete($item);
534 $client->respond($target_id);
544 __PACKAGE__->register_method(
545 method => "item_delete",
546 api_name => "open-ils.actor.container.item.delete",
547 notes => <<" NOTES");
548 PARAMS(authtoken, class, itemId)
552 my( $self, $client, $authtoken, $class, $itemid ) = @_;
554 my $e = new_editor(xact=>1, authtoken=>$authtoken);
555 return $e->event unless $e->checkauth;
557 my $ret = __item_delete($e, $class, $itemid);
558 $e->commit unless $U->event_code($ret);
563 my( $e, $class, $itemid ) = @_;
564 my( $bucket, $item, $evt);
566 ( $item, $evt ) = $U->fetch_container_item_e( $e, $itemid, $class );
569 ( $bucket, $evt ) = $U->fetch_container_e($e, $item->bucket, $class);
572 if( $bucket->owner ne $e->requestor->id ) {
573 my $owner = $e->retrieve_actor_user($bucket->owner)
574 or return $e->die_event;
575 return $e->event unless $e->allowed('DELETE_CONTAINER_ITEM', $owner->home_ou);
579 if( $class eq 'copy' ) {
580 for my $note (@{$e->search_container_copy_bucket_item_note({item => $item->id})}) {
581 return $e->event unless
582 $e->delete_container_copy_bucket_item_note($note);
584 return $e->event unless
585 $stat = $e->delete_container_copy_bucket_item($item);
588 if( $class eq 'callnumber' ) {
589 for my $note (@{$e->search_container_call_number_bucket_item_note({item => $item->id})}) {
590 return $e->event unless
591 $e->delete_container_call_number_bucket_item_note($note);
593 return $e->event unless
594 $stat = $e->delete_container_call_number_bucket_item($item);
597 if( $class eq 'biblio' ) {
598 for my $note (@{$e->search_container_biblio_record_entry_bucket_item_note({item => $item->id})}) {
599 return $e->event unless
600 $e->delete_container_biblio_record_entry_bucket_item_note($note);
602 return $e->event unless
603 $stat = $e->delete_container_biblio_record_entry_bucket_item($item);
606 if( $class eq 'user') {
607 for my $note (@{$e->search_container_user_bucket_item_note({item => $item->id})}) {
608 return $e->event unless
609 $e->delete_container_user_bucket_item_note($note);
611 return $e->event unless
612 $stat = $e->delete_container_user_bucket_item($item);
619 __PACKAGE__->register_method(
620 method => 'full_delete',
621 api_name => 'open-ils.actor.container.full_delete',
622 notes => "Complety removes a container including all attached items",
626 my( $self, $client, $authtoken, $class, $containerId ) = @_;
627 my( $container, $evt);
629 my $e = new_editor(xact=>1, authtoken=>$authtoken);
630 return $e->event unless $e->checkauth;
632 ( $container, $evt ) = $apputils->fetch_container_e($e, $containerId, $class);
635 if( $container->owner ne $e->requestor->id ) {
636 my $owner = $e->retrieve_actor_user($container->owner)
637 or return $e->die_event;
638 return $e->event unless $e->allowed('DELETE_CONTAINER', $owner->home_ou);
643 my @s = ({bucket => $containerId}, {idlist=>1});
645 if( $class eq 'copy' ) {
646 $items = $e->search_container_copy_bucket_item(@s);
649 if( $class eq 'callnumber' ) {
650 $items = $e->search_container_call_number_bucket_item(@s);
653 if( $class eq 'biblio' ) {
654 $items = $e->search_container_biblio_record_entry_bucket_item(@s);
657 if( $class eq 'user') {
658 $items = $e->search_container_user_bucket_item(@s);
661 __item_delete($e, $class, $_) for @$items;
664 if( $class eq 'copy' ) {
665 return $e->event unless
666 $stat = $e->delete_container_copy_bucket($container);
669 if( $class eq 'callnumber' ) {
670 return $e->event unless
671 $stat = $e->delete_container_call_number_bucket($container);
674 if( $class eq 'biblio' ) {
675 return $e->event unless
676 $stat = $e->delete_container_biblio_record_entry_bucket($container);
679 if( $class eq 'user') {
680 return $e->event unless
681 $stat = $e->delete_container_user_bucket($container);
688 __PACKAGE__->register_method(
689 method => 'container_update',
690 api_name => 'open-ils.actor.container.update',
692 Updates the given container item.
693 @param authtoken The login session key
694 @param class The container class
695 @param container The container item
696 @return true on success, 0 on no update, Event on error
700 sub container_update {
701 my( $self, $conn, $authtoken, $class, $container ) = @_;
703 my $e = new_editor(xact=>1, authtoken=>$authtoken);
704 return $e->event unless $e->checkauth;
706 my ( $dbcontainer, $evt ) = $U->fetch_container_e($e, $container->id, $class);
709 if( $e->requestor->id ne $container->owner ) {
710 return $e->event unless $e->allowed('UPDATE_CONTAINER');
714 if( $class eq 'copy' ) {
715 return $e->event unless
716 $stat = $e->update_container_copy_bucket($container);
719 if( $class eq 'callnumber' ) {
720 return $e->event unless
721 $stat = $e->update_container_call_number_bucket($container);
724 if( $class eq 'biblio' ) {
725 return $e->event unless
726 $stat = $e->update_container_biblio_record_entry_bucket($container);
729 if( $class eq 'user') {
730 return $e->event unless
731 $stat = $e->update_container_user_bucket($container);
740 __PACKAGE__->register_method(
741 method => "anon_cache",
742 api_name => "open-ils.actor.anon_cache.set_value",
745 Sets a value in the anon web cache. If the session key is
746 undefined, one will be automatically generated.
749 {desc => 'Session key', type => 'string'},
751 desc => q/Field name. The name of the field in this cache session whose value to set/,
755 desc => q/The cached value. This can be any type of object (hash, array, string, etc.)/,
760 desc => 'session key on success, undef on error',
766 __PACKAGE__->register_method(
767 method => "anon_cache",
768 api_name => "open-ils.actor.anon_cache.get_value",
771 Returns the cached data at the specified field within the specified cache session.
774 {desc => 'Session key', type => 'string'},
776 desc => q/Field name. The name of the field in this cache session whose value to set/,
781 desc => 'cached value on success, undef on error',
787 __PACKAGE__->register_method(
788 method => "anon_cache",
789 api_name => "open-ils.actor.anon_cache.delete_session",
792 Deletes a cache session.
795 {desc => 'Session key', type => 'string'},
798 desc => 'Session key',
805 my($self, $conn, $ses_key, $field_key, $value) = @_;
807 my $sc = OpenSRF::Utils::SettingsClient->new;
808 my $cache = OpenSRF::Utils::Cache->new('anon');
809 my $cache_timeout = $sc->config_value(cache => anon => 'max_cache_time') || 1800; # 30 minutes
810 my $cache_size = $sc->config_value(cache => anon => 'max_cache_size') || 102400; # 100k
812 if($self->api_name =~ /delete_session/) {
814 return $cache->delete_cache($ses_key);
816 } elsif( $self->api_name =~ /set_value/ ) {
818 $ses_key = md5_hex(time . rand($$)) unless $ses_key;
819 my $blob = $cache->get_cache($ses_key) || {};
820 $blob->{$field_key} = $value;
822 length(OpenSRF::Utils::JSON->perl2JSON($blob)) > $cache_size; # bytes, characters, whatever ;)
823 $cache->put_cache($ses_key, $blob, $cache_timeout);
828 my $blob = $cache->get_cache($ses_key) or return undef;
829 return $blob if (!defined($field_key));
830 return $blob->{$field_key};
834 sub batch_statcat_apply {
841 # $changes is a hashref that looks like:
843 # remove => [ qw/ stat cat ids to remove / ],
844 # apply => { $statcat_id => $value_string, ... }
852 my $e = new_editor(xact=>1, authtoken=>$ses);
853 return $e->die_event unless $e->checkauth;
854 $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
855 return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
857 my $meth = 'retrieve_' . $ctypes{$class};
858 my $bkt = $e->$meth($c_id) or return $e->die_event;
860 unless($bkt->owner eq $e->requestor->id) {
861 $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
862 my $owner = $e->retrieve_actor_user($bkt->owner)
863 or return $e->die_event;
864 return $e->die_event unless (
865 $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
869 $meth = 'search_' . $ctypes{$class} . '_item';
870 my $contents = $e->$meth({bucket => $c_id});
872 if ($self->{perms}) {
873 $max = scalar(@$contents);
874 $client->respond({ ord => $stage, max => $max, count => 0, stage => 'ITEM_PERM_CHECK' });
875 for my $item (@$contents) {
877 $meth = 'retrieve_' . $itypes{$class};
878 my $field = 'target_'.$ttypes{$class};
879 my $obj = $e->$meth($item->$field);
881 for my $perm_field (keys %{$self->{perms}}) {
882 my $perm_def = $self->{perms}->{$perm_field};
883 my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
884 for my $p (@$pwhat) {
885 $e->allowed($p, $obj->$pwhere) or return $e->die_event;
888 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
893 my @users = map { $_->target_user } @$contents;
894 $max = scalar(@users) * scalar(@{$changes->{remove}});
896 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' });
898 my $chunk = int($max / 10) || 1;
899 my $to_remove = $e->search_actor_stat_cat_entry_user_map({ target_usr => \@users, stat_cat => $changes->{remove} });
900 for my $t (@$to_remove) {
901 $e->delete_actor_stat_cat_entry_user_map($t);
903 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' })
904 unless ($count % $chunk);
909 $max = scalar(@users) * scalar(keys %{$changes->{apply}});
911 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' });
913 $chunk = int($max / 10) || 1;
914 for my $item (@$contents) {
915 for my $astatcat (keys %{$changes->{apply}}) {
916 my $new_value = $changes->{apply}->{$astatcat};
917 my $to_change = $e->search_actor_stat_cat_entry_user_map({ target_usr => $item->target_user, stat_cat => $astatcat });
919 $to_change = $$to_change[0];
920 $to_change->stat_cat_entry($new_value);
921 $e->update_actor_stat_cat_entry_user_map($to_change);
923 $to_change = new Fieldmapper::actor::stat_cat_entry_user_map;
924 $to_change->stat_cat_entry($new_value);
925 $to_change->stat_cat($astatcat);
926 $to_change->target_usr($item->target_user);
927 $e->create_actor_stat_cat_entry_user_map($to_change);
930 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' })
931 unless ($count % $chunk);
937 return { stage => 'COMPLETE' };
940 __PACKAGE__->register_method(
941 method => "batch_statcat_apply",
942 api_name => "open-ils.actor.container.user.batch_statcat_apply",
945 home_ou => 'UPDATE_USER', # field -> perm means "test this perm with field as context OU", both old and new
947 fields => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
949 desc => 'Edits allowed fields on users in a bucket',
951 desc => 'Session key', type => 'string',
952 desc => 'User container id',
953 desc => 'Hash of statcats to apply or remove', type => 'hash',
956 desc => 'Object with the structure { stage => "stage string", max => max_for_stage, count => count_in_stage }',
968 my $main_fsg = shift;
974 my $class = $self->{ctype} or return undef;
976 my $e = new_editor(xact=>1, authtoken=>$ses);
977 return $e->die_event unless $e->checkauth;
979 for my $bp (@{$batch_perm{$class}}) {
980 return { stage => 'COMPLETE' } unless $e->allowed($bp);
983 $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
984 return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
986 my $meth = 'retrieve_' . $ctypes{$class};
987 my $bkt = $e->$meth($c_id) or return $e->die_event;
989 unless($bkt->owner eq $e->requestor->id) {
990 $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
991 my $owner = $e->retrieve_actor_user($bkt->owner)
992 or return $e->die_event;
993 return $e->die_event unless (
994 $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
998 $main_fsg = $e->retrieve_action_fieldset_group($main_fsg);
999 return { stage => 'COMPLETE', error => 'No field set group' } unless $main_fsg;
1001 my $rbg = $e->retrieve_action_fieldset_group($main_fsg->rollback_group);
1002 return { stage => 'COMPLETE', error => 'No rollback field set group' } unless $rbg;
1004 my $fieldsets = $e->search_action_fieldset({fieldset_group => $rbg->id});
1005 $max = scalar(@$fieldsets);
1007 $client->respond({ ord => $stage, max => $max, count => 0, stage => 'APPLY_EDITS' });
1008 for my $fs (@$fieldsets) {
1009 my $res = $e->json_query({
1010 from => ['action.apply_fieldset', $fs->id, $table{$class}, 'id', undef]
1011 })->[0]->{'action.apply_fieldset'};
1017 stage => 'APPLY_EDITS',
1018 error => $res ? "Could not apply fieldset ".$fs->id.": $res" : undef
1022 $main_fsg->rollback_time('now');
1023 $e->update_action_fieldset_group($main_fsg);
1027 return { stage => 'COMPLETE' };
1029 __PACKAGE__->register_method(
1030 method => "apply_rollback",
1031 max_bundle_count => 1,
1032 api_name => "open-ils.actor.container.user.apply_rollback",
1035 desc => 'Applys rollback of a fieldset group to users in a bucket',
1037 { desc => 'Session key', type => 'string' },
1038 { desc => 'User container id', type => 'number' },
1039 { desc => 'Main (non-rollback) fieldset group' },
1042 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
1054 my $edit_name = shift;
1061 my $class = $self->{ctype} or return undef;
1063 my $e = new_editor(xact=>1, authtoken=>$ses);
1064 return $e->die_event unless $e->checkauth;
1066 for my $bp (@{$batch_perm{$class}}) {
1067 return { stage => 'COMPLETE' } unless $e->allowed($bp);
1070 $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
1071 return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
1073 my $meth = 'retrieve_' . $ctypes{$class};
1074 my $bkt = $e->$meth($c_id) or return $e->die_event;
1076 unless($bkt->owner eq $e->requestor->id) {
1077 $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
1078 my $owner = $e->retrieve_actor_user($bkt->owner)
1079 or return $e->die_event;
1080 return $e->die_event unless (
1081 $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
1085 $meth = 'search_' . $ctypes{$class} . '_item';
1086 my $contents = $e->$meth({bucket => $c_id});
1089 $max = scalar(@$contents) if ($self->{perms});
1090 $max += scalar(@$contents) if ($self->{base_perm});
1093 if ($self->{base_perm}) {
1094 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1095 for my $item (@$contents) {
1097 $meth = 'retrieve_' . $itypes{$class};
1098 my $field = 'target_'.$ttypes{$class};
1099 my $obj = $$obj_cache{$item->$field} = $e->$meth($item->$field);
1101 for my $perm_field (keys %{$self->{base_perm}}) {
1102 my $perm_def = $self->{base_perm}->{$perm_field};
1103 my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
1104 for my $p (@$pwhat) {
1105 $e->allowed($p, $obj->$pwhere) or return $e->die_event;
1106 if ($$edits{$pwhere}) {
1107 $e->allowed($p, $$edits{$pwhere}) or do {
1108 $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
1109 return $e->die_event;
1114 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1118 if ($self->{perms}) {
1119 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1120 for my $item (@$contents) {
1122 $meth = 'retrieve_' . $itypes{$class};
1123 my $field = 'target_'.$ttypes{$class};
1124 my $obj = $$obj_cache{$item->$field} || $e->$meth($item->$field);
1126 for my $perm_field (keys %{$self->{perms}}) {
1127 my $perm_def = $self->{perms}->{$perm_field};
1128 if (ref($perm_def) eq 'HASH') { # we care about specific values being set
1129 for my $perm_value (keys %$perm_def) {
1130 if (exists $$edits{$perm_field} && $$edits{$perm_field} eq $perm_value) { # check permission
1131 while (my ($pwhat,$pwhere) = each %{$$perm_def{$perm_value}}) {
1132 if ($pwhere eq '*') {
1135 $pwhere = $obj->$pwhere;
1137 $pwhat = [ split / /, $pwhat ];
1138 for my $p (@$pwhat) {
1139 $e->allowed($p, $pwhere) or do {
1140 $pwhere ||= "everywhere";
1141 $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
1142 return $e->die_event;
1148 } elsif (ref($perm_def) eq 'CODE') { # we need to run the code on old and new, and pass both tests
1149 if (exists $$edits{$perm_field}) {
1150 $perm_def->($e, $obj->$perm_field) or return $e->die_event;
1151 $perm_def->($e, $$edits{$perm_field}) or return $e->die_event;
1153 } else { # we're checking an ou field
1154 my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
1155 if ($$edits{$pwhere}) {
1156 for my $p (@$pwhat) {
1157 $e->allowed($p, $obj->$pwhere) or return $e->die_event;
1158 $e->allowed($p, $$edits{$pwhere}) or do {
1159 $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
1160 return $e->die_event;
1166 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1171 $client->respond({ ord => $stage++, stage => 'FIELDSET_GROUP_CREATE' });
1172 my $fsgroup = Fieldmapper::action::fieldset_group->new;
1174 $fsgroup->name($edit_name);
1175 $fsgroup->creator($e->requestor->id);
1176 $fsgroup->owning_lib($e->requestor->ws_ou);
1177 $fsgroup->container($c_id);
1178 $fsgroup->container_type($ttypes{$class});
1179 $fsgroup = $e->create_action_fieldset_group($fsgroup);
1181 $client->respond({ ord => $stage++, stage => 'FIELDSET_CREATE' });
1182 my $fieldset = Fieldmapper::action::fieldset->new;
1183 $fieldset->isnew(1);
1184 $fieldset->fieldset_group($fsgroup->id);
1185 $fieldset->owner($e->requestor->id);
1186 $fieldset->owning_lib($e->requestor->ws_ou);
1187 $fieldset->status('PENDING');
1188 $fieldset->classname($htypes{$class});
1189 $fieldset->name($edit_name . ' batch group fieldset');
1190 $fieldset->stored_query($qtypes{$class});
1191 $fieldset = $e->create_action_fieldset($fieldset);
1193 my @keys = keys %$edits;
1194 $max = scalar(@keys);
1196 $client->respond({ ord => $stage, count=> $count, max => $max, stage => 'FIELDSET_EDITS_CREATE' });
1197 for my $key (@keys) {
1198 if ($self->{fields}) { # restrict edits to registered fields
1199 next unless (grep { $_ eq $key } @{$self->{fields}});
1201 my $fs_cv = Fieldmapper::action::fieldset_col_val->new;
1203 $fs_cv->fieldset($fieldset->id);
1205 $fs_cv->val($$edits{$key});
1206 $e->create_action_fieldset_col_val($fs_cv);
1208 $client->respond({ ord => $stage, count=> $count, max => $max, stage => 'FIELDSET_EDITS_CREATE' });
1211 $client->respond({ ord => ++$stage, stage => 'CONSTRUCT_QUERY' });
1212 my $qstore = OpenSRF::AppSession->connect('open-ils.qstore');
1213 my $prep = $qstore->request('open-ils.qstore.prepare', $fieldset->stored_query)->gather(1);
1214 my $token = $prep->{token};
1215 $qstore->request('open-ils.qstore.bind_param', $token, {bucket => $c_id})->gather(1);
1216 my $sql = $qstore->request('open-ils.qstore.sql', $token)->gather(1);
1217 $sql =~ s/\n\s*/ /g; # normalize the string
1218 $sql =~ s/;\s*//g; # kill trailing semicolon
1220 $client->respond({ ord => ++$stage, stage => 'APPLY_EDITS' });
1221 my $res = $e->json_query({
1222 from => ['action.apply_fieldset', $fieldset->id, $table{$class}, 'id', $sql]
1223 })->[0]->{'action.apply_fieldset'};
1226 $qstore->disconnect;
1228 return { fieldset_group => $fsgroup->id, stage => 'COMPLETE', error => $res };
1231 __PACKAGE__->register_method(
1232 method => "batch_edit",
1233 max_bundle_count => 1,
1234 api_name => "open-ils.actor.container.user.batch_edit",
1236 base_perm => { home_ou => 'UPDATE_USER' },
1239 my ($e, $group) = @_;
1240 my $g = $e->retrieve_permission_grp_tree($group);
1241 if (my $p = $g->application_perm()) {
1242 return $e->allowed($p);
1245 }, # code ref is run with params (editor,value), for both old and new value
1246 # home_ou => 'UPDATE_USER', # field -> perm means "test this perm with field as context OU", both old and new
1248 t => { BAR_PATRON => 'home_ou' },
1249 f => { UNBAR_PATRON => 'home_ou' }
1250 } # field -> struct means "if field getting value "key" check -> perm -> at context org, both old and new
1252 fields => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
1254 desc => 'Edits allowed fields on users in a bucket',
1256 { desc => 'Session key', type => 'string' },
1257 { desc => 'User container id', type => 'number' },
1258 { desc => 'Batch edit name', type => 'string' },
1259 { desc => 'Edit hash, key is column, value is new value to apply', type => 'hash' },
1262 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
1268 __PACKAGE__->register_method(
1269 method => "batch_edit",
1270 api_name => "open-ils.actor.container.user.batch_delete",
1274 t => { 'DELETE_USER UPDATE_USER' => 'home_ou' },
1275 f => { 'UPDATE_USER' => 'home_ou' }
1278 fields => [ qw/deleted/ ],
1280 desc => 'Deletes users in a bucket',
1282 { desc => 'Session key', type => 'string' },
1283 { desc => 'User container id', type => 'number' },
1284 { desc => 'Batch delete name', type => 'string' },
1285 { desc => 'Edit delete, key is "deleted", value is new value to apply ("t")', type => 'hash' },
1289 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',