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