[Bps-public-commit] RT-Extension-SLA branch, performance-stats, created. 0.08-27-gfebab40

Alex Vandiver alexmv at bestpractical.com
Mon Jun 16 20:58:12 EDT 2014


The branch, performance-stats has been created
        at  febab40de51e0f2468943001d14d672f5240cbac (commit)

- Log -----------------------------------------------------------------
commit e91485d3c52fc21a321268e98a5215f3ab04cd1f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Apr 27 19:04:02 2009 +0000

    fist pass on a reporting

diff --git a/lib/RT/Extension/SLA/Report.pm b/lib/RT/Extension/SLA/Report.pm
new file mode 100644
index 0000000..afbae9e
--- /dev/null
+++ b/lib/RT/Extension/SLA/Report.pm
@@ -0,0 +1,98 @@
+use 5.8.0;
+use strict;
+use warnings;
+
+package RT::Extension::SLA::Report;
+
+sub new {}
+
+sub init {}
+
+sub State {
+    my $self = shift;
+    return $self->{State} ||= {};
+}
+
+{ my $cache;
+sub Handlers {
+    my $self = shift;
+
+    return $cache if $cache;
+    
+    $cache = {
+        Create => 'OnCreate',
+        Set    => {
+            Owner => 'OnOwnerChange',
+        },
+        Correpond => 'OnResponse',
+        CustomField => { map $_ => 'OnServiceLevelChange', $self->ServiceLevelCustomFields },
+    };
+
+    return $cache;
+}
+
+sub Drive {
+    my $self = shift;
+    my $txns = shift;
+
+    my $state = $self->State;
+    my $handler = $self->Handlers;
+
+    while ( my $txn = $txns->Next ) {
+        my ($type, $field) = ($txn->Type, $txn->Field);
+
+        my $h = $handler->{ $type };
+        unless ( $h ) {
+            $RT::Logger->debug( "No handler for $type transaction, skipping" );
+        } elsif ( ref $h ) {
+            unless ( $h = $h->{ $field } ) {
+                $RT::Logger->debug( "No handler for ($type, $field) transaction, skipping" );
+            }
+        }
+        next unless $h;
+
+        $self->$h( Transaction => $txn, State => $state );
+    }
+}
+
+sub InitialServiceLevel {
+    my $self = shift;
+    my $ticket = shift;
+
+    my $txns = $ticket->Transactions;
+    foreach my $cf ( $self->ServiceLevelCustomFields ) {
+        $txns->_OpenParen('ServiceLevelCustomFields');
+        $txns->Limit(
+            SUBCLAUSE       => 'ServiceLevelCustomFields',
+            ENTRYAGGREGATOR => 'OR',
+            FIELD           => 'Type',
+            VALUE           => 'CustomField',
+        );
+        $txns->Limit(
+            SUBCLAUSE       => 'ServiceLevelCustomFields',
+            ENTRYAGGREGATOR => 'AND',
+            FIELD           => 'Field',
+            VALUE           => $cf->id,
+        );
+        $txns->_CloseParen('ServiceLevelCustomFields');
+    }
+
+    return $self;
+}
+
+{ my @cache = ();
+sub ServiceLevelCustomFields {
+    my $self = shift;
+    return @cache if @cache;
+
+    my $cfs = RT::CustomFields->new( $RT::SystemUser );
+    $cfs->Limit( FIELD => 'Name', VALUE => 'SLA' );
+    $cfs->Limit( FIELD => 'LookupType', VALUE => RT::Ticket->CustomFieldLookupType );
+    # XXX: limit to applied custom fields only
+
+    push @cache, $_ while $_ = $cfs->Next;
+
+    return @cache;
+} }
+
+1;

commit e1ee74897098a91996aece6c071782edde373ca1
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Apr 29 20:54:43 2009 +0000

    another pass on reporting

diff --git a/lib/RT/Extension/SLA/Report.pm b/lib/RT/Extension/SLA/Report.pm
index afbae9e..52a254e 100644
--- a/lib/RT/Extension/SLA/Report.pm
+++ b/lib/RT/Extension/SLA/Report.pm
@@ -24,12 +24,14 @@ sub Handlers {
         Set    => {
             Owner => 'OnOwnerChange',
         },
-        Correpond => 'OnResponse',
+        Correspond => 'OnResponse',
         CustomField => { map $_ => 'OnServiceLevelChange', $self->ServiceLevelCustomFields },
+        AddWatcher => { Requestor => 'OnRequestorChange' },
+        DelWatcher => { Requestor => 'OnRequestorChange' },
     };
 
     return $cache;
-}
+} }
 
 sub Drive {
     my $self = shift;
@@ -55,29 +57,157 @@ sub Drive {
     }
 }
 
