File Coverage

blib/lib/App/Dochazka/REST/Fillup.pm
Criterion Covered Total %
statement 60 416 14.4
branch 0 168 0.0
condition 0 50 0.0
subroutine 18 45 40.0
pod 6 6 100.0
total 84 685 12.2


line stmt bran cond sub pod time code
1             # *************************************************************************
2             # Copyright (c) 2014-2017, SUSE LLC
3             #
4             # All rights reserved.
5             #
6             # Redistribution and use in source and binary forms, with or without
7             # modification, are permitted provided that the following conditions are met:
8             #
9             # 1. Redistributions of source code must retain the above copyright notice,
10             # this list of conditions and the following disclaimer.
11             #
12             # 2. Redistributions in binary form must reproduce the above copyright
13             # notice, this list of conditions and the following disclaimer in the
14             # documentation and/or other materials provided with the distribution.
15             #
16             # 3. Neither the name of SUSE LLC nor the names of its contributors may be
17             # used to endorse or promote products derived from this software without
18             # specific prior written permission.
19             #
20             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21             # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23             # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24             # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25             # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26             # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27             # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28             # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29             # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30             # POSSIBILITY OF SUCH DAMAGE.
31             # *************************************************************************
32              
33             package App::Dochazka::REST::Fillup;
34              
35 41     41   1111 use 5.012;
  41         135  
36 41     41   190 use strict;
  41         68  
  41         702  
37 41     41   166 use warnings;
  41         74  
  41         1040  
38 41     41   183 use App::CELL qw( $CELL $log $meta $site );
  41         72  
  41         3480  
39 41     41   239 use App::Dochazka::Common::Model;
  41         80  
  41         992  
40 41     41   172 use App::Dochazka::REST::ConnBank qw( $dbix_conn );
  41         79  
  41         2714  
41 41     41   550 use App::Dochazka::REST::Model::Employee;
  41         81  
  41         1340  
42 41         2269 use App::Dochazka::REST::Model::Interval qw(
43             fetch_intervals_by_eid_and_tsrange_inclusive
44 41     41   11217 );
  41         102  
45 41         1732 use App::Dochazka::REST::Model::Shared qw(
46             canonicalize_tsrange
47             split_tsrange
48 41     41   280 );
  41         75  
49 41         2771 use App::Dochazka::REST::Model::Tempintvl qw(
50             fetch_tempintvls_by_tiid_and_tsrange
51 41     41   11576 );
  41         103  
52 41         2074 use App::Dochazka::REST::Holiday qw(
53             calculate_hours
54             canon_date_diff
55             canon_to_ymd
56             get_tomorrow
57             holidays_in_daterange
58             tsrange_to_dates_and_times
59             ymd_to_canon
60 41     41   273 );
  41         69  
61 41     41   237 use Data::Dumper;
  41         67  
  41         1411  
62 41         1622 use Date::Calc qw(
63             Add_Delta_Days
64             Date_to_Days
65             Day_of_Week
66             check_date
67 41     41   196 );
  41         65  
68 41     41   205 use JSON qw( decode_json );
  41         72  
  41         299  
69 41     41   4534 use Params::Validate qw( :all );
  41         94  
  41         4788  
70 41     41   235 use Try::Tiny;
  41         71  
  41         1891  
