[Rt-commit] rt branch, 4.2/date-time-improvements-in-charts, updated. rt-3.8.10-53-gf1c6bf7

Ruslan Zakirov ruz at bestpractical.com
Fri Jun 10 11:04:44 EDT 2011


The branch, 4.2/date-time-improvements-in-charts has been updated
       via  f1c6bf744003ef6031481fd5b971b60a436b1679 (commit)
       via  673366c76fff0ba0a1dd3b1a65dc4ee55e2f2c1b (commit)
       via  2f4f4058c4d14fe052c82130ec789f336f773909 (commit)
       via  2c714a81ab4c824824631f9005750c23c5881280 (commit)
       via  98eb23d9a6d946379d0d28ab16647e60c18687a1 (commit)
       via  6ad4ee3ec3265f72934a77d4e011719ed1b8b35c (commit)
       via  41090efbc2391813c6abcec7d1a45c1fd95f5712 (commit)
       via  6d6d13e631c9eea4a9af0dad01b2f07ae8a83baa (commit)
       via  1407a419cf953346e0c6c502a2460f46f9a49f6b (commit)
       via  aba84d0b4f128231a3b7c9bd1cb9a8912a0a1266 (commit)
       via  89a4fa6d4d24711b3d14e2f27056bab3034a9632 (commit)
       via  e161edcb5a05a5c2b6059f8687e0ab1912be6bc9 (commit)
       via  4ba737d0d8bb70803988f7af6aa1deacb42c0049 (commit)
       via  80a22510ceb252eefda3ad86b762d466a2d42924 (commit)
       via  065633d70d0dde0b7873974df1dfc13039a067a2 (commit)
       via  df27e033c1f4165135c79144b80274d80f6d72e2 (commit)
       via  c8980af307b6c675dee917153bfce4b3556c3f83 (commit)
       via  6625591866bf9e88cef071beaf9e1da0d402eccc (commit)
       via  cea8d8403b9d0894ef393b06d68d318d3ad531bc (commit)
       via  c71149c3a1101df2e34288f9bc8d162969406634 (commit)
       via  b9b41f04b23965d1180291f1ed21029a460d2f1c (commit)
       via  d05cb5371824af680a1d5100f3b7b4e38e30008f (commit)
       via  f15c48d8eab06d81b23c05af6509c7390a5b1321 (commit)
       via  f5a3e988f4142224c79bab960c22b56edad01278 (commit)
      from  741146cd6b0480feff521323aae5f1c6baef8991 (commit)

Summary of changes:
 TODO.charts                                    |    6 +-
 lib/RT/Report/Tickets.pm                       |  508 +++++++++++++++++-------
 lib/RT/Report/Tickets/Entry.pm                 |   29 +-
 lib/RT/SearchBuilder.pm                        |   35 ++
 lib/RT/Tickets_Overlay.pm                      |    4 +-
 share/html/Search/Chart                        |   27 +-
 share/html/Search/Chart.html                   |    4 +-
 share/html/Search/Elements/Chart               |   24 +-
 share/html/Search/Elements/SelectChartFunction |   17 +-
 9 files changed, 445 insertions(+), 209 deletions(-)

- Log -----------------------------------------------------------------
commit f5a3e988f4142224c79bab960c22b56edad01278
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 3 04:57:03 2011 +0400

    no need in CombineFunctionWithField

diff --git a/TODO.charts b/TODO.charts
index 18a0276..09e82ba 100644
--- a/TODO.charts
+++ b/TODO.charts
@@ -4,8 +4,6 @@ move abuse protection code from callers to SetupGrouping
 
 protect Function in SetupGrouping from abuse
 
-extend GroupBy in DBIx::SB with CombineFunctionWithField
-
 Y-axis in charts still always labeled as 'Tickets' even
 if calculate different function
 
diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index d10690d..b3758cc 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -178,8 +178,6 @@ sub GroupBy {
 
     foreach my $e ( @args ) {
         $e = { $self->_FieldToFunction( %$e ) };
-        $e->{'FUNCTION'} = $self->CombineFunctionWithField( %$e )
-            if $e->{'FUNCTION'};
     }
 
     $self->SUPER::GroupBy( @args );

commit f15c48d8eab06d81b23c05af6509c7390a5b1321
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 3 05:03:10 2011 +0400

    drop unused SecondaryGroupBy variable

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 83768fc..f1fc175 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -48,7 +48,6 @@
 <%args>
 $Query => "id > 0"
 $PrimaryGroupBy => 'Queue'
-$SecondaryGroupBy => undef
 $ChartStyle => 'bars'
 $ChartFunction => 'COUNT id'
 </%args>
diff --git a/share/html/Search/Chart.html b/share/html/Search/Chart.html
index 1d7b25c..5b92775 100644
--- a/share/html/Search/Chart.html
+++ b/share/html/Search/Chart.html
@@ -47,13 +47,11 @@
 %# END BPS TAGGED BLOCK }}}
 <%args>
 $PrimaryGroupBy => 'Queue'
-$SecondaryGroupBy => ''
 $ChartStyle => 'bars'
 $ChartFunction => 'COUNT id'
 $Description => undef
 </%args>
 <%init>
-$ARGS{SecondaryGroupBy} ||= '';
 $ARGS{Query} ||= 'id > 0';
 
 # FIXME: should be factored with RT::Report::Tickets::Label :(
@@ -75,7 +73,7 @@ my $title = loc( "Search results grouped by [_1]", $PrimaryGroupByLabel );
 
 my $saved_search = $m->comp( '/Widgets/SavedSearch:new',
     SearchType   => 'Chart',
-    SearchFields => [qw(Query PrimaryGroupBy SecondaryGroupBy ChartStyle)] );
+    SearchFields => [qw(Query PrimaryGroupBy ChartStyle)] );
 
 my @actions = $m->comp( '/Widgets/SavedSearch:process', args => \%ARGS, self => $saved_search );
 
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 9733352..d42b47d 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -48,7 +48,6 @@
 <%args>
 $Query => "id > 0"
 $PrimaryGroupBy => 'Queue'
-$SecondaryGroupBy => undef
 $ChartStyle => 'bars'
 $ChartFunction => 'COUNT id'
 </%args>

commit d05cb5371824af680a1d5100f3b7b4e38e30008f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 7 21:48:03 2011 +0400

    new @GROUPINGS, %GROUPINGS_META variables
    
    * purpose like in TicketSQL
    * handle grouping to function mapping
    * handle sub keys generation

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index b3758cc..d5481fc 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -54,66 +54,129 @@ use RT::Report::Tickets::Entry;
 use strict;
 use warnings;
 
