[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