File Coverage

blib/lib/App/Dochazka/REST/Model/Interval.pm
Criterion Covered Total %
statement 40 151 26.4
branch 0 58 0.0
condition 0 33 0.0
subroutine 14 22 63.6
pod 8 8 100.0
total 62 272 22.7


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   1943 use strict;
  41         133  
36 41     41   194 use warnings;
  41         73  
  41         764  
37 41     41   184 use App::CELL qw( $CELL $log $meta $site );
  41         68  
  41         1257  
38 41     41   288 use Data::Dumper;
  41         103  
  41         3592  
39 41     41   258 use App::Dochazka::REST::Model::Activity qw( code_by_aid );
  41         87  
  41         1873  
40 41     41   14179 use App::Dochazka::REST::Model::Lock qw( count_locks_in_tsrange );
  41         100  
  41         2108  
41 41     41   15583 use App::Dochazka::REST::Model::Shared qw(
  41         97  
  41         2550  
42 41         2406 canonicalize_tsrange
43             cud
44             cud_generic
45             load
46             load_multiple
47             select_single
48             tsrange_intersection
49             );
50 41     41   262 use App::Dochazka::REST::Holiday qw(
  41         84  
51 41         2663 calculate_hours
52             holidays_and_weekends
53             tsrange_to_dates_and_times
54             );
55 41     41   15531 use Params::Validate qw( :all );
  41         110  
56 41     41   288  
  41         79  
  41         5128  
57             # we get 'spawn', 'reset', and accessors from parent
58             use parent 'App::Dochazka::Common::Model::Interval';
59 41     41   263  
  41         71  
  41         254  