-our %GROUPINGS = (
-    User => [qw(
-        Name RealName NickName
-        EmailAddress
-        Organization
-        Lang City Country Timezone
-    )],
-    Date => [qw(
-        Time
-        Hourly Hour
-        Date Daily
-        DayOfWeek Day DayOfMonth DayOfYear
-        Month Monthly
-        Year Annually
-        WeekOfYear
-    )],
+our @GROUPINGS = (
+    Status => 'Enum',
+
+    Queue  => 'Queue',
+
+    Owner         => 'User',
+    Creator       => 'User',
+    LastUpdatedBy => 'User',
+
+    Requestor     => 'Watcher',
+    Cc            => 'Watcher',
+    AdminCc       => 'Watcher',
+    Watcher       => 'Watcher',
+
+    Created       => 'Date',
+    Starts        => 'Date',
+    Started       => 'Date',
+    Resolved      => 'Date',
+    Due           => 'Date',
+    Told          => 'Date',
+    LastUpdated   => 'Date',
+
+    CF            => 'CustomField',
+);
+our %GROUPINGS;
+
+our %GROUPINGS_META = (
+    Queue => {
+    },
+    User => {
+        SubFields => [qw(
+            Name RealName NickName
+            EmailAddress
+            Organization
+            Lang City Country Timezone
+        )],
+        Function => 'GenerateUserFunction',
+    },
+    User => {
+        SubFields => [qw(
+            Name RealName NickName
+            EmailAddress
+            Organization
+            Lang City Country Timezone
+        )],
+        Function => 'GenerateWatcherFunction',
+    },
+    Date => {
+        SubFields => [qw(
+            Time
+            Hourly Hour
+            Date Daily
+            DayOfWeek Day DayOfMonth DayOfYear
+            Month Monthly
+            Year Annually
+            WeekOfYear
+        )],
+        Function => 'GenerateDateFunction',
+    },
+    CustomField => {
+        SubFields => sub {
+            my $self = shift;
+            my $args = shift;
+
+            my $queues = $args->{'Queues'};
+            if ( !$queues && $args->{'Query'} ) {
+                require RT::Interface::Web::QueryBuilder::Tree;
+                my $tree = RT::Interface::Web::QueryBuilder::Tree->new('AND');
+                $tree->ParseSQL( Query => $args->{'Query'}, CurrentUser => $self->CurrentUser );
+                $queues = $args->{'Queues'} = $tree->GetReferencedQueues;
+            }
+            return () unless $queues;
+
+            my @res;
+
+            my $CustomFields = RT::CustomFields->new( $self->CurrentUser );
+            foreach my $id (keys %$queues) {
+                my $queue = RT::Queue->new( $self->CurrentUser );
+                $queue->Load($id);
+                unless ($queue->id) {
+                    # XXX TODO: This ancient code dates from a former developer
+                    # we have no idea what it means or why cfqueues are so encoded.
+                    $id =~ s/^.'*(.*).'*$/$1/;
+                    $queue->Load($id);
+                }
+                next unless $queue->id;
+
+                $CustomFields->LimitToQueue($queue->id);
+            }
+            $CustomFields->LimitToGlobal;
+            while ( my $CustomField = $CustomFields->Next ) {
+                push @res, "Custom field '". $CustomField->Name ."'", "CF.{". $CustomField->id ."}";
+            }
+            return @res;
+        },
+    },
+    Enum => {
+    },
 );
 
 sub Groupings {
     my $self = shift;
     my %args = (@_);
-    my @fields = map {$_, $_} qw(
-        Status
-        Queue
-    );
 
-    foreach my $type ( qw(Owner Creator LastUpdatedBy Requestor Cc AdminCc Watcher) ) {
-        push @fields, map { ("$type $_", "$type.$_") } @{ $GROUPINGS{'User'} };
-    }
+    my @fields;
 
-    for my $field (qw(Due Resolved Created LastUpdated Started Starts)) {
-        for my $frequency (@{ $GROUPINGS{'Date'} }) {
-            push @fields, "$field $frequency", "$field.$frequency";
+    my @tmp = @GROUPINGS;
+    while ( my ($field, $type) = splice @tmp, 0, 2 ) {
+        my $meta = $GROUPINGS_META{ $type } || {};
+        unless ( $meta->{'SubFields'} ) {
+            push @fields, $field, $field;
         }
-    }
-
-    my $queues = $args{'Queues'};
-    if ( !$queues && $args{'Query'} ) {
-        require RT::Interface::Web::QueryBuilder::Tree;
-        my $tree = RT::Interface::Web::QueryBuilder::Tree->new('AND');
-        $tree->ParseSQL( Query => $args{'Query'}, CurrentUser => $self->CurrentUser );
-        $queues = $tree->GetReferencedQueues;
-    }
-
-    if ( $queues ) {
-        my $CustomFields = RT::CustomFields->new( $self->CurrentUser );
-        foreach my $id (keys %$queues) {
-            my $queue = RT::Queue->new( $self->CurrentUser );
-            $queue->Load($id);
-            unless ($queue->id) {
-                # XXX TODO: This ancient code dates from a former developer
-                # we have no idea what it means or why cfqueues are so encoded.
-                $id =~ s/^.'*(.*).'*$/$1/;
-                $queue->Load($id);
-            }
-            $CustomFields->LimitToQueue($queue->Id);
+        elsif ( ref( $meta->{'SubFields'} ) eq 'ARRAY' ) {
+            push @fields, map { ("$field $_", "$field.$_") } @{ $meta->{'SubFields'} };
+        }
+        elsif ( ref( $meta->{'SubFields'} ) eq 'CODE' ) {
+            push @fields, $meta->{'SubFields'}->(
+                $self,
+                \%args,
+            );
         }
-        $CustomFields->LimitToGlobal;
-        while ( my $CustomField = $CustomFields->Next ) {
-            push @fields, "Custom field '". $CustomField->Name ."'", "CF.{". $CustomField->id ."}";
+        else {
+            $RT::Logger->error("%GROUPINGS_META for $type has unsupported SubFields");
         }
     }
     return @fields;
@@ -225,59 +288,32 @@ sub _FieldToFunction {
     my $self = shift;
     my %args = (@_);
 
-    my $field = $args{'FIELD'};
+    @args{'FIELD', 'SUBKEY'} = split /\./, $args{'FIELD'}, 2;
 
-    my ($key, $subkey) = split /\./, $field, 2;
+    %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
 
-    if ( $subkey && grep $_ eq $subkey, @{ $GROUPINGS{'Date'} } ) {
-        my $tz;
-        if ( RT->Config->Get('ChartsTimezonesInDB') ) {
-            my $to = $self->CurrentUser->UserObj->Timezone
-                || RT->Config->Get('Timezone');
-            $tz = { From => 'UTC', To => $to }
-                if $to && lc $to ne 'utc';
-        }
+    my $meta = $GROUPINGS_META{ $GROUPINGS{ $args{'FIELD'} } };
+    return ('FUNCTION' => 'NULL') unless $meta;
 
-        $args{'FUNCTION'} = $RT::Handle->DateTimeFunction(
-            Type => $subkey,
-            Field => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
-            Timezone => $tz,
-        );
-        $args{'FIELD'} = $key;
-    } elsif ( $field =~ /^(?:CF|CustomField)\.{(.*)}$/ ) { #XXX: use CFDecipher method
-        my $cf_name = $1;
-        my $cf = RT::CustomField->new( $self->CurrentUser );
-        $cf->Load($cf_name);
-        unless ( $cf->id ) {
-            $RT::Logger->error("Couldn't load CustomField #$cf_name");
-        } else {
-            my ($ticket_cf_alias, $cf_alias) = $self->_CustomFieldJoin($cf->id, $cf->id, $cf_name);
-            @args{qw(ALIAS FIELD)} = ($ticket_cf_alias, 'Content');
-        }
-    } elsif ( $field =~ /^(?:(Owner|Creator|LastUpdatedBy))(?:\.(.*))?$/ ) {
-        my $type = $1 || '';
-        my $column = $2 || 'Name';
-        my $u_alias = $self->{"_sql_report_${type}_users_${column}"}
-            ||= $self->Join(
-                TYPE   => 'LEFT',
-                ALIAS1 => 'main',
-                FIELD1 => $type,
-                TABLE2 => 'Users',
-                FIELD2 => 'id',
-            );
-        @args{qw(ALIAS FIELD)} = ($u_alias, $column);
-    } elsif ( $field =~ /^(?:Watcher|(Requestor|Cc|AdminCc))(?:\.(.*))?$/ ) {
-        my $type = $1 || '';
-        my $column = $2 || 'Name';
-        my $u_alias = $self->{"_sql_report_watcher_users_alias_$type"};
-        unless ( $u_alias ) {
-            my ($g_alias, $gm_alias);
-            ($g_alias, $gm_alias, $u_alias) = $self->_WatcherJoin( $type );
-            $self->{"_sql_report_watcher_users_alias_$type"} = $u_alias;
+    return %args unless $meta->{'Function'};
+
+    my $code;
+    unless ( ref $meta->{'Function'} ) {
+        $code = $self->can( $meta->{'Function'} );
+        unless ( $code ) {
+            $RT::Logger->error("No method ". $meta->{'Function'} );
+            return ('FUNCTION' => 'NULL');
         }
-        @args{qw(ALIAS FIELD)} = ($u_alias, $column);
     }
-    return %args;
+    elsif ( ref( $meta->{'Function'} ) eq 'CODE' ) {
+        $code = $meta->{'Function'};
+    }
+    else {
+        $RT::Logger->error("%GROUPINGS_META for $args{FIELD} has unsupported Function");
+        return ('FUNCTION' => 'NULL');
+    }
+
+    return $code->( $self, %args );
 }
 
 
@@ -334,6 +370,80 @@ sub AddEmptyRows {
     }
 }
 
+sub GenerateDateFunction {
+    my $self = shift;
+    my %args = @_;
+
+    my $tz;
+    if ( RT->Config->Get('ChartsTimezonesInDB') ) {
+        my $to = $self->CurrentUser->UserObj->Timezone
+            || RT->Config->Get('Timezone');
+        $tz = { From => 'UTC', To => $to }
+            if $to && lc $to ne 'utc';
+    }
+
+    $args{'FUNCTION'} = $RT::Handle->DateTimeFunction(
+        Type     => delete $args{'SUBKEY'},
+        Field    => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
+        Timezone => $tz,
+    );
+    return %args;
+}
+
+sub GenerateCustomFieldFunction {
+    my $self = shift;
+    my %args = @_;
+
+    my ($name) = ( (delete $args{'SUBKEY'}) =~ /^\.{(.*)}$/ );
+    my $cf = RT::CustomField->new( $self->CurrentUser );
+    $cf->Load($name);
+    unless ( $cf->id ) {
+        $RT::Logger->error("Couldn't load CustomField #$name");
+        @args{qw(FUNCTION FIELD)} = ('NULL', undef);
+    } else {
+        my ($ticket_cf_alias, $cf_alias) = $self->_CustomFieldJoin($cf->id, $cf->id, $name);
+        @args{qw(ALIAS FIELD)} = ($ticket_cf_alias, 'Content');
+    }
+    return %args;
+}
+
+sub GenerateUserFunction {
+    my $self = shift;
+    my %args = @_;
+
+    my $column = delete $args{'SUBKEY'} || 'Name';
+    my $u_alias = $self->{"_sql_report_$args{FIELD}_users_$column"}
+        ||= $self->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => 'main',
+            FIELD1 => $args{'FIELD'},
+            TABLE2 => 'Users',
+            FIELD2 => 'id',
+        );
+    @args{qw(ALIAS FIELD)} = ($u_alias, $column);
+    return %args;
+}
+
+sub GenerateWatcherFunction {
+    my $self = shift;
+    my %args = @_;
+
+    my $type = $args{'FIELD'};
+    $type = '' if $type eq 'Watcher';
+
+    my $column = delete $args{'SUBKEY'} || 'Name';
+
+    my $u_alias = $self->{"_sql_report_watcher_users_alias_$type"};
+    unless ( $u_alias ) {
+        my ($g_alias, $gm_alias);
+        ($g_alias, $gm_alias, $u_alias) = $self->_WatcherJoin( $type );
+        $self->{"_sql_report_watcher_users_alias_$type"} = $u_alias;
+    }
+    @args{qw(ALIAS FIELD)} = ($u_alias, $column);
+
+    return %args;
+}
+
 RT::Base->_ImportOverlays();
 
 1;

commit b9b41f04b23965d1180291f1ed21029a460d2f1c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 7 22:20:41 2011 +0400

    move value upgrade code into META

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index d5481fc..9848c4b 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -112,6 +112,22 @@ our %GROUPINGS_META = (
             WeekOfYear
         )],
         Function => 'GenerateDateFunction',
+        Display => sub {
+            my $self = shift;
+            my %args = (@_);
+
+            my $raw = $args{'VALUE'};
+            return $raw unless defined $raw;
+
+            my ($field, $subkey) = split /\./, $args{'FIELD'}, 2;
+            if ( $subkey eq 'DayOfWeek' ) {
+                return $RT::Date::DAYS_OF_WEEK[ int $raw ];
+            }
+            elsif ( $subkey eq 'Month' ) {
+                return $RT::Date::MONTHS[ int $raw ];
+            }
+            return $raw;
+        },
     },
     CustomField => {
         SubFields => sub {
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index a7d0d00..4a8cae5 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -75,12 +75,26 @@ sub LabelValue {
     my $raw = $self->RawValue( $name, @_ );
 
     my $type = $self->ColumnType( $name );
-    return $raw unless $type;
+    my $meta = $self->FieldMeta( $type->{'FIELD'} );
+    return $raw unless $meta && $meta->{'Display'};
+
+    my $code;
+    unless ( ref $meta->{'Display'} ) {
+        $code = $self->can( $meta->{'Display'} );
+        unless ( $code ) {
+            $RT::Logger->error("No method ". $meta->{'Display'} );
+            return $raw;
+        }
+    }
+    elsif ( ref( $meta->{'Display'} ) eq 'CODE' ) {
+        $code = $meta->{'Display'};
+    }
+    else {
+        return $raw;
+    }
 
-    my $field = $type->{'FIELD'};
-    return $raw unless $field;
+    return $code->( $self, %$type, VALUE => $raw );
 
-    my ($key, $subkey) = split /\./, $field, 2;
     if ( $subkey && grep $_ eq $subkey, @{ $RT::Report::Tickets::GROUPINGS{'Date'} } ) {
         return $raw unless defined $raw;
         if ( $subkey eq 'DayOfWeek' ) {
@@ -98,6 +112,21 @@ sub RawValue {
     return (shift)->__Value( @_ );
 }
 
+sub FieldMeta {
+    my $self = shift;
+    my $field = shift or return undef;
+
+    ($field) = split /\./, $field, 2;
+
+    %RT::Report::Tickets::GROUPINGS
+        = @RT::Report::Tickets::GROUPINGS
+        unless keys %RT::Report::Tickets::GROUPINGS;
+
+    return $RT::Report::Tickets::GROUPINGS_META{
+        $RT::Report::Tickets::GROUPINGS{ $field }
+    };
+}
+
 RT::Base->_ImportOverlays();
 
 1;

commit c71149c3a1101df2e34288f9bc8d162969406634
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 02:11:52 2011 +0400

    make SetupGrouping mandatory
    
    * call _FieldToFunction asap
    * use KEY/SUBKEY to store original FIELD value splitted
    * no need in GroupBy and custom Column
    * no need in continuouse split

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 9848c4b..95a5fcc 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -119,11 +119,10 @@ our %GROUPINGS_META = (
             my $raw = $args{'VALUE'};
             return $raw unless defined $raw;
 
-            my ($field, $subkey) = split /\./, $args{'FIELD'}, 2;
-            if ( $subkey eq 'DayOfWeek' ) {
+            if ( $args{'SUBKEY'} eq 'DayOfWeek' ) {
                 return $RT::Date::DAYS_OF_WEEK[ int $raw ];
             }
-            elsif ( $subkey eq 'Month' ) {
+            elsif ( $args{'SUBKEY'} eq 'Month' ) {
                 return $RT::Date::MONTHS[ int $raw ];
             }
             return $raw;
@@ -223,7 +222,11 @@ sub SetupGroupings {
     $self->FromSQL( $args{'Query'} );
 
     my @group_by = ref( $args{'GroupBy'} )? @{ $args{'GroupBy'} } : ($args{'GroupBy'});
-    $self->GroupBy( map { {FIELD => $_} } @group_by );
+    foreach my $e ( @group_by ) {
+        my ($key, $subkey) = split /\./, $e, 2;
+        $e = { $self->_FieldToFunction( KEY => $key, SUBKEY => $subkey ) };
+    }
+    $self->GroupBy( @group_by );
 
     # UseSQLForACLChecks may add late joins
     my $joined = ($self->_isJoined || RT->Config->Get('UseSQLForACLChecks')) ? 1 : 0;
@@ -239,8 +242,8 @@ sub SetupGroupings {
     }
 
     foreach my $group_by ( @group_by ) {
-        my $alias = $self->Column( FIELD => $group_by );
-        $column_type{ $alias } = { FIELD => $group_by };
+        my $alias = $self->Column( %$group_by );
+        $column_type{ $alias } = $group_by;
         push @res, $alias;
     }
 
@@ -249,30 +252,6 @@ sub SetupGroupings {
     return @res;
 }
 
-sub GroupBy {
-    my $self = shift;
-    my @args = ref $_[0]? @_ : { @_ };
-
-    @{ $self->{'_group_by_field'} ||= [] } = map $_->{'FIELD'}, @args;
-
-    foreach my $e ( @args ) {
-        $e = { $self->_FieldToFunction( %$e ) };
-    }
-
-    $self->SUPER::GroupBy( @args );
-}
-
-sub Column {
-    my $self = shift;
-    my %args = (@_);
-
-    if ( $args{'FIELD'} && !$args{'FUNCTION'} ) {
-        %args = $self->_FieldToFunction( %args );
-    }
-
-    return $self->SUPER::Column( %args );
-}
-
 =head2 _DoSearch
 
 Subclass _DoSearch from our parent so we can go through and add in empty 
@@ -304,11 +283,11 @@ sub _FieldToFunction {
     my $self = shift;
     my %args = (@_);
 
-    @args{'FIELD', 'SUBKEY'} = split /\./, $args{'FIELD'}, 2;
+    $args{'FIELD'} ||= $args{'KEY'};
 
     %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
 
-    my $meta = $GROUPINGS_META{ $GROUPINGS{ $args{'FIELD'} } };
+    my $meta = $GROUPINGS_META{ $GROUPINGS{ $args{'KEY'} } };
     return ('FUNCTION' => 'NULL') unless $meta;
 
     return %args unless $meta->{'Function'};
@@ -399,7 +378,7 @@ sub GenerateDateFunction {
     }
 
     $args{'FUNCTION'} = $RT::Handle->DateTimeFunction(
-        Type     => delete $args{'SUBKEY'},
+        Type     => $args{'SUBKEY'},
         Field    => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
         Timezone => $tz,
     );
@@ -410,7 +389,7 @@ sub GenerateCustomFieldFunction {
     my $self = shift;
     my %args = @_;
 
-    my ($name) = ( (delete $args{'SUBKEY'}) =~ /^\.{(.*)}$/ );
+    my ($name) = ( $args{'SUBKEY'} =~ /^\.{(.*)}$/ );
     my $cf = RT::CustomField->new( $self->CurrentUser );
     $cf->Load($name);
     unless ( $cf->id ) {
@@ -427,7 +406,7 @@ sub GenerateUserFunction {
     my $self = shift;
     my %args = @_;
 
-    my $column = delete $args{'SUBKEY'} || 'Name';
+    my $column = $args{'SUBKEY'} || 'Name';
     my $u_alias = $self->{"_sql_report_$args{FIELD}_users_$column"}
         ||= $self->Join(
             TYPE   => 'LEFT',
@@ -447,7 +426,7 @@ sub GenerateWatcherFunction {
     my $type = $args{'FIELD'};
     $type = '' if $type eq 'Watcher';
 
-    my $column = delete $args{'SUBKEY'} || 'Name';
+    my $column = $args{'SUBKEY'} || 'Name';
 
     my $u_alias = $self->{"_sql_report_watcher_users_alias_$type"};
     unless ( $u_alias ) {
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 4a8cae5..b6cea72 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -75,7 +75,7 @@ sub LabelValue {
     my $raw = $self->RawValue( $name, @_ );
 
     my $type = $self->ColumnType( $name );
-    my $meta = $self->FieldMeta( $type->{'FIELD'} );
+    my $meta = $self->FieldMeta( $type->{'KEY'} );
     return $raw unless $meta && $meta->{'Display'};
 
     my $code;
@@ -116,8 +116,6 @@ sub FieldMeta {
     my $self = shift;
     my $field = shift or return undef;
 
-    ($field) = split /\./, $field, 2;
-
     %RT::Report::Tickets::GROUPINGS
         = @RT::Report::Tickets::GROUPINGS
         unless keys %RT::Report::Tickets::GROUPINGS;

commit cea8d8403b9d0894ef393b06d68d318d3ad531bc
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 02:21:00 2011 +0400

    delete code we never reach

diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index b6cea72..aa3fe08 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -94,18 +94,6 @@ sub LabelValue {
     }
 
     return $code->( $self, %$type, VALUE => $raw );
-
-    if ( $subkey && grep $_ eq $subkey, @{ $RT::Report::Tickets::GROUPINGS{'Date'} } ) {
-        return $raw unless defined $raw;
-        if ( $subkey eq 'DayOfWeek' ) {
-            return $RT::Date::DAYS_OF_WEEK[ int $raw ];
-        }
-        elsif ( $subkey eq 'Month' ) {
-            return $RT::Date::MONTHS[ int $raw ];
-        }
-    }
-
-    return $raw;
 }
 
 sub RawValue {

commit 6625591866bf9e88cef071beaf9e1da0d402eccc
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 03:39:57 2011 +0400

    store META and type around for re-use

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 95a5fcc..7079234 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -221,10 +221,14 @@ sub SetupGroupings {
 
     $self->FromSQL( $args{'Query'} );
 
+    %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
+
     my @group_by = ref( $args{'GroupBy'} )? @{ $args{'GroupBy'} } : ($args{'GroupBy'});
     foreach my $e ( @group_by ) {
         my ($key, $subkey) = split /\./, $e, 2;
         $e = { $self->_FieldToFunction( KEY => $key, SUBKEY => $subkey ) };
+        $e->{'TYPE'} = $GROUPINGS{ $key };
+        $e->{'META'} = $GROUPINGS{ $e->{'TYPE'} };
     }
     $self->GroupBy( @group_by );
 
@@ -285,8 +289,6 @@ sub _FieldToFunction {
 
     $args{'FIELD'} ||= $args{'KEY'};
 
-    %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
-
     my $meta = $GROUPINGS_META{ $GROUPINGS{ $args{'KEY'} } };
     return ('FUNCTION' => 'NULL') unless $meta;
 
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index aa3fe08..212e673 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -74,8 +74,9 @@ sub LabelValue {
 
     my $raw = $self->RawValue( $name, @_ );
 
-    my $type = $self->ColumnType( $name );
-    my $meta = $self->FieldMeta( $type->{'KEY'} );
+    my $info = $self->ColumnType( $name );
+
+    my $meta = $info->{'META'};
     return $raw unless $meta && $meta->{'Display'};
 
     my $code;
@@ -93,26 +94,13 @@ sub LabelValue {
         return $raw;
     }
 
-    return $code->( $self, %$type, VALUE => $raw );
+    return $code->( $self, %$meta, VALUE => $raw );
 }
 
 sub RawValue {
     return (shift)->__Value( @_ );
 }
 
-sub FieldMeta {
-    my $self = shift;
-    my $field = shift or return undef;
-
-    %RT::Report::Tickets::GROUPINGS
-        = @RT::Report::Tickets::GROUPINGS
-        unless keys %RT::Report::Tickets::GROUPINGS;
-
-    return $RT::Report::Tickets::GROUPINGS_META{
-        $RT::Report::Tickets::GROUPINGS{ $field }
-    };
-}
-
 RT::Base->_ImportOverlays();
 
 1;

commit c8980af307b6c675dee917153bfce4b3556c3f83
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 16:29:55 2011 +0400

    typo

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 7079234..f8eed31 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -92,7 +92,7 @@ our %GROUPINGS_META = (
         )],
         Function => 'GenerateUserFunction',
     },
-    User => {
+    Watcher => {
         SubFields => [qw(
             Name RealName NickName
             EmailAddress

commit df27e033c1f4165135c79144b80274d80f6d72e2
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 16:31:06 2011 +0400

    Display function for grouping by Queue

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index f8eed31..d4d7d12 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -82,6 +82,14 @@ our %GROUPINGS;
 
 our %GROUPINGS_META = (
     Queue => {
+        Display => {
+            my $self = shift;
+            my %args = (@_);
+
+            my $queue = RT::Queue->new( $self->CurrentUser );
+            $queue->Load( $args{'VALUE'} );
+            return $queue->Name;
+        },
     },
     User => {
         SubFields => [qw(

commit 065633d70d0dde0b7873974df1dfc13039a067a2
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 16:31:53 2011 +0400

    drop code, it's now covered in libs
    
    LabelValue method in ::Entry deals with it now

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index f1fc175..369d371 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -74,28 +74,12 @@ my ($count_name, $value_name) = $tix->SetupGroupings(
     Function => $ChartFunction,
 );
 
-my %class = (
-    Queue => 'RT::Queue',
-    Owner => 'RT::User',
-    Creator => 'RT::User',
-    LastUpdatedBy => 'RT::User',
-);
-my $class = $class{ $PrimaryGroupBy };
-
 my %data;
 my $max_value = 0;
 my $max_key_length = 0;
 while ( my $entry = $tix->Next ) {
-    my $key;
-    if ( $class ) {
-        my $q = $class->new( $session{'CurrentUser'} );
-        $q->Load( $entry->LabelValue( $value_name ) );
-        $key = $q->Name;
-    }
-    else {
-        $key = $entry->LabelValue($value_name);
-    }
-    $key ||= '(no value)';
+    my $key = $entry->LabelValue($value_name)
+        || '(no value)';
     
     my $value = $entry->__Value( $count_name );
     if ($chart_class eq 'GD::Graph::pie') {
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index d42b47d..abc6281 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -66,25 +66,10 @@ my ($count_name, $value_name) = $tix->SetupGroupings(
     Function => $ChartFunction,
 );
 
-my %class = (
-    Queue => 'RT::Queue',
-    Owner => 'RT::User',
-    Creator => 'RT::User',
-    LastUpdatedBy => 'RT::User',
-);
-my $class = $class{ $PrimaryGroupBy };
-
 my (@keys, @values);
 while ( my $entry = $tix->Next ) {
-    if ($class) {
-        my $q = $class->new( $session{'CurrentUser'} );
-        $q->Load( $entry->LabelValue( $value_name ) );
-        push @keys, $q->Name;
-    }
-    else {
-        push @keys, $entry->LabelValue( $value_name );
-    }
-    $keys[-1] ||= loc('(no value)');
+    push @keys, $entry->LabelValue( $value_name )
+        || loc('(no value)');
     push @values, $entry->__Value( $count_name );
 }
 

commit 80a22510ceb252eefda3ad86b762d466a2d42924
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 17:25:56 2011 +0400

    update TODO

diff --git a/TODO.charts b/TODO.charts
index 09e82ba..e555d32 100644
--- a/TODO.charts
+++ b/TODO.charts
@@ -12,3 +12,7 @@ to dot
 
 it'd be nice if full day and month names worked in 
 "Created.DayOfWeek = 'Thu'" searches.
+
+we shouldn't sort "day of week" bars according to translated
+values. Now we get Mon, Fri, Wed, Sat, Thu... in russian, so
+it's very annoying to look at.

commit 4ba737d0d8bb70803988f7af6aa1deacb42c0049
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 22:53:16 2011 +0400

    typo, we need a sub, not a hash reference

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index d4d7d12..9b4653b 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -82,7 +82,7 @@ our %GROUPINGS;
 
 our %GROUPINGS_META = (
     Queue => {
-        Display => {
+        Display => sub {
             my $self = shift;
             my %args = (@_);
 

commit e161edcb5a05a5c2b6059f8687e0ab1912be6bc9
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 22:54:03 2011 +0400

    fetch meta from the proper hash

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 9b4653b..e17c09e 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -236,7 +236,7 @@ sub SetupGroupings {
         my ($key, $subkey) = split /\./, $e, 2;
         $e = { $self->_FieldToFunction( KEY => $key, SUBKEY => $subkey ) };
         $e->{'TYPE'} = $GROUPINGS{ $key };
-        $e->{'META'} = $GROUPINGS{ $e->{'TYPE'} };
+        $e->{'META'} = $GROUPINGS_META{ $e->{'TYPE'} };
     }
     $self->GroupBy( @group_by );
 

commit 89a4fa6d4d24711b3d14e2f27056bab3034a9632
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 22:54:25 2011 +0400

    IsValidGrouping method for checks

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index e17c09e..c598096 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -205,6 +205,34 @@ sub Groupings {
     return @fields;
 }
 
+sub IsValidGrouping {
+    my $self = shift;
+    my %args = (@_);
+    return 0 unless $args{'GroupBy'};
+
+    my ($key, $subkey) = split /\./, $args{'GroupBy'}, 2;
+
+    %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
+    my $type = $GROUPINGS{$key};
+    return 0 unless $type;
+    return 1 unless $subkey;
+
+    my $meta = $GROUPINGS_META{ $type } || {};
+    unless ( $meta->{'SubFields'} ) {
+        return 0;
+    }
+    elsif ( ref( $meta->{'SubFields'} ) eq 'ARRAY' ) {
+        return 1 if grep $_ eq $subkey, @{ $meta->{'SubFields'} };
+    }
+    elsif ( ref( $meta->{'SubFields'} ) eq 'CODE' ) {
+        return 1 if grep $_ eq "$key.$subkey", $meta->{'SubFields'}->(
+            $self,
+            \%args,
+        );
+    }
+    return 0;
+}
+
 sub Label {
     my $self = shift;
     my $field = shift;

commit aba84d0b4f128231a3b7c9bd1cb9a8912a0a1266
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jun 9 00:12:31 2011 +0400

    use new IsValidGrouping

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 369d371..5776215 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -66,8 +66,10 @@ if ($ChartStyle eq 'pie') {
 
 use RT::Report::Tickets;
 my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} );
-my %AllowedGroupings = reverse $tix->Groupings( Query => $Query );
-$PrimaryGroupBy = 'Queue' unless exists $AllowedGroupings{$PrimaryGroupBy};
+
+$PrimaryGroupBy = 'Queue'
+    unless $tix->IsValidGrouping( Query => $Query, GroupBy => $PrimaryGroupBy );
+
 my ($count_name, $value_name) = $tix->SetupGroupings(
     Query => $Query,
     GroupBy => $PrimaryGroupBy,
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index abc6281..edc3a62 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -57,8 +57,8 @@ $PrimaryGroupBy ||= 'Queue'; # make sure PrimaryGroupBy is not undef
 
 my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} );
 
-my %AllowedGroupings = reverse $tix->Groupings( Query => $Query );
-$PrimaryGroupBy = 'Queue' unless exists $AllowedGroupings{$PrimaryGroupBy};
+$PrimaryGroupBy = 'Queue'
+    unless $tix->IsValidGrouping( Query => $Query, GroupBy => $PrimaryGroupBy );
 
 my ($count_name, $value_name) = $tix->SetupGroupings(
     Query => $Query,

commit 1407a419cf953346e0c6c502a2460f46f9a49f6b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 00:57:14 2011 +0400

    feactor out NotSetDateToNullFunction method
    
    incapsulate "CASE ..." SQL we use to turn not set dates into NULLs

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index c598096..adf6b39 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -417,7 +417,7 @@ sub GenerateDateFunction {
 
     $args{'FUNCTION'} = $RT::Handle->DateTimeFunction(
         Type     => $args{'SUBKEY'},
-        Field    => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
+        Field    => $self->NotSetDateToNullFunction,
         Timezone => $tz,
     );
     return %args;
diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index e45ac07..555e348 100755
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -386,6 +386,11 @@ sub _DoCount {
     return $self->SUPER::_DoCount(@_);
 }
 
+sub NotSetDateToNullFunction {
+    my $self = shift;
+    return "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END";
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/lib/RT/Tickets_Overlay.pm b/lib/RT/Tickets_Overlay.pm
index fddaa39..3a43e4c 100755
--- a/lib/RT/Tickets_Overlay.pm
+++ b/lib/RT/Tickets_Overlay.pm
@@ -556,7 +556,7 @@ sub _DateLimit {
 
         my $function = $RT::Handle->DateTimeFunction(
             Type     => $subkey,
-            Field    => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
+            Field    => $self->NotSetDateToNullFunction,
             Timezone => $tz,
         );
 
@@ -605,7 +605,7 @@ sub _DateLimit {
     }
     else {
         $sb->_SQLLimit(
-            FUNCTION => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
+            FUNCTION => $self->NotSetDateToNullFunction,
             FIELD    => $meta->[1],
             OPERATOR => $op,
             VALUE    => $date->ISO,

commit 6d6d13e631c9eea4a9af0dad01b2f07ae8a83baa
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 02:56:27 2011 +0400

    typo, $self called $sb in this particular class

diff --git a/lib/RT/Tickets_Overlay.pm b/lib/RT/Tickets_Overlay.pm
index 3a43e4c..d903e60 100755
--- a/lib/RT/Tickets_Overlay.pm
+++ b/lib/RT/Tickets_Overlay.pm
@@ -556,7 +556,7 @@ sub _DateLimit {
 
         my $function = $RT::Handle->DateTimeFunction(
             Type     => $subkey,
-            Field    => $self->NotSetDateToNullFunction,
+            Field    => $sb->NotSetDateToNullFunction,
             Timezone => $tz,
         );
 
@@ -605,7 +605,7 @@ sub _DateLimit {
     }
     else {
         $sb->_SQLLimit(
-            FUNCTION => $self->NotSetDateToNullFunction,
+            FUNCTION => $sb->NotSetDateToNullFunction,
             FIELD    => $meta->[1],
             OPERATOR => $op,
             VALUE    => $date->ISO,

commit 41090efbc2391813c6abcec7d1a45c1fd95f5712
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 02:57:35 2011 +0400

    typo, we want to pass %$info with KEY, SUBKEY,...

diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 212e673..d69819f 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -94,7 +94,7 @@ sub LabelValue {
         return $raw;
     }
 
-    return $code->( $self, %$meta, VALUE => $raw );
+    return $code->( $self, %$info, VALUE => $raw );
 }
 
 sub RawValue {

commit 6ad4ee3ec3265f72934a77d4e011719ed1b8b35c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 18:48:32 2011 +0400

    @STATISTICS and Co. to track what we can calculate
    
    we need more flexibility, counting number of tickets is simple,
    new functions need post processing, labels and more

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index adf6b39..b69318d 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -177,6 +177,84 @@ our %GROUPINGS_META = (
     },
 );
 
+our @STATISTICS = (
+    COUNT             => ['Tickets', 'Count', 'id'],
+
+    'SUM(TimeWorked)' => ['Total time worked',   'Simple', 'SUM', 'TimeWorked' ],
+    'AVG(TimeWorked)' => ['Average time worked', 'Simple', 'AVG', 'TimeWorked' ],
+    'MIN(TimeWorked)' => ['Minimum time worked', 'Simple', 'MIN', 'TimeWorked' ],
+    'MAX(TimeWorked)' => ['Maximum time worked', 'Simple', 'MAX', 'TimeWorked' ],
+
+    'SUM(TimeEstimated)' => ['Total time estimated',   'Simple', 'SUM', 'TimeEstimated' ],
+    'AVG(TimeEstimated)' => ['Average time estimated', 'Simple', 'AVG', 'TimeEstimated' ],
+    'MIN(TimeEstimated)' => ['Minimum time estimated', 'Simple', 'MIN', 'TimeEstimated' ],
+    'MAX(TimeEstimated)' => ['Maximum time estimated', 'Simple', 'MAX', 'TimeEstimated' ],
+
+    'SUM(TimeLeft)' => ['Total time left',   'Simple', 'SUM', 'TimeLeft' ],
+    'AVG(TimeLeft)' => ['Average time left', 'Simple', 'AVG', 'TimeLeft' ],
+    'MIN(TimeLeft)' => ['Minimum time left', 'Simple', 'MIN', 'TimeLeft' ],
+    'MAX(TimeLeft)' => ['Maximum time left', 'Simple', 'MAX', 'TimeLeft' ],
+
+    'SUM(Created-Resolved)'
+        => ['Summary of Created-Resolved', 'DateTimeInterval', 'SUM', 'Created', 'Resolved' ],
+    'AVG(Created-Resolved)'
+        => ['Average Created-Resolved', 'DateTimeInterval', 'AVG', 'Created', 'Resolved' ],
+    'MIN(Created-Resolved)'
+        => ['Minimum Created-Resolved', 'DateTimeInterval', 'MIN', 'Created', 'Resolved' ],
+    'MAX(Created-Resolved)'
+        => ['Maximum Created-Resolved', 'DateTimeInterval', 'MAX', 'Created', 'Resolved' ],
+
+    'SUM(Created-LastUpdated)'
+        => ['Summary of Created-LastUpdated', 'DateTimeInterval', 'SUM', 'Created', 'LastUpdated' ],
+    'AVG(Created-LastUpdated)'
+        => ['Average Created-LastUpdated', 'DateTimeInterval', 'AVG', 'Created', 'LastUpdated' ],
+    'MIN(Created-LastUpdated)'
+        => ['Minimum Created-LastUpdated', 'DateTimeInterval', 'MIN', 'Created', 'LastUpdated' ],
+    'MAX(Created-LastUpdated)'
+        => ['Maximum Created-LastUpdated', 'DateTimeInterval', 'MAX', 'Created', 'LastUpdated' ],
+);
+our %STATISTICS;
+
+our %STATISTICS_META = (
+    Count => {
+        Function => sub {
+            my $self = shift;
+            my $field = shift || 'id';
+
+            # UseSQLForACLChecks may add late joins
+            my $joined = ($self->_isJoined || RT->Config->Get('UseSQLForACLChecks')) ? 1 : 0;
+
+            my $function = 'COUNT';
+            $function = 'DISTINCT COUNT'
+                if $self->_isJoined || RT->Config->Get('UseSQLForACLChecks');
+
+            return (FUNCTION => $function, FIELD => 'id');
+        },
+
+    },
+    Simple => {
+        Function => sub {
+            my $self = shift;
+            my ($function, $field) = @_;
+            return (FUNCTION => $function, FIELD => $field);
+        },
+    },
+    DateTimeInterval => {
+        Function => sub {
+            my $self = shift;
+            my ($function, $from, $to) = @_;
+
+            my $interval = $self->DateTimeIntervalFunction(
+                From => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $from ) },
+                To   => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $to ) },
+            );
+
+            return (FUNCTION => "$function($interval)");
+        },
+    },
+
+);
+
 sub Groupings {
     my $self = shift;
     my %args = (@_);
@@ -233,6 +311,11 @@ sub IsValidGrouping {
     return 0;
 }
 
+sub Statistics {
+    my $self = shift;
+    return map { ref($_)? $_->[0] : $_ } @STATISTICS;
+}
+
 sub Label {
     my $self = shift;
     my $field = shift;

commit 98eb23d9a6d946379d0d28ab16647e60c18687a1
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 18:52:13 2011 +0400

    _StatsToFunction, turns stat's name into SQL

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index b69318d..53bec71 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -432,6 +432,24 @@ sub _FieldToFunction {
     return $code->( $self, %args );
 }
 
+sub _StatsToFunction {
+    my $self = shift;
+    my ($stat) = (@_);
+
+    %STATISTICS = @STATISTICS unless keys %STATISTICS;
+
+    my ($display, $type, @args) = @{ $STATISTICS{ $stat } || [] };
+    unless ( $type ) {
+        $RT::Logger->error("'$stat' is not valid statistics for report");
+        return ('FUNCTION' => 'NULL');
+    }
+
+    my $meta = $STATISTICS_META{ $type };
+    return ('FUNCTION' => 'NULL') unless $meta;
+    return ('FUNCTION' => 'NULL') unless $meta->{'Function'};
+    return $meta->{'Function'}->( $self, @args );
+}
+
 
 # Override the AddRecord from DBI::SearchBuilder::Unique. id isn't id here
 # wedon't want to disambiguate all the items with a count of 1.

commit 2c714a81ab4c824824631f9005750c23c5881280
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 18:53:42 2011 +0400

    switch over _StatsToFunction from naive approach

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 53bec71..f4f173c 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -351,17 +351,13 @@ sub SetupGroupings {
     }
     $self->GroupBy( @group_by );
 
-    # UseSQLForACLChecks may add late joins
-    my $joined = ($self->_isJoined || RT->Config->Get('UseSQLForACLChecks')) ? 1 : 0;
-
     my (@res, %column_type);
 
     my @function = ref( $args{'Function'} )? @{ $args{'Function'} } : ($args{'Function'});
     foreach my $e ( @function ) {
-        my ($function, $field) = split /\s+/, $e, 2;
-        $function = 'DISTINCT COUNT' if $joined && lc($function) eq 'count';
-        push @res, $self->Column( FUNCTION => $function, FIELD => $field );
-        $column_type{ $res[-1] } = { FUNCTION => $function, FIELD => $field };
+        my %args = $self->_StatsToFunction( $e );
+        push @res, $self->Column( %args );
+        $column_type{ $res[-1] } = \%args;
     }
 
     foreach my $group_by ( @group_by ) {

commit 2f4f4058c4d14fe052c82130ec789f336f773909
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 18:54:30 2011 +0400

    use new method to get list of available stats

diff --git a/share/html/Search/Elements/SelectChartFunction b/share/html/Search/Elements/SelectChartFunction
index cff9bd9..1ee9db9 100644
--- a/share/html/Search/Elements/SelectChartFunction
+++ b/share/html/Search/Elements/SelectChartFunction
@@ -46,8 +46,7 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 <select name="<% $Name %>">
-% my @tmp = @functions;
-% while ( my ($display, $value) = splice @tmp, 0, 2 ) {
+% while ( my ($value, $display) = splice @functions, 0, 2 ) {
 <option value="<% $value %>"<% $value eq $Default ? qq[ selected="selected"] : '' |n %>><% loc( $display ) %></option>
 % }
 </select>
@@ -55,14 +54,6 @@
 $Name => 'ChartFunction'
 $Default => 'COUNT'
 </%ARGS>
-<%ONCE>
-my @functions = (
-    'Count of tickets'  => 'COUNT id',
-);
-foreach my $field (qw(Worked Estimated Left)) {
-    push @functions, "Total time \L$field" => "SUM Time$field",
-        "Average time \L$field" => "AVG Time$field",
-        "Minimum time \L$field" => "MIN Time$field",
-        "Maximum time \L$field" => "MAX Time$field";
-}
-</%ONCE>
+<%INIT>
+my @functions = RT::Report::Tickets->Statistics;
+</%INIT>

commit 673366c76fff0ba0a1dd3b1a65dc4ee55e2f2c1b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 18:56:46 2011 +0400

    FIELD argument in NotSetDateToNullFunction
    
    if it passed then subst it right into result

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 555e348..28ac109 100755
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -388,7 +388,13 @@ sub _DoCount {
 
 sub NotSetDateToNullFunction {
     my $self = shift;
-    return "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END";
+    my %args = ( FIELD => undef, @_ );
+
+    my $res = "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END";
+    if ( $args{FIELD} ) {
+        $res = $self->CombineFunctionWithField( %args, FUNCTION => $res );
+    }
+    return $res;
 }
 
 RT::Base->_ImportOverlays();

commit f1c6bf744003ef6031481fd5b971b60a436b1679
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 18:57:33 2011 +0400

    DateTimeIntervalFunction, two dates -> seconds interval

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 28ac109..99c886b 100755
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -397,6 +397,30 @@ sub NotSetDateToNullFunction {
     return $res;
 }
 
+sub DateTimeIntervalFunction {
+    my $self = shift;
+    my %args = @_;
+
+    my $res = '';
+
+    my $db_type = RT->Config->Get('DatabaseType');
+    if ( $db_type eq 'mysql' ) {
+        $res = 'TIMESTAMPDIFF(SECOND'
+                .', '. $self->CombineFunctionWithField( %{ $args{'From'} } )
+                .', '. $self->CombineFunctionWithField( %{ $args{'To'} } )
+            .')'
+        ;
+    }
+    elsif ( $db_type eq 'Pg' ) {
+        $res = 'EXTRACT(EPOCH FROM AGE('
+                . RT::SearchBuilder->CombineFunctionWithField( %{ $args{'From'} } )
+                .', '. RT::SearchBuilder->CombineFunctionWithField( %{ $args{'To'} } )
+            .'))'
+        ;
+    }
+    return $res;
+}
+
 RT::Base->_ImportOverlays();
 
 1;

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


More information about the Rt-commit mailing list