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/;
8 use OpenSRF::AppSession;
9 use OpenSRF::Utils::SettingsClient;
10 use OpenSRF::Utils::Logger qw/$logger/;
11 use OpenSRF::Utils qw/:datetime/;
14 use DateTime::Format::ISO8601;
16 use OpenILS::Utils::Fieldmapper;
17 use OpenILS::Utils::CStoreEditor q/:funcs/;
18 use OpenILS::Application::Trigger::Event;
19 use OpenILS::Application::Trigger::EventGroup;
22 my $log = 'OpenSRF::Utils::Logger';
27 sub create_active_events_for_object {
34 my $ident = $target->Identity;
35 my $ident_value = $target->$ident();
37 my $editor = new_editor(xact=>1);
39 my $hooks = $editor->search_action_trigger_hook(
41 core_type => $target->json_hint
50 my %hook_hash = map { ($_->key, $_) } @$hooks;
52 my $orgs = $editor->json_query({ from => [ 'actor.org_unit_ancestors' => $location ] });
53 my $defs = $editor->search_action_trigger_event_definition(
54 { hook => [ keys %hook_hash ],
55 owner => [ map { $_->{id} } @$orgs ],
60 for my $def ( @$defs ) {
62 if ($def->usr_field && $def->opt_in_setting) {
63 my $ufield = $def->usr_field;
64 my $uid = $target->$ufield;
65 $uid = $uid->id if (ref $uid); # fleshed user object, unflesh it
67 my $opt_in_setting = $editor->search_actor_usr_setting(
69 name => $def->opt_in_setting,
74 next unless (@$opt_in_setting);
77 my $date = DateTime->now;
79 if ($hook_hash{$def->hook}->passive eq 'f') {
81 if (my $dfield = $def->delay_field) {
82 if ($target->$dfield()) {
83 $date = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($target->$dfield) );
89 $date->add( seconds => interval_to_seconds($def->delay) );
92 my $event = Fieldmapper::action_trigger::event->new();
93 $event->target( $ident_value );
94 $event->event_def( $def->id );
95 $event->run_time( $date->strftime( '%F %T%z' ) );
97 $editor->create_action_trigger_event( $event );
99 $client->respond( $event->id );
106 __PACKAGE__->register_method(
107 api_name => 'open-ils.trigger.event.autocreate',
108 method => 'create_active_events_for_object',
114 sub create_event_for_object_and_def {
117 my $definitions = shift;
119 my $location = shift;
121 my $ident = $target->Identity;
122 my $ident_value = $target->$ident();
124 my @active = ($self->api_name =~ /inactive/o) ? () : ( active => 't' );
126 my $editor = new_editor(xact=>1);
128 my $orgs = $editor->json_query({ from => [ 'actor.org_unit_ancestors' => $location ] });
129 my $defs = $editor->search_action_trigger_event_definition(
130 { id => $definitions,
131 owner => [ map { $_->{id} } @$orgs ],
136 my $hooks = $editor->search_action_trigger_hook(
137 { key => [ map { $_->hook } @$defs ],
138 core_type => $target->json_hint
142 my %hook_hash = map { ($_->key, $_) } @$hooks;
144 for my $def ( @$defs ) {
146 if ($def->usr_field && $def->opt_in_setting) {
147 my $ufield = $def->usr_field;
148 my $uid = $target->$ufield;
149 $uid = $uid->id if (ref $uid); # fleshed user object, unflesh it
151 my $opt_in_setting = $editor->search_actor_usr_setting(
153 name => $def->opt_in_setting,
158 next unless (@$opt_in_setting);
161 my $date = DateTime->now;
163 if ($hook_hash{$def->hook}->passive eq 'f') {
165 if (my $dfield = $def->delay_field) {
166 if ($target->$dfield()) {
167 $date = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($target->$dfield) );
173 $date->add( seconds => interval_to_seconds($def->delay) );
176 my $event = Fieldmapper::action_trigger::event->new();
177 $event->target( $ident_value );
178 $event->event_def( $def->id );
179 $event->run_time( $date->strftime( '%F %T%z' ) );
181 $editor->create_action_trigger_event( $event );
183 $client->respond( $event->id );
190 __PACKAGE__->register_method(
191 api_name => 'open-ils.trigger.event.autocreate.by_definition',
192 method => 'create_event_for_object_and_def',
197 __PACKAGE__->register_method(
198 api_name => 'open-ils.trigger.event.autocreate.by_definition.include_inactive',
199 method => 'create_event_for_object_and_def',
206 # Retrieves events by object, or object type + filter
207 # $object : a target object or object type (class hint)
209 # $filter : an optional hash of filters ... top level keys:
211 # filters on the atev objects, such as states or null-ness of timing
212 # fields. contains the effective default of:
213 # { state => 'pending' }
214 # an example, which overrides the default, and will find
215 # stale 'found' events:
216 # { state => 'found', update_time => { '<' => 'yesterday' } }
219 # filters on the atevdef object. contains the effective default of:
223 # filters on the hook object. no defaults, but there is a pinned,
224 # unchangeable filter based on the passed hint or object type (see
225 # $object above). an example for finding passive events:
229 # filters against the target field on the event. this can contain
230 # either an array of target ids (if you passed an object type, and
231 # not an object) or can contain a json_query that will return exactly
232 # a list of target-type ids. If you pass an object, the pkey value of
233 # that object will be used as a filter in addition to the filter passed
234 # in here. example filter for circs of user 1234 that are open:
235 # { select => { circ => ['id'] },
239 # checkin_time => undef,
241 # { stop_fines => undef },
242 # { stop_fines => { 'not in' => ['LOST','LONGOVERDUE','CLAIMSRETURNED'] } }
246 sub events_by_target {
250 my $filter = shift || {};
251 my $flesh_fields = shift || {};
252 my $flesh_depth = shift || 1;
254 my $obj_class = ref($object) || _fm_class_by_hint($object);
255 my $obj_hint = ref($object) ? _fm_hint_by_class(ref($object)) : $object;
257 my $object_ident_field = $obj_class->Identity;
260 select => { atev => ["id"] },
267 ath => { field => "key", fkey => "hook" }
273 "+ath" => { core_type => $obj_hint },
274 "+atevdef" => { active => 't' },
275 "+atev" => { state => 'pending' }
277 order_by => { "atev" => [ 'run_time', 'add_time' ] }
280 $query->{limit} = $filter->{limit} if defined $filter->{limit};
281 $query->{offset} = $filter->{offset} if defined $filter->{offset};
282 $query->{order_by} = $filter->{order_by} if defined $filter->{order_by};
285 # allow multiple 'target' filters
286 $query->{where}->{'+atev'}->{'-and'} = [];
288 # if we got a real object, filter on its pkey value
289 if (ref($object)) { # pass an object, require that target
290 push @{ $query->{where}->{'+atev'}->{'-and'} },
291 { target => $object->$object_ident_field }
294 # we have a fancy complex target filter or a list of target ids
295 if ($$filter{target}) {
296 push @{ $query->{where}->{'+atev'}->{'-and'} },
297 { target => {in => $$filter{target} } };
300 # pass no target filter or object, you get no events
301 if (!@{ $query->{where}->{'+atev'}->{'-and'} }) {
305 # any hook filters, other than the required core_type filter
306 if ($$filter{hook}) {
307 $query->{where}->{'+ath'}->{$_} = $$filter{hook}{$_}
308 for (grep { $_ ne 'core_type' } keys %{$$filter{hook}});
311 # any event_def filters. defaults to { active => 't' }
312 if ($$filter{event_def}) {
313 $query->{where}->{'+atevdef'}->{$_} = $$filter{event_def}{$_}
314 for (keys %{$$filter{event_def}});
317 # any event filters. defaults to { state => 'pending' }.
318 # don't overwrite '-and' used for multiple target filters above
319 if ($$filter{event}) {
320 $query->{where}->{'+atev'}->{$_} = $$filter{event}{$_}
321 for (grep { $_ ne '-and' } keys %{$$filter{event}});
324 my $e = new_editor();
326 my $events = $e->json_query($query);
328 $flesh_fields->{atev} = ['event_def'] unless $flesh_fields->{atev};
330 for my $id (@$events) {
331 my $event = $e->retrieve_action_trigger_event([
333 {flesh => $flesh_depth, flesh_fields => $flesh_fields}
336 (my $meth = $obj_class) =~ s/^Fieldmapper:://o;
338 $meth = 'retrieve_'.$meth;
340 $event->target($e->$meth($event->target));
341 $client->respond($event);
346 __PACKAGE__->register_method(
347 api_name => 'open-ils.trigger.events_by_target',
348 method => 'events_by_target',
354 sub _fm_hint_by_class {
356 return Fieldmapper->publish_fieldmapper->{$class}->{hint};
359 sub _fm_class_by_hint {
363 Fieldmapper->publish_fieldmapper->{$_}->{hint} eq $hint
364 } keys %{ Fieldmapper->publish_fieldmapper };
369 sub create_batch_events {
373 my $location_field = shift; # where to look for event_def.owner filtering ... circ_lib, for instance, where hook.core_type = circ
374 my $filter = shift || {};
375 my $granularity = shift;
377 my $active = ($self->api_name =~ /active/o) ? 1 : 0;
378 if ($active && !keys(%$filter)) {
379 $log->info("Active batch event creation requires a target filter but none was supplied to create_batch_events");
383 return undef unless ($key && $location_field);
385 my $editor = new_editor(xact=>1);
386 my $hooks = $editor->search_action_trigger_hook(
387 { passive => $active ? 'f' : 't', key => $key }
390 my %hook_hash = map { ($_->key, $_) } @$hooks;
392 my $defs = $editor->search_action_trigger_event_definition(
393 { hook => [ keys %hook_hash ], active => 't' },
396 my $orig_filter_and = [];
397 if ($$filter{'-and'}) {
398 for my $f ( @{ $$filter{'-and'} } ) {
399 push @$orig_filter_and, $f;
403 for my $def ( @$defs ) {
404 next if ($granularity && $def->granularity ne $granularity );
406 my $date = DateTime->now->subtract( seconds => interval_to_seconds($def->delay) );
408 # we may need to do some work to backport this to 1.2
409 $filter->{ $location_field } = { 'in' =>
411 select => { aou => [{ column => 'id', transform => 'actor.org_unit_descendants', result_field => 'id' }] },
413 where => { id => $def->owner }
417 my $run_time = 'now';
422 ->add( seconds => interval_to_seconds($def->delay) )
423 ->strftime( '%F %T%z' );
425 if ($def->max_delay) {
426 my @times = sort {$a <=> $b} interval_to_seconds($def->delay), interval_to_seconds($def->max_delay);
427 $filter->{ $def->delay_field } = {
429 DateTime->now->subtract( seconds => $times[1] )->strftime( '%F %T%z' ),
430 DateTime->now->subtract( seconds => $times[0] )->strftime( '%F %T%z' )
434 $filter->{ $def->delay_field } = {
435 '<=' => DateTime->now->subtract( seconds => interval_to_seconds($def->delay) )->strftime( '%F %T%z' )
440 my $class = _fm_class_by_hint($hook_hash{$def->hook}->core_type);
442 # filter where this target has an event (and it's pending, for active hooks)
443 $$filter{'-and'} = [];
444 for my $f ( @$orig_filter_and ) {
445 push @{ $$filter{'-and'} }, $f;
448 push @{ $filter->{'-and'} }, {
452 event_def => $def->id,
453 target => { '=' => { '+' . $hook_hash{$def->hook}->core_type => $class->Identity } },
454 ($active ? (state => 'pending') : ())
459 if ($def->usr_field && $def->opt_in_setting) {
460 push @{ $filter->{'-and'} }, {
465 usr => { '=' => { '+' . $hook_hash{$def->hook}->core_type => $def->usr_field } },
472 $class =~ s/^Fieldmapper:://o;
475 my $method = 'search_'. $class;
476 my $object_ids = $editor->$method( $filter, {idlist => 1, timeout => 1800} );
478 for my $o_id (@$object_ids) {
480 my $event = Fieldmapper::action_trigger::event->new();
481 $event->target( $o_id );
482 $event->event_def( $def->id );
483 $event->run_time( $run_time );
485 $editor->create_action_trigger_event( $event );
487 $client->respond( $event->id );
495 __PACKAGE__->register_method(
496 api_name => 'open-ils.trigger.passive.event.autocreate.batch',
497 method => 'create_batch_events',
503 __PACKAGE__->register_method(
504 api_name => 'open-ils.trigger.active.event.autocreate.batch',
505 method => 'create_batch_events',
511 sub fire_single_event {
514 my $event_id = shift;
516 my $e = OpenILS::Application::Trigger::Event->new($event_id);
518 if ($e->validate->valid) {
519 $logger->info("Event is valid, reacting...");
523 $e->editor->disconnect;
527 reacted => $e->reacted,
528 cleanedup => $e->cleanedup,
532 __PACKAGE__->register_method(
533 api_name => 'open-ils.trigger.event.fire',
534 method => 'fire_single_event',
539 sub fire_event_group {
544 my $e = OpenILS::Application::Trigger::EventGroup->new(@$events);
546 if ($e->validate->valid) {
547 $logger->info("Event group is valid, reacting...");
551 $e->editor->disconnect;
555 reacted => $e->reacted,
556 cleanedup => $e->cleanedup,
557 events => [map { $_->event } @{$e->events}]
560 __PACKAGE__->register_method(
561 api_name => 'open-ils.trigger.event_group.fire',
562 method => 'fire_event_group',
570 my $granularity = shift;
572 my $editor = new_editor();
574 my $query = [{ state => 'pending', run_time => {'<' => 'now'} }, { order_by => { atev => [ qw/run_time add_time/] }, 'join' => 'atevdef' }];
576 if (defined $granularity) {
577 $query->[0]->{'+atevdef'} = {'-or' => [ {granularity => $granularity}, {granularity => undef} ] };
579 $query->[0]->{'+atevdef'} = {granularity => undef};
582 return $editor->search_action_trigger_event(
583 $query, { idlist=> 1, timeout => 1800 }
586 __PACKAGE__->register_method(
587 api_name => 'open-ils.trigger.event.find_pending',
588 method => 'pending_events',
595 my $granularity = shift;
597 my ($events) = $self->method_lookup('open-ils.trigger.event.find_pending')->run($granularity);
599 my %groups = ( '*' => [] );
601 for my $e_id ( @$events ) {
602 $logger->info("trigger: processing event $e_id");
604 # let the client know we're still chugging along TODO add osrf support for method_lookup $client's
605 $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
609 $e = OpenILS::Application::Trigger::Event->new($e_id);
611 $logger->error("Event creation failed with ".shift());
617 $e->build_environment;
619 $logger->error("Event environment building failed with ".shift());
622 if (my $group = $e->event->event_def->group_field) {
624 # split the grouping link steps
625 my @steps = split /\./, $group;
627 # find the grouping object
628 my $node = $e->target;
629 $node = $node->$_() for ( @steps );
631 # get the pkey value for the grouping object on this event
632 my $node_ident = $node->Identity;
633 my $ident_value = $node->$node_ident();
635 # push this event onto the event+grouping_pkey_value stack
636 $groups{$e->event->event_def->id}{$ident_value} ||= [];
637 push @{ $groups{$e->event->event_def->id}{$ident_value} }, $e;
639 # it's a non-grouped event
640 push @{ $groups{'*'} }, $e;
643 $e->editor->disconnect;
648 __PACKAGE__->register_method(
649 api_name => 'open-ils.trigger.event.find_pending_by_group',
650 method => 'grouped_events',
657 my $granularity = shift;
659 my ($groups) = $self->method_lookup('open-ils.trigger.event.find_pending_by_group')->run($granularity);
661 for my $def ( keys %$groups ) {
663 for my $event ( @{ $$groups{'*'} } ) {
667 ->method_lookup('open-ils.trigger.event.fire')
671 $logger->error("event firing failed with ".shift());
675 my $defgroup = $$groups{$def};
676 for my $ident ( keys %$defgroup ) {
680 ->method_lookup('open-ils.trigger.event_group.fire')
681 ->run($$defgroup{$ident})
684 $logger->error("event firing failed with ".shift());
692 __PACKAGE__->register_method(
693 api_name => 'open-ils.trigger.event.run_all_pending',
694 method => 'run_all_events',