+sub OnCreate {
+    my $self = shift;
+    my %args = ( Ticket => undef, Transaction => undef, State => undef, @_);
+
+    my $level = $self->InitialServiceLevel( $args{'Ticket'} );
+
+    my $state = $args{'State'};
+    %$state = ();
+    $state->{'level'} = $level;
+    $state->{'transaction'} = $args{'Transaction'};
+    $state->{'requestors'} = [ $self->InitialRequestors( $args{'Ticket'} ) ];
+    $state->{'owner'} = $self->InitialOwner( $args{'Ticket'} );
+    return;
+}
+
+sub OnRequestorChange {
+    my $self = shift;
+    my %args = ( Ticket => undef, Transaction => undef, State => undef, @_);
+
+    my $requestors = $self->State->{'requestors'};
+    if ( $args{'Transaction'}->Type eq 'AddWatcher' ) {
+        push @$requestors, $args{'Transaction'}->NewValue;
+    }
+    else {
+        my $id = $args{'Transaction'}->OldValue;
+        @$requestors = grep $_ != $id, @$requestors;
+    }
+}
+
+sub OnResponse {
+    my $self = shift;
+    my $self
+}
+
+sub IsRequestorsAct {
+    my $self = shift;
+    my $txn = shift;
+
+    my $actor = $txn->Creator;
+
+    # owner is always treated as non-requestor
+    return 0 if $actor == $self->State->{'owner'};
+    return 1 if grep $_ == $actor, @{ $self->State->{'requestors'} };
+
+    # in case requestor is a group
+    foreach my $id ( @{ $self->State->{'requestors'} } ){
+        my $cgm = RT::CachedGroupMember->new( $RT::SystemUser );
+        $cgm->LoadByCols( GroupId => $id, MemberId => $actor, Disabled => 0 );
+        return 1 if $cgm->id;
+    }
+    return 1;
+}
+
 sub InitialServiceLevel {
     my $self = shift;
     my $ticket = shift;
 
+    return $self->InitialValue(
+        Ticket   => $ticket,
+        Current  => $ticket->FirstCustomFieldValue('SLA'),
+        Criteria => { CustomField => [ map $_->id, $self->ServiceLevelCustomFields ] },
+    );
+}
+
+sub InitialRequestors {
+    my $self = shift;
+    my $ticket = shift;
+
+    my @current = map $_->Member, @{ $ticket->Requestors->MembersObj->ItemsArrayRef };
+
+    my $txns = $self->Transactions(
+        Ticket => $ticket,
+        Order => 'DESC',
+        Criteria => { 'AddWatcher' => 'Requestor', DelWatcher => 'Requestor' },
+    );
+    while ( my $txn = $txns->Next ) {
+        if ( $txn->Type eq 'AddWatcher' ) {
+            my $id = $txn->NewValue;
+            @current = grep $_ != $id, @current;
+        }
+        else {
+            push @current, $txn->OldValue;
+        }
+    }
+
+    return @current;
+}
+
+sub InitialOwner {
+    my $self = shift;
+    my $ticket = shift;
+
+    return $self->InitialValue(
+        %args,
+        Current => $ticket->Owner,
+        Criteria => { 'Set', 'Owner' },
+    );
+}
+
+sub InitialValue {
+    my $self = shift;
+    my %args = ( Ticket => undef, Current => undef, Criteria => {}, @_ );
+
+    my $txns = $self->Transactions( %args );
+    if ( my $first_change = $txns->First ) {
+        # intial value is old value of the first change
+        return $first_change->OldValue;
+    }
+
+    # no change -> initial value is the current
+    return $args{'Current'};
+}
+
+sub Transactions {
+    my $self = shift;
+    my %args = (Ticket => undef, Criteria => undef, Order => 'ASC', @_);
+
     my $txns = $ticket->Transactions;
-    foreach my $cf ( $self->ServiceLevelCustomFields ) {
-        $txns->_OpenParen('ServiceLevelCustomFields');
+
+    my $clause = 'ByTypeAndField';
+    while ( my ($type, $field) = each %{ $args{'Criteria'} } ) {
+        $txns->_OpenParen( $clause );
         $txns->Limit(
-            SUBCLAUSE       => 'ServiceLevelCustomFields',
             ENTRYAGGREGATOR => 'OR',
+            SUBCLAUSE       => $clause,
             FIELD           => 'Type',
-            VALUE           => 'CustomField',
-        );
-        $txns->Limit(
-            SUBCLAUSE       => 'ServiceLevelCustomFields',
-            ENTRYAGGREGATOR => 'AND',
-            FIELD           => 'Field',
-            VALUE           => $cf->id,
+            VALUE           => $type,
         );
-        $txns->_CloseParen('ServiceLevelCustomFields');
+        if ( $field ) {
+            my $tmp = ref $field? $field : [$field];
+            $txns->_OpenParen( $clause );
+            my $first = 1;
+            foreach my $value ( @$tmp ) {
+                $txns->Limit(
+                    SUBCLAUSE       => $clause,
+                    ENTRYAGGREGATOR => $first? 'AND' : 'OR',
+                    FIELD           => 'Field',
+                    VALUE           => $value,
+                );
+                $first = 0;
+            }
+            $txns->_CloseParen( $clause );
+        }
+        $txns->_CloseParen( $clause );
     }
+    $txns->OrderByCols(
+        { FIELD => 'Created', ORDER => $args{'Order'} },
+        { FIELD => 'id',      ORDER => $args{'Order'} },
+    );
 
-    return $self;
+    return $txns;
 }
 
 { my @cache = ();

commit d649911634eadf7912eb1c8fe2dc1451f170cb71
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat May 2 22:49:34 2009 +0000

    another round, close to something testable

diff --git a/lib/RT/Extension/SLA.pm b/lib/RT/Extension/SLA.pm
index 2834f14..0d46a9e 100644
--- a/lib/RT/Extension/SLA.pm
+++ b/lib/RT/Extension/SLA.pm
@@ -14,6 +14,14 @@ RT::Extension::SLA - Service Level Agreements for RT
 
 RT extension to implement automated due dates using service levels.
 
+=head1 UPGRADING
+
+On upgrade you shouldn't run 'make initdb'.
+
+If you were using 0.02 or older version of this extension with
+RT 3.8.1 then you have to uninstall that manually. List of files
+you can find in the MANIFEST.
+
 =head1 INSTALLATION
 
 =over
@@ -80,7 +88,7 @@ There is no WebUI in the current version. Almost everything is
 controlled in the RT's config using option C<%RT::ServiceAgreements>
 and C<%RT::ServiceBusinessHours>. For example:
 
-    %RT::ServiceAgreements = (
+    Set( %ServiceAgreements,
         Default => '4h',
         QueueDefault => {
             'Incident' => '2h',
@@ -321,7 +329,7 @@ arbitrarily, which could wreak havoc on your SLA performance.
 In the config you can set one or more work schedules. Use the following
 format:
 
-    %RT::ServiceBusinessHours = (
+    Set( %ServiceBusinessHours,
         'Default' => {
             ... description ...
         },
@@ -347,7 +355,7 @@ hours.
 
 then %RT::ServiceBusinessHours should have the corresponding definition:
 
-    %RT::ServiceBusinessHours = (
+    Set( %ServiceBusinessHours,
         'work just in Monday' => {
             1 => { Name => 'Monday', Start => '9:00', End => '18:00' },
         },
@@ -359,14 +367,14 @@ Default Business Hours setting is in $RT::ServiceBusinessHours{'Default'}.
 
 In the config you can set per queue defaults, using:
 
-    %RT::ServiceAgreements = (
+    Set( %ServiceAgreements,
         Default => 'global default level of service',
         QueueDefault => {
             'queue name' => 'default value for this queue',
             ...
         },
         ...
-    };
+    );
 
 =head2 Access control
 
@@ -542,6 +550,12 @@ sub GetDefaultServiceLevel {
     return $RT::ServiceAgreements{'Default'};
 }
 
+sub ReportOnTicket {
+    my $self = shift;
+    my $id = shift;
+
+}
+
 =head1 TODO
 
     * [implemented, TODO: tests for options in the config] default SLA for queues
diff --git a/lib/RT/Extension/SLA/Report.pm b/lib/RT/Extension/SLA/Report.pm
index 52a254e..65ee23d 100644
--- a/lib/RT/Extension/SLA/Report.pm
+++ b/lib/RT/Extension/SLA/Report.pm
@@ -4,13 +4,29 @@ use warnings;
 
 package RT::Extension::SLA::Report;
 
-sub new {}
+sub new {
+    my $proto = shift;
+    my $self = bless {}, ref($proto)||$proto;
+    return $self->init( @_ );
+}
 
-sub init {}
+sub init {
+    my $self = shift;
+    my %args = (Ticket => undef, @_);
+    $self->{'Ticket'} = $args{'Ticket'} || die "boo";
+    $self->{'State'} = {};
+    $self->{'Stats'} = [];
+    return $self;
+}
 
 sub State {
     my $self = shift;
-    return $self->{State} ||= {};
+    return $self->{State};
+}
+
+sub Stats {
+    my $self = shift;
+    return $self->{Stats};
 }
 
 { my $cache;
@@ -33,9 +49,9 @@ sub Handlers {
     return $cache;
 } }
 
-sub Drive {
+sub Run {
     my $self = shift;
-    my $txns = shift;
+    my $txns = shift || $self->{'Ticket'}->Transactions;
 
     my $state = $self->State;
     my $handler = $self->Handlers;
@@ -53,20 +69,18 @@ sub Drive {
         }
         next unless $h;
 
-        $self->$h( Transaction => $txn, State => $state );
+        $self->$h( Ticket => $self->{'Ticket'}, Transaction => $txn, State => $state );
     }
+    return $self;
 }
 
 sub OnCreate {
     my $self = shift;
     my %args = ( Ticket => undef, Transaction => undef, State => undef, @_);
 
-    my $level = $self->InitialServiceLevel( $args{'Ticket'} );
-
     my $state = $args{'State'};
     %$state = ();
-    $state->{'level'} = $level;
-    $state->{'transaction'} = $args{'Transaction'};
+    $state->{'level'} = $self->InitialServiceLevel( $args{'Ticket'} );
     $state->{'requestors'} = [ $self->InitialRequestors( $args{'Ticket'} ) ];
     $state->{'owner'} = $self->InitialOwner( $args{'Ticket'} );
     return;
@@ -88,7 +102,76 @@ sub OnRequestorChange {
 
 sub OnResponse {
     my $self = shift;
-    my $self
+    my %args = ( Ticket => undef, Transaction => undef, State => undef, @_);
+
+    my $txn = $args{'Transaction'};
+    unless ( $args{'State'}->{'level'} ) {
+        $RT::Logger->debug('No service level -> ignore txn #'. $txn->id );
+        return;
+    }
+
+    my $act = $args{'State'}->{'act'};
+    if ( $self->IsRequestorsAct( $txn ) ) {
+        if ( $act && $act->{'requestor'} ) {
+            # several requestors' acts in a row don't move deadlines
+            return;
+        }
+        $act ||= $args{'State'}->{'act'} = {};
+
+        $act->{'requestor'} = 1;
+        $act->{'acted'} = $txn->CreatedObj->Unix;
+    } else {
+        unless ( $act ) {
+            die "not yet implemented";
+        }
+        unless ( $act->{'requestor'} ) {
+            # check keep in loop
+            my $deadline = RT::Extension::SLA->Due(
+                Type  => 'KeepInLoop',
+                Level => $args{'State'}->{'level'},
+                Time  => $args{'State'}->{'acted'},
+            );
+            unless ( defined $deadline ) {
+                $RT::Logger->debug( "Multiple non-requestors replies in a raw, without keep in loop deadline");
+                return;
+            }
+            # keep in loop
+            my $failed = $txn->CreatedObj->Unix > $deadline? 1 : 0;
+            my $owner = $args{'State'}->{'owner'} == $txn->Creator? 1 : 0;
+            my $stat = {
+                type      => 'KeepInLoop',
+                owner     => $args{'State'}->{'owner'},
+                failed    => $failed,
+                owner_act => $owner,
+                shift     => $txn->CreatedObj->Unix - $deadline,                
+            };
+            push @{ $self->Stats }, $stat;
+        }
+        else {
+            # check response
+            my $deadline = RT::Extension::SLA->Due(
+                Type  => 'Response',
+                Level => $args{'State'}->{'level'},
+                Time  => $args{'State'}->{'acted'},
+            );
+            unless ( defined $deadline ) {
+                $RT::Logger->debug( "Non-requestors' reply after requestors', without response deadline");
+                return;
+            }
+
+            # repsonse
+            my $failed = $txn->CreatedObj->Unix > $deadline? 1 : 0;
+            my $owner = $args{'State'}->{'owner'} == $txn->Creator? 1 : 0;
+            my $stat = {
+                type      => 'KeepInLoop',
+                owner     => $args{'State'}->{'owner'},
+                failed    => $failed,
+                owner_act => $owner,
+                shift     => $txn->CreatedObj->Unix - $deadline,                
+            };
+            push @{ $self->Stats }, $stat;
+        }
+    }
 }
 
 sub IsRequestorsAct {
@@ -107,7 +190,7 @@ sub IsRequestorsAct {
         $cgm->LoadByCols( GroupId => $id, MemberId => $actor, Disabled => 0 );
         return 1 if $cgm->id;
     }
-    return 1;
+    return 0;
 }
 
 sub InitialServiceLevel {
@@ -147,11 +230,10 @@ sub InitialRequestors {
 
 sub InitialOwner {
     my $self = shift;
-    my $ticket = shift;
-
+    my %args = (Ticket => undef, @_);
     return $self->InitialValue(
         %args,
-        Current => $ticket->Owner,
+        Current => $args{'Ticket'}->Owner,
         Criteria => { 'Set', 'Owner' },
     );
 }
@@ -174,7 +256,7 @@ sub Transactions {
     my $self = shift;
     my %args = (Ticket => undef, Criteria => undef, Order => 'ASC', @_);
 
-    my $txns = $ticket->Transactions;
+    my $txns = $args{'Ticket'}->Transactions;
 
     my $clause = 'ByTypeAndField';
     while ( my ($type, $field) = each %{ $args{'Criteria'} } ) {
diff --git a/t/basics.t b/t/basics.t
index 4583112..ad39227 100644
--- a/t/basics.t
+++ b/t/basics.t
@@ -3,8 +3,9 @@
 use strict;
 use warnings;
 
-use RT::Extension::SLA::Test tests => 1, nodb => 1;
+use RT::Extension::SLA::Test tests => 2, nodb => 1;
 
 use_ok 'RT::Extension::SLA';
+use_ok 'RT::Extension::SLA::Report';
 
 1;

commit e4272680396fba5a57bbb995e7100ea1be3dc546
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon May 4 19:13:52 2009 +0000

    update TODO with some ideas

diff --git a/lib/RT/Extension/SLA.pm b/lib/RT/Extension/SLA.pm
index 0d46a9e..08cb4c0 100644
--- a/lib/RT/Extension/SLA.pm
+++ b/lib/RT/Extension/SLA.pm
@@ -556,7 +556,20 @@ sub ReportOnTicket {
 
 }
 
-=head1 TODO
+=head1 TODO and CAVEATS
+
+    * [not implemented] KeepInLoop and Response deadlines need adjusting. For example
+      KeepInLoop is 2h and Response is 2h as well. Owner replies at point 0, deadline
+      is 2h, at 1h requestor replies with anything -> deadline is moved according to
+      response deadline to 3h when it must stay at 2h waiting for KeepInLoop follow up
+      from owner and then move to another KeepInLoop deadline at 4h.
+
+    * [not implemented] Manually entered Due date should be treated as Resolve deadline.
+      We should store it and use later, so this module can be used for projects. For
+      example: Response 4 hours, KeepInLoop 1 day, Resolve 5 b.days; these are defaults,
+      but any manual change to Due date changes Resolve deadline.
+
+    * [not implemented] WebUI
 
     * [implemented, TODO: tests for options in the config] default SLA for queues
 
@@ -566,8 +579,6 @@ sub ReportOnTicket {
       something else). So people would be able to handle tickets in the right
       order using Due dates.
 
-    * [not implemented] WebUI
-
 =head1 DESIGN
 
 =head2 Classes

commit caed8787c37fe32ed814b852b097b723c059389b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon May 4 22:32:51 2009 +0000

    more changes here and there regarding reporting
    * new Summary.pm to help combine multiple reports
      together

diff --git a/lib/RT/Extension/SLA.pm b/lib/RT/Extension/SLA.pm
index 08cb4c0..b2dc857 100644
--- a/lib/RT/Extension/SLA.pm
+++ b/lib/RT/Extension/SLA.pm
@@ -550,10 +550,12 @@ sub GetDefaultServiceLevel {
     return $RT::ServiceAgreements{'Default'};
 }
 
-sub ReportOnTicket {
+sub Report {
     my $self = shift;
-    my $id = shift;
+    my $ticket = shift;
 
+    require RT::Extension::SLA::Report;
+    return RT::Extension::SLA::Report->new( Ticket => $ticket )->Run;
 }
 
 =head1 TODO and CAVEATS
diff --git a/lib/RT/Extension/SLA/Report.pm b/lib/RT/Extension/SLA/Report.pm
index 65ee23d..dbb10ff 100644
--- a/lib/RT/Extension/SLA/Report.pm
+++ b/lib/RT/Extension/SLA/Report.pm
@@ -19,6 +19,31 @@ sub init {
     return $self;
 }
 
+sub Run {
+    my $self = shift;
+    my $txns = shift || $self->{'Ticket'}->Transactions;
+
+    my $state = $self->State;
+    my $handler = $self->Handlers;
+
+    while ( my $txn = $txns->Next ) {
+        my ($type, $field) = ($txn->Type, $txn->Field);
+
+        my $h = $handler->{ $type };
+        unless ( $h ) {
+            $RT::Logger->debug( "No handler for $type transaction, skipping" );
+        } elsif ( ref $h ) {
+            unless ( $h = $h->{ $field } ) {
+                $RT::Logger->debug( "No handler for ($type, $field) transaction, skipping" );
+            }
+        }
+        next unless $h;
+
+        $self->$h( Ticket => $self->{'Ticket'}, Transaction => $txn, State => $state );
+    }
+    return $self;
+}
+
 sub State {
     my $self = shift;
     return $self->{State};
@@ -41,49 +66,27 @@ sub Handlers {
             Owner => 'OnOwnerChange',
         },
         Correspond => 'OnResponse',
-        CustomField => { map $_ => 'OnServiceLevelChange', $self->ServiceLevelCustomFields },
+        CustomField => { map { $_->id => 'OnServiceLevelChange' } $self->ServiceLevelCustomFields },
         AddWatcher => { Requestor => 'OnRequestorChange' },
         DelWatcher => { Requestor => 'OnRequestorChange' },
     };
 
+    use Data::Dumper;
+    Test::More::diag( Dumper $cache );
+
     return $cache;
 } }
 
-sub Run {
-    my $self = shift;
-    my $txns = shift || $self->{'Ticket'}->Transactions;
-
-    my $state = $self->State;
-    my $handler = $self->Handlers;
-
-    while ( my $txn = $txns->Next ) {
-        my ($type, $field) = ($txn->Type, $txn->Field);
-
-        my $h = $handler->{ $type };
-        unless ( $h ) {
-            $RT::Logger->debug( "No handler for $type transaction, skipping" );
-        } elsif ( ref $h ) {
-            unless ( $h = $h->{ $field } ) {
-                $RT::Logger->debug( "No handler for ($type, $field) transaction, skipping" );
-            }
-        }
-        next unless $h;
-
-        $self->$h( Ticket => $self->{'Ticket'}, Transaction => $txn, State => $state );
-    }
-    return $self;
-}
-
 sub OnCreate {
     my $self = shift;
     my %args = ( Ticket => undef, Transaction => undef, State => undef, @_);
 
     my $state = $args{'State'};
     %$state = ();
-    $state->{'level'} = $self->InitialServiceLevel( $args{'Ticket'} );
-    $state->{'requestors'} = [ $self->InitialRequestors( $args{'Ticket'} ) ];
-    $state->{'owner'} = $self->InitialOwner( $args{'Ticket'} );
-    return;
+    $state->{'level'} = $self->InitialServiceLevel( Ticket => $args{'Ticket'} );
+    $state->{'requestors'} = [ $self->InitialRequestors( Ticket => $args{'Ticket'} ) ];
+    $state->{'owner'} = $self->InitialOwner( Ticket => $args{'Ticket'} );
+    return $self->OnResponse( %args );
 }
 
 sub OnRequestorChange {
@@ -100,15 +103,21 @@ sub OnRequestorChange {
     }
 }
 
+sub OnServiceLevelChange {
+    my $self = shift;
+    my %args = ( Ticket => undef, Transaction => undef, State => undef, @_);
+    $self->State->{'level'} = $args{'Transaction'}->NewValue;
+}
+
 sub OnResponse {
     my $self = shift;
     my %args = ( Ticket => undef, Transaction => undef, State => undef, @_);
 
     my $txn = $args{'Transaction'};
-    unless ( $args{'State'}->{'level'} ) {
-        $RT::Logger->debug('No service level -> ignore txn #'. $txn->id );
-        return;
-    }
+#    unless ( $args{'State'}->{'level'} ) {
+#        $RT::Logger->debug('No service level -> ignore txn #'. $txn->id );
+#        return;
+#    }
 
     my $act = $args{'State'}->{'act'};
     if ( $self->IsRequestorsAct( $txn ) ) {
@@ -143,6 +152,7 @@ sub OnResponse {
                 owner     => $args{'State'}->{'owner'},
                 failed    => $failed,
                 owner_act => $owner,
+                actor     => $txn->Creator,
                 shift     => $txn->CreatedObj->Unix - $deadline,                
             };
             push @{ $self->Stats }, $stat;
@@ -152,22 +162,25 @@ sub OnResponse {
             my $deadline = RT::Extension::SLA->Due(
                 Type  => 'Response',
                 Level => $args{'State'}->{'level'},
-                Time  => $args{'State'}->{'acted'},
+                Time  => $args{'State'}->{'act'}->{'acted'},
             );
             unless ( defined $deadline ) {
                 $RT::Logger->debug( "Non-requestors' reply after requestors', without response deadline");
                 return;
             }
 
+            Test::More::diag( 'deadline '. $deadline .' '. Dumper( $args{'State'} ) );
+
             # repsonse
             my $failed = $txn->CreatedObj->Unix > $deadline? 1 : 0;
             my $owner = $args{'State'}->{'owner'} == $txn->Creator? 1 : 0;
             my $stat = {
-                type      => 'KeepInLoop',
+                type      => 'Response',
                 owner     => $args{'State'}->{'owner'},
                 failed    => $failed,
                 owner_act => $owner,
-                shift     => $txn->CreatedObj->Unix - $deadline,                
+                actor     => $txn->Creator,
+                shift     => ($txn->CreatedObj->Unix - $deadline),
             };
             push @{ $self->Stats }, $stat;
         }
@@ -195,23 +208,23 @@ sub IsRequestorsAct {
 
 sub InitialServiceLevel {
     my $self = shift;
-    my $ticket = shift;
+    my %args = @_;
 
     return $self->InitialValue(
-        Ticket   => $ticket,
-        Current  => $ticket->FirstCustomFieldValue('SLA'),
+        Ticket   => $args{'Ticket'},
+        Current  => $args{'Ticket'}->FirstCustomFieldValue('SLA'),
         Criteria => { CustomField => [ map $_->id, $self->ServiceLevelCustomFields ] },
     );
 }
 
 sub InitialRequestors {
     my $self = shift;
-    my $ticket = shift;
+    my %args = @_;
 
-    my @current = map $_->Member, @{ $ticket->Requestors->MembersObj->ItemsArrayRef };
+    my @current = map $_->MemberId, @{ $args{'Ticket'}->Requestors->MembersObj->ItemsArrayRef };
 
     my $txns = $self->Transactions(
-        Ticket => $ticket,
+        Ticket => $args{'Ticket'},
         Order => 'DESC',
         Criteria => { 'AddWatcher' => 'Requestor', DelWatcher => 'Requestor' },
     );
@@ -302,9 +315,7 @@ sub ServiceLevelCustomFields {
     $cfs->Limit( FIELD => 'LookupType', VALUE => RT::Ticket->CustomFieldLookupType );
     # XXX: limit to applied custom fields only
 
-    push @cache, $_ while $_ = $cfs->Next;
-
-    return @cache;
+    return @cache = @{ $cfs->ItemsArrayRef };
 } }
 
 1;
diff --git a/lib/RT/Extension/SLA/Summary.pm b/lib/RT/Extension/SLA/Summary.pm
new file mode 100644
index 0000000..b59ba9d
--- /dev/null
+++ b/lib/RT/Extension/SLA/Summary.pm
@@ -0,0 +1,68 @@
+use 5.8.0;
+use strict;
+use warnings;
+
+package RT::Extension::SLA::Summary;
+
+sub new {
+    my $proto = shift;
+    my $self = bless {}, ref($proto)||$proto;
+    return $self->init( @_ );
+}
+
+sub init {
+    my $self = shift;
+    return $self;
+}
+
+sub Result {
+    my $self = shift;
+    return $self->{'Result'} ||= { };
+}
+
+sub AddReport {
+    my $self = shift;
+    my $report = shift;
+
+    my $new = $self->OnReport( $report );
+
+    my $total = $self->Result;
+    while ( my ($user, $stat) = each %$new ) {
+        my $tmp = $total->{$user} ||= {};
+        while ( my ($action, $count) = each %$stat ) {
+            $tmp->{$action} += $count;
+        }
+    }
+
+    return $self;
+}
+
+sub OnReport {
+    my $self = shift;
+    my $report = shift;
+
+    my $res = {};
+    foreach my $stat ( @{ $report->Stats } ) {
+        if ( $stat->{'owner_act'} ) {
+            my $owner = $res->{ $stat->{'owner'} } ||= { };
+            if ( $stat->{'failed'} ) {
+                $owner->{'failed'}++;
+            } else {
+                $owner->{'passed'}++;
+            }
+        } else {
+            my $owner = $res->{ $stat->{'owner'} } ||= { };
+            my $actor = $res->{ $stat->{'actor'} } ||= { };
+            if ( $stat->{'failed'} ) {
+                $owner->{'failed'}++;
+                $actor->{'late help'}++;
+            } else {
+                $owner->{'got help'}++;
+                $actor->{'helped'}++;
+            }
+        }
+    }
+    return $res;
+}
+
+1;

commit 8e581f88b34797ebf46d42fdcd67ae126902c63d
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 5 01:11:11 2009 +0000

    add first html, more simple tests and changes Summary

diff --git a/META.yml b/META.yml
index 771f809..0ffa792 100644
--- a/META.yml
+++ b/META.yml
@@ -18,6 +18,7 @@ name: RT-Extension-SLA
 no_index:
   directory:
     - etc
+    - html
     - inc
     - t
 requires:
diff --git a/html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default b/html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default
new file mode 100644
index 0000000..73c310b
--- /dev/null
+++ b/html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default
@@ -0,0 +1,9 @@
+<%ARGS>
+$tabs => {}
+</%ARGS>
+<%INIT>
+$tabs->{'s'} = {
+    title => loc('Service Level Aggreements'),
+    path  => 'Tools/Reports/SLA.html',
+};
+</%INIT>
diff --git a/html/Tools/Reports/SLA.html b/html/Tools/Reports/SLA.html
new file mode 100644
index 0000000..1cdeb03
--- /dev/null
+++ b/html/Tools/Reports/SLA.html
@@ -0,0 +1,43 @@
+<& /Elements/Header, Title => $title &>
+<& /Tools/Reports/Elements/Tabs, current_tab => 'Tools/Reports/SLA.html', Title => $title &>
+
+<table>
+<tr>
+<th><% loc('Owner') %></th>
+% my @columns = $summary->Labels;
+% my $i = 0;
+% foreach ( map $_->[0], grep $i++%2, @columns ) {
+<th><% loc($_) %></th>
+% }
+</tr>
+
+% while ( my ($owner, $stats) = each %$result ) {
+    <tr><td><% $owner %><td>
+% my $i = 1;
+% foreach ( map $stats->{ $_ }, grep $i++%2, @columns ) {
+<td><% $_ || 0 %></td>
+% }
+    </tr>
+% }
+</table>
+
+<%ARGS>
+$Query => undef
+</%ARGS>
+<%INIT>
+my $title = loc("Report on Service Level Agreements");
+
+use RT::Extension::SLA::Summary;
+my $summary = new RT::Extension::SLA::Summary;
+
+my $tickets = RT::Tickets->new( $session{'CurrentUser'} );
+$tickets->FromSQL( $Query );
+$tickets->OrderByCols( {FIELD => 'id', ORDER => 'ASC'} );
+while ( my $ticket = $tickets->Next ) {
+    my $report = RT::Extension::SLA->Report( Ticket => $ticket );
+    $summary->AddReport( $report );
+}
+
+my $result = $summary->Result;
+
+</%INIT>
diff --git a/lib/RT/Extension/SLA/Summary.pm b/lib/RT/Extension/SLA/Summary.pm
index b59ba9d..a0dc52f 100644
--- a/lib/RT/Extension/SLA/Summary.pm
+++ b/lib/RT/Extension/SLA/Summary.pm
@@ -20,6 +20,18 @@ sub Result {
     return $self->{'Result'} ||= { };
 }
 
+our @known_stats = (
+    'passed' => ['Passed', 'Replied before a deadline'],
+    'failed' => ['Failed', 'Replied after a deadline or not replied at all'],
+    'helped' => ['Helped', 'Helped another user to reach a deadline'],
+    'late help' => ['Helped (late)', 'Helped another user, however failed a deadline'],
+    'got help'  => ['Got help', 'Got help from another user within a deadline'],
+);
+
+sub Labels {
+    return @known_stats;
+}
+
 sub AddReport {
     my $self = shift;
     my $report = shift;
diff --git a/t/basics.t b/t/basics.t
index ad39227..2ab707b 100644
--- a/t/basics.t
+++ b/t/basics.t
@@ -3,9 +3,10 @@
 use strict;
 use warnings;
 
-use RT::Extension::SLA::Test tests => 2, nodb => 1;
+use RT::Extension::SLA::Test tests => 3, nodb => 1;
 
 use_ok 'RT::Extension::SLA';
 use_ok 'RT::Extension::SLA::Report';
+use_ok 'RT::Extension::SLA::Summary';
 
 1;

commit 821f0a0cef6e81b9012767e93da98421743f5fe2
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 5 01:50:43 2009 +0000

    cleanup debug code
    * fix some bugs

diff --git a/html/Tools/Reports/SLA.html b/html/Tools/Reports/SLA.html
index 1cdeb03..df07885 100644
--- a/html/Tools/Reports/SLA.html
+++ b/html/Tools/Reports/SLA.html
@@ -12,12 +12,13 @@
 </tr>
 
 % while ( my ($owner, $stats) = each %$result ) {
-    <tr><td><% $owner %><td>
+<tr>
+<td><% $owner %></td>
 % my $i = 1;
 % foreach ( map $stats->{ $_ }, grep $i++%2, @columns ) {
 <td><% $_ || 0 %></td>
 % }
-    </tr>
+</tr>
 % }
 </table>
 
@@ -34,10 +35,9 @@ my $tickets = RT::Tickets->new( $session{'CurrentUser'} );
 $tickets->FromSQL( $Query );
 $tickets->OrderByCols( {FIELD => 'id', ORDER => 'ASC'} );
 while ( my $ticket = $tickets->Next ) {
-    my $report = RT::Extension::SLA->Report( Ticket => $ticket );
+    my $report = RT::Extension::SLA->Report( $ticket );
     $summary->AddReport( $report );
 }
 
 my $result = $summary->Result;
-
 </%INIT>
diff --git a/lib/RT/Extension/SLA/Report.pm b/lib/RT/Extension/SLA/Report.pm
index dbb10ff..9d0d8c2 100644
--- a/lib/RT/Extension/SLA/Report.pm
+++ b/lib/RT/Extension/SLA/Report.pm
@@ -39,6 +39,8 @@ sub Run {
         }
         next unless $h;
 
+        $RT::Logger->debug( "Handling transaction #". $txn->id ." ($type, $field) of ticket #". $self->{'Ticket'}->id );
+
         $self->$h( Ticket => $self->{'Ticket'}, Transaction => $txn, State => $state );
     }
     return $self;
@@ -71,9 +73,6 @@ sub Handlers {
         DelWatcher => { Requestor => 'OnRequestorChange' },
     };
 
-    use Data::Dumper;
-    Test::More::diag( Dumper $cache );
-
     return $cache;
 } }
 
@@ -131,7 +130,10 @@ sub OnResponse {
         $act->{'acted'} = $txn->CreatedObj->Unix;
     } else {
         unless ( $act ) {
-            die "not yet implemented";
+            $act = $args{'State'}->{'act'} = {};
+            $act->{'requestor'} = 0;
+            $act->{'acted'} = $txn->CreatedObj->Unix;
+            return;
         }
         unless ( $act->{'requestor'} ) {
             # check keep in loop
@@ -169,8 +171,6 @@ sub OnResponse {
                 return;
             }
 
-            Test::More::diag( 'deadline '. $deadline .' '. Dumper( $args{'State'} ) );
-
             # repsonse
             my $failed = $txn->CreatedObj->Unix > $deadline? 1 : 0;
             my $owner = $args{'State'}->{'owner'} == $txn->Creator? 1 : 0;

commit cc90411c362efbb3dd756ebbbcfa33cd795fad00
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 5 14:46:40 2009 +0000

    show user using a component
    * show some simple form

diff --git a/html/Tools/Reports/SLA.html b/html/Tools/Reports/SLA.html
index df07885..d50bbc8 100644
--- a/html/Tools/Reports/SLA.html
+++ b/html/Tools/Reports/SLA.html
@@ -12,8 +12,10 @@
 </tr>
 
 % while ( my ($owner, $stats) = each %$result ) {
+% my $user = RT::User->new( $session{'CurrentUser'} );
+% $user->Load( $owner );
 <tr>
-<td><% $owner %></td>
+<td><& /Elements/ShowUser, User => $user &></td>
 % my $i = 1;
 % foreach ( map $stats->{ $_ }, grep $i++%2, @columns ) {
 <td><% $_ || 0 %></td>
@@ -22,6 +24,11 @@
 % }
 </table>
 
+<form method="post" action="SLA.html">
+<&|/l&>Query</&>:<textarea cols="60" rows="20" name="Query"><% $Query %></textarea>
+<& /Elements/Submit, Label => loc('Update report') &>
+</form>
+
 <%ARGS>
 $Query => undef
 </%ARGS>

commit cdc0a9dc4fc533a24522e40ea74afd12322641aa
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 5 15:36:23 2009 +0000

    new test file, basics of reporting

diff --git a/t/reporting/basic.t b/t/reporting/basic.t
new file mode 100644
index 0000000..55befef
--- /dev/null
+++ b/t/reporting/basic.t
@@ -0,0 +1,114 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Test::MockTime qw(set_fixed_time);
+
+use Test::More tests => 72;
+
+require 't/utils.pl';
+
+use_ok 'RT';
+RT::LoadConfig();
+$RT::LogToScreen = $ENV{'TEST_VERBOSE'} ? 'debug': 'warning';
+RT::Init();
+
+use_ok 'RT::Ticket';
+use_ok 'RT::Extension::SLA::Report';
+
+my $root = RT::User->new( $RT::SystemUser );
+$root->LoadByEmail('root at localhost');
+ok $root->id, 'loaded root user';
+
+diag '';
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => { Response => { RealMinutes => 60*2 } },
+        },
+    );
+
+    set_fixed_time('2009-05-05T10:00:00Z');
+
+    my $time = time;
+
+    # requestor creates
+    my $id;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        ($id) = $ticket->Create( Queue => 'General', Subject => 'xxx', Requestor => $root->id );
+        ok $id, "created ticket #$id";
+
+        is $ticket->FirstCustomFieldValue('SLA'), '2', 'default sla';
+
+        my $due = $ticket->DueObj->Unix;
+        is $due, $time + 2*60*60, 'Due date is two hours from "now"';
+    }
+
+    set_fixed_time('2009-05-05T11:00:00Z');
+
+    # non-requestor reply
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        $ticket->Correspond( Content => 'we are working on this.' );
+    }
+
+    my $ticket = RT::Ticket->new( $RT::SystemUser );
+    $ticket->Load( $id );
+    my $report = RT::Extension::SLA::Report->new( Ticket => $ticket )->Run;
+    is_deeply $report->Stats,
+        [ {type => 'Response', owner => $RT::Nobody->id, owner_act => 0, failed => 0, shift => -3600 } ],
+        'correct stats'
+    ;
+}
+
+
+diag '';
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => { Response => { RealMinutes => 60*2 } },
+        },
+    );
+
+    set_fixed_time('2009-05-05T10:00:00Z');
+
+    my $time = time;
+
+    # requestor creates
+    my $id;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        ($id) = $ticket->Create( Queue => 'General', Subject => 'xxx', Requestor => $root->id );
+        ok $id, "created ticket #$id";
+
+        is $ticket->FirstCustomFieldValue('SLA'), '2', 'default sla';
+
+        my $due = $ticket->DueObj->Unix;
+        is $due, $time + 2*60*60, 'Due date is two hours from "now"';
+    }
+
+    set_fixed_time('2009-05-05T11:00:00Z');
+
+    # non-requestor reply
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        $ticket->Correspond( Content => 'we are working on this.' );
+    }
+
+    my $ticket = RT::Ticket->new( $RT::SystemUser );
+    $ticket->Load( $id );
+    my $report = RT::Extension::SLA::Report->new( Ticket => $ticket )->Run;
+    is_deeply $report->Stats,
+        [ {type => 'Response', owner => $RT::Nobody->id, owner_act => 0, failed => 0, shift => -3600 } ],
+        'correct stats'
+    ;
+}
+
+

commit a70a5c071d632f6ba4342048a0f63c1f84317adc
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 5 15:37:19 2009 +0000

    bump dev. version, update manifest and meta

diff --git a/MANIFEST b/MANIFEST
index 72c9c0a..1246c19 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,6 +1,8 @@
 Changes
 etc/initialdata
 etc/upgrade/0.06/content
+html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default
+html/Tools/Reports/SLA.html
 inc/Module/AutoInstall.pm
 inc/Module/Install.pm
 inc/Module/Install/AutoInstall.pm
@@ -25,6 +27,8 @@ lib/RT/Condition/SLA_RequireDefault.pm
 lib/RT/Condition/SLA_RequireDueSet.pm
 lib/RT/Condition/SLA_RequireStartsSet.pm
 lib/RT/Extension/SLA.pm
+lib/RT/Extension/SLA/Report.pm
+lib/RT/Extension/SLA/Summary.pm
 lib/RT/Extension/SLA/Test.pm.in
 lib/RT/Queue_SLA.pm
 Makefile.PL
@@ -37,5 +41,6 @@ t/business_hours.t
 t/due.t
 t/ignore-on-statuses.t
 t/queue.t
+t/reporting/basic.t
 t/starts.t
 t/timezone.t
diff --git a/META.yml b/META.yml
index 0ffa792..eb6ad5c 100644
--- a/META.yml
+++ b/META.yml
@@ -26,4 +26,4 @@ requires:
   perl: 5.8.0
 resources:
   license: http://opensource.org/licenses/gpl-2.0.php
-version: '0.08'
+version: 0.08_01
diff --git a/README b/README
index 38a3472..99d637a 100644
--- a/README
+++ b/README
@@ -4,6 +4,13 @@ NAME
 DESCRIPTION
     RT extension to implement automated due dates using service levels.
 
+UPGRADING
+    On upgrade you shouldn't run 'make initdb'.
+
+    If you were using 0.02 or older version of this extension with RT 3.8.1
+    then you have to uninstall that manually. List of files you can find in
+    the MANIFEST.
+
 INSTALLATION
     "perl Makefile.PL"
     "make"
@@ -58,7 +65,7 @@ CONFIGURATION
     controlled in the RT's config using option %RT::ServiceAgreements and
     %RT::ServiceBusinessHours. For example:
 
-        %RT::ServiceAgreements = (
+        Set( %ServiceAgreements,
             Default => '4h',
             QueueDefault => {
                 'Incident' => '2h',
@@ -269,7 +276,7 @@ CONFIGURATION
     In the config you can set one or more work schedules. Use the following
     format:
 
-        %RT::ServiceBusinessHours = (
+        Set( %ServiceBusinessHours,
             'Default' => {
                 ... description ...
             },
@@ -294,7 +301,7 @@ CONFIGURATION
 
     then %RT::ServiceBusinessHours should have the corresponding definition:
 
-        %RT::ServiceBusinessHours = (
+        Set( %ServiceBusinessHours,
             'work just in Monday' => {
                 1 => { Name => 'Monday', Start => '9:00', End => '18:00' },
             },
@@ -306,14 +313,14 @@ CONFIGURATION
   Defining service levels per queue
     In the config you can set per queue defaults, using:
 
-        %RT::ServiceAgreements = (
+        Set( %ServiceAgreements,
             Default => 'global default level of service',
             QueueDefault => {
                 'queue name' => 'default value for this queue',
                 ...
             },
             ...
-        };
+        );
 
   Access control
     You can totally hide SLA custom field from users and use per queue
@@ -325,7 +332,20 @@ CONFIGURATION
     You may want to allow customers or managers to escalate thier tickets.
     Just grant them ModifyCustomField right.
 
-TODO
+TODO and CAVEATS
+        * [not implemented] KeepInLoop and Response deadlines need adjusting. For example
+          KeepInLoop is 2h and Response is 2h as well. Owner replies at point 0, deadline
+          is 2h, at 1h requestor replies with anything -> deadline is moved according to
+          response deadline to 3h when it must stay at 2h waiting for KeepInLoop follow up
+          from owner and then move to another KeepInLoop deadline at 4h.
+
+        * [not implemented] Manually entered Due date should be treated as Resolve deadline.
+          We should store it and use later, so this module can be used for projects. For
+          example: Response 4 hours, KeepInLoop 1 day, Resolve 5 b.days; these are defaults,
+          but any manual change to Due date changes Resolve deadline.
+
+        * [not implemented] WebUI
+
         * [implemented, TODO: tests for options in the config] default SLA for queues
 
         * [implemented, TODO: tests] add support for multiple b-hours definitions,
@@ -334,8 +354,6 @@ TODO
           something else). So people would be able to handle tickets in the right
           order using Due dates.
 
-        * [not implemented] WebUI
-
 DESIGN
   Classes
     Actions are subclasses of RT::Action::SLA class that is subclass of
diff --git a/lib/RT/Extension/SLA.pm b/lib/RT/Extension/SLA.pm
index b2dc857..90e3270 100644
--- a/lib/RT/Extension/SLA.pm
+++ b/lib/RT/Extension/SLA.pm
@@ -4,7 +4,7 @@ use warnings;
 
 package RT::Extension::SLA;
 
-our $VERSION = '0.08';
+our $VERSION = '0.08_01';
 
 =head1 NAME
 

commit 8fbcfbaa6ed7bf7b6b85310a5baf074b0fd6e492
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed May 13 21:58:49 2009 +0000

    add protection by a right

diff --git a/html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default b/html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default
index 73c310b..d1f352c 100644
--- a/html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default
+++ b/html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default
@@ -2,6 +2,9 @@
 $tabs => {}
 </%ARGS>
 <%INIT>
+return unless $session{'CurrentUser'}->PrincipalObj->HasRight(
+    Object => $RT::System, Right => 'SeeSLAReports',
+);
 $tabs->{'s'} = {
     title => loc('Service Level Aggreements'),
     path  => 'Tools/Reports/SLA.html',
diff --git a/html/Tools/Reports/SLA.html b/html/Tools/Reports/SLA.html
index d50bbc8..ee2f6ae 100644
--- a/html/Tools/Reports/SLA.html
+++ b/html/Tools/Reports/SLA.html
@@ -33,6 +33,14 @@
 $Query => undef
 </%ARGS>
 <%INIT>
+unless (
+    $session{'CurrentUser'}->PrincipalObj->HasRight(
+        Object => $RT::System, Right => 'SeeSLAReports',
+    )
+) {
+    Abort("You're not allowed to see SLA reports.");
+}
+
 my $title = loc("Report on Service Level Agreements");
 
 use RT::Extension::SLA::Summary;
diff --git a/lib/RT/Extension/SLA.pm b/lib/RT/Extension/SLA.pm
index 90e3270..88c4602 100644
--- a/lib/RT/Extension/SLA.pm
+++ b/lib/RT/Extension/SLA.pm
@@ -389,6 +389,14 @@ Just grant them ModifyCustomField right.
 
 =cut
 
+{
+    my $right = 'SeeSLAReports';
+    use RT::System;
+    $RT::System::Rights->{$right} = 'See service level performance reports';
+    use RT::ACE;
+    $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
+}
+
 sub BusinessHours {
     my $self = shift;
     my $name = shift || 'Default';

commit f949be6eb3c8c0cbcdb9cc7a5c69d11794800f01
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed May 13 22:00:18 2009 +0000

    add tabs to tickets so you can jump to a report right after search

diff --git a/html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default b/html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default
new file mode 100644
index 0000000..d8bbb01
--- /dev/null
+++ b/html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default
@@ -0,0 +1,15 @@
+<%ARGS>
+$Query => undef
+$tabs => {}
+</%ARGS>
+<%INIT>
+
+$Query ||= $session{'CurrentSearchHash'}->{'Query'};
+
+return unless $Query;
+
+$tabs->{"m"} = {
+    path  => "Tools/Reports/SLA.html?". $m->comp( '/Elements/QueryString', Query => $Query ),
+    title => loc('Report SLA'),
+};
+</%INIT>

commit 4d8961046dd8448c887ee8b88dbc66ab813b3a17
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu May 14 14:27:44 2009 +0000

    add report per ticket for debugging and analysis

diff --git a/html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default b/html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default
index d8bbb01..76253a0 100644
--- a/html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default
+++ b/html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default
@@ -1,15 +1,24 @@
 <%ARGS>
 $Query => undef
 $tabs => {}
+$Ticket => undef
 </%ARGS>
 <%INIT>
 
-$Query ||= $session{'CurrentSearchHash'}->{'Query'};
+return unless $session{'CurrentUser'}->PrincipalObj->HasRight(
+        Object => $RT::System, Right => 'SeeSLAReports',
+);
 
-return unless $Query;
-
-$tabs->{"m"} = {
-    path  => "Tools/Reports/SLA.html?". $m->comp( '/Elements/QueryString', Query => $Query ),
-    title => loc('Report SLA'),
-};
+if ( $Ticket ) {
+    $tabs->{'this'}->{"subtabs"}->{'_DA'} = {
+        path  => "Ticket/SLA.html?id=". $Ticket->id,
+        title => loc('Report SLA'),
+    };
+}
+elsif ( $Query ||= $session{'CurrentSearchHash'}->{'Query'} ) {
+    $tabs->{"m"} = {
+        path  => "Tools/Reports/SLA.html?". $m->comp( '/Elements/QueryString', Query => $Query ),
+        title => loc('Report SLA'),
+    };
+}
 </%INIT>
diff --git a/html/Ticket/SLA.html b/html/Ticket/SLA.html
new file mode 100644
index 0000000..0cde6da
--- /dev/null
+++ b/html/Ticket/SLA.html
@@ -0,0 +1,46 @@
+<& /Elements/Header, Title => $title &>
+<& /Ticket/Elements/Tabs, 
+    Ticket => $ticket,
+    current_tab => "Ticket/SLA.html?id=$id",
+    Title => $title,
+&>
+
+<table>
+<tr><th>#</th><th>Description</th><th>Type</th><th>Owner</th><th>Failed</th><th>Shift</th></tr>
+% foreach my $stat ( @{ $report->Stats } ) {
+<tr>
+<td><% $stat->{transaction}->id %></td>
+<td><% $stat->{transaction}->Description %></td>
+<td><% $stat->{owner_act}? 'yes' : 'no' %></td>
+<td><% $stat->{failed}? 'yes' : 'no' %></td>
+<td><% $stat->{shift} %></td>
+</tr>
+% }
+</table>
+
+<%ARGS>
+$id => undef
+</%ARGS>
+<%INIT>
+
+unless (
+    $session{'CurrentUser'}->PrincipalObj->HasRight(
+        Object => $RT::System, Right => 'SeeSLAReports',
+    )
+) {
+    Abort("You're not allowed to see SLA reports.");
+}
+
+my $ticket = LoadTicket($id);
+unless ($ticket->CurrentUserHasRight('ShowTicket')) {
+    Abort("No permission to view ticket");
+}
+$id = $ARGS{'id'} = $ticket->id;
+
+my $title = loc("SLA performance on ticket #[_1]", $id);
+
+use RT::Extension::SLA;
+my $report = RT::Extension::SLA->Report( $ticket );
+use Data::Dumper;
+$RT::Logger->crit( Dumper $report );
+</%INIT>

commit 657771e7464494f8075912aae64a994ea5b7a8f7
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu May 14 14:29:08 2009 +0000

    store txn in the stats

diff --git a/lib/RT/Extension/SLA/Report.pm b/lib/RT/Extension/SLA/Report.pm
index 9d0d8c2..94290cd 100644
--- a/lib/RT/Extension/SLA/Report.pm
+++ b/lib/RT/Extension/SLA/Report.pm
@@ -150,12 +150,13 @@ sub OnResponse {
             my $failed = $txn->CreatedObj->Unix > $deadline? 1 : 0;
             my $owner = $args{'State'}->{'owner'} == $txn->Creator? 1 : 0;
             my $stat = {
-                type      => 'KeepInLoop',
-                owner     => $args{'State'}->{'owner'},
-                failed    => $failed,
-                owner_act => $owner,
-                actor     => $txn->Creator,
-                shift     => $txn->CreatedObj->Unix - $deadline,                
+                type        => 'KeepInLoop',
+                owner       => $args{'State'}->{'owner'},
+                failed      => $failed,
+                owner_act   => $owner,
+                transaction => $txn,
+                actor       => $txn->Creator,
+                shift       => $txn->CreatedObj->Unix - $deadline,                
             };
             push @{ $self->Stats }, $stat;
         }
@@ -175,12 +176,13 @@ sub OnResponse {
             my $failed = $txn->CreatedObj->Unix > $deadline? 1 : 0;
             my $owner = $args{'State'}->{'owner'} == $txn->Creator? 1 : 0;
             my $stat = {
-                type      => 'Response',
-                owner     => $args{'State'}->{'owner'},
-                failed    => $failed,
-                owner_act => $owner,
-                actor     => $txn->Creator,
-                shift     => ($txn->CreatedObj->Unix - $deadline),
+                type        => 'Response',
+                owner       => $args{'State'}->{'owner'},
+                failed      => $failed,
+                owner_act   => $owner,
+                transaction => $txn,
+                actor       => $txn->Creator,
+                shift       => ($txn->CreatedObj->Unix - $deadline),
             };
             push @{ $self->Stats }, $stat;
         }

commit a57aabcda84085513da31b5d22089e6e8e7a0660
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu May 14 15:07:41 2009 +0000

    update manifest

diff --git a/MANIFEST b/MANIFEST
index 1246c19..8828e78 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,7 +1,9 @@
 Changes
 etc/initialdata
 etc/upgrade/0.06/content
+html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default
 html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default
+html/Ticket/SLA.html
 html/Tools/Reports/SLA.html
 inc/Module/AutoInstall.pm
 inc/Module/Install.pm

commit af6067f0ea8f9d5e671f5e7d54752fad33cacdbc
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Feb 29 16:01:06 2012 +0400

    convert tests over Test.pm

diff --git a/t/reporting/basic.t b/t/reporting/basic.t
index 55befef..f152130 100644
--- a/t/reporting/basic.t
+++ b/t/reporting/basic.t
@@ -4,16 +4,8 @@ use strict;
 use warnings;
 use Test::MockTime qw(set_fixed_time);
 
-use Test::More tests => 72;
+use RT::Extension::SLA::Test tests => 6;
 
-require 't/utils.pl';
-
-use_ok 'RT';
-RT::LoadConfig();
-$RT::LogToScreen = $ENV{'TEST_VERBOSE'} ? 'debug': 'warning';
-RT::Init();
-
-use_ok 'RT::Ticket';
 use_ok 'RT::Extension::SLA::Report';
 
 my $root = RT::User->new( $RT::SystemUser );

commit 1a6f8600ca9629e924e816c0a1ca41f52b31b959
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Mar 3 16:07:05 2012 +0400

    rework SLA Reports
    
    * port over 4.0 menu
    * summary instead of detailed stats
    * count replies by requestor, owner and other
    * first response time stats
    * response times stats for requestor, owner and other
    * deadlines stats

diff --git a/html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged b/html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged
new file mode 100644
index 0000000..5bbcc46
--- /dev/null
+++ b/html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged
@@ -0,0 +1,34 @@
+<%INIT>
+
+my $request_path = $HTML::Mason::Commands::r->path_info;
+
+if ( $request_path =~ m{^/Ticket/} ) {
+    if ( ( $m->request_args->{'id'} || '' ) =~ /^(\d+)$/ ) {
+        my $obj = RT::Ticket->new( $session{'CurrentUser'} );
+        $obj->Load($1);
+        return unless $obj->id;
+
+        return unless $obj->CurrentUserHasRight('SeeSLAReports');
+
+        PageMenu->child('actions')->child('sla_report' =>
+            path  => "/Ticket/SLA.html?id=". $obj->id,
+            title => loc('SLA Report'),
+        );
+    }
+}
+
+#elsif ( $Query ||= $session{'CurrentSearchHash'}->{'Query'} ) {
+#    $tabs->{"m"} = {
+#        path  => "Tools/Reports/SLA.html?". $m->comp( '/Elements/QueryString', Query => $Query ),
+#        title => loc('Report SLA'),
+#    };
+#}
+
+#return unless $session{'CurrentUser'}->PrincipalObj->HasRight(
+#    Object => $RT::System, Right => 'SeeSLAReports',
+#);
+#$tabs->{'s'} = {
+#    title => loc('Service Level Aggreements'),
+#    path  => 'Tools/Reports/SLA.html',
+#};
+</%INIT>
\ No newline at end of file
diff --git a/html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default b/html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default
deleted file mode 100644
index 76253a0..0000000
--- a/html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default
+++ /dev/null
@@ -1,24 +0,0 @@
-<%ARGS>
-$Query => undef
-$tabs => {}
-$Ticket => undef
-</%ARGS>
-<%INIT>
-
-return unless $session{'CurrentUser'}->PrincipalObj->HasRight(
-        Object => $RT::System, Right => 'SeeSLAReports',
-);
-
-if ( $Ticket ) {
-    $tabs->{'this'}->{"subtabs"}->{'_DA'} = {
-        path  => "Ticket/SLA.html?id=". $Ticket->id,
-        title => loc('Report SLA'),
-    };
-}
-elsif ( $Query ||= $session{'CurrentSearchHash'}->{'Query'} ) {
-    $tabs->{"m"} = {
-        path  => "Tools/Reports/SLA.html?". $m->comp( '/Elements/QueryString', Query => $Query ),
-        title => loc('Report SLA'),
-    };
-}
-</%INIT>
diff --git a/html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default b/html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default
deleted file mode 100644
index d1f352c..0000000
--- a/html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default
+++ /dev/null
@@ -1,12 +0,0 @@
-<%ARGS>
-$tabs => {}
-</%ARGS>
-<%INIT>
-return unless $session{'CurrentUser'}->PrincipalObj->HasRight(
-    Object => $RT::System, Right => 'SeeSLAReports',
-);
-$tabs->{'s'} = {
-    title => loc('Service Level Aggreements'),
-    path  => 'Tools/Reports/SLA.html',
-};
-</%INIT>
diff --git a/html/Elements/SLA/ShowReportSummary b/html/Elements/SLA/ShowReportSummary
new file mode 100644
index 0000000..f0e18ff
--- /dev/null
+++ b/html/Elements/SLA/ShowReportSummary
@@ -0,0 +1,114 @@
+<em>All replies by requestors, owners and other.</em>
+
+<table class="sla">
+<tbody>
+% foreach my $role (qw(requestor owner other)) {
+% my $v = $data->{'messages'}{ $role } or next;
+<tr><th><% loc($label{ $role }) %></th><td><% $v %></td></tr>
+% }
+</tbody>
+<tfoot>
+<tr><th><% loc($label{'*'}) %></th><td><% $data->{'messages'}{'*'} %></td></tr>
+</tfoot>
+</table>
+
+% if ( my $fr = $data->{'FirstResponse'} ) {
+<em>First response</em>
+% if ( $fr->{'count'} == 1 ) {
+<table class="sla"><tr><td><% $time_interval->( $fr->{'sum'} ) %></td></tr></table>
+% }
+% else {
+<table class="sla">
+<tbody>
+<tr><th><% loc('Min') %></th><td><% $time_interval->( $fr->{'min'} ) %></td></tr>
+<tr><th><% loc('Average') %></th><td><% $time_interval->( $fr->{'avg'} ) %></td></tr>
+<tr><th><% loc('Max') %></th><td><% $time_interval->( $fr->{'max'} ) %></td></tr>
+</tbody>
+</table>
+% }
+% }
+
+% if ( $data->{'Response'} ) {
+<em>Repsonse time to requestor and from requestor</em>
+<table class="sla">
+<thead>
+    <tr>
+        <th> </th>
+        <th><% loc('Count') %></th>
+        <th><% loc('Min') %></th>
+        <th><% loc('Average') %></th>
+        <th><% loc('Max') %></th>
+        <th><% loc('Sum') %></th>
+    </tr>
+</thead>
+<%PERL>
+my $render_row = sub {
+    my $role = shift;
+    my $data = shift;
+    my $res = '<tr>';
+    $res .= '<th>'. $eh->( loc( $label{ $role } ) ) .'</th>';
+    $res .= '<td>'. $eh->( $data->{'count'} ) .'</td>';
+    if ( $data->{'count'} > 1 ) {
+        foreach (qw(min avg max sum)) {
+            $res .= '<td>'. $eh->( $time_interval->( $data->{$_} ) ) .'</td>';
+        }
+    }
+    else {
+        $res .= '<td colspan="4">'. $eh->( $time_interval->( $data->{'sum'} ) ) .'</td>';
+    }
+    $res .= '</tr>';
+    return $res;
+};
+</%PERL>
+<tbody>
+% foreach my $role (qw(requestor owner other)) {
+% my $data = $data->{'Response'}{ $role } or next;
+<% $render_row->( $role, $data ) |n %>
+% }
+</tbody>
+% if ( my $totals = $data->{'Response'}{'*'} ) {
+<tfoot><% $render_row->( '*' => $totals ) |n %></tfoot>
+% }
+</table>
+% }
+
+% if ( my $dp = $data->{'deadlines'}{'passed'} ) {
+<em>Deadlines met</em>
+<table class="sla"><tr><td><% $dp %></td></tr></table>
+% }
+
+% if ( my $failed = $data->{'deadlines'}{'failed'} ) {
+<em>Missed deadlines</em>
+<table class="sla">
+<tbody>
+<tr><th><% loc('Count') %></th><td><% $failed->{'count'} %></td></tr>
+% if ( $failed->{'count'} > 1 ) {
+<tr><th><% loc('Min') %></th><td><% $time_interval->( $failed->{'min'} ) %></td></tr>
+<tr><th><% loc('Average') %></th><td><% $time_interval->( $failed->{'avg'} ) %></td></tr>
+<tr><th><% loc('Max') %></th><td><% $time_interval->( $failed->{'max'} ) %></td></tr>
+% }
+<tr><th><% loc('Sum') %></th><td><% $time_interval->( $failed->{'sum'} ) %></td></tr>
+</tbody>
+</table>
+% }
+
+<%ONCE>
+my $eh = sub { my $v = shift; RT::Interface::Web::EscapeUTF8( \$v ); return $v };
+my $time_interval = sub {
+    return RT::Date->new( $session{'CurrentUser'} )
+        ->DurationAsString( shift );
+};
+my %label = (
+    requestor => 'Requestors',
+    owner => 'Owners',
+    other => 'Other',
+    '*'   => 'Total',
+);
+
+</%ONCE>
+<%ARGS>
+$Summary
+</%ARGS>
+<%INIT>
+my $data = $Summary->Result;
+</%INIT>
diff --git a/html/NoAuth/css/base/sla-table.css b/html/NoAuth/css/base/sla-table.css
new file mode 100644
index 0000000..cb51021
--- /dev/null
+++ b/html/NoAuth/css/base/sla-table.css
@@ -0,0 +1,29 @@
+table.sla {
+  max-width: 100%;
+  border-spacing: 0;
+  border-top: 1px solid #ddd;
+  border-right: 1px solid #ddd;
+  margin-bottom: 1em;
+  background: white;
+}
+table.sla th, table.sla td {
+  padding: 1em;
+  vertical-align: top;
+  border-left: 1px solid #ddd;
+  border-bottom: 1px solid #ddd;
+}
+table.sla td {
+  text-align: right;
+}
+table.sla tbody th, table.sla tfoot th {
+  text-align: right;
+}
+
+table.sla thead + tbody tr:first-child th,
+table.sla thead + tbody tr:first-child td,
+table.sla tbody + tbody tr:first-child th,
+table.sla tbody + tbody tr:first-child td,
+table.sla tbody + tfoot tr:first-child th,
+table.sla tbody + tfoot tr:first-child td {
+  border-top: 3px solid #ddd;
+}
diff --git a/html/Ticket/SLA.html b/html/Ticket/SLA.html
index 0cde6da..537510e 100644
--- a/html/Ticket/SLA.html
+++ b/html/Ticket/SLA.html
@@ -1,22 +1,9 @@
 <& /Elements/Header, Title => $title &>
-<& /Ticket/Elements/Tabs, 
-    Ticket => $ticket,
-    current_tab => "Ticket/SLA.html?id=$id",
-    Title => $title,
-&>
+<& /Elements/Tabs &>
 
-<table>
-<tr><th>#</th><th>Description</th><th>Type</th><th>Owner</th><th>Failed</th><th>Shift</th></tr>
-% foreach my $stat ( @{ $report->Stats } ) {
-<tr>
-<td><% $stat->{transaction}->id %></td>
-<td><% $stat->{transaction}->Description %></td>
-<td><% $stat->{owner_act}? 'yes' : 'no' %></td>
-<td><% $stat->{failed}? 'yes' : 'no' %></td>
-<td><% $stat->{shift} %></td>
-</tr>
-% }
-</table>
+<&| /Widgets/TitleBox, title => loc('Summary') &>
+<& /Elements/SLA/ShowReportSummary, Summary => $summary &>
+</&>
 
 <%ARGS>
 $id => undef
@@ -40,7 +27,6 @@ $id = $ARGS{'id'} = $ticket->id;
 my $title = loc("SLA performance on ticket #[_1]", $id);
 
 use RT::Extension::SLA;
-my $report = RT::Extension::SLA->Report( $ticket );
-use Data::Dumper;
-$RT::Logger->crit( Dumper $report );
+my $report = RT::Extension::SLA->TicketReport( $ticket );
+my $summary = $report->Summary;
 </%INIT>
diff --git a/html/Tools/Reports/SLA.html b/html/Tools/Reports/SLA.html
index ee2f6ae..038d06e 100644
--- a/html/Tools/Reports/SLA.html
+++ b/html/Tools/Reports/SLA.html
@@ -1,33 +1,24 @@
 <& /Elements/Header, Title => $title &>
-<& /Tools/Reports/Elements/Tabs, current_tab => 'Tools/Reports/SLA.html', Title => $title &>
+<& /Elements/Tabs &>
 
+<&| /Widgets/TitleBox, title => loc('Summary') &>
+<form method="post" action="SLA.html">
 <table>
-<tr>
-<th><% loc('Owner') %></th>
-% my @columns = $summary->Labels;
-% my $i = 0;
-% foreach ( map $_->[0], grep $i++%2, @columns ) {
-<th><% loc($_) %></th>
-% }
-</tr>
-
-% while ( my ($owner, $stats) = each %$result ) {
-% my $user = RT::User->new( $session{'CurrentUser'} );
-% $user->Load( $owner );
-<tr>
-<td><& /Elements/ShowUser, User => $user &></td>
-% my $i = 1;
-% foreach ( map $stats->{ $_ }, grep $i++%2, @columns ) {
-<td><% $_ || 0 %></td>
-% }
-</tr>
-% }
+    <tr>
+        <td class="label"><&|/l&>Query</&>:</td>
+        <td class="value"><textarea cols="60" rows="20" name="Query"><% $Query %></textarea></td>
+    </tr>
 </table>
 
-<form method="post" action="SLA.html">
-<&|/l&>Query</&>:<textarea cols="60" rows="20" name="Query"><% $Query %></textarea>
-<& /Elements/Submit, Label => loc('Update report') &>
+<& /Elements/Submit, Label => loc('Update') &>
 </form>
+</&>
+
+% if ( $summary ) {
+<&| /Widgets/TitleBox, title => loc('Summary') &>
+<& /Elements/SLA/ShowReportSummary, Summary => $summary &>
+</&>
+% }
 
 <%ARGS>
 $Query => undef
@@ -43,16 +34,16 @@ unless (
 
 my $title = loc("Report on Service Level Agreements");
 
-use RT::Extension::SLA::Summary;
-my $summary = new RT::Extension::SLA::Summary;
-
-my $tickets = RT::Tickets->new( $session{'CurrentUser'} );
-$tickets->FromSQL( $Query );
-$tickets->OrderByCols( {FIELD => 'id', ORDER => 'ASC'} );
-while ( my $ticket = $tickets->Next ) {
-    my $report = RT::Extension::SLA->Report( $ticket );
-    $summary->AddReport( $report );
+my $summary;
+if ( $Query ) {
+    $summary = new RT::Extension::SLA::Summary;
+
+    my $tickets = RT::Tickets->new( $session{'CurrentUser'} );
+    $tickets->FromSQL( $Query );
+    $tickets->OrderByCols( {FIELD => 'id', ORDER => 'ASC'} );
+    while ( my $ticket = $tickets->Next ) {
+        $summary->AddReport( RT::Extension::SLA->TicketReport( $ticket ) );
+    }
+    $summary->Finalize;
 }
-
-my $result = $summary->Result;
 </%INIT>
diff --git a/lib/RT/Extension/SLA.pm b/lib/RT/Extension/SLA.pm
index 88c4602..a506e0b 100644
--- a/lib/RT/Extension/SLA.pm
+++ b/lib/RT/Extension/SLA.pm
@@ -6,6 +6,8 @@ package RT::Extension::SLA;
 
 our $VERSION = '0.08_01';
 
+use RT::Extension::SLA::Report;
+
 =head1 NAME
 
 RT::Extension::SLA - Service Level Agreements for RT
@@ -389,6 +391,8 @@ Just grant them ModifyCustomField right.
 
 =cut
 
+push @{ scalar RT->Config->Get('CSSFiles') }, 'base/sla-table.css';
+
 {
     my $right = 'SeeSLAReports';
     use RT::System;
@@ -466,6 +470,22 @@ sub Due {
     return $self->CalculateTime( @_ );
 }
 
+sub SecondsBetween {
+    my $self = shift;
+    my %args = ( Level => undef, From => undef, To => undef, @_);
+    my ($from, $to) = @args{'From', 'To'};
+
+    my $sign = 1;
+    if ( $from > $to ) {
+        $sign = -1;
+        ($from, $to) = ($to, $from);
+    }
+
+    return $sign * ( $self->BusinessHours(
+        $RT::ServiceAgreements{'Levels'}{ $args{'Level'} }{'BusinessHours'}
+    )->between( $from, $to ) - 1 );
+}
+
 sub Starts {
     my $self = shift;
     return $self->CalculateTime( @_, Type => 'Starts' );
@@ -558,7 +578,7 @@ sub GetDefaultServiceLevel {
     return $RT::ServiceAgreements{'Default'};
 }
 
-sub Report {
+sub TicketReport {
     my $self = shift;
     my $ticket = shift;
 
diff --git a/lib/RT/Extension/SLA/Report.pm b/lib/RT/Extension/SLA/Report.pm
index 94290cd..5ce96a8 100644
--- a/lib/RT/Extension/SLA/Report.pm
+++ b/lib/RT/Extension/SLA/Report.pm
@@ -13,7 +13,7 @@ sub new {
 sub init {
     my $self = shift;
     my %args = (Ticket => undef, @_);
-    $self->{'Ticket'} = $args{'Ticket'} || die "boo";
+    $self->{'Ticket'} = $args{'Ticket'} || die Carp::longmess( "boo" );
     $self->{'State'} = {};
     $self->{'Stats'} = [];
     return $self;
@@ -23,11 +23,11 @@ sub Run {
     my $self = shift;
     my $txns = shift || $self->{'Ticket'}->Transactions;
 
-    my $state = $self->State;
     my $handler = $self->Handlers;
 
     while ( my $txn = $txns->Next ) {
         my ($type, $field) = ($txn->Type, $txn->Field);
+        $_ ||= '' foreach $type, $field;
 
         my $h = $handler->{ $type };
         unless ( $h ) {
@@ -41,19 +41,25 @@ sub Run {
 
         $RT::Logger->debug( "Handling transaction #". $txn->id ." ($type, $field) of ticket #". $self->{'Ticket'}->id );
 
-        $self->$h( Ticket => $self->{'Ticket'}, Transaction => $txn, State => $state );
+        $self->$h( Ticket => $self->{'Ticket'}, Transaction => $txn );
     }
     return $self;
 }
 
 sub State {
     my $self = shift;
-    return $self->{State};
+    return $self->{State} ||= {};
 }
 
 sub Stats {
     my $self = shift;
-    return $self->{Stats};
+    return $self->{Stats} ||= [];
+}
+
+sub Summary {
+    my $self = shift;
+    use RT::Extension::SLA::Summary;
+    return RT::Extension::SLA::Summary->new->AddReport( $self )->Finalize;
 }
 
 { my $cache;
@@ -61,7 +67,7 @@ sub Handlers {
     my $self = shift;
 
     return $cache if $cache;
-    
+
     $cache = {
         Create => 'OnCreate',
         Set    => {
@@ -78,9 +84,9 @@ sub Handlers {
 
 sub OnCreate {
     my $self = shift;
-    my %args = ( Ticket => undef, Transaction => undef, State => undef, @_);
+    my %args = ( Ticket => undef, Transaction => undef, @_);
 
-    my $state = $args{'State'};
+    my $state = $self->State;
     %$state = ();
     $state->{'level'} = $self->InitialServiceLevel( Ticket => $args{'Ticket'} );
     $state->{'requestors'} = [ $self->InitialRequestors( Ticket => $args{'Ticket'} ) ];
@@ -90,9 +96,9 @@ sub OnCreate {
 
 sub OnRequestorChange {
     my $self = shift;
-    my %args = ( Ticket => undef, Transaction => undef, State => undef, @_);
+    my %args = ( Ticket => undef, Transaction => undef, @_);
 
-    my $requestors = $self->State->{'requestors'};
+    my $requestors = $self->State->{'requestors'} ||= [];
     if ( $args{'Transaction'}->Type eq 'AddWatcher' ) {
         push @$requestors, $args{'Transaction'}->NewValue;
     }
@@ -104,89 +110,78 @@ sub OnRequestorChange {
 
 sub OnServiceLevelChange {
     my $self = shift;
-    my %args = ( Ticket => undef, Transaction => undef, State => undef, @_);
+    my %args = ( Transaction => undef, @_);
     $self->State->{'level'} = $args{'Transaction'}->NewValue;
 }
 
 sub OnResponse {
     my $self = shift;
-    my %args = ( Ticket => undef, Transaction => undef, State => undef, @_);
+    my %args = ( Transaction => undef, Create => 0, @_);
 
+    my $state = $self->State;
     my $txn = $args{'Transaction'};
-#    unless ( $args{'State'}->{'level'} ) {
-#        $RT::Logger->debug('No service level -> ignore txn #'. $txn->id );
-#        return;
-#    }
-
-    my $act = $args{'State'}->{'act'};
-    if ( $self->IsRequestorsAct( $txn ) ) {
-        if ( $act && $act->{'requestor'} ) {
-            # several requestors' acts in a row don't move deadlines
-            return;
-        }
-        $act ||= $args{'State'}->{'act'} = {};
-
-        $act->{'requestor'} = 1;
-        $act->{'acted'} = $txn->CreatedObj->Unix;
-    } else {
-        unless ( $act ) {
-            $act = $args{'State'}->{'act'} = {};
-            $act->{'requestor'} = 0;
-            $act->{'acted'} = $txn->CreatedObj->Unix;
-            return;
-        }
-        unless ( $act->{'requestor'} ) {
-            # check keep in loop
-            my $deadline = RT::Extension::SLA->Due(
-                Type  => 'KeepInLoop',
-                Level => $args{'State'}->{'level'},
-                Time  => $args{'State'}->{'acted'},
-            );
-            unless ( defined $deadline ) {
-                $RT::Logger->debug( "Multiple non-requestors replies in a raw, without keep in loop deadline");
-                return;
+
+    my %stats = (
+        transaction => $txn->id,
+        owner       => $state->{'owner'},
+        actor       => $txn->Creator,
+        actor_role  =>
+            $self->IsRequestorsAct( $txn ) ? 'requestor'
+            : $state->{'owner'} == $txn->Creator ? 'owner'
+            : 'other'
+        ,
+        acted_on    => $txn->CreatedObj->Unix,
+        previous    => $self->Stats->[-1],
+    );
+
+    unless ( $stats{'previous'} ) {
+        $stats{'type'} = 'Create';
+    }
+    elsif ( $stats{'actor_role'} eq 'requestor' ) {
+        if ( $stats{'previous'}{'actor_role'} eq 'requestor' ) {
+            $stats{'type'} = 'FollowUp';
+            $stats{'to'} = $stats{'previous'};
+        } else {
+            $stats{'type'} = 'Response';
+            my $tmp = $stats{'previous'};
+            while ( $tmp->{'previous'} && $tmp->{'previous'}{'actor_role'} ne 'requestor' ) {
+                $tmp = $tmp->{'previous'};
             }
-            # keep in loop
-            my $failed = $txn->CreatedObj->Unix > $deadline? 1 : 0;
-            my $owner = $args{'State'}->{'owner'} == $txn->Creator? 1 : 0;
-            my $stat = {
-                type        => 'KeepInLoop',
-                owner       => $args{'State'}->{'owner'},
-                failed      => $failed,
-                owner_act   => $owner,
-                transaction => $txn,
-                actor       => $txn->Creator,
-                shift       => $txn->CreatedObj->Unix - $deadline,                
-            };
-            push @{ $self->Stats }, $stat;
+            $stats{'to'} = $tmp;
         }
-        else {
-            # check response
-            my $deadline = RT::Extension::SLA->Due(
-                Type  => 'Response',
-                Level => $args{'State'}->{'level'},
-                Time  => $args{'State'}->{'act'}->{'acted'},
-            );
-            unless ( defined $deadline ) {
-                $RT::Logger->debug( "Non-requestors' reply after requestors', without response deadline");
-                return;
+    }
+    else {
+        if ( $stats{'previous'}{'actor_role'} ne 'requestor' ) {
+            $stats{'type'} = 'KeepInLoop';
+            $stats{'to'} = $stats{'previous'};
+        } else {
+            $stats{'type'} = 'Response';
+            my $tmp = $stats{'previous'};
+            while ( $tmp->{'previous'} && $tmp->{'previous'}{'actor_role'} eq 'requestor' ) {
+                $tmp = $tmp->{'previous'};
             }
-
-            # repsonse
-            my $failed = $txn->CreatedObj->Unix > $deadline? 1 : 0;
-            my $owner = $args{'State'}->{'owner'} == $txn->Creator? 1 : 0;
-            my $stat = {
-                type        => 'Response',
-                owner       => $args{'State'}->{'owner'},
-                failed      => $failed,
-                owner_act   => $owner,
-                transaction => $txn,
-                actor       => $txn->Creator,
-                shift       => ($txn->CreatedObj->Unix - $deadline),
-            };
-            push @{ $self->Stats }, $stat;
+            $stats{'to'} = $tmp;
         }
+
+        $stats{'deadline'} = RT::Extension::SLA->Due(
+            Type  => $stats{'type'},
+            Level => $state->{'level'},
+            Time  => $stats{'to'}{'acted_on'},
+        );
+        $stats{'difference'} = RT::Extension::SLA->SecondsBetween(
+            Level => $state->{'level'},
+            From  => $stats{'deadline'},
+            To    => $stats{'acted_on'},
+        ) if defined $stats{'deadline'};
     }
+
+    $stats{'time'} = RT::Extension::SLA->SecondsBetween(
+        Level => $state->{'level'},
+        From  => $stats{'to'}{'acted_on'},
+        To    => $stats{'acted_on'},
+    ) if $stats{'to'};
+
+    push @{ $self->Stats }, \%stats;
 }
 
 sub IsRequestorsAct {
@@ -197,10 +192,10 @@ sub IsRequestorsAct {
 
     # owner is always treated as non-requestor
     return 0 if $actor == $self->State->{'owner'};
-    return 1 if grep $_ == $actor, @{ $self->State->{'requestors'} };
+    return 1 if grep $_ == $actor, @{ $self->State->{'requestors'} || [] };
 
     # in case requestor is a group
-    foreach my $id ( @{ $self->State->{'requestors'} } ){
+    foreach my $id ( @{ $self->State->{'requestors'} || [] } ){
         my $cgm = RT::CachedGroupMember->new( $RT::SystemUser );
         $cgm->LoadByCols( GroupId => $id, MemberId => $actor, Disabled => 0 );
         return 1 if $cgm->id;
@@ -271,7 +266,8 @@ sub Transactions {
     my $self = shift;
     my %args = (Ticket => undef, Criteria => undef, Order => 'ASC', @_);
 
-    my $txns = $args{'Ticket'}->Transactions;
+    my $txns = RT::Transactions->new( $args{'Ticket'}->CurrentUser );
+    $txns->LimitToTicket( $args{'Ticket'}->id );
 
     my $clause = 'ByTypeAndField';
     while ( my ($type, $field) = each %{ $args{'Criteria'} } ) {
diff --git a/lib/RT/Extension/SLA/Summary.pm b/lib/RT/Extension/SLA/Summary.pm
index a0dc52f..112f42a 100644
--- a/lib/RT/Extension/SLA/Summary.pm
+++ b/lib/RT/Extension/SLA/Summary.pm
@@ -20,61 +20,132 @@ sub Result {
     return $self->{'Result'} ||= { };
 }
 
-our @known_stats = (
-    'passed' => ['Passed', 'Replied before a deadline'],
-    'failed' => ['Failed', 'Replied after a deadline or not replied at all'],
-    'helped' => ['Helped', 'Helped another user to reach a deadline'],
-    'late help' => ['Helped (late)', 'Helped another user, however failed a deadline'],
-    'got help'  => ['Got help', 'Got help from another user within a deadline'],
-);
-
-sub Labels {
-    return @known_stats;
-}
-
 sub AddReport {
     my $self = shift;
     my $report = shift;
 
     my $new = $self->OnReport( $report );
+    return $self->MergeResults( $new ) if keys %{ $self->Result };
+    %{ $self->Result } = %$new;
+    return $self;
+}
 
-    my $total = $self->Result;
-    while ( my ($user, $stat) = each %$new ) {
-        my $tmp = $total->{$user} ||= {};
-        while ( my ($action, $count) = each %$stat ) {
-            $tmp->{$action} += $count;
-        }
-    }
+sub Finalize {
+    my $self = shift;
+
+    my $res = $self->Result;
 
+    $res->{'messages'}{'*'} += $_ foreach values %{ $res->{'messages'} };
+
+    foreach my $type ( grep $res->{$_}, qw(KeepInLoop FollowUp Response) ) {
+        $self->MergeCountMinMaxSum( $_ => $res->{$type}{'*'} ||= {} )
+            foreach values %{ $res->{$type} };
+        $_->{'avg'} = $_->{'sum'}/$_->{'count'}
+            foreach grep $_->{'count'}, values %{ $res->{$type} };
+    }
+    foreach ( grep $_, $res->{'FirstResponse'}, $res->{'deadlines'}{'failed'} ) {
+        $_->{'avg'} = $_->{'sum'}/$_->{'count'};
+    }
     return $self;
 }
 
+# min, avg, max - initial response time
+# min, avg, max - response time
+# number of passed
+# number of failed
+# min, avg, max - past due time
+# responses by role
+
 sub OnReport {
     my $self = shift;
     my $report = shift;
 
-    my $res = {};
+    my %res;
     foreach my $stat ( @{ $report->Stats } ) {
-        if ( $stat->{'owner_act'} ) {
-            my $owner = $res->{ $stat->{'owner'} } ||= { };
-            if ( $stat->{'failed'} ) {
-                $owner->{'failed'}++;
-            } else {
-                $owner->{'passed'}++;
+        $res{'messages'}{ $stat->{'actor_role'} }++;
+
+        $self->CountMinMaxSum(
+            $res{ $stat->{'type'} }{ $stat->{'actor_role'} } ||= {},
+            $stat->{'time'},
+        ) if $stat->{'time'};
+
+        if ( $stat->{'deadline'} ) {
+            if ( $stat->{'difference'} > 0 ) {
+                $self->CountMinMaxSum(
+                    $res{'deadlines'}{'failed'} ||= {},
+                    $stat->{'difference'},
+                );
             }
-        } else {
-            my $owner = $res->{ $stat->{'owner'} } ||= { };
-            my $actor = $res->{ $stat->{'actor'} } ||= { };
-            if ( $stat->{'failed'} ) {
-                $owner->{'failed'}++;
-                $actor->{'late help'}++;
+            else {
+                $res{'deadlines'}{'passed'}++;
+            }
+        }
+    }
+
+    if ( $report->Stats->[0]{'actor_role'} eq 'requestor' ) {
+        my ($first_response) = (grep $_->{'actor_role'} ne 'requestor', @{ $report->Stats });
+        $self->CountMinMaxSum(
+            $res{'FirstResponse'} ||= {},
+            $first_response->{'time'},
+        ) if $first_response;
+    }
+
+    return \%res;
+}
+
+sub MergeResults {
+    my $self = shift;
+    my $src = shift;
+    my $dst = shift || $self->Result;
+
+
+    while ( my ($k, $v) = each %$src ) {
+        unless ( ref $v ) {
+            $dst->{$k} += $v;
+        }
+        elsif ( ref $v eq 'HASH' ) {
+            if ( exists $v->{'count'} ) {
+                $self->MergeCountMinMaxSum( $src, $dst );
+                $self->MergeResults(
+                    { map { $_ => $v->{$_} } grep !/^(?:count|min|max|sum)$/, keys %$v  },
+                    $dst->{ $k }
+                );
             } else {
-                $owner->{'got help'}++;
-                $actor->{'helped'}++;
+                $self->MergeResults( $v, $dst->{$k} );
             }
         }
+        else {
+            die "Don't know how to merge";
+        }
     }
-    return $res;
+    return $self;
+}
+
+sub CountMinMaxSum {
+    my $self = shift;
+    my $hash = shift || {};
+    my $value = shift;
+
+    $hash->{'count'}++;
+    $hash->{'min'} = $value if !defined $hash->{'min'} || $hash->{'min'} > $value;
+    $hash->{'max'} = $value if !defined $hash->{'max'} || $hash->{'max'} < $value;
+    $hash->{'sum'} += $value;
+    return $hash;
+}
+
+sub MergeCountMinMaxSum {
+    my $self = shift;
+    my $src = shift || {};
+    my $dst = shift;
+
+    $dst->{'count'} += $src->{'count'};
+    $dst->{'min'} = $src->{'min'}
+        if !defined $dst->{'min'} || $dst->{'min'} > $src->{'min'};
+    $dst->{'max'} = $src->{'max'}
+        if !defined $dst->{'max'} || $dst->{'max'} < $src->{'max'};
+    $dst->{'sum'} += $src->{'sum'};
+
+    return $self;
 }
 
 1;
diff --git a/t/reporting/basic.t b/t/reporting/basic.t
index f152130..b4c3e8b 100644
--- a/t/reporting/basic.t
+++ b/t/reporting/basic.t
@@ -4,14 +4,18 @@ use strict;
 use warnings;
 use Test::MockTime qw(set_fixed_time);
 
-use RT::Extension::SLA::Test tests => 6;
+use RT::Extension::SLA::Test tests => 17;
 
 use_ok 'RT::Extension::SLA::Report';
 
+use Data::Dumper;
+
 my $root = RT::User->new( $RT::SystemUser );
 $root->LoadByEmail('root at localhost');
 ok $root->id, 'loaded root user';
 
+my $hour = 60*60;
+
 diag '';
 {
     %RT::ServiceAgreements = (
@@ -29,8 +33,8 @@ diag '';
     my $id;
     {
         my $ticket = RT::Ticket->new( $root );
-        ($id) = $ticket->Create( Queue => 'General', Subject => 'xxx', Requestor => $root->id );
-        ok $id, "created ticket #$id";
+        ($id, undef, my $msg) = $ticket->Create( Queue => 'General', Subject => 'xxx', Requestor => $root->id );
+        ok $id, "created ticket #$id" or diag "error: $msg";
 
         is $ticket->FirstCustomFieldValue('SLA'), '2', 'default sla';
 
@@ -50,15 +54,56 @@ diag '';
 
     my $ticket = RT::Ticket->new( $RT::SystemUser );
     $ticket->Load( $id );
-    my $report = RT::Extension::SLA::Report->new( Ticket => $ticket )->Run;
-    is_deeply $report->Stats,
-        [ {type => 'Response', owner => $RT::Nobody->id, owner_act => 0, failed => 0, shift => -3600 } ],
-        'correct stats'
-    ;
+    test_ticket_report(
+        $ticket,
+        [
+          {
+            'previous' => undef,
+            'owner' => 6,
+            'actor_role' => 'requestor',
+            'transaction' => '24',
+            'type' => 'Create',
+            'acted_on' => 1241517600,
+            'actor' => '12',
+          },
+          {
+            'owner' => 6,
+            'deadline' => 1241524800,
+            'difference' => - $hour,
+            'actor' => '1',
+            'previous' => -1,
+            'to' => -1,
+            'time' => $hour,
+            'actor_role' => 'other',
+            'transaction' => '29',
+            'type' => 'Response',
+            'acted_on' => 1241521200
+          }
+        ],
+        {
+          'messages' => { '*' => 2, 'other' => 1, 'requestor' => 1, },
+          'Response' => {
+            'other' => {
+                'count' => 1,
+                'min' => $hour, 'avg' => $hour, 'max' => $hour,
+                'sum' => $hour,
+            },
+            '*' => {
+                'count' => 1,
+                'min' => $hour, 'avg' => $hour, 'max' => $hour,
+                'sum' => $hour,
+            },
+          },
+          'FirstResponse' => {
+                'count' => 1,
+                'min' => $hour, 'avg' => $hour, 'max' => $hour,
+                'sum' => $hour,
+          },
+          'deadlines' => { 'passed' => 1, failed => undef },
+        },
+    );
 }
 
-
-diag '';
 {
     %RT::ServiceAgreements = (
         Default => '2',
@@ -75,8 +120,8 @@ diag '';
     my $id;
     {
         my $ticket = RT::Ticket->new( $root );
-        ($id) = $ticket->Create( Queue => 'General', Subject => 'xxx', Requestor => $root->id );
-        ok $id, "created ticket #$id";
+        ($id, undef, my $msg) = $ticket->Create( Queue => 'General', Subject => 'xxx', Requestor => $root->id );
+        ok $id, "created ticket #$id" or diag "error: $msg";
 
         is $ticket->FirstCustomFieldValue('SLA'), '2', 'default sla';
 
@@ -84,7 +129,7 @@ diag '';
         is $due, $time + 2*60*60, 'Due date is two hours from "now"';
     }
 
-    set_fixed_time('2009-05-05T11:00:00Z');
+    set_fixed_time('2009-05-05T13:00:00Z');
 
     # non-requestor reply
     {
@@ -96,11 +141,75 @@ diag '';
 
     my $ticket = RT::Ticket->new( $RT::SystemUser );
     $ticket->Load( $id );
-    my $report = RT::Extension::SLA::Report->new( Ticket => $ticket )->Run;
-    is_deeply $report->Stats,
-        [ {type => 'Response', owner => $RT::Nobody->id, owner_act => 0, failed => 0, shift => -3600 } ],
-        'correct stats'
-    ;
+    test_ticket_report(
+        $ticket,
+        [
+          {
+            'previous' => undef,
+            'owner' => 6,
+            'actor_role' => 'requestor',
+            'transaction' => '37',
+            'type' => 'Create',
+            'acted_on' => 1241517600,
+            'actor' => '12',
+          },
+          {
+            'owner' => 6,
+            'deadline' => 1241524800,
+            'difference' => $hour,
+            'actor' => '1',
+            'previous' => -1,
+            'to' => -1,
+            'time' => 3*$hour,
+            'actor_role' => 'other',
+            'transaction' => '42',
+            'type' => 'Response',
+            'acted_on' => 1241528400,
+          }
+        ],
+        {
+          'messages' => { '*' => 2, 'other' => 1, 'requestor' => 1, },
+          'Response' => {
+            'other' => {
+                'count' => 1,
+                'min' => 3*$hour, 'avg' => 3*$hour, 'max' => 3*$hour,
+                'sum' => 3*$hour,
+            },
+            '*' => {
+                'count' => 1,
+                'min' => 3*$hour, 'avg' => 3*$hour, 'max' => 3*$hour,
+                'sum' => 3*$hour,
+            },
+          },
+          'FirstResponse' => {
+                'count' => 1,
+                'min' => 3*$hour, 'avg' => 3*$hour, 'max' => 3*$hour,
+                'sum' => 3*$hour,
+          },
+          'deadlines' => { failed => {
+                'count' => 1,
+                'min' => $hour, 'avg' => $hour, 'max' => $hour,
+                'sum' => $hour,
+          } },
+        },
+    );
 }
 
+sub test_ticket_report {
+    my ($ticket, $exp_report, $exp_summary) = @_;
+
+    for ( my $i = 0; $i < @$exp_report; $i++ ) {
+        foreach ( grep $exp_report->[$i]{$_}, qw(to previous) ) {
+            $exp_report->[$i]{$_} = $exp_report->[ $i + $exp_report->[$i]{$_} ];
+        }
+    }
+
+    my $report = RT::Extension::SLA::Report->new( Ticket => $ticket )->Run;
+    is_deeply( $report->Stats, $exp_report, 'correct stats' )
+        or diag Dumper( $report->Stats );
+
+    my $summary = $report->Summary;
+    is_deeply( $summary->Result, $exp_summary, 'correct summary' )
+        or diag Dumper( $summary->Result );
+}
 

commit 411a8b6934730ca9266aa45c1dd4c324872a1b22
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Mar 13 02:05:00 2012 +0400

    make sure $field is empty before we fall into if block
    
    Open/Close paren don't work well when there is no limit
    call in between.

diff --git a/lib/RT/Extension/SLA/Report.pm b/lib/RT/Extension/SLA/Report.pm
index 5ce96a8..f15ea7a 100644
--- a/lib/RT/Extension/SLA/Report.pm
+++ b/lib/RT/Extension/SLA/Report.pm
@@ -278,11 +278,12 @@ sub Transactions {
             FIELD           => 'Type',
             VALUE           => $type,
         );
-        if ( $field ) {
-            my $tmp = ref $field? $field : [$field];
+
+        my @fields = (grep defined && length, ref $field? @$field : ($field));
+        if ( @fields ) {
             $txns->_OpenParen( $clause );
             my $first = 1;
-            foreach my $value ( @$tmp ) {
+            foreach my $value ( @fields ) {
                 $txns->Limit(
                     SUBCLAUSE       => $clause,
                     ENTRYAGGREGATOR => $first? 'AND' : 'OR',

commit 3cf062673a927d519eea98a1491ccc743f4ad513
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Mar 13 02:43:57 2012 +0400

    fix merging results
    
    we were passing incorrect values into MergeCountMinMaxSum

diff --git a/lib/RT/Extension/SLA/Summary.pm b/lib/RT/Extension/SLA/Summary.pm
index 112f42a..3b0cfaf 100644
--- a/lib/RT/Extension/SLA/Summary.pm
+++ b/lib/RT/Extension/SLA/Summary.pm
@@ -99,13 +99,16 @@ sub MergeResults {
     my $dst = shift || $self->Result;
 
 
-    while ( my ($k, $v) = each %$src ) {
+    foreach my $k ( keys %$src ) {
+        my $v = $src->{ $k };
+
         unless ( ref $v ) {
             $dst->{$k} += $v;
         }
         elsif ( ref $v eq 'HASH' ) {
+            $dst->{$k} ||= {};
             if ( exists $v->{'count'} ) {
-                $self->MergeCountMinMaxSum( $src, $dst );
+                $self->MergeCountMinMaxSum( $v, $dst->{$k} );
                 $self->MergeResults(
                     { map { $_ => $v->{$_} } grep !/^(?:count|min|max|sum)$/, keys %$v  },
                     $dst->{ $k }

commit b72ba23f85183397d13da66a86cd93cf506e94dd
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Mar 13 02:45:21 2012 +0400

    protect all menu items with right check

diff --git a/html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged b/html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged
index 5bbcc46..6fc19d2 100644
--- a/html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged
+++ b/html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged
@@ -1,5 +1,9 @@
 <%INIT>
 
+return unless $session{'CurrentUser'}->PrincipalObj->HasRight(
+    Object => $RT::System, Right => 'SeeSLAReports',
+);
+
 my $request_path = $HTML::Mason::Commands::r->path_info;
 
 if ( $request_path =~ m{^/Ticket/} ) {

commit a5093d18ee53038dcfa48efea4c3359cb4299676
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Mar 13 02:45:53 2012 +0400

    put menu under Tools and Feeds

diff --git a/html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged b/html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged
index 6fc19d2..4b1dab6 100644
--- a/html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged
+++ b/html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged
@@ -21,18 +21,18 @@ if ( $request_path =~ m{^/Ticket/} ) {
     }
 }
 
-#elsif ( $Query ||= $session{'CurrentSearchHash'}->{'Query'} ) {
-#    $tabs->{"m"} = {
-#        path  => "Tools/Reports/SLA.html?". $m->comp( '/Elements/QueryString', Query => $Query ),
-#        title => loc('Report SLA'),
-#    };
-#}
+my $Query = $m->request_args->{'Query'} || $session{"CurrentSearchHash"}{'Query'};
+if ( $Query && PageMenu->child('more') ) {
+    PageMenu->child('more')->child(
+        sla =>
+        path => "/Tools/Reports/SLA.html?". $m->comp( '/Elements/QueryString', Query => $Query ),
+        title => loc('SLA Report'),
+    );
+}
 
-#return unless $session{'CurrentUser'}->PrincipalObj->HasRight(
-#    Object => $RT::System, Right => 'SeeSLAReports',
-#);
-#$tabs->{'s'} = {
-#    title => loc('Service Level Aggreements'),
-#    path  => 'Tools/Reports/SLA.html',
-#};
+Menu->child('tools')->child(
+    sla =>
+    path => "/Tools/Reports/SLA.html",
+    title => loc('SLA Report'),
+);
 </%INIT>
\ No newline at end of file

commit d4bd1c16eb309d2dffef40d761e907ba0075d89d
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Mar 13 02:46:40 2012 +0400

    incorrect box heading

diff --git a/html/Tools/Reports/SLA.html b/html/Tools/Reports/SLA.html
index 038d06e..360db0e 100644
--- a/html/Tools/Reports/SLA.html
+++ b/html/Tools/Reports/SLA.html
@@ -1,7 +1,7 @@
 <& /Elements/Header, Title => $title &>
 <& /Elements/Tabs &>
 
-<&| /Widgets/TitleBox, title => loc('Summary') &>
+<&| /Widgets/TitleBox, title => loc('Properties') &>
 <form method="post" action="SLA.html">
 <table>
     <tr>

commit cc493830df991602bf8565852365dd4240104312
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Mar 13 02:46:59 2012 +0400

    make it clear that query is required field

diff --git a/html/Tools/Reports/SLA.html b/html/Tools/Reports/SLA.html
index 360db0e..dddc476 100644
--- a/html/Tools/Reports/SLA.html
+++ b/html/Tools/Reports/SLA.html
@@ -5,7 +5,7 @@
 <form method="post" action="SLA.html">
 <table>
     <tr>
-        <td class="label"><&|/l&>Query</&>:</td>
+        <td class="label"><&|/l&>Query</&><&|/l&>(required)</&>:</td>
         <td class="value"><textarea cols="60" rows="20" name="Query"><% $Query %></textarea></td>
     </tr>
 </table>

commit 4eecfe0b41731dc726a3468f8509c7862fd27b1a
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Mar 13 03:11:49 2012 +0400

    update POD

diff --git a/lib/RT/Extension/SLA.pm b/lib/RT/Extension/SLA.pm
index a506e0b..d99292b 100644
--- a/lib/RT/Extension/SLA.pm
+++ b/lib/RT/Extension/SLA.pm
@@ -389,6 +389,41 @@ to then grant SeeCustomField right.
 You may want to allow customers or managers to escalate thier tickets.
 Just grant them ModifyCustomField right.
 
+=head1 REPORTING
+
+Since version 0.06 extension supports reporting. It works only with RT
+4.0+. Reports accessible in the UI. Each ticket has 'SLA Report' element
+in the page menu under 'Actions'. Search results also has element in the
+menu under 'Feeds'. Also, 'SLA Reports' are under 'Tools' in the main
+menu. This interface is protected by a new right 'SeeSLAReports'.
+
+For purpose of statistics actors are splitted into three groups: requestors,
+owner and other. Any user who was not requestor or owner at the moment is
+assigned to 'other group'.
+
+All time intervals are calculated in business hours.
+
+The following statistics are collected:
+
+=over 4
+
+=item * count of replies splitted by above groups
+
+=item * first response to requestor
+
+Tickets where first act was not by requestor are ignored. This happens
+when somebody on the staff created ticket for client.
+
+=item * response times
+
+=item * number of deadlines met
+
+=item * failed deadlines and timing of such
+
+=back
+
+Note that changing configuration changes stats.
+
 =cut
 
 push @{ scalar RT->Config->Get('CSSFiles') }, 'base/sla-table.css';

commit 831e145a27cc24b01a8ef9d012b9950428e4ae02
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Mar 13 03:13:43 2012 +0400

    generate README from POD

diff --git a/README b/README
index 99d637a..5e33ad2 100644
--- a/README
+++ b/README
@@ -332,6 +332,36 @@ CONFIGURATION
     You may want to allow customers or managers to escalate thier tickets.
     Just grant them ModifyCustomField right.
 
+REPORTING
+    Since version 0.06 extension supports reporting. It works only with RT
+    4.0+. Reports accessible in the UI. Each ticket has 'SLA Report' element
+    in the page menu under 'Actions'. Search results also has element in the
+    menu under 'Feeds'. Also, 'SLA Reports' are under 'Tools' in the main
+    menu. This interface is protected by a new right 'SeeSLAReports'.
+
+    For purpose of statistics actors are splitted into three groups:
+    requestors, owner and other. Any user who was not requestor or owner at
+    the moment is assigned to 'other group'.
+
+    All time intervals are calculated in business hours.
+
+    The following statistics are collected:
+
+    *   count of replies splitted by above groups
+
+    *   first response to requestor
+
+        Tickets where first act was not by requestor are ignored. This
+        happens when somebody on the staff created ticket for client.
+
+    *   response times
+
+    *   number of deadlines met
+
+    *   failed deadlines and timing of such
+
+    Note that changing configuration changes stats.
+
 TODO and CAVEATS
         * [not implemented] KeepInLoop and Response deadlines need adjusting. For example
           KeepInLoop is 2h and Response is 2h as well. Owner replies at point 0, deadline

commit d8e9eca12b1c57b94cc5aa1b1d029e328596cf19
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Mar 13 03:15:02 2012 +0400

    update manifest

diff --git a/MANIFEST b/MANIFEST
index 8828e78..d84cd05 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,8 +1,11 @@
 Changes
 etc/initialdata
 etc/upgrade/0.06/content
+html/Callbacks/RT-Extension-SLA/Elements/Tabs/Privileged
 html/Callbacks/RT-Extension-SLA/Ticket/Elements/Tabs/Default
 html/Callbacks/RT-Extension-SLA/Tools/Reports/Elements/Tabs/Default
+html/Elements/SLA/ShowReportSummary
+html/NoAuth/css/base/sla-table.css
 html/Ticket/SLA.html
 html/Tools/Reports/SLA.html
 inc/Module/AutoInstall.pm

commit febab40de51e0f2468943001d14d672f5240cbac
Author: Kevin Falcone <falcone at bestpractical.com>
Date:   Sat Jun 30 00:15:57 2012 -0400

    There was code to handle owner changes, but no method
    
    This stubs in some code to swap the internal Owner, so you can tell if a
    correspondence was from the owner.

diff --git a/lib/RT/Extension/SLA/Report.pm b/lib/RT/Extension/SLA/Report.pm
index f15ea7a..d69bfe9 100644
--- a/lib/RT/Extension/SLA/Report.pm
+++ b/lib/RT/Extension/SLA/Report.pm
@@ -108,6 +108,13 @@ sub OnRequestorChange {
     }
 }
 
+sub OnOwnerChange {
+    my $self = shift;
+    my %args = ( Ticket => undef, Transaction => undef, @_);
+
+    $self->State->{'owner'} = $args{'Transaction'}->NewValue;
+}
+
 sub OnServiceLevelChange {
     my $self = shift;
     my %args = ( Transaction => undef, @_);

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list