App-Dochazka-REST

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

- t/model/: add '+01' to some timestamp literals because they are now timestamptz

0.305  2014-11-24 18:11 CET
- dbinit_Config.pm: notice that we already have a 'valid_intvl' trigger; revamp
  'intervals' and 'locks' triggers to use it
- t/dispatch/interval_lock.t: add a group of tests to keep track of which
  pathological tsranges ('intervals' in Dochazka terms) we are successfully
  checking for

0.306  2014-11-26 09:31 CET
- dbinit_Config.pm: add 'intvl_not_locked' trigger before update or insert on
  'intervals' table - checks to make sure the interval would not overlap with
  any existing lock
- Model/Shared.pm: fix bug where 'load' routine was needlessly adding 
  'count => 1' to the payload, which is supposed to be just an object
- t/: some tests started to fail because we were creating intervals and locks
  with the same intervals - fixed by changing the lock interval
- t/dispatch/interval_lock.t: now that we have a functioning trigger, add tests
  that attempt to insert intervals that conflict with a lock
- t/model/triggers: add new subdirectory for trigger tests
- t/model/triggers/immutable_id.t: new unit to test triggers that make ID

Changes  view on Meta::CPAN

  from introducing intervals in time periods when the employee has no or
  ambiguous scheduling
- t/dispatch/interval_lock.t: adapt to the above trigger (ensure testing user
  has a schedule and the schedule is in effect)
- t/: disable a bunch of tests broken by introduction of this trigger

0.311  2014-11-27 23:04 CET
- dbinit_Config.pm: expand no_lock_conflict to handle DELETE as well as INSERT/UPDATE
- t/model/{interval,lock}.t: adapt to current state
- t/model/lock.t: add test confirming that it is no longer possible to delete a
  locked interval

0.312  2014-11-28 11:27 CET
- t/dispatch/interval_lock.t: add some tests simulating: (1) create interval,
  (2) lock it, (3) attempt to update it - interval is locked, (4) attempt to
  delete it - interval is locked
- dbinit_Config.pm: add another trigger and run into problems because I didn't realize PostgreSQL
  executes triggers in alphabetical order

0.313  2014-11-28 12:20 CET
- dbinit_Config.pm: rename triggers so "alphabetical order" is aligned with
  "order of intended execution"
- Dispatch/{Interval,Lock,Shared}.pm: since all sanity checks will be
  implemented via triggers, the sanity stuff we put here before is unnecessary,
  so delete it
- t/dispatch/interval_lock.t: we are no longer returning 400 when intervals

config/sql/dbinit_Config.pm  view on Meta::CPAN

              this_eid := NEW.eid;
              this_intvl := NEW.intvl;
          ELSE
              -- TG_OP = 'DELETE'
              this_eid := OLD.eid;
              this_intvl := OLD.intvl;
          END IF;

          SELECT count(*) INTO lock_count FROM locks WHERE eid=this_eid AND intvl && this_intvl;
          IF lock_count > 0 THEN
              RAISE EXCEPTION 'interval is locked';
          END IF;

          IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
              RETURN NEW;
          ELSE
              RETURN OLD;
          END IF;

      END;
      $IMM$ LANGUAGE plpgsql/,
          
    q/CREATE TRIGGER intvl_not_locked BEFORE INSERT OR UPDATE OR DELETE ON intervals
      FOR EACH ROW EXECUTE PROCEDURE no_lock_conflict()/,

    # the 'tempintvls' table and associated plumbing

    q/CREATE SEQUENCE temp_intvl_seq/,

    q/COMMENT ON SEQUENCE temp_intvl_seq IS 'sequence guaranteeing that each set of temporary intervals will have a unique identifier'/,

    q/-- tempintvls
      -- for staging fillup intervals 

lib/App/Dochazka/REST/Docs/Resources.pm  view on Meta::CPAN

    }

=item * DELETE

Deletes the interval object whose iid is specified by the ':iid' URI parameter.
As long as the interval does not overlap with a lock interval, the delete operation
will probably work as expected.

=back

ACL note: 'active' employees can update/delete only their own unlocked intervals.


=back

=head2 C<< interval/new >>


=over

Allowed methods: POST

lib/App/Dochazka/REST/Docs/Resources.pm  view on Meta::CPAN

=back

=head2 C<< lock/new >>


=over

Allowed methods: POST

This is the resource by which the attendance data entered by an employee 
for a given time period can be "locked" to prevent any subsequent
modifications.  It takes a request body containing, at the very least, an
C<intvl> property specifying the tsrange to lock. Additionally, administrators
can specify C<remark> and C<eid> properties.


=back

=head2 C<< lock/nick/:nick/:tsrange >>


lib/App/Dochazka/REST/Docs/Workflow.pm  view on Meta::CPAN

=over

=item (a) only on their own attendance,

=item (b) only on past attendance and up to the end of the current month,

=item (c) lock interval must not overlap with any other lock intervals.

=back

=head3 Modify one's own unlocked past attendance data

Active employees can modify/delete any existing attendance data, provided the
attendance intervals in question do not conflict with any lock.

