]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Trigger.pm
added support for optional limit, offset, and order_by filters to open-ils.trigger...
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Trigger.pm
1 package OpenILS::Application::Trigger;
2 use strict; use warnings;
3 use OpenILS::Application;
4 use base qw/OpenILS::Application/;
5
6 use OpenSRF::EX qw/:try/;
7
8 use OpenSRF::AppSession;
9 use OpenSRF::Utils::SettingsClient;
10 use OpenSRF::Utils::Logger qw/$logger/;
11 use OpenSRF::Utils qw/:datetime/;
12
13 use DateTime;
14 use DateTime::Format::ISO8601;
15
16 use OpenILS::Utils::Fieldmapper;
17 use OpenILS::Utils::CStoreEditor q/:funcs/;
18 use OpenILS::Application::Trigger::Event;
19 use OpenILS::Application::Trigger::EventGroup;
20
21
22 my $log = 'OpenSRF::Utils::Logger';
23
24 sub initialize {}
25 sub child_init {}
26
27 sub create_active_events_for_object {
28     my $self = shift;
29     my $client = shift;
30     my $key = shift;
31     my $target = shift;
32     my $location = shift;
33
34     my $ident = $target->Identity;
35     my $ident_value = $target->$ident();
36
37     my $editor = new_editor(xact=>1);
38
39     my $hooks = $editor->search_action_trigger_hook(
40         { key       => $key,
41           core_type => $target->json_hint
42         }
43     );
44
45     unless(@$hooks) {
46         $editor->rollback;
47         return undef;
48     }
49
50     my %hook_hash = map { ($_->key, $_) } @$hooks;
51
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  ],
56           active => 't'
57         }
58     );
59
60     for my $def ( @$defs ) {
61
62         my $date = DateTime->now;
63
64         if ($hook_hash{$def->hook}->passive eq 'f') {
65
66             if (my $dfield = $def->delay_field) {
67                 if ($target->$dfield()) {
68                     $date = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($target->$dfield) );
69                 } else {
70                     next;
71                 }
72             }
73
74             $date->add( seconds => interval_to_seconds($def->delay) );
75         }
76
77         my $event = Fieldmapper::action_trigger::event->new();
78         $event->target( $ident_value );
79         $event->event_def( $def->id );
80         $event->run_time( $date->strftime( '%F %T%z' ) );
81
82         $editor->create_action_trigger_event( $event );
83
84         $client->respond( $event->id );
85     }
86
87     $editor->commit;
88
89     return undef;
90 }
91 __PACKAGE__->register_method(
92     api_name => 'open-ils.trigger.event.autocreate',
93     method   => 'create_active_events_for_object',
94     api_level=> 1,
95     stream   => 1,
96     argc     => 3
97 );
98
99 sub create_event_for_object_and_def {
100     my $self = shift;
101     my $client = shift;
102     my $definitions = shift;
103     my $target = shift;
104     my $location = shift;
105
106     my $ident = $target->Identity;
107     my $ident_value = $target->$ident();
108
109     my @active = ($self->api_name =~ /inactive/o) ? () : ( active => 't' );
110
111     my $editor = new_editor(xact=>1);
112
113     my $orgs = $editor->json_query({ from => [ 'actor.org_unit_ancestors' => $location ] });
114     my $defs = $editor->search_action_trigger_event_definition(
115         { id => $definitions,
116           owner  => [ map { $_->{id} } @$orgs  ],
117           @active
118         }
119     );
120
121     my $hooks = $editor->search_action_trigger_hook(
122         { key       => [ map { $_->hook } @$defs ],
123           core_type => $target->json_hint
124         }
125     );
126
127     my %hook_hash = map { ($_->key, $_) } @$hooks;
128
129     for my $def ( @$defs ) {
130
131         my $date = DateTime->now;
132
133         if ($hook_hash{$def->hook}->passive eq 'f') {
134
135             if (my $dfield = $def->delay_field) {
136                 if ($target->$dfield()) {
137                     $date = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($target->$dfield) );
138                 } else {
139                     next;
140                 }
141             }
142
143             $date->add( seconds => interval_to_seconds($def->delay) );
144         }
145
146         my $event = Fieldmapper::action_trigger::event->new();
147         $event->target( $ident_value );
148         $event->event_def( $def->id );
149         $event->run_time( $date->strftime( '%F %T%z' ) );
150
151         $editor->create_action_trigger_event( $event );
152
153         $client->respond( $event->id );
154     }
155
156     $editor->commit;
157
158     return undef;
159 }
160 __PACKAGE__->register_method(
161     api_name => 'open-ils.trigger.event.autocreate.by_definition',
162     method   => 'create_event_for_object_and_def',
163     api_level=> 1,
164     stream   => 1,
165     argc     => 3
166 );
167 __PACKAGE__->register_method(
168     api_name => 'open-ils.trigger.event.autocreate.by_definition.include_inactive',
169     method   => 'create_event_for_object_and_def',
170     api_level=> 1,
171     stream   => 1,
172     argc     => 3
173 );
174
175
176 # Retrieves events by object, or object type + filter
177 #  $object : a target object or object type (class hint)
178 #
179 #  $filter : an optional hash of filters ... top level keys:
180 #     event
181 #        filters on the atev objects, such as states or null-ness of timing
182 #        fields. contains the effective default of:
183 #          { state => 'pending' }
184 #        an example, which overrides the default, and will find
185 #        stale 'found' events:
186 #          { state => 'found', update_time => { '<' => 'yesterday' } }
187 #
188 #      event_def
189 #        filters on the atevdef object. contains the effective default of:
190 #          { active => 't' }
191 #
192 #      hook
193 #        filters on the hook object. no defaults, but there is a pinned,
194 #        unchangeable filter based on the passed hint or object type (see
195 #        $object above). an example for finding passive events:
196 #          { passive => 't' }
197 #
198 #     target
199 #        filters against the target field on the event. this can contain
200 #        either an array of target ids (if you passed an object type, and
201 #        not an object) or can contain a json_query that will return exactly
202 #        a list of target-type ids.  If you pass an object, the pkey value of
203 #        that object will be used as a filter in addition to the filter passed
204 #        in here.  example filter for circs of user 1234 that are open:
205 #          { select => { circ => ['id'] },
206 #            from => 'circ',
207 #            where => {
208 #              usr => 1234,
209 #              checkin_time => undef, 
210 #              '-or' => [
211 #                { stop_fines => undef },
212 #                { stop_fines => { 'not in' => ['LOST','LONGOVERDUE','CLAIMSRETURNED'] } }
213 #              ]
214 #            }
215
216 sub events_by_target {
217     my $self = shift;
218     my $client = shift;
219     my $object = shift;
220     my $filter = shift || {};
221     my $flesh_fields = shift || {};
222     my $flesh_depth = shift || 1;
223
224     my $obj_class = ref($object) || _fm_class_by_hint($object);
225     my $obj_hint = ref($object) ? _fm_hint_by_class(ref($object)) : $object;
226
227     my $object_ident_field = $obj_class->Identity;
228
229     my $query = {
230         select => { atev => ["id"] },
231         from   => {
232             atev => {
233                 atevdef => {
234                     field => "id",
235                     fkey => "event_def",
236                     join => {
237                         ath => { field => "key", fkey => "hook" }
238                     }
239                 }
240             }
241         },
242         where  => {
243             "+ath"  => { core_type => $obj_hint },
244             "+atevdef" => { active => 't' },
245             "+atev" => { state => 'pending' }
246         },
247         order_by => { "atev" => [ 'run_time', 'add_time' ] }
248     };
249
250     $query->{limit} = $filter->{limit} if defined $filter->{limit};
251     $query->{offset} = $filter->{offset} if defined $filter->{offset};
252     $query->{order_by} = $filter->{order_by} if defined $filter->{order_by};
253
254
255     # allow multiple 'target' filters
256     $query->{where}->{'+atev'}->{'-and'} = [];
257
258     # if we got a real object, filter on its pkey value
259     if (ref($object)) { # pass an object, require that target
260         push @{ $query->{where}->{'+atev'}->{'-and'} },
261             { target => $object->$object_ident_field }
262     }
263
264     # we have a fancy complex target filter or a list of target ids
265     if ($$filter{target}) {
266         push @{ $query->{where}->{'+atev'}->{'-and'} },
267             { target => {in => $$filter{target} } };
268     }
269
270     # pass no target filter or object, you get no events
271     if (!@{ $query->{where}->{'+atev'}->{'-and'} }) {
272         return undef; 
273     }
274
275     # any hook filters, other than the required core_type filter
276     if ($$filter{hook}) {
277         $query->{where}->{'+ath'}->{$_} = $$filter{hook}{$_}
278             for (grep { $_ ne 'core_type' } keys %{$$filter{hook}});
279     }
280
281     # any event_def filters.  defaults to { active => 't' }
282     if ($$filter{event_def}) {
283         $query->{where}->{'+atevdef'}->{$_} = $$filter{event_def}{$_}
284             for (keys %{$$filter{event_def}});
285     }
286
287     # any event filters.  defaults to { state => 'pending' }.
288     # don't overwrite '-and' used for multiple target filters above
289     if ($$filter{event}) {
290         $query->{where}->{'+atev'}->{$_} = $$filter{event}{$_}
291             for (grep { $_ ne '-and' } keys %{$$filter{event}});
292     }
293
294     my $e = new_editor();
295
296     my $events = $e->json_query($query);
297
298     $flesh_fields->{atev} = ['event_def'] unless $flesh_fields->{atev};
299
300     for my $id (@$events) {
301         my $event = $e->retrieve_action_trigger_event([
302             $id->{id},
303             {flesh => $flesh_depth, flesh_fields => $flesh_fields}
304         ]);
305
306         (my $meth = $obj_class) =~ s/^Fieldmapper:://o;
307         $meth =~ s/::/_/go;
308         $meth = 'retrieve_'.$meth;
309
310         $event->target($e->$meth($event->target));
311         $client->respond($event);
312     }
313
314     return undef;
315 }
316 __PACKAGE__->register_method(
317     api_name => 'open-ils.trigger.events_by_target',
318     method   => 'events_by_target',
319     api_level=> 1,
320     stream   => 1,
321     argc     => 2
322 );
323  
324 sub _fm_hint_by_class {
325     my $class = shift;
326     return Fieldmapper->publish_fieldmapper->{$class}->{hint};
327 }
328
329 sub _fm_class_by_hint {
330     my $hint = shift;
331
332     my ($class) = grep {
333         Fieldmapper->publish_fieldmapper->{$_}->{hint} eq $hint
334     } keys %{ Fieldmapper->publish_fieldmapper };
335
336     return $class;
337 }
338
339 sub create_batch_events {
340     my $self = shift;
341     my $client = shift;
342     my $key = shift;
343     my $location_field = shift; # where to look for event_def.owner filtering ... circ_lib, for instance, where hook.core_type = circ
344     my $filter = shift || {};
345     my $granularity = shift;
346
347     my $active = ($self->api_name =~ /active/o) ? 1 : 0;
348     if ($active && !keys(%$filter)) {
349         $log->info("Active batch event creation requires a target filter but none was supplied to create_batch_events");
350         return undef;
351     }
352
353     return undef unless ($key && $location_field);
354
355     my $editor = new_editor(xact=>1);
356     my $hooks = $editor->search_action_trigger_hook(
357         { passive => $active ? 'f' : 't', key => $key }
358     );
359
360     my %hook_hash = map { ($_->key, $_) } @$hooks;
361
362     my $defs = $editor->search_action_trigger_event_definition(
363         { hook   => [ keys %hook_hash ], active => 't' },
364     );
365
366     my $orig_filter_and = [];
367     if ($$filter{'-and'}) {
368         for my $f ( @{ $$filter{'-and'} } ) {
369             push @$orig_filter_and, $f;
370         }
371     }
372
373     for my $def ( @$defs ) {
374         next if ($granularity && $def->granularity ne $granularity );
375
376         my $date = DateTime->now->subtract( seconds => interval_to_seconds($def->delay) );
377
378         # we may need to do some work to backport this to 1.2
379         $filter->{ $location_field } = { 'in' =>
380             {
381                 select  => { aou => [{ column => 'id', transform => 'actor.org_unit_descendants', result_field => 'id' }] },
382                 from    => 'aou',
383                 where   => { id => $def->owner }
384             }
385         };
386
387         my $run_time = 'now';
388         if ($active) {
389             $run_time = 
390                 DateTime
391                     ->now
392                     ->add( seconds => interval_to_seconds($def->delay) )
393                     ->strftime( '%F %T%z' );
394         } else {
395             if ($def->max_delay) {
396                 my @times = sort {$a <=> $b} interval_to_seconds($def->delay), interval_to_seconds($def->max_delay);
397                 $filter->{ $def->delay_field } = {
398                     'between' => [
399                         DateTime->now->subtract( seconds => $times[1] )->strftime( '%F %T%z' ),
400                         DateTime->now->subtract( seconds => $times[0] )->strftime( '%F %T%z' )
401                     ]
402                 };
403             } else {
404                 $filter->{ $def->delay_field } = {
405                     '<=' => DateTime->now->subtract( seconds => interval_to_seconds($def->delay) )->strftime( '%F %T%z' )
406                 };
407             }
408         }
409
410         my $class = _fm_class_by_hint($hook_hash{$def->hook}->core_type);
411
412         # filter where this target has an event (and it's pending, for active hooks)
413         $$filter{'-and'} = [];
414         for my $f ( @$orig_filter_and ) {
415             push @{ $$filter{'-and'} }, $f;
416         }
417
418         push @{ $filter->{'-and'} }, {
419             '-not-exists' => {
420                 from  => 'atev',
421                 where => {
422                     event_def => $def->id,
423                     target    => { '=' => { '+' . $hook_hash{$def->hook}->core_type => $class->Identity } },
424                     ($active ? (state  => 'pending') : ())
425                 }
426             }
427         };
428
429         $class =~ s/^Fieldmapper:://o;
430         $class =~ s/::/_/go;
431
432         my $method = 'search_'. $class;
433         my $object_ids = $editor->$method( $filter, {idlist => 1, timeout => 1800} );
434
435         for my $o_id (@$object_ids) {
436
437             my $event = Fieldmapper::action_trigger::event->new();
438             $event->target( $o_id );
439             $event->event_def( $def->id );
440             $event->run_time( $run_time );
441
442             $editor->create_action_trigger_event( $event );
443
444             $client->respond( $event->id );
445         }
446     }
447
448     $editor->commit;
449
450     return undef;
451 }
452 __PACKAGE__->register_method(
453     api_name => 'open-ils.trigger.passive.event.autocreate.batch',
454     method   => 'create_batch_events',
455     api_level=> 1,
456     stream   => 1,
457     argc     => 2
458 );
459
460 __PACKAGE__->register_method(
461     api_name => 'open-ils.trigger.active.event.autocreate.batch',
462     method   => 'create_batch_events',
463     api_level=> 1,
464     stream   => 1,
465     argc     => 2
466 );
467
468 sub fire_single_event {
469     my $self = shift;
470     my $client = shift;
471     my $event_id = shift;
472
473     my $e = OpenILS::Application::Trigger::Event->new($event_id);
474
475     if ($e->validate->valid) {
476         $logger->info("Event is valid, reacting...");
477         $e->react->cleanup;
478     }
479
480     $e->editor->disconnect;
481
482     return {
483         valid     => $e->valid,
484         reacted   => $e->reacted,
485         cleanedup => $e->cleanedup,
486         event     => $e->event
487     };
488 }
489 __PACKAGE__->register_method(
490     api_name => 'open-ils.trigger.event.fire',
491     method   => 'fire_single_event',
492     api_level=> 1,
493     argc     => 1
494 );
495
496 sub fire_event_group {
497     my $self = shift;
498     my $client = shift;
499     my $events = shift;
500
501     my $e = OpenILS::Application::Trigger::EventGroup->new(@$events);
502
503     if ($e->validate->valid) {
504         $logger->info("Event group is valid, reacting...");
505         $e->react->cleanup;
506     }
507
508     $e->editor->disconnect;
509
510     return {
511         valid     => $e->valid,
512         reacted   => $e->reacted,
513         cleanedup => $e->cleanedup,
514         events    => [map { $_->event } @{$e->events}]
515     };
516 }
517 __PACKAGE__->register_method(
518     api_name => 'open-ils.trigger.event_group.fire',
519     method   => 'fire_event_group',
520     api_level=> 1,
521     argc     => 1
522 );
523
524 sub pending_events {
525     my $self = shift;
526     my $client = shift;
527     my $granularity = shift;
528
529     my $editor = new_editor();
530
531     my $query = [{ state => 'pending', run_time => {'<' => 'now'} }, { order_by => { atev => [ qw/run_time add_time/] }, 'join' => 'atevdef' }];
532
533     if (defined $granularity) {
534         $query->[0]->{'+atevdef'} = {'-or' => [ {granularity => $granularity}, {granularity => undef} ] };
535     } else {
536         $query->[0]->{'+atevdef'} = {granularity => undef};
537     }
538
539     return $editor->search_action_trigger_event(
540         $query, { idlist=> 1, timeout => 1800 }
541     );
542 }
543 __PACKAGE__->register_method(
544     api_name => 'open-ils.trigger.event.find_pending',
545     method   => 'pending_events',
546     api_level=> 1
547 );
548
549 sub grouped_events {
550     my $self = shift;
551     my $client = shift;
552     my $granularity = shift;
553
554     my ($events) = $self->method_lookup('open-ils.trigger.event.find_pending')->run($granularity);
555
556     my %groups = ( '*' => [] );
557
558     for my $e_id ( @$events ) {
559         $logger->info("trigger: processing event $e_id");
560
561         # let the client know we're still chugging along TODO add osrf support for method_lookup $client's
562         $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
563
564         my $e;
565         try {
566            $e = OpenILS::Application::Trigger::Event->new($e_id);
567         } catch Error with {
568             $logger->error("Event creation failed with ".shift());
569         };
570
571         next unless $e; 
572
573         try {
574             $e->build_environment;
575         } catch Error with {
576             $logger->error("Event environment building failed with ".shift());
577         };
578
579         if (my $group = $e->event->event_def->group_field) {
580
581             # split the grouping link steps
582             my @steps = split /\./, $group;
583
584             # find the grouping object
585             my $node = $e->target;
586             $node = $node->$_() for ( @steps );
587
588             # get the pkey value for the grouping object on this event
589             my $node_ident = $node->Identity;
590             my $ident_value = $node->$node_ident();
591
592             # push this event onto the event+grouping_pkey_value stack
593             $groups{$e->event->event_def->id}{$ident_value} ||= [];
594             push @{ $groups{$e->event->event_def->id}{$ident_value} }, $e;
595         } else {
596             # it's a non-grouped event
597             push @{ $groups{'*'} }, $e;
598         }
599
600         $e->editor->disconnect;
601     }
602
603     return \%groups;
604 }
605 __PACKAGE__->register_method(
606     api_name => 'open-ils.trigger.event.find_pending_by_group',
607     method   => 'grouped_events',
608     api_level=> 1
609 );
610
611 sub run_all_events {
612     my $self = shift;
613     my $client = shift;
614     my $granularity = shift;
615
616     my ($groups) = $self->method_lookup('open-ils.trigger.event.find_pending_by_group')->run($granularity);
617
618     for my $def ( keys %$groups ) {
619         if ($def eq '*') {
620             for my $event ( @{ $$groups{'*'} } ) {
621                 try {
622                     $client->respond(
623                         $self
624                             ->method_lookup('open-ils.trigger.event.fire')
625                             ->run($event)
626                     );
627                 } catch Error with { 
628                     $logger->error("event firing failed with ".shift());
629                 };
630             }
631         } else {
632             my $defgroup = $$groups{$def};
633             for my $ident ( keys %$defgroup ) {
634                 try {
635                     $client->respond(
636                         $self
637                             ->method_lookup('open-ils.trigger.event_group.fire')
638                             ->run($$defgroup{$ident})
639                     );
640                 } catch Error with {
641                     $logger->error("event firing failed with ".shift());
642                 };
643             }
644         }
645     }
646                 
647             
648 }
649 __PACKAGE__->register_method(
650     api_name => 'open-ils.trigger.event.run_all_pending',
651     method   => 'run_all_events',
652     api_level=> 1
653 );
654
655
656 1;