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/:level/;
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
45 my %hook_hash = map { ($_->key, $_) } @$hooks;
47 my $orgs = $editor->json_query({ from => [ 'actor.org_unit_ancestors' => $location ] });
48 my $defs = $editor->search_action_trigger_event_definition(
49 { hook => [ keys %hook_hash ],
50 owner => [ map { $_->{id} } @$orgs ],
55 for my $def ( @$defs ) {
57 my $date = DateTime->now;
59 if ($hook_hash{$def->hook}->passive eq 'f') {
61 if (my $dfield = $def->delay_field) {
62 if ($target->$dfield()) {
63 $date = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($target->$dfield) );
69 $date->add( seconds => interval_to_seconds($def->delay) );
72 my $event = Fieldmapper::action_trigger::event->new();
73 $event->target( $ident_value );
74 $event->event_def( $def->id );
75 $event->run_time( $date->strftime( '%F %T%z' ) );
77 $editor->create_action_trigger_event( $event );
79 $client->respond( $event->id );
86 __PACKAGE__->register_method(
87 api_name => 'open-ils.trigger.event.autocreate',
88 method => 'create_active_events_for_object',
95 # Retrieves events by object, or object type + filter
96 # $object : a target object or object type (class hint)
98 # $filter : an optional hash of filters ... top level keys:
100 # filters on the atev objects, such as states or null-ness of timing
101 # fields. contains the effective default of:
102 # { state => 'pending' }
103 # an example, which overrides the default, and will find
104 # stale 'found' events:
105 # { state => 'found', update_time => { '<' => 'yesterday' } }
108 # filters on the atevdef object. contains the effective default of:
112 # filters on the hook object. no defaults, but there is a pinned,
113 # unchangeable filter based on the passed hint or object type (see
114 # $object above). an example for finding passive events:
118 # filters against the target field on the event. this can contain
119 # either an array of target ids (if you passed an object type, and
120 # not an object) or can contain a json_query that will return exactly
121 # a list of target-type ids. If you pass an object, the pkey value of
122 # that object will be used as a filter in addition to the filter passed
123 # in here. example filter for circs of user 1234 that are open:
124 # { select => { circ => ['id'] },
128 # checkin_time => undef,
130 # { stop_fines => undef },
131 # { stop_fines => { 'not in' => ['LOST','LONGOVERDUE','CLAIMSRETURNED'] } }
135 sub events_by_target {
139 my $filter = shift || {};
141 my $obj_class = ref($object) || _fm_class_by_hint($object);
143 my $object_ident_field = $obj_class->Identity;
146 select => { atev => ["id"] },
153 ath => { field => "key", fkey => "hook" }
159 "+ath" => { core_type=> $object_ident_field },
160 "+atevdef" => { active => 't' },
161 "+atev" => { state => 'pending' }
163 order_by => { "+atev" => [ 'run_time' ] },
168 # allow multiple 'target' filters
169 $query->{where}->{'+atev'}->{'-and'} = [];
171 # if we got a real object, filter on its pkey value
172 if (ref($object)) { # pass an object, require that target
173 push @{ $query->{where}->{'+atev'}->{'-and'} },
174 { target => $object->$object_ident_field }
177 # we have a fancy complex target filter or a list of target ids
178 if ($$filter{target}) {
179 push @{ $query->{where}->{'+atev'}->{'-and'} },
180 { target => {in => $$filter{target} } };
183 # pass no target filter or object, you get no events
184 if (!@{ $query->{where}->{'+atev'}->{'-and'} }) {
188 # any hook filters, other than the required core_type filter
189 if ($$filter{hook}) {
190 $query->{where}->{'+ath'}->{$_} = $$filter{hook}{$_}
191 for (grep { $_ ne 'core_type' } keys %{$$filter{hook}});
194 # any event_def filters. defaults to { active => 't' }
195 if ($$filter{event_def}) {
196 $query->{where}->{'+atevdef'}->{$_} = $$filter{event_def}{$_}
197 for (keys %{$$filter{event_def}});
200 # any event filters. defaults to { state => 'pending' }.
201 # don't overwrite '-and' used for multiple target filters above
202 if ($$filter{event}) {
203 $query->{where}->{'+atev'}->{$_} = $$filter{event}{$_}
204 for (grep { $_ ne '-and' } keys %{$$filter{event}});
207 my $e = new_editor();
209 my $events = $e->json_query($query);
211 for my $id (@$events) {
212 my $event = $e->retrieve_action_trigger_event([
214 {flesh => 1, flesh_fields => {atev => ['event_def']}}
217 (my $meth = $obj_class) =~ s/^Fieldmapper:://o;
219 $meth = 'retrieve_'.$meth;
221 $event->target($e->$meth($event->target));
222 $client->respond($event);
227 __PACKAGE__->register_method(
228 api_name => 'open-ils.trigger.events_by_target',
229 method => 'events_by_target',
236 sub _fm_class_by_hint {
240 Fieldmapper->publish_fieldmapper->{$_}->{hint} eq $hint
241 } keys %{ Fieldmapper->publish_fieldmapper };
246 sub create_passive_events {
250 my $location_field = shift; # where to look for event_def.owner filtering ... circ_lib, for instance, where hook.core_type = circ
251 my $filter = shift || {};
253 return undef unless ($key && $location_field);
255 my $editor = new_editor(xact=>1);
256 my $hooks = $editor->search_action_trigger_hook(
257 { passive => 't', key => $key }
260 my %hook_hash = map { ($_->key, $_) } @$hooks;
262 my $defs = $editor->search_action_trigger_event_definition(
263 { hook => [ keys %hook_hash ], active => 't' },
266 for my $def ( @$defs ) {
268 my $date = DateTime->now->subtract( seconds => interval_to_seconds($def->delay) );
270 # we may need to do some work to backport this to 1.2
271 $filter->{ $location_field } = { 'in' =>
273 select => { aou => [{ column => 'id', transform => 'actor.org_unit_descendents', result_field => 'id' }] },
275 where => { id => $def->owner }
279 $filter->{ $def->delay_field } = {
282 ->subtract( seconds => interval_to_seconds($def->delay) )
283 ->strftime( '%F %T%z' )
286 my $class = _fm_class_by_hint($hook_hash{$def->hook}->core_type);
287 $class =~ s/^Fieldmapper:://o;
290 my $method = 'search_'. $class;
291 my $objects = $editor->$method( $filter );
293 for my $o (@$objects) {
295 my $ident = $o->Identity;
296 my $ident_value = $o->$ident();
298 my $previous = $editor->search_action_trigger_event({
299 event_def => $def->id,
300 target => $ident_value
304 # only allow one event of type $def for each target
305 next if (@$previous);
307 my $event = Fieldmapper::action_trigger::event->new();
308 $event->target( $ident_value );
309 $event->event_def( $def->id );
310 $event->run_time( 'now' );
312 $editor->create_action_trigger_event( $event );
314 $client->respond( $event->id );
322 __PACKAGE__->register_method(
323 api_name => 'open-ils.trigger.passive.event.autocreate',
324 method => 'create_passive_events',
331 sub fire_single_event {
334 my $event_id = shift;
336 my $e = OpenILS::Application::Trigger::Event->new($event_id);
338 if ($e->validate->valid) {
344 reacted => $e->reacted,
345 cleanedup => $e->cleanedup,
349 __PACKAGE__->register_method(
350 api_name => 'open-ils.trigger.event.fire',
351 method => 'fire_single_event',
356 sub fire_event_group {
361 my $e = OpenILS::Application::Trigger::EventGroup->new(@$events);
363 if ($e->validate->valid) {
369 reacted => $e->reacted,
370 cleanedup => $e->cleanedup,
371 events => [map { $_->event } @{$e->events}]
374 __PACKAGE__->register_method(
375 api_name => 'open-ils.trigger.event_group.fire',
376 method => 'fire_event_group',
385 my $editor = new_editor();
387 return $editor->search_action_trigger_event([
388 { state => 'pending', run_time => {'<' => 'now'} },
392 __PACKAGE__->register_method(
393 api_name => 'open-ils.trigger.event.find_pending',
394 method => 'pending_events',
403 my ($events) = $self->method_lookup('open-ils.trigger.event.find_pending')->run();
405 my %groups = ( '*' => [] );
407 for my $e_id ( @$events ) {
408 my $e = OpenILS::Application::Trigger::Event->new($e_id);
409 if ($e->validate->valid) {
410 if (my $group = $e->event->event_def->group_field) {
412 # split the grouping link steps
413 my @steps = split '.', $group;
415 # find the grouping object
416 my $node = $e->target;
417 $node = $node->$_() for ( @steps );
419 # get the pkey value for the grouping object on this event
420 my $node_ident = $node->Identity;
421 my $ident_value = $node->$node_ident();
423 # push this event onto the event+grouping_pkey_value stack
424 $groups{$e->event->event_def->id}{$ident_value} ||= [];
425 push @{ $groups{$e->event->event_def->id}{$ident_value} }, $e;
427 # it's a non-grouped event
428 push @{ $groups{'*'} }, $e;
435 __PACKAGE__->register_method(
436 api_name => 'open-ils.trigger.event.find_pending_by_group',
437 method => 'grouped_events',
445 my ($groups) = $self->method_lookup('open-ils.trigger.event.find_pending_by_group')->run();
447 for my $def ( %$groups ) {
449 for my $event ( @{ $$groups{'*'} } ) {
452 ->method_lookup('open-ils.trigger.event.fire')
457 my $defgroup = $$groups{$def};
458 for my $ident ( keys %$defgroup ) {
461 ->method_lookup('open-ils.trigger.event_group.fire')
462 ->run($$defgroup{$ident})
470 __PACKAGE__->register_method(
471 api_name => 'open-ils.trigger.event.run_all_pending',
472 method => 'run_all_events',