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