1 package OpenILS::Application::Trigger;
2 use strict; use warnings;
3 use OpenILS::Application;
4 use base qw/OpenILS::Application/;
6 use OpenSRF::EX qw/:try/;
7 use OpenSRF::Utils::JSON;
9 use OpenSRF::AppSession;
10 use OpenSRF::Utils::SettingsClient;
11 use OpenSRF::Utils::Logger qw/$logger/;
12 use OpenSRF::Utils qw/:datetime/;
15 use DateTime::Format::ISO8601;
17 use OpenILS::Utils::Fieldmapper;
18 use OpenILS::Utils::CStoreEditor q/:funcs/;
19 use OpenILS::Application::Trigger::Event;
20 use OpenILS::Application::Trigger::EventGroup;
23 my $log = 'OpenSRF::Utils::Logger';
28 sub create_active_events_for_object {
34 my $granularity = shift;
35 my $user_data = shift;
37 my $ident = $target->Identity;
38 my $ident_value = $target->$ident();
40 my $editor = new_editor(xact=>1);
42 my $hooks = $editor->search_action_trigger_hook(
44 core_type => $target->json_hint
53 my %hook_hash = map { ($_->key, $_) } @$hooks;
55 my $orgs = $editor->json_query({ from => [ 'actor.org_unit_ancestors' => $location ] });
56 my $defs = $editor->search_action_trigger_event_definition(
57 { hook => [ keys %hook_hash ],
58 owner => [ map { $_->{id} } @$orgs ],
63 for my $def ( @$defs ) {
64 next if ($granularity && $def->granularity ne $granularity );
66 if ($def->usr_field && $def->opt_in_setting) {
67 my $ufield = $def->usr_field;
68 my $uid = $target->$ufield;
69 $uid = $uid->id if (ref $uid); # fleshed user object, unflesh it
71 my $opt_in_setting = $editor->search_actor_user_setting(
73 name => $def->opt_in_setting,
78 next unless (@$opt_in_setting);
81 my $date = DateTime->now;
83 if ($hook_hash{$def->hook}->passive eq 'f') {
85 if (my $dfield = $def->delay_field) {
86 if ($target->$dfield()) {
87 $date = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($target->$dfield) );
93 $date->add( seconds => interval_to_seconds($def->delay) );
96 my $event = Fieldmapper::action_trigger::event->new();
97 $event->target( $ident_value );
98 $event->event_def( $def->id );
99 $event->run_time( $date->strftime( '%F %T%z' ) );
100 $event->user_data( OpenSRF::Utils::JSON->perl2JSON($user_data) ) if (defined($user_data));
102 $editor->create_action_trigger_event( $event );
104 $client->respond( $event->id );
111 __PACKAGE__->register_method(
112 api_name => 'open-ils.trigger.event.autocreate',
113 method => 'create_active_events_for_object',
119 sub create_event_for_object_and_def {
122 my $definitions = shift;
124 my $location = shift;
125 my $user_data = shift;
127 my $ident = $target->Identity;
128 my $ident_value = $target->$ident();
130 my @active = ($self->api_name =~ /inactive/o) ? () : ( active => 't' );
132 my $editor = new_editor(xact=>1);
134 my $orgs = $editor->json_query({ from => [ 'actor.org_unit_ancestors' => $location ] });
135 my $defs = $editor->search_action_trigger_event_definition(
136 { id => $definitions,
137 owner => [ map { $_->{id} } @$orgs ],
142 my $hooks = $editor->search_action_trigger_hook(
143 { key => [ map { $_->hook } @$defs ],
144 core_type => $target->json_hint
148 my %hook_hash = map { ($_->key, $_) } @$hooks;
150 for my $def ( @$defs ) {
152 if ($def->usr_field && $def->opt_in_setting) {
153 my $ufield = $def->usr_field;
154 my $uid = $target->$ufield;
155 $uid = $uid->id if (ref $uid); # fleshed user object, unflesh it
157 my $opt_in_setting = $editor->search_actor_user_setting(
159 name => $def->opt_in_setting,
164 next unless (@$opt_in_setting);
167 my $date = DateTime->now;
169 if ($hook_hash{$def->hook}->passive eq 'f') {
171 if (my $dfield = $def->delay_field) {
172 if ($target->$dfield()) {
173 $date = DateTime::Format::ISO8601->new->parse_datetime( cleanse_ISO8601($target->$dfield) );
179 $date->add( seconds => interval_to_seconds($def->delay) );
182 my $event = Fieldmapper::action_trigger::event->new();
183 $event->target( $ident_value );
184 $event->event_def( $def->id );
185 $event->run_time( $date->strftime( '%F %T%z' ) );
186 $event->user_data( OpenSRF::Utils::JSON->perl2JSON($user_data) ) if (defined($user_data));
188 $editor->create_action_trigger_event( $event );
190 $client->respond( $event->id );
197 __PACKAGE__->register_method(
198 api_name => 'open-ils.trigger.event.autocreate.by_definition',
199 method => 'create_event_for_object_and_def',
204 __PACKAGE__->register_method(
205 api_name => 'open-ils.trigger.event.autocreate.by_definition.include_inactive',
206 method => 'create_event_for_object_and_def',
213 # Retrieves events by object, or object type + filter
214 # $object : a target object or object type (class hint)
216 # $filter : an optional hash of filters ... top level keys:
218 # filters on the atev objects, such as states or null-ness of timing
219 # fields. contains the effective default of:
220 # { state => 'pending' }
221 # an example, which overrides the default, and will find
222 # stale 'found' events:
223 # { state => 'found', update_time => { '<' => 'yesterday' } }
226 # filters on the atevdef object. contains the effective default of:
230 # filters on the hook object. no defaults, but there is a pinned,
231 # unchangeable filter based on the passed hint or object type (see
232 # $object above). an example for finding passive events:
236 # filters against the target field on the event. this can contain
237 # either an array of target ids (if you passed an object type, and
238 # not an object) or can contain a json_query that will return exactly
239 # a list of target-type ids. If you pass an object, the pkey value of
240 # that object will be used as a filter in addition to the filter passed
241 # in here. example filter for circs of user 1234 that are open:
242 # { select => { circ => ['id'] },
246 # checkin_time => undef,
248 # { stop_fines => undef },
249 # { stop_fines => { 'not in' => ['LOST','LONGOVERDUE','CLAIMSRETURNED'] } }
253 sub events_by_target {
257 my $filter = shift || {};
258 my $flesh_fields = shift || {};
259 my $flesh_depth = shift || 1;
261 my $obj_class = ref($object) || _fm_class_by_hint($object);
262 my $obj_hint = ref($object) ? _fm_hint_by_class(ref($object)) : $object;
264 my $object_ident_field = $obj_class->Identity;
267 select => { atev => ["id"] },
274 ath => { field => "key", fkey => "hook" }
280 "+ath" => { core_type => $obj_hint },
281 "+atevdef" => { active => 't' },
282 "+atev" => { state => 'pending' }
284 order_by => { "atev" => [ 'run_time', 'add_time' ] }
287 $query->{limit} = $filter->{limit} if defined $filter->{limit};
288 $query->{offset} = $filter->{offset} if defined $filter->{offset};
289 $query->{order_by} = $filter->{order_by} if defined $filter->{order_by};
292 # allow multiple 'target' filters
293 $query->{where}->{'+atev'}->{'-and'} = [];
295 # if we got a real object, filter on its pkey value
296 if (ref($object)) { # pass an object, require that target
297 push @{ $query->{where}->{'+atev'}->{'-and'} },
298 { target => $object->$object_ident_field }
301 # we have a fancy complex target filter or a list of target ids
302 if ($$filter{target}) {
303 push @{ $query->{where}->{'+atev'}->{'-and'} },
304 { target => {in => $$filter{target} } };
307 # pass no target filter or object, you get no events
308 if (!@{ $query->{where}->{'+atev'}->{'-and'} }) {
312 # any hook filters, other than the required core_type filter
313 if ($$filter{hook}) {
314 $query->{where}->{'+ath'}->{$_} = $$filter{hook}{$_}
315 for (grep { $_ ne 'core_type' } keys %{$$filter{hook}});
318 # any event_def filters. defaults to { active => 't' }
319 if ($$filter{event_def}) {
320 $query->{where}->{'+atevdef'}->{$_} = $$filter{event_def}{$_}
321 for (keys %{$$filter{event_def}});
324 # any event filters. defaults to { state => 'pending' }.
325 # don't overwrite '-and' used for multiple target filters above
326 if ($$filter{event}) {
327 $query->{where}->{'+atev'}->{$_} = $$filter{event}{$_}
328 for (grep { $_ ne '-and' } keys %{$$filter{event}});
331 my $e = new_editor(xact=>1);
333 my $events = $e->json_query($query);
335 $flesh_fields->{atev} = ['event_def'] unless $flesh_fields->{atev};
337 for my $id (@$events) {
338 my $event = $e->retrieve_action_trigger_event([
340 {flesh => $flesh_depth, flesh_fields => $flesh_fields}
343 (my $meth = $obj_class) =~ s/^Fieldmapper:://o;
345 $meth = 'retrieve_'.$meth;
347 $event->target($e->$meth($event->target));
348 $client->respond($event);
353 __PACKAGE__->register_method(
354 api_name => 'open-ils.trigger.events_by_target',
355 method => 'events_by_target',
361 sub _fm_hint_by_class {
363 return Fieldmapper->publish_fieldmapper->{$class}->{hint};
366 sub _fm_class_by_hint {
370 Fieldmapper->publish_fieldmapper->{$_}->{hint} eq $hint
371 } keys %{ Fieldmapper->publish_fieldmapper };
376 sub create_batch_events {
380 my $location_field = shift; # where to look for event_def.owner filtering ... circ_lib, for instance, where hook.core_type = circ
381 my $filter = shift || {};
382 my $granularity = shift;
383 my $user_data = shift;
385 my $active = ($self->api_name =~ /active/o) ? 1 : 0;
386 if ($active && !keys(%$filter)) {
387 $log->info("Active batch event creation requires a target filter but none was supplied to create_batch_events");
391 return undef unless ($key && $location_field);
393 my $editor = new_editor(xact=>1);
394 my $hooks = $editor->search_action_trigger_hook(
395 { passive => $active ? 'f' : 't', key => $key }
398 my %hook_hash = map { ($_->key, $_) } @$hooks;
400 my $defs = $editor->search_action_trigger_event_definition(
401 { hook => [ keys %hook_hash ], active => 't' },
404 my $orig_filter_and = [];
405 if ($$filter{'-and'}) {
406 for my $f ( @{ $$filter{'-and'} } ) {
407 push @$orig_filter_and, $f;
411 for my $def ( @$defs ) {
412 next if ($granularity && $def->granularity ne $granularity );
414 my $date = DateTime->now->subtract( seconds => interval_to_seconds($def->delay) );
416 # we may need to do some work to backport this to 1.2
417 $filter->{ $location_field } = { 'in' =>
419 select => { aou => [{ column => 'id', transform => 'actor.org_unit_descendants', result_field => 'id' }] },
421 where => { id => $def->owner }
425 my $run_time = 'now';
430 ->add( seconds => interval_to_seconds($def->delay) )
431 ->strftime( '%F %T%z' );
433 if ($def->max_delay) {
434 my @times = sort {$a <=> $b} interval_to_seconds($def->delay), interval_to_seconds($def->max_delay);
435 $filter->{ $def->delay_field } = {
437 DateTime->now->subtract( seconds => $times[1] )->strftime( '%F %T%z' ),
438 DateTime->now->subtract( seconds => $times[0] )->strftime( '%F %T%z' )
442 $filter->{ $def->delay_field } = {
443 '<=' => DateTime->now->subtract( seconds => interval_to_seconds($def->delay) )->strftime( '%F %T%z' )
448 my $class = _fm_class_by_hint($hook_hash{$def->hook}->core_type);
450 # filter where this target has an event (and it's pending, for active hooks)
451 $$filter{'-and'} = [];
452 for my $f ( @$orig_filter_and ) {
453 push @{ $$filter{'-and'} }, $f;
456 my $join = { 'join' => {
459 fkey => $class->Identity,
461 filter => { event_def => $def->id }
465 push @{ $filter->{'-and'} }, { '+atev' => { id => undef } };
467 if ($def->usr_field && $def->opt_in_setting) {
468 push @{ $filter->{'-and'} }, {
472 name => $def->opt_in_setting,
473 usr => { '=' => { '+' . $hook_hash{$def->hook}->core_type => $def->usr_field } },
480 $class =~ s/^Fieldmapper:://o;
482 my $method = 'search_'. $class;
484 # for cleaner logging
485 my $def_id = $def->id;
486 my $hook = $def->hook;
488 $logger->info("trigger: create_batch_events() collecting object IDs for def=$def_id / hook=$hook");
490 my $object_ids = $editor->$method( [$filter, $join], {idlist => 1, timeout => 10800} );
493 $logger->info("trigger: create_batch_events() fetched ".scalar(@$object_ids)." object IDs for def=$def_id / hook=$hook");
495 $logger->warn("trigger: create_batch_events() timeout occurred collecting object IDs for def=$def_id / hook=$hook");
498 for my $o_id (@$object_ids) {
500 my $event = Fieldmapper::action_trigger::event->new();
501 $event->target( $o_id );
502 $event->event_def( $def->id );
503 $event->run_time( $run_time );
504 $event->user_data( OpenSRF::Utils::JSON->perl2JSON($user_data) ) if (defined($user_data));
506 $editor->create_action_trigger_event( $event );
508 $client->respond( $event->id );
511 $logger->info("trigger: create_batch_events() successfully created events for def=$def_id / hook=$hook");
514 $logger->info("trigger: create_batch_events() done creating events");
520 __PACKAGE__->register_method(
521 api_name => 'open-ils.trigger.passive.event.autocreate.batch',
522 method => 'create_batch_events',
528 __PACKAGE__->register_method(
529 api_name => 'open-ils.trigger.active.event.autocreate.batch',
530 method => 'create_batch_events',
536 sub fire_single_event {
539 my $event_id = shift;
541 my $e = OpenILS::Application::Trigger::Event->new($event_id);
543 if ($e->validate->valid) {
544 $logger->info("trigger: Event is valid, reacting...");
548 $e->editor->disconnect;
549 OpenILS::Application::Trigger::Event->ClearObjectCache();
553 reacted => $e->reacted,
554 cleanedup => $e->cleanedup,
558 __PACKAGE__->register_method(
559 api_name => 'open-ils.trigger.event.fire',
560 method => 'fire_single_event',
565 sub fire_event_group {
570 my $e = OpenILS::Application::Trigger::EventGroup->new(@$events);
572 if ($e->validate->valid) {
573 $logger->info("trigger: Event group is valid, reacting...");
577 $e->editor->disconnect;
578 OpenILS::Application::Trigger::Event->ClearObjectCache();
582 reacted => $e->reacted,
583 cleanedup => $e->cleanedup,
584 events => [map { $_->event } @{$e->events}]
587 __PACKAGE__->register_method(
588 api_name => 'open-ils.trigger.event_group.fire',
589 method => 'fire_event_group',
597 my $granularity = shift;
598 my $granflag = shift;
600 my $query = [{ state => 'pending', run_time => {'<' => 'now'} }, { order_by => { atev => [ qw/run_time add_time/] }, 'join' => 'atevdef' }];
602 if (defined $granularity) {
604 $query->[0]->{'+atevdef'} = {granularity => $granularity};
606 $query->[0]->{'+atevdef'} = {'-or' => [ {granularity => $granularity}, {granularity => undef} ] };
609 $query->[0]->{'+atevdef'} = {granularity => undef};
612 return new_editor(xact=>1)->search_action_trigger_event(
613 $query, { idlist=> 1, timeout => 7200, substream => 1 }
616 __PACKAGE__->register_method(
617 api_name => 'open-ils.trigger.event.find_pending',
618 method => 'pending_events',
625 my $granularity = shift;
626 my $granflag = shift;
628 my ($events) = $self->method_lookup('open-ils.trigger.event.find_pending')->run($granularity, $granflag);
630 my %groups = ( '*' => [] );
633 $logger->info("trigger: grouped_events found ".scalar(@$events)." pending events to process");
635 $logger->warn("trigger: grouped_events timed out loading pending events");
639 for my $e_id ( @$events ) {
640 $logger->info("trigger: processing event $e_id");
642 # let the client know we're still chugging along TODO add osrf support for method_lookup $client's
643 $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
647 $e = OpenILS::Application::Trigger::Event->new($e_id);
649 $logger->error("trigger: Event creation failed with ".shift());
655 $e->build_environment;
657 $logger->error("trigger: Event environment building failed with ".shift());
660 if (my $group = $e->event->event_def->group_field) {
662 # split the grouping link steps
663 my @steps = split /\./, $group;
664 my $group_field = pop(@steps); # we didn't flesh to this, it's a field not an object
666 # find the grouping object
667 my $node = $e->target;
668 $node = $node->$_() for ( @steps );
670 # get the grouping value for the grouping object on this event
671 my $ident_value = $node->$group_field();
673 # push this event onto the event+grouping_value stack
674 $groups{$e->event->event_def->id}{$ident_value} ||= [];
675 push @{ $groups{$e->event->event_def->id}{$ident_value} }, $e;
677 # it's a non-grouped event
678 push @{ $groups{'*'} }, $e;
681 $e->editor->disconnect;
684 OpenILS::Application::Trigger::Event->ClearObjectCache();
687 __PACKAGE__->register_method(
688 api_name => 'open-ils.trigger.event.find_pending_by_group',
689 method => 'grouped_events',
696 my $granularity = shift;
697 my $granflag = shift;
699 my ($groups) = $self->method_lookup('open-ils.trigger.event.find_pending_by_group')->run($granularity, $granflag);
701 # Could report on how the "found" events were grouped, but who's going to
702 # consume that information?
703 for my $key (keys %$groups) {
704 if (@{ $$groups{$key} }) {
705 $client->respond({"status" => "found"});
710 for my $def ( keys %$groups ) {
712 $logger->info("trigger: run_all_events firing un-grouped events");
713 for my $event ( @{ $$groups{'*'} } ) {
717 ->method_lookup('open-ils.trigger.event.fire')
721 $logger->error("trigger: event firing failed with ".shift());
724 $logger->info("trigger: run_all_events completed firing un-grouped events");
727 my $defgroup = $$groups{$def};
728 $logger->info("trigger: run_all_events firing events for grouped event def=$def");
729 for my $ident ( keys %$defgroup ) {
733 ->method_lookup('open-ils.trigger.event_group.fire')
734 ->run($$defgroup{$ident})
737 $logger->error("trigger: event firing failed with ".shift());
740 $logger->info("trigger: run_all_events completed firing events for grouped event def=$def");
744 __PACKAGE__->register_method(
745 api_name => 'open-ils.trigger.event.run_all_pending',
746 method => 'run_all_events',