File Coverage

blib/lib/App/Dochazka/REST/Model/Employee.pm
Criterion Covered Total %
statement 48 219 21.9
branch 0 70 0.0
condition 0 4 0.0
subroutine 16 37 43.2
pod 19 19 100.0
total 83 349 23.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::Employee;
34              
35 41     41   5255 use 5.012;
  41         207  
36 41     41   274 use strict;
  41         111  
  41         914  
37 41     41   229 use warnings;
  41         100  
  41         1294  
38 41     41   262 use App::CELL qw( $CELL $log $meta $site );
  41         85  
  41         4344  
39 41     41   3786 use App::Dochazka::Common qw( $today init_timepiece );
  41         58254  
  41         3685  
40 41     41   17597 use App::Dochazka::REST::LDAP qw( ldap_search );
  41         176  
  41         2758  
41 41         3627 use App::Dochazka::REST::Model::Shared qw(
42             cud
43             load
44             load_multiple
45             noof
46             priv_by_eid
47             schedule_by_eid
48             select_single
49             select_set_of_single_scalar_rows
50 41     41   12273 );
  41         374  
51 41     41   286 use Carp;
  41         85  
  41         2032  
52 41     41   256 use Data::Dumper;
  41         85  
  41         1751  
53 41     41   3342 use DBI qw(:sql_types);
  41         35006  
  41         14378  
54 41     41   328 use Params::Validate qw( :all );
  41         100  
  41         5776  
55 41     41   314 use Try::Tiny;
  41         133  
  41         4237  
56              
57             # send DBI warnings to the log
58             $SIG{__WARN__} = sub {
59             $log->notice( $_[0] );
60             };
61              
62             # we get 'spawn', 'reset', and accessors from parent
63 41     41   309 use parent 'App::Dochazka::Common::Model::Employee';
  41         92  
  41         405  
