File Coverage

blib/lib/App/Dochazka/REST/Model/Schedule.pm
Criterion Covered Total %
statement 37 89 41.5
branch 0 22 0.0
condition 0 12 0.0
subroutine 13 19 68.4
pod 6 6 100.0
total 56 148 37.8


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   3704 use strict;
  41         132  
36 41     41   225 use warnings;
  41         88  
  41         699  
37 41     41   184 use App::CELL qw( $CELL $log $meta $site );
  41         78  
  41         1053  
38 41     41   224 use App::Dochazka::REST::Model::Shared qw( cud decode_schedule_json load load_multiple select_single );
  41         104  
  41         4087  
39 41     41   1559 use Data::Dumper;
  41         96  
  41         2575  
40 41     41   265 use JSON;
  41         88  
  41         1507  
41 41     41   259 use Params::Validate qw( :all );
  41         89  
  41         253  
42 41     41   4313 use Try::Tiny;
  41         158  
  41         5366  
43 41     41   281  
  41         74  
  41         1789  
44             # we get 'spawn', 'reset', and accessors from parent
45             use parent 'App::Dochazka::Common::Model::Schedule';
46 41     41   1919  
  41         1997  
  41         4744  
47              
48              
49              
50             =head1 NAME
51              
52             App::Dochazka::REST::Model::Schedule - schedule functions
53              
54              
55              
56              
57             =head1 SYNOPSIS
58              
59             use App::Dochazka::REST::Model::Schedule;
60              
61             ...
62              
63              
64              
65             =head1 DESCRIPTION
66              
67             A description of the schedule data model follows.
68              
69              
70              
71             =head2 Schedules in the database
72              
73              
74             =head3 Table
75              
76             Schedules are stored the C<schedules> table. For any given schedule, there is
77             always only one record in the table -- i.e., individual schedules can be used
78             for multiple employees. (For example, an organization might have hundreds of
79             employees on a single, unified schedule.)
80              
81             CREATE TABLE IF NOT EXISTS schedules (
82             sid serial PRIMARY KEY,
83             schedule text UNIQUE NOT NULL,
84             disabled boolean,
85             remark text
86             );
87              
88             The value of the 'schedule' field is a JSON array which looks something like this:
89              
90             [
91             { low_dow:"MON", low_time:"08:00", high_dow:"MON", high_time:"12:00" },
92             { low_dow:"MON", low_time:"12:30", high_dow:"MON", high_time:"16:30" },
93             { low_dow:"TUE", low_time:"08:00", high_dow:"TUE", high_time:"12:00" },
94             { low_dow:"TUE", low_time:"12:30", high_dow:"TUE", high_time:"16:30" },
95             ...
96             ]
97              
98             Or, to give an example of a more convoluted schedule:
99              
100             [
101             { low_dow:"WED", low_time:"22:15", high_dow:"THU", high_time:"03:25" },
102             { low_dow:"THU", low_time:"05:25", high_dow:"THU", high_time:"09:55" },
103             { low_dow:"SAT", low_time:"19:05", high_dow:"SUN", high_time:"24:00" }
104             ]
105              
106             The intervals in the JSON string must be sorted and the whitespace, etc.
107             must be consistent in order for the UNIQUE constraint in the 'schedule'
108             table to work properly. However, these precautions will no longer be
109             necessary after PostgreSQL 9.4 comes out and the field type is changed to
110             'jsonb'.
111              
112             The 'disabled' field is intended go be used to control which schedules get
113             offered in, e.g., front-end dialogs when administrators choose which schedule
114             to assign to a new employee, and the like. For example, there may be schedules
115             in the database that were used in the past, but it is no longer desirable to
116             offer these schedules in the front-end dialog, so the administrator can "remove"
117             them from the dialog by setting this field to 'true'.
118              
119              
120             =head3 Process for creating new schedules
121              
122             It is important to understand how the JSON string introduced in the previous
123             section is assembled -- or, more generally, how a schedule is created. Essentially,
124             the schedule is first created in a C<schedintvls> table, with a record for each
125             time interval in the schedule. This table has triggers and a C<gist> index that
126             enforce schedule data integrity so that only a valid schedule can be inserted.
127             Once the schedule has been successfully built up in C<schedintvls>, it is
128             "translated" (using a stored procedure) into a single JSON string, which is
129             stored in the C<schedules> table. This process is described in more detail below:
130              
131             First, if the schedule already exists in the C<schedules> table, nothing
132             more need be done -- we can skip to L<Schedhistory>
133              
134             If the schedule we need is not yet in the database, we will have to create it.
135             This is a three-step process: (1) build up the schedule in the C<schedintvls>
136             table (sometimes referred to as the "scratch schedule" table because it is used
137             to store an intermediate product with only a short lifespan); (2) translate the
138             schedule to form the schedule's JSON representation; (3) insert the JSON string
139             into the C<schedules> table.
140              
141             The C<schedintvls>, or "scratch schedule", table:
142              
143             CREATE SEQUENCE scratch_sid_seq;
144              
145             CREATE TABLE IF NOT EXISTS schedintvls (
146             int_id serial PRIMARY KEY,
147             ssid integer NOT NULL,
148             intvl tsrange NOT NULL,
149             EXCLUDE USING gist (ssid WITH =, intvl WITH &&)
150             )/,
151              
152             As stated above, before the C<schedule> table is touched, a "scratch schedule"
153             must first be created in the C<schedintvls> table. Although this operation
154             changes the database, it should be seen as a "dry run". The C<gist> index and
155             a trigger assure that:
156              
157             =over
158              
159             =item * no overlapping entries are entered
160              
161             =item * all the entries fall within a single 168-hour period
162              
163             =item * all the times are evenly divisible by five minutes
164              
165             =back
166              
167             #
168             # FIXME: expand the trigger to check for "closed-open" C<< [ ..., ... ) >> tsrange
169             #
170              
171             If the schedule is successfully inserted into C<schedintvls>, the next step is
172             to "translate", or convert, the individual intervals (expressed as tsrange
173             values) into the four-key hashes described in L<Schedules in the database>,
174             assemble the JSON string, and insert a new row in C<schedules>.
175              
176             To facilitate this conversion, a stored procedure C<translate_schedintvl> was
177             developed.
178              
179             Successful insertion into C<schedules> will generate a Schedule ID (SID) for
180             the schedule, enabling it to be used to make Schedhistory objects.
181              
182             At this point, the scratch schedule is deleted from the C<schedintvls> table.
183              
184              
185             =head2 Schedules in the Perl API
186              
187              
188             =head3 L<Schedintvls> class
189              
190             =over
191              
192             =item * constructor (L<spawn>)
193              
194             =item * L<reset> method (recycles an existing object)
195              
196             =item * basic accessor (L<ssid>)
197              
198             =item * L<intvls> accessor (arrayref containing all tsrange intervals in schedule)
199              
200             =item * L<schedule> accessor (arrayref containing "translated" intervals)
201              
202             =item * L<load> method (load the object from the database and translate the tsrange intervals)
203              
204             =item * L<insert> method (insert all the tsrange elements in one go)
205              
206             =item * L<delete> method (delete all the tsrange elements when we're done with them)
207              
208             =item * L<json> method (generate JSON string from the translated intervals)
209              
210             =back
211              
212             For basic workflow, see C<t/model/schedule.t>.
213              
214              
215             =head3 C<Schedule> class
216              
217             =over
218              
219             =item * constructor (L<spawn>)
220              
221             =item * L<reset> method (recycles an existing object)
222              
223             =item * basic accessors (L<sid>, L<schedule>, L<remark>)
224              
225             =item * L<insert> method (inserts the schedule if it isn't in the database already)
226              
227             =item * L<delete> method
228              
229             =item * L<load> method (not implemented yet)
230              
231             #=item * L<get_schedule_json> function (get JSON string associated with a given SID)
232             #
233             =back
234              
235             For basic workflow, see C<t/model/schedule.t>.
236              
237              
238              
239              
240             =head1 EXPORTS
241              
242             This module provides the following exports:
243              
244             =over
245              
246             #=item * C<get_schedule_json>
247             #
248             =item * C<get_all_schedules>
249              
250             =item * C<sid_exists> (boolean)
251              
252             =back
253              
254             =cut
255              
256             use Exporter qw( import );
257 41     41   77318 our @EXPORT_OK = qw(
  41         100  
  41         33129  
258             get_all_schedules
259             sid_exists
260             );
261              
262              
263              
264             =head1 METHODS
265              
266              
267             =head2 insert
268              
269             Instance method. Attempts to INSERT a record into the 'schedules' table.
270             Field values are taken from the object. Returns a status object.
271              
272             If the "schedule" field of the schedule to be inserted matches an existing
273             schedule, no new record is inserted. Instead, the existing schedule record
274             is returned. In such a case, the "scode", "remark", and "disabled" fields
275             are ignored - except when they are NULL in the existing record.
276              
277             =cut
278              
279             my $self = shift;
280             my ( $context ) = validate_pos( @_, { type => HASHREF } );
281 0     0 1    
282 0           # if the exact same schedule is already in the database, we
283             # don't insert it again
284             my $status = select_single(
285             conn => $context->{'dbix_conn'},
286             sql => $site->SQL_SCHEDULES_SELECT_BY_SCHEDULE,
287             keys => [ $self->{schedule} ],
288             );
289 0           $log->info( "select_single returned: " . Dumper $status );
290             if ( $status->level eq 'OK' ) {
291 0           my $found_sched = App::Dochazka::REST::Model::Schedule->spawn(
292 0 0         sid => $status->payload->[0],
    0          
293 0           scode => $status->payload->[1],
294             schedule => $status->payload->[2],
295             remark => $status->payload->[3],
296             disabled => $status->payload->[4],
297             );
298             $self->{'sid'} = $found_sched->sid;
299             {
300 0           #
301             # the exact schedule exists, but if any of { scode, remark, disabled }
302             # are NULL and we have a value, update the record to reflect the value
303             # (in other words, do not prefer NULLs over real values)
304             #
305             my $do_update = 0;
306             if ( ! defined( $found_sched->scode ) and defined( $self->scode ) ) {
307 0           $found_sched->scode( $self->scode );
  0            
308 0 0 0       $do_update = 1;
309 0           }
310 0           if ( ! defined( $found_sched->remark ) and defined( $self->remark ) ) {
311             $found_sched->remark( $self->remark );
312 0 0 0       $do_update = 1;
313 0           }
314 0           if ( ! defined( $found_sched->disabled ) and defined( $self->disabled ) ) {
315             $found_sched->disabled( $self->disabled );
316 0 0 0       $do_update = 1;
317 0           }
318 0           if ( $do_update ) {
319             $status = $found_sched->update( $context );
320 0 0         if ( $status->level eq 'OK' and $status->code eq 'DOCHAZKA_CUD_OK' ) {
321 0           $status->code( 'DOCHAZKA_SCHEDULE_UPDATE_OK' );
322 0 0 0       }
323 0           return $status;
324             }
325 0           return $CELL->status_ok( 'DOCHAZKA_SCHEDULE_EXISTS', args => [ $self->{sid} ],
326             payload => $found_sched );
327 0           }
328             } elsif( $status->level ne 'NOTICE' ) {
329             $log->info( "select_single status was neither OK nor NOTICE; returning it as-is" );
330             return $status;
331 0           }
332 0            
333             # no exact match found, insert a new record
334             $log->debug( __PACKAGE__ . "::insert calling cud to insert new schedule" );
335             $status = cud(
336 0           conn => $context->{'dbix_conn'},
337             eid => $context->{'current'}->{'eid'},
338             object => $self,
339 0           sql => $site->SQL_SCHEDULE_INSERT,
340             attrs => [ 'scode', 'schedule', 'remark' ],
341             );
342              
343             if ( $status->ok ) {
344             $status->code( 'DOCHAZKA_SCHEDULE_INSERT_OK' );
345 0 0         $log->info( "Inserted new schedule with SID " . $self->{sid} );
346 0           }
347 0           return $status;
348             }
349 0            
350              
351             =head2 update
352              
353             Although we do not allow the 'sid' or 'schedule' fields to be updated, schedule
354             records have 'scode', 'remark' and 'disabled' fields that can be updated via this
355             method.
356              
357             =cut
358              
359             my $self = shift;
360             my ( $context ) = validate_pos( @_, { type => HASHREF } );
361              
362 0     0 1   return $CELL->status_err( 'DOCHAZKA_MALFORMED_400' ) unless $self->{'sid'};
363 0            
364             my $status = cud(
365 0 0         conn => $context->{'dbix_conn'},
366             eid => $context->{'current'}->{'eid'},
367             object => $self,
368             sql => $site->SQL_SCHEDULE_UPDATE,
369 0           attrs => [ 'scode', 'remark', 'disabled', 'sid' ],
370             );
371              
372             return $status;
373             }
374              
375 0            
376             =head2 delete
377              
378             Instance method. Attempts to DELETE a schedule record. This may succeed
379             if no other records in the database refer to this schedule.
380              
381             =cut
382              
383             my $self = shift;
384             my ( $context ) = validate_pos( @_, { type => HASHREF } );
385              
386             my $status = cud(
387 0     0 1   conn => $context->{'dbix_conn'},
388 0           eid => $context->{'current'}->{'eid'},
389             object => $self,
390             sql => $site->SQL_SCHEDULE_DELETE,
391             attrs => [ 'sid' ],
392 0           );
393             $self->reset( sid => $self->{sid} ) if $status->ok;
394              
395             $log->debug( "Entering " . __PACKAGE__ . "::delete with status " . Dumper( $status ) );
396             return $status;
397 0 0         }
398              
399 0            
400 0           =head2 load_by_scode
401              
402             Analogous function to L<App::Dochazka::REST::Model::Activity/"load_by_aid">.
403              
404             =cut
405              
406             my $self = shift;
407             my ( $conn, $scode ) = validate_pos( @_,
408             { isa => 'DBIx::Connector' },
409             { type => SCALAR },
410             );
411 0     0 1    
412 0           return load(
413             conn => $conn,
414             class => __PACKAGE__,
415             sql => $site->SQL_SCHEDULE_SELECT_BY_SCODE,
416             keys => [ $scode ],
417 0           );
418             }
419              
420              
421              
422             =head2 load_by_sid
423              
424             Analogous function to L<App::Dochazka::REST::Model::Activity/"load_by_aid">.
425              
426             =cut
427              
428             my $self = shift;
429             my ( $conn, $sid ) = validate_pos( @_,
430             { isa => 'DBIx::Connector' },
431             { type => SCALAR },
432             );
433              
434 0     0 1   return load(
435 0           conn => $conn,
436             class => __PACKAGE__,
437             sql => $site->SQL_SCHEDULE_SELECT_BY_SID,
438             keys => [ $sid ],
439             );
440 0           }
441              
442              
443              
444             =head1 FUNCTIONS
445              
446              
447             =head2 sid_exists
448              
449             Boolean function
450              
451             =cut
452              
453             BEGIN {
454             no strict 'refs';
455             *{'sid_exists'} = App::Dochazka::REST::Model::Shared::make_test_exists( 'sid' );
456             }
457              
458              
459             =head2 get_all_schedules
460 41     41   330  
  41         118  
  41         5039  