71              
72             BEGIN {
73 41     41   216 no strict 'refs';
  41         72  
  41         11496  
74 41     41   816 our %attr= (
75             act_obj => {
76             type => HASHREF,
77             isa => 'App::Dochazka::REST::Model::Activity',
78             optional => 1
79             },
80             clobber => { type => BOOLEAN, optional => 1 },
81             constructor_status => {
82             type => HASHREF,
83             isa => 'App::CELL::Status',
84             optional => 1
85             },
86             context => { type => HASHREF, optional => 1 },
87             date_list => { type => ARRAYREF, optional => 1 },
88             dry_run => { type => BOOLEAN, optional => 1 },
89             emp_obj => {
90             type => HASHREF,
91             isa => 'App::Dochazka::REST::Model::Employee',
92             optional => 1
93             },
94             intervals => { type => ARRAYREF, optional => 1 },
95             long_desc => { type => SCALAR, optional => 1 },
96             remark => { type => SCALAR, optional => 1 },
97             tiid => { type => SCALAR, optional => 1 },
98             tsrange => { type => HASHREF, optional => 1 },
99             tsranges => { type => ARRAYREF, optional => 1 },
100             );
101             map {
102 41         179 my $fn = __PACKAGE__ . "::$_";
  533         1072  
103 533         2620 $log->debug( "BEGIN BLOCK: $_ $fn" );
104 533         5832 *{ $fn } =
105 533         85054 App::Dochazka::Common::Model::make_accessor( $_, $attr{ $_ } );
106             } keys %attr;
107              
108 41         158 *{ 'reset' } = sub {
109             # process arguments
110 0     0   0 my $self = shift;
111 0 0 0     0 my %ARGS = validate( @_, \%attr ) if @_ and defined $_[0];
112              
113             # Wipe out current TIID
114 0         0 $self->DESTROY;
115              
116             # Set attributes to run-time values sent in argument list.
117             # Attributes that are not in the argument list will get set to undef.
118 0         0 map { $self->{$_} = $ARGS{$_}; } keys %attr;
  0         0  
119              
120             # run the populate function, if any
121 0 0       0 $self->populate() if $self->can( 'populate' );
122              
123             # return an appropriate throw-away value
124 0         0 return;
125 41         178 };
126              
127 41         146870 *{ 'TO_JSON' } = sub {
128 0     0     my $self = shift;
129 0           my $unblessed_copy;
130 0           map { $unblessed_copy->{$_} = $self->{$_}; } keys %attr;
  0            
131 0           return $unblessed_copy;
132 41         169 };
133              
134             }
135              
136             my %dow_to_num = (
137             'MON' => 1,
138             'TUE' => 2,
139             'WED' => 3,
140             'THU' => 4,
141             'FRI' => 5,
142             'SAT' => 6,
143             'SUN' => 7,
144             );
145             my %num_to_dow = reverse %dow_to_num;
146              
147              
148              
149             =head1 NAME
150              
151             App::Dochazka::REST::Fillup - fillup routines
152              
153              
154              
155              
156             =head1 SYNOPSIS
157              
158             use App::Dochazka::REST::Fillup;
159              
160             ...
161              
162              
163              
164              
165             =head1 METHODS
166              
167              
168             =head2 populate
169              
170             Get the next TIID and store in the object
171              
172             =cut
173              
174             sub populate {
175 0     0 1   my $self = shift;
176 0 0 0       if ( ! exists( $self->{tiid} ) or ! defined( $self->{tiid} ) or $self->{tiid} == 0 ) {
      0        
177 0           my $ss = _next_tiid();
178 0           $log->info( "Got next TIID: $ss" );
179 0           $self->{tiid} = $ss;
180             }
181 0           return;
182             }
183              
184              
185             =head2 Accessors
186              
187             Make accessors for all the attributes. Already done, above, in BEGIN block.
188              
189             =cut
190              
191              
192             =head2 _vet_context
193              
194             Performs various tests on the C<context> attribute. If the value of that
195             attribute is not what we're expecting, returns a non-OK status. Otherwise,
196             returns an OK status.
197              
198             =cut
199              
200             sub _vet_context {
201 0     0     my $self = shift;
202 0           my %ARGS = @_;
203 0 0         return $CELL->status_not_ok unless $ARGS{context};
204 0 0         return $CELL->status_not_ok unless $ARGS{context}->{dbix_conn};
205 0 0         return $CELL->status_not_ok unless $ARGS{context}->{dbix_conn}->isa('DBIx::Connector');
206 0           $self->context( $ARGS{context} );
207 0           $self->{'vetted'}->{'context'} = 1;
208 0           return $CELL->status_ok;
209             }
210              
211              
212             =head2 _vet_date_spec
213              
214             The user can specify fillup dates either as a tsrange or as a list of
215             individual dates.
216              
217             One or the other must be given, not neither and not both.
218              
219             Returns a status object.
220              
221             =cut
222              
223             sub _vet_date_spec {
224 0     0     my $self = shift;
225 0           my %ARGS = @_;
226 0           $log->debug( "Entering " . __PACKAGE__ . "::_vet_date_spec to enforce date specification policy" );
227              
228 0 0 0       if ( defined( $ARGS{date_list} ) and defined( $ARGS{tsrange} ) ) {
229 0           $log->debug( "date_spec is NOT OK" );
230 0           return $CELL->status_not_ok;
231             }
232 0 0 0       if ( ! defined( $ARGS{date_list} ) and ! defined( $ARGS{tsrange} ) ) {
233 0           $log->debug( "date_spec is NOT OK" );
234 0           return $CELL->status_not_ok;
235             }
236 0           $self->{'vetted'}->{'date_spec'} = 1;
237 0           $log->debug( "date_spec is OK" );
238 0           return $CELL->status_ok;
239             }
240              
241              
242             =head2 _vet_date_list
243              
244             This function takes one named argument: date_list, the value of which must
245             be a reference to an array of dates, each in canonical YYYY-MM-DD form. For
246             example, this
247              
248             [ '2016-01-13', '2016-01-27', '2016-01-14' ]
249              
250             is a legal C<date_list> argument.
251              
252             This function performs various checks on the date list, sorts it, and
253             populates the C<tsrange> and C<tsranges> attributes based on it. For the
254             sample date list given above, the tsrange will be something like
255              
256             { tsrange => "[\"2016-01-13 00:00:00+01\",\"2016-01-28 00:00:00+01\")" }
257            
258             This is used to make sure the employee's schedule and priv level did not
259             change during the time period represented by the date list, as well as in
260             C<fillup_tempintvls> to generate the C<tempintvl> working set.
261              
262             Returns a status object.
263              
264             =cut
265              
266             sub _vet_date_list {
267 0     0     my $self = shift;
268 0           my ( %ARGS ) = validate( @_, {
269             date_list => { type => ARRAYREF|UNDEF },
270             } );
271 0           $log->debug( "Entering " . __PACKAGE__ . "::_vet_date_list to vet/populate the date_list property" );
272 0 0         if ( $ARGS{'date_list'} ) {
273 0           $log->debug( "Date list is " . Dumper $ARGS{'date_list'} );
274             }
275              
276 0 0         die "GOPHFQQ! tsrange property must not be populated in _vet_date_list()" if $self->tsrange;
277              
278 0 0         return $CELL->status_ok if not defined( $ARGS{date_list} );
279 0 0         return $CELL->status_err( 'DOCHAZKA_EMPTY_DATE_LIST' ) if scalar( @{ $ARGS{date_list} } ) == 0;
  0            
280              
281             # check that dates are valid and in canonical form
282 0           my @canonicalized_date_list = ();
283 0           foreach my $date ( @{ $ARGS{date_list} } ) {
  0            
284 0           my ( $y, $m, $d ) = canon_to_ymd( $date );
285 0 0         if ( ! check_date( $y, $m, $d ) ) {
286 0           return $CELL->status_err(
287             "DOCHAZKA_INVALID_DATE_IN_DATE_LIST",
288             args => [ $date ],
289             );
290             }
291 0           push @canonicalized_date_list, sprintf( "%04d-%02d-%02d", $y, $m, $d );
292             }
293 0           my @sorted_date_list = sort @canonicalized_date_list;
294 0           $self->date_list( \@sorted_date_list );
295              
296 0           my $noof_entries = scalar( @{ $self->date_list } );
  0            
297 0 0         if ( $noof_entries > $site->DOCHAZKA_INTERVAL_FILLUP_MAX_DATELIST_ENTRIES ) {
298 0           return $CELL->status_err(
299             'DOCHAZKA_INTERVAL_FILLUP_DATELIST_TOO_LONG',
300             args => [ $noof_entries ],
301             );
302             }
303              
304             # populate tsrange
305 0 0         if ( scalar @sorted_date_list == 0 ) {
    0          
306 0           $self->tsrange( undef );
307             } elsif ( scalar @sorted_date_list == 1 ) {
308 0           my $t = "[ $sorted_date_list[0] 00:00, $sorted_date_list[0] 24:00 )";
309 0           my $status = canonicalize_tsrange( $self->context->{dbix_conn}, $t );
310 0 0         return $status unless $status->ok;
311 0           $self->tsrange( { tsrange => $status->payload } );
312             } else {
313 0           my $t = "[ $sorted_date_list[0] 00:00, $sorted_date_list[-1] 24:00 )";
314 0           my $status = canonicalize_tsrange( $self->context->{dbix_conn}, $t );
315 0 0         return $status unless $status->ok;
316 0           $self->tsrange( { tsrange => $status->payload } );
317             }
318              
319             # populate tsranges
320 0 0         if ( scalar @sorted_date_list == 0 ) {
321 0           $self->tsranges( undef );
322             } else {
323 0           my @tsranges = ();
324 0           foreach my $date ( @sorted_date_list ) {
325 0           my $t = "[ $date 00:00, $date 24:00 )";
326             my $status = canonicalize_tsrange(
327             $self->context->{dbix_conn},
328 0           $t,
329             );
330 0 0         return $status unless $status->ok;
331             # push canonicalized tsrange onto result stack
332 0           push @tsranges, { tsrange => $status->payload };
333             }
334 0           $self->tsranges( \@tsranges );
335             }
336            
337 0           $self->{'vetted'}->{'date_list'} = 1;
338 0           return $CELL->status_ok;
339             }
340              
341              
342             =head2 _vet_tsrange
343              
344             Takes constructor arguments. Checks the tsrange for sanity and populates
345             the C<tsrange>, C<lower_canon>, C<lower_ymd>, C<upper_canon>, C<upper_ymd>
346             attributes. Returns a status object.
347              
348             =cut
349              
350             sub _vet_tsrange {
351 0     0     my $self = shift;
352 0           my %ARGS = @_;
353             $log->debug( "Entering " . __PACKAGE__ . "::_vet_tsrange to vet the tsrange " .
354 0 0         ( defined( $ARGS{tsrange} ) ? $ARGS{tsrange} : "(undef)" ) );
355              
356 0 0         die "YAHOOEY! No DBIx::Connector in object" unless $self->context->{dbix_conn};
357              
358             # if a tsrange property was given in the arguments, that means no
359             # date_list was given: convert the tsrange argument into an arrayref
360 0 0         if ( my $t = $ARGS{tsrange} ) {
361             my $status = canonicalize_tsrange(
362             $self->context->{dbix_conn},
363 0           $t,
364             );
365 0 0         return $status unless $status->ok;
366 0           $self->tsrange( { tsrange => $status->payload } );
367 0           $self->tsranges( [ { tsrange => $status->payload } ] );
368             }
369              
370 0           foreach my $t_hash ( @{ $self->tsranges }, $self->tsrange ) {
  0            
371              
372             # split the tsrange
373 0           my @parens = $t_hash->{tsrange} =~ m/[^\[(]*([\[(])[^\])]*([\])])/;
374 0           my $status = split_tsrange( $self->context->{'dbix_conn'}, $t_hash->{tsrange} );
375 0           $log->info( "split_tsrange() returned: " . Dumper( $status ) );
376 0 0         return $status unless $status->ok;
377 0           my $low = $status->payload->[0];
378 0           my $upp = $status->payload->[1];
379 0           my @low = canon_to_ymd( $low );
380 0           my @upp = canon_to_ymd( $upp );
381              
382             # lower date bound = tsrange:begin_date minus one day
383 0           @low = Add_Delta_Days( @low, -1 );
384 0           $low = ymd_to_canon( @low );
385              
386             # upper date bound = tsrange:begin_date plus one day
387 0           @upp = Add_Delta_Days( @upp, 1 );
388 0           $upp = ymd_to_canon( @upp );
389              
390             # check DOCHAZKA_INTERVAL_FILLUP_LIMIT
391             # - add two days to the limit to account for how we just stretched $low and $upp
392 0           my $fillup_limit = $site->DOCHAZKA_INTERVAL_FILLUP_LIMIT + 2;
393 0 0         if ( $fillup_limit < canon_date_diff( $low, $upp ) ) {
394 0           return $CELL->status_err( 'DOCHAZKA_FILLUP_TSRANGE_TOO_LONG', args => [ $ARGS{tsrange} ] )
395             }
396              
397 0           $t_hash->{'lower_ymd'} = \@low;
398 0           $t_hash->{'upper_ymd'} = \@upp;
399 0           $t_hash->{'lower_canon'} = $low;
400 0           $t_hash->{'upper_canon'} = $upp;
401             }
402              
403 0           $self->{'vetted'}->{'tsrange'} = 1;
404 0           return $CELL->status_ok( 'SUCCESS' );
405             }
406              
407              
408             =head2 _vet_employee
409              
410             Expects to be called *after* C<_vet_tsrange>.
411              
412             Takes an employee object. First, retrieves
413             from the database the employee object corresponding to the EID. Second,
414             checks that the employee's privlevel did not change during the tsrange.
415             Third, retrieves the prevailing schedule and checks that the schedule does
416             not change at all during the tsrange. Returns a status object.
417              
418             =cut
419              
420             sub _vet_employee {
421 0     0     my $self = shift;
422 0           my ( %ARGS ) = validate( @_, {
423             emp_obj => {
424             type => HASHREF,
425             isa => 'App::Dochazka::REST::Model::Employee',
426             },
427             } );
428 0           my $status;
429              
430 0 0         die 'AKLDWW###%AAAAAH!' unless $ARGS{emp_obj}->eid;
431 0           $self->{'emp_obj'} = $ARGS{emp_obj};
432              
433 0           $log->debug( "Fillup _vet_employee(): check for priv changes during the tsrange" );
434 0 0         if ( $self->{'emp_obj'}->priv_change_during_range(
435             $self->context->{'dbix_conn'},
436             $self->tsrange->{'tsrange'},
437             ) ) {
438 0           return $CELL->status_err( 'DOCHAZKA_EMPLOYEE_PRIV_CHANGED' );
439             }
440 0           $log->debug( "Fillup _vet_employee(): check for schedule changes during the tsrange" );
441 0 0         if ( $self->{'emp_obj'}->schedule_change_during_range(
442             $self->context->{'dbix_conn'},
443             $self->tsrange->{'tsrange'},
444             ) ) {
445 0           return $CELL->status_err( 'DOCHAZKA_EMPLOYEE_SCHEDULE_CHANGED' );
446             }
447              
448             # get privhistory record prevailing at beginning of tsrange
449             my $probj = $self->{emp_obj}->privhistory_at_timestamp(
450             $self->context->{'dbix_conn'},
451 0           $self->tsrange->{'tsrange'},
452             );
453 0 0         if ( ! $probj->priv ) {
454 0           return $CELL->status_err( 'DISPATCH_EMPLOYEE_NO_PRIVHISTORY' );
455             }
456 0 0 0       if ( $probj->priv eq 'active' or $probj->priv eq 'admin' ) {
457             # all green
458             } else {
459 0           return $CELL->status_err( 'DOCHAZKA_INSUFFICIENT_PRIVILEGE', args => [ $probj->priv ] );
460             }
461              
462             # get schedhistory record prevailing at beginning of tsrange
463             my $shobj = $self->{emp_obj}->schedhistory_at_timestamp(
464             $self->context->{'dbix_conn'},
465 0           $self->tsrange->{'tsrange'},
466             );
467 0 0         if ( ! $shobj->sid ) {
468 0           return $CELL->status_err( 'DISPATCH_EMPLOYEE_NO_SCHEDULE' );
469             }
470             my $sched_obj = App::Dochazka::REST::Model::Schedule->load_by_sid(
471 0           $self->context->{'dbix_conn'},
472             $shobj->sid,
473             )->payload;
474 0 0 0       die "AGAHO-NO!" unless ref( $sched_obj) eq 'App::Dochazka::REST::Model::Schedule'
475             and $sched_obj->schedule =~ m/high_dow/;
476 0           $self->{'sched_obj'} = $sched_obj;
477              
478 0           $self->{'vetted'}->{'employee'} = 1;
479 0           return $CELL->status_ok( 'SUCCESS' );
480             }
481              
482              
483             =head2 _vet_activity
484              
485             Takes a C<DBIx::Connector> object and an AID. Verifies that the AID exists
486             and populates the C<activity_obj> attribute.
487              
488             =cut
489              
490             sub _vet_activity {
491 0     0     my $self = shift;
492 0           my ( %ARGS ) = validate( @_, {
493             aid => { type => SCALAR|UNDEF, optional => 1 },
494             } );
495 0           my $status;
496              
497 0 0 0       if ( exists( $ARGS{aid} ) and defined( $ARGS{aid} ) ) {
498             # load activity object from database into $self->{act_obj}
499             $status = App::Dochazka::REST::Model::Activity->load_by_aid(
500             $self->context->{'dbix_conn'},
501             $ARGS{aid}
502 0           );
503 0 0 0       if ( $status->ok and $status->code eq 'DISPATCH_RECORDS_FOUND' ) {
    0 0        
504             # all green; fall thru to success
505 0           $self->{'act_obj'} = $status->payload;
506 0           $self->{'aid'} = $status->payload->aid;
507             } elsif ( $status->level eq 'NOTICE' and $status->code eq 'DISPATCH_NO_RECORDS_FOUND' ) {
508             # non-existent activity
509 0           return $CELL->status_err( 'DOCHAZKA_GENERIC_NOT_EXIST', args => [ 'activity', 'AID', $ARGS{aid} ] );
510             } else {
511 0           return $status;
512             }
513             } else {
514             # if no aid given, try to look up "WORK"
515             $status = App::Dochazka::REST::Model::Activity->load_by_code(
516 0           $self->context->{'dbix_conn'},
517             'WORK'
518             );
519 0 0 0       if ( $status->ok and $status->code eq 'DISPATCH_RECORDS_FOUND' ) {
    0 0        
520             # all green; fall thru to success
521 0           $self->{'act_obj'} = $status->payload;
522 0           $self->{'aid'} = $status->payload->aid;
523             } elsif ( $status->level eq 'NOTICE' and $status->code eq 'DISPATCH_NO_RECORDS_FOUND' ) {
524 0           return $CELL->status_err( 'DOCHAZKA_GENERIC_NOT_EXIST', args => [ 'activity', 'code', 'WORK' ] );
525             } else {
526 0           return $status;
527             }
528             }
529              
530 0           $self->{'vetted'}->{'activity'} = 1;
531 0           return $CELL->status_ok( 'SUCCESS' );
532             }
533              
534              
535             =head2 vetted
536              
537             Returns boolean true if object has been completely vetted. Otherwise false.
538              
539             =cut
540              
541             sub vetted {
542 0     0 1   my $self = shift;
543             (
544             $self->{'vetted'}->{'tsrange'} and
545             $self->{'tsrange'} and
546             $self->{'vetted'}->{'employee'} and
547             $self->emp_obj and
548             ref( $self->emp_obj ) eq 'App::Dochazka::REST::Model::Employee' and
549 0 0 0       $self->{'vetted'}->{'activity'} and
550             $self->act_obj and
551             ref( $self->act_obj ) eq 'App::Dochazka::REST::Model::Activity'
552             ) ? 1 : 0;
553             }
554              
555              
556             =head2 fillup_tempintvls
557              
558             This method takes no arguments and expects to be called on a fully vetted
559             object (see C<vetted>, above).
560              
561             This method creates (and attempts to INSERT records corresponding to) a
562             number of Tempintvl objects according to the C<tsrange> (as stored in the
563             Fillup object) and the employee's schedule.
564              
565             Note that the purpose of this method is to generate a set of Tempintvl
566             objects that could potentially become attendance intervals. The
567             C<fillup_tempintvls> method only deals with Tempintvls. It is up to the
568             C<commit> method to choose the right Tempintvls for the fillup
569             operation in question and to construct and insert the corresponding
570             Interval objects.
571              
572             Returns a status object.
573              
574             =cut
575              
576             sub fillup_tempintvls {
577 0     0 1   my $self = shift;
578 0           $log->debug( "Entering " . __PACKAGE__ . "::fillup_tempintvls" );
579              
580 0 0         die "FILLUP_OBJECT_NOT_VETTED" unless $self->vetted;
581              
582 0           my $rest_sched_hash_lower = _init_lower_sched_hash( $self->{sched_obj}->schedule );
583              
584 0           my $status;
585             my @pushed_intervals;
586              
587             my $holidays = holidays_in_daterange(
588             'begin' => $self->tsrange->{lower_canon},
589             'end' => $self->tsrange->{upper_canon},
590 0           );
591              
592             # create a bunch of Tempintvl objects
593 0           my @tempintvls;
594              
595 0           my $d = $self->tsrange->{'lower_canon'};
596 0           my $days_upper = Date_to_Days( @{ $self->tsrange->{upper_ymd} } );
  0            
597 0           WHILE_LOOP: while ( $d ne get_tomorrow( $self->tsrange->{'upper_canon'} ) ) {
598 0 0         if ( _is_holiday( $d, $holidays ) ) {
599 0           $d = get_tomorrow( $d );
600 0           next WHILE_LOOP;
601             }
602              
603 0           my ( $ly, $lm, $ld ) = canon_to_ymd( $d );
604 0           my $days_lower = Date_to_Days( $ly, $lm, $ld );
605 0           my $ndow = Day_of_Week( $ly, $lm, $ld );
606              
607             # get schedule entries starting on that DOW
608 0           foreach my $entry ( @{ $rest_sched_hash_lower->{ $ndow } } ) {
  0            
609 0           my ( $days_high_dow, $hy, $hm, $hd );
610             # convert "high_dow" into a number of days
611             $days_high_dow = $days_lower +
612 0           ( $dow_to_num{ $entry->{'high_dow'} } - $dow_to_num{ $entry->{'low_dow'} } );
613 0 0         if ( $days_high_dow <= $days_upper ) {
614              
615             # create a Tempintvl object
616 0           my $to = App::Dochazka::REST::Model::Tempintvl->spawn( tiid => $self->tiid );
617 0 0 0       die "COUGH! GAG! Tempintvl object tiid problem!"
618             unless $to->tiid and $to->tiid == $self->tiid;
619              
620             # compile the intvl
621 0           ( $hy, $hm, $hd ) = Days_to_Date( $days_high_dow );
622             $to->intvl( "[ " . ymd_to_canon( $ly,$lm,$ld ) . " " . $entry->{'low_time'} .
623 0           ", " . ymd_to_canon( $hy,$hm,$hd ) . " ". $entry->{'high_time'} . " )" );
624              
625             # insert the object
626 0           my $status = $to->insert( $self->context );
627 0 0         return $status unless $status->ok;
628              
629             # push it onto results array
630 0           push @tempintvls, $to;
631             }
632             }
633 0           $d = get_tomorrow( $d );
634             }
635              
636 0           $log->debug( "fillup_tempintvls completed successfully, " . scalar( @tempintvls ) .
637             " tempintvl objects created and inserted into database" );
638 0           $self->intervals( \@tempintvls );
639 0           return $CELL->status_ok( 'DOCHAZKA_TEMPINTVLS_INSERT_OK' );
640             }
641              
642              
643             =head2 new
644              
645             Constructor method. Returns an C<App::Dochazka::REST::Fillup>
646             object.
647              
648             The constructor method does everything up to C<fillup>. It also populates the
649             C<constructor_status> attribute with an C<App::CELL::Status> object.
650              
651             =cut
652              
653             sub new {
654 0     0 1   my $class = shift;
655 0           my ( %ARGS ) = validate( @_, {
656             context => { type => HASHREF },
657             emp_obj => {
658             type => HASHREF,
659             isa => 'App::Dochazka::REST::Model::Employee',
660             },
661             aid => { type => SCALAR|UNDEF, optional => 1 },
662             code => { type => SCALAR|UNDEF, optional => 1 },
663             tsrange => { type => SCALAR, optional => 1 },
664             date_list => { type => ARRAYREF, optional => 1 },
665             long_desc => { type => SCALAR|UNDEF, optional => 1 },
666             remark => { type => SCALAR|UNDEF, optional => 1 },
667             clobber => { default => 0 },
668             dry_run => { default => 0 },
669             } );
670 0           $log->debug( "Entering " . __PACKAGE__ . "::new" );
671              
672 0           my ( $self, $status );
673             # (re-)initialize $self
674 0 0         if ( $class eq __PACKAGE__ ) {
675 0           $self = bless {}, $class;
676 0           $self->populate();
677             } else {
678 0           die "AGHOOPOWDD@! Constructor must be called like this App::Dochazka::REST::Fillup->new()";
679             }
680 0 0         die "AGHOOPOWDD@! No tiid in Fillup object!" unless $self->tiid;
681              
682             map {
683 0 0         if ( ref( $ARGS{$_} ) eq 'JSON::PP::Boolean' ) {
  0            
684 0 0         $ARGS{$_} = $ARGS{$_} ? 1 : 0;
685             }
686 0 0         $self->$_( $ARGS{$_} ) if defined( $ARGS{$_} );
687             } qw( long_desc remark clobber dry_run );
688              
689             # the order of the following checks is significant!
690 0           $self->constructor_status( $self->_vet_context( context => $ARGS{context} ) );
691 0 0         return $self unless $self->constructor_status->ok;
692 0           $self->constructor_status( $self->_vet_date_spec( %ARGS ) );
693 0 0         return $self unless $self->constructor_status->ok;
694 0           $self->constructor_status( $self->_vet_date_list( date_list => $ARGS{date_list} ) );
695 0 0         return $self unless $self->constructor_status->ok;
696 0           $self->constructor_status( $self->_vet_tsrange( %ARGS ) );
697 0 0         return $self unless $self->constructor_status->ok;
698 0           $self->constructor_status( $self->_vet_employee( emp_obj => $ARGS{emp_obj} ) );
699 0 0         return $self unless $self->constructor_status->ok;
700 0           $self->constructor_status( $self->_vet_activity( aid => $ARGS{aid} ) );
701 0 0         return $self unless $self->constructor_status->ok;
702 0 0         die "AGHGCHKFSCK! should be vetted by now!" unless $self->vetted;
703              
704 0           $self->constructor_status( $self->fillup_tempintvls );
705 0 0         return $self unless $self->constructor_status->ok;
706              
707 0           return $self;
708             }
709              
710              
711             =head2 commit
712              
713             If the C<dry_run> attribute is true, assemble and return an array of attendance
714             intervals that would need to be created to reach 100% schedule fulfillment over
715             the tsranges.
716              
717             If the C<dry_run> attribute is false, iterate over all those intervals and
718             INSERT them into the intervals table.
719              
720             Alternatively, if C<dry_run> is true and C<clobber> is true, ignore existing
721             attendance intervals that might conflict and just return the scheduled intervals.
722             If C<dry_run> is false, C<clobber> setting is ignored.
723              
724             Returns a status object containing all the fillup intervals generated, divided
725             into "success" and "failure" sets, with the latter containing any intervals
726             that failed to be inserted for whatever reason. If C<dry_run> is true, all the
727             intervals will be in the "success" set.
728              
729             =cut
730              
731             sub commit {
732 0     0 1   my $self = shift;
733 0 0         $log->debug( "Entering " . __PACKAGE__ . "::commit with dry_run " . ( $self->dry_run ? "TRUE" : "FALSE" ) );
734              
735 0           my ( $code, $status, @result_set, @conflicting, @success_set, @failure_set );
736              
737 0           foreach my $t_hash ( @{ $self->tsranges } ) {
  0            
738             my $tempintvls = fetch_tempintvls_by_tiid_and_tsrange(
739             $self->context->{dbix_conn},
740             $self->tiid,
741             $t_hash->{tsrange},
742 0           );
743              
744             # Iterate over the tempintvl objects, each of which corresponds
745             # to a scheduled interval in the fillup period.
746 0           TEMPINTVL_LOOP: foreach my $tempintvl ( @$tempintvls ) {
747              
748 0 0         if ( $self->clobber ) {
749 0           push @result_set, $self->_gen_int( $tempintvl->intvl );
750 0           next TEMPINTVL_LOOP;
751             }
752              
753             # check for existing attendance intervals that conflict
754 0           @conflicting = ();
755 0           push @conflicting, @{ $self->_conflicting_intervals( $tempintvl ) };
  0            
756 0           $log->debug( "Conflicting intervals" . Dumper \@conflicting );
757              
758             # for each conflicting interval, generate new intervals to
759             # reach 100% fulfillment of the scheduled interval
760 0           my $conflicts = scalar @conflicting;
761 0           my $count = 0;
762 0           CONFLICTING_LOOP: foreach my $this ( @conflicting ) {
763 0           my ( $next, $newintvl );
764 0 0         if ( $count == 0 ) {
765             # $newintvl might be from the beginning of $tempintvl to the $beginning of $this
766 0           $self->_tsrange_begin_to_begin( $tempintvl, $this, \@result_set );
767             }
768 0 0         if ( $count < $conflicts - 1 ) {
769 0           $next = $conflicting[$count + 1];
770             # $newintvl might be from the end of $this to the beginning of $next
771 0           $self->_tsrange_end_to_begin( $this, $next, \@result_set );
772             }
773 0 0         if ( $count == $conflicts - 1 ) {
774             # $newintvl might be from the end of $this to the end of $tempintvl
775 0           $self->_tsrange_end_to_end( $this, $tempintvl, \@result_set );
776             }
777 0           $count += 1;
778             }
779              
780 0 0         if ( $count == 0 ) {
781 0           push @result_set, $self->_gen_int( $tempintvl->intvl );
782             }
783             }
784             }
785              
786 0           foreach my $int ( @result_set ) {
787 0 0         if ( $self->dry_run ) {
788 0           push @success_set, $int;
789             } else {
790 0           $status = $int->insert( $self->context );
791 0 0         if ( $status->ok ) {
792 0           push @success_set, $int;
793             } else {
794 0           push @failure_set, {
795             interval => $int,
796             status => $status->expurgate,
797             };
798             }
799             }
800             }
801              
802 0           my $pl = {
803             "success" => {
804             count => scalar @success_set,
805             intervals => \@success_set,
806             },
807             "failure" => {
808             count => scalar @failure_set,
809             intervals => \@failure_set,
810             },
811             };
812 0 0         if ( my $count = scalar @result_set ) {
813 0 0         $code = 'DISPATCH_SCHEDULED_INTERVALS_' . ( $self->dry_run ? 'IDENTIFIED' : 'CREATED' );
814 0           return $CELL->status_ok(
815             $code,
816             args => [ $count ],
817             payload => $pl,
818             count => $count,
819             );
820             }
821 0 0         $code = 'DISPATCH_NO_SCHEDULED_INTERVALS_' . ( $self->dry_run ? 'IDENTIFIED' : 'CREATED' );
822 0           return $CELL->status_ok( $code, count => 0 );
823             }
824              
825             sub _gen_int {
826 0     0     my ( $self, $intvl ) = @_;
827 0   0       return App::Dochazka::REST::Model::Interval->spawn(
828             eid => $self->emp_obj->eid,
829             aid => $self->act_obj->aid,
830             code => $self->act_obj->code,
831             intvl => $intvl,
832             long_desc => $self->long_desc,
833             remark => $self->remark || 'fillup',
834             partial => 0,
835             );
836             }
837              
838             # Given two intervals which are assumed to be in order, construct a new
839             # interval from the beginning of the first to the beginning of the second
840             # and push it onto @$result_set
841             sub _tsrange_begin_to_begin {
842 0     0     my ( $self, $this, $next, $result_set ) = @_;
843              
844 0           my ( $status, $pl, $t );
845              
846 0           $status = tsrange_to_dates_and_times( $this->intvl );
847 0 0         return unless $status->ok;
848 0           $pl = $status->payload;
849 0           my $this_begin = "\"" . $pl->{begin}->[0] . " " . $pl->{begin}->[1] . "\"";
850              
851 0           $status = tsrange_to_dates_and_times( $next->intvl );
852 0 0         return unless $status->ok;
853 0           $pl = $status->payload;
854 0           my $next_begin = "\"" . $pl->{begin}->[0] . " " . $pl->{begin}->[1] . "\"";
855              
856 0           $t = "[ " . $this_begin . ", " . $next_begin . " )";
857 0           $status = canonicalize_tsrange( $self->context->{dbix_conn}, $t );
858 0 0         return unless $status->ok;
859 0           push @$result_set, $self->_gen_int( $status->payload );
860             }
861              
862             # Given two intervals which are assumed to be in order, construct a new
863             # interval from the end of the first to the beginning of the second
864             # and push it onto @$result_set
865             sub _tsrange_end_to_begin {
866 0     0     my ( $self, $this, $next, $result_set ) = @_;
867              
868 0           my ( $status, $pl, $t );
869              
870 0           $status = tsrange_to_dates_and_times( $this->intvl );
871 0 0         return unless $status->ok;
872 0           $pl = $status->payload;
873 0           my $this_end = "\"" . $pl->{end}->[0] . " " . $pl->{end}->[1] . "\"";
874              
875 0           $status = tsrange_to_dates_and_times( $next->intvl );
876 0 0         return unless $status->ok;
877 0           $pl = $status->payload;
878 0           my $next_begin = "\"" . $pl->{begin}->[0] . " " . $pl->{begin}->[1] . "\"";
879              
880 0           $t = "[ " . $this_end . ", " . $next_begin . " )";
881 0           $status = canonicalize_tsrange( $self->context->{dbix_conn}, $t );
882 0 0         return unless $status->ok;
883 0           push @$result_set, $self->_gen_int( $status->payload );
884             }
885              
886             # Given two intervals which are assumed to be in order, construct a new
887             # interval from the end of the first to the end of the second
888             # and push it onto @$result_set
889             sub _tsrange_end_to_end {
890 0     0     my ( $self, $this, $next, $result_set ) = @_;
891              
892 0           my ( $status, $pl, $t );
893              
894 0           $status = tsrange_to_dates_and_times( $this->intvl );
895 0 0         return unless $status->ok;
896 0           $pl = $status->payload;
897 0           my $this_end = "\"" . $pl->{end}->[0] . " " . $pl->{end}->[1] . "\"";
898              
899 0           $status = tsrange_to_dates_and_times( $next->intvl );
900 0 0         return unless $status->ok;
901 0           $pl = $status->payload;
902 0           my $next_end = "\"" . $pl->{end}->[0] . " " . $pl->{end}->[1] . "\"";
903              
904 0           $t = "[ " . $this_end . ", " . $next_end . " )";
905 0           $status = canonicalize_tsrange( $self->context->{dbix_conn}, $t );
906 0 0         return unless $status->ok;
907 0           push @$result_set, $self->_gen_int( $status->payload );
908             }
909              
910             # Given a tempintvl object (which represents a single scheduled interval), find
911             # and return reference to array of existing attendance intervals that conflict
912             # with it.
913             sub _conflicting_intervals {
914 0     0     my ( $self, $tempintvl ) = @_;
915              
916 0           my @conflicting_intervals = ();
917              
918             my $status = fetch_intervals_by_eid_and_tsrange_inclusive(
919 0           $self->context->{'dbix_conn'},
920             $self->emp_obj->eid,
921             $tempintvl->intvl,
922             );
923 0 0 0       if ( $status->ok and $status->code eq 'DISPATCH_RECORDS_FOUND' ) {
    0 0        
924 0           foreach my $int ( @{ $status->payload } ) {
  0            
925 0           push @conflicting_intervals, $int;
926             }
927             } elsif ( $status->level eq 'NOTICE' and $status->code eq 'DISPATCH_NO_RECORDS_FOUND' ) {
928 0           $log->debug( "Scheduled interval " . $tempintvl->intvl .
929             " does not overlap with any existing intervals" );
930             } else {
931 0           $log->crit( "IN FILLUP, FAILED TO FETCH CONFLICTING INTERVALS: " . $status->text );
932             }
933 0           return \@conflicting_intervals;
934             }
935              
936              
937             =head2 DESTROY
938              
939             Instance destructor. Once we are done with the scratch intervals, they can be deleted.
940             Returns a status object.
941              
942             =cut
943              
944             sub DESTROY {
945 0     0     my $self = shift;
946 0           $log->debug( "Entering " . __PACKAGE__ . "::DESTROY with arguments " . join( ' ', @_ ) );
947              
948 0 0         $log->notice( "GLOBAL DESTRUCTION" ) if ${^GLOBAL_PHASE} eq 'DESTRUCT';
949              
950 0           my $status;
951             try {
952             $dbix_conn->run( fixup => sub {
953 0           my $sth = $_->prepare( $site->SQL_TEMPINTVLS_DELETE_MULTIPLE );
954 0           $sth->bind_param( 1, $self->tiid );
955 0           $sth->execute;
956 0           my $rows = $sth->rows;
957 0 0         if ( $rows > 0 ) {
    0          
958 0           $status = $CELL->status_ok( 'DOCHAZKA_RECORDS_DELETED', args => [ $rows ], count => $rows );
959             } elsif ( $rows == 0 ) {
960 0           $status = $CELL->status_warn( 'DOCHAZKA_RECORDS_DELETED', args => [ $rows ], count => $rows );
961             } else {
962 0           die( "\$sth->rows returned a weird value $rows" );
963             }
964 0     0     } );
965             } catch {
966 0     0     $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
967 0           };
968 0           $log->notice( "Fillup destructor says " . $status->level . ": " . $status->text );
969 0 0         return $status if $status;
970 0           return $CELL->status_ok;
971             }
972              
973              
974              
975             =head1 FUNCTIONS
976              
977             =head2 _next_tiid
978              
979             Get next value from the temp_intvl_seq sequence
980              
981             =cut
982              
983             sub _next_tiid {
984 0     0     my $val;
985             my $status;
986             try {
987             $dbix_conn->run( fixup => sub {
988 0           ( $val ) = $_->selectrow_array( $site->SQL_NEXT_TIID );
989 0     0     } );
990             } catch {
991 0     0     $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
992 0           };
993 0 0         if ( $status ) {
994 0           $log->crit( $status->text );
995 0           return;
996             }
997 0           return $val;
998             }
999              
1000              
1001             =head2 Days_to_Date
1002              
1003             Missing function in L<Date::Calc>
1004              
1005             =cut
1006              
1007             sub Days_to_Date {
1008 0     0 1   my $canonical = shift;
1009 0           my ( $year, $month, $day ) = Add_Delta_Days(1,1,1, $canonical - 1);
1010 0           return ( $year, $month, $day );
1011             }
1012              
1013              
1014             =head2 _init_lower_sched_hash
1015              
1016             Given schedule hash (JSON string from database), return schedule
1017             hash keyed on the "low_dow" property. In other words, convert the
1018             schedule to hash format keyed on numeric form of "low_dow" i.e. 1 for
1019             MON, 2 for TUE, etc. The values are references to arrays containing
1020             the entries beginning on the given DOW.
1021              
1022             =cut
1023              
1024             sub _init_lower_sched_hash {
1025 0     0     my $rest_sched_json = shift;
1026              
1027             # initialize
1028 0           my $rest_sched_hash_lower = {};
1029 0           foreach my $ndow ( 1 .. 7 ) {
1030 0           $rest_sched_hash_lower->{ $ndow } = [];
1031             }
1032              
1033             # fill up
1034 0           foreach my $entry ( @{ decode_json $rest_sched_json } ) {
  0            
1035 0           my $ndow = $dow_to_num{ $entry->{'low_dow'} };
1036 0           push @{ $rest_sched_hash_lower->{ $ndow } }, $entry;
  0            
1037             }
1038              
1039 0           return $rest_sched_hash_lower;
1040             }
1041              
1042              
1043             =head2 _is_holiday
1044              
1045             Takes a date and a C<$holidays> hashref. Returns true or false.
1046              
1047             =cut
1048              
1049             sub _is_holiday {
1050 0     0     my ( $datum, $holidays ) = @_;
1051 0           return exists( $holidays->{ $datum } );
1052             }
1053              
1054              
1055             =head1 AUTHOR
1056              
1057             Nathan Cutler, C<< <presnypreklad@gmail.com> >>
1058              
1059             =cut
1060              
1061             1;
1062