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 ) = $apputils->fetch_container_e($e, $item->bucket, $class);
356 if( $bucket->owner ne $e->requestor->id ) {
357 return $e->die_event unless
358 $e->allowed('CREATE_CONTAINER_ITEM');
361 # return $e->event unless
362 # $e->allowed('CREATE_CONTAINER_ITEM'); # new perm here?
365 for my $one_item (@$items) {
370 if( $class eq 'copy' ) {
371 return $e->die_event unless
372 $stat = $e->create_container_copy_bucket_item($one_item);
375 if( $class eq 'callnumber' ) {
376 return $e->die_event unless
377 $stat = $e->create_container_call_number_bucket_item($one_item);
380 if( $class eq 'biblio' ) {
381 return $e->die_event unless
382 $stat = $e->create_container_biblio_record_entry_bucket_item($one_item);
385 if( $class eq 'user') {
386 return $e->die_event unless
387 $stat = $e->create_container_user_bucket_item($one_item);
393 # CStoreEeditor inserts the id (pkey) on newly created objects
394 return [ map { $_->id } @$items ] if ref $item eq 'ARRAY';
400 __PACKAGE__->register_method(
401 method => "item_delete",
402 api_name => "open-ils.actor.container.item.delete",
403 notes => <<" NOTES");
404 PARAMS(authtoken, class, itemId)
408 my( $self, $client, $authtoken, $class, $itemid ) = @_;
410 my $e = new_editor(xact=>1, authtoken=>$authtoken);
411 return $e->event unless $e->checkauth;
413 my $ret = __item_delete($e, $class, $itemid);
414 $e->commit unless $U->event_code($ret);
419 my( $e, $class, $itemid ) = @_;
420 my( $bucket, $item, $evt);
422 ( $item, $evt ) = $U->fetch_container_item_e( $e, $itemid, $class );
425 ( $bucket, $evt ) = $U->fetch_container_e($e, $item->bucket, $class);
428 if( $bucket->owner ne $e->requestor->id ) {
429 my $owner = $e->retrieve_actor_user($bucket->owner)
430 or return $e->die_event;
431 return $e->event unless $e->allowed('DELETE_CONTAINER_ITEM', $owner->home_ou);
435 if( $class eq 'copy' ) {
436 for my $note (@{$e->search_container_copy_bucket_item_note({item => $item->id})}) {
437 return $e->event unless
438 $e->delete_container_copy_bucket_item_note($note);
440 return $e->event unless
441 $stat = $e->delete_container_copy_bucket_item($item);
444 if( $class eq 'callnumber' ) {
445 for my $note (@{$e->search_container_call_number_bucket_item_note({item => $item->id})}) {
446 return $e->event unless
447 $e->delete_container_call_number_bucket_item_note($note);
449 return $e->event unless
450 $stat = $e->delete_container_call_number_bucket_item($item);
453 if( $class eq 'biblio' ) {
454 for my $note (@{$e->search_container_biblio_record_entry_bucket_item_note({item => $item->id})}) {
455 return $e->event unless
456 $e->delete_container_biblio_record_entry_bucket_item_note($note);
458 return $e->event unless
459 $stat = $e->delete_container_biblio_record_entry_bucket_item($item);
462 if( $class eq 'user') {
463 for my $note (@{$e->search_container_user_bucket_item_note({item => $item->id})}) {
464 return $e->event unless
465 $e->delete_container_user_bucket_item_note($note);
467 return $e->event unless
468 $stat = $e->delete_container_user_bucket_item($item);
475 __PACKAGE__->register_method(
476 method => 'full_delete',
477 api_name => 'open-ils.actor.container.full_delete',
478 notes => "Complety removes a container including all attached items",
482 my( $self, $client, $authtoken, $class, $containerId ) = @_;
483 my( $container, $evt);
485 my $e = new_editor(xact=>1, authtoken=>$authtoken);
486 return $e->event unless $e->checkauth;
488 ( $container, $evt ) = $apputils->fetch_container_e($e, $containerId, $class);
491 if( $container->owner ne $e->requestor->id ) {
492 my $owner = $e->retrieve_actor_user($container->owner)
493 or return $e->die_event;
494 return $e->event unless $e->allowed('DELETE_CONTAINER', $owner->home_ou);
499 my @s = ({bucket => $containerId}, {idlist=>1});
501 if( $class eq 'copy' ) {
502 $items = $e->search_container_copy_bucket_item(@s);
505 if( $class eq 'callnumber' ) {
506 $items = $e->search_container_call_number_bucket_item(@s);
509 if( $class eq 'biblio' ) {
510 $items = $e->search_container_biblio_record_entry_bucket_item(@s);
513 if( $class eq 'user') {
514 $items = $e->search_container_user_bucket_item(@s);
517 __item_delete($e, $class, $_) for @$items;
520 if( $class eq 'copy' ) {
521 return $e->event unless
522 $stat = $e->delete_container_copy_bucket($container);
525 if( $class eq 'callnumber' ) {
526 return $e->event unless
527 $stat = $e->delete_container_call_number_bucket($container);
530 if( $class eq 'biblio' ) {
531 return $e->event unless
532 $stat = $e->delete_container_biblio_record_entry_bucket($container);
535 if( $class eq 'user') {
536 return $e->event unless
537 $stat = $e->delete_container_user_bucket($container);
544 __PACKAGE__->register_method(
545 method => 'container_update',
546 api_name => 'open-ils.actor.container.update',
548 Updates the given container item.
549 @param authtoken The login session key
550 @param class The container class
551 @param container The container item
552 @return true on success, 0 on no update, Event on error
556 sub container_update {
557 my( $self, $conn, $authtoken, $class, $container ) = @_;
559 my $e = new_editor(xact=>1, authtoken=>$authtoken);
560 return $e->event unless $e->checkauth;
562 my ( $dbcontainer, $evt ) = $U->fetch_container_e($e, $container->id, $class);
565 if( $e->requestor->id ne $container->owner ) {
566 return $e->event unless $e->allowed('UPDATE_CONTAINER');
570 if( $class eq 'copy' ) {
571 return $e->event unless
572 $stat = $e->update_container_copy_bucket($container);
575 if( $class eq 'callnumber' ) {
576 return $e->event unless
577 $stat = $e->update_container_call_number_bucket($container);
580 if( $class eq 'biblio' ) {
581 return $e->event unless
582 $stat = $e->update_container_biblio_record_entry_bucket($container);
585 if( $class eq 'user') {
586 return $e->event unless
587 $stat = $e->update_container_user_bucket($container);
596 __PACKAGE__->register_method(
597 method => "anon_cache",
598 api_name => "open-ils.actor.anon_cache.set_value",
601 Sets a value in the anon web cache. If the session key is
602 undefined, one will be automatically generated.
605 {desc => 'Session key', type => 'string'},
607 desc => q/Field name. The name of the field in this cache session whose value to set/,
611 desc => q/The cached value. This can be any type of object (hash, array, string, etc.)/,
616 desc => 'session key on success, undef on error',
622 __PACKAGE__->register_method(
623 method => "anon_cache",
624 api_name => "open-ils.actor.anon_cache.get_value",
627 Returns the cached data at the specified field within the specified cache session.
630 {desc => 'Session key', type => 'string'},
632 desc => q/Field name. The name of the field in this cache session whose value to set/,
637 desc => 'cached value on success, undef on error',
643 __PACKAGE__->register_method(
644 method => "anon_cache",
645 api_name => "open-ils.actor.anon_cache.delete_session",
648 Deletes a cache session.
651 {desc => 'Session key', type => 'string'},
654 desc => 'Session key',
661 my($self, $conn, $ses_key, $field_key, $value) = @_;
663 my $sc = OpenSRF::Utils::SettingsClient->new;
664 my $cache = OpenSRF::Utils::Cache->new('anon');
665 my $cache_timeout = $sc->config_value(cache => anon => 'max_cache_time') || 1800; # 30 minutes
666 my $cache_size = $sc->config_value(cache => anon => 'max_cache_size') || 102400; # 100k
668 if($self->api_name =~ /delete_session/) {
670 return $cache->delete_cache($ses_key);
672 } elsif( $self->api_name =~ /set_value/ ) {
674 $ses_key = md5_hex(time . rand($$)) unless $ses_key;
675 my $blob = $cache->get_cache($ses_key) || {};
676 $blob->{$field_key} = $value;
678 length(OpenSRF::Utils::JSON->perl2JSON($blob)) > $cache_size; # bytes, characters, whatever ;)
679 $cache->put_cache($ses_key, $blob, $cache_timeout);
684 my $blob = $cache->get_cache($ses_key) or return undef;
685 return $blob if (!defined($field_key));
686 return $blob->{$field_key};
690 sub batch_statcat_apply {
697 # $changes is a hashref that looks like:
699 # remove => [ qw/ stat cat ids to remove / ],
700 # apply => { $statcat_id => $value_string, ... }
708 my $e = new_editor(xact=>1, authtoken=>$ses);
709 return $e->die_event unless $e->checkauth;
710 $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
711 return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
713 my $meth = 'retrieve_' . $ctypes{$class};
714 my $bkt = $e->$meth($c_id) or return $e->die_event;
716 unless($bkt->owner eq $e->requestor->id) {
717 $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
718 my $owner = $e->retrieve_actor_user($bkt->owner)
719 or return $e->die_event;
720 return $e->die_event unless (
721 $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
725 $meth = 'search_' . $ctypes{$class} . '_item';
726 my $contents = $e->$meth({bucket => $c_id});
728 if ($self->{perms}) {
729 $max = scalar(@$contents);
730 $client->respond({ ord => $stage, max => $max, count => 0, stage => 'ITEM_PERM_CHECK' });
731 for my $item (@$contents) {
733 $meth = 'retrieve_' . $itypes{$class};
734 my $field = 'target_'.$ttypes{$class};
735 my $obj = $e->$meth($item->$field);
737 for my $perm_field (keys %{$self->{perms}}) {
738 my $perm_def = $self->{perms}->{$perm_field};
739 my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
740 for my $p (@$pwhat) {
741 $e->allowed($p, $obj->$pwhere) or return $e->die_event;
744 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
749 my @users = map { $_->target_user } @$contents;
750 $max = scalar(@users) * scalar(@{$changes->{remove}});
752 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' });
754 my $chunk = int($max / 10) || 1;
755 my $to_remove = $e->search_actor_stat_cat_entry_user_map({ target_usr => \@users, stat_cat => $changes->{remove} });
756 for my $t (@$to_remove) {
757 $e->delete_actor_stat_cat_entry_user_map($t);
759 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_REMOVE' })
760 unless ($count % $chunk);
765 $max = scalar(@users) * scalar(keys %{$changes->{apply}});
767 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' });
769 $chunk = int($max / 10) || 1;
770 for my $item (@$contents) {
771 for my $astatcat (keys %{$changes->{apply}}) {
772 my $new_value = $changes->{apply}->{$astatcat};
773 my $to_change = $e->search_actor_stat_cat_entry_user_map({ target_usr => $item->target_user, stat_cat => $astatcat });
775 $to_change = $$to_change[0];
776 $to_change->stat_cat_entry($new_value);
777 $e->update_actor_stat_cat_entry_user_map($to_change);
779 $to_change = new Fieldmapper::actor::stat_cat_entry_user_map;
780 $to_change->stat_cat_entry($new_value);
781 $to_change->stat_cat($astatcat);
782 $to_change->target_usr($item->target_user);
783 $e->create_actor_stat_cat_entry_user_map($to_change);
786 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'STAT_CAT_APPLY' })
787 unless ($count % $chunk);
793 return { stage => 'COMPLETE' };
796 __PACKAGE__->register_method(
797 method => "batch_statcat_apply",
798 api_name => "open-ils.actor.container.user.batch_statcat_apply",
801 home_ou => 'UPDATE_USER', # field -> perm means "test this perm with field as context OU", both old and new
803 fields => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
805 desc => 'Edits allowed fields on users in a bucket',
807 desc => 'Session key', type => 'string',
808 desc => 'User container id',
809 desc => 'Hash of statcats to apply or remove', type => 'hash',
812 desc => 'Object with the structure { stage => "stage string", max => max_for_stage, count => count_in_stage }',
824 my $main_fsg = shift;
830 my $class = $self->{ctype} or return undef;
832 my $e = new_editor(xact=>1, authtoken=>$ses);
833 return $e->die_event unless $e->checkauth;
835 for my $bp (@{$batch_perm{$class}}) {
836 return { stage => 'COMPLETE' } unless $e->allowed($bp);
839 $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
840 return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
842 my $meth = 'retrieve_' . $ctypes{$class};
843 my $bkt = $e->$meth($c_id) or return $e->die_event;
845 unless($bkt->owner eq $e->requestor->id) {
846 $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
847 my $owner = $e->retrieve_actor_user($bkt->owner)
848 or return $e->die_event;
849 return $e->die_event unless (
850 $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
854 $main_fsg = $e->retrieve_action_fieldset_group($main_fsg);
855 return { stage => 'COMPLETE', error => 'No field set group' } unless $main_fsg;
857 my $rbg = $e->retrieve_action_fieldset_group($main_fsg->rollback_group);
858 return { stage => 'COMPLETE', error => 'No rollback field set group' } unless $rbg;
860 my $fieldsets = $e->search_action_fieldset({fieldset_group => $rbg->id});
861 $max = scalar(@$fieldsets);
863 $client->respond({ ord => $stage, max => $max, count => 0, stage => 'APPLY_EDITS' });
864 for my $fs (@$fieldsets) {
865 my $res = $e->json_query({
866 from => ['action.apply_fieldset', $fs->id, $table{$class}, 'id', undef]
867 })->[0]->{'action.apply_fieldset'};
873 stage => 'APPLY_EDITS',
874 error => $res ? "Could not apply fieldset ".$fs->id.": $res" : undef
878 $main_fsg->rollback_time('now');
879 $e->update_action_fieldset_group($main_fsg);
883 return { stage => 'COMPLETE' };
885 __PACKAGE__->register_method(
886 method => "apply_rollback",
887 max_bundle_count => 1,
888 api_name => "open-ils.actor.container.user.apply_rollback",
891 desc => 'Applys rollback of a fieldset group to users in a bucket',
893 { desc => 'Session key', type => 'string' },
894 { desc => 'User container id', type => 'number' },
895 { desc => 'Main (non-rollback) fieldset group' },
898 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
910 my $edit_name = shift;
917 my $class = $self->{ctype} or return undef;
919 my $e = new_editor(xact=>1, authtoken=>$ses);
920 return $e->die_event unless $e->checkauth;
922 for my $bp (@{$batch_perm{$class}}) {
923 return { stage => 'COMPLETE' } unless $e->allowed($bp);
926 $client->respond({ ord => $stage++, stage => 'CONTAINER_BATCH_UPDATE_PERM_CHECK' });
927 return $e->die_event unless $e->allowed('CONTAINER_BATCH_UPDATE');
929 my $meth = 'retrieve_' . $ctypes{$class};
930 my $bkt = $e->$meth($c_id) or return $e->die_event;
932 unless($bkt->owner eq $e->requestor->id) {
933 $client->respond({ ord => $stage++, stage => 'CONTAINER_PERM_CHECK' });
934 my $owner = $e->retrieve_actor_user($bkt->owner)
935 or return $e->die_event;
936 return $e->die_event unless (
937 $e->allowed('VIEW_CONTAINER', $bkt->owning_lib) || $e->allowed('VIEW_CONTAINER', $owner->home_ou)
941 $meth = 'search_' . $ctypes{$class} . '_item';
942 my $contents = $e->$meth({bucket => $c_id});
945 $max = scalar(@$contents) if ($self->{perms});
946 $max += scalar(@$contents) if ($self->{base_perm});
949 if ($self->{base_perm}) {
950 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
951 for my $item (@$contents) {
953 $meth = 'retrieve_' . $itypes{$class};
954 my $field = 'target_'.$ttypes{$class};
955 my $obj = $$obj_cache{$item->$field} = $e->$meth($item->$field);
957 for my $perm_field (keys %{$self->{base_perm}}) {
958 my $perm_def = $self->{base_perm}->{$perm_field};
959 my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
960 for my $p (@$pwhat) {
961 $e->allowed($p, $obj->$pwhere) or return $e->die_event;
962 if ($$edits{$pwhere}) {
963 $e->allowed($p, $$edits{$pwhere}) or do {
964 $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
965 return $e->die_event;
970 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
974 if ($self->{perms}) {
975 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
976 for my $item (@$contents) {
978 $meth = 'retrieve_' . $itypes{$class};
979 my $field = 'target_'.$ttypes{$class};
980 my $obj = $$obj_cache{$item->$field} || $e->$meth($item->$field);
982 for my $perm_field (keys %{$self->{perms}}) {
983 my $perm_def = $self->{perms}->{$perm_field};
984 if (ref($perm_def) eq 'HASH') { # we care about specific values being set
985 for my $perm_value (keys %$perm_def) {
986 if (exists $$edits{$perm_field} && $$edits{$perm_field} eq $perm_value) { # check permission
987 while (my ($pwhat,$pwhere) = each %{$$perm_def{$perm_value}}) {
988 if ($pwhere eq '*') {
991 $pwhere = $obj->$pwhere;
993 $pwhat = [ split / /, $pwhat ];
994 for my $p (@$pwhat) {
995 $e->allowed($p, $pwhere) or do {
996 $pwhere ||= "everywhere";
997 $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
998 return $e->die_event;
1004 } elsif (ref($perm_def) eq 'CODE') { # we need to run the code on old and new, and pass both tests
1005 if (exists $$edits{$perm_field}) {
1006 $perm_def->($e, $obj->$perm_field) or return $e->die_event;
1007 $perm_def->($e, $$edits{$perm_field}) or return $e->die_event;
1009 } else { # we're checking an ou field
1010 my ($pwhat,$pwhere) = ([split ' ', $perm_def], $perm_field);
1011 if ($$edits{$pwhere}) {
1012 for my $p (@$pwhat) {
1013 $e->allowed($p, $obj->$pwhere) or return $e->die_event;
1014 $e->allowed($p, $$edits{$pwhere}) or do {
1015 $logger->warn("Cannot update $class ".$obj->id.", $pwhat at $pwhere not allowed.");
1016 return $e->die_event;
1022 $client->respond({ ord => $stage, max => $max, count => $count, stage => 'ITEM_PERM_CHECK' });
1027 $client->respond({ ord => $stage++, stage => 'FIELDSET_GROUP_CREATE' });
1028 my $fsgroup = Fieldmapper::action::fieldset_group->new;
1030 $fsgroup->name($edit_name);
1031 $fsgroup->creator($e->requestor->id);
1032 $fsgroup->owning_lib($e->requestor->ws_ou);
1033 $fsgroup->container($c_id);
1034 $fsgroup->container_type($ttypes{$class});
1035 $fsgroup = $e->create_action_fieldset_group($fsgroup);
1037 $client->respond({ ord => $stage++, stage => 'FIELDSET_CREATE' });
1038 my $fieldset = Fieldmapper::action::fieldset->new;
1039 $fieldset->isnew(1);
1040 $fieldset->fieldset_group($fsgroup->id);
1041 $fieldset->owner($e->requestor->id);
1042 $fieldset->owning_lib($e->requestor->ws_ou);
1043 $fieldset->status('PENDING');
1044 $fieldset->classname($htypes{$class});
1045 $fieldset->name($edit_name . ' batch group fieldset');
1046 $fieldset->stored_query($qtypes{$class});
1047 $fieldset = $e->create_action_fieldset($fieldset);
1049 my @keys = keys %$edits;
1050 $max = scalar(@keys);
1052 $client->respond({ ord => $stage, count=> $count, max => $max, stage => 'FIELDSET_EDITS_CREATE' });
1053 for my $key (@keys) {
1054 if ($self->{fields}) { # restrict edits to registered fields
1055 next unless (grep { $_ eq $key } @{$self->{fields}});
1057 my $fs_cv = Fieldmapper::action::fieldset_col_val->new;
1059 $fs_cv->fieldset($fieldset->id);
1061 $fs_cv->val($$edits{$key});
1062 $e->create_action_fieldset_col_val($fs_cv);
1064 $client->respond({ ord => $stage, count=> $count, max => $max, stage => 'FIELDSET_EDITS_CREATE' });
1067 $client->respond({ ord => ++$stage, stage => 'CONSTRUCT_QUERY' });
1068 my $qstore = OpenSRF::AppSession->connect('open-ils.qstore');
1069 my $prep = $qstore->request('open-ils.qstore.prepare', $fieldset->stored_query)->gather(1);
1070 my $token = $prep->{token};
1071 $qstore->request('open-ils.qstore.bind_param', $token, {bucket => $c_id})->gather(1);
1072 my $sql = $qstore->request('open-ils.qstore.sql', $token)->gather(1);
1073 $sql =~ s/\n\s*/ /g; # normalize the string
1074 $sql =~ s/;\s*//g; # kill trailing semicolon
1076 $client->respond({ ord => ++$stage, stage => 'APPLY_EDITS' });
1077 my $res = $e->json_query({
1078 from => ['action.apply_fieldset', $fieldset->id, $table{$class}, 'id', $sql]
1079 })->[0]->{'action.apply_fieldset'};
1082 $qstore->disconnect;
1084 return { fieldset_group => $fsgroup->id, stage => 'COMPLETE', error => $res };
1087 __PACKAGE__->register_method(
1088 method => "batch_edit",
1089 max_bundle_count => 1,
1090 api_name => "open-ils.actor.container.user.batch_edit",
1092 base_perm => { home_ou => 'UPDATE_USER' },
1095 my ($e, $group) = @_;
1096 my $g = $e->retrieve_permission_grp_tree($group);
1097 if (my $p = $g->application_perm()) {
1098 return $e->allowed($p);
1101 }, # code ref is run with params (editor,value), for both old and new value
1102 # home_ou => 'UPDATE_USER', # field -> perm means "test this perm with field as context OU", both old and new
1104 t => { BAR_PATRON => 'home_ou' },
1105 f => { UNBAR_PATRON => 'home_ou' }
1106 } # field -> struct means "if field getting value "key" check -> perm -> at context org, both old and new
1108 fields => [ qw/active profile juvenile home_ou expire_date barred net_access_level/ ],
1110 desc => 'Edits allowed fields on users in a bucket',
1112 { desc => 'Session key', type => 'string' },
1113 { desc => 'User container id', type => 'number' },
1114 { desc => 'Batch edit name', type => 'string' },
1115 { desc => 'Edit hash, key is column, value is new value to apply', type => 'hash' },
1118 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',
1124 __PACKAGE__->register_method(
1125 method => "batch_edit",
1126 api_name => "open-ils.actor.container.user.batch_delete",
1130 t => { 'DELETE_USER UPDATE_USER' => 'home_ou' },
1131 f => { 'UPDATE_USER' => 'home_ou' }
1134 fields => [ qw/deleted/ ],
1136 desc => 'Deletes users in a bucket',
1138 { desc => 'Session key', type => 'string' },
1139 { desc => 'User container id', type => 'number' },
1140 { desc => 'Batch delete name', type => 'string' },
1141 { desc => 'Edit delete, key is "deleted", value is new value to apply ("t")', type => 'hash' },
1145 desc => 'Object with the structure { fieldset_group => $id, stage => "COMPLETE", error => ("error string if any"|undef if none) }',