60              
61              
62             =head1 NAME
63              
64             App::Dochazka::REST::Model::Interval - activity intervals data model
65              
66              
67              
68              
69             =head1 SYNOPSIS
70              
71             use App::Dochazka::REST::Model::Interval;
72              
73             ...
74              
75              
76             =head1 DESCRIPTION
77              
78             A description of the activity interval data model follows.
79              
80              
81             =head2 Intervals in the database
82              
83             Activity intervals are stored in the C<intervals> database table, which has
84             the following structure:
85              
86             CREATE TABLE intervals (
87             iid serial PRIMARY KEY,
88             eid integer REFERENCES employees (eid) NOT NULL,
89             aid integer REFERENCES activities (aid) NOT NULL,
90             intvl tsrange NOT NULL,
91             long_desc text,
92             remark text,
93             EXCLUDE USING gist (eid WITH =, intvl WITH &&)
94             );
95              
96             Note the use of the C<tsrange> operator introduced in PostgreSQL 9.2.
97              
98             In addition to the Interval ID (C<iid>), which is assigned by PostgreSQL,
99             the Employee ID (C<eid>), and the Activity ID (C<aid>), which are provided
100             by the client, an interval can optionally have a long description
101             (C<long_desc>), which is the employee's description of what she did during
102             the interval, and an admin remark (C<remark>).
103              
104              
105             =head2 Intervals in the Perl API
106              
107             In the data model, individual activity intervals (records in the
108             C<intervals> table) are represented by "interval objects". All methods
109             and functions for manipulating these objects are contained in this module.
110             The most important methods are:
111              
112             =over
113              
114             =item * constructor (L<spawn>)
115              
116             =item * basic accessors (L<iid>, L<eid>, L<aid>, L<intvl>, L<long_desc>,
117             L<remark>)
118              
119             =item * L<reset> (recycles an existing object by setting it to desired
120             state)
121              
122             =item * L<insert> (inserts object into database)
123              
124             =item * L<delete> (deletes object from database)
125              
126             =back
127              
128             For basic activity interval workflow, see C<t/model/interval.t>.
129              
130              
131              
132              
133             =head1 EXPORTS
134              
135             This module provides the following exports:
136              
137             =cut
138              
139             use Exporter qw( import );
140 41     41   49792 our @EXPORT_OK = qw(
  41         100  
  41         16854  
141             delete_intervals_by_eid_and_tsrange
142             fetch_intervals_by_eid_and_tsrange
143             fetch_intervals_by_eid_and_tsrange_inclusive
144             generate_interval_summary
145             iid_exists
146             );
147              
148              
149              
150             =head1 METHODS
151              
152              
153             =head2 load_by_iid
154              
155             Boilerplate.
156              
157             =cut
158              
159             my $self = shift;
160             my ( $conn, $iid ) = validate_pos( @_,
161 0     0 1   { isa => 'DBIx::Connector' },
162 0           { type => SCALAR },
163             );
164              
165             return load(
166             conn => $conn,
167 0           class => __PACKAGE__,
168             sql => $site->SQL_INTERVAL_SELECT_BY_IID,
169             keys => [ $iid ],
170             );
171             }
172            
173              
174             =head2 insert
175              
176             Instance method. Attempts to INSERT a record.
177             Field values are taken from the object. Returns a status object.
178              
179             =cut
180              
181             my $self = shift;
182             my ( $context ) = validate_pos( @_, { type => HASHREF } );
183              
184 0     0 1   return $CELL->status_err(
185 0           "DOCHAZKA_PARTIAL_INTERVAL_ILLEGAL_OPERATION"
186             ) if $self->partial;
187 0 0          
188             my $status = cud(
189             conn => $context->{'dbix_conn'},
190             eid => $context->{'current'}->{'eid'},
191             object => $self,
192             sql => $site->SQL_INTERVAL_INSERT,
193 0           attrs => [ 'eid', 'aid', 'intvl', 'long_desc', 'remark' ],
194             );
195              
196             return $status;
197             }
198              
199 0            
200             =head2 update
201              
202             Instance method. Attempts to UPDATE a record.
203             Field values are taken from the object. Returns a status object.
204              
205             =cut
206              
207             my $self = shift;
208             my ( $context ) = validate_pos( @_, { type => HASHREF } );
209              
210             return $CELL->status_err( 'DOCHAZKA_MALFORMED_400' ) unless $self->{'iid'};
211 0     0 1    
212 0           return $CELL->status_err(
213             "DOCHAZKA_PARTIAL_INTERVAL_ILLEGAL_OPERATION"
214 0 0         ) if $self->partial;
215              
216 0 0         my $status = cud(
217             conn => $context->{'dbix_conn'},
218             eid => $context->{'current'}->{'eid'},
219             object => $self,
220             sql => $site->SQL_INTERVAL_UPDATE,
221             attrs => [ qw( eid aid intvl long_desc remark iid ) ],
222 0           );
223              
224             return $status;
225             }
226              
227              
228 0           =head2 delete
229              
230             Instance method. Attempts to DELETE a record.
231             Field values are taken from the object. Returns a status object.
232              
233             =cut
234              
235             my $self = shift;
236             my ( $context ) = validate_pos( @_, { type => HASHREF } );
237              
238             return $CELL->status_err(
239             "DOCHAZKA_PARTIAL_INTERVAL_ILLEGAL_OPERATION"
240 0     0 1   ) if $self->partial;
241 0            
242             my $status = cud(
243 0 0         conn => $context->{'dbix_conn'},
244             eid => $context->{'current'}->{'eid'},
245             object => $self,
246             sql => $site->SQL_INTERVAL_DELETE,
247             attrs => [ 'iid' ],
248             );
249 0           $self->reset( iid => $self->{iid} ) if $status->ok;
250              
251             return $status;
252             }
253              
254 0 0          
255              
256 0           =head1 FUNCTIONS
257              
258              
259             =head2 iid_exists
260              
261             Boolean function
262              
263             =cut
264              
265             BEGIN {
266             no strict 'refs';
267             *{'iid_exists'} = App::Dochazka::REST::Model::Shared::make_test_exists( 'iid' );
268             }
269              
270              
271 41     41   276 =head2 fetch_intervals_by_eid_and_tsrange_inclusive
  41         134  
  41         1479  
272 41     41   303  
  41         48155  