=head3 Retrieve list of non-disabled activities

Since attendance data must be associated with a valid activity, active employees
are authorized to retrieve the entire list of non-disabled activities using
a GET request on the C<activity/all> resource.

lib/App/Dochazka/REST/Guide.pm  view on Meta::CPAN

=item * Privhistory (history of changes in an employee's privilege level)

=item * Schedule (a schedule)

=item * Schedhistory (history of changes in an employee's schedule)

=item * Activities (what kinds of work are recognized)

=item * Intervals (the "work", or "attendance", itself)

=item * Locks (determining whether a reporting period is locked or not)

=item * Components (Mason components, i.e. report templates)

=back

The "state" of each object is stored in a PostgreSQL database (see
L<"DATABASE"> for details).

These classes are described in the following sections.

lib/App/Dochazka/REST/Guide.pm  view on Meta::CPAN

Dochazka has four privilege levels: C<admin>, C<active>, C<inactive>, and
C<passerby>: 

=over

=item * C<admin> -- employee can view, modify, and place/remove locks on her
own attendance data as well as that of other employees; she can also
administer employee accounts and set privilege levels of other employees

=item * C<active> -- employee can view her own profile, attendance data,
modify her own unlocked attendance data, and place locks on her attendance
data

=item * C<inactive> -- employee can view her own profile and attendance data

=item * C<passerby> -- employee can view her own profile

=back

Dochazka's C<privhistory> object is used to track changes in an employee's
privilege level over time. Each time an employee's privilege level changes, 

lib/App/Dochazka/REST/Guide.pm  view on Meta::CPAN

of what she did during the interval) and a C<remark> (admin remark).

For details, see L<App::Dochazka::REST::Model::Interval>.


=head2 Lock

In Dochazka, a "lock" is a record in the "locks" table specifying that
a particular user's attendance data (i.e. activity intervals) for a 
given period (tsrange) cannot be changed. That means, for intervals in 
the locked tsrange:

=over

=item * existing intervals cannot be updated or deleted

=item * no new intervals can be inserted

=back

Employees can create locks (i.e., insert records into the locks table) on their
own EID, but they cannot delete or update those locks (or any others).
Administrators can insert, update, or delete locks at will.

How the lock is used will differ from site to site, and some sites may not
even use locking at all. The typical use case would be to lock all the
employee's attendance data within the given period as part of pre-payroll
processing. For example, the Dochazka client application may be set up to
enable reports to be generated only on fully locked periods. 

"Fully locked" means either that a single lock record has been inserted
covering the entire period, or that the entire period is covered by multiple
locks.

Any attempts (even by administrators) to enter activity intervals that 
intersect an existing lock will result in an error.

Clients can of course make it easy for the employee to lock entire blocks
of time (weeks, months, years . . .) at once, if that is deemed expedient.

For details, see L<App::Dochazka::REST::Model::Lock>.

lib/App/Dochazka/REST/Model/Lock.pm  view on Meta::CPAN


=head2 Locks in the database

    CREATE TABLE locks (
        lid     serial PRIMARY KEY,
        eid     integer REFERENCES Employees (EID),
        intvl   tsrange NOT NULL,
        remark  text
    )

There is also a stored procedure, C<fully_locked>, that takes an EID
and a tsrange, and returns a boolean value indicating whether or not
that period is fully locked for the given employee.


=head3 Locks in the Perl API

# FIXME: MISSING VERBIAGE




=head1 EXPORTS

lib/App/Dochazka/REST/ResourceDefs.pm  view on Meta::CPAN

    }

=item * DELETE

Deletes the interval object whose iid is specified by the ':iid' URI parameter.
As long as the interval does not overlap with a lock interval, the delete operation
will probably work as expected.

=back

ACL note: 'active' employees can update/delete only their own unlocked intervals.
EOH
    },

    # /interval/new
    'interval/new' => 
    {
        parent => 'interval',
        handler => {
            POST => 'handler_interval_new',
        },

lib/App/Dochazka/REST/ResourceDefs.pm  view on Meta::CPAN

        handler => {
            POST => 'handler_lock_new',
        },
        acl_profile => 'active', 
        cli => 'lock new $JSON',
        description => 'Add a new attendance data lock',
        documentation => <<'EOH',
=pod

This is the resource by which the attendance data entered by an employee 
for a given time period can be "locked" to prevent any subsequent
modifications.  It takes a request body containing, at the very least, an
C<intvl> property specifying the tsrange to lock. Additionally, administrators
can specify C<remark> and C<eid> properties.
EOH
    },

    # /lock/nick/:nick/:tsrange
    'lock/nick/:nick/:tsrange' => 
    {
        parent => 'lock',

t/dispatch/interval_lock.t  view on Meta::CPAN

        diag( Dumper $status );
        BAIL_OUT(0);
    }
    is( $status->level, 'OK' );
    is( $status->code, 'DOCHAZKA_CUD_OK' );
    ok( $status->{'payload'} );
    ok( $status->{'payload'}->{'lid'} );
    my $lid = $status->payload->{'lid'};
    note( "$user successfully created a lock" );

    note( "let $user try to add an intervals that overlap the locked period in various ways" );
    foreach my $intvl ( 
        '[1957-01-02 08:00, 1957-01-02 12:00)', # completely within the lock interval
        '[1957-01-03 23:00, 1957-01-04 01:00)', # extends past end of lock interval
        '[1957-01-02 08:00, today)',            # -- " -- but with 'today'
        '[1956-12-31 08:00, 1957-01-02 00:05)', # starts before beginning of lock interval
    ) {
        dbi_err( $test, 500, $user, 'POST', 'interval/new', 
            '{ "aid" : ' . $aid_of_work . ', "intvl" : "' . $intvl . '" }',
            qr/interval is locked/i 
        );
    }

    note( "'active' can't delete locks so we have to delete them as root" );
    $status = req( $test, 200, 'root', 'DELETE', "/lock/lid/$lid" );
    is( $status->level, 'OK' );
    is( $status->code, 'DOCHAZKA_CUD_OK' );
}

