1 package OpenILS::Application::Actor;
2 use OpenILS::Application;
3 use base qw/OpenILS::Application/;
4 use strict; use warnings;
6 $Data::Dumper::Indent = 0;
9 use Digest::MD5 qw(md5_hex);
11 use OpenSRF::EX qw(:try);
14 use OpenILS::Application::AppUtils;
16 use OpenILS::Utils::Fieldmapper;
17 use OpenILS::Utils::ModsParser;
18 use OpenSRF::Utils::Logger qw/$logger/;
19 use OpenSRF::Utils qw/:datetime/;
20 use OpenSRF::Utils::SettingsClient;
22 use OpenSRF::Utils::Cache;
24 use OpenSRF::Utils::JSON;
26 use DateTime::Format::ISO8601;
27 use OpenILS::Const qw/:const/;
29 use OpenILS::Application::Actor::Container;
30 use OpenILS::Application::Actor::ClosedDates;
31 use OpenILS::Application::Actor::UserGroups;
32 use OpenILS::Application::Actor::Friends;
33 use OpenILS::Application::Actor::Stage;
35 use OpenILS::Utils::CStoreEditor qw/:funcs/;
36 use OpenILS::Utils::Penalty;
37 use OpenILS::Utils::BadContact;
38 use List::Util qw/max reduce/;
40 use UUID::Tiny qw/:std/;
43 OpenILS::Application::Actor::Container->initialize();
44 OpenILS::Application::Actor::UserGroups->initialize();
45 OpenILS::Application::Actor::ClosedDates->initialize();
48 my $apputils = "OpenILS::Application::AppUtils";
51 sub _d { warn "Patron:\n" . Dumper(shift()); }
54 my $set_user_settings;
58 #__PACKAGE__->register_method(
59 # method => "allowed_test",
60 # api_name => "open-ils.actor.allowed_test",
63 # my($self, $conn, $auth, $orgid, $permcode) = @_;
64 # my $e = new_editor(authtoken => $auth);
65 # return $e->die_event unless $e->checkauth;
69 # permcode => $permcode,
70 # result => $e->allowed($permcode, $orgid)
74 __PACKAGE__->register_method(
75 method => "update_user_setting",
76 api_name => "open-ils.actor.patron.settings.update",
78 sub update_user_setting {
79 my($self, $conn, $auth, $user_id, $settings) = @_;
80 my $e = new_editor(xact => 1, authtoken => $auth);
81 return $e->die_event unless $e->checkauth;
83 $user_id = $e->requestor->id unless defined $user_id;
85 unless($e->requestor->id == $user_id) {
86 my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
87 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
90 for my $name (keys %$settings) {
91 my $val = $$settings{$name};
92 my $set = $e->search_actor_user_setting({usr => $user_id, name => $name})->[0];
95 $val = OpenSRF::Utils::JSON->perl2JSON($val);
98 $e->update_actor_user_setting($set) or return $e->die_event;
100 $set = Fieldmapper::actor::user_setting->new;
104 $e->create_actor_user_setting($set) or return $e->die_event;
107 $e->delete_actor_user_setting($set) or return $e->die_event;
116 __PACKAGE__->register_method(
117 method => "set_ou_settings",
118 api_name => "open-ils.actor.org_unit.settings.update",
120 desc => "Updates the value for a given org unit setting. The permission to update " .
121 "an org unit setting is either the UPDATE_ORG_UNIT_SETTING_ALL, or a specific " .
122 "permission specified in the update_perm column of the config.org_unit_setting_type " .
123 "table's row corresponding to the setting being changed." ,
125 {desc => 'Authentication token', type => 'string'},
126 {desc => 'Org unit ID', type => 'number'},
127 {desc => 'Hash of setting name-value pairs', type => 'object'}
129 return => {desc => '1 on success, Event on error'}
133 sub set_ou_settings {
134 my( $self, $client, $auth, $org_id, $settings ) = @_;
136 my $e = new_editor(authtoken => $auth, xact => 1);
137 return $e->die_event unless $e->checkauth;
139 my $all_allowed = $e->allowed("UPDATE_ORG_UNIT_SETTING_ALL", $org_id);
141 for my $name (keys %$settings) {
142 my $val = $$settings{$name};
144 my $type = $e->retrieve_config_org_unit_setting_type([
146 {flesh => 1, flesh_fields => {'coust' => ['update_perm']}}
147 ]) or return $e->die_event;
148 my $set = $e->search_actor_org_unit_setting({org_unit => $org_id, name => $name})->[0];
150 # If there is no relevant permission, the default assumption will
151 # be, "no, the caller cannot change that value."
152 return $e->die_event unless ($all_allowed ||
153 ($type->update_perm && $e->allowed($type->update_perm->code, $org_id)));
156 $val = OpenSRF::Utils::JSON->perl2JSON($val);
159 $e->update_actor_org_unit_setting($set) or return $e->die_event;
161 $set = Fieldmapper::actor::org_unit_setting->new;
162 $set->org_unit($org_id);
165 $e->create_actor_org_unit_setting($set) or return $e->die_event;
168 $e->delete_actor_org_unit_setting($set) or return $e->die_event;
176 __PACKAGE__->register_method(
177 method => "user_settings",
179 api_name => "open-ils.actor.patron.settings.retrieve",
182 my( $self, $client, $auth, $user_id, $setting ) = @_;
184 my $e = new_editor(authtoken => $auth);
185 return $e->event unless $e->checkauth;
186 $user_id = $e->requestor->id unless defined $user_id;
188 my $patron = $e->retrieve_actor_user($user_id) or return $e->event;
189 if($e->requestor->id != $user_id) {
190 return $e->event unless $e->allowed('VIEW_USER', $patron->home_ou);
194 my($e, $user_id, $setting) = @_;
195 my $val = $e->search_actor_user_setting({usr => $user_id, name => $setting})->[0];
196 return undef unless $val; # XXX this should really return undef, but needs testing
197 return OpenSRF::Utils::JSON->JSON2perl($val->value);
201 if(ref $setting eq 'ARRAY') {
203 $settings{$_} = get_setting($e, $user_id, $_) for @$setting;
206 return get_setting($e, $user_id, $setting);
209 my $s = $e->search_actor_user_setting({usr => $user_id});
210 return { map { ( $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) ) } @$s };
215 __PACKAGE__->register_method(
216 method => "ranged_ou_settings",
217 api_name => "open-ils.actor.org_unit_setting.values.ranged.retrieve",
219 desc => "Retrieves all org unit settings for the given org_id, up to whatever limit " .
220 "is implied for retrieving OU settings by the authenticated users' permissions.",
222 {desc => 'Authentication token', type => 'string'},
223 {desc => 'Org unit ID', type => 'number'},
225 return => {desc => 'A hashref of "ranged" settings, event on error'}
228 sub ranged_ou_settings {
229 my( $self, $client, $auth, $org_id ) = @_;
231 my $e = new_editor(authtoken => $auth);
232 return $e->event unless $e->checkauth;
235 my $org_list = $U->get_org_ancestors($org_id);
236 my $settings = $e->search_actor_org_unit_setting({org_unit => $org_list});
237 $org_list = [ reverse @$org_list ];
239 # start at the context org and capture the setting value
240 # without clobbering settings we've already captured
241 for my $this_org_id (@$org_list) {
243 my @sets = grep { $_->org_unit == $this_org_id } @$settings;
245 for my $set (@sets) {
246 my $type = $e->retrieve_config_org_unit_setting_type([
248 {flesh => 1, flesh_fields => {coust => ['view_perm']}}
251 # If there is no relevant permission, the default assumption will
252 # be, "yes, the caller can have that value."
253 if ($type && $type->view_perm) {
254 next if not $e->allowed($type->view_perm->code, $org_id);
257 $ranged_settings{$set->name} = OpenSRF::Utils::JSON->JSON2perl($set->value)
258 unless defined $ranged_settings{$set->name};
262 return \%ranged_settings;
267 __PACKAGE__->register_method(
268 api_name => 'open-ils.actor.ou_setting.ancestor_default',
269 method => 'ou_ancestor_setting',
271 desc => 'Get the org unit setting value associated with the setting name as seen from the specified org unit. ' .
272 'This method will make sure that the given user has permission to view that setting, if there is a ' .
273 'permission associated with the setting. If a permission is required and no authtoken is given, or ' .
274 'the user lacks the permisssion, undef will be returned.' ,
276 { desc => 'Org unit ID', type => 'number' },
277 { desc => 'setting name', type => 'string' },
278 { desc => 'authtoken (optional)', type => 'string' }
280 return => {desc => 'A value for the org unit setting, or undef'}
284 # ------------------------------------------------------------------
285 # Attempts to find the org setting value for a given org. if not
286 # found at the requested org, searches up the org tree until it
287 # finds a parent that has the requested setting.
288 # when found, returns { org => $id, value => $value }
289 # otherwise, returns NULL
290 # ------------------------------------------------------------------
291 sub ou_ancestor_setting {
292 my( $self, $client, $orgid, $name, $auth ) = @_;
293 # Make sure $auth is set to something if not given.
295 return $U->ou_ancestor_setting($orgid, $name, undef, $auth);
298 __PACKAGE__->register_method(
299 api_name => 'open-ils.actor.ou_setting.ancestor_default.batch',
300 method => 'ou_ancestor_setting_batch',
302 desc => 'Get org unit setting name => value pairs for a list of names, as seen from the specified org unit. ' .
303 'This method will make sure that the given user has permission to view that setting, if there is a ' .
304 'permission associated with the setting. If a permission is required and no authtoken is given, or ' .
305 'the user lacks the permisssion, undef will be returned.' ,
307 { desc => 'Org unit ID', type => 'number' },
308 { desc => 'setting name list', type => 'array' },
309 { desc => 'authtoken (optional)', type => 'string' }
311 return => {desc => 'A hash with name => value pairs for the org unit settings'}
314 sub ou_ancestor_setting_batch {
315 my( $self, $client, $orgid, $name_list, $auth ) = @_;
317 # splitting the list of settings to fetch values
318 # so that ones that *don't* require view_perm checks
319 # can be fetched in one fell swoop, which is
320 # significantly faster in cases where a large
321 # number of settings need to be fetched.
322 my %perm_check_required = ();
323 my @perm_check_not_required = ();
325 # Note that ->ou_ancestor_setting also can check
326 # to see if the setting has a view_perm, but testing
327 # suggests that the redundant checks do not significantly
328 # increase the time it takes to fetch the values of
329 # permission-controlled settings.
330 my $e = new_editor();
331 my $res = $e->search_config_org_unit_setting_type({
333 view_perm => { "!=" => undef },
335 %perm_check_required = map { $_->name() => 1 } @$res;
336 foreach my $setting (@$name_list) {
337 push @perm_check_not_required, $setting
338 unless exists($perm_check_required{$setting});
342 if (@perm_check_not_required) {
343 %values = $U->ou_ancestor_setting_batch_insecure($orgid, \@perm_check_not_required);
345 $values{$_} = $U->ou_ancestor_setting(
348 ) for keys(%perm_check_required);
354 __PACKAGE__->register_method(
355 method => "update_patron",
356 api_name => "open-ils.actor.patron.update",
359 Update an existing user, or create a new one. Related objects,
360 like cards, addresses, survey responses, and stat cats,
361 can be updated by attaching them to the user object in their
362 respective fields. For examples, the billing address object
363 may be inserted into the 'billing_address' field, etc. For each
364 attached object, indicate if the object should be created,
365 updated, or deleted using the built-in 'isnew', 'ischanged',
366 and 'isdeleted' fields on the object.
369 { desc => 'Authentication token', type => 'string' },
370 { desc => 'Patron data object', type => 'object' }
372 return => {desc => 'A fleshed user object, event on error'}
377 my( $self, $client, $auth, $patron ) = @_;
379 my $e = new_editor(xact => 1, authtoken => $auth);
380 return $e->event unless $e->checkauth;
382 $logger->info($patron->isnew ? "Creating new patron..." :
383 "Updating Patron: " . $patron->id);
385 my $evt = check_group_perm($e, $e->requestor, $patron);
388 # $new_patron is the patron in progress. $patron is the original patron
389 # passed in with the method. new_patron will change as the components
390 # of patron are added/updated.
394 # unflesh the real items on the patron
395 $patron->card( $patron->card->id ) if(ref($patron->card));
396 $patron->billing_address( $patron->billing_address->id )
397 if(ref($patron->billing_address));
398 $patron->mailing_address( $patron->mailing_address->id )
399 if(ref($patron->mailing_address));
401 # create/update the patron first so we can use his id
403 # $patron is the obj from the client (new data) and $new_patron is the
404 # patron object properly built for db insertion, so we need a third variable
405 # if we want to represent the old patron.
408 my $barred_hook = '';
410 if($patron->isnew()) {
411 ( $new_patron, $evt ) = _add_patron($e, _clone_patron($patron));
413 if($U->is_true($patron->barred)) {
414 return $e->die_event unless
415 $e->allowed('BAR_PATRON', $patron->home_ou);
418 $new_patron = $patron;
420 # Did auth checking above already.
421 $old_patron = $e->retrieve_actor_user($patron->id) or
422 return $e->die_event;
424 if($U->is_true($old_patron->barred) != $U->is_true($new_patron->barred)) {
425 my $perm = $U->is_true($old_patron->barred) ? 'UNBAR_PATRON' : 'BAR_PATRON';
426 return $e->die_event unless $e->allowed($perm, $patron->home_ou);
428 $barred_hook = $U->is_true($new_patron->barred) ?
429 'au.barred' : 'au.unbarred';
432 # update the password by itself to avoid the password protection magic
433 if ($patron->passwd && $patron->passwd ne $old_patron->passwd) {
434 modify_migrated_user_password($e, $patron->id, $patron->passwd);
435 $new_patron->passwd(''); # subsequent update will set
436 # actor.usr.passwd to MD5('')
440 ( $new_patron, $evt ) = _add_update_addresses($e, $patron, $new_patron);
443 ( $new_patron, $evt ) = _add_update_cards($e, $patron, $new_patron);
446 ( $new_patron, $evt ) = _add_survey_responses($e, $patron, $new_patron);
449 # re-update the patron if anything has happened to him during this process
450 if($new_patron->ischanged()) {
451 ( $new_patron, $evt ) = _update_patron($e, $new_patron);
455 ( $new_patron, $evt ) = _clear_badcontact_penalties($e, $old_patron, $new_patron);
458 ($new_patron, $evt) = _create_stat_maps($e, $patron, $new_patron);
461 ($new_patron, $evt) = _create_perm_maps($e, $patron, $new_patron);
464 $evt = apply_invalid_addr_penalty($e, $patron);
469 my $tses = OpenSRF::AppSession->create('open-ils.trigger');
471 $tses->request('open-ils.trigger.event.autocreate',
472 'au.create', $new_patron, $new_patron->home_ou);
474 $tses->request('open-ils.trigger.event.autocreate',
475 'au.update', $new_patron, $new_patron->home_ou);
477 $tses->request('open-ils.trigger.event.autocreate', $barred_hook,
478 $new_patron, $new_patron->home_ou) if $barred_hook;
481 $e->xact_begin; # $e->rollback is called in new_flesh_user
482 return flesh_user($new_patron->id(), $e);
485 sub apply_invalid_addr_penalty {
489 # grab the invalid address penalty if set
490 my $penalties = OpenILS::Utils::Penalty->retrieve_usr_penalties($e, $patron->id, $patron->home_ou);
492 my ($addr_penalty) = grep
493 { $_->standing_penalty->name eq 'INVALID_PATRON_ADDRESS' } @$penalties;
495 # do we enforce invalid address penalty
496 my $enforce = $U->ou_ancestor_setting_value(
497 $patron->home_ou, 'circ.patron_invalid_address_apply_penalty') || 0;
499 my $addrs = $e->search_actor_user_address(
500 {usr => $patron->id, valid => 'f', id => {'>' => 0}}, {idlist => 1});
501 my $addr_count = scalar(@$addrs);
503 if($addr_count == 0 and $addr_penalty) {
505 # regardless of any settings, remove the penalty when the user has no invalid addresses
506 $e->delete_actor_user_standing_penalty($addr_penalty) or return $e->die_event;
509 } elsif($enforce and $addr_count > 0 and !$addr_penalty) {
511 my $ptype = $e->retrieve_config_standing_penalty(29) or return $e->die_event;
512 my $depth = $ptype->org_depth;
513 my $ctx_org = $U->org_unit_ancestor_at_depth($patron->home_ou, $depth) if defined $depth;
514 $ctx_org = $patron->home_ou unless defined $ctx_org;
516 my $penalty = Fieldmapper::actor::user_standing_penalty->new;
517 $penalty->usr($patron->id);
518 $penalty->org_unit($ctx_org);
519 $penalty->standing_penalty(OILS_PENALTY_INVALID_PATRON_ADDRESS);
521 $e->create_actor_user_standing_penalty($penalty) or return $e->die_event;
536 "standing_penalties",
545 push @$fields, "home_ou" if $home_ou;
546 return new_flesh_user($id, $fields, $e );
554 # clone and clear stuff that would break the database
558 my $new_patron = $patron->clone;
560 $new_patron->clear_billing_address();
561 $new_patron->clear_mailing_address();
562 $new_patron->clear_addresses();
563 $new_patron->clear_card();
564 $new_patron->clear_cards();
565 $new_patron->clear_id();
566 $new_patron->clear_isnew();
567 $new_patron->clear_ischanged();
568 $new_patron->clear_isdeleted();
569 $new_patron->clear_stat_cat_entries();
570 $new_patron->clear_permissions();
571 $new_patron->clear_standing_penalties();
582 return (undef, $e->die_event) unless
583 $e->allowed('CREATE_USER', $patron->home_ou);
585 my $ex = $e->search_actor_user(
586 {usrname => $patron->usrname}, {idlist => 1});
587 return (undef, OpenILS::Event->new('USERNAME_EXISTS')) if @$ex;
589 $logger->info("Creating new user in the DB with username: ".$patron->usrname());
591 # do a dance to get the password hashed securely
592 my $saved_password = $patron->passwd;
594 $e->create_actor_user($patron) or return (undef, $e->die_event);
595 modify_migrated_user_password($e, $patron->id, $saved_password);
597 my $id = $patron->id; # added by CStoreEditor
599 $logger->info("Successfully created new user [$id] in DB");
600 return ($e->retrieve_actor_user($id), undef);
604 sub check_group_perm {
605 my( $e, $requestor, $patron ) = @_;
608 # first let's see if the requestor has
609 # priveleges to update this user in any way
610 if( ! $patron->isnew ) {
611 my $p = $e->retrieve_actor_user($patron->id);
613 # If we are the requestor (trying to update our own account)
614 # and we are not trying to change our profile, we're good
615 if( $p->id == $requestor->id and
616 $p->profile == $patron->profile ) {
621 $evt = group_perm_failed($e, $requestor, $p);
625 # They are allowed to edit this patron.. can they put the
626 # patron into the group requested?
627 $evt = group_perm_failed($e, $requestor, $patron);
633 sub group_perm_failed {
634 my( $e, $requestor, $patron ) = @_;
638 my $grpid = $patron->profile;
642 $logger->debug("user update looking for group perm for group $grpid");
643 $grp = $e->retrieve_permission_grp_tree($grpid);
645 } while( !($perm = $grp->application_perm) and ($grpid = $grp->parent) );
647 $logger->info("user update checking perm $perm on user ".
648 $requestor->id." for update/create on user username=".$patron->usrname);
650 return $e->allowed($perm, $patron->home_ou) ? undef : $e->die_event;
656 my( $e, $patron, $noperm) = @_;
658 $logger->info("Updating patron ".$patron->id." in DB");
663 return (undef, $e->die_event)
664 unless $e->allowed('UPDATE_USER', $patron->home_ou);
667 if(!$patron->ident_type) {
668 $patron->clear_ident_type;
669 $patron->clear_ident_value;
672 $evt = verify_last_xact($e, $patron);
673 return (undef, $evt) if $evt;
675 $e->update_actor_user($patron) or return (undef, $e->die_event);
677 # re-fetch the user to pick up the latest last_xact_id value
678 # to avoid collisions.
679 $patron = $e->retrieve_actor_user($patron->id);
684 sub verify_last_xact {
685 my( $e, $patron ) = @_;
686 return undef unless $patron->id and $patron->id > 0;
687 my $p = $e->retrieve_actor_user($patron->id);
688 my $xact = $p->last_xact_id;
689 return undef unless $xact;
690 $logger->info("user xact = $xact, saving with xact " . $patron->last_xact_id);
691 return OpenILS::Event->new('XACT_COLLISION')
692 if $xact ne $patron->last_xact_id;
697 sub _check_dup_ident {
698 my( $session, $patron ) = @_;
700 return undef unless $patron->ident_value;
703 ident_type => $patron->ident_type,
704 ident_value => $patron->ident_value,
707 $logger->debug("patron update searching for dup ident values: " .
708 $patron->ident_type . ':' . $patron->ident_value);
710 $search->{id} = {'!=' => $patron->id} if $patron->id and $patron->id > 0;
712 my $dups = $session->request(
713 'open-ils.storage.direct.actor.user.search_where.atomic', $search )->gather(1);
716 return OpenILS::Event->new('PATRON_DUP_IDENT1', payload => $patron )
723 sub _add_update_addresses {
727 my $new_patron = shift;
731 my $current_id; # id of the address before creation
733 my $addresses = $patron->addresses();
735 for my $address (@$addresses) {
737 next unless ref $address;
738 $current_id = $address->id();
740 if( $patron->billing_address() and
741 $patron->billing_address() == $current_id ) {
742 $logger->info("setting billing addr to $current_id");
743 $new_patron->billing_address($address->id());
744 $new_patron->ischanged(1);
747 if( $patron->mailing_address() and
748 $patron->mailing_address() == $current_id ) {
749 $new_patron->mailing_address($address->id());
750 $logger->info("setting mailing addr to $current_id");
751 $new_patron->ischanged(1);
755 if($address->isnew()) {
757 $address->usr($new_patron->id());
759 ($address, $evt) = _add_address($e,$address);
760 return (undef, $evt) if $evt;
762 # we need to get the new id
763 if( $patron->billing_address() and
764 $patron->billing_address() == $current_id ) {
765 $new_patron->billing_address($address->id());
766 $logger->info("setting billing addr to $current_id");
767 $new_patron->ischanged(1);
770 if( $patron->mailing_address() and
771 $patron->mailing_address() == $current_id ) {
772 $new_patron->mailing_address($address->id());
773 $logger->info("setting mailing addr to $current_id");
774 $new_patron->ischanged(1);
777 } elsif($address->ischanged() ) {
779 ($address, $evt) = _update_address($e, $address);
780 return (undef, $evt) if $evt;
782 } elsif($address->isdeleted() ) {
784 if( $address->id() == $new_patron->mailing_address() ) {
785 $new_patron->clear_mailing_address();
786 ($new_patron, $evt) = _update_patron($e, $new_patron);
787 return (undef, $evt) if $evt;
790 if( $address->id() == $new_patron->billing_address() ) {
791 $new_patron->clear_billing_address();
792 ($new_patron, $evt) = _update_patron($e, $new_patron);
793 return (undef, $evt) if $evt;
796 $evt = _delete_address($e, $address);
797 return (undef, $evt) if $evt;
801 return ( $new_patron, undef );
805 # adds an address to the db and returns the address with new id
807 my($e, $address) = @_;
808 $address->clear_id();
810 $logger->info("Creating new address at street ".$address->street1);
812 # put the address into the database
813 $e->create_actor_user_address($address) or return (undef, $e->die_event);
814 return ($address, undef);
818 sub _update_address {
819 my( $e, $address ) = @_;
821 $logger->info("Updating address ".$address->id." in the DB");
823 $e->update_actor_user_address($address) or return (undef, $e->die_event);
825 return ($address, undef);
830 sub _add_update_cards {
834 my $new_patron = shift;
838 my $virtual_id; #id of the card before creation
840 my $cards = $patron->cards();
841 for my $card (@$cards) {
843 $card->usr($new_patron->id());
845 if(ref($card) and $card->isnew()) {
847 $virtual_id = $card->id();
848 ( $card, $evt ) = _add_card($e, $card);
849 return (undef, $evt) if $evt;
851 #if(ref($patron->card)) { $patron->card($patron->card->id); }
852 if($patron->card() == $virtual_id) {
853 $new_patron->card($card->id());
854 $new_patron->ischanged(1);
857 } elsif( ref($card) and $card->ischanged() ) {
858 $evt = _update_card($e, $card);
859 return (undef, $evt) if $evt;
863 return ( $new_patron, undef );
867 # adds an card to the db and returns the card with new id
869 my( $e, $card ) = @_;
872 $logger->info("Adding new patron card ".$card->barcode);
874 $e->create_actor_card($card) or return (undef, $e->die_event);
876 return ( $card, undef );
880 # returns event on error. returns undef otherwise
882 my( $e, $card ) = @_;
883 $logger->info("Updating patron card ".$card->id);
885 $e->update_actor_card($card) or return $e->die_event;
892 # returns event on error. returns undef otherwise
893 sub _delete_address {
894 my( $e, $address ) = @_;
896 $logger->info("Deleting address ".$address->id." from DB");
898 $e->delete_actor_user_address($address) or return $e->die_event;
904 sub _add_survey_responses {
905 my ($e, $patron, $new_patron) = @_;
907 $logger->info( "Updating survey responses for patron ".$new_patron->id );
909 my $responses = $patron->survey_responses;
913 $_->usr($new_patron->id) for (@$responses);
915 my $evt = $U->simplereq( "open-ils.circ",
916 "open-ils.circ.survey.submit.user_id", $responses );
918 return (undef, $evt) if defined($U->event_code($evt));
922 return ( $new_patron, undef );
925 sub _clear_badcontact_penalties {
926 my ($e, $old_patron, $new_patron) = @_;
928 return ($new_patron, undef) unless $old_patron;
930 my $PNM = $OpenILS::Utils::BadContact::PENALTY_NAME_MAP;
932 # This ignores whether the caller of update_patron has any permission
933 # to remove penalties, but these penalties no longer make sense
934 # if an email address field (for example) is changed (and the caller must
935 # have perms to do *that*) so there's no reason not to clear the penalties.
937 my $bad_contact_penalties = $e->search_actor_user_standing_penalty([
939 "+csp" => {"name" => [values(%$PNM)]},
940 "+ausp" => {"stop_date" => undef, "usr" => $new_patron->id}
942 "join" => {"csp" => {}},
944 "flesh_fields" => {"ausp" => ["standing_penalty"]}
946 ]) or return (undef, $e->die_event);
948 return ($new_patron, undef) unless @$bad_contact_penalties;
950 my @penalties_to_clear;
951 my ($field, $penalty_name);
953 # For each field that might have an associated bad contact penalty,
954 # check for such penalties and add them to the to-clear list if that
956 while (($field, $penalty_name) = each(%$PNM)) {
957 if ($old_patron->$field ne $new_patron->$field) {
958 push @penalties_to_clear, grep {
959 $_->standing_penalty->name eq $penalty_name
960 } @$bad_contact_penalties;
964 foreach (@penalties_to_clear) {
965 # Note that this "archives" penalties, in the terminology of the staff
966 # client, instead of just deleting them. This may assist reporting,
967 # or preserving old contact information when it is still potentially
969 $_->standing_penalty($_->standing_penalty->id); # deflesh
970 $_->stop_date('now');
971 $e->update_actor_user_standing_penalty($_) or return (undef, $e->die_event);
974 return ($new_patron, undef);
978 sub _create_stat_maps {
980 my($e, $patron, $new_patron) = @_;
982 my $maps = $patron->stat_cat_entries();
984 for my $map (@$maps) {
986 my $method = "update_actor_stat_cat_entry_user_map";
988 if ($map->isdeleted()) {
989 $method = "delete_actor_stat_cat_entry_user_map";
991 } elsif ($map->isnew()) {
992 $method = "create_actor_stat_cat_entry_user_map";
997 $map->target_usr($new_patron->id);
999 $logger->info("Updating stat entry with method $method and map $map");
1001 $e->$method($map) or return (undef, $e->die_event);
1004 return ($new_patron, undef);
1007 sub _create_perm_maps {
1009 my($e, $patron, $new_patron) = @_;
1011 my $maps = $patron->permissions;
1013 for my $map (@$maps) {
1015 my $method = "update_permission_usr_perm_map";
1016 if ($map->isdeleted()) {
1017 $method = "delete_permission_usr_perm_map";
1018 } elsif ($map->isnew()) {
1019 $method = "create_permission_usr_perm_map";
1023 $map->usr($new_patron->id);
1025 $logger->info( "Updating permissions with method $method and map $map" );
1027 $e->$method($map) or return (undef, $e->die_event);
1030 return ($new_patron, undef);
1034 __PACKAGE__->register_method(
1035 method => "set_user_work_ous",
1036 api_name => "open-ils.actor.user.work_ous.update",
1039 sub set_user_work_ous {
1045 my( $requestor, $evt ) = $apputils->checksesperm( $ses, 'ASSIGN_WORK_ORG_UNIT' );
1046 return $evt if $evt;
1048 my $session = $apputils->start_db_session();
1049 $apputils->set_audit_info($session, $ses, $requestor->id, $requestor->wsid);
1051 for my $map (@$maps) {
1053 my $method = "open-ils.storage.direct.permission.usr_work_ou_map.update";
1054 if ($map->isdeleted()) {
1055 $method = "open-ils.storage.direct.permission.usr_work_ou_map.delete";
1056 } elsif ($map->isnew()) {
1057 $method = "open-ils.storage.direct.permission.usr_work_ou_map.create";
1061 #warn( "Updating permissions with method $method and session $ses and map $map" );
1062 $logger->info( "Updating work_ou map with method $method and map $map" );
1064 my $stat = $session->request($method, $map)->gather(1);
1065 $logger->warn( "update failed: ".$U->DB_UPDATE_FAILED($map) ) unless defined($stat);
1069 $apputils->commit_db_session($session);
1071 return scalar(@$maps);
1075 __PACKAGE__->register_method(
1076 method => "set_user_perms",
1077 api_name => "open-ils.actor.user.permissions.update",
1080 sub set_user_perms {
1086 my $session = $apputils->start_db_session();
1088 my( $user_obj, $evt ) = $U->checkses($ses);
1089 return $evt if $evt;
1090 $apputils->set_audit_info($session, $ses, $user_obj->id, $user_obj->wsid);
1092 my $perms = $session->request('open-ils.storage.permission.user_perms.atomic', $user_obj->id)->gather(1);
1095 $all = 1 if ($U->is_true($user_obj->super_user()));
1096 $all = 1 unless ($U->check_perms($user_obj->id, $user_obj->home_ou, 'EVERYTHING'));
1098 for my $map (@$maps) {
1100 my $method = "open-ils.storage.direct.permission.usr_perm_map.update";
1101 if ($map->isdeleted()) {
1102 $method = "open-ils.storage.direct.permission.usr_perm_map.delete";
1103 } elsif ($map->isnew()) {
1104 $method = "open-ils.storage.direct.permission.usr_perm_map.create";
1108 next if (!$all and !grep { $_->perm eq $map->perm and $U->is_true($_->grantable) and $_->depth <= $map->depth } @$perms);
1109 #warn( "Updating permissions with method $method and session $ses and map $map" );
1110 $logger->info( "Updating permissions with method $method and map $map" );
1112 my $stat = $session->request($method, $map)->gather(1);
1113 $logger->warn( "update failed: ".$U->DB_UPDATE_FAILED($map) ) unless defined($stat);
1117 $apputils->commit_db_session($session);
1119 return scalar(@$maps);
1123 __PACKAGE__->register_method(
1124 method => "user_retrieve_by_barcode",
1126 api_name => "open-ils.actor.user.fleshed.retrieve_by_barcode",);
1128 sub user_retrieve_by_barcode {
1129 my($self, $client, $auth, $barcode, $flesh_home_ou) = @_;
1131 my $e = new_editor(authtoken => $auth);
1132 return $e->event unless $e->checkauth;
1134 my $card = $e->search_actor_card({barcode => $barcode})->[0]
1135 or return $e->event;
1137 my $user = flesh_user($card->usr, $e, $flesh_home_ou);
1138 return $e->event unless $e->allowed(
1139 "VIEW_USER", $flesh_home_ou ? $user->home_ou->id : $user->home_ou
1146 __PACKAGE__->register_method(
1147 method => "get_user_by_id",
1149 api_name => "open-ils.actor.user.retrieve",
1152 sub get_user_by_id {
1153 my ($self, $client, $auth, $id) = @_;
1154 my $e = new_editor(authtoken=>$auth);
1155 return $e->event unless $e->checkauth;
1156 my $user = $e->retrieve_actor_user($id) or return $e->event;
1157 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
1162 __PACKAGE__->register_method(
1163 method => "get_org_types",
1164 api_name => "open-ils.actor.org_types.retrieve",
1167 return $U->get_org_types();
1171 __PACKAGE__->register_method(
1172 method => "get_user_ident_types",
1173 api_name => "open-ils.actor.user.ident_types.retrieve",
1176 sub get_user_ident_types {
1177 return $ident_types if $ident_types;
1178 return $ident_types =
1179 new_editor()->retrieve_all_config_identification_type();
1183 __PACKAGE__->register_method(
1184 method => "get_org_unit",
1185 api_name => "open-ils.actor.org_unit.retrieve",
1189 my( $self, $client, $user_session, $org_id ) = @_;
1190 my $e = new_editor(authtoken => $user_session);
1192 return $e->event unless $e->checkauth;
1193 $org_id = $e->requestor->ws_ou;
1195 my $o = $e->retrieve_actor_org_unit($org_id)
1196 or return $e->event;
1200 __PACKAGE__->register_method(
1201 method => "search_org_unit",
1202 api_name => "open-ils.actor.org_unit_list.search",
1205 sub search_org_unit {
1207 my( $self, $client, $field, $value ) = @_;
1209 my $list = OpenILS::Application::AppUtils->simple_scalar_request(
1211 "open-ils.cstore.direct.actor.org_unit.search.atomic",
1212 { $field => $value } );
1218 # build the org tree
1220 __PACKAGE__->register_method(
1221 method => "get_org_tree",
1222 api_name => "open-ils.actor.org_tree.retrieve",
1224 note => "Returns the entire org tree structure",
1230 return $U->get_org_tree($client->session->session_locale);
1234 __PACKAGE__->register_method(
1235 method => "get_org_descendants",
1236 api_name => "open-ils.actor.org_tree.descendants.retrieve"
1239 # depth is optional. org_unit is the id
1240 sub get_org_descendants {
1241 my( $self, $client, $org_unit, $depth ) = @_;
1243 if(ref $org_unit eq 'ARRAY') {
1246 for my $i (0..scalar(@$org_unit)-1) {
1247 my $list = $U->simple_scalar_request(
1249 "open-ils.storage.actor.org_unit.descendants.atomic",
1250 $org_unit->[$i], $depth->[$i] );
1251 push(@trees, $U->build_org_tree($list));
1256 my $orglist = $apputils->simple_scalar_request(
1258 "open-ils.storage.actor.org_unit.descendants.atomic",
1259 $org_unit, $depth );
1260 return $U->build_org_tree($orglist);
1265 __PACKAGE__->register_method(
1266 method => "get_org_ancestors",
1267 api_name => "open-ils.actor.org_tree.ancestors.retrieve"
1270 # depth is optional. org_unit is the id
1271 sub get_org_ancestors {
1272 my( $self, $client, $org_unit, $depth ) = @_;
1273 my $orglist = $apputils->simple_scalar_request(
1275 "open-ils.storage.actor.org_unit.ancestors.atomic",
1276 $org_unit, $depth );
1277 return $U->build_org_tree($orglist);
1281 __PACKAGE__->register_method(
1282 method => "get_standings",
1283 api_name => "open-ils.actor.standings.retrieve"
1288 return $user_standings if $user_standings;
1289 return $user_standings =
1290 $apputils->simple_scalar_request(
1292 "open-ils.cstore.direct.config.standing.search.atomic",
1293 { id => { "!=" => undef } }
1298 __PACKAGE__->register_method(
1299 method => "get_my_org_path",
1300 api_name => "open-ils.actor.org_unit.full_path.retrieve"
1303 sub get_my_org_path {
1304 my( $self, $client, $auth, $org_id ) = @_;
1305 my $e = new_editor(authtoken=>$auth);
1306 return $e->event unless $e->checkauth;
1307 $org_id = $e->requestor->ws_ou unless defined $org_id;
1309 return $apputils->simple_scalar_request(
1311 "open-ils.storage.actor.org_unit.full_path.atomic",
1316 __PACKAGE__->register_method(
1317 method => "patron_adv_search",
1318 api_name => "open-ils.actor.patron.search.advanced"
1321 __PACKAGE__->register_method(
1322 method => "patron_adv_search",
1323 api_name => "open-ils.actor.patron.search.advanced.fleshed",
1325 # TODO: change when opensrf 'bundling' is merged.
1326 # set a relatively small bundle size so the caller can start
1327 # seeing results fairly quickly
1328 max_chunk_size => 4096, # bundling
1331 # pending opensrf work -- also, not sure if needed since we're not
1332 # actaully creating an alternate vesrion, only offering to return a
1336 desc => q/Returns a stream of fleshed user objects instead of
1337 a pile of identifiers/
1341 sub patron_adv_search {
1342 my( $self, $client, $auth, $search_hash, $search_limit,
1343 $search_sort, $include_inactive, $search_ou, $flesh_fields, $offset) = @_;
1345 # API params sanity checks.
1346 # Exit early with empty result if no filter exists.
1347 # .fleshed call is streaming. Non-fleshed is effectively atomic.
1348 my $fleshed = ($self->api_name =~ /fleshed/);
1349 return ($fleshed ? undef : []) unless (ref $search_hash ||'') eq 'HASH';
1351 for my $key (keys %$search_hash) {
1352 next if $search_hash->{$key}{value} =~ /^\s*$/; # empty filter
1356 return ($fleshed ? undef : []) unless $search_ok;
1358 my $e = new_editor(authtoken=>$auth);
1359 return $e->event unless $e->checkauth;
1360 return $e->event unless $e->allowed('VIEW_USER');
1362 # depth boundary outside of which patrons must opt-in, default to 0
1363 my $opt_boundary = 0;
1364 $opt_boundary = $U->ou_ancestor_setting_value($e->requestor->ws_ou,'org.patron_opt_boundary') if user_opt_in_enabled($self);
1366 if (not defined $search_ou) {
1367 my $depth = $U->ou_ancestor_setting_value(
1368 $e->requestor->ws_ou,
1369 'circ.patron_edit.duplicate_patron_check_depth'
1372 if (defined $depth) {
1373 $search_ou = $U->org_unit_ancestor_at_depth(
1374 $e->requestor->ws_ou, $depth
1379 my $ids = $U->storagereq(
1380 "open-ils.storage.actor.user.crazy_search", $search_hash,
1381 $search_limit, $search_sort, $include_inactive,
1382 $e->requestor->ws_ou, $search_ou, $opt_boundary, $offset);
1384 return $ids unless $self->api_name =~ /fleshed/;
1386 $client->respond(new_flesh_user($_, $flesh_fields, $e)) for @$ids;
1392 # A migrated (main) password has the form:
1393 # CRYPT( MD5( pw_salt || MD5(real_password) ), pw_salt )
1394 sub modify_migrated_user_password {
1395 my ($e, $user_id, $passwd) = @_;
1397 # new password gets a new salt
1398 my $new_salt = $e->json_query({
1399 from => ['actor.create_salt', 'main']})->[0];
1400 $new_salt = $new_salt->{'actor.create_salt'};
1407 md5_hex($new_salt . md5_hex($passwd)),
1415 __PACKAGE__->register_method(
1416 method => "update_passwd",
1417 api_name => "open-ils.actor.user.password.update",
1419 desc => "Update the operator's password",
1421 { desc => 'Authentication token', type => 'string' },
1422 { desc => 'New password', type => 'string' },
1423 { desc => 'Current password', type => 'string' }
1425 return => {desc => '1 on success, Event on error or incorrect current password'}
1429 __PACKAGE__->register_method(
1430 method => "update_passwd",
1431 api_name => "open-ils.actor.user.username.update",
1433 desc => "Update the operator's username",
1435 { desc => 'Authentication token', type => 'string' },
1436 { desc => 'New username', type => 'string' },
1437 { desc => 'Current password', type => 'string' }
1439 return => {desc => '1 on success, Event on error or incorrect current password'}
1443 __PACKAGE__->register_method(
1444 method => "update_passwd",
1445 api_name => "open-ils.actor.user.email.update",
1447 desc => "Update the operator's email address",
1449 { desc => 'Authentication token', type => 'string' },
1450 { desc => 'New email address', type => 'string' },
1451 { desc => 'Current password', type => 'string' }
1453 return => {desc => '1 on success, Event on error or incorrect current password'}
1458 my( $self, $conn, $auth, $new_val, $orig_pw ) = @_;
1459 my $e = new_editor(xact=>1, authtoken=>$auth);
1460 return $e->die_event unless $e->checkauth;
1462 my $db_user = $e->retrieve_actor_user($e->requestor->id)
1463 or return $e->die_event;
1464 my $api = $self->api_name;
1466 if (!$U->verify_migrated_user_password($e, $db_user->id, $orig_pw)) {
1468 return new OpenILS::Event('INCORRECT_PASSWORD');
1471 if( $api =~ /password/o ) {
1472 # NOTE: with access to the plain text password we could crypt
1473 # the password without the extra MD5 pre-hashing. Other changes
1474 # would be required. Noting here for future reference.
1475 modify_migrated_user_password($e, $db_user->id, $new_val);
1476 $db_user->passwd('');
1480 # if we don't clear the password, the user will be updated with
1481 # a hashed version of the hashed version of their password
1482 $db_user->clear_passwd;
1484 if( $api =~ /username/o ) {
1486 # make sure no one else has this username
1487 my $exist = $e->search_actor_user({usrname=>$new_val},{idlist=>1});
1490 return new OpenILS::Event('USERNAME_EXISTS');
1492 $db_user->usrname($new_val);
1494 } elsif( $api =~ /email/o ) {
1495 $db_user->email($new_val);
1499 $e->update_actor_user($db_user) or return $e->die_event;
1502 # update the cached user to pick up these changes
1503 $U->simplereq('open-ils.auth', 'open-ils.auth.session.reset_timeout', $auth, 1);
1509 __PACKAGE__->register_method(
1510 method => "check_user_perms",
1511 api_name => "open-ils.actor.user.perm.check",
1512 notes => <<" NOTES");
1513 Takes a login session, user id, an org id, and an array of perm type strings. For each
1514 perm type, if the user does *not* have the given permission it is added
1515 to a list which is returned from the method. If all permissions
1516 are allowed, an empty list is returned
1517 if the logged in user does not match 'user_id', then the logged in user must
1518 have VIEW_PERMISSION priveleges.
1521 sub check_user_perms {
1522 my( $self, $client, $login_session, $user_id, $org_id, $perm_types ) = @_;
1524 my( $staff, $evt ) = $apputils->checkses($login_session);
1525 return $evt if $evt;
1527 if($staff->id ne $user_id) {
1528 if( $evt = $apputils->check_perms(
1529 $staff->id, $org_id, 'VIEW_PERMISSION') ) {
1535 for my $perm (@$perm_types) {
1536 if($apputils->check_perms($user_id, $org_id, $perm)) {
1537 push @not_allowed, $perm;
1541 return \@not_allowed
1544 __PACKAGE__->register_method(
1545 method => "check_user_perms2",
1546 api_name => "open-ils.actor.user.perm.check.multi_org",
1548 Checks the permissions on a list of perms and orgs for a user
1549 @param authtoken The login session key
1550 @param user_id The id of the user to check
1551 @param orgs The array of org ids
1552 @param perms The array of permission names
1553 @return An array of [ orgId, permissionName ] arrays that FAILED the check
1554 if the logged in user does not match 'user_id', then the logged in user must
1555 have VIEW_PERMISSION priveleges.
1558 sub check_user_perms2 {
1559 my( $self, $client, $authtoken, $user_id, $orgs, $perms ) = @_;
1561 my( $staff, $target, $evt ) = $apputils->checkses_requestor(
1562 $authtoken, $user_id, 'VIEW_PERMISSION' );
1563 return $evt if $evt;
1566 for my $org (@$orgs) {
1567 for my $perm (@$perms) {
1568 if($apputils->check_perms($user_id, $org, $perm)) {
1569 push @not_allowed, [ $org, $perm ];
1574 return \@not_allowed
1578 __PACKAGE__->register_method(
1579 method => 'check_user_perms3',
1580 api_name => 'open-ils.actor.user.perm.highest_org',
1582 Returns the highest org unit id at which a user has a given permission
1583 If the requestor does not match the target user, the requestor must have
1584 'VIEW_PERMISSION' rights at the home org unit of the target user
1585 @param authtoken The login session key
1586 @param userid The id of the user in question
1587 @param perm The permission to check
1588 @return The org unit highest in the org tree within which the user has
1589 the requested permission
1592 sub check_user_perms3 {
1593 my($self, $client, $authtoken, $user_id, $perm) = @_;
1594 my $e = new_editor(authtoken=>$authtoken);
1595 return $e->event unless $e->checkauth;
1597 my $tree = $U->get_org_tree();
1599 unless($e->requestor->id == $user_id) {
1600 my $user = $e->retrieve_actor_user($user_id)
1601 or return $e->event;
1602 return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1603 return $U->find_highest_perm_org($perm, $user_id, $user->home_ou, $tree );
1606 return $U->find_highest_perm_org($perm, $user_id, $e->requestor->ws_ou, $tree);
1609 __PACKAGE__->register_method(
1610 method => 'user_has_work_perm_at',
1611 api_name => 'open-ils.actor.user.has_work_perm_at',
1615 Returns a set of org unit IDs which represent the highest orgs in
1616 the org tree where the user has the requested permission. The
1617 purpose of this method is to return the smallest set of org units
1618 which represent the full expanse of the user's ability to perform
1619 the requested action. The user whose perms this method should
1620 check is implied by the authtoken. /,
1622 {desc => 'authtoken', type => 'string'},
1623 {desc => 'permission name', type => 'string'},
1624 {desc => q/user id, optional. If present, check perms for
1625 this user instead of the logged in user/, type => 'number'},
1627 return => {desc => 'An array of org IDs'}
1631 sub user_has_work_perm_at {
1632 my($self, $conn, $auth, $perm, $user_id) = @_;
1633 my $e = new_editor(authtoken=>$auth);
1634 return $e->event unless $e->checkauth;
1635 if(defined $user_id) {
1636 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1637 return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1639 return $U->user_has_work_perm_at($e, $perm, undef, $user_id);
1642 __PACKAGE__->register_method(
1643 method => 'user_has_work_perm_at_batch',
1644 api_name => 'open-ils.actor.user.has_work_perm_at.batch',
1648 sub user_has_work_perm_at_batch {
1649 my($self, $conn, $auth, $perms, $user_id) = @_;
1650 my $e = new_editor(authtoken=>$auth);
1651 return $e->event unless $e->checkauth;
1652 if(defined $user_id) {
1653 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1654 return $e->event unless $e->allowed('VIEW_PERMISSION', $user->home_ou);
1657 $map->{$_} = $U->user_has_work_perm_at($e, $_) for @$perms;
1663 __PACKAGE__->register_method(
1664 method => 'check_user_perms4',
1665 api_name => 'open-ils.actor.user.perm.highest_org.batch',
1667 Returns the highest org unit id at which a user has a given permission
1668 If the requestor does not match the target user, the requestor must have
1669 'VIEW_PERMISSION' rights at the home org unit of the target user
1670 @param authtoken The login session key
1671 @param userid The id of the user in question
1672 @param perms An array of perm names to check
1673 @return An array of orgId's representing the org unit
1674 highest in the org tree within which the user has the requested permission
1675 The arrah of orgId's has matches the order of the perms array
1678 sub check_user_perms4 {
1679 my( $self, $client, $authtoken, $userid, $perms ) = @_;
1681 my( $staff, $target, $org, $evt );
1683 ( $staff, $target, $evt ) = $apputils->checkses_requestor(
1684 $authtoken, $userid, 'VIEW_PERMISSION' );
1685 return $evt if $evt;
1688 return [] unless ref($perms);
1689 my $tree = $U->get_org_tree();
1691 for my $p (@$perms) {
1692 push( @arr, $U->find_highest_perm_org( $p, $userid, $target->home_ou, $tree ) );
1698 __PACKAGE__->register_method(
1699 method => "user_fines_summary",
1700 api_name => "open-ils.actor.user.fines.summary",
1703 desc => 'Returns a short summary of the users total open fines, ' .
1704 'excluding voided fines Params are login_session, user_id' ,
1706 {desc => 'Authentication token', type => 'string'},
1707 {desc => 'User ID', type => 'string'} # number?
1710 desc => "a 'mous' object, event on error",
1715 sub user_fines_summary {
1716 my( $self, $client, $auth, $user_id ) = @_;
1718 my $e = new_editor(authtoken=>$auth);
1719 return $e->event unless $e->checkauth;
1721 if( $user_id ne $e->requestor->id ) {
1722 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1723 return $e->event unless
1724 $e->allowed('VIEW_USER_FINES_SUMMARY', $user->home_ou);
1727 return $e->search_money_open_user_summary({usr => $user_id})->[0];
1731 __PACKAGE__->register_method(
1732 method => "user_opac_vitals",
1733 api_name => "open-ils.actor.user.opac.vital_stats",
1737 desc => 'Returns a short summary of the users vital stats, including ' .
1738 'identification information, accumulated balance, number of holds, ' .
1739 'and current open circulation stats' ,
1741 {desc => 'Authentication token', type => 'string'},
1742 {desc => 'Optional User ID, for use in the staff client', type => 'number'} # number?
1745 desc => "An object with four properties: user, fines, checkouts and holds."
1750 sub user_opac_vitals {
1751 my( $self, $client, $auth, $user_id ) = @_;
1753 my $e = new_editor(authtoken=>$auth);
1754 return $e->event unless $e->checkauth;
1756 $user_id ||= $e->requestor->id;
1758 my $user = $e->retrieve_actor_user( $user_id );
1761 ->method_lookup('open-ils.actor.user.fines.summary')
1762 ->run($auth => $user_id);
1763 return $fines if (defined($U->event_code($fines)));
1766 $fines = new Fieldmapper::money::open_user_summary ();
1767 $fines->balance_owed(0.00);
1768 $fines->total_owed(0.00);
1769 $fines->total_paid(0.00);
1770 $fines->usr($user_id);
1774 ->method_lookup('open-ils.actor.user.hold_requests.count')
1775 ->run($auth => $user_id);
1776 return $holds if (defined($U->event_code($holds)));
1779 ->method_lookup('open-ils.actor.user.checked_out.count')
1780 ->run($auth => $user_id);
1781 return $out if (defined($U->event_code($out)));
1783 $out->{"total_out"} = reduce { $a + $out->{$b} } 0, qw/out overdue/;
1785 my $unread_msgs = $e->search_actor_usr_message([
1786 {usr => $user_id, read_date => undef, deleted => 'f'},
1792 first_given_name => $user->first_given_name,
1793 second_given_name => $user->second_given_name,
1794 family_name => $user->family_name,
1795 alias => $user->alias,
1796 usrname => $user->usrname
1798 fines => $fines->to_bare_hash,
1801 messages => { unread => scalar(@$unread_msgs) }
1806 ##### a small consolidation of related method registrations
1807 my $common_params = [
1808 { desc => 'Authentication token', type => 'string' },
1809 { desc => 'User ID', type => 'string' },
1810 { desc => 'Transactions type (optional, defaults to all)', type => 'string' },
1811 { desc => 'Options hash. May contain limit and offset for paged results.', type => 'object' },
1814 'open-ils.actor.user.transactions' => '',
1815 'open-ils.actor.user.transactions.fleshed' => '',
1816 'open-ils.actor.user.transactions.have_charge' => ' that have an initial charge',
1817 'open-ils.actor.user.transactions.have_charge.fleshed' => ' that have an initial charge',
1818 'open-ils.actor.user.transactions.have_balance' => ' that have an outstanding balance',
1819 'open-ils.actor.user.transactions.have_balance.fleshed' => ' that have an outstanding balance',
1822 foreach (keys %methods) {
1824 method => "user_transactions",
1827 desc => 'For a given user, retrieve a list of '
1828 . (/\.fleshed/ ? 'fleshed ' : '')
1829 . 'transactions' . $methods{$_}
1830 . ' optionally limited to transactions of a given type.',
1831 params => $common_params,
1833 desc => "List of objects, or event on error. Each object is a hash containing: transaction, circ, record. "
1834 . 'These represent the relevant (mbts) transaction, attached circulation and title pointed to in the circ, respectively.',
1838 $args{authoritative} = 1;
1839 __PACKAGE__->register_method(%args);
1842 # Now for the counts
1844 'open-ils.actor.user.transactions.count' => '',
1845 'open-ils.actor.user.transactions.have_charge.count' => ' that have an initial charge',
1846 'open-ils.actor.user.transactions.have_balance.count' => ' that have an outstanding balance',
1849 foreach (keys %methods) {
1851 method => "user_transactions",
1854 desc => 'For a given user, retrieve a count of open '
1855 . 'transactions' . $methods{$_}
1856 . ' optionally limited to transactions of a given type.',
1857 params => $common_params,
1858 return => { desc => "Integer count of transactions, or event on error" }
1861 /\.have_balance/ and $args{authoritative} = 1; # FIXME: I don't know why have_charge isn't authoritative
1862 __PACKAGE__->register_method(%args);
1865 __PACKAGE__->register_method(
1866 method => "user_transactions",
1867 api_name => "open-ils.actor.user.transactions.have_balance.total",
1870 desc => 'For a given user, retrieve the total balance owed for open transactions,'
1871 . ' optionally limited to transactions of a given type.',
1872 params => $common_params,
1873 return => { desc => "Decimal balance value, or event on error" }
1878 sub user_transactions {
1879 my( $self, $client, $auth, $user_id, $type, $options ) = @_;
1882 my $e = new_editor(authtoken => $auth);
1883 return $e->event unless $e->checkauth;
1885 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
1887 return $e->event unless
1888 $e->requestor->id == $user_id or
1889 $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
1891 my $api = $self->api_name();
1893 my $filter = ($api =~ /have_balance/o) ?
1894 { 'balance_owed' => { '<>' => 0 } }:
1895 { 'total_owed' => { '>' => 0 } };
1897 my $method = 'open-ils.actor.user.transactions.history.still_open';
1898 $method = "$method.authoritative" if $api =~ /authoritative/;
1899 my ($trans) = $self->method_lookup($method)->run($auth, $user_id, $type, $filter, $options);
1901 if($api =~ /total/o) {
1903 $total += $_->balance_owed for @$trans;
1907 ($api =~ /count/o ) and return scalar @$trans;
1908 ($api !~ /fleshed/o) and return $trans;
1911 for my $t (@$trans) {
1913 if( $t->xact_type ne 'circulation' ) {
1914 push @resp, {transaction => $t};
1918 my $circ_data = flesh_circ($e, $t->id);
1919 push @resp, {transaction => $t, %$circ_data};
1926 __PACKAGE__->register_method(
1927 method => "user_transaction_retrieve",
1928 api_name => "open-ils.actor.user.transaction.fleshed.retrieve",
1931 notes => "Returns a fleshed transaction record"
1934 __PACKAGE__->register_method(
1935 method => "user_transaction_retrieve",
1936 api_name => "open-ils.actor.user.transaction.retrieve",
1939 notes => "Returns a transaction record"
1942 sub user_transaction_retrieve {
1943 my($self, $client, $auth, $bill_id) = @_;
1945 my $e = new_editor(authtoken => $auth);
1946 return $e->event unless $e->checkauth;
1948 my $trans = $e->retrieve_money_billable_transaction_summary(
1949 [$bill_id, {flesh => 1, flesh_fields => {mbts => ['usr']}}]) or return $e->event;
1951 return $e->event unless $e->allowed('VIEW_USER_TRANSACTIONS', $trans->usr->home_ou);
1953 $trans->usr($trans->usr->id); # de-flesh for backwards compat
1955 return $trans unless $self->api_name =~ /flesh/;
1956 return {transaction => $trans} if $trans->xact_type ne 'circulation';
1958 my $circ_data = flesh_circ($e, $trans->id, 1);
1960 return {transaction => $trans, %$circ_data};
1965 my $circ_id = shift;
1966 my $flesh_copy = shift;
1968 my $circ = $e->retrieve_action_circulation([
1972 circ => ['target_copy'],
1973 acp => ['call_number'],
1980 my $copy = $circ->target_copy;
1982 if($circ->target_copy->call_number->id == OILS_PRECAT_CALL_NUMBER) {
1983 $mods = new Fieldmapper::metabib::virtual_record;
1984 $mods->doc_id(OILS_PRECAT_RECORD);
1985 $mods->title($copy->dummy_title);
1986 $mods->author($copy->dummy_author);
1989 $mods = $U->record_to_mvr($circ->target_copy->call_number->record);
1993 $circ->target_copy($circ->target_copy->id);
1994 $copy->call_number($copy->call_number->id);
1996 return {circ => $circ, record => $mods, copy => ($flesh_copy) ? $copy : undef };
2000 __PACKAGE__->register_method(
2001 method => "hold_request_count",
2002 api_name => "open-ils.actor.user.hold_requests.count",
2006 Returns hold ready vs. total counts.
2007 If a context org unit is provided, a third value
2008 is returned with key 'behind_desk', which reports
2009 how many holds are ready at the pickup library
2010 with the behind_desk flag set to true.
2014 sub hold_request_count {
2015 my( $self, $client, $authtoken, $user_id, $ctx_org ) = @_;
2016 my $e = new_editor(authtoken => $authtoken);
2017 return $e->event unless $e->checkauth;
2019 $user_id = $e->requestor->id unless defined $user_id;
2021 if($e->requestor->id ne $user_id) {
2022 my $user = $e->retrieve_actor_user($user_id);
2023 return $e->event unless $e->allowed('VIEW_HOLD', $user->home_ou);
2026 my $holds = $e->json_query({
2027 select => {ahr => ['pickup_lib', 'current_shelf_lib', 'behind_desk']},
2031 fulfillment_time => {"=" => undef },
2032 cancel_time => undef,
2037 $_->{current_shelf_lib} and # avoid undef warnings
2038 $_->{pickup_lib} eq $_->{current_shelf_lib}
2042 total => scalar(@$holds),
2043 ready => scalar(@ready)
2047 # count of holds ready at pickup lib with behind_desk true.
2048 $resp->{behind_desk} = scalar(
2050 $_->{pickup_lib} == $ctx_org and
2051 $U->is_true($_->{behind_desk})
2059 __PACKAGE__->register_method(
2060 method => "checked_out",
2061 api_name => "open-ils.actor.user.checked_out",
2065 desc => "For a given user, returns a structure of circulations objects sorted by out, overdue, lost, claims_returned, long_overdue. "
2066 . "A list of IDs are returned of each type. Circs marked lost, long_overdue, and claims_returned will not be 'finished' "
2067 . "(i.e., outstanding balance or some other pending action on the circ). "
2068 . "The .count method also includes a 'total' field which sums all open circs.",
2070 { desc => 'Authentication Token', type => 'string'},
2071 { desc => 'User ID', type => 'string'},
2074 desc => 'Returns event on error, or an object with ID lists, like: '
2075 . '{"out":[12552,451232], "claims_returned":[], "long_overdue":[23421] "overdue":[], "lost":[]}'
2080 __PACKAGE__->register_method(
2081 method => "checked_out",
2082 api_name => "open-ils.actor.user.checked_out.count",
2085 signature => q/@see open-ils.actor.user.checked_out/
2089 my( $self, $conn, $auth, $userid ) = @_;
2091 my $e = new_editor(authtoken=>$auth);
2092 return $e->event unless $e->checkauth;
2094 if( $userid ne $e->requestor->id ) {
2095 my $user = $e->retrieve_actor_user($userid) or return $e->event;
2096 unless($e->allowed('VIEW_CIRCULATIONS', $user->home_ou)) {
2098 # see if there is a friend link allowing circ.view perms
2099 my $allowed = OpenILS::Application::Actor::Friends->friend_perm_allowed(
2100 $e, $userid, $e->requestor->id, 'circ.view');
2101 return $e->event unless $allowed;
2105 my $count = $self->api_name =~ /count/;
2106 return _checked_out( $count, $e, $userid );
2110 my( $iscount, $e, $userid ) = @_;
2116 claims_returned => [],
2119 my $meth = 'retrieve_action_open_circ_';
2127 claims_returned => 0,
2134 my $data = $e->$meth($userid);
2138 $result{$_} += $data->$_() for (keys %result);
2139 $result{total} += $data->$_() for (keys %result);
2141 for my $k (keys %result) {
2142 $result{$k} = [ grep { $_ > 0 } split( ',', $data->$k()) ];
2152 __PACKAGE__->register_method(
2153 method => "checked_in_with_fines",
2154 api_name => "open-ils.actor.user.checked_in_with_fines",
2157 signature => q/@see open-ils.actor.user.checked_out/
2160 sub checked_in_with_fines {
2161 my( $self, $conn, $auth, $userid ) = @_;
2163 my $e = new_editor(authtoken=>$auth);
2164 return $e->event unless $e->checkauth;
2166 if( $userid ne $e->requestor->id ) {
2167 return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
2170 # money is owed on these items and they are checked in
2171 my $open = $e->search_action_circulation(
2174 xact_finish => undef,
2175 checkin_time => { "!=" => undef },
2180 my( @lost, @cr, @lo );
2181 for my $c (@$open) {
2182 push( @lost, $c->id ) if ($c->stop_fines eq 'LOST');
2183 push( @cr, $c->id ) if $c->stop_fines eq 'CLAIMSRETURNED';
2184 push( @lo, $c->id ) if $c->stop_fines eq 'LONGOVERDUE';
2189 claims_returned => \@cr,
2190 long_overdue => \@lo
2196 my ($api, $desc, $auth) = @_;
2197 $desc = $desc ? (" " . $desc) : '';
2198 my $ids = ($api =~ /ids$/) ? 1 : 0;
2201 method => "user_transaction_history",
2202 api_name => "open-ils.actor.user.transactions.$api",
2204 desc => "For a given User ID, returns a list of billable transaction" .
2205 ($ids ? " id" : '') .
2206 "s$desc, optionally filtered by type and/or fields in money.billable_xact_summary. " .
2207 "The VIEW_USER_TRANSACTIONS permission is required to view another user's transactions",
2209 {desc => 'Authentication token', type => 'string'},
2210 {desc => 'User ID', type => 'number'},
2211 {desc => 'Transaction type (optional)', type => 'number'},
2212 {desc => 'Hash of Billable Transaction Summary filters (optional)', type => 'object'}
2215 desc => 'List of transaction' . ($ids ? " id" : '') . 's, Event on error'
2219 $auth and push @sig, (authoritative => 1);
2223 my %auth_hist_methods = (
2225 'history.have_charge' => 'that have an initial charge',
2226 'history.still_open' => 'that are not finished',
2227 'history.have_balance' => 'that have a balance',
2228 'history.have_bill' => 'that have billings',
2229 'history.have_bill_or_payment' => 'that have non-zero-sum billings or at least 1 payment',
2230 'history.have_payment' => 'that have at least 1 payment',
2233 foreach (keys %auth_hist_methods) {
2234 __PACKAGE__->register_method(_sigmaker($_, $auth_hist_methods{$_}, 1));
2235 __PACKAGE__->register_method(_sigmaker("$_.ids", $auth_hist_methods{$_}, 1));
2236 __PACKAGE__->register_method(_sigmaker("$_.fleshed", $auth_hist_methods{$_}, 1));
2239 sub user_transaction_history {
2240 my( $self, $conn, $auth, $userid, $type, $filter, $options ) = @_;
2244 my $e = new_editor(authtoken=>$auth);
2245 return $e->die_event unless $e->checkauth;
2247 if ($e->requestor->id ne $userid) {
2248 return $e->die_event unless $e->allowed('VIEW_USER_TRANSACTIONS');
2251 my $api = $self->api_name;
2252 my @xact_finish = (xact_finish => undef ) if ($api =~ /history\.still_open$/); # What about history.still_open.ids?
2254 if(defined($type)) {
2255 $filter->{'xact_type'} = $type;
2258 if($api =~ /have_bill_or_payment/o) {
2260 # transactions that have a non-zero sum across all billings or at least 1 payment
2261 $filter->{'-or'} = {
2262 'balance_owed' => { '<>' => 0 },
2263 'last_payment_ts' => { '<>' => undef }
2266 } elsif($api =~ /have_payment/) {
2268 $filter->{last_payment_ts} ||= {'<>' => undef};
2270 } elsif( $api =~ /have_balance/o) {
2272 # transactions that have a non-zero overall balance
2273 $filter->{'balance_owed'} = { '<>' => 0 };
2275 } elsif( $api =~ /have_charge/o) {
2277 # transactions that have at least 1 billing, regardless of whether it was voided
2278 $filter->{'last_billing_ts'} = { '<>' => undef };
2280 } elsif( $api =~ /have_bill/o) { # needs to be an elsif, or we double-match have_bill_or_payment!
2282 # transactions that have non-zero sum across all billings. This will exclude
2283 # xacts where all billings have been voided
2284 $filter->{'total_owed'} = { '<>' => 0 };
2287 my $options_clause = { order_by => { mbt => 'xact_start DESC' } };
2288 $options_clause->{'limit'} = $options->{'limit'} if $options->{'limit'};
2289 $options_clause->{'offset'} = $options->{'offset'} if $options->{'offset'};
2291 my $mbts = $e->search_money_billable_transaction_summary(
2292 [ { usr => $userid, @xact_finish, %$filter },
2297 return [map {$_->id} @$mbts] if $api =~ /\.ids/;
2298 return $mbts unless $api =~ /fleshed/;
2301 for my $t (@$mbts) {
2303 if( $t->xact_type ne 'circulation' ) {
2304 push @resp, {transaction => $t};
2308 my $circ_data = flesh_circ($e, $t->id);
2309 push @resp, {transaction => $t, %$circ_data};
2317 __PACKAGE__->register_method(
2318 method => "user_perms",
2319 api_name => "open-ils.actor.permissions.user_perms.retrieve",
2321 notes => "Returns a list of permissions"
2325 my( $self, $client, $authtoken, $user ) = @_;
2327 my( $staff, $evt ) = $apputils->checkses($authtoken);
2328 return $evt if $evt;
2330 $user ||= $staff->id;
2332 if( $user != $staff->id and $evt = $apputils->check_perms( $staff->id, $staff->home_ou, 'VIEW_PERMISSION') ) {
2336 return $apputils->simple_scalar_request(
2338 "open-ils.storage.permission.user_perms.atomic",
2342 __PACKAGE__->register_method(
2343 method => "retrieve_perms",
2344 api_name => "open-ils.actor.permissions.retrieve",
2345 notes => "Returns a list of permissions"
2347 sub retrieve_perms {
2348 my( $self, $client ) = @_;
2349 return $apputils->simple_scalar_request(
2351 "open-ils.cstore.direct.permission.perm_list.search.atomic",
2352 { id => { '!=' => undef } }
2356 __PACKAGE__->register_method(
2357 method => "retrieve_groups",
2358 api_name => "open-ils.actor.groups.retrieve",
2359 notes => "Returns a list of user groups"
2361 sub retrieve_groups {
2362 my( $self, $client ) = @_;
2363 return new_editor()->retrieve_all_permission_grp_tree();
2366 __PACKAGE__->register_method(
2367 method => "retrieve_org_address",
2368 api_name => "open-ils.actor.org_unit.address.retrieve",
2369 notes => <<' NOTES');
2370 Returns an org_unit address by ID
2371 @param An org_address ID
2373 sub retrieve_org_address {
2374 my( $self, $client, $id ) = @_;
2375 return $apputils->simple_scalar_request(
2377 "open-ils.cstore.direct.actor.org_address.retrieve",
2382 __PACKAGE__->register_method(
2383 method => "retrieve_groups_tree",
2384 api_name => "open-ils.actor.groups.tree.retrieve",
2385 notes => "Returns a list of user groups"
2388 sub retrieve_groups_tree {
2389 my( $self, $client ) = @_;
2390 return new_editor()->search_permission_grp_tree(
2395 flesh_fields => { pgt => ["children"] },
2396 order_by => { pgt => 'name'}
2403 __PACKAGE__->register_method(
2404 method => "add_user_to_groups",
2405 api_name => "open-ils.actor.user.set_groups",
2406 notes => "Adds a user to one or more permission groups"
2409 sub add_user_to_groups {
2410 my( $self, $client, $authtoken, $userid, $groups ) = @_;
2412 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2413 $authtoken, $userid, 'CREATE_USER_GROUP_LINK' );
2414 return $evt if $evt;
2416 ( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2417 $authtoken, $userid, 'REMOVE_USER_GROUP_LINK' );
2418 return $evt if $evt;
2420 $apputils->simplereq(
2422 'open-ils.storage.direct.permission.usr_grp_map.mass_delete', { usr => $userid } );
2424 for my $group (@$groups) {
2425 my $link = Fieldmapper::permission::usr_grp_map->new;
2427 $link->usr($userid);
2429 my $id = $apputils->simplereq(
2431 'open-ils.storage.direct.permission.usr_grp_map.create', $link );
2437 __PACKAGE__->register_method(
2438 method => "get_user_perm_groups",
2439 api_name => "open-ils.actor.user.get_groups",
2440 notes => "Retrieve a user's permission groups."
2444 sub get_user_perm_groups {
2445 my( $self, $client, $authtoken, $userid ) = @_;
2447 my( $requestor, $target, $evt ) = $apputils->checkses_requestor(
2448 $authtoken, $userid, 'VIEW_PERM_GROUPS' );
2449 return $evt if $evt;
2451 return $apputils->simplereq(
2453 'open-ils.cstore.direct.permission.usr_grp_map.search.atomic', { usr => $userid } );
2457 __PACKAGE__->register_method(
2458 method => "get_user_work_ous",
2459 api_name => "open-ils.actor.user.get_work_ous",
2460 notes => "Retrieve a user's work org units."
2463 __PACKAGE__->register_method(
2464 method => "get_user_work_ous",
2465 api_name => "open-ils.actor.user.get_work_ous.ids",
2466 notes => "Retrieve a user's work org units."
2469 sub get_user_work_ous {
2470 my( $self, $client, $auth, $userid ) = @_;
2471 my $e = new_editor(authtoken=>$auth);
2472 return $e->event unless $e->checkauth;
2473 $userid ||= $e->requestor->id;
2475 if($e->requestor->id != $userid) {
2476 my $user = $e->retrieve_actor_user($userid)
2477 or return $e->event;
2478 return $e->event unless $e->allowed('ASSIGN_WORK_ORG_UNIT', $user->home_ou);
2481 return $e->search_permission_usr_work_ou_map({usr => $userid})
2482 unless $self->api_name =~ /.ids$/;
2484 # client just wants a list of org IDs
2485 return $U->get_user_work_ou_ids($e, $userid);
2490 __PACKAGE__->register_method(
2491 method => 'register_workstation',
2492 api_name => 'open-ils.actor.workstation.register.override',
2493 signature => q/@see open-ils.actor.workstation.register/
2496 __PACKAGE__->register_method(
2497 method => 'register_workstation',
2498 api_name => 'open-ils.actor.workstation.register',
2500 Registers a new workstion in the system
2501 @param authtoken The login session key
2502 @param name The name of the workstation id
2503 @param owner The org unit that owns this workstation
2504 @return The workstation id on success, WORKSTATION_NAME_EXISTS
2505 if the name is already in use.
2509 sub register_workstation {
2510 my( $self, $conn, $authtoken, $name, $owner, $oargs ) = @_;
2512 my $e = new_editor(authtoken=>$authtoken, xact=>1);
2513 return $e->die_event unless $e->checkauth;
2514 return $e->die_event unless $e->allowed('REGISTER_WORKSTATION', $owner);
2515 my $existing = $e->search_actor_workstation({name => $name})->[0];
2516 $oargs = { all => 1 } unless defined $oargs;
2520 if( $self->api_name =~ /override/o && ($oargs->{all} || grep { $_ eq 'WORKSTATION_NAME_EXISTS' } @{$oargs->{events}}) ) {
2521 # workstation with the given name exists.
2523 if($owner ne $existing->owning_lib) {
2524 # if necessary, update the owning_lib of the workstation
2526 $logger->info("changing owning lib of workstation ".$existing->id.
2527 " from ".$existing->owning_lib." to $owner");
2528 return $e->die_event unless
2529 $e->allowed('UPDATE_WORKSTATION', $existing->owning_lib);
2531 return $e->die_event unless $e->allowed('UPDATE_WORKSTATION', $owner);
2533 $existing->owning_lib($owner);
2534 return $e->die_event unless $e->update_actor_workstation($existing);
2540 "attempt to register an existing workstation. returning existing ID");
2543 return $existing->id;
2546 return OpenILS::Event->new('WORKSTATION_NAME_EXISTS')
2550 my $ws = Fieldmapper::actor::workstation->new;
2551 $ws->owning_lib($owner);
2553 $e->create_actor_workstation($ws) or return $e->die_event;
2555 return $ws->id; # note: editor sets the id on the new object for us
2558 __PACKAGE__->register_method(
2559 method => 'workstation_list',
2560 api_name => 'open-ils.actor.workstation.list',
2562 Returns a list of workstations registered at the given location
2563 @param authtoken The login session key
2564 @param ids A list of org_unit.id's for the workstation owners
2568 sub workstation_list {
2569 my( $self, $conn, $authtoken, @orgs ) = @_;
2571 my $e = new_editor(authtoken=>$authtoken);
2572 return $e->event unless $e->checkauth;
2577 unless $e->allowed('REGISTER_WORKSTATION', $o);
2578 $results{$o} = $e->search_actor_workstation({owning_lib=>$o});
2584 __PACKAGE__->register_method(
2585 method => 'fetch_patron_note',
2586 api_name => 'open-ils.actor.note.retrieve.all',
2589 Returns a list of notes for a given user
2590 Requestor must have VIEW_USER permission if pub==false and
2591 @param authtoken The login session key
2592 @param args Hash of params including
2593 patronid : the patron's id
2594 pub : true if retrieving only public notes
2598 sub fetch_patron_note {
2599 my( $self, $conn, $authtoken, $args ) = @_;
2600 my $patronid = $$args{patronid};
2602 my($reqr, $evt) = $U->checkses($authtoken);
2603 return $evt if $evt;
2606 ($patron, $evt) = $U->fetch_user($patronid);
2607 return $evt if $evt;
2610 if( $patronid ne $reqr->id ) {
2611 $evt = $U->check_perms($reqr->id, $patron->home_ou, 'VIEW_USER');
2612 return $evt if $evt;
2614 return $U->cstorereq(
2615 'open-ils.cstore.direct.actor.usr_note.search.atomic',
2616 { usr => $patronid, pub => 't' } );
2619 $evt = $U->check_perms($reqr->id, $patron->home_ou, 'VIEW_USER');
2620 return $evt if $evt;
2622 return $U->cstorereq(
2623 'open-ils.cstore.direct.actor.usr_note.search.atomic', { usr => $patronid } );
2626 __PACKAGE__->register_method(
2627 method => 'create_user_note',
2628 api_name => 'open-ils.actor.note.create',
2630 Creates a new note for the given user
2631 @param authtoken The login session key
2632 @param note The note object
2635 sub create_user_note {
2636 my( $self, $conn, $authtoken, $note ) = @_;
2637 my $e = new_editor(xact=>1, authtoken=>$authtoken);
2638 return $e->die_event unless $e->checkauth;
2640 my $user = $e->retrieve_actor_user($note->usr)
2641 or return $e->die_event;
2643 return $e->die_event unless
2644 $e->allowed('UPDATE_USER',$user->home_ou);
2646 $note->creator($e->requestor->id);
2647 $e->create_actor_usr_note($note) or return $e->die_event;
2653 __PACKAGE__->register_method(
2654 method => 'delete_user_note',
2655 api_name => 'open-ils.actor.note.delete',
2657 Deletes a note for the given user
2658 @param authtoken The login session key
2659 @param noteid The note id
2662 sub delete_user_note {
2663 my( $self, $conn, $authtoken, $noteid ) = @_;
2665 my $e = new_editor(xact=>1, authtoken=>$authtoken);
2666 return $e->die_event unless $e->checkauth;
2667 my $note = $e->retrieve_actor_usr_note($noteid)
2668 or return $e->die_event;
2669 my $user = $e->retrieve_actor_user($note->usr)
2670 or return $e->die_event;
2671 return $e->die_event unless
2672 $e->allowed('UPDATE_USER', $user->home_ou);
2674 $e->delete_actor_usr_note($note) or return $e->die_event;
2680 __PACKAGE__->register_method(
2681 method => 'update_user_note',
2682 api_name => 'open-ils.actor.note.update',
2684 @param authtoken The login session key
2685 @param note The note
2689 sub update_user_note {
2690 my( $self, $conn, $auth, $note ) = @_;
2691 my $e = new_editor(authtoken=>$auth, xact=>1);
2692 return $e->die_event unless $e->checkauth;
2693 my $patron = $e->retrieve_actor_user($note->usr)
2694 or return $e->die_event;
2695 return $e->die_event unless
2696 $e->allowed('UPDATE_USER', $patron->home_ou);
2697 $e->update_actor_user_note($note)
2698 or return $e->die_event;
2703 __PACKAGE__->register_method(
2704 method => 'fetch_patron_messages',
2705 api_name => 'open-ils.actor.message.retrieve',
2708 Returns a list of notes for a given user, not
2709 including ones marked deleted
2710 @param authtoken The login session key
2711 @param patronid patron ID
2712 @param options hash containing optional limit and offset
2716 sub fetch_patron_messages {
2717 my( $self, $conn, $auth, $patronid, $options ) = @_;
2721 my $e = new_editor(authtoken => $auth);
2722 return $e->die_event unless $e->checkauth;
2724 if ($e->requestor->id ne $patronid) {
2725 return $e->die_event unless $e->allowed('VIEW_USER');
2728 my $select_clause = { usr => $patronid };
2729 my $options_clause = { order_by => { aum => 'create_date DESC' } };
2730 $options_clause->{'limit'} = $options->{'limit'} if $options->{'limit'};
2731 $options_clause->{'offset'} = $options->{'offset'} if $options->{'offset'};
2733 my $aum = $e->search_actor_usr_message([ $select_clause, $options_clause ]);
2738 __PACKAGE__->register_method(
2739 method => 'usrname_exists',
2740 api_name => 'open-ils.actor.username.exists',
2742 desc => 'Check if a username is already taken (by an undeleted patron)',
2744 {desc => 'Authentication token', type => 'string'},
2745 {desc => 'Username', type => 'string'}
2748 desc => 'id of existing user if username exists, undef otherwise. Event on error'
2753 sub usrname_exists {
2754 my( $self, $conn, $auth, $usrname ) = @_;
2755 my $e = new_editor(authtoken=>$auth);
2756 return $e->event unless $e->checkauth;
2757 my $a = $e->search_actor_user({usrname => $usrname}, {idlist=>1});
2758 return $$a[0] if $a and @$a;
2762 __PACKAGE__->register_method(
2763 method => 'barcode_exists',
2764 api_name => 'open-ils.actor.barcode.exists',
2766 signature => 'Returns 1 if the requested barcode exists, returns 0 otherwise'
2769 sub barcode_exists {
2770 my( $self, $conn, $auth, $barcode ) = @_;
2771 my $e = new_editor(authtoken=>$auth);
2772 return $e->event unless $e->checkauth;
2773 my $card = $e->search_actor_card({barcode => $barcode});
2779 #return undef unless @$card;
2780 #return $card->[0]->usr;
2784 __PACKAGE__->register_method(
2785 method => 'retrieve_net_levels',
2786 api_name => 'open-ils.actor.net_access_level.retrieve.all',
2789 sub retrieve_net_levels {
2790 my( $self, $conn, $auth ) = @_;
2791 my $e = new_editor(authtoken=>$auth);
2792 return $e->event unless $e->checkauth;
2793 return $e->retrieve_all_config_net_access_level();
2796 # Retain the old typo API name just in case
2797 __PACKAGE__->register_method(
2798 method => 'fetch_org_by_shortname',
2799 api_name => 'open-ils.actor.org_unit.retrieve_by_shorname',
2801 __PACKAGE__->register_method(
2802 method => 'fetch_org_by_shortname',
2803 api_name => 'open-ils.actor.org_unit.retrieve_by_shortname',
2805 sub fetch_org_by_shortname {
2806 my( $self, $conn, $sname ) = @_;
2807 my $e = new_editor();
2808 my $org = $e->search_actor_org_unit({ shortname => uc($sname)})->[0];
2809 return $e->event unless $org;
2814 __PACKAGE__->register_method(
2815 method => 'session_home_lib',
2816 api_name => 'open-ils.actor.session.home_lib',
2819 sub session_home_lib {
2820 my( $self, $conn, $auth ) = @_;
2821 my $e = new_editor(authtoken=>$auth);
2822 return undef unless $e->checkauth;
2823 my $org = $e->retrieve_actor_org_unit($e->requestor->home_ou);
2824 return $org->shortname;
2827 __PACKAGE__->register_method(
2828 method => 'session_safe_token',
2829 api_name => 'open-ils.actor.session.safe_token',
2831 Returns a hashed session ID that is safe for export to the world.
2832 This safe token will expire after 1 hour of non-use.
2833 @param auth Active authentication token
2837 sub session_safe_token {
2838 my( $self, $conn, $auth ) = @_;
2839 my $e = new_editor(authtoken=>$auth);
2840 return undef unless $e->checkauth;
2842 my $safe_token = md5_hex($auth);
2844 $cache ||= OpenSRF::Utils::Cache->new("global", 0);
2846 # add more user fields as needed
2848 "safe-token-user-$safe_token", {
2849 id => $e->requestor->id,
2850 home_ou_shortname => $e->retrieve_actor_org_unit(
2851 $e->requestor->home_ou)->shortname,
2860 __PACKAGE__->register_method(
2861 method => 'safe_token_home_lib',
2862 api_name => 'open-ils.actor.safe_token.home_lib.shortname',
2864 Returns the home library shortname from the session
2865 asscociated with a safe token from generated by
2866 open-ils.actor.session.safe_token.
2867 @param safe_token Active safe token
2868 @param who Optional user activity "ewho" value
2872 sub safe_token_home_lib {
2873 my( $self, $conn, $safe_token, $who ) = @_;
2874 $cache ||= OpenSRF::Utils::Cache->new("global", 0);
2876 my $blob = $cache->get_cache("safe-token-user-$safe_token");
2877 return unless $blob;
2879 $U->log_user_activity($blob->{id}, $who, 'verify');
2880 return $blob->{home_ou_shortname};
2884 __PACKAGE__->register_method(
2885 method => "update_penalties",
2886 api_name => "open-ils.actor.user.penalties.update"
2889 sub update_penalties {
2890 my($self, $conn, $auth, $user_id) = @_;
2891 my $e = new_editor(authtoken=>$auth, xact => 1);
2892 return $e->die_event unless $e->checkauth;
2893 my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
2894 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2895 my $evt = OpenILS::Utils::Penalty->calculate_penalties($e, $user_id, $e->requestor->ws_ou);
2896 return $evt if $evt;
2902 __PACKAGE__->register_method(
2903 method => "apply_penalty",
2904 api_name => "open-ils.actor.user.penalty.apply"
2908 my($self, $conn, $auth, $penalty) = @_;
2910 my $e = new_editor(authtoken=>$auth, xact => 1);
2911 return $e->die_event unless $e->checkauth;
2913 my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
2914 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2916 my $ptype = $e->retrieve_config_standing_penalty($penalty->standing_penalty) or return $e->die_event;
2919 (defined $ptype->org_depth) ?
2920 $U->org_unit_ancestor_at_depth($penalty->org_unit, $ptype->org_depth) :
2923 $penalty->org_unit($ctx_org);
2924 $penalty->staff($e->requestor->id);
2925 $e->create_actor_user_standing_penalty($penalty) or return $e->die_event;
2928 return $penalty->id;
2931 __PACKAGE__->register_method(
2932 method => "remove_penalty",
2933 api_name => "open-ils.actor.user.penalty.remove"
2936 sub remove_penalty {
2937 my($self, $conn, $auth, $penalty) = @_;
2938 my $e = new_editor(authtoken=>$auth, xact => 1);
2939 return $e->die_event unless $e->checkauth;
2940 my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
2941 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2943 $e->delete_actor_user_standing_penalty($penalty) or return $e->die_event;
2948 __PACKAGE__->register_method(
2949 method => "update_penalty_note",
2950 api_name => "open-ils.actor.user.penalty.note.update"
2953 sub update_penalty_note {
2954 my($self, $conn, $auth, $penalty_ids, $note) = @_;
2955 my $e = new_editor(authtoken=>$auth, xact => 1);
2956 return $e->die_event unless $e->checkauth;
2957 for my $penalty_id (@$penalty_ids) {
2958 my $penalty = $e->search_actor_user_standing_penalty( { id => $penalty_id } )->[0];
2959 if (! $penalty ) { return $e->die_event; }
2960 my $user = $e->retrieve_actor_user($penalty->usr) or return $e->die_event;
2961 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
2963 $penalty->note( $note ); $penalty->ischanged( 1 );
2965 $e->update_actor_user_standing_penalty($penalty) or return $e->die_event;
2971 __PACKAGE__->register_method(
2972 method => "ranged_penalty_thresholds",
2973 api_name => "open-ils.actor.grp_penalty_threshold.ranged.retrieve",
2977 sub ranged_penalty_thresholds {
2978 my($self, $conn, $auth, $context_org) = @_;
2979 my $e = new_editor(authtoken=>$auth);
2980 return $e->event unless $e->checkauth;
2981 return $e->event unless $e->allowed('VIEW_GROUP_PENALTY_THRESHOLD', $context_org);
2982 my $list = $e->search_permission_grp_penalty_threshold([
2983 {org_unit => $U->get_org_ancestors($context_org)},
2984 {order_by => {pgpt => 'id'}}
2986 $conn->respond($_) for @$list;
2992 __PACKAGE__->register_method(
2993 method => "user_retrieve_fleshed_by_id",
2995 api_name => "open-ils.actor.user.fleshed.retrieve",
2998 sub user_retrieve_fleshed_by_id {
2999 my( $self, $client, $auth, $user_id, $fields ) = @_;
3000 my $e = new_editor(authtoken => $auth);
3001 return $e->event unless $e->checkauth;
3003 if( $e->requestor->id != $user_id ) {
3004 return $e->event unless $e->allowed('VIEW_USER');
3011 "standing_penalties",
3018 return new_flesh_user($user_id, $fields, $e);
3022 sub new_flesh_user {
3025 my $fields = shift || [];
3028 my $fetch_penalties = 0;
3029 if(grep {$_ eq 'standing_penalties'} @$fields) {
3030 $fields = [grep {$_ ne 'standing_penalties'} @$fields];
3031 $fetch_penalties = 1;
3034 my $fetch_usr_act = 0;
3035 if(grep {$_ eq 'usr_activity'} @$fields) {
3036 $fields = [grep {$_ ne 'usr_activity'} @$fields];
3040 my $user = $e->retrieve_actor_user(
3045 "flesh_fields" => { "au" => $fields }
3048 ) or return $e->die_event;
3051 if( grep { $_ eq 'addresses' } @$fields ) {
3053 $user->addresses([]) unless @{$user->addresses};
3054 # don't expose "replaced" addresses by default
3055 $user->addresses([grep {$_->id >= 0} @{$user->addresses}]);
3057 if( ref $user->billing_address ) {
3058 unless( grep { $user->billing_address->id == $_->id } @{$user->addresses} ) {
3059 push( @{$user->addresses}, $user->billing_address );
3063 if( ref $user->mailing_address ) {
3064 unless( grep { $user->mailing_address->id == $_->id } @{$user->addresses} ) {
3065 push( @{$user->addresses}, $user->mailing_address );
3070 if($fetch_penalties) {
3071 # grab the user penalties ranged for this location
3072 $user->standing_penalties(
3073 $e->search_actor_user_standing_penalty([
3076 {stop_date => undef},
3077 {stop_date => {'>' => 'now'}}
3079 org_unit => $U->get_org_full_path($e->requestor->ws_ou)
3082 flesh_fields => {ausp => ['standing_penalty']}
3088 # retrieve the most recent usr_activity entry
3089 if ($fetch_usr_act) {
3091 # max number to return for simple patron fleshing
3092 my $limit = $U->ou_ancestor_setting_value(
3093 $e->requestor->ws_ou,
3094 'circ.patron.usr_activity_retrieve.max');
3098 flesh_fields => {auact => ['etype']},
3099 order_by => {auact => 'event_time DESC'},
3102 # 0 == none, <0 == return all
3103 $limit = 1 unless defined $limit;
3104 $opts->{limit} = $limit if $limit > 0;
3106 $user->usr_activity(
3108 [] : # skip the DB call
3109 $e->search_actor_usr_activity([{usr => $user->id}, $opts])
3114 $user->clear_passwd();
3121 __PACKAGE__->register_method(
3122 method => "user_retrieve_parts",
3123 api_name => "open-ils.actor.user.retrieve.parts",
3126 sub user_retrieve_parts {
3127 my( $self, $client, $auth, $user_id, $fields ) = @_;
3128 my $e = new_editor(authtoken => $auth);
3129 return $e->event unless $e->checkauth;
3130 $user_id ||= $e->requestor->id;
3131 if( $e->requestor->id != $user_id ) {
3132 return $e->event unless $e->allowed('VIEW_USER');
3135 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3136 push(@resp, $user->$_()) for(@$fields);
3142 __PACKAGE__->register_method(
3143 method => 'user_opt_in_enabled',
3144 api_name => 'open-ils.actor.user.org_unit_opt_in.enabled',
3145 signature => '@return 1 if user opt-in is globally enabled, 0 otherwise.'
3148 sub user_opt_in_enabled {
3149 my($self, $conn) = @_;
3150 my $sc = OpenSRF::Utils::SettingsClient->new;
3151 return 1 if lc($sc->config_value(share => user => 'opt_in')) eq 'true';
3156 __PACKAGE__->register_method(
3157 method => 'user_opt_in_at_org',
3158 api_name => 'open-ils.actor.user.org_unit_opt_in.check',
3160 @param $auth The auth token
3161 @param user_id The ID of the user to test
3162 @return 1 if the user has opted in at the specified org,
3163 2 if opt-in is disallowed for the user's home org,
3164 event on error, and 0 otherwise. /
3166 sub user_opt_in_at_org {
3167 my($self, $conn, $auth, $user_id) = @_;
3169 # see if we even need to enforce the opt-in value
3170 return 1 unless user_opt_in_enabled($self);
3172 my $e = new_editor(authtoken => $auth);
3173 return $e->event unless $e->checkauth;
3175 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3176 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3178 my $ws_org = $e->requestor->ws_ou;
3179 # user is automatically opted-in if they are from the local org
3180 return 1 if $user->home_ou eq $ws_org;
3182 # get the boundary setting
3183 my $opt_boundary = $U->ou_ancestor_setting_value($e->requestor->ws_ou,'org.patron_opt_boundary');
3185 # auto opt in if user falls within the opt boundary
3186 my $opt_orgs = $U->get_org_descendants($ws_org, $opt_boundary);
3188 return 1 if grep $_ eq $user->home_ou, @$opt_orgs;
3190 # check whether opt-in is restricted at the user's home library
3191 my $opt_restrict_depth = $U->ou_ancestor_setting_value($user->home_ou, 'org.restrict_opt_to_depth');
3192 if ($opt_restrict_depth) {
3193 my $restrict_ancestor = $U->org_unit_ancestor_at_depth($user->home_ou, $opt_restrict_depth);
3194 my $unrestricted_orgs = $U->get_org_descendants($restrict_ancestor);
3196 # opt-in is disallowed unless the workstation org is within the home
3197 # library's opt-in scope
3198 return 2 unless grep $_ eq $e->requestor->ws_ou, @$unrestricted_orgs;
3201 my $vals = $e->search_actor_usr_org_unit_opt_in(
3202 {org_unit=>$opt_orgs, usr=>$user_id},{idlist=>1});
3208 __PACKAGE__->register_method(
3209 method => 'create_user_opt_in_at_org',
3210 api_name => 'open-ils.actor.user.org_unit_opt_in.create',
3212 @param $auth The auth token
3213 @param user_id The ID of the user to test
3214 @return The ID of the newly created object, event on error./
3217 sub create_user_opt_in_at_org {
3218 my($self, $conn, $auth, $user_id, $org_id) = @_;
3220 my $e = new_editor(authtoken => $auth, xact=>1);
3221 return $e->die_event unless $e->checkauth;
3223 # if a specific org unit wasn't passed in, get one based on the defaults;
3225 my $wsou = $e->requestor->ws_ou;
3226 # get the default opt depth
3227 my $opt_depth = $U->ou_ancestor_setting_value($wsou,'org.patron_opt_default');
3228 # get the org unit at that depth
3229 my $org = $e->json_query({
3230 from => [ 'actor.org_unit_ancestor_at_depth', $wsou, $opt_depth ]})->[0];
3231 $org_id = $org->{id};
3234 # fall back to the workstation OU, the pre-opt-in-boundary way
3235 $org_id = $e->requestor->ws_ou;
3238 my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3239 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3241 my $opt_in = Fieldmapper::actor::usr_org_unit_opt_in->new;
3243 $opt_in->org_unit($org_id);
3244 $opt_in->usr($user_id);
3245 $opt_in->staff($e->requestor->id);
3246 $opt_in->opt_in_ts('now');
3247 $opt_in->opt_in_ws($e->requestor->wsid);
3249 $opt_in = $e->create_actor_usr_org_unit_opt_in($opt_in)
3250 or return $e->die_event;
3258 __PACKAGE__->register_method (
3259 method => 'retrieve_org_hours',
3260 api_name => 'open-ils.actor.org_unit.hours_of_operation.retrieve',
3262 Returns the hours of operation for a specified org unit
3263 @param authtoken The login session key
3264 @param org_id The org_unit ID
3268 sub retrieve_org_hours {
3269 my($self, $conn, $auth, $org_id) = @_;
3270 my $e = new_editor(authtoken => $auth);
3271 return $e->die_event unless $e->checkauth;
3272 $org_id ||= $e->requestor->ws_ou;
3273 return $e->retrieve_actor_org_unit_hours_of_operation($org_id);
3277 __PACKAGE__->register_method (
3278 method => 'verify_user_password',
3279 api_name => 'open-ils.actor.verify_user_password',
3281 Given a barcode or username and the MD5 encoded password,
3282 returns 1 if the password is correct. Returns 0 otherwise.
3286 sub verify_user_password {
3287 my($self, $conn, $auth, $barcode, $username, $password) = @_;
3288 my $e = new_editor(authtoken => $auth);
3289 return $e->die_event unless $e->checkauth;
3291 my $user_by_barcode;
3292 my $user_by_username;
3294 my $card = $e->search_actor_card([
3295 {barcode => $barcode},
3296 {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0] or return 0;
3297 $user_by_barcode = $card->usr;
3298 $user = $user_by_barcode;
3301 $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return 0;
3302 $user = $user_by_username;
3304 return 0 if (!$user || $U->is_true($user->deleted));
3305 return 0 if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id);
3306 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3307 return $U->verify_migrated_user_password($e, $user->id, $password, 1);
3310 __PACKAGE__->register_method (
3311 method => 'retrieve_usr_id_via_barcode_or_usrname',
3312 api_name => "open-ils.actor.user.retrieve_id_by_barcode_or_username",
3314 Given a barcode or username returns the id for the user or
3319 sub retrieve_usr_id_via_barcode_or_usrname {
3320 my($self, $conn, $auth, $barcode, $username) = @_;
3321 my $e = new_editor(authtoken => $auth);
3322 return $e->die_event unless $e->checkauth;
3323 my $id_as_barcode= OpenSRF::Utils::SettingsClient->new->config_value(apps => 'open-ils.actor' => app_settings => 'id_as_barcode');
3325 my $user_by_barcode;
3326 my $user_by_username;
3327 $logger->info("$id_as_barcode is the ID as BARCODE");
3329 my $card = $e->search_actor_card([
3330 {barcode => $barcode},
3331 {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
3332 if ($id_as_barcode =~ /^t/i) {
3334 $user = $e->retrieve_actor_user($barcode);
3335 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$user);
3337 $user_by_barcode = $card->usr;
3338 $user = $user_by_barcode;
3341 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if(!$card);
3342 $user_by_barcode = $card->usr;
3343 $user = $user_by_barcode;
3348 $user_by_username = $e->search_actor_user({usrname => $username})->[0] or return OpenILS::Event->new( 'ACTOR_USR_NOT_FOUND' );
3350 $user = $user_by_username;
3352 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if (!$user);
3353 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' ) if ($user_by_username && $user_by_barcode && $user_by_username->id != $user_by_barcode->id);
3354 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3359 __PACKAGE__->register_method (
3360 method => 'merge_users',
3361 api_name => 'open-ils.actor.user.merge',
3364 Given a list of source users and destination user, transfer all data from the source
3365 to the dest user and delete the source user. All user related data is
3366 transferred, including circulations, holds, bookbags, etc.
3372 my($self, $conn, $auth, $master_id, $user_ids, $options) = @_;
3373 my $e = new_editor(xact => 1, authtoken => $auth);
3374 return $e->die_event unless $e->checkauth;
3376 # disallow the merge if any subordinate accounts are in collections
3377 my $colls = $e->search_money_collections_tracker({usr => $user_ids}, {idlist => 1});
3378 return OpenILS::Event->new('MERGED_USER_IN_COLLECTIONS', payload => $user_ids) if @$colls;
3380 my $master_user = $e->retrieve_actor_user($master_id) or return $e->die_event;
3381 my $del_addrs = ($U->ou_ancestor_setting_value(
3382 $master_user->home_ou, 'circ.user_merge.delete_addresses', $e)) ? 't' : 'f';
3383 my $del_cards = ($U->ou_ancestor_setting_value(
3384 $master_user->home_ou, 'circ.user_merge.delete_cards', $e)) ? 't' : 'f';
3385 my $deactivate_cards = ($U->ou_ancestor_setting_value(
3386 $master_user->home_ou, 'circ.user_merge.deactivate_cards', $e)) ? 't' : 'f';
3388 for my $src_id (@$user_ids) {
3389 my $src_user = $e->retrieve_actor_user($src_id) or return $e->die_event;
3391 return $e->die_event unless $e->allowed('MERGE_USERS', $src_user->home_ou);
3392 if($src_user->home_ou ne $master_user->home_ou) {
3393 return $e->die_event unless $e->allowed('MERGE_USERS', $master_user->home_ou);
3396 return $e->die_event unless
3397 $e->json_query({from => [
3412 __PACKAGE__->register_method (
3413 method => 'approve_user_address',
3414 api_name => 'open-ils.actor.user.pending_address.approve',
3421 sub approve_user_address {
3422 my($self, $conn, $auth, $addr) = @_;
3423 my $e = new_editor(xact => 1, authtoken => $auth);
3424 return $e->die_event unless $e->checkauth;
3426 # if the caller passes an address object, assume they want to
3427 # update it first before approving it
3428 $e->update_actor_user_address($addr) or return $e->die_event;
3430 $addr = $e->retrieve_actor_user_address($addr) or return $e->die_event;
3432 my $user = $e->retrieve_actor_user($addr->usr);
3433 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3434 my $result = $e->json_query({from => ['actor.approve_pending_address', $addr->id]})->[0]
3435 or return $e->die_event;
3437 return [values %$result]->[0];
3441 __PACKAGE__->register_method (
3442 method => 'retrieve_friends',
3443 api_name => 'open-ils.actor.friends.retrieve',
3446 returns { confirmed: [], pending_out: [], pending_in: []}
3447 pending_out are users I'm requesting friendship with
3448 pending_in are users requesting friendship with me
3453 sub retrieve_friends {
3454 my($self, $conn, $auth, $user_id, $options) = @_;
3455 my $e = new_editor(authtoken => $auth);
3456 return $e->event unless $e->checkauth;
3457 $user_id ||= $e->requestor->id;
3459 if($user_id != $e->requestor->id) {
3460 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3461 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3464 return OpenILS::Application::Actor::Friends->retrieve_friends(
3465 $e, $user_id, $options);
3470 __PACKAGE__->register_method (
3471 method => 'apply_friend_perms',
3472 api_name => 'open-ils.actor.friends.perms.apply',
3478 sub apply_friend_perms {
3479 my($self, $conn, $auth, $user_id, $delegate_id, @perms) = @_;
3480 my $e = new_editor(authtoken => $auth, xact => 1);
3481 return $e->die_event unless $e->checkauth;
3483 if($user_id != $e->requestor->id) {
3484 my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3485 return $e->die_event unless $e->allowed('VIEW_USER', $user->home_ou);
3488 for my $perm (@perms) {
3490 OpenILS::Application::Actor::Friends->apply_friend_perm(
3491 $e, $user_id, $delegate_id, $perm);
3492 return $evt if $evt;
3500 __PACKAGE__->register_method (
3501 method => 'update_user_pending_address',
3502 api_name => 'open-ils.actor.user.address.pending.cud'
3505 sub update_user_pending_address {
3506 my($self, $conn, $auth, $addr) = @_;
3507 my $e = new_editor(authtoken => $auth, xact => 1);
3508 return $e->die_event unless $e->checkauth;
3510 if($addr->usr != $e->requestor->id) {
3511 my $user = $e->retrieve_actor_user($addr->usr) or return $e->die_event;
3512 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3516 $e->create_actor_user_address($addr) or return $e->die_event;
3517 } elsif($addr->isdeleted) {
3518 $e->delete_actor_user_address($addr) or return $e->die_event;
3520 $e->update_actor_user_address($addr) or return $e->die_event;
3528 __PACKAGE__->register_method (
3529 method => 'user_events',
3530 api_name => 'open-ils.actor.user.events.circ',
3533 __PACKAGE__->register_method (
3534 method => 'user_events',
3535 api_name => 'open-ils.actor.user.events.ahr',
3540 my($self, $conn, $auth, $user_id, $filters) = @_;
3541 my $e = new_editor(authtoken => $auth);
3542 return $e->event unless $e->checkauth;
3544 (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3545 my $user_field = 'usr';
3548 $filters->{target} = {
3549 select => { $obj_type => ['id'] },
3551 where => {usr => $user_id}
3554 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3555 if($e->requestor->id != $user_id) {
3556 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3559 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3560 my $req = $ses->request('open-ils.trigger.events_by_target',
3561 $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3563 while(my $resp = $req->recv) {
3564 my $val = $resp->content;
3565 my $tgt = $val->target;
3567 if($obj_type eq 'circ') {
3568 $tgt->target_copy($e->retrieve_asset_copy($tgt->target_copy));
3570 } elsif($obj_type eq 'ahr') {
3571 $tgt->current_copy($e->retrieve_asset_copy($tgt->current_copy))
3572 if $tgt->current_copy;
3575 $conn->respond($val) if $val;
3581 __PACKAGE__->register_method (
3582 method => 'copy_events',
3583 api_name => 'open-ils.actor.copy.events.circ',
3586 __PACKAGE__->register_method (
3587 method => 'copy_events',
3588 api_name => 'open-ils.actor.copy.events.ahr',
3593 my($self, $conn, $auth, $copy_id, $filters) = @_;
3594 my $e = new_editor(authtoken => $auth);
3595 return $e->event unless $e->checkauth;
3597 (my $obj_type = $self->api_name) =~ s/.*\.([a-z]+)$/$1/;
3599 my $copy = $e->retrieve_asset_copy($copy_id) or return $e->event;
3601 my $copy_field = 'target_copy';
3602 $copy_field = 'current_copy' if $obj_type eq 'ahr';
3605 $filters->{target} = {
3606 select => { $obj_type => ['id'] },
3608 where => {$copy_field => $copy_id}
3612 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3613 my $req = $ses->request('open-ils.trigger.events_by_target',
3614 $obj_type, $filters, {atevdef => ['reactor', 'validator']}, 2);
3616 while(my $resp = $req->recv) {
3617 my $val = $resp->content;
3618 my $tgt = $val->target;
3620 my $user = $e->retrieve_actor_user($tgt->usr);
3621 if($e->requestor->id != $user->id) {
3622 return $e->event unless $e->allowed('VIEW_USER', $user->home_ou);
3625 $tgt->$copy_field($copy);
3628 $conn->respond($val) if $val;
3635 __PACKAGE__->register_method (
3636 method => 'get_itemsout_notices',
3637 api_name => 'open-ils.actor.user.itemsout.notices',
3642 sub get_itemsout_notices{
3643 my( $self, $conn, $auth, $circId, $patronId) = @_;
3645 my $e = new_editor(authtoken => $auth);
3646 return $e->event unless $e->checkauth;
3648 my $requestorId = $e->requestor->id;
3650 if( $patronId ne $requestorId ){
3651 my $user = $e->retrieve_actor_user($requestorId) or return $e->event;
3652 return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
3655 #my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3656 #my $req = $ses->request('open-ils.trigger.events_by_target',
3657 # 'circ', {target => [$circId], event=> {state=>'complete'}});
3658 # ^ Above removed in favor of faster json_query.
3661 # select complete_time
3662 # from action_trigger.event atev
3663 # JOIN action_trigger.event_definition def ON (def.id = atev.event_def)
3664 # JOIN action_trigger.hook athook ON (athook.key = def.hook)
3665 # where hook = 'checkout.due' AND state = 'complete' and target = <circId>;
3668 my $ctx_loc = $e->requestor->ws_ou;
3669 my $exclude_courtesy_notices = $U->ou_ancestor_setting_value($ctx_loc, 'webstaff.circ.itemsout_notice_count_excludes_courtesies');
3671 select => { atev => ["complete_time"] },
3674 atevdef => { field => "id",fkey => "event_def", join => { ath => { field => "key", fkey => "hook" }} }
3677 where => {"+ath" => { key => "checkout.due" },"+atevdef" => { active => 't' },"+atev" => { target => $circId, state => 'complete' }}
3680 if ($exclude_courtesy_notices){
3681 $query->{"where"}->{"+atevdef"}->{validator} = { "<>" => "CircIsOpen"};
3684 my %resblob = ( numNotices => 0, lastDt => undef );
3686 my $res = $e->json_query($query);
3687 for my $ndate (@$res) {
3688 $resblob{numNotices}++;
3689 if( !defined $resblob{lastDt}){
3690 $resblob{lastDt} = $$ndate{complete_time};
3693 if ($resblob{lastDt} lt $$ndate{complete_time}){
3694 $resblob{lastDt} = $$ndate{complete_time};
3698 $conn->respond(\%resblob);
3702 __PACKAGE__->register_method (
3703 method => 'update_events',
3704 api_name => 'open-ils.actor.user.event.cancel.batch',
3707 __PACKAGE__->register_method (
3708 method => 'update_events',
3709 api_name => 'open-ils.actor.user.event.reset.batch',
3714 my($self, $conn, $auth, $event_ids) = @_;
3715 my $e = new_editor(xact => 1, authtoken => $auth);
3716 return $e->die_event unless $e->checkauth;
3719 for my $id (@$event_ids) {
3721 # do a little dance to determine what user we are ultimately affecting
3722 my $event = $e->retrieve_action_trigger_event([
3725 flesh_fields => {atev => ['event_def'], atevdef => ['hook']}
3727 ]) or return $e->die_event;
3730 if($event->event_def->hook->core_type eq 'circ') {
3731 $user_id = $e->retrieve_action_circulation($event->target)->usr;
3732 } elsif($event->event_def->hook->core_type eq 'ahr') {
3733 $user_id = $e->retrieve_action_hold_request($event->target)->usr;
3738 my $user = $e->retrieve_actor_user($user_id);
3739 return $e->die_event unless $e->allowed('UPDATE_USER', $user->home_ou);
3741 if($self->api_name =~ /cancel/) {
3742 $event->state('invalid');
3743 } elsif($self->api_name =~ /reset/) {
3744 $event->clear_start_time;
3745 $event->clear_update_time;
3746 $event->state('pending');
3749 $e->update_action_trigger_event($event) or return $e->die_event;
3750 $conn->respond({maximum => scalar(@$event_ids), progress => $x++});
3754 return {complete => 1};
3758 __PACKAGE__->register_method (
3759 method => 'really_delete_user',
3760 api_name => 'open-ils.actor.user.delete.override',
3761 signature => q/@see open-ils.actor.user.delete/
3764 __PACKAGE__->register_method (
3765 method => 'really_delete_user',
3766 api_name => 'open-ils.actor.user.delete',
3768 It anonymizes all personally identifiable information in actor.usr. By calling actor.usr_purge_data()
3769 it also purges related data from other tables, sometimes by transferring it to a designated destination user.
3770 The usrname field (along with first_given_name and family_name) is updated to id '-PURGED-' now().
3771 dest_usr_id is only required when deleting a user that performs staff functions.
3775 sub really_delete_user {
3776 my($self, $conn, $auth, $user_id, $dest_user_id, $oargs) = @_;
3777 my $e = new_editor(authtoken => $auth, xact => 1);
3778 return $e->die_event unless $e->checkauth;
3779 $oargs = { all => 1 } unless defined $oargs;
3781 # Find all unclosed billings for for user $user_id, thereby, also checking for open circs
3782 my $open_bills = $e->json_query({
3783 select => { mbts => ['id'] },
3786 xact_finish => { '=' => undef },
3787 usr => { '=' => $user_id },
3789 }) or return $e->die_event;
3791 my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
3793 # No deleting patrons with open billings or checked out copies, unless perm-enabled override
3795 return $e->die_event(OpenILS::Event->new('ACTOR_USER_DELETE_OPEN_XACTS'))
3796 unless $self->api_name =~ /override/o && ($oargs->{all} || grep { $_ eq 'ACTOR_USER_DELETE_OPEN_XACTS' } @{$oargs->{events}})
3797 && $e->allowed('ACTOR_USER_DELETE_OPEN_XACTS.override', $user->home_ou);
3799 # No deleting yourself - UI is supposed to stop you first, though.
3800 return $e->die_event unless $e->requestor->id != $user->id;
3801 return $e->die_event unless $e->allowed('DELETE_USER', $user->home_ou);
3802 # Check if you are allowed to mess with this patron permission group at all
3803 my $evt = group_perm_failed($e, $e->requestor, $user);
3804 return $e->die_event($evt) if $evt;
3805 my $stat = $e->json_query(
3806 {from => ['actor.usr_delete', $user_id, $dest_user_id]})->[0]
3807 or return $e->die_event;
3813 __PACKAGE__->register_method (
3814 method => 'user_payments',
3815 api_name => 'open-ils.actor.user.payments.retrieve',
3818 Returns all payments for a given user. Default order is newest payments first.
3819 @param auth Authentication token
3820 @param user_id The user ID
3821 @param filters An optional hash of filters, including limit, offset, and order_by definitions
3826 my($self, $conn, $auth, $user_id, $filters) = @_;
3829 my $e = new_editor(authtoken => $auth);
3830 return $e->die_event unless $e->checkauth;
3832 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
3833 return $e->event unless
3834 $e->requestor->id == $user_id or
3835 $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
3837 # Find all payments for all transactions for user $user_id
3839 select => {mp => ['id']},
3844 select => {mbt => ['id']},
3846 where => {usr => $user_id}
3851 { # by default, order newest payments first
3853 field => 'payment_ts',
3856 # secondary sort in ID as a tie-breaker, since payments created
3857 # within the same transaction will have identical payment_ts's
3864 for (qw/order_by limit offset/) {
3865 $query->{$_} = $filters->{$_} if defined $filters->{$_};
3868 if(defined $filters->{where}) {
3869 foreach (keys %{$filters->{where}}) {
3870 # don't allow the caller to expand the result set to other users
3871 $query->{where}->{$_} = $filters->{where}->{$_} unless $_ eq 'xact';
3875 my $payment_ids = $e->json_query($query);
3876 for my $pid (@$payment_ids) {
3877 my $pay = $e->retrieve_money_payment([
3882 mbt => ['summary', 'circulation', 'grocery'],
3883 circ => ['target_copy'],
3884 acp => ['call_number'],
3892 xact_type => $pay->xact->summary->xact_type,
3893 last_billing_type => $pay->xact->summary->last_billing_type,
3896 if($pay->xact->summary->xact_type eq 'circulation') {
3897 $resp->{barcode} = $pay->xact->circulation->target_copy->barcode;
3898 $resp->{title} = $U->record_to_mvr($pay->xact->circulation->target_copy->call_number->record)->title;
3901 $pay->xact($pay->xact->id); # de-flesh
3902 $conn->respond($resp);
3910 __PACKAGE__->register_method (
3911 method => 'negative_balance_users',
3912 api_name => 'open-ils.actor.users.negative_balance',
3915 Returns all users that have an overall negative balance
3916 @param auth Authentication token
3917 @param org_id The context org unit as an ID or list of IDs. This will be the home
3918 library of the user. If no org_unit is specified, no org unit filter is applied
3922 sub negative_balance_users {
3923 my($self, $conn, $auth, $org_id) = @_;
3925 my $e = new_editor(authtoken => $auth);
3926 return $e->die_event unless $e->checkauth;
3927 return $e->die_event unless $e->allowed('VIEW_USER', $org_id);
3931 mous => ['usr', 'balance_owed'],
3934 {column => 'last_billing_ts', transform => 'max', aggregate => 1},
3935 {column => 'last_payment_ts', transform => 'max', aggregate => 1},
3952 where => {'+mous' => {balance_owed => {'<' => 0}}}
3955 $query->{from}->{mous}->{au}->{filter}->{home_ou} = $org_id if $org_id;
3957 my $list = $e->json_query($query, {timeout => 600});
3959 for my $data (@$list) {
3961 usr => $e->retrieve_actor_user([$data->{usr}, {flesh => 1, flesh_fields => {au => ['card']}}]),
3962 balance_owed => $data->{balance_owed},
3963 last_billing_activity => max($data->{last_billing_ts}, $data->{last_payment_ts})
3970 __PACKAGE__->register_method(
3971 method => "request_password_reset",
3972 api_name => "open-ils.actor.patron.password_reset.request",
3974 desc => "Generates a UUID token usable with the open-ils.actor.patron.password_reset.commit " .
3975 "method for changing a user's password. The UUID token is distributed via A/T " .
3976 "templates (i.e. email to the user).",
3978 { desc => 'user_id_type', type => 'string' },
3979 { desc => 'user_id', type => 'string' },
3980 { desc => 'optional (based on library setting) matching email address for authorizing request', type => 'string' },
3982 return => {desc => '1 on success, Event on error'}
3985 sub request_password_reset {
3986 my($self, $conn, $user_id_type, $user_id, $email) = @_;
3988 # Check to see if password reset requests are already being throttled:
3989 # 0. Check cache to see if we're in throttle mode (avoid hitting database)
3991 my $e = new_editor(xact => 1);
3994 # Get the user, if any, depending on the input value
3995 if ($user_id_type eq 'username') {
3996 $user = $e->search_actor_user({usrname => $user_id})->[0];
3999 return OpenILS::Event->new( 'ACTOR_USER_NOT_FOUND' );
4001 } elsif ($user_id_type eq 'barcode') {
4002 my $card = $e->search_actor_card([
4003 {barcode => $user_id},
4004 {flesh => 1, flesh_fields => {ac => ['usr']}}])->[0];
4007 return OpenILS::Event->new('ACTOR_USER_NOT_FOUND');
4012 # If the user doesn't have an email address, we can't help them
4013 if (!$user->email) {
4015 return OpenILS::Event->new('PATRON_NO_EMAIL_ADDRESS');
4018 my $email_must_match = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_requires_matching_email');
4019 if ($email_must_match) {
4020 if (lc($user->email) ne lc($email)) {
4021 return OpenILS::Event->new('EMAIL_VERIFICATION_FAILED');
4025 _reset_password_request($conn, $e, $user);
4028 # Once we have the user, we can issue the password reset request
4029 # XXX Add a wrapper method that accepts barcode + email input
4030 sub _reset_password_request {
4031 my ($conn, $e, $user) = @_;
4033 # 1. Get throttle threshold and time-to-live from OU_settings
4034 my $aupr_throttle = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_throttle') || 1000;
4035 my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
4037 my $threshold_time = DateTime->now(time_zone => 'local')->subtract(seconds => $aupr_ttl)->iso8601();
4039 # 2. Get time of last request and number of active requests (num_active)
4040 my $active_requests = $e->json_query({
4046 transform => 'COUNT'
4049 column => 'request_time',
4055 has_been_reset => { '=' => 'f' },
4056 request_time => { '>' => $threshold_time }
4060 # Guard against no active requests
4061 if ($active_requests->[0]->{'request_time'}) {
4062 my $last_request = DateTime::Format::ISO8601->parse_datetime(clense_ISO8601($active_requests->[0]->{'request_time'}));
4063 my $now = DateTime::Format::ISO8601->new();
4065 # 3. if (num_active > throttle_threshold) and (now - last_request < 1 minute)
4066 if (($active_requests->[0]->{'usr'} > $aupr_throttle) &&
4067 ($last_request->add_duration('1 minute') > $now)) {
4068 $cache->put_cache('open-ils.actor.password.throttle', DateTime::Format::ISO8601->new(), 60);
4070 return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
4074 # TODO Check to see if the user is in a password-reset-restricted group
4076 # Otherwise, go ahead and try to get the user.
4078 # Check the number of active requests for this user
4079 $active_requests = $e->json_query({
4085 transform => 'COUNT'
4090 usr => { '=' => $user->id },
4091 has_been_reset => { '=' => 'f' },
4092 request_time => { '>' => $threshold_time }
4096 $logger->info("User " . $user->id . " has " . $active_requests->[0]->{'usr'} . " active password reset requests.");
4098 # if less than or equal to per-user threshold, proceed; otherwise, return event
4099 my $aupr_per_user_limit = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_per_user_limit') || 3;
4100 if ($active_requests->[0]->{'usr'} > $aupr_per_user_limit) {
4102 return OpenILS::Event->new('PATRON_TOO_MANY_ACTIVE_PASSWORD_RESET_REQUESTS');
4105 # Create the aupr object and insert into the database
4106 my $reset_request = Fieldmapper::actor::usr_password_reset->new;
4107 my $uuid = create_uuid_as_string(UUID_V4);
4108 $reset_request->uuid($uuid);
4109 $reset_request->usr($user->id);
4111 my $aupr = $e->create_actor_usr_password_reset($reset_request) or return $e->die_event;
4114 # Create an event to notify user of the URL to reset their password
4116 # Can we stuff this in the user_data param for trigger autocreate?
4117 my $hostname = $U->ou_ancestor_setting_value($user->home_ou, 'lib.hostname') || 'localhost';
4119 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
4120 $ses->request('open-ils.trigger.event.autocreate', 'password.reset_request', $aupr, $user->home_ou);
4123 # $U->create_trigger_event('password.reset_request', $aupr, $user->home_ou);
4128 __PACKAGE__->register_method(
4129 method => "commit_password_reset",
4130 api_name => "open-ils.actor.patron.password_reset.commit",
4132 desc => "Checks a UUID token generated by the open-ils.actor.patron.password_reset.request method for " .
4133 "validity, and if valid, uses it as authorization for changing the associated user's password " .
4134 "with the supplied password.",
4136 { desc => 'uuid', type => 'string' },
4137 { desc => 'password', type => 'string' },
4139 return => {desc => '1 on success, Event on error'}
4142 sub commit_password_reset {
4143 my($self, $conn, $uuid, $password) = @_;
4145 # Check to see if password reset requests are already being throttled:
4146 # 0. Check cache to see if we're in throttle mode (avoid hitting database)
4147 $cache ||= OpenSRF::Utils::Cache->new("global", 0);
4148 my $throttle = $cache->get_cache('open-ils.actor.password.throttle') || undef;
4150 return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4153 my $e = new_editor(xact => 1);
4155 my $aupr = $e->search_actor_usr_password_reset({
4162 return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4164 my $user_id = $aupr->[0]->usr;
4165 my $user = $e->retrieve_actor_user($user_id);
4167 # Ensure we're still within the TTL for the request
4168 my $aupr_ttl = $U->ou_ancestor_setting_value($user->home_ou, 'circ.password_reset_request_time_to_live') || 24*60*60;
4169 my $threshold = DateTime::Format::ISO8601->parse_datetime(clense_ISO8601($aupr->[0]->request_time))->add(seconds => $aupr_ttl);
4170 if ($threshold < DateTime->now(time_zone => 'local')) {
4172 $logger->info("Password reset request needed to be submitted before $threshold");
4173 return OpenILS::Event->new('PATRON_NOT_AN_ACTIVE_PASSWORD_RESET_REQUEST');
4176 # Check complexity of password against OU-defined regex
4177 my $pw_regex = $U->ou_ancestor_setting_value($user->home_ou, 'global.password_regex');
4181 # Calling JSON2perl on the $pw_regex causes failure, even before the fancy Unicode regex
4182 # ($pw_regex = OpenSRF::Utils::JSON->JSON2perl($pw_regex)) =~ s/\\u([0-9a-fA-F]{4})/\\x{$1}/gs;
4183 $is_strong = check_password_strength_custom($password, $pw_regex);
4185 $is_strong = check_password_strength_default($password);
4190 return OpenILS::Event->new('PATRON_PASSWORD_WAS_NOT_STRONG');
4193 # All is well; update the password
4194 modify_migrated_user_password($e, $user->id, $password);
4196 # And flag that this password reset request has been honoured
4197 $aupr->[0]->has_been_reset('t');
4198 $e->update_actor_usr_password_reset($aupr->[0]);
4204 sub check_password_strength_default {
4205 my $password = shift;
4206 # Use the default set of checks
4207 if ( (length($password) < 7) or
4208 ($password !~ m/.*\d+.*/) or
4209 ($password !~ m/.*[A-Za-z]+.*/)
4216 sub check_password_strength_custom {
4217 my ($password, $pw_regex) = @_;
4219 $pw_regex = qr/$pw_regex/;
4220 if ($password !~ /$pw_regex/) {
4228 __PACKAGE__->register_method(
4229 method => "event_def_opt_in_settings",
4230 api_name => "open-ils.actor.event_def.opt_in.settings",
4233 desc => 'Streams the set of "cust" objects that are used as opt-in settings for event definitions',
4235 { desc => 'Authentication token', type => 'string'},
4237 desc => 'Org Unit ID. (optional). If no org ID is present, the home_ou of the requesting user is used',
4242 desc => q/set of "cust" objects that are used as opt-in settings for event definitions at the specified org unit/,
4249 sub event_def_opt_in_settings {
4250 my($self, $conn, $auth, $org_id) = @_;
4251 my $e = new_editor(authtoken => $auth);
4252 return $e->event unless $e->checkauth;
4254 if(defined $org_id and $org_id != $e->requestor->home_ou) {
4255 return $e->event unless
4256 $e->allowed(['VIEW_USER_SETTING_TYPE', 'ADMIN_USER_SETTING_TYPE'], $org_id);
4258 $org_id = $e->requestor->home_ou;
4261 # find all config.user_setting_type's related to event_defs for the requested org unit
4262 my $types = $e->json_query({
4263 select => {cust => ['name']},
4264 from => {atevdef => 'cust'},
4267 owner => $U->get_org_ancestors($org_id), # context org plus parents
4274 $conn->respond($_) for
4275 @{$e->search_config_usr_setting_type({name => [map {$_->{name}} @$types]})};
4282 __PACKAGE__->register_method(
4283 method => "user_circ_history",
4284 api_name => "open-ils.actor.history.circ",
4288 desc => 'Returns user circ history objects for the calling user',
4290 { desc => 'Authentication token', type => 'string'},
4291 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4294 desc => q/Stream of 'auch' circ history objects/,
4300 __PACKAGE__->register_method(
4301 method => "user_circ_history",
4302 api_name => "open-ils.actor.history.circ.clear",
4305 desc => 'Delete all user circ history entries for the calling user',
4307 { desc => 'Authentication token', type => 'string'},
4308 { desc => "Options hash. 'circ_ids' is an arrayref of circulation IDs to delete", type => 'object' },
4311 desc => q/1 on success, event on error/,
4317 __PACKAGE__->register_method(
4318 method => "user_circ_history",
4319 api_name => "open-ils.actor.history.circ.print",
4322 desc => q/Returns printable output for the caller's circ history objects/,
4324 { desc => 'Authentication token', type => 'string'},
4325 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4328 desc => q/An action_trigger.event object or error event./,
4334 __PACKAGE__->register_method(
4335 method => "user_circ_history",
4336 api_name => "open-ils.actor.history.circ.email",
4339 desc => q/Emails the caller's circ history/,
4341 { desc => 'Authentication token', type => 'string'},
4342 { desc => 'User ID. If no user id is present, the authenticated user is assumed', type => 'number' },
4343 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4346 desc => q/undef, or event on error/
4351 sub user_circ_history {
4352 my ($self, $conn, $auth, $options) = @_;
4355 my $for_print = ($self->api_name =~ /print/);
4356 my $for_email = ($self->api_name =~ /email/);
4357 my $for_clear = ($self->api_name =~ /clear/);
4359 # No perm check is performed. Caller may only access his/her own
4360 # circ history entries.
4361 my $e = new_editor(authtoken => $auth);
4362 return $e->event unless $e->checkauth;
4365 if (!$for_clear) { # clear deletes all
4366 $limits{offset} = $options->{offset} if defined $options->{offset};
4367 $limits{limit} = $options->{limit} if defined $options->{limit};
4370 my %circ_id_filter = $options->{circ_ids} ?
4371 (id => $options->{circ_ids}) : ();
4373 my $circs = $e->search_action_user_circ_history([
4374 { usr => $e->requestor->id,
4377 { # order newest to oldest by default
4378 order_by => {auch => 'xact_start DESC'},
4381 {substream => 1} # could be a large list
4385 return $U->fire_object_event(undef,
4386 'circ.format.history.print', $circs, $e->requestor->home_ou);
4389 $e->xact_begin if $for_clear;
4390 $conn->respond_complete(1) if $for_email; # no sense in waiting
4392 for my $circ (@$circs) {
4395 # events will be fired from action_trigger_runner
4396 $U->create_events_for_hook('circ.format.history.email',
4397 $circ, $e->editor->home_ou, undef, undef, 1);
4399 } elsif ($for_clear) {
4401 $e->delete_action_user_circ_history($circ)
4402 or return $e->die_event;
4405 $conn->respond($circ);
4418 __PACKAGE__->register_method(
4419 method => "user_visible_holds",
4420 api_name => "open-ils.actor.history.hold.visible",
4423 desc => 'Returns the set of opt-in visible holds',
4425 { desc => 'Authentication token', type => 'string'},
4426 { desc => 'User ID. If no user id is present, the authenticated user is assumed', type => 'number' },
4427 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4430 desc => q/An object with 1 field: "hold"/,
4436 __PACKAGE__->register_method(
4437 method => "user_visible_holds",
4438 api_name => "open-ils.actor.history.hold.visible.print",
4441 desc => 'Returns printable output for the set of opt-in visible holds',
4443 { desc => 'Authentication token', type => 'string'},
4444 { desc => 'User ID. If no user id is present, the authenticated user is assumed', type => 'number' },
4445 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4448 desc => q/An action_trigger.event object or error event./,
4454 __PACKAGE__->register_method(
4455 method => "user_visible_holds",
4456 api_name => "open-ils.actor.history.hold.visible.email",
4459 desc => 'Emails the set of opt-in visible holds to the requestor',
4461 { desc => 'Authentication token', type => 'string'},
4462 { desc => 'User ID. If no user id is present, the authenticated user is assumed', type => 'number' },
4463 { desc => 'Options hash. Supported fields are "limit" and "offset"', type => 'object' },
4466 desc => q/undef, or event on error/
4471 sub user_visible_holds {
4472 my($self, $conn, $auth, $user_id, $options) = @_;
4475 my $for_print = ($self->api_name =~ /print/);
4476 my $for_email = ($self->api_name =~ /email/);
4477 my $e = new_editor(authtoken => $auth);
4478 return $e->event unless $e->checkauth;
4480 $user_id ||= $e->requestor->id;
4482 $options->{limit} ||= 50;
4483 $options->{offset} ||= 0;
4485 if($user_id != $e->requestor->id) {
4486 my $perm = ($is_hold) ? 'VIEW_HOLD' : 'VIEW_CIRCULATIONS';
4487 my $user = $e->retrieve_actor_user($user_id) or return $e->event;
4488 return $e->event unless $e->allowed($perm, $user->home_ou);
4491 my $db_func = ($is_hold) ? 'action.usr_visible_holds' : 'action.usr_visible_circs';
4493 my $data = $e->json_query({
4494 from => [$db_func, $user_id],
4495 limit => $$options{limit},
4496 offset => $$options{offset}
4498 # TODO: I only want IDs. code below didn't get me there
4499 # {"select":{"au":[{"column":"id", "result_field":"id",
4500 # "transform":"action.usr_visible_circs"}]}, "where":{"id":10}, "from":"au"}
4505 return undef unless @$data;
4509 # collect the batch of objects
4513 my $hold_list = $e->search_action_hold_request({id => [map { $_->{id} } @$data]});
4514 return $U->fire_object_event(undef, 'ahr.format.history.print', $hold_list, $$hold_list[0]->request_lib);
4518 my $circ_list = $e->search_action_circulation({id => [map { $_->{id} } @$data]});
4519 return $U->fire_object_event(undef, 'circ.format.history.print', $circ_list, $$circ_list[0]->circ_lib);
4522 } elsif ($for_email) {
4524 $conn->respond_complete(1) if $for_email; # no sense in waiting
4532 my $hold = $e->retrieve_action_hold_request($id);
4533 $U->create_events_for_hook('ahr.format.history.email', $hold, $hold->request_lib, undef, undef, 1);
4534 # events will be fired from action_trigger_runner
4538 my $circ = $e->retrieve_action_circulation($id);
4539 $U->create_events_for_hook('circ.format.history.email', $circ, $circ->circ_lib, undef, undef, 1);
4540 # events will be fired from action_trigger_runner
4544 } else { # just give me the data please
4552 my $hold = $e->retrieve_action_hold_request($id);
4553 $conn->respond({hold => $hold});
4557 my $circ = $e->retrieve_action_circulation($id);
4560 summary => $U->create_circ_chain_summary($e, $id)
4569 __PACKAGE__->register_method(
4570 method => "user_saved_search_cud",
4571 api_name => "open-ils.actor.user.saved_search.cud",
4574 desc => 'Create/Update/Delete Access to user saved searches',
4576 { desc => 'Authentication token', type => 'string' },
4577 { desc => 'Saved Search Object', type => 'object', class => 'auss' }
4580 desc => q/The retrieved or updated saved search object, or id of a deleted object; Event on error/,
4586 __PACKAGE__->register_method(
4587 method => "user_saved_search_cud",
4588 api_name => "open-ils.actor.user.saved_search.retrieve",
4591 desc => 'Retrieve a saved search object',
4593 { desc => 'Authentication token', type => 'string' },
4594 { desc => 'Saved Search ID', type => 'number' }
4597 desc => q/The saved search object, Event on error/,
4603 sub user_saved_search_cud {
4604 my( $self, $client, $auth, $search ) = @_;
4605 my $e = new_editor( authtoken=>$auth );
4606 return $e->die_event unless $e->checkauth;
4608 my $o_search; # prior version of the object, if any
4609 my $res; # to be returned
4611 # branch on the operation type
4613 if( $self->api_name =~ /retrieve/ ) { # Retrieve
4615 # Get the old version, to check ownership
4616 $o_search = $e->retrieve_actor_usr_saved_search( $search )
4617 or return $e->die_event;
4619 # You can't read somebody else's search
4620 return OpenILS::Event->new('BAD_PARAMS')
4621 unless $o_search->owner == $e->requestor->id;
4627 $e->xact_begin; # start an editor transaction
4629 if( $search->isnew ) { # Create
4631 # You can't create a search for somebody else
4632 return OpenILS::Event->new('BAD_PARAMS')
4633 unless $search->owner == $e->requestor->id;
4635 $e->create_actor_usr_saved_search( $search )
4636 or return $e->die_event;
4640 } elsif( $search->ischanged ) { # Update
4642 # You can't change ownership of a search
4643 return OpenILS::Event->new('BAD_PARAMS')
4644 unless $search->owner == $e->requestor->id;
4646 # Get the old version, to check ownership
4647 $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4648 or return $e->die_event;
4650 # You can't update somebody else's search
4651 return OpenILS::Event->new('BAD_PARAMS')
4652 unless $o_search->owner == $e->requestor->id;
4655 $e->update_actor_usr_saved_search( $search )
4656 or return $e->die_event;
4660 } elsif( $search->isdeleted ) { # Delete
4662 # Get the old version, to check ownership
4663 $o_search = $e->retrieve_actor_usr_saved_search( $search->id )
4664 or return $e->die_event;
4666 # You can't delete somebody else's search
4667 return OpenILS::Event->new('BAD_PARAMS')
4668 unless $o_search->owner == $e->requestor->id;
4671 $e->delete_actor_usr_saved_search( $o_search )
4672 or return $e->die_event;
4683 __PACKAGE__->register_method(
4684 method => "get_barcodes",
4685 api_name => "open-ils.actor.get_barcodes"
4689 my( $self, $client, $auth, $org_id, $context, $barcode ) = @_;
4690 my $e = new_editor(authtoken => $auth);
4691 return $e->event unless $e->checkauth;
4692 return $e->event unless $e->allowed('STAFF_LOGIN', $org_id);
4694 my $db_result = $e->json_query(
4696 'evergreen.get_barcodes',
4697 $org_id, $context, $barcode,
4701 if($context =~ /actor/) {
4702 my $filter_result = ();
4704 foreach my $result (@$db_result) {
4705 if($result->{type} eq 'actor') {
4706 if($e->requestor->id != $result->{id}) {
4707 $patron = $e->retrieve_actor_user($result->{id});
4709 push(@$filter_result, $e->event);
4712 if($e->allowed('VIEW_USER', $patron->home_ou)) {
4713 push(@$filter_result, $result);
4716 push(@$filter_result, $e->event);
4720 push(@$filter_result, $result);
4724 push(@$filter_result, $result);
4727 return $filter_result;
4733 __PACKAGE__->register_method(
4734 method => 'address_alert_test',
4735 api_name => 'open-ils.actor.address_alert.test',
4737 desc => "Tests a set of address fields to determine if they match with an address_alert",
4739 {desc => 'Authentication token', type => 'string'},
4740 {desc => 'Org Unit', type => 'number'},
4741 {desc => 'Fields', type => 'hash'},
4743 return => {desc => 'List of matching address_alerts'}
4747 sub address_alert_test {
4748 my ($self, $client, $auth, $org_unit, $fields) = @_;
4749 return [] unless $fields and grep {$_} values %$fields;
4751 my $e = new_editor(authtoken => $auth);
4752 return $e->event unless $e->checkauth;
4753 return $e->event unless $e->allowed('CREATE_USER', $org_unit);
4754 $org_unit ||= $e->requestor->ws_ou;
4756 my $alerts = $e->json_query({
4758 'actor.address_alert_matches',
4766 $$fields{post_code},
4767 $$fields{mailing_address},
4768 $$fields{billing_address}
4772 # map the json_query hashes to real objects
4774 map {$e->retrieve_actor_address_alert($_)}
4775 (map {$_->{id}} @$alerts)
4779 __PACKAGE__->register_method(
4780 method => "mark_users_contact_invalid",
4781 api_name => "open-ils.actor.invalidate.email",
4783 desc => "Given a patron or email address, clear the email field for one patron or all patrons with that email address and put the old email address into a note and/or create a standing penalty, depending on OU settings",
4785 {desc => "Authentication token", type => "string"},
4786 {desc => "Patron ID (optional if Email address specified)", type => "number"},
4787 {desc => "Additional note text (optional)", type => "string"},
4788 {desc => "penalty org unit ID (optional)", type => "number"},
4789 {desc => "Email address (optional)", type => "string"}
4791 return => {desc => "Event describing success or failure", type => "object"}
4795 __PACKAGE__->register_method(
4796 method => "mark_users_contact_invalid",
4797 api_name => "open-ils.actor.invalidate.day_phone",
4799 desc => "Given a patron or phone number, clear the day_phone field for one patron or all patrons with that day_phone number and put the old day_phone into a note and/or create a standing penalty, depending on OU settings",
4801 {desc => "Authentication token", type => "string"},
4802 {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
4803 {desc => "Additional note text (optional)", type => "string"},
4804 {desc => "penalty org unit ID (optional)", type => "number"},
4805 {desc => "Phone Number (optional)", type => "string"}
4807 return => {desc => "Event describing success or failure", type => "object"}
4811 __PACKAGE__->register_method(
4812 method => "mark_users_contact_invalid",
4813 api_name => "open-ils.actor.invalidate.evening_phone",
4815 desc => "Given a patron or phone number, clear the evening_phone field for one patron or all patrons with that evening_phone number and put the old evening_phone into a note and/or create a standing penalty, depending on OU settings",
4817 {desc => "Authentication token", type => "string"},
4818 {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
4819 {desc => "Additional note text (optional)", type => "string"},
4820 {desc => "penalty org unit ID (optional)", type => "number"},
4821 {desc => "Phone Number (optional)", type => "string"}
4823 return => {desc => "Event describing success or failure", type => "object"}
4827 __PACKAGE__->register_method(
4828 method => "mark_users_contact_invalid",
4829 api_name => "open-ils.actor.invalidate.other_phone",
4831 desc => "Given a patron or phone number, clear the other_phone field for one patron or all patrons with that other_phone number and put the old other_phone into a note and/or create a standing penalty, depending on OU settings",
4833 {desc => "Authentication token", type => "string"},
4834 {desc => "Patron ID (optional if Phone Number specified)", type => "number"},
4835 {desc => "Additional note text (optional)", type => "string"},
4836 {desc => "penalty org unit ID (optional, default to top of org tree)",
4838 {desc => "Phone Number (optional)", type => "string"}
4840 return => {desc => "Event describing success or failure", type => "object"}
4844 sub mark_users_contact_invalid {
4845 my ($self, $conn, $auth, $patron_id, $addl_note, $penalty_ou, $contact) = @_;
4847 # This method invalidates an email address or a phone_number which
4848 # removes the bad email address or phone number, copying its contents
4849 # to a patron note, and institutes a standing penalty for "bad email"
4850 # or "bad phone number" which is cleared when the user is saved or
4851 # optionally only when the user is saved with an email address or
4852 # phone number (or staff manually delete the penalty).
4854 my $contact_type = ($self->api_name =~ /invalidate.(\w+)(\.|$)/)[0];
4856 my $e = new_editor(authtoken => $auth, xact => 1);
4857 return $e->die_event unless $e->checkauth;
4860 if (defined $patron_id && $patron_id ne "") {
4861 $howfind = {usr => $patron_id};
4862 } elsif (defined $contact && $contact ne "") {
4863 $howfind = {$contact_type => $contact};
4865 # Error out if no patron id set or no contact is set.
4866 return OpenILS::Event->new('BAD_PARAMS');
4869 return OpenILS::Utils::BadContact->mark_users_contact_invalid(
4870 $e, $contact_type, $howfind,
4871 $addl_note, $penalty_ou, $e->requestor->id
4875 # Putting the following method in open-ils.actor is a bad fit, except in that
4876 # it serves an interface that lives under 'actor' in the templates directory,
4877 # and in that there's nowhere else obvious to put it (open-ils.trigger is
4879 __PACKAGE__->register_method(
4880 api_name => "open-ils.actor.action_trigger.reactors.all_in_use",
4881 method => "get_all_at_reactors_in_use",
4886 { name => 'authtoken', type => 'string' }
4889 desc => 'list of reactor names', type => 'array'
4894 sub get_all_at_reactors_in_use {
4895 my ($self, $conn, $auth) = @_;
4897 my $e = new_editor(authtoken => $auth);
4898 $e->checkauth or return $e->die_event;
4899 return $e->die_event unless $e->allowed('VIEW_TRIGGER_EVENT_DEF');
4901 my $reactors = $e->json_query({
4903 atevdef => [{column => "reactor", transform => "distinct"}]
4905 from => {atevdef => {}}
4908 return $e->die_event unless ref $reactors eq "ARRAY";
4911 return [ map { $_->{reactor} } @$reactors ];
4914 __PACKAGE__->register_method(
4915 method => "filter_group_entry_crud",
4916 api_name => "open-ils.actor.filter_group_entry.crud",
4919 Provides CRUD access to filter group entry objects. These are not full accessible
4920 via PCRUD, since they requre "asq" objects for storing the query, and "asq" objects
4921 are not accessible via PCRUD (because they have no fields against which to link perms)
4924 {desc => "Authentication token", type => "string"},
4925 {desc => "Entry ID / Entry Object", type => "number"},
4926 {desc => "Additional note text (optional)", type => "string"},
4927 {desc => "penalty org unit ID (optional, default to top of org tree)",
4931 desc => "Entry fleshed with query on Create, Retrieve, and Uupdate. 1 on Delete",
4937 sub filter_group_entry_crud {
4938 my ($self, $conn, $auth, $arg) = @_;
4940 return OpenILS::Event->new('BAD_PARAMS') unless $arg;
4941 my $e = new_editor(authtoken => $auth, xact => 1);
4942 return $e->die_event unless $e->checkauth;
4948 my $grp = $e->retrieve_actor_search_filter_group($arg->grp)
4949 or return $e->die_event;
4951 return $e->die_event unless $e->allowed(
4952 'ADMIN_SEARCH_FILTER_GROUP', $grp->owner);
4954 my $query = $arg->query;
4955 $query = $e->create_actor_search_query($query) or return $e->die_event;
4956 $arg->query($query->id);
4957 my $entry = $e->create_actor_search_filter_group_entry($arg) or return $e->die_event;
4958 $entry->query($query);
4963 } elsif ($arg->ischanged) {
4965 my $entry = $e->retrieve_actor_search_filter_group_entry([
4968 flesh_fields => {asfge => ['grp']}
4970 ]) or return $e->die_event;
4972 return $e->die_event unless $e->allowed(
4973 'ADMIN_SEARCH_FILTER_GROUP', $entry->grp->owner);
4975 my $query = $e->update_actor_search_query($arg->query) or return $e->die_event;
4976 $arg->query($arg->query->id);
4977 $e->update_actor_search_filter_group_entry($arg) or return $e->die_event;
4978 $arg->query($query);
4983 } elsif ($arg->isdeleted) {
4985 my $entry = $e->retrieve_actor_search_filter_group_entry([
4988 flesh_fields => {asfge => ['grp', 'query']}
4990 ]) or return $e->die_event;
4992 return $e->die_event unless $e->allowed(
4993 'ADMIN_SEARCH_FILTER_GROUP', $entry->grp->owner);
4995 $e->delete_actor_search_filter_group_entry($entry) or return $e->die_event;
4996 $e->delete_actor_search_query($entry->query) or return $e->die_event;
5009 my $entry = $e->retrieve_actor_search_filter_group_entry([
5012 flesh_fields => {asfge => ['grp', 'query']}
5014 ]) or return $e->die_event;
5016 return $e->die_event unless $e->allowed(
5017 ['ADMIN_SEARCH_FILTER_GROUP', 'VIEW_SEARCH_FILTER_GROUP'],
5018 $entry->grp->owner);
5021 $entry->grp($entry->grp->id); # for consistency