]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Trigger.pm
adding support for user-opt-in when allowed on the event definition
[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         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
66
67             my $opt_in_setting = $editor->search_actor_usr_setting(
68                 { usr   => $uid,
69                   name  => $def->opt_in_setting,
70                   value => 'true'
71                 }
72             );
73
74             next unless (@$opt_in_setting);
75         }
76
77         my $date = DateTime->now;
78
79         if ($hook_hash{$def->hook}->passive eq 'f') {
80
81             if (my $dfield = $def->delay_field) {
82                 if ($target->$dfield()) {
83                     $date = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($target->$dfield) );
84                 } else {
85                     next;
86                 }
87             }
88
89             $date->add( seconds => interval_to_seconds($def->delay) );
90         }
91
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' ) );
96
97         $editor->create_action_trigger_event( $event );
98
99         $client->respond( $event->id );
100     }
101
102     $editor->commit;
103
104     return undef;
105 }
106 __PACKAGE__->register_method(
107     api_name => 'open-ils.trigger.event.autocreate',
108     method   => 'create_active_events_for_object',
109     api_level=> 1,
110     stream   => 1,
111     argc     => 3
112 );
113
114 sub create_event_for_object_and_def {
115     my $self = shift;
116     my $client = shift;
117     my $definitions = shift;
118     my $target = shift;
119     my $location = shift;
120
121     my $ident = $target->Identity;
122     my $ident_value = $target->$ident();
123
124     my @active = ($self->api_name =~ /inactive/o) ? () : ( active => 't' );
125
126     my $editor = new_editor(xact=>1);
127
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  ],
132           @active
133         }
134     );
135
136     my $hooks = $editor->search_action_trigger_hook(
137         { key       => [ map { $_->hook } @$defs ],
138           core_type => $target->json_hint
139         }
140     );
141
142     my %hook_hash = map { ($_->key, $_) } @$hooks;
143
144     for my $def ( @$defs ) {
145
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
150
151             my $opt_in_setting = $editor->search_actor_usr_setting(
152                 { usr   => $uid,
153                   name  => $def->opt_in_setting,
154                   value => 'true'
155                 }
156             );
157
158             next unless (@$opt_in_setting);
159         }
160
161         my $date = DateTime->now;
162
163         if ($hook_hash{$def->hook}->passive eq 'f') {
164
165             if (my $dfield = $def->delay_field) {
166                 if ($target->$dfield()) {
167                     $date = DateTime::Format::ISO8601->new->parse_datetime( clense_ISO8601($target->$dfield) );
168                 } else {
169                     next;
170                 }
171             }
172
173             $date->add( seconds => interval_to_seconds($def->delay) );
174         }
175
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' ) );
180
181         $editor->create_action_trigger_event( $event );
182
183         $client->respond( $event->id );
184     }
185
186     $editor->commit;
187
188     return undef;
189 }
190 __PACKAGE__->register_method(
191     api_name => 'open-ils.trigger.event.autocreate.by_definition',
192     method   => 'create_event_for_object_and_def',
193     api_level=> 1,
194     stream   => 1,
195     argc     => 3
196 );
197 __PACKAGE__->register_method(
198     api_name => 'open-ils.trigger.event.autocreate.by_definition.include_inactive',
199     method   => 'create_event_for_object_and_def',
200     api_level=> 1,
201     stream   => 1,
202     argc     => 3
203 );
204
205
206 # Retrieves events by object, or object type + filter
207 #  $object : a target object or object type (class hint)
208 #
209 #  $filter : an optional hash of filters ... top level keys:
210 #     event
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' } }
217 #
218 #      event_def
219 #        filters on the atevdef object. contains the effective default of:
220 #          { active => 't' }
221 #
222 #      hook
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:
226 #          { passive => 't' }
227 #
228 #     target
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'] },
236 #            from => 'circ',
237 #            where => {
238 #              usr => 1234,
239 #              checkin_time => undef, 
240 #              '-or' => [
241 #                { stop_fines => undef },
242 #                { stop_fines => { 'not in' => ['LOST','LONGOVERDUE','CLAIMSRETURNED'] } }
243 #              ]
244 #            }
245
246 sub events_by_target {
247     my $self = shift;
248     my $client = shift;
249     my $object = shift;
250     my $filter = shift || {};
251     my $flesh_fields = shift || {};
252     my $flesh_depth = shift || 1;
253
254     my $obj_class = ref($object) || _fm_class_by_hint($object);
255     my $obj_hint = ref($object) ? _fm_hint_by_class(ref($object)) : $object;
256
257     my $object_ident_field = $obj_class->Identity;
258
259     my $query = {
260         select => { atev => ["id"] },
261         from   => {
262             atev => {
263                 atevdef => {
264                     field => "id",
265                     fkey => "event_def",
266                     join => {
267                         ath => { field => "key", fkey => "hook" }
268                     }
269                 }
270             }
271         },
272         where  => {
273             "+ath"  => { core_type => $obj_hint },
274             "+atevdef" => { active => 't' },
275             "+atev" => { state => 'pending' }
276         },
277         order_by => { "atev" => [ 'run_time', 'add_time' ] }
278     };
279
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};
283
284
285     # allow multiple 'target' filters
286     $query->{where}->{'+atev'}->{'-and'} = [];
287
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 }
292     }
293
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} } };
298     }
299
300     # pass no target filter or object, you get no events
301     if (!@{ $query->{where}->{'+atev'}->{'-and'} }) {
302         return undef; 
303     }
304
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}});
309     }
310
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}});
315     }
316
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}});
322     }
323
324     my $e = new_editor();
325
326     my $events = $e->json_query($query);
327
328     $flesh_fields->{atev} = ['event_def'] unless $flesh_fields->{atev};
329
330     for my $id (@$events) {
331         my $event = $e->retrieve_action_trigger_event([
332             $id->{id},
333             {flesh => $flesh_depth, flesh_fields => $flesh_fields}
334         ]);
335
336         (my $meth = $obj_class) =~ s/^Fieldmapper:://o;
337         $meth =~ s/::/_/go;
338         $meth = 'retrieve_'.$meth;
339
340         $event->target($e->$meth($event->target));
341         $client->respond($event);
342     }
343
344     return undef;
345 }
346 __PACKAGE__->register_method(
347     api_name => 'open-ils.trigger.events_by_target',
348     method   => 'events_by_target',
349     api_level=> 1,
350     stream   => 1,
351     argc     => 2
352 );
353  
354 sub _fm_hint_by_class {
355     my $class = shift;
356     return Fieldmapper->publish_fieldmapper->{$class}->{hint};
357 }
358
359 sub _fm_class_by_hint {
360     my $hint = shift;
361
362     my ($class) = grep {
363         Fieldmapper->publish_fieldmapper->{$_}->{hint} eq $hint
364     } keys %{ Fieldmapper->publish_fieldmapper };
365
366     return $class;
367 }
368
369 sub create_batch_events {
370     my $self = shift;
371     my $client = shift;
372     my $key = shift;
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;
376
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");
380         return undef;
381     }
382
383     return undef unless ($key && $location_field);
384
385     my $editor = new_editor(xact=>1);
386     my $hooks = $editor->search_action_trigger_hook(
387         { passive => $active ? 'f' : 't', key => $key }
388     );
389
390     my %hook_hash = map { ($_->key, $_) } @$hooks;
391
392     my $defs = $editor->search_action_trigger_event_definition(
393         { hook   => [ keys %hook_hash ], active => 't' },
394     );
395
396     my $orig_filter_and = [];
397     if ($$filter{'-and'}) {
398         for my $f ( @{ $$filter{'-and'} } ) {
399             push @$orig_filter_and, $f;
400         }
401     }
402
403     for my $def ( @$defs ) {
404         next if ($granularity && $def->granularity ne $granularity );
405
406         my $date = DateTime->now->subtract( seconds => interval_to_seconds($def->delay) );
407
408         # we may need to do some work to backport this to 1.2
409         $filter->{ $location_field } = { 'in' =>
410             {
411                 select  => { aou => [{ column => 'id', transform => 'actor.org_unit_descendants', result_field => 'id' }] },
412                 from    => 'aou',
413                 where   => { id => $def->owner }
414             }
415         };
416
417         my $run_time = 'now';
418         if ($active) {
419             $run_time = 
420                 DateTime
421                     ->now
422                     ->add( seconds => interval_to_seconds($def->delay) )
423                     ->strftime( '%F %T%z' );
424         } else {
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 } = {
428                     'between' => [
429                         DateTime->now->subtract( seconds => $times[1] )->strftime( '%F %T%z' ),
430                         DateTime->now->subtract( seconds => $times[0] )->strftime( '%F %T%z' )
431                     ]
432                 };
433             } else {
434                 $filter->{ $def->delay_field } = {
435                     '<=' => DateTime->now->subtract( seconds => interval_to_seconds($def->delay) )->strftime( '%F %T%z' )
436                 };
437             }
438         }
439
440         my $class = _fm_class_by_hint($hook_hash{$def->hook}->core_type);
441
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;
446         }
447
448         push @{ $filter->{'-and'} }, {
449             '-not-exists' => {
450                 from  => 'atev',
451                 where => {
452                     event_def => $def->id,
453                     target    => { '=' => { '+' . $hook_hash{$def->hook}->core_type => $class->Identity } },
454                     ($active ? (state  => 'pending') : ())
455                 }
456             }
457         };
458
459         if ($def->usr_field && $def->opt_in_setting) {
460             push @{ $filter->{'-and'} }, {
461                 '-exists' => {
462                     from  => 'aus',
463                     where => {
464                         name => $def->id,
465                         usr  => { '=' => { '+' . $hook_hash{$def->hook}->core_type => $def->usr_field } },
466                         value=> 'true'
467                     }
468                 }
469             };
470         }
471
472         $class =~ s/^Fieldmapper:://o;
473         $class =~ s/::/_/go;
474
475         my $method = 'search_'. $class;
476         my $object_ids = $editor->$method( $filter, {idlist => 1, timeout => 1800} );
477
478         for my $o_id (@$object_ids) {
479
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 );
484
485             $editor->create_action_trigger_event( $event );
486
487             $client->respond( $event->id );
488         }
489     }
490
491     $editor->commit;
492
493     return undef;
494 }
495 __PACKAGE__->register_method(
496     api_name => 'open-ils.trigger.passive.event.autocreate.batch',
497     method   => 'create_batch_events',
498     api_level=> 1,
499     stream   => 1,
500     argc     => 2
501 );
502
503 __PACKAGE__->register_method(
504     api_name => 'open-ils.trigger.active.event.autocreate.batch',
505     method   => 'create_batch_events',
506     api_level=> 1,
507     stream   => 1,
508     argc     => 2
509 );
510
511 sub fire_single_event {
512     my $self = shift;
513     my $client = shift;
514     my $event_id = shift;
515
516     my $e = OpenILS::Application::Trigger::Event->new($event_id);
517
518     if ($e->validate->valid) {
519         $logger->info("Event is valid, reacting...");
520         $e->react->cleanup;
521     }
522
523     $e->editor->disconnect;
524
525     return {
526         valid     => $e->valid,
527         reacted   => $e->reacted,
528         cleanedup => $e->cleanedup,
529         event     => $e->event
530     };
531 }
532 __PACKAGE__->register_method(
533     api_name => 'open-ils.trigger.event.fire',
534     method   => 'fire_single_event',
535     api_level=> 1,
536     argc     => 1
537 );
538
539 sub fire_event_group {
540     my $self = shift;
541     my $client = shift;
542     my $events = shift;
543
544     my $e = OpenILS::Application::Trigger::EventGroup->new(@$events);
545
546     if ($e->validate->valid) {
547         $logger->info("Event group is valid, reacting...");
548         $e->react->cleanup;
549     }
550
551     $e->editor->disconnect;
552
553     return {
554         valid     => $e->valid,
555         reacted   => $e->reacted,
556         cleanedup => $e->cleanedup,
557         events    => [map { $_->event } @{$e->events}]
558     };
559 }
560 __PACKAGE__->register_method(
561     api_name => 'open-ils.trigger.event_group.fire',
562     method   => 'fire_event_group',
563     api_level=> 1,
564     argc     => 1
565 );
566
567 sub pending_events {
568     my $self = shift;
569     my $client = shift;
570     my $granularity = shift;
571
572     my $editor = new_editor();
573
574     my $query = [{ state => 'pending', run_time => {'<' => 'now'} }, { order_by => { atev => [ qw/run_time add_time/] }, 'join' => 'atevdef' }];
575
576     if (defined $granularity) {
577         $query->[0]->{'+atevdef'} = {'-or' => [ {granularity => $granularity}, {granularity => undef} ] };
578     } else {
579         $query->[0]->{'+atevdef'} = {granularity => undef};
580     }
581
582     return $editor->search_action_trigger_event(
583         $query, { idlist=> 1, timeout => 1800 }
584     );
585 }
586 __PACKAGE__->register_method(
587     api_name => 'open-ils.trigger.event.find_pending',
588     method   => 'pending_events',
589     api_level=> 1
590 );
591
592 sub grouped_events {
593     my $self = shift;
594     my $client = shift;
595     my $granularity = shift;
596
597     my ($events) = $self->method_lookup('open-ils.trigger.event.find_pending')->run($granularity);
598
599     my %groups = ( '*' => [] );
600
601     for my $e_id ( @$events ) {
602         $logger->info("trigger: processing event $e_id");
603
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 );
606
607         my $e;
608         try {
609            $e = OpenILS::Application::Trigger::Event->new($e_id);
610         } catch Error with {
611             $logger->error("Event creation failed with ".shift());
612         };
613
614         next unless $e; 
615
616         try {
617             $e->build_environment;
618         } catch Error with {
619             $logger->error("Event environment building failed with ".shift());
620         };
621
622         if (my $group = $e->event->event_def->group_field) {
623
624             # split the grouping link steps
625             my @steps = split /\./, $group;
626
627             # find the grouping object
628             my $node = $e->target;
629             $node = $node->$_() for ( @steps );
630
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();
634
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;
638         } else {
639             # it's a non-grouped event
640             push @{ $groups{'*'} }, $e;
641         }
642
643         $e->editor->disconnect;
644     }
645
646     return \%groups;
647 }
648 __PACKAGE__->register_method(
649     api_name => 'open-ils.trigger.event.find_pending_by_group',
650     method   => 'grouped_events',
651     api_level=> 1
652 );
653
654 sub run_all_events {
655     my $self = shift;
656     my $client = shift;
657     my $granularity = shift;
658
659     my ($groups) = $self->method_lookup('open-ils.trigger.event.find_pending_by_group')->run($granularity);
660
661     for my $def ( keys %$groups ) {
662         if ($def eq '*') {
663             for my $event ( @{ $$groups{'*'} } ) {
664                 try {
665                     $client->respond(
666                         $self
667                             ->method_lookup('open-ils.trigger.event.fire')
668                             ->run($event)
669                     );
670                 } catch Error with { 
671                     $logger->error("event firing failed with ".shift());
672                 };
673             }
674         } else {
675             my $defgroup = $$groups{$def};
676             for my $ident ( keys %$defgroup ) {
677                 try {
678                     $client->respond(
679                         $self
680                             ->method_lookup('open-ils.trigger.event_group.fire')
681                             ->run($$defgroup{$ident})
682                     );
683                 } catch Error with {
684                     $logger->error("event firing failed with ".shift());
685                 };
686             }
687         }
688     }
689                 
690             
691 }
692 __PACKAGE__->register_method(
693     api_name => 'open-ils.trigger.event.run_all_pending',
694     method   => 'run_all_events',
695     api_level=> 1
696 );
697
698
699 1;