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