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