273             Given a L<DBIx::Connector> object, an EID and a tsrange, fetch all that
274             employee's intervals that overlap (have at least one point in common with)
275             that tsrange.
276              
277             Returns a status object. If status level is OK, the payload contains at
278             least one interval. If the status level is NOTICE, it means the operation
279             completed successfully and no overlapping intervals were found.
280              
281             =cut
282              
283             my ( $conn, $eid, $tsrange ) = validate_pos( @_,
284             { isa => 'DBIx::Connector' },
285             { type => SCALAR },
286             { type => SCALAR },
287             );
288              
289 0     0 1   my $status = canonicalize_tsrange( $conn, $tsrange );
290             return $status unless $status->ok;
291             $tsrange = $status->payload;
292              
293             $status = App::Dochazka::REST::Model::Employee->load_by_eid( $conn, $eid );
294             return $status unless $status->ok;
295 0            
296 0 0         $status = load_multiple(
297 0           conn => $conn,
298             class => __PACKAGE__,
299 0           sql => $site->SQL_INTERVAL_SELECT_BY_EID_AND_TSRANGE_INCLUSIVE,
300 0 0         keys => [ $eid, $tsrange, $site->DOCHAZKA_INTERVAL_SELECT_LIMIT ],
301             );
302 0            
303             return $status;
304             }
305              
306              
307             =head2 fetch_intervals_by_eid_and_tsrange
308              
309 0           Given a L<DBIx::Connector> object, an EID and a tsrange, return all that
310             employee's intervals that fall within that tsrange. Partial intervals are
311             marked as such (using the C<partial> property).
312              
313             Before any records are returned, the tsrange is checked to see if it
314             overlaps with any privlevel or schedule changes - in which case an error is
315             returned. This is so interval report-generators do not have to handle
316             changes in employee status.
317              
318             =cut
319              
320             my ( $conn, $eid, $tsrange ) = validate_pos( @_,
321             { isa => 'DBIx::Connector' },
322             { type => SCALAR },
323             { type => SCALAR, optional => 1 },
324             );
325              
326             my $status = canonicalize_tsrange( $conn, $tsrange );
327 0     0 1   return $status unless $status->ok;
328             $tsrange = $status->payload;
329              
330             $status = App::Dochazka::REST::Model::Employee->load_by_eid( $conn, $eid );
331             return $status unless $status->ok;
332             my $emp = $status->payload;
333 0           die "AAGHA!" unless $emp->eid == $eid;
334 0 0          
335 0           # check for priv change during tsrange
336             my $priv_change = $emp->priv_change_during_range( $conn, $tsrange );
337 0           $log->debug( "fetch_intervals_by_eid_and_tsrange: priv_change_during_range returned " . Dumper( $priv_change ) );
338 0 0         if ( $priv_change ) {
339 0           return $CELL->status_err( 'DOCHAZKA_EMPLOYEE_PRIV_CHANGED' );
340 0 0         }
341              
342             # check for sched change during tsrange
343 0           my $schedule_change = $emp->schedule_change_during_range( $conn, $tsrange );
344 0           $log->debug( "fetch_intervals_by_eid_and_tsrange: schedule_change_during_range returned " . Dumper($schedule_change ) );
345 0 0         if ( $schedule_change ) {
346 0           return $CELL->status_err( 'DOCHAZKA_EMPLOYEE_SCHEDULE_CHANGED' );
347             }
348              
349             $status = load_multiple(
350 0           conn => $conn,
351 0           class => __PACKAGE__,
352 0 0         sql => $site->SQL_INTERVAL_SELECT_BY_EID_AND_TSRANGE,
353 0           keys => [ $eid, $tsrange, $site->DOCHAZKA_INTERVAL_SELECT_LIMIT ],
354             );
355             return $status unless
356 0           ( $status->ok and $status->code eq 'DISPATCH_RECORDS_FOUND' ) or
357             ( $status->level eq 'NOTICE' and $status->code eq 'DISPATCH_NO_RECORDS_FOUND' );
358             my $whole_intervals = $status->payload;
359              
360             $status = load_multiple(
361             conn => $conn,
362 0 0 0       class => __PACKAGE__,
      0        
      0        
363             sql => $site->SQL_INTERVAL_SELECT_BY_EID_AND_TSRANGE_PARTIAL_INTERVALS,
364             keys => [ $eid, $tsrange, $eid, $tsrange ],
365 0           );
366             return $status unless
367 0           ( $status->ok and $status->code eq 'DISPATCH_RECORDS_FOUND' ) or
368             ( $status->level eq 'NOTICE' and $status->code eq 'DISPATCH_NO_RECORDS_FOUND' );
369             my $partial_intervals = $status->payload;
370              
371             map { $_->partial( 0 ) } ( @$whole_intervals );
372             foreach my $int ( @$partial_intervals ) {
373 0 0 0       $int->partial( 1 );
      0        
      0        
374             $int->intvl( tsrange_intersection( $conn, $tsrange, $int->intvl ) );
375             }
376 0          
377             my $result_set = $whole_intervals;
378 0           push @$result_set, @$partial_intervals;
  0            
