]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Trigger.pm
provide a little more flexibility to the caller of open-ils.trigger.events_by_target...
[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
95 # Retrieves events by object, or object type + filter
96 #  $object : a target object or object type (class hint)
97 #
98 #  $filter : an optional hash of filters ... top level keys:
99 #     event
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' } }
106 #
107 #      event_def
108 #        filters on the atevdef object. contains the effective default of:
109 #          { active => 't' }
110 #
111 #      hook
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:
115 #          { passive => 't' }
116 #
117 #     target
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'] },
125 #            from => 'circ',
126 #            where => {
127 #              usr => 1234,
128 #              checkin_time => undef, 
129 #              '-or' => [
130 #                { stop_fines => undef },
131 #                { stop_fines => { 'not in' => ['LOST','LONGOVERDUE','CLAIMSRETURNED'] } }
132 #              ]
133 #            }
134
135 sub events_by_target {
136     my $self = shift;
137     my $client = shift;
138     my $object = shift;
139     my $filter = shift || {};
140     my $flesh_fields = shift || {};
141     my $flesh_depth = shift || 1;
142
143     my $obj_class = ref($object) || _fm_class_by_hint($object);
144     my $obj_hint = ref($object) ? _fm_hint_by_class(ref($object)) : $object;
145
146     my $object_ident_field = $obj_class->Identity;
147
148     my $query = {
149         select => { atev => ["id"] },
150         from   => {
151             atev => {
152                 atevdef => {
153                     field => "id",
154                     fkey => "event_def",
155                     join => {
156                         ath => { field => "key", fkey => "hook" }
157                     }
158                 }
159             }
160         },
161         where  => {
162             "+ath"  => { core_type => $obj_hint },
163             "+atevdef" => { active => 't' },
164             "+atev" => { state => 'pending' }
165         },
166         order_by => { "+atev" => [ 'run_time' ] },
167         distinct => 1
168     };
169
170
171     # allow multiple 'target' filters
172     $query->{where}->{'+atev'}->{'-and'} = [];
173
174     # if we got a real object, filter on its pkey value
175     if (ref($object)) { # pass an object, require that target
176         push @{ $query->{where}->{'+atev'}->{'-and'} },
177             { target => $object->$object_ident_field }
178     }
179
180     # we have a fancy complex target filter or a list of target ids
181     if ($$filter{target}) {
182         push @{ $query->{where}->{'+atev'}->{'-and'} },
183             { target => {in => $$filter{target} } };
184     }
185
186     # pass no target filter or object, you get no events
187     if (!@{ $query->{where}->{'+atev'}->{'-and'} }) {
188         return undef; 
189     }
190
191     # any hook filters, other than the required core_type filter
192     if ($$filter{hook}) {
193         $query->{where}->{'+ath'}->{$_} = $$filter{hook}{$_}
194             for (grep { $_ ne 'core_type' } keys %{$$filter{hook}});
195     }
196
197     # any event_def filters.  defaults to { active => 't' }
198     if ($$filter{event_def}) {
199         $query->{where}->{'+atevdef'}->{$_} = $$filter{event_def}{$_}
200             for (keys %{$$filter{event_def}});
201     }
202
203     # any event filters.  defaults to { state => 'pending' }.
204     # don't overwrite '-and' used for multiple target filters above
205     if ($$filter{event}) {
206         $query->{where}->{'+atev'}->{$_} = $$filter{event}{$_}
207             for (grep { $_ ne '-and' } keys %{$$filter{event}});
208     }
209
210     my $e = new_editor();
211
212     my $events = $e->json_query($query);
213
214     $flesh_fields->{atev} = ['event_def'] unless $flesh_fields->{atev};
215
216     for my $id (@$events) {
217         my $event = $e->retrieve_action_trigger_event([
218             $id->{id},
219             {flesh => $flesh_depth, flesh_fields => $flesh_fields}
220         ]);
221
222         (my $meth = $obj_class) =~ s/^Fieldmapper:://o;
223         $meth =~ s/::/_/go;
224         $meth = 'retrieve_'.$meth;
225
226         $event->target($e->$meth($event->target));
227         $client->respond($event);
228     }
229
230     return undef;
231 }
232 __PACKAGE__->register_method(
233     api_name => 'open-ils.trigger.events_by_target',
234     method   => 'events_by_target',
235     api_level=> 1,
236     stream   => 1,
237     argc     => 2
238 );
239  
240 sub _fm_hint_by_class {
241     my $class = shift;
242     return Fieldmapper->publish_fieldmapper->{$class}->{hint};
243 }
244
245 sub _fm_class_by_hint {
246     my $hint = shift;
247
248     my ($class) = grep {
249         Fieldmapper->publish_fieldmapper->{$_}->{hint} eq $hint
250     } keys %{ Fieldmapper->publish_fieldmapper };
251
252     return $class;
253 }
254
255 sub create_batch_events {
256     my $self = shift;
257     my $client = shift;
258     my $key = shift;
259     my $location_field = shift; # where to look for event_def.owner filtering ... circ_lib, for instance, where hook.core_type = circ
260     my $filter = shift || {};
261
262     my $active = ($self->api_name =~ /active/o) ? 1 : 0;
263     if ($active && !keys(%$filter)) {
264         $log->info("Active batch event creation requires a target filter but none was supplied to create_batch_events");
265         return undef;
266     }
267
268     return undef unless ($key && $location_field);
269
270     my $editor = new_editor(xact=>1);
271     my $hooks = $editor->search_action_trigger_hook(
272         { passive => $active ? 'f' : 't', key => $key }
273     );
274
275     my %hook_hash = map { ($_->key, $_) } @$hooks;
276
277     my $defs = $editor->search_action_trigger_event_definition(
278         { hook   => [ keys %hook_hash ], active => 't' },
279     );
280
281     for my $def ( @$defs ) {
282
283         my $date = DateTime->now->subtract( seconds => interval_to_seconds($def->delay) );
284
285         # we may need to do some work to backport this to 1.2
286         $filter->{ $location_field } = { 'in' =>
287             {
288                 select  => { aou => [{ column => 'id', transform => 'actor.org_unit_descendents', result_field => 'id' }] },
289                 from    => 'aou',
290                 where   => { id => $def->owner }
291             }
292         };
293
294         my $run_time = 'now';
295         if ($active) {
296             $run_time = {
297                 '<=' => DateTime
298                     ->now
299                     ->add( seconds => interval_to_seconds($def->delay) )
300                     ->strftime( '%F %T%z' )
301             };
302         } else {
303             $filter->{ $def->delay_field } = {
304                 '<=' => DateTime
305                     ->now
306                     ->subtract( seconds => interval_to_seconds($def->delay) )
307                     ->strftime( '%F %T%z' )
308             };
309         }
310
311         my $class = _fm_class_by_hint($hook_hash{$def->hook}->core_type);
312         $class =~ s/^Fieldmapper:://o;
313         $class =~ s/::/_/go;
314
315         my $method = 'search_'. $class;
316         my $objects = $editor->$method( $filter );
317
318         for my $o (@$objects) {
319
320             my $ident = $o->Identity;
321             my $ident_value = $o->$ident();
322
323             my $previous;
324             if ($active) {
325                 # only allow one pending event of type $def for each target
326                 $previous = $editor->search_action_trigger_event({
327                     event_def => $def->id,
328                     target    => $ident_value,
329                     state     => 'pending'
330                 });
331
332             } else {
333                 # only allow one event of type $def for each target
334                 $previous = $editor->search_action_trigger_event({
335                     event_def => $def->id,
336                     target    => $ident_value
337                 });
338
339             }
340
341             next if (ref($previous) && @$previous);
342
343             my $event = Fieldmapper::action_trigger::event->new();
344             $event->target( $ident_value );
345             $event->event_def( $def->id );
346             $event->run_time( $run_time );
347
348             $editor->create_action_trigger_event( $event );
349
350             $client->respond( $event->id );
351         }
352     }
353
354     $editor->commit;
355
356     return undef;
357 }
358 __PACKAGE__->register_method(
359     api_name => 'open-ils.trigger.passive.event.autocreate.batch',
360     method   => 'create_batch_events',
361     api_level=> 1,
362     stream   => 1,
363     argc     => 2
364 );
365
366 __PACKAGE__->register_method(
367     api_name => 'open-ils.trigger.active.event.autocreate.batch',
368     method   => 'create_batch_events',
369     api_level=> 1,
370     stream   => 1,
371     argc     => 2
372 );
373
374
375 sub fire_single_event {
376     my $self = shift;
377     my $client = shift;
378     my $event_id = shift;
379
380     my $e = OpenILS::Application::Trigger::Event->new($event_id);
381
382     if ($e->validate->valid) {
383         $e->react->cleanup;
384     }
385
386     return {
387         valid     => $e->valid,
388         reacted   => $e->reacted,
389         cleanedup => $e->cleanedup,
390         event     => $e->event
391     };
392 }
393 __PACKAGE__->register_method(
394     api_name => 'open-ils.trigger.event.fire',
395     method   => 'fire_single_event',
396     api_level=> 1,
397     argc     => 1
398 );
399
400 sub fire_event_group {
401     my $self = shift;
402     my $client = shift;
403     my $events = shift;
404
405     my $e = OpenILS::Application::Trigger::EventGroup->new(@$events);
406
407     if ($e->validate->valid) {
408         $e->react->cleanup;
409     }
410
411     return {
412         valid     => $e->valid,
413         reacted   => $e->reacted,
414         cleanedup => $e->cleanedup,
415         events    => [map { $_->event } @{$e->events}]
416     };
417 }
418 __PACKAGE__->register_method(
419     api_name => 'open-ils.trigger.event_group.fire',
420     method   => 'fire_event_group',
421     api_level=> 1,
422     argc     => 1
423 );
424
425 sub pending_events {
426     my $self = shift;
427     my $client = shift;
428
429     my $editor = new_editor();
430
431     return $editor->search_action_trigger_event([
432         { state => 'pending', run_time => {'<' => 'now'} },
433         { idlist=> 1 }
434     ]);
435 }
436 __PACKAGE__->register_method(
437     api_name => 'open-ils.trigger.event.find_pending',
438     method   => 'pending_events',
439     api_level=> 1
440 );
441
442
443 sub grouped_events {
444     my $self = shift;
445     my $client = shift;
446
447     my ($events) = $self->method_lookup('open-ils.trigger.event.find_pending')->run();
448
449     my %groups = ( '*' => [] );
450
451     for my $e_id ( @$events ) {
452         my $e = OpenILS::Application::Trigger::Event->new($e_id);
453         if ($e->validate->valid) {
454             if (my $group = $e->event->event_def->group_field) {
455
456                 # split the grouping link steps
457                 my @steps = split '.', $group;
458
459                 # find the grouping object
460                 my $node = $e->target;
461                 $node = $node->$_() for ( @steps );
462
463                 # get the pkey value for the grouping object on this event
464                 my $node_ident = $node->Identity;
465                 my $ident_value = $node->$node_ident();
466
467                 # push this event onto the event+grouping_pkey_value stack
468                 $groups{$e->event->event_def->id}{$ident_value} ||= [];
469                 push @{ $groups{$e->event->event_def->id}{$ident_value} }, $e;
470             } else {
471                 # it's a non-grouped event
472                 push @{ $groups{'*'} }, $e;
473             }
474         }
475     }
476
477     return \%groups;
478 }
479 __PACKAGE__->register_method(
480     api_name => 'open-ils.trigger.event.find_pending_by_group',
481     method   => 'grouped_events',
482     api_level=> 1
483 );
484
485 sub run_all_events {
486     my $self = shift;
487     my $client = shift;
488
489     my ($groups) = $self->method_lookup('open-ils.trigger.event.find_pending_by_group')->run();
490
491     for my $def ( %$groups ) {
492         if ($def eq '*') {
493             for my $event ( @{ $$groups{'*'} } ) {
494                 $client->respond(
495                     $self
496                         ->method_lookup('open-ils.trigger.event.fire')
497                         ->run($event)
498                 );
499             }
500         } else {
501             my $defgroup = $$groups{$def};
502             for my $ident ( keys %$defgroup ) {
503                 $client->respond(
504                     $self
505                         ->method_lookup('open-ils.trigger.event_group.fire')
506                         ->run($$defgroup{$ident})
507                 );
508             }
509         }
510     }
511                 
512             
513 }
514 __PACKAGE__->register_method(
515     api_name => 'open-ils.trigger.event.run_all_pending',
516     method   => 'run_all_events',
517     api_level=> 1
518 );
519
520
521 1;