64              
65              
66              
67             =head1 NAME
68              
69             App::Dochazka::REST::Model::Employee - Employee data model
70              
71              
72              
73              
74             =head1 SYNOPSIS
75              
76             Employee data model
77              
78             use App::Dochazka::REST::Model::Employee;
79              
80             ...
81              
82              
83              
84             =head1 DESCRIPTION
85              
86             A description of the employee data model follows.
87              
88              
89             =head2 Employees in the database
90              
91             At the database level, C<App::Dochazka::REST> needs to be able to distinguish
92             one employee from another. This is accomplished by the EID. All the other
93             fields in the C<employees> table are optional.
94              
95             The C<employees> database table is defined as follows:
96              
97             CREATE TABLE IF NOT EXISTS employees (
98             eid serial PRIMARY KEY,
99             nick varchar(32) UNIQUE NOT NULL,
100             sec_id varchar(64) UNIQUE,
101             fullname varchar(96) UNIQUE,
102             email text UNIQUE,
103             passhash text,
104             salt text,
105             supervisor integer REFERENCES employees (eid),
106             remark text,
107             CONSTRAINT kosher_nick CHECK (nick ~* '^[[:alnum:]_][[:alnum:]_-]+$')
108             )
109              
110             =head3 EID
111              
112             The Employee ID (EID) is Dochazka's principal means of identifying an
113             employee. At the site, employees will be known by other means, like their
114             full name, their username, their user ID, etc. But these can and will
115             change from time to time. The EID should never, ever change.
116              
117              
118             =head3 nick
119              
120             The idea behind the C<nick> field is that each employee can have an
121             easy-to-remember nickname - ideally something that appeals to them, personally.
122             The C<nick> is required and can only contain certain characters (alphanumerics,
123             underscore, hyphen).
124              
125              
126             =head3 sec_id
127              
128             The secondary ID is an optional unique string identifying the employee.
129             This could be useful at sites where employees already have a nick (username)
130             and a numeric ID, for example. This gives administrators and supervisors the
131             ability to look up employees by their numeric ID as well as their username
132             (nick).
133              
134              
135             =head3 fullname, email
136              
137             These fields are optional. If they have a value, it must be unique. value.
138             Dochazka does not check if the email address is valid.
139              
140             Depending on how C<App::Dochazka::REST> is configured (see especially the
141             C<DOCHAZKA_PROFILE_EDITABLE_FIELDS> site parameter), these fields may be
142             read-only for employees (changeable by admins only), or the employee may be
143             allowed to maintain their own information.
144              
145              
146             =head3 passhash, salt
147              
148             The optional passhash and salt fields are designed to hold a hashed password
149             and random salt. See L<App::Dochazka::REST::Guide/AUTHENTICATION AND SESSION
150             MANAGEMENT> for details.
151              
152              
153             =head3 supervisor
154              
155             If the employee has a supervisor who will use Dochazka to monitor the
156             employee's attendance, and provided that supervisor has an EID, this field can
157             be used to set up the relationship.
158              
159              
160             =head3 remark
161              
162             This field can be used by administrators for any purpose. Ordinarily, the
163             employee herself is not permitted to edit or even display it.
164              
165              
166              
167             =head2 Employees in the Perl API
168              
169             Individual employees are represented by "employee objects". All methods and
170             functions for manipulating these objects are contained in
171             L<App::Dochazka::REST::Model::Employee>. The most important methods are:
172              
173             =over
174              
175             =item * constructor (L<spawn>)
176              
177             =item * basic accessors (L<eid>, L<sec_id>, L<nick>, L<fullname>, L<email>,
178             L<passhash>, L<salt>, L<remark>)
179              
180             =item * L<priv> (privilege "accessor" - but privilege info is not stored in
181             the object)
182              
183             =item * L<schedule> (schedule "accessor" - but schedule info is not stored
184             in the object)
185              
186             =item * L<reset> (recycles an existing object by setting it to desired state)
187              
188             =item * L<insert> (inserts object into database)
189              
190             =item * L<update> (updates database to match the object)
191              
192             =item * L<delete> (deletes record from database if nothing references it)
193              
194             =item * L<load_by_eid> (loads a single employee into the object)
195              
196             =item * L<load_by_nick> (loads a single employee into the object)
197              
198             =item * L<team_nicks> (returns list of nicks of employees whose supervisor is this employee)
199              
200             =back
201              
202             L<App::Dochazka::REST::Model::Employee> also exports some convenience
203             functions:
204              
205             =over
206              
207             =item * L<nick_exists> (given a nick, return true/false)
208              
209             =item * L<eid_exists> (given an EID, return true/false)
210              
211             =item * L<list_employees_by_priv> (given a priv level, return hash of employees with that priv level)
212              
213             =item * L<noof_employees_by_priv> (given a priv level, return number of employees with that priv level)
214              
215             =back
216              
217             For basic C<employee> object workflow, see the unit tests in
218             C<t/model/employee.t>.
219              
220              
221              
222             =head1 EXPORTS
223              
224             This module provides the following exports:
225              
226             =over
227              
228             =item L<autocreate_employee> - function
229              
230             =item L<eid_exists> - function
231              
232             =item L<get_all_sync_employees> - function
233              
234             =item L<list_employees_by_priv> - function
235              
236             =item L<nick_exists> - function
237              
238             =item L<noof_employees_by_priv> - function
239              
240             =back
241              
242             =cut
243              
244 41     41   383125 use Exporter qw( import );
  41         115  
  41         113781  