note( 'as long as all required properties are present, JSON with bogus properties' );

t/dispatch/interval_lock.t  view on Meta::CPAN

note( '- lock it' );
$status = req( $test, 201, 'root', 'POST', 'lock/new', <<"EOH" );
{ "intvl" : "[1957-01-01 00:00, 1957-02-01 00:00)" }
EOH
is( $status->level, 'OK' );
is( $status->code, 'DOCHAZKA_CUD_OK' );
my $tl = $status->payload->{'lid'};

note( '- try to update it' );
dbi_err( $test, 500, 'root', 'PUT', "interval/iid/$ti", 
    '{ "long_desc" : "I\'m changing this interval even though it\'s locked!" }',
    qr/interval is locked/ );

note( '- try to delete it' );
dbi_err( $test, 500, 'root', 'DELETE', "interval/iid/$ti", undef,
    qr/interval is locked/ );

note( '- remove the lock' );
$status = req( $test, 200, 'root', 'DELETE', "lock/lid/$tl" );
is( $status->level, 'OK' );
is( $status->code, 'DOCHAZKA_CUD_OK' );

note( '- now we can delete it' );
$status = req( $test, 200, 'root', 'DELETE', "interval/iid/$ti" );
is( $status->level, 'OK' );
is( $status->code, 'DOCHAZKA_CUD_OK' );

t/dispatch/interval_lock.t  view on Meta::CPAN

dbi_err( $test, 500, 'active', 'POST', 'interval/new', 
    '{ "aid" : ' . $aid_of_work . ', "eid" : ' . $eid_active . 
        ', "intvl" : "[2014-07-31 20:00, 2014-08-01 00:00]" }', $illegal );

note( '- upper bound not evenly divisible by 5 minutes' );
dbi_err( $test, 500, 'active', 'POST', 'interval/new',
    '{ "aid" : '. $aid_of_work . ', "eid" : ' . $eid_active .
        ', "intvl" : "[2014-07-31 20:00, 2014-08-01 00:01)" }',
    qr/upper and lower bounds of interval must be evenly divisible by 5 minutes/ );

note( '- interval is locked' );
dbi_err( $test, 500, 'active', 'POST', 'interval/new',
    '{ "aid" : '. $aid_of_work . ', "eid" : ' . $eid_active .
        ', "intvl" : "[2014-07-31 20:00, 2014-08-01 00:05)" }',
    qr/interval is locked/ );

note( 'now let\'s try to attack upper bound of lock' );
note( '- this one looks like it might conflict with the lock\'s upper bound');
note( '  (2014-09-01), but since the upper bound is non-inclusive, the interval will');
note( '  be OK');

$status = req( $test, 201, 'active', 'POST', 'interval/new', <<"EOH" );
{ "aid" : $aid_of_work, "eid" : $eid_active, "intvl" : "[2014-09-01 00:00, 2014-09-01 04:00)" }
EOH
is( $status->level, 'OK' );

t/model/interval_lock.t  view on Meta::CPAN

note( 'insert the lock object' );
is( noof( $dbix_conn, 'locks' ), 0 );
$status = $lock->insert( $faux_context );
is( noof( $dbix_conn, 'locks' ), 1 );
push @locks_to_delete, $lock;

note( 'attept to delete the testing interval' );
$status = $int->delete( $faux_context );
is( $status->level, 'ERR' );
is( $status->code, 'DOCHAZKA_DBI_ERR' );
like( $status->text, qr/interval is locked/ );

note( 'now test history_policy triggers:' );
note( '1. the interval runs from 08:00 - 12:00 today' );
note( '2. so attempt to insert a privhistory record effective 10:00 today' );
note( '   -- i.e., a clear policy violation' );
my $vio_ph = App::Dochazka::REST::Model::Privhistory->spawn(
    eid => $emp->eid,
    priv => 'passerby',
    effective => "$today 10:00"
);



( run in 0.600 second using v1.01-cache-2.11-cpan-49f99fa48dc )