379 0           # But now the intervals are out of order
380 0           my @sorted_results = sort { $a->intvl cmp $b->intvl } @$result_set;
381 0            
382             if ( my $count = scalar @$result_set ) {
383             return $CELL->status_ok( 'DISPATCH_RECORDS_FOUND',
384 0           payload => \@sorted_results, count => $count, args => [ $count ] );
385 0           }
386             return $CELL->status_notice( 'DISPATCH_NO_RECORDS_FOUND' );
387 0           }
  0            
388              
389 0 0          
390 0           =head2 delete_intervals_by_eid_and_tsrange
391              
392             Given an EID and a tsrange, delete all that employee's intervals that
393 0           fall within that tsrange.
394              
395             Returns a status object.
396              
397             =cut
398              
399             my ( $conn, $eid, $tsrange ) = validate_pos( @_,
400             { isa => 'DBIx::Connector' },
401             { type => SCALAR },
402             { type => SCALAR },
403             );
404              
405             my $status = canonicalize_tsrange( $conn, $tsrange );
406             return $status unless $status->ok;
407 0     0 1   $tsrange = $status->payload;
408              
409             # check for locks
410             $status = count_locks_in_tsrange( $conn, $eid, $tsrange );
411             return $status unless $status->ok;
412             # number of locks is in $status->payload
413 0           if ( $status->payload > 0 ) {
414 0 0         return $CELL->status_err( 'DOCHAZKA_TSRANGE_LOCKED', args => [ $tsrange, $status->payload ] );
415 0           }
416              
417             $status = App::Dochazka::REST::Model::Employee->load_by_eid( $conn, $eid );
418 0           return $status unless $status->ok;
419 0 0         my $emp = $status->payload;
420             die "AAGHA!" unless $emp->eid == $eid;
421 0 0          
422 0           # check for priv change during tsrange
423             my $search_tsrange = $tsrange;
424             $search_tsrange =~ s/^[^\[]*\[/\(/;
425 0           my $priv_change = $emp->priv_change_during_range( $conn, $search_tsrange );
426 0 0         $log->debug( "delete_intervals_by_eid_and_tsrange: priv_change_during_range returned " . Dumper( $priv_change ) );
427 0           if ( $priv_change ) {
428 0 0         return $CELL->status_err( 'DOCHAZKA_EMPLOYEE_PRIV_CHANGED' );
429             }
430              
431 0           # check for sched change during tsrange
432 0           my $schedule_change = $emp->schedule_change_during_range( $conn, $search_tsrange );
433 0           $log->debug( "delete_intervals_by_eid_and_tsrange: schedule_change_during_range returned " . Dumper($schedule_change ) );
434 0           if ( $schedule_change ) {
435 0 0         return $CELL->status_err( 'DOCHAZKA_EMPLOYEE_SCHEDULE_CHANGED' );
436 0           }
437              
438             # check how many intervals we are talking about here
439             $status = select_single(
440 0           conn => $conn,
441 0           sql => $site->SQL_INTERVAL_SELECT_COUNT_BY_EID_AND_TSRANGE,
442 0 0         keys => [ $eid, $tsrange, $site->DOCHAZKA_INTERVAL_SELECT_LIMIT ],
443 0           );
444             return $status unless $status->ok;
445             # $status->payload contains [ $count ]
446             my $count = $status->payload->[0];
447 0            
448             # if it's greater than or equal to the limit, no go
449             return $CELL->status_err( 'DOCHAZKA_INTERVAL_DELETE_LIMIT_EXCEEDED', args => [ $count ] )
450             if $count >= $site->DOCHAZKA_INTERVAL_DELETE_LIMIT;
451            
452 0 0         return cud_generic(
453             conn => $conn,
454 0           eid => $eid,
455             sql => $site->SQL_INTERVAL_DELETE_BY_EID_AND_TSRANGE,
456             bind_params => [ $eid, $tsrange ],
457 0 0         );
458             }
459              
460 0            
461             =head2 generate_interval_summary
462              
463             Given DBIx::Connector object, EID, and tsrange, generate a hash keyed on
464             dates (YYYY-MM-DD) in the range. The value of each key/date is another
465             hash keyed on activity codes. For each activity code the value is the
466             total number of hours spent by the employee doing that activity on the day
467             in question.
468              
469             The interval must start and end on a day boundary (i.e. 00:00 or 24:00)
470             and partial intervals are treated the same as whole intervals.
471              
472             =cut
473              
474             my ( $conn, $eid, $tsrange ) = validate_pos( @_,
475             { isa => 'DBIx::Connector' },
476             { type => SCALAR },
477             { type => SCALAR },
478             );
479              
480             my $status = canonicalize_tsrange( $conn, $tsrange );
481             return $status unless $status->ok;
482             my $canon_tsrange = $status->payload;
483 0     0 1   $log->debug( "generate_interval_summary: $canon_tsrange" );
484              
485             # convert canonicalized tsrange into begin, end dates
486             $status = tsrange_to_dates_and_times( $canon_tsrange );
487             return $status unless $status->ok;
488              
489 0           # extract the beginning/ending dates/times
490 0 0         my $pl = $status->payload;
491 0           my $begin_date = $pl->{begin}->[0];
492 0           my $begin_time = $pl->{begin}->[1];
493             my $end_date = $pl->{end}->[0];
494             my $end_time = $pl->{end}->[1];
495 0            
496 0 0         # interval must begin and end at 00:00/24:00,
497             # otherwise no game
498             return $CELL->status_err( 'DISPATCH_SUMMARY_ILLEGAL_TSRANGE' ) unless
499 0           ( $begin_time eq '00:00' or $begin_time eq '24:00' ) and
500 0           ( $end_time eq '00:00' or $end_time eq '24:00' );
501 0            
502 0           # get list of dates in range
503 0           my $date_hash = holidays_and_weekends( begin => $begin_date, end => $end_date );
504              
505             # get intervals for each date
506             foreach my $date ( keys %$date_hash ) {
507 0 0 0       my $status = fetch_intervals_by_eid_and_tsrange(
      0        
      0        
508             $conn,
509             $eid,
510             "[ $date 00:00, $date 24:00 )",
511             );
512 0           if ( $status->ok and $status->code eq 'DISPATCH_RECORDS_FOUND' ) {
513             map { $date_hash
514             ->{ $date }
515 0           ->{ code_by_aid( $conn, $_->aid ) } += calculate_hours( $_->intvl )
516 0           } ( @{ $status->payload } );
517             } elsif ( $status->level eq 'NOTICE' and $status->code eq 'DISPATCH_NO_RECORDS_FOUND' ) {
518             # do nothing
519             } else {
520             return $CELL->status_crit(
521 0 0 0       'DISPATCH_SUMMARY_UNEXPECTED_FAILURE',
    0 0        
522             payload => $status->text
523             );
524 0           }
525 0           }
  0            
526              
527             return $CELL->status_ok( 'DISPATCH_SUMMARY_OK', payload => $date_hash );
528             }
529 0            
530              
531              
532             =head1 AUTHOR
533              
534             Nathan Cutler, C<< <presnypreklad@gmail.com> >>
535              
536 0           =cut
537              
538             1;
539