245             our @EXPORT_OK = qw(
246             autocreate_employee
247             eid_exists
248             get_all_sync_employees
249             list_employees_by_priv
250             nick_exists
251             noof_employees_by_priv
252             );
253              
254              
255              
256             =head1 METHODS
257              
258             The following functions expect to be called as methods on an employee object.
259              
260             The standard way to create an object containing an existing employee is to use
261             'load_by_eid' or 'load_by_nick':
262              
263             my $status = App::Dochazka::REST::Model::Employee->load_by_nick( 'georg' );
264             return $status unless $status->ok;
265             my $georg = $status->payload;
266             $georg->remark( 'Likes to fly kites' );
267             $status = $georg->update;
268             return $status unless $status->ok;
269              
270             ... and the like. To insert a new employee, do something like this:
271              
272             my $friedrich = App::Dochazka::REST::Model::Employee->spawn( nick => 'friedrich' );
273             my $status = $friedrich->insert;
274             return $status unless $status->ok;
275              
276             =head2 priv
277              
278             Accessor method. Wrapper for App::Dochazka::REST::Model::Shared::priv_by_eid
279             N.B.: for this method to work, the 'eid' attribute must be populated
280              
281             =cut
282              
283             sub priv {
284 0     0 1   my $self = shift;
285 0           my ( $conn, $timestamp ) = validate_pos( @_,
286             { isa => 'DBIx::Connector' },
287             { type => SCALAR, optional => 1 },
288             );
289 0 0         my $return_value = ( $timestamp )
290             ? priv_by_eid( $conn, $self->eid, $timestamp )
291             : priv_by_eid( $conn, $self->eid );
292 0 0         return if ref( $return_value );
293 0           return $return_value;
294             }
295              
296              
297             =head2 schedule
298              
299             Accessor method. Wrapper for App::Dochazka::REST::Model::Shared::schedule_by_eid
300             N.B.: for this method to work, the 'eid' attribute must be populated
301              
302             =cut
303              
304             sub schedule {
305 0     0 1   my $self = shift;
306 0           my ( $conn, $timestamp ) = validate_pos( @_,
307             { isa => 'DBIx::Connector' },
308             { type => SCALAR, optional => 1 },
309             );
310 0 0         my $return_value = ( $timestamp )
311             ? schedule_by_eid( $conn, $self->eid, $timestamp )
312             : schedule_by_eid( $conn, $self->eid );
313 0 0         return if ref( $return_value );
314 0           return $return_value;
315             }
316              
317              
318             =head2 insert
319              
320             Instance method. Takes the object, as it is, and attempts to insert it into
321             the database. On success, overwrites object attributes with field values
322             actually inserted. Returns a status object.
323              
324             =cut
325              
326             sub insert {
327 0     0 1   my $self = shift;
328 0           my ( $context ) = validate_pos( @_, { type => HASHREF } );
329              
330 0 0         $self->{sync} = 0 unless defined( $self->{sync} );
331              
332             my $status = cud(
333             conn => $context->{'dbix_conn'},
334 0           eid => $context->{'current'}->{'eid'},
335             object => $self,
336             sql => $site->SQL_EMPLOYEE_INSERT,
337             attrs => [ 'sec_id', 'nick', 'fullname', 'email', 'passhash', 'salt',
338             'sync', 'supervisor', 'remark' ],
339             );
340 0           return $status;
341             }
342              
343              
344             =head2 update
345              
346             Instance method. Assuming that the object has been prepared, i.e. the EID
347             corresponds to the employee to be updated and the attributes have been
348             changed as desired, this function runs the actual UPDATE, hopefully
349             bringing the database into line with the object. Overwrites all the
350             object's attributes with the values actually written to the database.
351             Returns status object.
352              
353             =cut
354              
355             sub update {
356 0     0 1   my $self = shift;
357 0           my ( $context ) = validate_pos( @_, { type => HASHREF } );
358              
359 0 0         return $CELL->status_err( 'DOCHAZKA_MALFORMED_400' ) unless $self->{'eid'};
360              
361 0 0         $self->{sync} = 0 unless defined( $self->{sync} );
362              
363             my $status = cud(
364             conn => $context->{'dbix_conn'},
365 0           eid => $context->{'current'}->{'eid'},
366             object => $self,
367             sql => $site->SQL_EMPLOYEE_UPDATE_BY_EID,
368             attrs => [ 'sec_id', 'nick', 'fullname', 'email', 'passhash', 'salt',
369             'sync', 'supervisor', 'remark', 'eid' ],
370             );
371 0           return $status;
372             }
373              
374              
375             =head2 delete
376              
377             Instance method. Assuming the EID really corresponds to the employee to be
378             deleted, this method will execute the DELETE statement in the database. It
379             won't succeed if there are any records anywhere in the database that point
380             to this EID. Returns a status object.
381              
382             =cut
383              
384             sub delete {
385 0     0 1   my $self = shift;
386 0           my ( $context ) = validate_pos( @_, { type => HASHREF } );
387              
388             my $status = cud(
389             conn => $context->{'dbix_conn'},
390 0           eid => $context->{'current'}->{'eid'},
391             object => $self,
392             sql => $site->SQL_EMPLOYEE_DELETE,
393             attrs => [ 'eid' ],
394             );
395             #$self->reset( eid => $self->eid ) if $status->ok;
396 0           return $status;
397             }
398              
399              
400             =head2 ldap_sync
401              
402             Sync the mapping fields to the values found in the LDAP database.
403              
404             =cut
405              
406             sub ldap_sync {
407 0     0 1   my $self = shift;
408 0           $log->debug( "Entering " . __PACKAGE__ . "::sync()" );
409 0 0         die "Employee nick property not populated!" unless $self->nick =~ /\S+/;
410 0           my $nick = $self->nick;
411              
412 0 0         return $CELL->status_err( 'DOCHAZKA_LDAP_NOT_ENABLED' ) unless $site->DOCHAZKA_LDAP;
413 0 0         return $CELL->status_err(
414             'DOCHAZKA_LDAP_SYNC_PROP_FALSE',
415             args => [ $nick ],
416             ) unless $self->sync;
417             return $CELL->status_err(
418             'DOCHAZKA_LDAP_SYSTEM_USER_NOSYNC',
419             args => [ $nick ],
420 0 0         ) if grep { $nick eq $_; } @{ $site->DOCHAZKA_SYSTEM_USERS };
  0            
  0            
421              
422 0           $log->debug( "About to populate $nick from LDAP" );
423              
424 0           require Net::LDAP;
425              
426             # initiate connection to LDAP server (anonymous bind)
427 0           my $server = $site->DOCHAZKA_LDAP_SERVER;
428 0           my $ldap = Net::LDAP->new( $server );
429 0 0         $log->error("$@") unless $ldap;
430 0 0         return $CELL->status_err( 'Could not connect to LDAP server' ) unless $ldap;
431              
432             # get LDAP properties and stuff them into the employee object
433 0           my $count = 0;
434 0           foreach my $key ( keys( %{ $site->DOCHAZKA_LDAP_MAPPING } ) ) {
  0            
435 0           my $prop = $site->DOCHAZKA_LDAP_MAPPING->{ $key };
436 0           my $value = ldap_search( $ldap, $nick, $prop );
437 0 0         last unless $value;
438 0           $log->debug( "Setting $key to $value" );
439 0           $self->set( $key, $value );
440 0           $count += 1;
441             }
442              
443 0           $ldap->unbind;
444              
445 0 0         return $CELL->status_ok(
446             'DOCHAZKA_LDAP_SYNC_SUCCESS',
447             args => [ $count ],
448             ) unless $count < 1;
449              
450 0           return $CELL->status_not_ok( 'DOCHAZKA_LDAP_SYNC_FAILURE' );
451             }
452              
453              
454             =head2 load_by_eid
455              
456             Analogous method to L<App::Dochazka::REST::Model::Activity/"load_by_aid">.
457              
458             =cut
459              
460             sub load_by_eid {
461 0     0 1   my $self = shift;
462 0           my ( $conn, $eid ) = validate_pos( @_,
463             { isa => 'DBIx::Connector' },
464             { type => SCALAR },
465             # { type => SCALAR, regex => qr/^-?\d+$/ }, <-- causes a regression
466             );
467 0           $log->debug( "Entering " . __PACKAGE__ . "::load_by_eid with argument $eid" );
468              
469 0           return load(
470             conn => $conn,
471             class => __PACKAGE__,
472             sql => $site->SQL_EMPLOYEE_SELECT_BY_EID,
473             keys => [ $eid ],
474             );
475             }
476              
477              
478             =head2 load_by_nick
479              
480             Analogous method to L<App::Dochazka::REST::Model::Activity/"load_by_aid">.
481              
482             =cut
483              
484             sub load_by_nick {
485 0     0 1   my $self = shift;
486 0           my ( $conn, $nick ) = validate_pos( @_,
487             { isa => 'DBIx::Connector' },
488             { type => SCALAR },
489             );
490 0           $log->debug( "Entering " . __PACKAGE__ . "::load_by_nick with argument $nick" );
491              
492 0           return load(
493             conn => $conn,
494             class => __PACKAGE__,
495             sql => $site->SQL_EMPLOYEE_SELECT_BY_NICK,
496             keys => [ $nick ],
497             );
498             }
499              
500              
501             =head2 load_by_sec_id
502              
503             Analogous method to L<App::Dochazka::REST::Model::Activity/"load_by_aid">.
504              
505             FIXME: add unit tests
506              
507             =cut
508              
509             sub load_by_sec_id {
510 0     0 1   my $self = shift;
511 0           my ( $conn, $sec_id ) = validate_pos( @_,
512             { isa => 'DBIx::Connector' },
513             { type => SCALAR },
514             );
515 0           $log->debug( "Entering " . __PACKAGE__ . "::load_by_sec_id with argument $sec_id" );
516              
517 0           return load(
518             conn => $conn,
519             class => __PACKAGE__,
520             sql => $site->SQL_EMPLOYEE_SELECT_BY_SEC_ID,
521             keys => [ $sec_id ],
522             );
523             }
524              
525              
526             =head2 priv_change_during_range
527              
528             Given a DBIx::Connector object and a tsrange, returns a non-negative integer
529             value signifying the number of times the employee's priv level changed during the
530             given range.
531              
532             =cut
533              
534             sub priv_change_during_range {
535 0     0 1   my $self = shift;
536 0           my ( $conn, $tsr ) = validate_pos( @_,
537             { isa => 'DBIx::Connector' },
538             { type => SCALAR },
539             );
540 0           $log->debug( "Entering " . __PACKAGE__ . "::priv_change_during_range with argument $tsr" );
541 0           $log->debug( "EID is " . $self->eid );
542              
543 0           my $status = select_single(
544             conn => $conn,
545             sql => $site->SQL_EMPLOYEE_PRIV_CHANGE_DURING_RANGE,
546             keys => [ $self->eid, $tsr ],
547             );
548 0           return _privsched_change_during_range_result( "SQL_EMPLOYEE_PRIV_CHANGE_DURING_RANGE", $status );
549             }
550              
551             sub _privsched_change_during_range_result {
552 0     0     my ( $sql_stmt, $status ) = @_;
553 0           $log->debug( "Entering " . __PACKAGE__ . "::_privsched_change_during_range_result with status " .
554             Dumper $status );
555             # there should always be a single record, and it should be either 0 or 1
556 0 0         if ( ref( $status->payload ) ne 'ARRAY' ) {
557 0           die "Unexpected _privsched_change_during_range_result: status payload is not an array!";
558             }
559 0           my ( $plval ) = validate_pos( @{ $status->payload },
560             {
561             type => SCALAR,
562             callbacks => {
563 0     0     'non-negative integer' => sub { $_[0] >= 0 }
564             }
565             },
566 0           );
567 0           return $plval;
568             }
569              
570              
571             =head2 privhistory_at_timestamp
572              
573             Given a DBIx::Connector object and a string that must be a timestamp (or a
574             tsrange), returns an L<App::Dochazka::REST::Model::Privhistory> object
575             containing the privhistory record applicable to the employee as of the
576             timestamp (or the lower bound of the tsrange). If there is no such record, the
577             object's properties will be undefined.
578              
579             NOTE: be careful that the argument really is a valid timestamp or tsrange. If
580             it isn't valid, the DBD::Pg error will be logged and the return value will be
581             undef (not a L<App::Dochazka::REST::Model::Schedhistory> object whose
582             properties are set to undef).
583              
584             =cut
585              
586             sub privhistory_at_timestamp {
587 0     0 1   my $self = shift;
588 0           my ( $conn, $arg ) = validate_pos( @_,
589             { isa => 'DBIx::Connector' },
590             { type => SCALAR },
591             );
592 0           $log->debug( "Entering " . __PACKAGE__ . "::privhistory_at_timestamp with argument $arg" );
593 0           $log->debug( "EID is " . $self->eid );
594              
595             # if it looks like a tsrange, use tsrange, otherwise use timestamp
596 0 0         my $sql = ( $arg =~ m/[[(].*,.*[])]/ )
597             ? $site->SQL_EMPLOYEE_PRIVHISTORY_AT_TSRANGE
598             : $site->SQL_EMPLOYEE_PRIVHISTORY_AT_TIMESTAMP;
599              
600 0           my $status = select_single(
601             conn => $conn,
602             sql => $sql,
603             keys => [ $self->eid, $arg ],
604             );
605 0 0         return undef unless $status->ok;
606              
607 0           $log->debug( 'privhistory_at_timestamp: database said: ' . Dumper( $status->payload ) );
608              
609 0           return App::Dochazka::REST::Model::Privhistory->spawn(
610             phid => $status->payload->[0],
611             eid => $status->payload->[1],
612             priv => $status->payload->[2],
613             effective => $status->payload->[3],
614             remark => $status->payload->[4],
615             );
616             }
617              
618              
619             =head2 schedule_change_during_range
620              
621             Given a DBIx::Connector object and a tsrange, returns a non-negative integer
622             value signifying the number of times the employee's schedule changed during the
623             given range.
624              
625             =cut
626              
627             sub schedule_change_during_range {
628 0     0 1   my $self = shift;
629 0           my ( $conn, $tsr ) = validate_pos( @_,
630             { isa => 'DBIx::Connector' },
631             { type => SCALAR },
632             );
633 0           $log->debug( "Entering " . __PACKAGE__ . "::schedule_change_during_range with argument $tsr" );
634 0           $log->debug( "EID is " . $self->eid );
635              
636 0           my $status = select_single(
637             conn => $conn,
638             sql => $site->SQL_EMPLOYEE_SCHEDULE_CHANGE_DURING_RANGE,
639             keys => [ $self->eid, $tsr ],
640             );
641 0           return _privsched_change_during_range_result( "SQL_EMPLOYEE_SCHEDULE_CHANGE_DURING_RANGE", $status );
642             }
643              
644              
645             =head2 schedhistory_at_timestamp
646              
647             Given a DBIx::Connector object and a string that must be a timestamp (or a
648             tsrange), returns an L<App::Dochazka::REST::Model::Schedhistory> object
649             containing the history record applicable to the employee as of the timestamp
650             (or the lower bound of the tsrange). If there is no such record, the object's
651             properties will be undefined.
652              
653             NOTE: be careful that the argument really is a valid timestamp or tsrange. If
654             it isn't valid, the DBD::Pg error will be logged and the return value will be
655             undef (not a L<App::Dochazka::REST::Model::Schedhistory> object whose
656             properties are set to undef).
657              
658             =cut
659              
660             sub schedhistory_at_timestamp {
661 0     0 1   my $self = shift;
662 0           my ( $conn, $arg ) = validate_pos( @_,
663             { isa => 'DBIx::Connector' },
664             { type => SCALAR },
665             );
666 0           $log->debug( "Entering " . __PACKAGE__ . "::schedhistory_at_timestamp with argument $arg" );
667 0           $log->debug( "EID is " . $self->eid );
668              
669             # if it looks like a tsrange, use tsrange, otherwise use timestamp
670 0 0         my $sql = ( $arg =~ m/[[(].*,.*[])]/ )
671             ? $site->SQL_EMPLOYEE_SCHEDHISTORY_AT_TSRANGE
672             : $site->SQL_EMPLOYEE_SCHEDHISTORY_AT_TIMESTAMP;
673              
674 0           my $status = select_single(
675             conn => $conn,
676             sql => $sql,
677             keys => [ $self->eid, $arg ],
678             );
679 0 0         return undef unless $status->ok;
680              
681 0           $log->debug( 'schedhistory_at_timestamp: database said: ' . Dumper( $status->payload ) );
682              
683 0           return App::Dochazka::REST::Model::Schedhistory->spawn(
684             shid => $status->payload->[0],
685             eid => $status->payload->[1],
686             sid => $status->payload->[2],
687             effective => $status->payload->[3],
688             remark => $status->payload->[4],
689             );
690             }
691              
692              
693             =head2 team_nicks
694              
695             Given a L<DBIx::Connector> object, return a status object that, if successful,
696             will contain in the payload a list of employees whose supervisor is the
697             employee corresponding to C<$self>.
698              
699             =cut
700              
701             sub team_nicks {
702 0     0 1   my $self = shift;
703 0           my ( $conn ) = validate_pos( @_,
704             { isa => 'DBIx::Connector' },
705             );
706 0   0       $log->debug( "Entering " . __PACKAGE__ . "::team_nicks for employee " . ( $self->nick || 'undefined' ) );
707              
708             # no EID, no team
709 0 0         return $CELL->status_ok( 'TEAM', payload => [] ) unless $self->eid;
710              
711             # if nick not populated, get it
712 0 0         $self->load_by_eid( $conn, $self->eid ) unless $self->nick =~ /\S+/;
713              
714 0           my $status = select_set_of_single_scalar_rows(
715             'conn' => $conn,
716             'sql' => $site->SQL_EMPLOYEE_SELECT_TEAM,
717             'keys' => [ $self->eid ],
718             );
719 0 0         return $status unless $status->ok;
720 0           return $CELL->status_ok(
721             'DISPATCH_LIST_EMPLOYEE_NICKS_TEAM',
722             args => [ $self->nick ],
723             payload => $status->payload,
724             );
725             }
726              
727              
728             =head2 has_reports
729              
730             Given a L<DBIx::Connector> object, return a status object that, if successful,
731             will contain in the payload an integer indicating how many "reports" the
732             employee has - i.e. how many employees, if any, there are whose supervisor is
733             the employee corresponding to C<$self>.
734              
735             =cut
736              
737             sub has_reports {
738 0     0 1   my $self = shift;
739 0           my ( $conn ) = validate_pos( @_,
740             { isa => 'DBIx::Connector' },
741             );
742 0   0       $log->debug( "Entering " . __PACKAGE__ . "::has_reports for employee " . ( $self->nick || 'undefined' ) );
743 0           my $reports;
744              
745             # no EID, no team
746 0 0         return $CELL->status_ok( 'TEAM', payload => [] ) unless $self->eid;
747              
748 0           my $status = select_single(
749             'conn' => $conn,
750             'sql' => $site->SQL_EMPLOYEE_HAS_REPORTS,
751             'keys' => [ $self->eid ],
752             );
753 0 0         return $status unless $status->ok;
754 0           ( $reports ) = @{ $status->payload };
  0            
755 0           return $CELL->status_ok(
756             'DISPATCH_EMPLOYEE_HAS_REPORTS_EID',
757             args => [ $self->eid ],
758             payload => $reports,
759             );
760             }
761              
762              
763             =head1 FUNCTIONS
764              
765             The following functions are not object methods.
766              
767              
768              
769             =head1 EXPORTED FUNCTIONS
770              
771             The following functions are exported and are not called as methods.
772              
773              
774             =head2 autocreate_employee
775              
776             Takes a DBIx::Connector object and a nick - the nick is assumed not to exist in
777             the Dochazka employees table. If DOCHAZKA_LDAP_AUTOCREATE is true, attempts to
778             create the employee. Returns a status object.
779              
780             =cut
781              
782             sub autocreate_employee {
783 0     0 1   my ( $dbix_conn, $nick ) = @_;
784 0           $log->debug( "Entering " . __PACKAGE__ . "::autocreate_employee()" );
785 0           my $status;
786              
787 0 0         return $CELL->status_ok() if nick_exists( $dbix_conn, $nick );
788 0 0         return $CELL->status_not_ok( 'DOCHAZKA_NO_AUTOCREATE' ) unless $site->DOCHAZKA_LDAP_AUTOCREATE;
789              
790 0           my $emp = App::Dochazka::REST::Model::Employee->spawn(
791             nick => $nick,
792             sync => 1,
793             remark => 'LDAP autocreate',
794             );
795 0           $status = $emp->ldap_sync();
796 0 0         return $status unless $status->ok;
797              
798 0           my $faux_context = { 'dbix_conn' => $dbix_conn, 'current' => { 'eid' => 1 } };
799 0           $status = $emp->insert( $faux_context );
800 0 0         if ( $status->not_ok ) {
801 0           my $reason = $status->text;
802 0           return $CELL->status_err(
803             'DOCHAZKA_EMPLOYEE_CREATE_FAIL',
804             args => [ $nick, $reason ],
805             );
806             }
807 0           $log->notice( "Auto-created employee $nick, who was authenticated via LDAP" );
808              
809 0           my $priv = $site->DOCHAZKA_LDAP_AUTOCREATE_AS;
810 0 0         if ( $priv !~ m/^(inactive)|(active)$/ ) {
811 0           return $CELL->status_err(
812             'DOCHAZKA_INVALID_PARAM',
813             args => [ 'DOCHAZKA_LDAP_AUTOCREATE_AS', $priv ],
814             );
815             }
816              
817             # create a privhistory record (inactive/active only)
818 0           init_timepiece();
819 0           my $ph_obj = App::Dochazka::REST::Model::Privhistory->spawn(
820             eid => $emp->eid,
821             priv => $priv,
822             effective => ( $today . ' 00:00' ),
823             remark => 'LDAP autocreate',
824             );
825 0           $status = $ph_obj->insert( $faux_context );
826 0 0         if ( $status->not_ok ) {
827 0           my $reason = $status->text;
828 0           $status = $CELL->status_err(
829             'DOCHAZKA_AUTOCREATE_PRIV_PROBLEM',
830             args => [ $nick, $reason ],
831             );
832             }
833              
834 0           return $status;
835             }
836              
837              
838             =head2 nick_exists
839              
840             See C<exists> routine in L<App::Dochazka::REST::Model::Shared>
841              
842              
843             =head2 eid_exists
844              
845             See C<exists> routine in L<App::Dochazka::REST::Model::Shared>
846              
847             =cut
848              
849             BEGIN {
850 41     41   394 no strict 'refs';
  41         138  
  41         2533  
851 41     41   283 *{"eid_exists"} = App::Dochazka::REST::Model::Shared::make_test_exists( 'eid' );
  41         252  
852 41         198 *{"nick_exists"} = App::Dochazka::REST::Model::Shared::make_test_exists( 'nick' );
  41         22606  
853             }
854              
855              
856             =head2 list_employees_by_priv
857              
858             Get employee nicks. Argument can be one of the following:
859              
860             all admin active inactive passerby
861              
862             =cut
863              
864             sub list_employees_by_priv {
865 0     0 1   my ( $conn, $priv ) = validate_pos( @_,
866             { isa => 'DBIx::Connector' },
867             { type => SCALAR, regex => qr/^(all)|(admin)|(active)|(inactive)|(passerby)$/ },
868             );
869 0           $log->debug( "Entering " . __PACKAGE__ . "::list_employees_by_priv with priv $priv" );
870              
871 0           my $nicks = []; # reference to array of nicks
872 0           my $sql = ''; # SQL statement
873 0           my $keys_arrayref = []; # reference to array of keys (may be empty)
874 0 0         if ( $priv eq 'all' ) {
875 0           $sql = $site->SQL_EMPLOYEE_SELECT_NICKS_ALL
876             } else {
877 0           $sql = $site->SQL_EMPLOYEE_SELECT_NICKS_BY_PRIV_LEVEL;
878 0           $keys_arrayref = [ $priv ];
879             }
880 0           my $status = select_set_of_single_scalar_rows(
881             'conn' => $conn,
882             'sql' => $sql,
883             'keys' => $keys_arrayref,
884             );
885 0 0         return $status unless $status->ok;
886            
887 0           return $CELL->status_ok( 'DISPATCH_LIST_EMPLOYEE_NICKS',
888             args => [ $priv ],
889             payload => $status->payload,
890             );
891             }
892              
893              
894             =head2 noof_employees_by_priv
895              
896             Get number of employees. Argument can be one of the following:
897              
898             total admin active inactive passerby
899              
900             =cut
901              
902             sub noof_employees_by_priv {
903 0     0 1   my ( $conn, $priv ) = validate_pos( @_,
904             { isa => 'DBIx::Connector' },
905             { type => SCALAR, regex => qr/^(total)|(admin)|(active)|(inactive)|(passerby)$/ },
906             );
907 0           $log->debug( "Entering " . __PACKAGE__ . "::noof_employees_by_priv with priv $priv" );
908              
909 0           $priv = lc $priv;
910              
911 0 0         if ( $priv eq 'total' ) {
912 0           my $count = noof( $conn, 'employees' );
913 0           return $CELL->status_ok(
914             'DISPATCH_COUNT_EMPLOYEES',
915             args => [ $count, $priv ],
916             payload => { count => $count } );
917             }
918              
919 0 0         return $CELL->status_err( 'DOCHAZKA_NOT_FOUND_404' ) unless
920             $priv =~ m/^(passerby)|(inactive)|(active)|(admin)$/i;
921              
922 0           my $sql = $site->SQL_EMPLOYEE_COUNT_BY_PRIV_LEVEL;
923 0           my ( $count ) = @{ select_single( conn => $conn, sql => $sql, keys => [ $priv ] )->payload };
  0            
924 0           $log->debug( "select_single returned: $count" );
925 0           $count += 0;
926 0           $CELL->status_ok( 'DISPATCH_COUNT_EMPLOYEES', args => [ $count, $priv ],
927             payload => { 'priv' => $priv, 'count' => $count } );
928             }
929              
930              
931             =head2 get_all_sync_employees
932              
933             Function returns a status object. If the status is OK, the payload will contain
934             a reference to an array of employee objects whose sync property is true.
935              
936             =cut
937              
938             sub get_all_sync_employees {
939 0     0 1   my ( $conn ) = validate_pos( @_,
940             { isa => 'DBIx::Connector' },
941             );
942 0           return load_multiple(
943             conn => $conn,
944             class => 'App::Dochazka::REST::Model::Employee',
945             sql => $site->SQL_EMPLOYEE_SELECT_MULTIPLE_BY_SYNC,
946             keys => [ 1 ],
947             );
948             }
949              
950              
951             =head1 AUTHOR
952              
953             Nathan Cutler, C<< <presnypreklad@gmail.com> >>
954              
955             =cut
956              
957             1;
958