461 41     41   5760 Returns a list of all schedule objects, ordered by sid. Takes one
  41         11098  
462             argument - a paramhash that can contain only one key, 'disabled',
463             which can be either true or false (defaults to true).
464              
465             =cut
466              
467             my %PH = validate( @_, {
468             conn => { isa => 'DBIx::Connector' },
469             disabled => { type => SCALAR, default => 0 }
470             } );
471            
472             my $sql = $PH{disabled}
473             ? $site->SQL_SCHEDULES_SELECT_ALL_INCLUDING_DISABLED
474 0     0 1   : $site->SQL_SCHEDULES_SELECT_ALL_EXCEPT_DISABLED;
475              
476             # run the query and gather the results
477              
478             return load_multiple(
479             conn => $PH{'conn'},
480 0 0         class => __PACKAGE__,
481             sql => $sql,
482             keys => [],
483             );
484             }
485              
486 0            
487             #=head2 get_schedule_json
488             #
489             #Given a SID, queries the database for the JSON string associated with the SID.
490             #Returns undef if not found.
491             #
492             #=cut
493             #
494             #sub get_schedule_json {
495             # my ( $sid ) = @_;
496             # die "Problem with arguments in get_schedule_json" if not defined $sid;
497             #
498             # my $json;
499             # try {
500             # $conn->do( fixup => sub {
501             # ( $json ) = $_->selectrow_array( $site->SQL_SCHEDULES_SELECT_SCHEDULE,
502             # undef,
503             # $sid );
504             # } );
505             # }
506             #
507             # if ( $json ) {
508             # $log->debug( __PACKAGE__ . "::get_schedule_json got schedule from database: $json" );
509             # return decode_schedule_json( $json );
510             # }
511             # return;
512             #}
513              
514              
515              
516             =head1 AUTHOR
517              
518             Nathan Cutler, C<< <presnypreklad@gmail.com> >>
519              
520             =cut
521              
522             1;
523