[Rt-commit] rt branch, 4.2/charts-improvments, created. rt-4.1.6-393-g88957db

Ruslan Zakirov ruz at bestpractical.com
Mon Mar 11 17:36:57 EDT 2013


The branch, 4.2/charts-improvments has been created
        at  88957db17139e2679ccc1895a79219fa964b7039 (commit)

- Log -----------------------------------------------------------------
commit eb4acdb59810790379ee92dafbdd54955302e6a6
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri May 20 12:56:28 2011 +0400

    list possible groupings by date in a variable

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index c39fa90..48dc27a 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -54,6 +54,18 @@ use RT::Report::Tickets::Entry;
 use strict;
 use warnings;
 
+our %GROUPINGS = (
+    Date => [qw(
+        Time
+        Hourly Hour
+        Date Daily
+        DayOfWeek Day DayOfMonth DayOfYear
+        Month Monthly
+        Year Annually
+        WeekOfYear
+    )],
+);
+
 sub Groupings {
     my $self = shift;
     my %args = (@_);

commit fc81a1255717e111fee8e71c83dca74bbe42f669
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri May 20 12:57:14 2011 +0400

    switch over . joined grouping for date fields
    
    we use regexp to parse it later with copied list of possible
    functions, list grows, harder to maintain regexp.
    
    Also, dot joined is consistent with TicketSQL and we'll need
    Created.DayOfWeek = 'Monday'

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 48dc27a..c31f5f8 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -85,11 +85,11 @@ sub Groupings {
 
 
     for my $field (qw(Due Resolved Created LastUpdated Started Starts Told)) { # loc_qw
-        for my $frequency (qw(Hourly Daily Monthly Annually)) { # loc_qw
+        for my $frequency (@{ $GROUPINGS{'Date'} }) {
             push @fields,
               $self->CurrentUser->loc($field)
-              . $self->CurrentUser->loc($frequency),
-              $field . $frequency;
+              .' '. $self->CurrentUser->loc($frequency),
+              "$field.$frequency";
         }
     }
 

commit ed4fd15d814e5eae13f22b2a85021f3b9bc74ff4
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri May 20 13:02:37 2011 +0400

    use DateTimeFunction, new method in SB::Handle
    
    we port our custom implementations of the following features
    into DBIx::SearchBuilder
    
    * timezones in DB
    * extracting parts of DateTime columns using DB's functions

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index c31f5f8..68afff4 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -204,53 +204,23 @@ sub _FieldToFunction {
 
     my $field = $args{'FIELD'};
 
-    if ($field =~ /^(.*)(Hourly|Daily|Monthly|Annually)$/) {
-        my ($field, $grouping) = ($1, $2);
-        my $alias = $args{'ALIAS'} || 'main';
+    my ($key, $subkey) = split /\./, $field, 2;
 
-        my $func = "$alias.$field";
-
-        my $db_type = RT->Config->Get('DatabaseType');
+    if ( $subkey && grep $_ eq $subkey, @{ $GROUPINGS{'Date'} } ) {
+        my $tz;
         if ( RT->Config->Get('ChartsTimezonesInDB') ) {
-            my $tz = $self->CurrentUser->UserObj->Timezone
-                || RT->Config->Get('Timezone')
-                || 'UTC';
-            if ( lc $tz eq 'utc' ) {
-                # do nothing
-            }
-            elsif ( $db_type eq 'Pg' ) {
-                $func = "timezone('UTC', $func)";
-                $func = "timezone(". $self->_Handle->dbh->quote($tz) .", $func)";
-            }
-            elsif ( $db_type eq 'mysql' ) {
-                $func = "CONVERT_TZ($func, 'UTC', "
-                    . $self->_Handle->dbh->quote($tz)
-                    .")";
-            }
-            else {
-                $RT::Logger->warning(
-                    "ChartsTimezonesInDB config option"
-                    ." is not supported on $db_type."
-                );
-            }
+            my $to = $self->CurrentUser->UserObj->Timezone
+                || RT->Config->Get('Timezone');
+            $tz = { From => 'UTC', To => $to }
+                if $to && lc $to ne 'utc';
         }
 
-        # Pg 8.3 requires explicit casting
-        $func .= '::text' if $db_type eq 'Pg';
-
-        if ( $grouping eq 'Hourly' ) {
-            $func = "SUBSTR($func,1,13)";
-        }
-        if ( $grouping eq 'Daily' ) {
-            $func = "SUBSTR($func,1,10)";
-        }
-        elsif ( $grouping eq 'Monthly' ) {
-            $func = "SUBSTR($func,1,7)";
-        }
-        elsif ( $grouping eq 'Annually' ) {
-            $func = "SUBSTR($func,1,4)";
-        }
-        $args{'FUNCTION'} = $func;
+        $args{'FUNCTION'} = $RT::Handle->DateTimeFunction(
+            Type => $subkey,
+            Field => "?",
+            Timezone => $tz,
+        );
+        $args{'FIELD'} = $key;
     } elsif ( $field =~ /^(?:CF|CustomField)\.{(.*)}$/ ) { #XXX: use CFDecipher method
         my $cf_name = $1;
         my $cf = RT::CustomField->new( $self->CurrentUser );

commit 380c819fed9a3061abdfab6ca2493a3c6b01da09
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu May 26 02:21:42 2011 +0400

    add searches by DateField.Xxx, for ex. Created.Day

diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 6c81505..31fd5e5 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -527,7 +527,7 @@ Meta Data:
 =cut
 
 sub _DateLimit {
-    my ( $sb, $field, $op, $value, @rest ) = @_;
+    my ( $sb, $field, $op, $value, %rest ) = @_;
 
     die "Invalid Date Op: $op"
         unless $op =~ /^(=|>|<|>=|<=)$/;
@@ -536,6 +536,28 @@ sub _DateLimit {
     die "Incorrect Meta Data for $field"
         unless ( defined $meta->[1] );
 
+    if ( my $subkey = $rest{SUBKEY} ) {
+        my $tz;
+        if ( RT->Config->Get('ChartsTimezonesInDB') ) {
+            my $to = $sb->CurrentUser->UserObj->Timezone
+                || RT->Config->Get('Timezone');
+            $tz = { From => 'UTC', To => $to }
+                if $to && lc $to ne 'utc';
+        }
+        my $function = $RT::Handle->DateTimeFunction(
+            Type     => $subkey,
+            Field    => '?',
+            Timezone => $tz,
+        );
+        return $sb->_SQLLimit(
+            FUNCTION => $function,
+            FIELD    => $meta->[1],
+            OPERATOR => $op,
+            VALUE    => $value,
+            %rest,
+        );
+    }
+
     my $date = RT::Date->new( $sb->CurrentUser );
     $date->Set( Format => 'unknown', Value => $value );
 
@@ -556,14 +578,14 @@ sub _DateLimit {
             FIELD    => $meta->[1],
             OPERATOR => ">=",
             VALUE    => $daystart,
-            @rest,
+            %rest,
         );
 
         $sb->Limit(
             FIELD    => $meta->[1],
             OPERATOR => "<",
             VALUE    => $dayend,
-            @rest,
+            %rest,
             ENTRYAGGREGATOR => 'AND',
         );
 
@@ -575,7 +597,7 @@ sub _DateLimit {
             FIELD    => $meta->[1],
             OPERATOR => $op,
             VALUE    => $date->ISO,
-            @rest,
+            %rest,
         );
     }
 }

commit 12adad5cd3f0b6e5dd8f012c5e8401cd9e82dcf3
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri May 27 22:58:42 2011 +0400

    delete split of FUNCTION into ALIAS/FIELD arguments
    
    it was only way to write a function on left hand side,
    now Limit in SearchBuilder has FUNCTION argumnt and we
    can avoid this mispractice

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 4bb4177..22fdcd1 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -269,10 +269,7 @@ sub Limit {
         $ARGS{'VALUE'} = 'NULL';
     }
 
-    if ($ARGS{FUNCTION}) {
-        ($ARGS{ALIAS}, $ARGS{FIELD}) = split /\./, delete $ARGS{FUNCTION}, 2;
-        $self->SUPER::Limit(%ARGS);
-    } elsif ($ARGS{FIELD} =~ /\W/
+    if ($ARGS{FIELD} =~ /\W/
           or $ARGS{OPERATOR} !~ /^(=|<|>|!=|<>|<=|>=
                                   |(NOT\s*)?LIKE
                                   |(NOT\s*)?(STARTS|ENDS)WITH

commit b0a6900d3a6be2d8151d94877a22e0c4dbce35e7
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun May 29 02:45:01 2011 +0400

    convert not set dates to NULLs
    
    introduce new NotSetDateToNullFunction method that generates CASE SQL
    to convert not set dates to NULLs and use it in searches and charts

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 68afff4..bc7a0be 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -217,7 +217,7 @@ sub _FieldToFunction {
 
         $args{'FUNCTION'} = $RT::Handle->DateTimeFunction(
             Type => $subkey,
-            Field => "?",
+            Field => $self->NotSetDateToNullFunction,
             Timezone => $tz,
         );
         $args{'FIELD'} = $key;
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index eb38993..90951d0 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -66,21 +66,7 @@ and ensuring that dates are in local not DB timezones.
 
 sub LabelValue {
     my $self  = shift;
-    my $field = shift;
-    my $value = $self->__Value( $field );
-
-    if ( $field =~ /(Daily|Monthly|Annually|Hourly)$/ ) {
-        my $re;
-        # it's not just 1970-01-01 00:00:00 because of timezone shifts
-        # and conversion from UTC to user's TZ
-        $re = qr{19(?:70-01-01|69-12-31) [0-9]{2}} if $field =~ /Hourly$/;
-        $re = qr{19(?:70-01-01|69-12-31)} if $field =~ /Daily$/;
-        $re = qr{19(?:70-01|69-12)} if $field =~ /Monthly$/;
-        $re = qr{19(?:70|69)} if $field =~ /Annually$/;
-        $value =~ s/^$re/Not Set/;
-    }
-
-    return $value;
+    return $self->__Value( @_ );
 }
 
 sub ObjectType {
diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 22fdcd1..8a1c82c 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -366,6 +366,17 @@ sub ColumnMapClassName {
     return $Class;
 }
 
+sub NotSetDateToNullFunction {
+    my $self = shift;
+    my %args = ( FIELD => undef, @_ );
+
+    my $res = "CASE WHEN ? BETWEEN '1969-12-31 11:59:59' AND '1970-01-01 12:00:01' THEN NULL ELSE ? END";
+    if ( $args{FIELD} ) {
+        $res = $self->CombineFunctionWithField( %args, FUNCTION => $res );
+    }
+    return $res;
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 31fd5e5..ac30089 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -546,7 +546,7 @@ sub _DateLimit {
         }
         my $function = $RT::Handle->DateTimeFunction(
             Type     => $subkey,
-            Field    => '?',
+            Field    => $sb->NotSetDateToNullFunction,
             Timezone => $tz,
         );
         return $sb->_SQLLimit(
@@ -594,6 +594,7 @@ sub _DateLimit {
     }
     else {
         $sb->Limit(
+            FUNCTION => $sb->NotSetDateToNullFunction,
             FIELD    => $meta->[1],
             OPERATOR => $op,
             VALUE    => $date->ISO,

commit 71876617016e70ee083f394c7dd93f3bf27f1fc5
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 31 03:13:20 2011 +0400

    add users' fields to our %GROUPINGS hash

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index bc7a0be..64294f2 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -55,6 +55,12 @@ use strict;
 use warnings;
 
 our %GROUPINGS = (
+    User => [qw(
+        Name RealName NickName
+        EmailAddress
+        Organization
+        Lang City Country Timezone
+    )],
     Date => [qw(
         Time
         Hourly Hour
@@ -73,13 +79,10 @@ sub Groupings {
       map { $self->CurrentUser->loc($_), $_ } qw( Status Queue );    # loc_qw
 
     foreach my $type ( qw(Owner Creator LastUpdatedBy Requestor Cc AdminCc Watcher) ) { # loc_qw
-        for my $field (
-            qw( Name EmailAddress RealName NickName Organization Lang City Country Timezone ) # loc_qw
-          )
-        {
+        for my $field ( @{ $GROUPINGS{'User'} } ) {
             push @fields,
               $self->CurrentUser->loc($type) . ' '
-              . $self->CurrentUser->loc($field), $type . '.' . $field;
+              . $self->CurrentUser->loc($field), "$type.$field";
         }
     }
 

commit f5ed6289b51c96e3d96e9046d0fcf1c550cba68b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 31 05:13:39 2011 +0400

    make it possible to search by day and month names
    
    for example:
        Created.DayOfWeek = 'Thu'
        Started.Month = 'Jun'

diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index ac30089..c0c100b 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -537,6 +537,25 @@ sub _DateLimit {
         unless ( defined $meta->[1] );
 
     if ( my $subkey = $rest{SUBKEY} ) {
+        if ( $subkey eq 'DayOfWeek' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
+            for ( my $i = 0; $i < @RT::Date::DAYS_OF_WEEK; $i++ ) {
+                next unless lc $RT::Date::DAYS_OF_WEEK[ $i ] eq lc $value;
+
+                $value = $i; last;
+            }
+            return $sb->_SQLLimit( FIELD => 'id', VALUE => 0, %rest )
+                if $value =~ /[^0-9]/;
+        }
+        elsif ( $subkey eq 'Month' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
+            for ( my $i = 0; $i < @RT::Date::MONTHS; $i++ ) {
+                next unless lc $RT::Date::MONTHS[ $i ] eq lc $value;
+
+                $value = $i; last;
+            }
+            return $sb->_SQLLimit( FIELD => 'id', VALUE => 0, %rest )
+                if $value =~ /[^0-9]/;
+        }
+
         my $tz;
         if ( RT->Config->Get('ChartsTimezonesInDB') ) {
             my $to = $sb->CurrentUser->UserObj->Timezone
@@ -544,11 +563,13 @@ sub _DateLimit {
             $tz = { From => 'UTC', To => $to }
                 if $to && lc $to ne 'utc';
         }
+
         my $function = $RT::Handle->DateTimeFunction(
             Type     => $subkey,
             Field    => $sb->NotSetDateToNullFunction,
             Timezone => $tz,
         );
+
         return $sb->_SQLLimit(
             FUNCTION => $function,
             FIELD    => $meta->[1],

commit ad130cf893e4cd9123dbe3bc218ab055ff28bc84
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 31 18:28:52 2011 +0400

    store grouping properties by column alias

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 64294f2..ad9b3fe 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -141,15 +141,28 @@ sub SetupGroupings {
     my %args = (Query => undef, GroupBy => undef, @_);
 
     $self->FromSQL( $args{'Query'} );
+
     my @group_by = ref( $args{'GroupBy'} )? @{ $args{'GroupBy'} } : ($args{'GroupBy'});
     $self->GroupBy( map { {FIELD => $_} } @group_by );
 
     # UseSQLForACLChecks may add late joins
     my $joined = ($self->_isJoined || RT->Config->Get('UseSQLForACLChecks')) ? 1 : 0;
 
+    my %column_type;
+
     my @res;
+
     push @res, $self->Column( FUNCTION => ($joined? 'DISTINCT COUNT' : 'COUNT'), FIELD => 'id' );
-    push @res, map $self->Column( FIELD => $_ ), @group_by;
+    $column_type{ $res[-1] } = { FUNCTION => ($joined? 'DISTINCT COUNT' : 'COUNT'), FIELD => 'id' };
+
+    foreach my $group_by ( @group_by ) {
+        my $alias = $self->Column( FIELD => $group_by );
+        $column_type{ $alias } = { FIELD => $group_by };
+        push @res, $alias;
+    }
+
+    $self->{'column_types'} = \%column_type;
+
     return @res;
 }
 

commit 4f64e3e225a22d4a7a94bcf6ffc218a81d86a1fe
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 31 18:31:38 2011 +0400

    pass properties of our report to individual entries

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index ad9b3fe..0754bad 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -296,7 +296,9 @@ sub Next {
 
 sub NewItem {
     my $self = shift;
-    return RT::Report::Tickets::Entry->new(RT->SystemUser); # $self->CurrentUser);
+    my $res = RT::Report::Tickets::Entry->new(RT->SystemUser); # $self->CurrentUser);
+    $res->{'column_types'} = $self->{'column_types'};
+    return $res;
 }
 
 # This is necessary since normally NewItem (above) is used to intuit the

commit ab34f06c623c6a51658e9705cee1c72ab7353197
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 31 18:34:18 2011 +0400

    upgrade DayOfWeek and Month to words in LabelValue

diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 90951d0..01965b6 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -56,6 +56,14 @@ use base qw/RT::Record/;
 # XXX TODO: how the heck do we acl a report?
 sub CurrentUserHasRight {1}
 
+sub ColumnType {
+    my $self = shift;
+    my $column = shift;
+
+    return $self->{'column_types'}{$column};
+}
+
+
 =head2 LabelValue
 
 If you're pulling a value out of this collection and using it as a label,
@@ -66,7 +74,32 @@ and ensuring that dates are in local not DB timezones.
 
 sub LabelValue {
     my $self  = shift;
-    return $self->__Value( @_ );
+    my $name = shift;
+
+    my $raw = $self->RawValue( $name, @_ );
+
+    my $type = $self->ColumnType( $name );
+    return $raw unless $type;
+
+    my $field = $type->{'FIELD'};
+    return $raw unless $field;
+
+    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' ) {
+            return $RT::Date::DAYS_OF_WEEK[ int $raw ];
+        }
+        elsif ( $subkey eq 'Month' ) {
+            return $RT::Date::MONTHS[ int $raw ];
+        }
+    }
+
+    return $raw;
+}
+
+sub RawValue {
+    return (shift)->__Value( @_ );
 }
 
 sub ObjectType {

commit 7aca2b4782c0fa087abdfa9548b0067a4901da39
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 1 02:55:38 2011 +0400

    component to pick a function calculated in charts

diff --git a/share/html/Search/Elements/SelectChartFunction b/share/html/Search/Elements/SelectChartFunction
new file mode 100644
index 0000000..cff9bd9
--- /dev/null
+++ b/share/html/Search/Elements/SelectChartFunction
@@ -0,0 +1,68 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<select name="<% $Name %>">
+% my @tmp = @functions;
+% while ( my ($display, $value) = splice @tmp, 0, 2 ) {
+<option value="<% $value %>"<% $value eq $Default ? qq[ selected="selected"] : '' |n %>><% loc( $display ) %></option>
+% }
+</select>
+<%ARGS>
+$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>

commit 8eccfda3a86067d1ab8acd2de6fa38953f47f6e8
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 1 02:57:06 2011 +0400

    Allow to pick chart's function and pass it around
    
    At the end pass it into SetupGrouping, but there is
    no implementation

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 5d1ec64..6d834f5 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -49,6 +49,7 @@
 $Query => "id > 0"
 $PrimaryGroupBy => 'Queue'
 $ChartStyle => 'bars'
+$ChartFunction => 'COUNT id'
 </%args>
 <%init>
 my $chart_class;
@@ -68,7 +69,9 @@ my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} );
 my %AllowedGroupings = reverse $tix->Groupings( Query => $Query );
 $PrimaryGroupBy = 'Queue' unless exists $AllowedGroupings{$PrimaryGroupBy};
 my ($count_name, $value_name) = $tix->SetupGroupings(
-    Query => $Query, GroupBy => $PrimaryGroupBy,
+    Query => $Query,
+    GroupBy => $PrimaryGroupBy,
+    Function => $ChartFunction,
 );
 
 my %class = (
diff --git a/share/html/Search/Chart.html b/share/html/Search/Chart.html
index 6ec5789..3544395 100644
--- a/share/html/Search/Chart.html
+++ b/share/html/Search/Chart.html
@@ -48,6 +48,7 @@
 <%args>
 $PrimaryGroupBy => 'Queue'
 $ChartStyle => 'bars'
+$ChartFunction => 'COUNT id'
 $Description => undef
 </%args>
 <%init>
@@ -124,8 +125,11 @@ my %query;
 <input type="hidden" class="hidden" name="Query" value="<% $ARGS{Query} %>" />
 <input type="hidden" class="hidden" name="SavedChartSearchId" value="<% $saved_search->{SearchId} || 'new' %>" />
 
-<&|/l_unsafe, $m->scomp('Elements/SelectChartType', Name => 'ChartStyle', Default => $ChartStyle), $m->scomp('Elements/SelectGroupBy', Name => 'PrimaryGroupBy', Query => $ARGS{Query}, Default => $PrimaryGroupBy) 
-&>[_1] chart by [_2]</&><input type="submit" class="button" value="<%loc('Update Chart')%>" />
+<&|/l_unsafe,
+  $m->scomp('Elements/SelectChartFunction', Default => $ChartFunction),
+  $m->scomp('Elements/SelectChartType', Default => $ChartStyle),
+  $m->scomp('Elements/SelectGroupBy', Name => 'PrimaryGroupBy', Query => $ARGS{Query}, Default => $PrimaryGroupBy) 
+&>[_1] [_2] chart by [_3]</&><input type="submit" class="button" value="<%loc('Update Chart')%>" />
 </form>
 </&>
 </div>
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 7343900..225a4ea 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -49,16 +49,21 @@
 $Query => "id > 0"
 $PrimaryGroupBy => 'Queue'
 $ChartStyle => 'bars'
+$ChartFunction => 'COUNT id'
 </%args>
 <%init>
 use RT::Report::Tickets;
 $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};
+
 my ($count_name, $value_name) = $tix->SetupGroupings(
-    Query => $Query, GroupBy => $PrimaryGroupBy,
+    Query => $Query,
+    GroupBy => $PrimaryGroupBy,
+    Function => $ChartFunction,
 );
 
 my %class = (

commit 6c85226d3b1e6b9ab17c38c273598f23d9352c8e
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 1 02:58:16 2011 +0400

    implement Function argument in ChartGrouping

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 0754bad..910d095 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -138,7 +138,12 @@ sub Label {
 
 sub SetupGroupings {
     my $self = shift;
-    my %args = (Query => undef, GroupBy => undef, @_);
+    my %args = (
+        Query => undef,
+        GroupBy => undef,
+        Function => undef,
+        @_
+    );
 
     $self->FromSQL( $args{'Query'} );
 
@@ -148,12 +153,15 @@ sub SetupGroupings {
     # UseSQLForACLChecks may add late joins
     my $joined = ($self->_isJoined || RT->Config->Get('UseSQLForACLChecks')) ? 1 : 0;
 
-    my %column_type;
+    my (@res, %column_type);
 
-    my @res;
-
-    push @res, $self->Column( FUNCTION => ($joined? 'DISTINCT COUNT' : 'COUNT'), FIELD => 'id' );
-    $column_type{ $res[-1] } = { FUNCTION => ($joined? 'DISTINCT COUNT' : 'COUNT'), FIELD => 'id' };
+    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 };
+    }
 
     foreach my $group_by ( @group_by ) {
         my $alias = $self->Column( FIELD => $group_by );

commit 898a89fa81981ddbbbcebc53229d53227ff0487f
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 910d095..d73ce97 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -54,70 +54,130 @@ 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',
+    },
+    Watcher => {
+        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 { $self->CurrentUser->loc($_), $_ } qw( Status Queue );    # loc_qw
-
-    foreach my $type ( qw(Owner Creator LastUpdatedBy Requestor Cc AdminCc Watcher) ) { # loc_qw
-        for my $field ( @{ $GROUPINGS{'User'} } ) {
-            push @fields,
-              $self->CurrentUser->loc($type) . ' '
-              . $self->CurrentUser->loc($field), "$type.$field";
-        }
-    }
 
+    my @fields;
 
-    for my $field (qw(Due Resolved Created LastUpdated Started Starts Told)) { # loc_qw
-        for my $frequency (@{ $GROUPINGS{'Date'} }) {
-            push @fields,
-              $self->CurrentUser->loc($field)
-              .' '. $self->CurrentUser->loc($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);
-            $CustomFields->LimitToQueue($queue->Id) if $queue->Id;
+        elsif ( ref( $meta->{'SubFields'} ) eq 'ARRAY' ) {
+            push @fields, map { ("$field $_", "$field.$_") } @{ $meta->{'SubFields'} };
         }
-        $CustomFields->LimitToGlobal;
-        while ( my $CustomField = $CustomFields->Next ) {
-            push @fields, $self->CurrentUser->loc(
-                "Custom field '[_1]'",
-                $CustomField->Name
-              ),
-              "CF.{" . $CustomField->id . "}";
+        elsif ( ref( $meta->{'SubFields'} ) eq 'CODE' ) {
+            push @fields, $meta->{'SubFields'}->(
+                $self,
+                \%args,
+            );
+        }
+        else {
+            $RT::Logger->error("%GROUPINGS_META for $type has unsupported SubFields");
         }
     }
     return @fields;
@@ -226,59 +286,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 => $self->NotSetDateToNullFunction,
-            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 => $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 );
 }
 
 
@@ -338,6 +371,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    => $self->NotSetDateToNullFunction,
+        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 36c3ab73dc9875d5ad81b537ab96bbb9b9bdf8dd
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 d73ce97..f073023 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 01965b6..224b6c2 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -79,23 +79,25 @@ sub LabelValue {
     my $raw = $self->RawValue( $name, @_ );
 
     my $type = $self->ColumnType( $name );
-    return $raw unless $type;
-
-    my $field = $type->{'FIELD'};
-    return $raw unless $field;
-
-    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' ) {
-            return $RT::Date::DAYS_OF_WEEK[ int $raw ];
-        }
-        elsif ( $subkey eq 'Month' ) {
-            return $RT::Date::MONTHS[ int $raw ];
+    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;
+    }
 
-    return $raw;
+    return $code->( $self, %$type, VALUE => $raw );
 }
 
 sub RawValue {
@@ -106,6 +108,21 @@ sub ObjectType {
     return 'RT::Ticket';
 }
 
+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 07d35f8a606761a7f0f243a0032aef6871661c35
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 f073023..9f8f23c 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;
@@ -224,7 +223,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;
@@ -240,8 +243,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;
     }
 
@@ -250,27 +253,6 @@ sub SetupGroupings {
     return @res;
 }
 
-sub GroupBy {
-    my $self = shift;
-    my @args = ref $_[0]? @_ : { @_ };
-
-    @{ $self->{'_group_by_field'} ||= [] } = map $_->{'FIELD'}, @args;
-    $_ = { $self->_FieldToFunction( %$_ ) } foreach @args;
-
-    $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 
@@ -302,11 +284,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'};
@@ -400,7 +382,7 @@ sub GenerateDateFunction {
     }
 
     $args{'FUNCTION'} = $RT::Handle->DateTimeFunction(
-        Type     => delete $args{'SUBKEY'},
+        Type     => $args{'SUBKEY'},
         Field    => $self->NotSetDateToNullFunction,
         Timezone => $tz,
     );
@@ -411,7 +393,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 ) {
@@ -428,7 +410,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',
@@ -448,7 +430,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 224b6c2..2655823 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -79,7 +79,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;
@@ -112,8 +112,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 fdcb362eb7c0a206940965a79d809d0acdf64b03
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 9f8f23c..3ae1c25 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -222,10 +222,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 );
 
@@ -286,8 +290,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 2655823..c269f41 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -78,8 +78,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;
@@ -97,7 +98,7 @@ sub LabelValue {
         return $raw;
     }
 
-    return $code->( $self, %$type, VALUE => $raw );
+    return $code->( $self, %$meta, VALUE => $raw );
 }
 
 sub RawValue {
@@ -108,19 +109,6 @@ sub ObjectType {
     return 'RT::Ticket';
 }
 
-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 2060801e99eb79fa54b4d02d7788650b8c1d4330
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 3ae1c25..7ad7012 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 4aa075c1cb2b82ce908eee5e54c81b3eeeecaecf
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 6d834f5..259d08f 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 225a4ea..fe387e3 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 4cd585d79da4086d1894e41ad39f8a9c3dc9edd6
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 7ad7012..f79a60b 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 e8bd31b8e57ecc06acecac4650453ebe0cde6ed4
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 f79a60b..cc318fb 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -237,7 +237,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 af2abea1080374a34c03deab657f41c24a1b8166
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 cc318fb..c6eaa3e 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -206,6 +206,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;
diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 259d08f..6215f9d 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 fe387e3..e8e09b7 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 496e682b0939588a698dc6715a6033467f25077f
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 c269f41..87f70a2 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -98,7 +98,7 @@ sub LabelValue {
         return $raw;
     }
 
-    return $code->( $self, %$meta, VALUE => $raw );
+    return $code->( $self, %$info, VALUE => $raw );
 }
 
 sub RawValue {

commit ef80c67825d07e31dcd8b632132411bae537aa00
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 c6eaa3e..1c7aa0a 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -178,6 +178,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->_Handle->DateTimeIntervalFunction(
+                From => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $from ) },
+                To   => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $to ) },
+            );
+
+            return (FUNCTION => "$function($interval)");
+        },
+    },
+
+);
+
 sub Groupings {
     my $self = shift;
     my %args = (@_);
@@ -234,6 +312,11 @@ sub IsValidGrouping {
     return 0;
 }
 
+sub Statistics {
+    my $self = shift;
+    return map { ref($_)? $_->[0] : $_ } @STATISTICS;
+}
+
 sub Label {
     my $self = shift;
     my $field = shift;

commit 35821adac9d9f39f77ed6ff2f052b44dc719da6e
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 1c7aa0a..2b99650 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -433,6 +433,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 bc1b1453d90562ac3e5096ecd919aa866cfaba2f
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 2b99650..b16f867 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -352,17 +352,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 b76c3c2e8d3d2ead04a4a1400f136e2153a40e6d
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 d1770ed9c6f097abe571f5dde4afb974aa62b0c3
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jun 11 00:44:38 2011 +0400

    rename ColumnType -> ColumnInfo

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index b16f867..387cd83 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -347,27 +347,31 @@ sub SetupGroupings {
     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_META{ $e->{'TYPE'} };
+        $e->{'TYPE'} = 'grouping';
+        $e->{'INFO'} = $GROUPINGS{ $key };
+        $e->{'META'} = $GROUPINGS_META{ $e->{'INFO'} };
     }
     $self->GroupBy( @group_by );
 
-    my (@res, %column_type);
+    my (@res, %column_info);
 
     my @function = ref( $args{'Function'} )? @{ $args{'Function'} } : ($args{'Function'});
     foreach my $e ( @function ) {
         my %args = $self->_StatsToFunction( $e );
+        $args{'TYPE'} = 'statistic';
+        $args{'INFO'} = $STATISTICS{ $e };
+        $args{'META'} = $STATISTICS_META{ $args{'INFO'}[1] };
         push @res, $self->Column( %args );
-        $column_type{ $res[-1] } = \%args;
+        $column_info{ $res[-1] } = \%args;
     }
 
     foreach my $group_by ( @group_by ) {
         my $alias = $self->Column( %$group_by );
-        $column_type{ $alias } = $group_by;
+        $column_info{ $alias } = $group_by;
         push @res, $alias;
     }
 
-    $self->{'column_types'} = \%column_type;
+    $self->{'column_info'} = \%column_info;
 
     return @res;
 }
@@ -471,7 +475,7 @@ sub Next {
 sub NewItem {
     my $self = shift;
     my $res = RT::Report::Tickets::Entry->new(RT->SystemUser); # $self->CurrentUser);
-    $res->{'column_types'} = $self->{'column_types'};
+    $res->{'column_info'} = $self->{'column_info'};
     return $res;
 }
 
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 87f70a2..365c31e 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -56,11 +56,11 @@ use base qw/RT::Record/;
 # XXX TODO: how the heck do we acl a report?
 sub CurrentUserHasRight {1}
 
-sub ColumnType {
+sub ColumnInfo {
     my $self = shift;
     my $column = shift;
 
-    return $self->{'column_types'}{$column};
+    return $self->{'column_info'}{$column};
 }
 
 
@@ -78,7 +78,7 @@ sub LabelValue {
 
     my $raw = $self->RawValue( $name, @_ );
 
-    my $info = $self->ColumnType( $name );
+    my $info = $self->ColumnInfo( $name );
 
     my $meta = $info->{'META'};
     return $raw unless $meta && $meta->{'Display'};

commit 806a9935dda563d90d8ea75f47d8d05afecefc69
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jun 11 00:45:38 2011 +0400

    change $report->Label, we need labels for stats
    
    get column name instead of grouping. Fetch data
    from META hashes

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 387cd83..f83e835 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -173,6 +173,19 @@ our %GROUPINGS_META = (
             }
             return @res;
         },
+        Label => sub {
+            my $self = shift;
+            my %args = (@_);
+
+            my ($cf) = ( $args{'SUBKEY'} =~ /^{(.*)}$/ );
+            if ( $cf =~ /^\d+$/ ) {
+                my $obj = RT::CustomField->new( $self->CurrentUser );
+                $obj->Load( $cf );
+                $cf = $obj->Name;
+            }
+
+            return 'Custom field [_1]', $self->CurrentUser->loc( $cf );
+        },
     },
     Enum => {
     },
@@ -231,7 +244,6 @@ our %STATISTICS_META = (
 
             return (FUNCTION => $function, FIELD => 'id');
         },
-
     },
     Simple => {
         Function => sub {
@@ -253,7 +265,6 @@ our %STATISTICS_META = (
             return (FUNCTION => "$function($interval)");
         },
     },
-
 );
 
 sub Groupings {
@@ -319,15 +330,48 @@ sub Statistics {
 
 sub Label {
     my $self = shift;
-    my $field = shift;
-    if ( $field =~ /^(?:CF|CustomField)\.{(.*)}$/ ) {
-        my $cf = $1;
-        return $self->CurrentUser->loc( "Custom field '[_1]'", $cf ) if $cf =~ /\D/;
-        my $obj = RT::CustomField->new( $self->CurrentUser );
-        $obj->Load( $cf );
-        return $self->CurrentUser->loc( "Custom field '[_1]'", $obj->Name );
+    my $column = shift;
+
+    my $info = $self->ColumnInfo( $column );
+    unless ( $info ) {
+        $RT::Logger->error("Unknown column '$column'");
+        return $self->CurrentUser->loc('(Incorrect data)');
+    }
+
+    my $label = $info->{'META'}{'Label'};
+    unless ( $label ) {
+        my $res = '';
+        if ( $info->{'TYPE'} eq 'statistic' ) {
+            $res = $info->{'INFO'}[0];
+        }
+        else {
+            $res = join ' ', grep defined && length, @{ $info }{'KEY', 'SUBKEY'};
+        }
+        return $self->CurrentUser->loc( $res );
+    }
+
+    my $code;
+    unless ( ref $label ) {
+        $code = $self->can( $label );
+        unless ( $code ) {
+            $RT::Logger->error("No method ". $label );
+            return $self->CurrentUser->loc('(Incorrect data)');
+        }
+    }
+    elsif ( ref( $label ) eq 'CODE' ) {
+        $code = $label;
     }
-    return $self->CurrentUser->loc($field);
+    else {
+        return $self->CurrentUser->loc('(Incorrect data)');
+    }
+    return $self->CurrentUser->loc( $code->( $self, %$info ) );
+}
+
+sub ColumnInfo {
+    my $self = shift;
+    my $column = shift;
+
+    return $self->{'column_info'}{$column};
 }
 
 sub SetupGroupings {
diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 6215f9d..755ef2d 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -137,8 +137,8 @@ if ($tix->Count == 0) {
 if ($chart_class eq "GD::Graph::bars") {
     my $count = keys %data;
     $chart->set(
-        x_label => $tix->Label( $PrimaryGroupBy ),
-        y_label => loc('Tickets'),
+        x_label => $tix->Label( $value_name ),
+        y_label => $tix->Label( $count_name ),
         show_values => 1,
         x_label_position => 0.6,
         y_label_position => 0.6,
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index e8e09b7..014d0c5 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -92,10 +92,8 @@ my ($i,$total);
 </span>
 <table class="collection-as-table chart">
 <tr>
-<th class="collection-as-table"><% loc($tix->Label($PrimaryGroupBy)) %>
-</th>
-<th class="collection-as-table"><&|/l&>Tickets</&>
-</th>
+<th class="collection-as-table"><% $tix->Label( $value_name ) %></th>
+<th class="collection-as-table"><% $tix->Label( $count_name ) %></th>
 </tr>
 <%perl>
  while (my $key = shift @sorted_keys) {

commit fbfb69ec6f4932f03eb28dc2b73f63818be54250
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 14 20:28:48 2011 +0400

    ColumnList - list of columns on the report entry

diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 365c31e..420252e 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -63,6 +63,11 @@ sub ColumnInfo {
     return $self->{'column_info'}{$column};
 }
 
+sub ColumnsList {
+    my $self = shift;
+    return keys %{ $self->{'column_info'} || {} };
+}
+
 
 =head2 LabelValue
 

commit b66b006d921c70d84fc4e0a693ac2eafc9d12b6e
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 14 20:29:24 2011 +0400

    FindImplementationCode - resolve entries to code ref

diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 420252e..1557b4d 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -114,6 +114,36 @@ sub ObjectType {
     return 'RT::Ticket';
 }
 
+sub FindImplementationCode {
+    my $self = shift;
+    my $value = shift;
+    my $silent = shift;
+
+    my $code;
+    unless ( $value ) {
+        $RT::Logger->error("Value is not defined. Should be method name or code reference")
+            unless $silent;
+        return undef;
+    }
+    elsif ( !ref $value ) {
+        $code = $self->can( $value );
+        unless ( $code ) {
+            $RT::Logger->error("No method $value in ". (ref $self || $self) ." class" )
+                unless $silent;
+            return undef;
+        }
+    }
+    elsif ( ref( $value ) eq 'CODE' ) {
+        $code = $value;
+    }
+    else {
+        $RT::Logger->error("$value is not method name or code reference")
+            unless $silent;
+        return undef;
+    }
+    return $code;
+}
+
 RT::Base->_ImportOverlays();
 
 1;

commit 0e81d8fe20c3d63722da8988f01ab1b0919a8014
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 14 20:30:34 2011 +0400

    ::Query - turn entry of the report into a query

diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 1557b4d..f3ff11e 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -114,6 +114,37 @@ sub ObjectType {
     return 'RT::Ticket';
 }
 
+sub Query {
+    my $self = shift;
+
+    my @parts;
+    foreach my $column ( $self->ColumnsList ) {
+        my $info = $self->ColumnInfo( $column );
+        next unless $info->{'TYPE'} eq 'grouping';
+
+        my $custom = $info->{'META'}{'Query'};
+        if ( $custom and my $code = $self->FindImplementationCode( $custom ) ) {
+            push @parts, $code->( $self, COLUMN => $column, %$info );
+        }
+        else {
+            my $field = join '.', grep $_, $info->{KEY}, $info->{SUBKEY};
+            my $value = $self->RawValue( $column );
+            my $op = '=';
+            if ( defined $value ) {
+                unless ( $value =~ /^\d+$/ ) {
+                    $value =~ s/(['\\])/\\$1/g;
+                    $value = "'$value'";
+                }
+            }
+            else {
+                ($op, $value) = ('IS', 'NULL');
+            }
+            push @parts, "$field $op $value";
+        }
+    }
+    return join ' AND ', grep defined && length, @parts;
+}
+
 sub FindImplementationCode {
     my $self = shift;
     my $value = shift;

commit a5d4bfeff4d3814fd82fb0bcf2011d7bb2cfaf1c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 14 21:01:04 2011 +0400

    spread usage of FindImplementationCode

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index f83e835..1321fee 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -282,14 +282,14 @@ sub Groupings {
         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,
-            );
+        elsif ( my $code = $self->FindImplementationCode( $meta->{'SubFields'} ) ) {
+            push @fields, $code->( $self, \%args );
         }
         else {
-            $RT::Logger->error("%GROUPINGS_META for $type has unsupported SubFields");
+            $RT::Logger->error(
+                "$type has unsupported SubFields."
+                ." Not an array, a method name or a code reference"
+            );
         }
     }
     return @fields;
@@ -314,11 +314,8 @@ sub IsValidGrouping {
     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,
-        );
+    elsif ( my $code = $self->FindImplementationCode( $meta->{'SubFields'}, 'silent' ) ) {
+        return 1 if grep $_ eq "$key.$subkey", $code->( $self, \%args );
     }
     return 0;
 }
@@ -338,33 +335,20 @@ sub Label {
         return $self->CurrentUser->loc('(Incorrect data)');
     }
 
-    my $label = $info->{'META'}{'Label'};
-    unless ( $label ) {
-        my $res = '';
-        if ( $info->{'TYPE'} eq 'statistic' ) {
-            $res = $info->{'INFO'}[0];
-        }
-        else {
-            $res = join ' ', grep defined && length, @{ $info }{'KEY', 'SUBKEY'};
-        }
-        return $self->CurrentUser->loc( $res );
+    if ( $info->{'META'}{'Label'} ) {
+        my $code = $self->FindImplementationCode( $info->{'META'}{'Label'} );
+        return $self->CurrentUser->loc( $code->( $self, %$info ) )
+            if $code;
     }
 
-    my $code;
-    unless ( ref $label ) {
-        $code = $self->can( $label );
-        unless ( $code ) {
-            $RT::Logger->error("No method ". $label );
-            return $self->CurrentUser->loc('(Incorrect data)');
-        }
-    }
-    elsif ( ref( $label ) eq 'CODE' ) {
-        $code = $label;
+    my $res = '';
+    if ( $info->{'TYPE'} eq 'statistic' ) {
+        $res = $info->{'INFO'}[0];
     }
     else {
-        return $self->CurrentUser->loc('(Incorrect data)');
+        $res = join ' ', grep defined && length, @{ $info }{'KEY', 'SUBKEY'};
     }
-    return $self->CurrentUser->loc( $code->( $self, %$info ) );
+    return $self->CurrentUser->loc( $res );
 }
 
 sub ColumnInfo {
@@ -458,21 +442,8 @@ sub _FieldToFunction {
 
     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');
-        }
-    }
-    elsif ( ref( $meta->{'Function'} ) eq 'CODE' ) {
-        $code = $meta->{'Function'};
-    }
-    else {
-        $RT::Logger->error("%GROUPINGS_META for $args{FIELD} has unsupported Function");
-        return ('FUNCTION' => 'NULL');
-    }
+    my $code = $self->FindImplementationCode( $meta->{'Function'} );
+    return ('FUNCTION' => 'NULL') unless $code;
 
     return $code->( $self, %args );
 }
@@ -626,6 +597,35 @@ sub GenerateWatcherFunction {
     return %args;
 }
 
+sub FindImplementationCode {
+    my $self = shift;
+    my $value = shift;
+    my $silent = shift;
+
+    my $code;
+    unless ( $value ) {
+        $RT::Logger->error("Value is not defined. Should be method name or code reference")
+            unless $silent;
+        return undef;
+    }
+    elsif ( !ref $value ) {
+        $code = $self->can( $value );
+        unless ( $code ) {
+            $RT::Logger->error("No method $value in ". (ref $self || $self) ." class" )
+                unless $silent;
+            return undef;
+        }
+    }
+    elsif ( ref( $value ) eq 'CODE' ) {
+        $code = $value;
+    }
+    else {
+        $RT::Logger->error("$value is not method name or code reference")
+            unless $silent;
+        return undef;
+    }
+    return $code;
+}
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index f3ff11e..509d2d1 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -88,20 +88,8 @@ sub LabelValue {
     my $meta = $info->{'META'};
     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 $code = $self->FindImplementationCode( $meta->{'Display'} );
+    return $raw unless $code;
 
     return $code->( $self, %$info, VALUE => $raw );
 }
@@ -146,33 +134,7 @@ sub Query {
 }
 
 sub FindImplementationCode {
-    my $self = shift;
-    my $value = shift;
-    my $silent = shift;
-
-    my $code;
-    unless ( $value ) {
-        $RT::Logger->error("Value is not defined. Should be method name or code reference")
-            unless $silent;
-        return undef;
-    }
-    elsif ( !ref $value ) {
-        $code = $self->can( $value );
-        unless ( $code ) {
-            $RT::Logger->error("No method $value in ". (ref $self || $self) ." class" )
-                unless $silent;
-            return undef;
-        }
-    }
-    elsif ( ref( $value ) eq 'CODE' ) {
-        $code = $value;
-    }
-    else {
-        $RT::Logger->error("$value is not method name or code reference")
-            unless $silent;
-        return undef;
-    }
-    return $code;
+    return RT::Report::Tickets->can('FindImplementationCode')->(@_);
 }
 
 RT::Base->_ImportOverlays();

commit 1268dc177faccd8490515462f7c2ce16d436832e
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 14 23:55:01 2011 +0400

    sort reports in libs

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 1321fee..165152d 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -135,6 +135,7 @@ our %GROUPINGS_META = (
             }
             return $raw;
         },
+        Sort => 'raw',
     },
     CustomField => {
         SubFields => sub {
@@ -357,6 +358,10 @@ sub ColumnInfo {
 
     return $self->{'column_info'}{$column};
 }
+sub ColumnsList {
+    my $self = shift;
+    return keys %{ $self->{'column_info'} || {} };
+}
 
 sub SetupGroupings {
     my $self = shift;
@@ -390,12 +395,14 @@ sub SetupGroupings {
         $args{'INFO'} = $STATISTICS{ $e };
         $args{'META'} = $STATISTICS_META{ $args{'INFO'}[1] };
         push @res, $self->Column( %args );
-        $column_info{ $res[-1] } = \%args;
+        $args{'NAME'} = $res[-1];
+        $column_info{ $args{'NAME'} } = \%args;
     }
 
     foreach my $group_by ( @group_by ) {
         my $alias = $self->Column( %$group_by );
         $column_info{ $alias } = $group_by;
+        $group_by->{'NAME'} = $alias;
         push @res, $alias;
     }
 
@@ -523,6 +530,53 @@ sub AddEmptyRows {
     }
 }
 
+sub SortEntries {
+    my $self = shift;
+
+    # XXX: at the begining let's support only grouping by one column
+    my ($group_by) =
+        grep $_->{'TYPE'} eq 'grouping',
+        map $self->ColumnInfo($_),
+        $self->ColumnsList;
+    return unless $group_by;
+
+    my $order = $group_by->{'META'}{Sort} || 'label';
+    if ( $order eq 'label' ) {
+        $self->{'items'} = [
+            map $_->[0],
+            sort { $a->[1] cmp $b->[1] }
+            map [ $_, $_->LabelValue( $group_by->{'NAME'} ) ],
+            @{ $self->{'items'} || [] }
+        ];
+    }
+    elsif ( $order eq 'numeric label' ) {
+        $self->{'items'} = [
+            map $_->[0],
+            sort { $a->[1] <=> $b->[1] }
+            map [ $_, $_->LabelValue( $group_by->{'NAME'} ) ],
+            @{ $self->{'items'} || [] }
+        ];
+    }
+    elsif ( $order eq 'raw' ) {
+        $self->{'items'} = [
+            map $_->[0],
+            sort { $a->[1] cmp $b->[1] }
+            map [ $_, $_->RawValue( $group_by->{'NAME'} ) ],
+            @{ $self->{'items'} || [] }
+        ];
+    }
+    elsif ( $order eq 'numeric raw' ) {
+        $self->{'items'} = [
+            map $_->[0],
+            sort { $a->[1] <=> $b->[1] }
+            map [ $_, $_->RawValue( $group_by->{'NAME'} ) ],
+            @{ $self->{'items'} || [] }
+        ];
+    } else {
+        $RT::Logger->error("Unknown sorting function '$order'");
+    }
+}
+
 sub GenerateDateFunction {
     my $self = shift;
     my %args = @_;

commit 8fa0f1f5c424e8273ccae2c3627520ff71fa1b71
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 14 23:56:47 2011 +0400

    use sorting from libs

diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 014d0c5..fea3ec7 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -66,21 +66,7 @@ my ($count_name, $value_name) = $tix->SetupGroupings(
     Function => $ChartFunction,
 );
 
-my (@keys, @values);
-while ( my $entry = $tix->Next ) {
-    push @keys, $entry->LabelValue( $value_name )
-        || loc('(no value)');
-    push @values, $entry->__Value( $count_name );
-}
-
-my %data;
-my %loc_keys;
-foreach my $key (@keys) { $data{$key} = shift @values; $loc_keys{$key} = loc($key); }
-my @sorted_keys = map { $loc_keys{$_}} sort { $loc_keys{$a} cmp $loc_keys{$b} } keys %loc_keys;
-my @sorted_values = map { $data{$_}} sort { $loc_keys{$a} cmp $loc_keys{$b} } keys %loc_keys;
 my $query_string = $m->comp('/Elements/QueryString', %ARGS);
-
-my ($i,$total);
 </%init>
 <div class="chart-wrapper">
 <span class="chart image">
@@ -91,49 +77,45 @@ my ($i,$total);
 % }
 </span>
 <table class="collection-as-table chart">
+
 <tr>
 <th class="collection-as-table"><% $tix->Label( $value_name ) %></th>
 <th class="collection-as-table"><% $tix->Label( $count_name ) %></th>
 </tr>
-<%perl>
- while (my $key = shift @sorted_keys) {
- $i++;
- my $value = shift @sorted_values;
- $total += $value;
-</%perl>
+
+% my $base_query = $m->comp('/Elements/QueryString',
+%     Format  => $ARGS{Format},
+%     Rows    => $ARGS{Rows},
+%     OrderBy => $ARGS{OrderBy},
+%     Order   => $ARGS{Order},
+% );
+
+% $tix->SortEntries;
+
+% my ($i,$total) = (0, 0);
+% while ( my $entry = $tix->Next ) {
 <tr class="<% $i%2 ? 'evenline' : 'oddline' %>">
-<%perl>
-# TODO sadly we don't have "creator.city is null" or alike support yet
-# so no link if the key is undef for now
- if ( $PrimaryGroupBy !~ /(Hourly|Daily|Monthly|Annually)$/
-        && $key ne loc('(no value)') ) {
- my $group = $PrimaryGroupBy; $group =~ s! !.!;
- my %orig_keys = reverse %loc_keys;
- my $QueryString = $m->comp('/Elements/QueryString',
-           Query => "$Query and $group = '$orig_keys{$key}'",
-                           Format  => $ARGS{Format},
-                           Rows    => $ARGS{Rows},
-                           OrderBy => $ARGS{OrderBy},
-                           Order   => $ARGS{Order},
-                         );
-</%perl>
+
 <td class="label collection-as-table">
-<a href=<% RT->Config->Get('WebPath') %>/Search/Results.html?<%$QueryString%>><%$key%></a>
-</td>
-<td class="value collection-as-table">
-<a href=<% RT->Config->Get('WebPath') %>/Search/Results.html?<%$QueryString%>><%$value%></a>
-</td>
+% my $key = $entry->LabelValue( $value_name ) || loc('(no value)');
+% my $entry_query = $entry->Query;
+% if ( $entry_query ) {
+<a href="<% RT->Config->Get('WebPath') %>/Search/Results.html?Query=<% "$Query AND $entry_query" |u %>&<% $base_query |hn %>"><% $key %></a>
 % } else {
-<td class="label collection-as-table"><% $key %></td>
-<td class="value collection-as-table"><% $value %></td>
+<% $key %>
 % }
+</td>
+
+% $total += my $value = $entry->RawValue( $count_name );
+<td class="value collection-as-table"><%$value%></td>
+
 </tr>
 % }
 
-%$i++;
-<tr class="<%$i%2 ? 'evenline' : 'oddline' %> total">
-<td class="label collection-as-table"><%loc('Total')%></td>
-<td class="value collection-as-table"><%$total||'0'%></td>
+% $i++;
+<tr class="<% $i%2 ? 'evenline' : 'oddline' %> total">
+<td class="label collection-as-table"><% loc('Total') %></td>
+<td class="value collection-as-table"><% $total || '' %></td>
 </tr>
 
 </table>

commit b5923d6556a3f6c9ea6ffd87401428701bb2ff72
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 15 12:18:50 2011 +0400

    incidently deleted index increment

diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index fea3ec7..c0e8b15 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -94,7 +94,7 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS);
 
 % my ($i,$total) = (0, 0);
 % while ( my $entry = $tix->Next ) {
-<tr class="<% $i%2 ? 'evenline' : 'oddline' %>">
+<tr class="<% ++$i%2 ? 'evenline' : 'oddline' %>">
 
 <td class="label collection-as-table">
 % my $key = $entry->LabelValue( $value_name ) || loc('(no value)');

commit 5965827f07d046ad293bffa918dcd34992dfc318
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 15 12:22:16 2011 +0400

    localize reports' values in libs

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 165152d..c045fe4 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -90,6 +90,7 @@ our %GROUPINGS_META = (
             $queue->Load( $args{'VALUE'} );
             return $queue->Name;
         },
+        Localize => 1,
     },
     User => {
         SubFields => [qw(
@@ -128,10 +129,10 @@ our %GROUPINGS_META = (
             return $raw unless defined $raw;
 
             if ( $args{'SUBKEY'} eq 'DayOfWeek' ) {
-                return $RT::Date::DAYS_OF_WEEK[ int $raw ];
+                return $self->loc($RT::Date::DAYS_OF_WEEK[ int $raw ]);
             }
             elsif ( $args{'SUBKEY'} eq 'Month' ) {
-                return $RT::Date::MONTHS[ int $raw ];
+                return $self->loc($RT::Date::MONTHS[ int $raw ]);
             }
             return $raw;
         },
@@ -189,6 +190,7 @@ our %GROUPINGS_META = (
         },
     },
     Enum => {
+        Localize => 1,
     },
 );
 
@@ -496,7 +498,7 @@ sub Next {
 
 sub NewItem {
     my $self = shift;
-    my $res = RT::Report::Tickets::Entry->new(RT->SystemUser); # $self->CurrentUser);
+    my $res = RT::Report::Tickets::Entry->new($self->CurrentUser);
     $res->{'column_info'} = $self->{'column_info'};
     return $res;
 }
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 509d2d1..e34e5e5 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -82,16 +82,19 @@ sub LabelValue {
     my $name = shift;
 
     my $raw = $self->RawValue( $name, @_ );
-
     my $info = $self->ColumnInfo( $name );
-
     my $meta = $info->{'META'};
-    return $raw unless $meta && $meta->{'Display'};
 
-    my $code = $self->FindImplementationCode( $meta->{'Display'} );
-    return $raw unless $code;
+    if (
+        $meta and $meta->{'Display'}
+        and my $code = $self->FindImplementationCode( $meta->{'Display'} )
+    ) {
+        return $code->( $self, %$info, VALUE => $raw );
+    }
 
-    return $code->( $self, %$info, VALUE => $raw );
+    return $self->loc('(no value)') unless defined $raw && length $raw;
+    return $self->loc($raw) if $info->{'META'}{'Localize'};
+    return $raw;
 }
 
 sub RawValue {
diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 755ef2d..369f44c 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -80,14 +80,11 @@ my %data;
 my $max_value = 0;
 my $max_key_length = 0;
 while ( my $entry = $tix->Next ) {
-    my $key = $entry->LabelValue($value_name)
-        || '(no value)';
+    my $key = $entry->LabelValue($value_name);
     
-    my $value = $entry->__Value( $count_name );
+    my $value = $entry->RawValue( $count_name );
     if ($chart_class eq 'GD::Graph::pie') {
-        $key = loc($key) ." - ". $value;
-    } else {
-        $key = loc($key);
+        $key .= ' - '. $value;
     }
     $data{ $key } = $value;
     $max_value = $value if $max_value < $value;
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index c0e8b15..d62b319 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -97,7 +97,7 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS);
 <tr class="<% ++$i%2 ? 'evenline' : 'oddline' %>">
 
 <td class="label collection-as-table">
-% my $key = $entry->LabelValue( $value_name ) || loc('(no value)');
+% my $key = $entry->LabelValue( $value_name );
 % my $entry_query = $entry->Query;
 % if ( $entry_query ) {
 <a href="<% RT->Config->Get('WebPath') %>/Search/Results.html?Query=<% "$Query AND $entry_query" |u %>&<% $base_query |hn %>"><% $key %></a>

commit d77db9300a5fc90d7ad3e84f55451102200bd804
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 15 12:50:50 2011 +0400

    deal with 'no data' earlier

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 369f44c..5edc65c 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -91,31 +91,15 @@ while ( my $entry = $tix->Next ) {
     $max_key_length = length $key if $max_key_length < length $key;
 }
 
-unless (keys %data) {
-    $data{''} = 0;
-}
-
-
-my $chart = $chart_class->new( 600 => 400 );
-$chart->set( pie_height => 60 ) if $chart_class eq 'GD::Graph::pie';
 my %font_config = RT->Config->Get('ChartFont');
 my $font = $font_config{ $session{CurrentUser}->UserObj->Lang || '' }
-  || $font_config{'others'};
-$chart->set_title_font( $font, 16 ) if $chart->can('set_title_font');
-$chart->set_legend_font( $font, 16 ) if $chart->can('set_legend_font');
-$chart->set_x_label_font( $font, 14 ) if $chart->can('set_x_label_font');
-$chart->set_y_label_font( $font, 14 ) if $chart->can('set_y_label_font');
-$chart->set_label_font( $font, 14 ) if $chart->can('set_label_font');
-$chart->set_x_axis_font( $font, 12 ) if $chart->can('set_x_axis_font');
-$chart->set_y_axis_font( $font, 12 ) if $chart->can('set_y_axis_font');
-$chart->set_values_font( $font, 12 ) if $chart->can('set_values_font');
-$chart->set_value_font( $font, 12 ) if $chart->can('set_value_font');
+    || $font_config{'others'};
 
 # Pie charts don't like having no input, so we show a special image
 # that indicates an error message. Because this is used in an <img>
 # context, it can't be a simple error message. Without this check,
 # the chart will just be a non-loading image.
-if ($tix->Count == 0) {
+unless ( $tix->Count ) {
     my $plot = GD::Image->new(600 => 400);
     $plot->colorAllocate(255, 255, 255); # background
     my $black = $plot->colorAllocate(0, 0, 0);
@@ -131,6 +115,18 @@ if ($tix->Count == 0) {
     $m->comp( 'SELF:Plot', plot => $plot, %ARGS );
 }
 
+my $chart = $chart_class->new( 600 => 400 );
+$chart->set( pie_height => 60 ) if $chart_class eq 'GD::Graph::pie';
+$chart->set_title_font( $font, 16 ) if $chart->can('set_title_font');
+$chart->set_legend_font( $font, 16 ) if $chart->can('set_legend_font');
+$chart->set_x_label_font( $font, 14 ) if $chart->can('set_x_label_font');
+$chart->set_y_label_font( $font, 14 ) if $chart->can('set_y_label_font');
+$chart->set_label_font( $font, 14 ) if $chart->can('set_label_font');
+$chart->set_x_axis_font( $font, 12 ) if $chart->can('set_x_axis_font');
+$chart->set_y_axis_font( $font, 12 ) if $chart->can('set_y_axis_font');
+$chart->set_values_font( $font, 12 ) if $chart->can('set_values_font');
+$chart->set_value_font( $font, 12 ) if $chart->can('set_value_font');
+
 if ($chart_class eq "GD::Graph::bars") {
     my $count = keys %data;
     $chart->set(

commit c19f39a65923586414e19f875bd27f8a3b2dc988
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 15 13:05:20 2011 +0400

    use new sorting method for image chart

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 5edc65c..aaa51d7 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -76,17 +76,21 @@ my ($count_name, $value_name) = $tix->SetupGroupings(
     Function => $ChartFunction,
 );
 
-my %data;
+$tix->SortEntries;
+
+my @data = ([],[]);
 my $max_value = 0;
 my $max_key_length = 0;
 while ( my $entry = $tix->Next ) {
     my $key = $entry->LabelValue($value_name);
-    
     my $value = $entry->RawValue( $count_name );
     if ($chart_class eq 'GD::Graph::pie') {
         $key .= ' - '. $value;
     }
-    $data{ $key } = $value;
+
+    push @{ $data[0] }, $key;
+    push @{ $data[1] }, $value;
+
     $max_value = $value if $max_value < $value;
     $max_key_length = length $key if $max_key_length < length $key;
 }
@@ -128,7 +132,7 @@ $chart->set_values_font( $font, 12 ) if $chart->can('set_values_font');
 $chart->set_value_font( $font, 12 ) if $chart->can('set_value_font');
 
 if ($chart_class eq "GD::Graph::bars") {
-    my $count = keys %data;
+    my $count = @{ $data[0] };
     $chart->set(
         x_label => $tix->Label( $value_name ),
         y_label => $tix->Label( $count_name ),
@@ -164,7 +168,7 @@ $chart->{dclrs} = [
     };
 }
 
-my $plot = $chart->plot( [ [sort keys %data], [map $data{$_}, sort keys %data] ] ) or die $chart->error;
+my $plot = $chart->plot( \@data ) or die $chart->error;
 $m->comp( 'SELF:Plot', plot => $plot, %ARGS );
 </%init>
 

commit cc36fbdd84d6cd8741f14908d6ab80d166bb8643
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 15 23:05:45 2011 +0400

    make it possible to calculate multiple functions
    
    * pictures only show first function
    * had to change return value of SetupGroupings

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index c045fe4..b3fda13 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -378,7 +378,8 @@ sub SetupGroupings {
 
     %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
 
-    my @group_by = ref( $args{'GroupBy'} )? @{ $args{'GroupBy'} } : ($args{'GroupBy'});
+    my @group_by = grep defined && length,
+        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 ) };
@@ -388,29 +389,30 @@ sub SetupGroupings {
     }
     $self->GroupBy( @group_by );
 
-    my (@res, %column_info);
+    my %res = (Groups => [], Functions => []);
+    my %column_info;
 
-    my @function = ref( $args{'Function'} )? @{ $args{'Function'} } : ($args{'Function'});
+    foreach my $group_by ( @group_by ) {
+        $group_by->{'NAME'} = $self->Column( %$group_by );
+        $column_info{ $group_by->{'NAME'} } = $group_by;
+        push @{ $res{'Groups'} }, $group_by->{'NAME'};
+    }
+
+    my @function = grep defined && length,
+        ref( $args{'Function'} )? @{ $args{'Function'} } : ($args{'Function'});
     foreach my $e ( @function ) {
         my %args = $self->_StatsToFunction( $e );
         $args{'TYPE'} = 'statistic';
         $args{'INFO'} = $STATISTICS{ $e };
         $args{'META'} = $STATISTICS_META{ $args{'INFO'}[1] };
-        push @res, $self->Column( %args );
-        $args{'NAME'} = $res[-1];
+        $args{'NAME'} = $self->Column( %args );
+        push @{ $res{'Functions'} }, $args{'NAME'};
         $column_info{ $args{'NAME'} } = \%args;
     }
 
-    foreach my $group_by ( @group_by ) {
-        my $alias = $self->Column( %$group_by );
-        $column_info{ $alias } = $group_by;
-        $group_by->{'NAME'} = $alias;
-        push @res, $alias;
-    }
-
     $self->{'column_info'} = \%column_info;
 
-    return @res;
+    return %res;
 }
 
 =head2 _DoSearch
diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index aaa51d7..0ba1851 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -49,7 +49,7 @@
 $Query => "id > 0"
 $PrimaryGroupBy => 'Queue'
 $ChartStyle => 'bars'
-$ChartFunction => 'COUNT id'
+ at ChartFunction => 'COUNT'
 </%args>
 <%init>
 my $chart_class;
@@ -70,10 +70,10 @@ my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} );
 $PrimaryGroupBy = 'Queue'
     unless $tix->IsValidGrouping( Query => $Query, GroupBy => $PrimaryGroupBy );
 
-my ($count_name, $value_name) = $tix->SetupGroupings(
+my %columns = $tix->SetupGroupings(
     Query => $Query,
     GroupBy => $PrimaryGroupBy,
-    Function => $ChartFunction,
+    Function => (grep defined && length, @ChartFunction)[0],
 );
 
 $tix->SortEntries;
@@ -82,8 +82,8 @@ my @data = ([],[]);
 my $max_value = 0;
 my $max_key_length = 0;
 while ( my $entry = $tix->Next ) {
-    my $key = $entry->LabelValue($value_name);
-    my $value = $entry->RawValue( $count_name );
+    my $key = join ' - ', map $entry->LabelValue( $_ ), @{ $columns{'Groups'} };
+    my $value = $entry->RawValue( $columns{'Functions'}[0] );
     if ($chart_class eq 'GD::Graph::pie') {
         $key .= ' - '. $value;
     }
@@ -134,8 +134,8 @@ $chart->set_value_font( $font, 12 ) if $chart->can('set_value_font');
 if ($chart_class eq "GD::Graph::bars") {
     my $count = @{ $data[0] };
     $chart->set(
-        x_label => $tix->Label( $value_name ),
-        y_label => $tix->Label( $count_name ),
+        x_label => join( ' - ', map $tix->Label( $_ ), @{ $columns{'Groups'} } ),
+        y_label => $tix->Label( $columns{'Functions'}[0] ),
         show_values => 1,
         x_label_position => 0.6,
         y_label_position => 0.6,
diff --git a/share/html/Search/Chart.html b/share/html/Search/Chart.html
index 3544395..2d4dd90 100644
--- a/share/html/Search/Chart.html
+++ b/share/html/Search/Chart.html
@@ -48,7 +48,7 @@
 <%args>
 $PrimaryGroupBy => 'Queue'
 $ChartStyle => 'bars'
-$ChartFunction => 'COUNT id'
+ at ChartFunction => ('COUNT')
 $Description => undef
 </%args>
 <%init>
@@ -120,18 +120,31 @@ my %query;
 
 <div class="chart-meta">
 <div class="chart-type">
-<&| /Widgets/TitleBox, title => loc('Chart Properties')&>
-<form method="get" action="<%RT->Config->Get('WebPath')%>/Search/Chart.html">
+
+<form method="get" action="<% RT->Config->Get('WebPath') %>/Search/Chart.html">
 <input type="hidden" class="hidden" name="Query" value="<% $ARGS{Query} %>" />
 <input type="hidden" class="hidden" name="SavedChartSearchId" value="<% $saved_search->{SearchId} || 'new' %>" />
 
-<&|/l_unsafe,
-  $m->scomp('Elements/SelectChartFunction', Default => $ChartFunction),
-  $m->scomp('Elements/SelectChartType', Default => $ChartStyle),
-  $m->scomp('Elements/SelectGroupBy', Name => 'PrimaryGroupBy', Query => $ARGS{Query}, Default => $PrimaryGroupBy) 
-&>[_1] [_2] chart by [_3]</&><input type="submit" class="button" value="<%loc('Update Chart')%>" />
-</form>
+<&| /Widgets/TitleBox, title => loc('Properties') &>
+<% loc('Group by') %> <& Elements/SelectGroupBy,
+    Name => 'PrimaryGroupBy',
+    Query => $ARGS{Query},
+    Default => $PrimaryGroupBy
+&><br />
+
+<% loc('Count') %> <& Elements/SelectChartFunction, Default => $ChartFunction[0] &>
+<& Elements/SelectChartFunction, Default => $ChartFunction[1], ShowEmpty => 1 &>
+<& Elements/SelectChartFunction, Default => $ChartFunction[2], ShowEmpty => 1 &><br />
+
 </&>
+
+<&| /Widgets/TitleBox, title => loc('Picture') &>
+<% loc('Style') %> <& Elements/SelectChartType, Default => $ChartStyle, Name => 'ChartStyle' &>
+</&>
+
+<& /Elements/Submit, Label => loc('Update Chart'), Name => 'Update' &>
+</form>
+
 </div>
 <div class="saved-search">
     <& /Widgets/SavedSearch:show, %ARGS, Action => 'Chart.html', self => $saved_search, Title => loc('Saved charts') &>
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index d62b319..e9b0e77 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -49,7 +49,7 @@
 $Query => "id > 0"
 $PrimaryGroupBy => 'Queue'
 $ChartStyle => 'bars'
-$ChartFunction => 'COUNT id'
+ at ChartFunction => 'COUNT'
 </%args>
 <%init>
 use RT::Report::Tickets;
@@ -60,10 +60,10 @@ my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} );
 $PrimaryGroupBy = 'Queue'
     unless $tix->IsValidGrouping( Query => $Query, GroupBy => $PrimaryGroupBy );
 
-my ($count_name, $value_name) = $tix->SetupGroupings(
+my %columns = $tix->SetupGroupings(
     Query => $Query,
     GroupBy => $PrimaryGroupBy,
-    Function => $ChartFunction,
+    Function => \@ChartFunction,
 );
 
 my $query_string = $m->comp('/Elements/QueryString', %ARGS);
@@ -79,8 +79,9 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS);
 <table class="collection-as-table chart">
 
 <tr>
-<th class="collection-as-table"><% $tix->Label( $value_name ) %></th>
-<th class="collection-as-table"><% $tix->Label( $count_name ) %></th>
+% foreach my $column ( @{ $columns{'Groups'} }, @{ $columns{'Functions'} } ) {
+<th class="collection-as-table"><% $tix->Label( $column ) %></th>
+% }
 </tr>
 
 % my $base_query = $m->comp('/Elements/QueryString',
@@ -92,30 +93,37 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS);
 
 % $tix->SortEntries;
 
-% my ($i,$total) = (0, 0);
+% my $i = 0;
+% my %total = map { $_ => 0 } @{ $columns{Functions} };
 % while ( my $entry = $tix->Next ) {
 <tr class="<% ++$i%2 ? 'evenline' : 'oddline' %>">
 
-<td class="label collection-as-table">
-% my $key = $entry->LabelValue( $value_name );
+% foreach my $column ( @{ $columns{'Groups'} } ) {
+<td class="label collection-as-table"><% $entry->LabelValue( $column ) %></td>
+% }
+
 % my $entry_query = $entry->Query;
+
+% foreach my $column ( @{ $columns{'Functions'} } ) {
+<td class="value collection-as-table">
+% $total{ $column } += my $value = $entry->RawValue( $column );
 % if ( $entry_query ) {
-<a href="<% RT->Config->Get('WebPath') %>/Search/Results.html?Query=<% "$Query AND $entry_query" |u %>&<% $base_query |hn %>"><% $key %></a>
+<a href="<% RT->Config->Get('WebPath') %>/Search/Results.html?Query=<% "$Query AND $entry_query" |un %>&<% $base_query %>"><% $value %></a>
 % } else {
-<% $key %>
+<% $value %>
 % }
 </td>
-
-% $total += my $value = $entry->RawValue( $count_name );
-<td class="value collection-as-table"><%$value%></td>
+% }
 
 </tr>
 % }
 
 % $i++;
 <tr class="<% $i%2 ? 'evenline' : 'oddline' %> total">
-<td class="label collection-as-table"><% loc('Total') %></td>
-<td class="value collection-as-table"><% $total || '' %></td>
+<td class="label collection-as-table" colspan="<% scalar @{ $columns{'Groups'} } %>"><% loc('Total') %></td>
+% foreach my $column ( @{ $columns{'Functions'} } ) {
+<td class="value collection-as-table"><% $total{ $column } %></td>
+% }
 </tr>
 
 </table>
diff --git a/share/html/Search/Elements/SelectChartFunction b/share/html/Search/Elements/SelectChartFunction
index 1ee9db9..37e0b40 100644
--- a/share/html/Search/Elements/SelectChartFunction
+++ b/share/html/Search/Elements/SelectChartFunction
@@ -46,6 +46,9 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 <select name="<% $Name %>">
+% if ( $ShowEmpty ) {
+<option value=""> </option>
+% }
 % while ( my ($value, $display) = splice @functions, 0, 2 ) {
 <option value="<% $value %>"<% $value eq $Default ? qq[ selected="selected"] : '' |n %>><% loc( $display ) %></option>
 % }
@@ -53,7 +56,9 @@
 <%ARGS>
 $Name => 'ChartFunction'
 $Default => 'COUNT'
+$ShowEmpty => 0
 </%ARGS>
 <%INIT>
 my @functions = RT::Report::Tickets->Statistics;
+$Default = '' unless defined $Default;
 </%INIT>

commit 8489f8190ec8375cbd93a52e2496d502e9d6f36a
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 17 19:45:47 2011 +0400

    we can avoid quoting quote character (backslash)

diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index e34e5e5..6d933b5 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -123,7 +123,7 @@ sub Query {
             my $op = '=';
             if ( defined $value ) {
                 unless ( $value =~ /^\d+$/ ) {
-                    $value =~ s/(['\\])/\\$1/g;
+                    $value =~ s/'/\\'/g;
                     $value = "'$value'";
                 }
             }

commit 40bd9d3a0196c08fec569a4c12657686428cad52
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 17 21:48:51 2011 +0400

    we actually should escape backslash, revert code back
    
    we're going to abstract this into methods in RT::SQL

diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 6d933b5..e34e5e5 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -123,7 +123,7 @@ sub Query {
             my $op = '=';
             if ( defined $value ) {
                 unless ( $value =~ /^\d+$/ ) {
-                    $value =~ s/'/\\'/g;
+                    $value =~ s/(['\\])/\\$1/g;
                     $value = "'$value'";
                 }
             }

commit 8c6b0ae2cd142cefd3675731fe463264cfdefec1
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 17 21:50:03 2011 +0400

    comment situation with picturing only one function

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 0ba1851..ade72e4 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -73,6 +73,7 @@ $PrimaryGroupBy = 'Queue'
 my %columns = $tix->SetupGroupings(
     Query => $Query,
     GroupBy => $PrimaryGroupBy,
+    # TODO: We don't picture more than one function at the moment
     Function => (grep defined && length, @ChartFunction)[0],
 );
 

commit bbe02d42bc89756259d1f1273e060430af67daa9
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 17 23:19:41 2011 +0400

    MUST not sort arguments before joining with '&'
    
    we sorted keys earlier, so things that are independant from
    order are sorted and the rest is order sensitive.
    
    X => [qw(foo bar)] resulted in X=bar&X=foo that is
    very broken

diff --git a/share/html/Elements/QueryString b/share/html/Elements/QueryString
index bb5cf91..0be8e35 100644
--- a/share/html/Elements/QueryString
+++ b/share/html/Elements/QueryString
@@ -60,5 +60,5 @@ for my $key (sort keys %ARGS) {
     }
 }
 
-return join '&', sort(@params);
+return join '&', @params;
 </%INIT>

commit 7c7ee342f89f3bf3f25433da774541bfec042c83
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 17 23:25:40 2011 +0400

    support groupings by multiple fields
    
    at the moment tables are in one dimmension
    
    values in pictures are joined with ' - '

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index ade72e4..4234fe3 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -47,7 +47,7 @@
 %# END BPS TAGGED BLOCK }}}
 <%args>
 $Query => "id > 0"
-$PrimaryGroupBy => 'Queue'
+ at GroupBy => 'Queue'
 $ChartStyle => 'bars'
 @ChartFunction => 'COUNT'
 </%args>
@@ -67,12 +67,15 @@ if ($ChartStyle eq 'pie') {
 use RT::Report::Tickets;
 my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} );
 
-$PrimaryGroupBy = 'Queue'
-    unless $tix->IsValidGrouping( Query => $Query, GroupBy => $PrimaryGroupBy );
+foreach my $e ( splice @GroupBy ) {
+    next unless defined $e && length $e;
+    next unless $tix->IsValidGrouping( Query => $Query, GroupBy => $e );
+    push @GroupBy, $e;
+}
 
 my %columns = $tix->SetupGroupings(
     Query => $Query,
-    GroupBy => $PrimaryGroupBy,
+    GroupBy => \@GroupBy,
     # TODO: We don't picture more than one function at the moment
     Function => (grep defined && length, @ChartFunction)[0],
 );
diff --git a/share/html/Search/Chart.html b/share/html/Search/Chart.html
index 2d4dd90..2b8141e 100644
--- a/share/html/Search/Chart.html
+++ b/share/html/Search/Chart.html
@@ -46,7 +46,7 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 <%args>
-$PrimaryGroupBy => 'Queue'
+ at GroupBy => 'Queue'
 $ChartStyle => 'bars'
 @ChartFunction => ('COUNT')
 $Description => undef
@@ -54,26 +54,11 @@ $Description => undef
 <%init>
 $ARGS{Query} ||= 'id > 0';
 
-# FIXME: should be factored with RT::Report::Tickets::Label :(
-my $PrimaryGroupByLabel;
-if ( $PrimaryGroupBy =~ /^(?:CF|CustomField)\.{(.*)}$/ ) {
-    my $cf = $1;
-    if ( $cf =~ /\D/ ) {
-        $PrimaryGroupByLabel = loc( "custom field '[_1]'", $cf );
-    } else {
-        my $obj = RT::CustomField->new( $session{'CurrentUser'} );
-        $obj->Load( $cf );
-        $PrimaryGroupByLabel = loc( "custom field '[_1]'", $obj->Name );
-    }
-} else {
-    $PrimaryGroupByLabel = loc( $PrimaryGroupBy );
-}
-
-my $title = loc( "Search results grouped by [_1]", $PrimaryGroupByLabel );
+my $title = loc( "Grouped search results");
 
 my $saved_search = $m->comp( '/Widgets/SavedSearch:new',
     SearchType   => 'Chart',
-    SearchFields => [qw(Query PrimaryGroupBy ChartStyle)] );
+    SearchFields => [qw(Query GroupBy ChartStyle ChartFunction)] );
 
 my @actions = $m->comp( '/Widgets/SavedSearch:process', args => \%ARGS, self => $saved_search );
 
@@ -127,9 +112,21 @@ my %query;
 
 <&| /Widgets/TitleBox, title => loc('Properties') &>
 <% loc('Group by') %> <& Elements/SelectGroupBy,
-    Name => 'PrimaryGroupBy',
+    Name => 'GroupBy',
+    Query => $ARGS{Query},
+    Default => $GroupBy[0],
+&>
+<& Elements/SelectGroupBy,
+    Name => 'GroupBy',
+    Query => $ARGS{Query},
+    Default => $GroupBy[1],
+    ShowEmpty => 1,
+&>
+<& Elements/SelectGroupBy,
+    Name => 'GroupBy',
     Query => $ARGS{Query},
-    Default => $PrimaryGroupBy
+    Default => $GroupBy[2],
+    ShowEmpty => 1,
 &><br />
 
 <% loc('Count') %> <& Elements/SelectChartFunction, Default => $ChartFunction[0] &>
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index e9b0e77..6d02182 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -47,26 +47,28 @@
 %# END BPS TAGGED BLOCK }}}
 <%args>
 $Query => "id > 0"
-$PrimaryGroupBy => 'Queue'
+ at GroupBy => 'Queue'
 $ChartStyle => 'bars'
 @ChartFunction => 'COUNT'
 </%args>
 <%init>
 use RT::Report::Tickets;
-$PrimaryGroupBy ||= 'Queue'; # make sure PrimaryGroupBy is not undef
 
 my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} );
 
-$PrimaryGroupBy = 'Queue'
-    unless $tix->IsValidGrouping( Query => $Query, GroupBy => $PrimaryGroupBy );
+foreach my $e ( splice @GroupBy ) {
+    next unless defined $e && length $e;
+    next unless $tix->IsValidGrouping( Query => $Query, GroupBy => $e );
+    push @GroupBy, $e;
+}
 
 my %columns = $tix->SetupGroupings(
     Query => $Query,
-    GroupBy => $PrimaryGroupBy,
+    GroupBy => \@GroupBy,
     Function => \@ChartFunction,
 );
 
-my $query_string = $m->comp('/Elements/QueryString', %ARGS);
+my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy );
 </%init>
 <div class="chart-wrapper">
 <span class="chart image">
diff --git a/share/html/Search/Elements/SelectGroupBy b/share/html/Search/Elements/SelectGroupBy
index 8b436c8..532cadc 100644
--- a/share/html/Search/Elements/SelectGroupBy
+++ b/share/html/Search/Elements/SelectGroupBy
@@ -49,11 +49,14 @@
 $Name => 'GroupBy'
 $Default => 'Status'
 $Query   => ''
+$ShowEmpty => 0
 </%args>
-<select id="<% $Name %>" name="<% $Name %>">
-% while (@options) {
-% my ($text, $value) = (shift @options, shift @options);
-<option value="<% $value %>" <% $value eq $Default ? 'selected="selected"' : '' |n%>><% $text %></option>
+<select name="<% $Name %>">
+% if ( $ShowEmpty ) {
+<option value=""> </option>
+% }
+% while ( my ($text, $value) = splice @options, 0, 2 ) {
+<option value="<% $value %>" <% $value eq ($Default||'') ? 'selected="selected"' : '' |n %>><% $text %></option>
 % }
 </select>
 <%init>

commit 603f5ae52621a0b22aaf4056cbccb91b73633523
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 21 16:55:57 2011 +0400

    s/tix/report/. it's a report, isn't it?

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 4234fe3..348e367 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -65,27 +65,27 @@ if ($ChartStyle eq 'pie') {
 }
 
 use RT::Report::Tickets;
-my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} );
+my $report = RT::Report::Tickets->new( $session{'CurrentUser'} );
 
 foreach my $e ( splice @GroupBy ) {
     next unless defined $e && length $e;
-    next unless $tix->IsValidGrouping( Query => $Query, GroupBy => $e );
+    next unless $report->IsValidGrouping( Query => $Query, GroupBy => $e );
     push @GroupBy, $e;
 }
 
-my %columns = $tix->SetupGroupings(
+my %columns = $report->SetupGroupings(
     Query => $Query,
     GroupBy => \@GroupBy,
     # TODO: We don't picture more than one function at the moment
     Function => (grep defined && length, @ChartFunction)[0],
 );
 
-$tix->SortEntries;
+$report->SortEntries;
 
 my @data = ([],[]);
 my $max_value = 0;
 my $max_key_length = 0;
-while ( my $entry = $tix->Next ) {
+while ( my $entry = $report->Next ) {
     my $key = join ' - ', map $entry->LabelValue( $_ ), @{ $columns{'Groups'} };
     my $value = $entry->RawValue( $columns{'Functions'}[0] );
     if ($chart_class eq 'GD::Graph::pie') {
@@ -107,7 +107,7 @@ my $font = $font_config{ $session{CurrentUser}->UserObj->Lang || '' }
 # that indicates an error message. Because this is used in an <img>
 # context, it can't be a simple error message. Without this check,
 # the chart will just be a non-loading image.
-unless ( $tix->Count ) {
+unless ( $report->Count ) {
     my $plot = GD::Image->new(600 => 400);
     $plot->colorAllocate(255, 255, 255); # background
     my $black = $plot->colorAllocate(0, 0, 0);
@@ -138,8 +138,8 @@ $chart->set_value_font( $font, 12 ) if $chart->can('set_value_font');
 if ($chart_class eq "GD::Graph::bars") {
     my $count = @{ $data[0] };
     $chart->set(
-        x_label => join( ' - ', map $tix->Label( $_ ), @{ $columns{'Groups'} } ),
-        y_label => $tix->Label( $columns{'Functions'}[0] ),
+        x_label => join( ' - ', map $report->Label( $_ ), @{ $columns{'Groups'} } ),
+        y_label => $report->Label( $columns{'Functions'}[0] ),
         show_values => 1,
         x_label_position => 0.6,
         y_label_position => 0.6,
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 6d02182..647d031 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -54,15 +54,15 @@ $ChartStyle => 'bars'
 <%init>
 use RT::Report::Tickets;
 
-my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} );
+my $report = RT::Report::Tickets->new( $session{'CurrentUser'} );
 
 foreach my $e ( splice @GroupBy ) {
     next unless defined $e && length $e;
-    next unless $tix->IsValidGrouping( Query => $Query, GroupBy => $e );
+    next unless $report->IsValidGrouping( Query => $Query, GroupBy => $e );
     push @GroupBy, $e;
 }
 
-my %columns = $tix->SetupGroupings(
+my %columns = $report->SetupGroupings(
     Query => $Query,
     GroupBy => \@GroupBy,
     Function => \@ChartFunction,
@@ -82,7 +82,7 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy
 
 <tr>
 % foreach my $column ( @{ $columns{'Groups'} }, @{ $columns{'Functions'} } ) {
-<th class="collection-as-table"><% $tix->Label( $column ) %></th>
+<th class="collection-as-table"><% $report->Label( $column ) %></th>
 % }
 </tr>
 
@@ -93,11 +93,11 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy
 %     Order   => $ARGS{Order},
 % );
 
-% $tix->SortEntries;
+% $report->SortEntries;
 
 % my $i = 0;
 % my %total = map { $_ => 0 } @{ $columns{Functions} };
-% while ( my $entry = $tix->Next ) {
+% while ( my $entry = $report->Next ) {
 <tr class="<% ++$i%2 ? 'evenline' : 'oddline' %>">
 
 % foreach my $column ( @{ $columns{'Groups'} } ) {

commit 41e0d4e76add3685b88da60ea812289ec76b4b9e
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 21 20:05:24 2011 +0400

    multiple functions in the charts, but bars only

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 348e367..e83b905 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -52,10 +52,13 @@ $ChartStyle => 'bars'
 @ChartFunction => 'COUNT'
 </%args>
 <%init>
-my $chart_class;
 use GD;
 use GD::Text;
 
+ at ChartFunction = grep defined && length, @ChartFunction;
+$ChartStyle = 'bars' if @ChartFunction > 1;
+
+my $chart_class;
 if ($ChartStyle eq 'pie') {
     require GD::Graph::pie;
     $chart_class = "GD::Graph::pie";
@@ -76,8 +79,7 @@ foreach my $e ( splice @GroupBy ) {
 my %columns = $report->SetupGroupings(
     Query => $Query,
     GroupBy => \@GroupBy,
-    # TODO: We don't picture more than one function at the moment
-    Function => (grep defined && length, @ChartFunction)[0],
+    Function => \@ChartFunction,
 );
 
 $report->SortEntries;
@@ -87,15 +89,20 @@ my $max_value = 0;
 my $max_key_length = 0;
 while ( my $entry = $report->Next ) {
     my $key = join ' - ', map $entry->LabelValue( $_ ), @{ $columns{'Groups'} };
-    my $value = $entry->RawValue( $columns{'Functions'}[0] );
+
+    my @values = map $entry->RawValue($_), @{ $columns{'Functions'} };
     if ($chart_class eq 'GD::Graph::pie') {
-        $key .= ' - '. $value;
+        $key .= ' - '. $values[0];
     }
 
     push @{ $data[0] }, $key;
-    push @{ $data[1] }, $value;
 
-    $max_value = $value if $max_value < $value;
+    my $i = 0;
+    push @{ $data[++$i] }, $_ foreach @values;
+
+    foreach my $v ( @values ) {
+        $max_value = $v if $max_value < $v;
+    }
     $max_key_length = length $key if $max_key_length < length $key;
 }
 

commit 18778ff52159f5bed80b81af777d28c40ccc5fa8
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 21 20:06:17 2011 +0400

    bargroup_spacing five times bigger than bar spacing

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index e83b905..daedc04 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -144,6 +144,12 @@ $chart->set_value_font( $font, 12 ) if $chart->can('set_value_font');
 
 if ($chart_class eq "GD::Graph::bars") {
     my $count = @{ $data[0] };
+    my $bar_spacing =
+        $count > 30 ? 1
+        : $count > 20 ? 2
+        : $count > 10 ? 3
+        : 5
+    ;
     $chart->set(
         x_label => join( ' - ', map $report->Label( $_ ), @{ $columns{'Groups'} } ),
         y_label => $report->Label( $columns{'Functions'}[0] ),
@@ -157,9 +163,8 @@ if ($chart_class eq "GD::Graph::bars") {
         y_max_value => 5*(int($max_value/5) + 2),
 # if there're too many bars or at least one key is too long, use vertical
         x_labels_vertical => ( $count * $max_key_length > 60 ) ? 1 : 0,
-        $count > 30 ? ( bar_spacing => 1 ) : ( $count > 20 ? ( bar_spacing => 2 ) : 
-            ( $count > 10 ? ( bar_spacing => 3 ) : ( bar_spacing => 5 ) )
-        ),
+        bar_spacing => $bar_spacing,
+        bargroup_spacing => $bar_spacing*5,
     );
 }
 

commit b04511954c37323ad2609c53aaa878243fc7e0e1
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 21 22:27:24 2011 +0400

    sort by all groupings

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index b3fda13..50994b8 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -534,52 +534,60 @@ sub AddEmptyRows {
     }
 }
 
+{ our @SORT_OPS;
+sub __sort_function_we_need_named($$) {
+    for my $f ( @SORT_OPS ) {
+        my $r = $f->($_[0], $_[1]);
+        return $r if $r;
+    }
+}
 sub SortEntries {
     my $self = shift;
 
-    # XXX: at the begining let's support only grouping by one column
-    my ($group_by) =
+    $self->_DoSearch if $self->{'must_redo_search'};
+    return unless $self->{'items'} && @{ $self->{'items'} };
+
+    my @groups =
         grep $_->{'TYPE'} eq 'grouping',
         map $self->ColumnInfo($_),
         $self->ColumnsList;
-    return unless $group_by;
-
-    my $order = $group_by->{'META'}{Sort} || 'label';
-    if ( $order eq 'label' ) {
-        $self->{'items'} = [
-            map $_->[0],
-            sort { $a->[1] cmp $b->[1] }
-            map [ $_, $_->LabelValue( $group_by->{'NAME'} ) ],
-            @{ $self->{'items'} || [] }
-        ];
-    }
-    elsif ( $order eq 'numeric label' ) {
-        $self->{'items'} = [
-            map $_->[0],
-            sort { $a->[1] <=> $b->[1] }
-            map [ $_, $_->LabelValue( $group_by->{'NAME'} ) ],
-            @{ $self->{'items'} || [] }
-        ];
-    }
-    elsif ( $order eq 'raw' ) {
-        $self->{'items'} = [
-            map $_->[0],
-            sort { $a->[1] cmp $b->[1] }
-            map [ $_, $_->RawValue( $group_by->{'NAME'} ) ],
-            @{ $self->{'items'} || [] }
-        ];
-    }
-    elsif ( $order eq 'numeric raw' ) {
-        $self->{'items'} = [
-            map $_->[0],
-            sort { $a->[1] <=> $b->[1] }
-            map [ $_, $_->RawValue( $group_by->{'NAME'} ) ],
-            @{ $self->{'items'} || [] }
-        ];
-    } else {
-        $RT::Logger->error("Unknown sorting function '$order'");
+    return unless @groups;
+
+    local @SORT_OPS;
+    my @data = map [$_], @{ $self->{'items'} };
+
+    for ( my $i = 0; $i < @groups; $i++ ) {
+        my $group_by = $groups[$i];
+        my $idx = $i+1;
+        my $method;
+
+        my $order = $group_by->{'META'}{Sort} || 'label';
+        if ( $order eq 'label' ) {
+            push @SORT_OPS, sub { $_[0][$idx] cmp $_[1][$idx] };
+            $method = 'LabelValue';
+        }
+        elsif ( $order eq 'numeric label' ) {
+            push @SORT_OPS, sub { $_[0][$idx] <=> $_[1][$idx] };
+            $method = 'LabelValue';
+        }
+        elsif ( $order eq 'raw' ) {
+            push @SORT_OPS, sub { $_[0][$idx] cmp $_[1][$idx] };
+            $method = 'RawValue';
+        }
+        elsif ( $order eq 'numeric raw' ) {
+            push @SORT_OPS, sub { $_[0][$idx] <=> $_[1][$idx] };
+            $method = 'RawValue';
+        } else {
+            $RT::Logger->error("Unknown sorting function '$order'");
+            next;
+        }
+        $_->[$idx] = $_->[0]->$method( $group_by->{'NAME'} ) for @data;
     }
-}
+    $self->{'items'} = [
+        map $_->[0],
+        sort __sort_function_we_need_named @data
+    ];
+} }
 
 sub GenerateDateFunction {
     my $self = shift;

commit 0d27716de6b1fb658308f8e2ac9c1af5f6c3e8df
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 22 04:28:57 2011 +0400

    pass only required things into GroupBy and Column
    
    these methods may store all passed attributes internaly
    this makes sreialization harder to do

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 50994b8..888143d 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -387,7 +387,11 @@ sub SetupGroupings {
         $e->{'INFO'} = $GROUPINGS{ $key };
         $e->{'META'} = $GROUPINGS_META{ $e->{'INFO'} };
     }
-    $self->GroupBy( @group_by );
+    $self->GroupBy( map { {
+        ALIAS    => $_->{'ALIAS'},
+        FIELD    => $_->{'FIELD'},
+        FUNCTION => $_->{'FUNCTION'},
+    } } @group_by );
 
     my %res = (Groups => [], Functions => []);
     my %column_info;
@@ -405,7 +409,11 @@ sub SetupGroupings {
         $args{'TYPE'} = 'statistic';
         $args{'INFO'} = $STATISTICS{ $e };
         $args{'META'} = $STATISTICS_META{ $args{'INFO'}[1] };
-        $args{'NAME'} = $self->Column( %args );
+        $args{'NAME'} = $self->Column(
+            ALIAS    => $args{'ALIAS'},
+            FIELD    => $args{'FIELD'},
+            FUNCTION => $args{'FUNCTION'},
+        );
         push @{ $res{'Functions'} }, $args{'NAME'};
         $column_info{ $args{'NAME'} } = \%args;
     }

commit bec84bf7fe508297c9a8de86176c5a16e33aa07d
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 22 04:31:28 2011 +0400

    sort report earlier, want to serialize sorted report

diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 647d031..4eb5763 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -68,6 +68,8 @@ my %columns = $report->SetupGroupings(
     Function => \@ChartFunction,
 );
 
+$report->SortEntries;
+
 my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy );
 </%init>
 <div class="chart-wrapper">
@@ -93,8 +95,6 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy
 %     Order   => $ARGS{Order},
 % );
 
-% $report->SortEntries;
-
 % my $i = 0;
 % my %total = map { $_ => 0 } @{ $columns{Functions} };
 % while ( my $entry = $report->Next ) {

commit d60b6d1e806c82a79cbeae766d53eb33eee083a8
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 22 04:33:27 2011 +0400

    Serialize and Deserialize methods for reports

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 888143d..b2e80ae 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -700,6 +700,40 @@ sub FindImplementationCode {
     }
     return $code;
 }
+
+sub Serialize {
+    my $self = shift;
+
+    my %clone = %$self;
+# current user, handle and column_info
+    delete @clone{'user', 'DBIxHandle', 'column_info'};
+    $clone{'items'} = [ map $_->{'values'}, @{ $clone{'items'} || [] } ];
+    $clone{'column_info'} = {};
+    while ( my ($k, $v) = each %{ $self->{'column_info'} } ) {
+        $clone{'column_info'}{$k} = { %$v };
+        delete $clone{'column_info'}{$k}{'META'};
+    }
+    return \%clone;
+}
+
+sub Deserialize {
+    my $self = shift;
+    my $data = shift;
+
+    $self->CleanSlate;
+    %$self = (%$self, %$data);
+
+    $self->{'items'} = [
+        map { my $r = $self->NewItem; $r->LoadFromHash( $_ ); $r }
+        @{ $self->{'items'} }
+    ];
+    foreach my $e ( values %{ $self->{column_info} } ) {
+        $e->{'META'} = $e->{'TYPE'} eq 'grouping'
+            ? $GROUPINGS_META{ $e->{'INFO'} }
+            : $STATISTICS_META{ $e->{'INFO'}[1] }
+    }
+}
+
 RT::Base->_ImportOverlays();
 
 1;

commit 2dedc128e16bb960714b6ea169c392aa02dbad1b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 22 04:36:53 2011 +0400

    cache report's data for pictures to re-use

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index daedc04..e7b6abb 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -46,6 +46,7 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 <%args>
+$Cache => undef
 $Query => "id > 0"
 @GroupBy => 'Queue'
 $ChartStyle => 'bars'
@@ -70,19 +71,27 @@ if ($ChartStyle eq 'pie') {
 use RT::Report::Tickets;
 my $report = RT::Report::Tickets->new( $session{'CurrentUser'} );
 
-foreach my $e ( splice @GroupBy ) {
-    next unless defined $e && length $e;
-    next unless $report->IsValidGrouping( Query => $Query, GroupBy => $e );
-    push @GroupBy, $e;
-}
+my %columns;
+
+if ( $Cache and my $data = delete $session{'charts_cache'}{ $Cache } ) {
+    %columns = %{ $data->{'columns'} };
+    $report->Deserialize( $data->{'report'} );
+    $session{'i'}++;
+} else {
+    foreach my $e ( splice @GroupBy ) {
+        next unless defined $e && length $e;
+        next unless $report->IsValidGrouping( Query => $Query, GroupBy => $e );
+        push @GroupBy, $e;
+    }
 
-my %columns = $report->SetupGroupings(
-    Query => $Query,
-    GroupBy => \@GroupBy,
-    Function => \@ChartFunction,
-);
+    %columns = $report->SetupGroupings(
+        Query => $Query,
+        GroupBy => \@GroupBy,
+        Function => \@ChartFunction,
+    );
 
-$report->SortEntries;
+    $report->SortEntries;
+}
 
 my @data = ([],[]);
 my $max_value = 0;
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 4eb5763..7a7a269 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -77,7 +77,10 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy
 % if (RT->Config->Get('DisableGD')) {
 <% loc('Graphical charts are not available.') %><br />
 % } else {
-<img src="<%RT->Config->Get('WebPath')%>/Search/Chart?<%$query_string|n%>" />
+% my $key = Digest::MD5::md5_hex( rand(1024) );
+% $session{'charts_cache'}{$key} = { columns => \%columns, report => $report->Serialize };
+% $session{'i'}++;
+<img src="<% RT->Config->Get('WebPath') %>/Search/Chart?Cache=<% $key |un %>&<% $query_string |n %>" />
 % }
 </span>
 <table class="collection-as-table chart">

commit e79ac8fe7a53b490a8483ee6575e75e7ea152d3c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 22 17:13:08 2011 +0400

    LabelValueCode - fector out it for re-use
    
    we need direct references to code that turns raw
    value of a statistic into a user friendly thing

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index b2e80ae..aa6b8e5 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -671,6 +671,17 @@ sub GenerateWatcherFunction {
     return %args;
 }
 
+
+sub LabelValueCode {
+    my $self = shift;
+    my $name = shift;
+
+    my $display = $self->ColumnInfo( $name )->{'META'}{'Display'};
+    return undef unless $display;
+    return $self->FindImplementationCode( $display );
+}
+
+
 sub FindImplementationCode {
     my $self = shift;
     my $value = shift;
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index e34e5e5..35e9cef 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -82,18 +82,13 @@ sub LabelValue {
     my $name = shift;
 
     my $raw = $self->RawValue( $name, @_ );
-    my $info = $self->ColumnInfo( $name );
-    my $meta = $info->{'META'};
-
-    if (
-        $meta and $meta->{'Display'}
-        and my $code = $self->FindImplementationCode( $meta->{'Display'} )
-    ) {
-        return $code->( $self, %$info, VALUE => $raw );
+
+    if ( my $code = $self->LabelCode( $name ) ) {
+        return $code->( $self, %{ $self->ColumnInfo( $name ) }, VALUE => $raw );
     }
 
     return $self->loc('(no value)') unless defined $raw && length $raw;
-    return $self->loc($raw) if $info->{'META'}{'Localize'};
+    return $self->loc($raw) if $self->ColumnInfo( $name )->{'META'}{'Localize'};
     return $raw;
 }
 
@@ -136,6 +131,10 @@ sub Query {
     return join ' AND ', grep defined && length, @parts;
 }
 
+sub LabelCode {
+    return RT::Report::Tickets->can('LabelValueCode')->(@_);
+}
+
 sub FindImplementationCode {
     return RT::Report::Tickets->can('FindImplementationCode')->(@_);
 }

commit ec698c7ee59c363bd7e57d73c174b85d28eec83c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 22 17:16:26 2011 +0400

    convert intervals from seconds

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index aa6b8e5..022a899 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -267,6 +267,13 @@ our %STATISTICS_META = (
 
             return (FUNCTION => "$function($interval)");
         },
+        Display => sub {
+            my $self = shift;
+            my %args = @_;
+            my $v = $args{'VALUE'};
+            return $self->loc("(no value)") unless defined $v && length $v;
+            return RT::Date->new( $self->CurrentUser )->DurationAsString( $v );
+        },
     },
 );
 

commit 512ee1d4a734362e2b9ea2507da7ffe18b1e89ff
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 22 17:19:28 2011 +0400

    apply conversion on stats in tables and pictures

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index e7b6abb..fcb906d 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -101,7 +101,7 @@ while ( my $entry = $report->Next ) {
 
     my @values = map $entry->RawValue($_), @{ $columns{'Functions'} };
     if ($chart_class eq 'GD::Graph::pie') {
-        $key .= ' - '. $values[0];
+        $key .= ' - '. $entry->LabelValue( $columns{'Functions'}[0] );
     }
 
     push @{ $data[0] }, $key;
@@ -152,19 +152,29 @@ $chart->set_values_font( $font, 12 ) if $chart->can('set_values_font');
 $chart->set_value_font( $font, 12 ) if $chart->can('set_value_font');
 
 if ($chart_class eq "GD::Graph::bars") {
+    my %args;
     my $count = @{ $data[0] };
-    my $bar_spacing =
+    $args{'bar_spacing'} =
         $count > 30 ? 1
         : $count > 20 ? 2
         : $count > 10 ? 3
         : 5
     ;
+    if ( my $code = $report->First->LabelCode( $columns{'Functions'}[0] ) ) {
+        my %info = %{ $report->ColumnInfo( $columns{'Functions'}[0] ) };
+        $args{'values_format'} = $args{'y_number_format'} = sub {
+            return $code->($report, %info, VALUE => shift );
+        };
+    }
+    $report->GotoFirstItem;
+
     $chart->set(
+        %args,
         x_label => join( ' - ', map $report->Label( $_ ), @{ $columns{'Groups'} } ),
-        y_label => $report->Label( $columns{'Functions'}[0] ),
-        show_values => 1,
         x_label_position => 0.6,
+        y_label => $report->Label( $columns{'Functions'}[0] ),
         y_label_position => 0.6,
+        show_values => 1,
         values_space => -1,
 # use a top margin enough to display values over the top line if needed
         t_margin => 18,
@@ -172,8 +182,7 @@ if ($chart_class eq "GD::Graph::bars") {
         y_max_value => 5*(int($max_value/5) + 2),
 # if there're too many bars or at least one key is too long, use vertical
         x_labels_vertical => ( $count * $max_key_length > 60 ) ? 1 : 0,
-        bar_spacing => $bar_spacing,
-        bargroup_spacing => $bar_spacing*5,
+        bargroup_spacing => $args{'bar_spacing'}*5,
     );
 }
 
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 7a7a269..7dbd7c0 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -111,7 +111,8 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy
 
 % foreach my $column ( @{ $columns{'Functions'} } ) {
 <td class="value collection-as-table">
-% $total{ $column } += my $value = $entry->RawValue( $column );
+% $total{ $column } += $entry->RawValue( $column );
+% my $value = $entry->LabelValue( $column );
 % if ( $entry_query ) {
 <a href="<% RT->Config->Get('WebPath') %>/Search/Results.html?Query=<% "$Query AND $entry_query" |un %>&<% $base_query %>"><% $value %></a>
 % } else {
@@ -127,8 +128,13 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy
 <tr class="<% $i%2 ? 'evenline' : 'oddline' %> total">
 <td class="label collection-as-table" colspan="<% scalar @{ $columns{'Groups'} } %>"><% loc('Total') %></td>
 % foreach my $column ( @{ $columns{'Functions'} } ) {
+% if ( my $code = $report->LabelValueCode( $column ) ) {
+% my $info = $report->ColumnInfo( $column );
+<td class="value collection-as-table"><% $code->( $report, %$info, VALUE => $total{ $column } ) %></td>
+% } else {
 <td class="value collection-as-table"><% $total{ $column } %></td>
 % }
+% }
 </tr>
 
 </table>

commit b80d335800ed3c431811dff8734eea2e6c86f5ec
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 22 18:34:53 2011 +0400

    put back grouping by Queue if all are invalid

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index fcb906d..1866701 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -83,6 +83,7 @@ if ( $Cache and my $data = delete $session{'charts_cache'}{ $Cache } ) {
         next unless $report->IsValidGrouping( Query => $Query, GroupBy => $e );
         push @GroupBy, $e;
     }
+    @GroupBy = ('Queue') unless @GroupBy;
 
     %columns = $report->SetupGroupings(
         Query => $Query,
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 7dbd7c0..e32dfab 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -61,6 +61,7 @@ foreach my $e ( splice @GroupBy ) {
     next unless $report->IsValidGrouping( Query => $Query, GroupBy => $e );
     push @GroupBy, $e;
 }
+ at GroupBy = ('Queue') unless @GroupBy;
 
 my %columns = $report->SetupGroupings(
     Query => $Query,

commit c459d85e86e85065a82fb15756e78c9a827d5947
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 22 18:36:08 2011 +0400

    update tests with the latest changes in code

diff --git a/t/web/charting.t b/t/web/charting.t
index 32d95d9..57551fe 100644
--- a/t/web/charting.t
+++ b/t/web/charting.t
@@ -45,34 +45,34 @@ ok( length($m->content), "Has content" );
 
 
 # Group by Queue
-$m->get_ok( "/Search/Chart.html?Query=id>0&PrimaryGroupBy=Queue" );
+$m->get_ok( "/Search/Chart.html?Query=id>0&GroupBy=Queue" );
 $m->content_like(qr{<th[^>]*>Queue\s*</th>\s*<th[^>]*>Tickets\s*</th>}, "Grouped by queue");
 $m->content_like(qr{General</a>\s*</td>\s*<td[^>]*>\s*<a[^>]*>7</a>}, "Found results in table");
 $m->content_like(qr{<img src="/Search/Chart\?}, "Found image");
 
-$m->get_ok( "/Search/Chart?Query=id>0&PrimaryGroupBy=Queue" );
+$m->get_ok( "/Search/Chart?Query=id>0&GroupBy=Queue" );
 is( $m->content_type, "image/png" );
 ok( length($m->content), "Has content" );
 
 
 # Group by Requestor email
-$m->get_ok( "/Search/Chart.html?Query=id>0&PrimaryGroupBy=Requestor.EmailAddress" );
-$m->content_like(qr{<th[^>]*>Requestor\.EmailAddress\s*</th>\s*<th[^>]*>Tickets\s*</th>},
+$m->get_ok( "/Search/Chart.html?Query=id>0&GroupBy=Requestor.EmailAddress" );
+$m->content_like(qr{<th[^>]*>Requestor\s+EmailAddress</th>\s*<th[^>]*>Tickets\s*</th>},
                  "Grouped by requestor");
 $m->content_like(qr{root0\@localhost</a>\s*</td>\s*<td[^>]*>\s*<a[^>]*>3</a>}, "Found results in table");
 $m->content_like(qr{<img src="/Search/Chart\?}, "Found image");
 
-$m->get_ok( "/Search/Chart?Query=id>0&PrimaryGroupBy=Requestor.Email" );
+$m->get_ok( "/Search/Chart?Query=id>0&GroupBy=Requestor.Email" );
 is( $m->content_type, "image/png" );
 ok( length($m->content), "Has content" );
 
 
 # Group by Requestor phone -- which is bogus, and falls back to queue
-$m->get_ok( "/Search/Chart.html?Query=id>0&PrimaryGroupBy=Requestor.Phone" );
+$m->get_ok( "/Search/Chart.html?Query=id>0&GroupBy=Requestor.Phone" );
 $m->content_like(qr{General</a>\s*</td>\s*<td[^>]*>\s*<a[^>]*>7</a>},
                  "Found queue results in table, as a default");
 $m->content_like(qr{<img src="/Search/Chart\?}, "Found image");
 
-$m->get_ok( "/Search/Chart?Query=id>0&PrimaryGroupBy=Requestor.Phone" );
+$m->get_ok( "/Search/Chart?Query=id>0&GroupBy=Requestor.Phone" );
 is( $m->content_type, "image/png" );
 ok( length($m->content), "Has content" );
diff --git a/t/web/saved_search_chart.t b/t/web/saved_search_chart.t
index 70111b9..3737b51 100644
--- a/t/web/saved_search_chart.t
+++ b/t/web/saved_search_chart.t
@@ -58,7 +58,7 @@ $m->submit_form(
     form_name => 'SaveSearch',
     fields    => {
         Query          => 'id=2',
-        PrimaryGroupBy => 'Status',
+        GroupBy        => 'Status',
         ChartStyle     => 'pie',
     },
     button => 'SavedSearchSave',
@@ -67,13 +67,13 @@ $m->submit_form(
 $m->content_contains("Chart first chart updated", 'found updated message' );
 $m->content_contains("id=2",                      'Query is updated' );
 $m->content_like( qr/value="Status"\s+selected="selected"/,
-    'PrimaryGroupBy is updated' );
+    'GroupBy is updated' );
 $m->content_like( qr/value="pie"\s+selected="selected"/,
     'ChartType is updated' );
 ok( $search->Load($id) );
 is( $search->SubValue('Query'), 'id=2', 'Query is indeed updated' );
-is( $search->SubValue('PrimaryGroupBy'),
-    'Status', 'PrimaryGroupBy is indeed updated' );
+is( $search->SubValue('GroupBy'),
+    'Status', 'GroupBy is indeed updated' );
 is( $search->SubValue('ChartStyle'), 'pie', 'ChartStyle is indeed updated' );
 
 # finally, let's test delete

commit d28c4bc39c297a904086502b3f4459bc9021139f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 22 18:44:15 2011 +0400

    y_min_value, 10% lower than minimum value

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 1866701..a2682f1 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -96,6 +96,7 @@ if ( $Cache and my $data = delete $session{'charts_cache'}{ $Cache } ) {
 
 my @data = ([],[]);
 my $max_value = 0;
+my $min_value;
 my $max_key_length = 0;
 while ( my $entry = $report->Next ) {
     my $key = join ' - ', map $entry->LabelValue( $_ ), @{ $columns{'Groups'} };
@@ -112,6 +113,7 @@ while ( my $entry = $report->Next ) {
 
     foreach my $v ( @values ) {
         $max_value = $v if $max_value < $v;
+        $min_value = $v if !defined $min_value || $min_value > $v;
     }
     $max_key_length = length $key if $max_key_length < length $key;
 }
@@ -180,7 +182,8 @@ if ($chart_class eq "GD::Graph::bars") {
 # use a top margin enough to display values over the top line if needed
         t_margin => 18,
 # the following line to make sure there's enough space for values to show
-        y_max_value => 5*(int($max_value/5) + 2),
+        y_max_value => $max_value * 1.10,
+        y_min_value => $min_value * 0.9,
 # if there're too many bars or at least one key is too long, use vertical
         x_labels_vertical => ( $count * $max_key_length > 60 ) ? 1 : 0,
         bargroup_spacing => $args{'bar_spacing'}*5,

commit 0addaaf0b40dc94b0e7065168c1af239dbf9e6fa
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jun 25 00:13:47 2011 +0400

    register function generator for Custom Fields

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 022a899..f4983da 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -175,6 +175,7 @@ our %GROUPINGS_META = (
             }
             return @res;
         },
+        Function => 'GenerateCustomFieldFunction',
         Label => sub {
             my $self = shift;
             my %args = (@_);

commit 2750f2756fa824da8ca73a55f25c11540ba19d62
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jun 25 00:14:29 2011 +0400

    "Time" statistic instead of "Simple" for time fields

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index f4983da..617cb67 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -198,20 +198,20 @@ 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(TimeWorked)' => ['Total time worked',   'Time', 'SUM', 'TimeWorked' ],
+    'AVG(TimeWorked)' => ['Average time worked', 'Time', 'AVG', 'TimeWorked' ],
+    'MIN(TimeWorked)' => ['Minimum time worked', 'Time', 'MIN', 'TimeWorked' ],
+    'MAX(TimeWorked)' => ['Maximum time worked', 'Time', '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(TimeEstimated)' => ['Total time estimated',   'Time', 'SUM', 'TimeEstimated' ],
+    'AVG(TimeEstimated)' => ['Average time estimated', 'Time', 'AVG', 'TimeEstimated' ],
+    'MIN(TimeEstimated)' => ['Minimum time estimated', 'Time', 'MIN', 'TimeEstimated' ],
+    'MAX(TimeEstimated)' => ['Maximum time estimated', 'Time', '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(TimeLeft)' => ['Total time left',   'Time', 'SUM', 'TimeLeft' ],
+    'AVG(TimeLeft)' => ['Average time left', 'Time', 'AVG', 'TimeLeft' ],
+    'MIN(TimeLeft)' => ['Minimum time left', 'Time', 'MIN', 'TimeLeft' ],
+    'MAX(TimeLeft)' => ['Maximum time left', 'Time', 'MAX', 'TimeLeft' ],
 
     'SUM(Created-Resolved)'
         => ['Summary of Created-Resolved', 'DateTimeInterval', 'SUM', 'Created', 'Resolved' ],
@@ -256,6 +256,20 @@ our %STATISTICS_META = (
             return (FUNCTION => $function, FIELD => $field);
         },
     },
+    Time => {
+        Function => sub {
+            my $self = shift;
+            my ($function, $field) = @_;
+            return (FUNCTION => $function, FIELD => $field);
+        },
+        Display => sub {
+            my $self = shift;
+            my %args = @_;
+            my $v = $args{'VALUE'};
+            return $self->loc("(no value)") unless defined $v && length $v;
+            return RT::Date->new( $self->CurrentUser )->DurationAsString( $v );
+        },
+    },
     DateTimeInterval => {
         Function => sub {
             my $self = shift;

commit a54889c6a20d0eca6701ee93c7a0852002644f9c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jun 27 15:15:05 2011 +0400

    time worked is in minutes

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 617cb67..b78bb3b 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -267,7 +267,7 @@ our %STATISTICS_META = (
             my %args = @_;
             my $v = $args{'VALUE'};
             return $self->loc("(no value)") unless defined $v && length $v;
-            return RT::Date->new( $self->CurrentUser )->DurationAsString( $v );
+            return RT::Date->new( $self->CurrentUser )->DurationAsString( $v*60 );
         },
     },
     DateTimeInterval => {

commit 97040807f05c06e2767b2b647bc77fd747f197ee
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jun 27 15:16:17 2011 +0400

    don't print zero totals

diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index e32dfab..37e442b 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -129,7 +129,9 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy
 <tr class="<% $i%2 ? 'evenline' : 'oddline' %> total">
 <td class="label collection-as-table" colspan="<% scalar @{ $columns{'Groups'} } %>"><% loc('Total') %></td>
 % foreach my $column ( @{ $columns{'Functions'} } ) {
-% if ( my $code = $report->LabelValueCode( $column ) ) {
+% if ( !$total{ $column } ) {
+<td class="value collection-as-table"> </td>
+% } elsif ( my $code = $report->LabelValueCode( $column ) ) {
 % my $info = $report->ColumnInfo( $column );
 <td class="value collection-as-table"><% $code->( $report, %$info, VALUE => $total{ $column } ) %></td>
 % } else {

commit 00bccffaeb999eac89646b4ad255805b2d4c1858
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jun 27 15:17:19 2011 +0400

    get rid _StatsToFunction so we can implement post calc functions

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index b78bb3b..a2c5745 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -382,6 +382,7 @@ sub ColumnInfo {
 
     return $self->{'column_info'}{$column};
 }
+
 sub ColumnsList {
     my $self = shift;
     return keys %{ $self->{'column_info'} || {} };
@@ -424,20 +425,42 @@ sub SetupGroupings {
         push @{ $res{'Groups'} }, $group_by->{'NAME'};
     }
 
+    %STATISTICS = @STATISTICS unless keys %STATISTICS;
+
     my @function = grep defined && length,
         ref( $args{'Function'} )? @{ $args{'Function'} } : ($args{'Function'});
     foreach my $e ( @function ) {
-        my %args = $self->_StatsToFunction( $e );
-        $args{'TYPE'} = 'statistic';
-        $args{'INFO'} = $STATISTICS{ $e };
-        $args{'META'} = $STATISTICS_META{ $args{'INFO'}[1] };
-        $args{'NAME'} = $self->Column(
-            ALIAS    => $args{'ALIAS'},
-            FIELD    => $args{'FIELD'},
-            FUNCTION => $args{'FUNCTION'},
-        );
-        push @{ $res{'Functions'} }, $args{'NAME'};
-        $column_info{ $args{'NAME'} } = \%args;
+        $e = {
+            TYPE => 'statistic',
+            KEY  => $e,
+            INFO => $STATISTICS{ $e },
+            META => $STATISTICS_META{ $STATISTICS{ $e }[1] },
+
+        };
+        unless ( $e->{'INFO'} && $e->{'META'} ) {
+            $RT::Logger->error("'". $e->{'KEY'} ."' is not valid statistic for report");
+            $e->{'FUNCTION'} = 'NULL';
+            $e->{'NAME'} = $self->Column( FUNCTION => 'NULL' );
+        }
+        elsif ( $e->{'META'}{'Function'} ) {
+            my $code = $self->FindImplementationCode( $e->{'META'}{'Function'} );
+            unless ( $code ) {
+                $e->{'FUNCTION'} = 'NULL';
+                $e->{'NAME'} = $self->Column( FUNCTION => 'NULL' );
+            }
+            else {
+                my %tmp = $code->( $self, @{ $e->{INFO} }[2 .. scalar @{ $e->{INFO} } -1 ] );
+                $e->{'NAME'} = $self->Column( %tmp );
+                @{ $e }{'FUNCTION', 'ALIAS', 'FIELD'} = @tmp{'FUNCTION', 'ALIAS', 'FIELD'};
+            }
+        }
+        elsif ( $e->{'META'}{'Calculate'} ) {
+            # ....
+        }
+        else {
+        }
+        push @{ $res{'Functions'} }, $e->{'NAME'};
+        $column_info{ $e->{'NAME'} } = $e;
     }
 
     $self->{'column_info'} = \%column_info;
@@ -489,25 +512,6 @@ 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.
 sub AddRecord {

commit 738e19c7f6bd1fc7cd6b670f1e8deb07a24c67fe
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jun 27 15:22:00 2011 +0400

    drop AddEmptyRow as it doesn't work

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index a2c5745..27ae06a 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -484,7 +484,6 @@ sub _DoSearch {
         );
     }
     else {
-        $self->AddEmptyRows;
     }
 }
 
@@ -543,31 +542,6 @@ sub NewItem {
 # correct class.  However, since we're abusing a subclass, it's incorrect.
 sub _RoleGroupClass { "RT::Ticket" }
 
-
-=head2 AddEmptyRows
-
-If we're grouping on a criterion we know how to add zero-value rows
-for, do that.
-
-=cut
-
-sub AddEmptyRows {
-    my $self = shift;
-    if ( @{ $self->{'_group_by_field'} || [] } == 1 && $self->{'_group_by_field'}[0] eq 'Status' ) {
-        my %has = map { $_->__Value('Status') => 1 } @{ $self->ItemsArrayRef || [] };
-
-        foreach my $status ( grep !$has{$_}, RT::Queue->new($self->CurrentUser)->StatusArray ) {
-
-            my $record = $self->NewItem;
-            $record->LoadFromHash( {
-                id     => 0,
-                status => $status
-            } );
-            $self->AddRecord($record);
-        }
-    }
-}
-
 { our @SORT_OPS;
 sub __sort_function_we_need_named($$) {
     for my $f ( @SORT_OPS ) {

commit 4a4d5c7aefd391d159fc954ee0d1d7bbf33abfdd
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jun 27 22:33:34 2011 +0400

    move chart's table formatting into libs

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 27ae06a..60aca1a 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -745,6 +745,72 @@ sub Deserialize {
     }
 }
 
+
+sub FormatTable {
+    my $self = shift;
+    my %columns = @_;
+
+    my @head = ({ cells => []});
+    foreach my $column ( @{ $columns{'Groups'} }, @{ $columns{'Functions'} } ) {
+        push @{ $head[0]{'cells'} }, { type => 'head', value => $self->Label($column) };
+    }
+
+    my %total;
+
+    my @body;
+    my $i = 0;
+    my @groups_stack = ();
+    while ( my $entry = $self->Next ) {
+        my %row = (
+            even => ++$i%2,
+            cells => [],
+        );
+        foreach my $column ( @{ $columns{'Groups'} } ) {
+            push @{ $row{'cells'} }, { type => 'label', value => $entry->LabelValue( $column ) };
+        }
+
+        my $entry_query = $entry->Query;
+        
+        foreach my $column ( @{ $columns{'Functions'} } ) {
+            my $raw = $entry->RawValue( $column );
+            $total{ $column } += $raw if defined $raw && length $raw;
+
+            my $value = $entry->LabelValue( $column );
+            push @{ $row{'cells'} }, { type => 'value', value => $value, query => $entry_query };
+        }
+        push @body, \%row;
+    }
+
+    my @footer;
+    {
+        my %row = (
+            even => ++$i%2,
+            cells => [],
+        );
+        push @{ $row{'cells'} }, {
+            type => 'label',
+            value => $self->loc('Total'),
+            colspan => scalar @{ $columns{'Groups'} }
+        };
+        foreach my $column ( @{ $columns{'Functions'} } ) {
+            my %cell = ( type => 'value' );
+            if ( !$total{ $column } ) {
+                $cell{'value'} = undef;
+            }
+            elsif ( my $code = $self->LabelValueCode( $column ) ) {
+                my $info = $self->ColumnInfo( $column );
+                $cell{'value'} = $code->( $self, %$info, VALUE => $total{ $column } );
+            } else {
+                $cell{'value'} = $total{ $column };
+            }
+            push @{ $row{'cells'} }, \%cell;
+        }
+        push @footer, \%row;
+    }
+
+    return thead => \@head, tbody => \@body, tfoot => \@footer;
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 37e442b..bb8f72f 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -84,14 +84,6 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy
 <img src="<% RT->Config->Get('WebPath') %>/Search/Chart?Cache=<% $key |un %>&<% $query_string |n %>" />
 % }
 </span>
-<table class="collection-as-table chart">
-
-<tr>
-% foreach my $column ( @{ $columns{'Groups'} }, @{ $columns{'Functions'} } ) {
-<th class="collection-as-table"><% $report->Label( $column ) %></th>
-% }
-</tr>
-
 % my $base_query = $m->comp('/Elements/QueryString',
 %     Format  => $ARGS{Format},
 %     Rows    => $ARGS{Rows},
@@ -99,47 +91,43 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy
 %     Order   => $ARGS{Order},
 % );
 
-% my $i = 0;
-% my %total = map { $_ => 0 } @{ $columns{Functions} };
-% while ( my $entry = $report->Next ) {
-<tr class="<% ++$i%2 ? 'evenline' : 'oddline' %>">
+% my %table = $report->FormatTable( %columns );
+<table class="collection-as-table chart">
+% foreach my $section (qw(thead tbody tfoot)) {
+% next unless $table{ $section } && @{ $table{ $section } };
+<<% $section %>>
+% foreach my $row ( @{ $table{ $section } } ) {
 
-% foreach my $column ( @{ $columns{'Groups'} } ) {
-<td class="label collection-as-table"><% $entry->LabelValue( $column ) %></td>
+<tr \
+  class="<% defined $row->{'even'}?  $row->{'even'}? 'evenline' : 'oddline' : '' %>" \
+>
+% foreach my $cell ( @{ $row->{'cells'} } ) {
+% my @class = ('collection-as-table');
+% push @class, ($cell->{'type'}) unless $cell->{'type'} eq 'head';
+% my $tag = $cell->{'type'} eq 'value'? 'td' : 'th';
+<<% $tag %> class="<% join ' ', @class %>" \
+% if ( $cell->{'colspan'} ) {
+colspan="<% int $cell->{'colspan'} %>" \
 % }
-
-% my $entry_query = $entry->Query;
-
-% foreach my $column ( @{ $columns{'Functions'} } ) {
-<td class="value collection-as-table">
-% $total{ $column } += $entry->RawValue( $column );
-% my $value = $entry->LabelValue( $column );
-% if ( $entry_query ) {
-<a href="<% RT->Config->Get('WebPath') %>/Search/Results.html?Query=<% "$Query AND $entry_query" |un %>&<% $base_query %>"><% $value %></a>
-% } else {
-<% $value %>
+% if ( $cell->{'rowspan'} ) {
+rowspan="<% int $cell->{'rowspan'} %>" \
 % }
-</td>
-% }
-
-</tr>
+>\
+% if ( defined $cell->{'value'} ) {
+% if ( my $q = $cell->{'query'} ) {
+<a href="<% RT->Config->Get('WebPath') %>/Search/Results.html?Query=<% "$Query AND $q" |un %>&<% $base_query %>"><% $cell->{'value'} %></a>\
+% } else {
+<% $cell->{'value'} %>\
 % }
-
-% $i++;
-<tr class="<% $i%2 ? 'evenline' : 'oddline' %> total">
-<td class="label collection-as-table" colspan="<% scalar @{ $columns{'Groups'} } %>"><% loc('Total') %></td>
-% foreach my $column ( @{ $columns{'Functions'} } ) {
-% if ( !$total{ $column } ) {
-<td class="value collection-as-table"> </td>
-% } elsif ( my $code = $report->LabelValueCode( $column ) ) {
-% my $info = $report->ColumnInfo( $column );
-<td class="value collection-as-table"><% $code->( $report, %$info, VALUE => $total{ $column } ) %></td>
 % } else {
-<td class="value collection-as-table"><% $total{ $column } %></td>
+ \
 % }
+</<% $tag %>>
 % }
 </tr>
-
+% }
+</<% $section %>>
+% }
 </table>
 <div class="query"><span class="label"><% loc('Query') %>:</span><span class="value"><% $Query %></span></div>
 </div>

commit ab2dd0492988bd3997f6de1a68c78b6622f1ecc3
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jun 27 22:59:19 2011 +0400

    move table rendering into it's own component

diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index bb8f72f..b6c939c 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -84,50 +84,6 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy
 <img src="<% RT->Config->Get('WebPath') %>/Search/Chart?Cache=<% $key |un %>&<% $query_string |n %>" />
 % }
 </span>
-% my $base_query = $m->comp('/Elements/QueryString',
-%     Format  => $ARGS{Format},
-%     Rows    => $ARGS{Rows},
-%     OrderBy => $ARGS{OrderBy},
-%     Order   => $ARGS{Order},
-% );
-
-% my %table = $report->FormatTable( %columns );
-<table class="collection-as-table chart">
-% foreach my $section (qw(thead tbody tfoot)) {
-% next unless $table{ $section } && @{ $table{ $section } };
-<<% $section %>>
-% foreach my $row ( @{ $table{ $section } } ) {
-
-<tr \
-  class="<% defined $row->{'even'}?  $row->{'even'}? 'evenline' : 'oddline' : '' %>" \
->
-% foreach my $cell ( @{ $row->{'cells'} } ) {
-% my @class = ('collection-as-table');
-% push @class, ($cell->{'type'}) unless $cell->{'type'} eq 'head';
-% my $tag = $cell->{'type'} eq 'value'? 'td' : 'th';
-<<% $tag %> class="<% join ' ', @class %>" \
-% if ( $cell->{'colspan'} ) {
-colspan="<% int $cell->{'colspan'} %>" \
-% }
-% if ( $cell->{'rowspan'} ) {
-rowspan="<% int $cell->{'rowspan'} %>" \
-% }
->\
-% if ( defined $cell->{'value'} ) {
-% if ( my $q = $cell->{'query'} ) {
-<a href="<% RT->Config->Get('WebPath') %>/Search/Results.html?Query=<% "$Query AND $q" |un %>&<% $base_query %>"><% $cell->{'value'} %></a>\
-% } else {
-<% $cell->{'value'} %>\
-% }
-% } else {
- \
-% }
-</<% $tag %>>
-% }
-</tr>
-% }
-</<% $section %>>
-% }
-</table>
+<& ChartTable, %ARGS, Table => { $report->FormatTable( %columns ) } &>
 <div class="query"><span class="label"><% loc('Query') %>:</span><span class="value"><% $Query %></span></div>
 </div>
diff --git a/share/html/Search/Elements/ChartTable b/share/html/Search/Elements/ChartTable
new file mode 100644
index 0000000..6ef0db7
--- /dev/null
+++ b/share/html/Search/Elements/ChartTable
@@ -0,0 +1,67 @@
+<%ARGS>
+%Table => ()
+$Query => undef
+</%ARGS>
+<%INIT>
+
+my $base_query = $m->comp('/Elements/QueryString',
+    Format  => $ARGS{Format},
+    Rows    => $ARGS{Rows},
+    OrderBy => $ARGS{OrderBy},
+    Order   => $ARGS{Order},
+);
+
+my $interp = $m->interp;
+my $eh  = sub { $interp->apply_escapes( @_, 'h' ) };
+my $eu  = sub { $interp->apply_escapes( @_, 'u' ) };
+
+$m->out('<table class="collection-as-table chart">'. "\n");
+foreach my $section (qw(thead tbody tfoot)) {
+    next unless $Table{ $section } && @{ $Table{ $section } };
+
+    $m->out("<$section>\n");
+    foreach my $row ( @{ $Table{ $section } } ) {
+        $m->out('  <tr');
+        $m->out(' class="'. ($row->{'even'}? 'evenline' : 'oddline') .'"')
+            if defined $row->{'even'};
+        $m->out(">");
+
+        foreach my $cell ( @{ $row->{'cells'} } ) {
+            my $tag = $cell->{'type'} eq 'value'? 'td' : 'th';
+            $m->out("<$tag");
+
+            my @class = ('collection-as-table');
+            push @class, ($cell->{'type'}) unless $cell->{'type'} eq 'head';
+            $m->out(' class="'. $eh->( join ' ', @class ) .'"');
+
+            foreach my $dir ( grep $cell->{$_}, qw(rowspan colspan) ) {
+                my $value = int $cell->{ $dir };
+                $m->out(qq{ $dir="$value"});
+            }
+            $m->out('>');
+            if ( defined $cell->{'value'} ) {
+                if ( my $q = $cell->{'query'} ) {
+                    $m->out(
+                        '<a href="'. RT->Config->Get('WebPath') .'/Search/Results.html'
+                        .'?Query='. $eu->("$Query AND $q")
+                        .'&'. $base_query
+                        . '">'
+                    );
+                    $m->out( $eh->( $cell->{'value'} ) );
+                    $m->out('</a>');
+                }
+                else {
+                    $m->out( $eh->( $cell->{'value'} ) );
+                }
+            }
+            else {
+                $m->out(' ');
+            }
+            $m->out("</$tag>");
+        }
+        $m->out("</tr>\n");
+    }
+    $m->out("</$section>\n\n");
+}
+$m->out("</table>");
+</%INIT>

commit c1f7b00aa853c904c235bbf7fb321e089fc72b09
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jun 27 23:19:41 2011 +0400

    first pass at caculating stats after querying DB

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 60aca1a..2f6e711 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -455,9 +455,7 @@ sub SetupGroupings {
             }
         }
         elsif ( $e->{'META'}{'Calculate'} ) {
-            # ....
-        }
-        else {
+            $e->{'NAME'} = 'postfunction'. $self->{'postfunctions'}++;
         }
         push @{ $res{'Functions'} }, $e->{'NAME'};
         $column_info{ $e->{'NAME'} } = $e;
@@ -484,6 +482,7 @@ sub _DoSearch {
         );
     }
     else {
+        $self->CalculatePostFunctions;
     }
 }
 
@@ -597,6 +596,40 @@ sub SortEntries {
     ];
 } }
 
+sub CalculatePostFunctions {
+    my $self = shift;
+
+    my $info = $self->{'column_info'};
+    foreach my $column ( values %$info ) {
+        next unless $column->{'TYPE'} eq 'statistic';
+        next unless $column->{'META'}{'Calculate'};
+
+        $self->CalculatePostFunction( $column );
+    }
+}
+
+sub CalculatePostFunction {
+    my $self = shift;
+    my $info = shift;
+
+    my $code = $self->FindImplementationCode( $info->{'META'}{'Calculate'} );
+    unless ( $code ) {
+        # TODO: fill in undefs
+        return;
+    }
+
+    my $column = $info->{'NAME'};
+
+    my $base_query = $self->{'_sql_query'};
+    foreach my $item ( @{ $self->{'items'} } ) {
+        $item->{'values'}{$column} = $code->(
+            $self,
+            Query => join( ' AND ', grep defined && length, $base_query, $item->Query ),
+        );
+        $item->{'fetched'}{$column} = 1;
+    }
+}
+
 sub GenerateDateFunction {
     my $self = shift;
     my %args = @_;
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 35e9cef..49a229a 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -128,6 +128,7 @@ sub Query {
             push @parts, "$field $op $value";
         }
     }
+    return () unless @parts;
     return join ' AND ', grep defined && length, @parts;
 }
 

commit fec9fa89510b3bd57d425f0e8c2c55dad844260c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jun 27 23:57:03 2011 +0400

    reverse how we process table, make it column first

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 2f6e711..b33189b 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -791,27 +791,36 @@ sub FormatTable {
     my %total;
 
     my @body;
+
     my $i = 0;
-    my @groups_stack = ();
     while ( my $entry = $self->Next ) {
-        my %row = (
-            even => ++$i%2,
-            cells => [],
-        );
-        foreach my $column ( @{ $columns{'Groups'} } ) {
-            push @{ $row{'cells'} }, { type => 'label', value => $entry->LabelValue( $column ) };
+        $body[ $i ] = { even => ($i+1)%2, cells => [] };
+        $i++;
+    }
+
+    foreach my $column ( @{ $columns{'Groups'} } ) {
+        $i = 0;
+        while ( my $entry = $self->Next ) {
+            push @{ $body[ $i++ ]{'cells'} }, {
+                type => 'label',
+                value => $entry->LabelValue( $column )
+            };
         }
+    }
 
-        my $entry_query = $entry->Query;
-        
-        foreach my $column ( @{ $columns{'Functions'} } ) {
+    foreach my $column ( @{ $columns{'Functions'} } ) {
+        $i = 0;
+        while ( my $entry = $self->Next ) {
             my $raw = $entry->RawValue( $column );
             $total{ $column } += $raw if defined $raw && length $raw;
 
             my $value = $entry->LabelValue( $column );
-            push @{ $row{'cells'} }, { type => 'value', value => $value, query => $entry_query };
+            push @{ $body[ $i++ ]{'cells'} }, {
+                type => 'value',
+                value => $value,
+                query => $entry->Query,
+            };
         }
-        push @body, \%row;
     }
 
     my @footer;

commit ae9df55dad5c530d714279f6f28957c595ed78a3
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 28 00:08:27 2011 +0400

    simplify how we calculate totals for chart's table

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index b33189b..edf2ac0 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -783,20 +783,19 @@ sub FormatTable {
     my $self = shift;
     my %columns = @_;
 
-    my @head = ({ cells => []});
+    my (@head, @body, @footer);
+
+    @head = ({ cells => []});
     foreach my $column ( @{ $columns{'Groups'} }, @{ $columns{'Functions'} } ) {
         push @{ $head[0]{'cells'} }, { type => 'head', value => $self->Label($column) };
     }
 
-    my %total;
-
-    my @body;
-
     my $i = 0;
     while ( my $entry = $self->Next ) {
         $body[ $i ] = { even => ($i+1)%2, cells => [] };
         $i++;
     }
+    @footer = ({ even => ++$i%2, cells => []});
 
     foreach my $column ( @{ $columns{'Groups'} } ) {
         $i = 0;
@@ -807,12 +806,18 @@ sub FormatTable {
             };
         }
     }
+    push @{ $footer[0]{'cells'} }, {
+        type => 'label',
+        value => $self->loc('Total'),
+        colspan => scalar @{ $columns{'Groups'} },
+    };
 
     foreach my $column ( @{ $columns{'Functions'} } ) {
         $i = 0;
+        my $total;
         while ( my $entry = $self->Next ) {
             my $raw = $entry->RawValue( $column );
-            $total{ $column } += $raw if defined $raw && length $raw;
+            $total += $raw if defined $raw && length $raw;
 
             my $value = $entry->LabelValue( $column );
             push @{ $body[ $i++ ]{'cells'} }, {
@@ -821,33 +826,11 @@ sub FormatTable {
                 query => $entry->Query,
             };
         }
-    }
-
-    my @footer;
-    {
-        my %row = (
-            even => ++$i%2,
-            cells => [],
-        );
-        push @{ $row{'cells'} }, {
-            type => 'label',
-            value => $self->loc('Total'),
-            colspan => scalar @{ $columns{'Groups'} }
-        };
-        foreach my $column ( @{ $columns{'Functions'} } ) {
-            my %cell = ( type => 'value' );
-            if ( !$total{ $column } ) {
-                $cell{'value'} = undef;
-            }
-            elsif ( my $code = $self->LabelValueCode( $column ) ) {
-                my $info = $self->ColumnInfo( $column );
-                $cell{'value'} = $code->( $self, %$info, VALUE => $total{ $column } );
-            } else {
-                $cell{'value'} = $total{ $column };
-            }
-            push @{ $row{'cells'} }, \%cell;
+        if ( $total and my $code = $self->LabelValueCode( $column ) ) {
+            my $info = $self->ColumnInfo( $column );
+            $total = $code->( $self, %$info, VALUE => $total );
         }
-        push @footer, \%row;
+        push @{ $footer[0]{'cells'} }, { type => 'value', value => $total };
     }
 
     return thead => \@head, tbody => \@body, tfoot => \@footer;

commit 1bf7e1d42c972cf8befe0d088c6d8557ccbcbf6c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 28 04:51:03 2011 +0400

    get rid of undef warning

diff --git a/share/html/Search/Elements/ChartTable b/share/html/Search/Elements/ChartTable
index 6ef0db7..f0ad1cf 100644
--- a/share/html/Search/Elements/ChartTable
+++ b/share/html/Search/Elements/ChartTable
@@ -43,7 +43,7 @@ foreach my $section (qw(thead tbody tfoot)) {
                 if ( my $q = $cell->{'query'} ) {
                     $m->out(
                         '<a href="'. RT->Config->Get('WebPath') .'/Search/Results.html'
-                        .'?Query='. $eu->("$Query AND $q")
+                        .'?Query='. $eu->(join ' AND ', grep defined && length, $Query, $q)
                         .'&'. $base_query
                         . '">'
                     );

commit 10b2939fd0741bcef818bdec5af19d9047b55c95
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 28 04:51:55 2011 +0400

    don't process references in RT::Record::__Value
    
    Nothing stops LoadFromHash or something else to
    set Value to a hash reference or array. We use
    it in reporting for compound functions.

diff --git a/lib/RT/Record.pm b/lib/RT/Record.pm
index 1942e7e..d93c214 100644
--- a/lib/RT/Record.pm
+++ b/lib/RT/Record.pm
@@ -649,6 +649,7 @@ sub __Value {
     }
 
     my $value = $self->SUPER::__Value($field);
+    return $value if ref $value;
 
     return undef if (!defined $value);
 

commit 672aa4ba8df40cef5c5f1b5fefc08587982c3d87
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 28 07:20:59 2011 +0400

    position columns, so sorting entries works

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index edf2ac0..5f51560 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -385,7 +385,8 @@ sub ColumnInfo {
 
 sub ColumnsList {
     my $self = shift;
-    return keys %{ $self->{'column_info'} || {} };
+    return sort { $self->{'column_info'}{$a}{'POSITION'} <=> $self->{'column_info'}{$b}{'POSITION'} }
+        keys %{ $self->{'column_info'} || {} };
 }
 
 sub SetupGroupings {
@@ -401,6 +402,8 @@ sub SetupGroupings {
 
     %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
 
+    my $i = 0;
+
     my @group_by = grep defined && length,
         ref( $args{'GroupBy'} )? @{ $args{'GroupBy'} } : ($args{'GroupBy'});
     foreach my $e ( @group_by ) {
@@ -409,6 +412,7 @@ sub SetupGroupings {
         $e->{'TYPE'} = 'grouping';
         $e->{'INFO'} = $GROUPINGS{ $key };
         $e->{'META'} = $GROUPINGS_META{ $e->{'INFO'} };
+        $e->{'POSITION'} = $i++;
     }
     $self->GroupBy( map { {
         ALIAS    => $_->{'ALIAS'},
@@ -435,7 +439,7 @@ sub SetupGroupings {
             KEY  => $e,
             INFO => $STATISTICS{ $e },
             META => $STATISTICS_META{ $STATISTICS{ $e }[1] },
-
+            POSITION => $i++,
         };
         unless ( $e->{'INFO'} && $e->{'META'} ) {
             $RT::Logger->error("'". $e->{'KEY'} ."' is not valid statistic for report");

commit fda32acc86e81065eea7711d9bbc048917b0a24b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 28 07:22:12 2011 +0400

    support compex functions in Entry->LabelValue

diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 49a229a..f28c058 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -87,9 +87,20 @@ sub LabelValue {
         return $code->( $self, %{ $self->ColumnInfo( $name ) }, VALUE => $raw );
     }
 
-    return $self->loc('(no value)') unless defined $raw && length $raw;
-    return $self->loc($raw) if $self->ColumnInfo( $name )->{'META'}{'Localize'};
-    return $raw;
+    unless ( ref $raw ) {
+        return $self->loc('(no value)') unless defined $raw && length $raw;
+        return $self->loc($raw) if $self->ColumnInfo( $name )->{'META'}{'Localize'};
+        return $raw;
+    } else {
+        my $loc = $self->ColumnInfo( $name )->{'META'}{'Localize'};
+        my %res = %$raw;
+        if ( $loc ) {
+            $res{ $self->loc($_) } = delete $res{ $_ } foreach keys %res;
+            $_ = $self->loc($_) foreach values %res;
+        }
+        $_ = $self->loc('(no value)') foreach grep !defined || !length, values %res;
+        return \%res;
+    }
 }
 
 sub RawValue {

commit 98024fe3c10897885069dcf7067d9401b54adbfa
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 28 07:24:35 2011 +0400

    table rendering of complex functions with SubValues

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 5f51560..accb2cd 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -790,8 +790,8 @@ sub FormatTable {
     my (@head, @body, @footer);
 
     @head = ({ cells => []});
-    foreach my $column ( @{ $columns{'Groups'} }, @{ $columns{'Functions'} } ) {
-        push @{ $head[0]{'cells'} }, { type => 'head', value => $self->Label($column) };
+    foreach my $column ( @{ $columns{'Groups'} } ) {
+        push @{ $head[0]{'cells'} }, { type => 'head', value => $self->Label( $column ) };
     }
 
     my $i = 0;
@@ -818,23 +818,80 @@ sub FormatTable {
 
     foreach my $column ( @{ $columns{'Functions'} } ) {
         $i = 0;
-        my $total;
+
+        my $info = $self->ColumnInfo( $column );
+
+        my @subs = ('');
+        if ( $info->{'META'}{'SubValues'} ) {
+            @subs = $self->FindImplementationCode( $info->{'META'}{'SubValues'} )->(
+                $self
+            );
+        }
+
+        my %total;
         while ( my $entry = $self->Next ) {
-            my $raw = $entry->RawValue( $column );
-            $total += $raw if defined $raw && length $raw;
+            my $raw = $entry->RawValue( $column ) || {};
+            $raw = { '' => $raw } unless ref $raw;
+            $total{ $_ } += $raw->{ $_ } foreach grep $raw->{$_}, @subs;
+        }
+        @subs = grep $total{$_}, @subs;
 
-            my $value = $entry->LabelValue( $column );
-            push @{ $body[ $i++ ]{'cells'} }, {
-                type => 'value',
-                value => $value,
-                query => $entry->Query,
+        my $label = $self->Label( $column );
+
+        unless (@subs) {
+            while ( my $entry = $self->Next ) {
+                push @{ $body[ $i++ ]{'cells'} }, {
+                    type => 'value',
+                    value => undef,
+                    query => $entry->Query,
+                };
+            }
+            push @{ $head[0]{'cells'} }, {
+                type => 'head',
+                value => $label,
+                rowspan => scalar @head,
+            };
+            push @{ $footer[0]{'cells'} }, { type => 'value', value => undef };
+            next;
+        }
+
+        if ( @subs > 1 && @head == 1 ) {
+            $_->{rowspan} = 2 foreach @{ $head[0]{'cells'} };
+        }
+
+        if ( @subs == 1 ) {
+            push @{ $head[0]{'cells'} }, {
+                type => 'head',
+                value => $label,
+                rowspan => scalar @head,
             };
+        } else {
+            push @{ $head[0]{'cells'} }, { type => 'head', value => $label, colspan => scalar @subs };
+            push @{ $head[1]{'cells'} }, { type => 'head', value => $_ }
+                foreach @subs;
         }
-        if ( $total and my $code = $self->LabelValueCode( $column ) ) {
-            my $info = $self->ColumnInfo( $column );
-            $total = $code->( $self, %$info, VALUE => $total );
+
+        while ( my $entry = $self->Next ) {
+            my $query = $entry->Query;
+            my $value = $entry->LabelValue( $column ) || {};
+            $value = { '' => $value } unless ref $value;
+            foreach my $e ( @subs ) {
+                push @{ $body[ $i ]{'cells'} }, {
+                    type => 'value',
+                    value => $value->{ $e },
+                    query => $query,
+                };
+            }
+            $i++;
+        }
+
+        my $total_code = $self->LabelValueCode( $column );
+        foreach my $e ( @subs ) {
+            my $total = $total{ $e };
+            $total = $total_code->( $self, %$info, VALUE => $total )
+                if $total_code;
+            push @{ $footer[0]{'cells'} }, { type => 'value', value => $total };
         }
-        push @{ $footer[0]{'cells'} }, { type => 'value', value => $total };
     }
 
     return thead => \@head, tbody => \@body, tfoot => \@footer;

commit fe91e32565eaa80c357c4e962ed6c80c28141faf
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 28 07:44:57 2011 +0400

    rowspan equal grouping values

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index accb2cd..a5e83d7 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -803,11 +803,16 @@ sub FormatTable {
 
     foreach my $column ( @{ $columns{'Groups'} } ) {
         $i = 0;
+        my $last;
         while ( my $entry = $self->Next ) {
-            push @{ $body[ $i++ ]{'cells'} }, {
-                type => 'label',
-                value => $entry->LabelValue( $column )
-            };
+            my $value = $entry->LabelValue( $column );
+            if ( !$last || $last->{'value'} ne $value ) {
+                push @{ $body[ $i++ ]{'cells'} }, $last = { type => 'label', value => $value, };
+            }
+            else {
+                $i++;
+                $last->{rowspan} = ($last->{rowspan}||1) + 1;
+            }
         }
     }
     push @{ $footer[0]{'cells'} }, {

commit a38e9f2fc662c1bffff771ab8ee4066e39c51e2e
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 29 00:19:28 2011 +0400

    support picturing complex functions with subvalues

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index a2682f1..a3457d6 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -56,23 +56,10 @@ $ChartStyle => 'bars'
 use GD;
 use GD::Text;
 
- at ChartFunction = grep defined && length, @ChartFunction;
-$ChartStyle = 'bars' if @ChartFunction > 1;
-
-my $chart_class;
-if ($ChartStyle eq 'pie') {
-    require GD::Graph::pie;
-    $chart_class = "GD::Graph::pie";
-} else {
-    require GD::Graph::bars;
-    $chart_class = "GD::Graph::bars";
-}
-
 use RT::Report::Tickets;
 my $report = RT::Report::Tickets->new( $session{'CurrentUser'} );
 
 my %columns;
-
 if ( $Cache and my $data = delete $session{'charts_cache'}{ $Cache } ) {
     %columns = %{ $data->{'columns'} };
     $report->Deserialize( $data->{'report'} );
@@ -100,14 +87,22 @@ my $min_value;
 my $max_key_length = 0;
 while ( my $entry = $report->Next ) {
     my $key = join ' - ', map $entry->LabelValue( $_ ), @{ $columns{'Groups'} };
+    push @{ $data[0] }, $key;
 
-    my @values = map $entry->RawValue($_), @{ $columns{'Functions'} };
-    if ($chart_class eq 'GD::Graph::pie') {
-        $key .= ' - '. $entry->LabelValue( $columns{'Functions'}[0] );
+    my @values;
+    foreach my $column ( @{ $columns{'Functions'} } ) {
+        my $v = $entry->RawValue( $column );
+        unless ( ref $v ) {
+            push @values, $v;
+            next;
+        }
+
+        my @subs = $report->FindImplementationCode(
+            $report->ColumnInfo( $column )->{'META'}{'SubValues'}
+        )->( $report );
+        push @values, map $v->{$_}, @subs;
     }
 
-    push @{ $data[0] }, $key;
-
     my $i = 0;
     push @{ $data[++$i] }, $_ foreach @values;
 
@@ -118,6 +113,23 @@ while ( my $entry = $report->Next ) {
     $max_key_length = length $key if $max_key_length < length $key;
 }
 
+$ChartStyle = 'bars' if @data > 2;
+if ( $ChartStyle eq 'pie' ) {
+    my $i = 0;
+    while ( my $entry = $report->Next ) {
+        $data[0][$i++] .= ' - '. $entry->LabelValue( $columns{'Functions'}[0] );
+    }
+}
+
+my $chart_class;
+if ($ChartStyle eq 'pie') {
+    require GD::Graph::pie;
+    $chart_class = "GD::Graph::pie";
+} else {
+    require GD::Graph::bars;
+    $chart_class = "GD::Graph::bars";
+}
+
 my %font_config = RT->Config->Get('ChartFont');
 my $font = $font_config{ $session{CurrentUser}->UserObj->Lang || '' }
     || $font_config{'others'};

commit 9ea3c30f22b77516b6f84a8527bb0f661ee960d1
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 29 00:20:19 2011 +0400

    NoTotals and NoHideEmpty

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index a5e83d7..5827217 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -834,12 +834,15 @@ sub FormatTable {
         }
 
         my %total;
-        while ( my $entry = $self->Next ) {
-            my $raw = $entry->RawValue( $column ) || {};
-            $raw = { '' => $raw } unless ref $raw;
-            $total{ $_ } += $raw->{ $_ } foreach grep $raw->{$_}, @subs;
+        unless ( $info->{'META'}{'NoTotals'} ) {
+            while ( my $entry = $self->Next ) {
+                my $raw = $entry->RawValue( $column ) || {};
+                $raw = { '' => $raw } unless ref $raw;
+                $total{ $_ } += $raw->{ $_ } foreach grep $raw->{$_}, @subs;
+            }
+            @subs = grep $total{$_}, @subs
+                unless $info->{'META'}{'NoHideEmpty'};
         }
-        @subs = grep $total{$_}, @subs;
 
         my $label = $self->Label( $column );
 
@@ -890,12 +893,19 @@ sub FormatTable {
             $i++;
         }
 
-        my $total_code = $self->LabelValueCode( $column );
-        foreach my $e ( @subs ) {
-            my $total = $total{ $e };
-            $total = $total_code->( $self, %$info, VALUE => $total )
-                if $total_code;
-            push @{ $footer[0]{'cells'} }, { type => 'value', value => $total };
+        unless ( $info->{'META'}{'NoTotals'} ) {
+            my $total_code = $self->LabelValueCode( $column );
+            foreach my $e ( @subs ) {
+                my $total = $total{ $e };
+                $total = $total_code->( $self, %$info, VALUE => $total )
+                    if $total_code;
+                push @{ $footer[0]{'cells'} }, { type => 'value', value => $total };
+            }
+        }
+        else {
+            foreach my $e ( @subs ) {
+                push @{ $footer[0]{'cells'} }, { type => 'value', value => undef };
+            }
         }
     }
 

commit 3eeefa8492d62ae76c6a5fb5bccba50ca30ba155
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jul 1 16:57:37 2011 +0400

    fill in @STATISTICS with foreach loops

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 5827217..3ab0f7c 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -197,40 +197,36 @@ our %GROUPINGS_META = (
 
 our @STATISTICS = (
     COUNT             => ['Tickets', 'Count', 'id'],
-
-    'SUM(TimeWorked)' => ['Total time worked',   'Time', 'SUM', 'TimeWorked' ],
-    'AVG(TimeWorked)' => ['Average time worked', 'Time', 'AVG', 'TimeWorked' ],
-    'MIN(TimeWorked)' => ['Minimum time worked', 'Time', 'MIN', 'TimeWorked' ],
-    'MAX(TimeWorked)' => ['Maximum time worked', 'Time', 'MAX', 'TimeWorked' ],
-
-    'SUM(TimeEstimated)' => ['Total time estimated',   'Time', 'SUM', 'TimeEstimated' ],
-    'AVG(TimeEstimated)' => ['Average time estimated', 'Time', 'AVG', 'TimeEstimated' ],
-    'MIN(TimeEstimated)' => ['Minimum time estimated', 'Time', 'MIN', 'TimeEstimated' ],
-    'MAX(TimeEstimated)' => ['Maximum time estimated', 'Time', 'MAX', 'TimeEstimated' ],
-
-    'SUM(TimeLeft)' => ['Total time left',   'Time', 'SUM', 'TimeLeft' ],
-    'AVG(TimeLeft)' => ['Average time left', 'Time', 'AVG', 'TimeLeft' ],
-    'MIN(TimeLeft)' => ['Minimum time left', 'Time', 'MIN', 'TimeLeft' ],
-    'MAX(TimeLeft)' => ['Maximum time left', 'Time', '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' ],
 );
+
+foreach my $field (qw(TimeWorked TimeEstimated TimeLeft)) {
+    my $friendly = lc join ' ', split /(?<=[a-z])(?=[A-Z])/, $field;
+    push @STATISTICS, (
+        "SUM($field)" => ["Total $friendly",   'Time', 'SUM', $field ],
+        "AVG($field)" => ["Average $friendly", 'Time', 'AVG', $field ],
+        "MIN($field)" => ["Minimum $friendly", 'Time', 'MIN', $field ],
+        "MAX($field)" => ["Maximum $friendly", 'Time', 'MAX', $field ],
+    );
+}
+
+
+foreach my $pair (qw(
+    Created-Started
+    Created-Resolved
+    Created-LastUpdated
+    Starts-Started
+    Due-Resolved
+    Started-Resolved
+)) {
+    my ($from, $to) = split /-/, $pair;
+    push @STATISTICS, (
+        "SUM($pair)" => ["Summary of $pair", 'DateTimeInterval', 'SUM', $from, $to ],
+        "AVG($pair)" => ["Average $pair", 'DateTimeInterval', 'AVG', $from, $to ],
+        "MIN($pair)" => ["Minimum $pair", 'DateTimeInterval', 'MIN', $from, $to ],
+        "MAX($pair)" => ["Maximum $pair", 'DateTimeInterval', 'MAX', $from, $to ],
+    );
+}
+
 our %STATISTICS;
 
 our %STATISTICS_META = (

commit 1f35f0f05f55e3ce58ffce0d0a2d5952ce112792
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jul 1 17:06:46 2011 +0400

    abstract seconds -> string conversion

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 3ab0f7c..2fe85f9 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -229,6 +229,24 @@ foreach my $pair (qw(
 
 our %STATISTICS;
 
+my $duration_to_string_cb = sub {
+    my $self = shift;
+    my %args = @_;
+    my $v = $args{'VALUE'};
+    unless ( ref $v ) {
+        return $self->loc("(no value)") unless defined $v && length $v;
+        return RT::Date->new( $self->CurrentUser )->DurationAsString( $v );
+    }
+
+    my $date = RT::Date->new( $self->CurrentUser );
+    my %res = %$v;
+    foreach my $e ( values %res ) {
+        $e = $date->DurationAsString( $e ) if defined $e && length $e;
+        $e = $self->loc("(no value)") unless defined $e && length $e;
+    }
+    return \%res;
+};
+
 our %STATISTICS_META = (
     Count => {
         Function => sub {
@@ -256,15 +274,9 @@ our %STATISTICS_META = (
         Function => sub {
             my $self = shift;
             my ($function, $field) = @_;
-            return (FUNCTION => $function, FIELD => $field);
-        },
-        Display => sub {
-            my $self = shift;
-            my %args = @_;
-            my $v = $args{'VALUE'};
-            return $self->loc("(no value)") unless defined $v && length $v;
-            return RT::Date->new( $self->CurrentUser )->DurationAsString( $v*60 );
+            return (FUNCTION => "$function(?)*60", FIELD => $field);
         },
+        Display => $duration_to_string_cb,
     },
     DateTimeInterval => {
         Function => sub {
@@ -278,13 +290,7 @@ our %STATISTICS_META = (
 
             return (FUNCTION => "$function($interval)");
         },
-        Display => sub {
-            my $self = shift;
-            my %args = @_;
-            my $v = $args{'VALUE'};
-            return $self->loc("(no value)") unless defined $v && length $v;
-            return RT::Date->new( $self->CurrentUser )->DurationAsString( $v );
-        },
+        Display => $duration_to_string_cb,
     },
 );
 

commit b9e82fc5f7a918efef93c619ae242911d75a9c27
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jul 1 17:12:00 2011 +0400

    SubValues for stats with SQL only
    
    This allows to group several stats like calculatables

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 2fe85f9..c3c3c8c 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -454,6 +454,15 @@ sub SetupGroupings {
                 $e->{'FUNCTION'} = 'NULL';
                 $e->{'NAME'} = $self->Column( FUNCTION => 'NULL' );
             }
+            elsif ( $e->{'META'}{'SubValues'} ) {
+                my %tmp = $code->( $self, @{ $e->{INFO} }[2 .. scalar @{ $e->{INFO} } -1 ] );
+                $e->{'NAME'} = 'postfunction'. $self->{'postfunctions'}++;
+                while ( my ($k, $v) = each %tmp ) {
+                    $e->{'MAP'}{ $k }{'NAME'} = $self->Column( %$v );
+                    @{ $e->{'MAP'}{ $k } }{'FUNCTION', 'ALIAS', 'FIELD'} =
+                        @{ $v }{'FUNCTION', 'ALIAS', 'FIELD'};
+                }
+            }
             else {
                 my %tmp = $code->( $self, @{ $e->{INFO} }[2 .. scalar @{ $e->{INFO} } -1 ] );
                 $e->{'NAME'} = $self->Column( %tmp );
@@ -608,9 +617,12 @@ sub CalculatePostFunctions {
     my $info = $self->{'column_info'};
     foreach my $column ( values %$info ) {
         next unless $column->{'TYPE'} eq 'statistic';
-        next unless $column->{'META'}{'Calculate'};
-
-        $self->CalculatePostFunction( $column );
+        if ( $column->{'META'}{'Calculate'} ) {
+            $self->CalculatePostFunction( $column );
+        }
+        elsif ( $column->{'META'}{'SubValues'} ) {
+            $self->MapSubValues( $column );
+        }
     }
 }
 
@@ -636,6 +648,23 @@ sub CalculatePostFunction {
     }
 }
 
+sub MapSubValues {
+    my $self = shift;
+    my $info = shift;
+
+    my $to = $info->{'NAME'};
+    my $map = $info->{'MAP'};
+
+    foreach my $item ( @{ $self->{'items'} } ) {
+        my $dst = $item->{'values'}{ $to } = { };
+        while (my ($k, $v) = each %{ $map } ) {
+            $dst->{ $k } = delete $item->{'values'}{ $v->{'NAME'} };
+            delete $item->{'fetched'}{ $v->{'NAME'} };
+        }
+        $item->{'fetched'}{ $to } = 1;
+    }
+}
+
 sub GenerateDateFunction {
     my $self = shift;
     my %args = @_;

commit acf98306703774c1073d605bc1215336a78fd73c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jul 1 17:23:57 2011 +0400

    TimeAll and DateTimeIntervalAll statistics

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index c3c3c8c..92d28a3 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -202,6 +202,7 @@ our @STATISTICS = (
 foreach my $field (qw(TimeWorked TimeEstimated TimeLeft)) {
     my $friendly = lc join ' ', split /(?<=[a-z])(?=[A-Z])/, $field;
     push @STATISTICS, (
+        "ALL($field)" => [ucfirst $friendly,   'TimeAll',     $field ],
         "SUM($field)" => ["Total $friendly",   'Time', 'SUM', $field ],
         "AVG($field)" => ["Average $friendly", 'Time', 'AVG', $field ],
         "MIN($field)" => ["Minimum $friendly", 'Time', 'MIN', $field ],
@@ -220,6 +221,7 @@ foreach my $pair (qw(
 )) {
     my ($from, $to) = split /-/, $pair;
     push @STATISTICS, (
+        "ALL($pair)" => [$pair, 'DateTimeIntervalAll', $from, $to ],
         "SUM($pair)" => ["Summary of $pair", 'DateTimeInterval', 'SUM', $from, $to ],
         "AVG($pair)" => ["Average $pair", 'DateTimeInterval', 'AVG', $from, $to ],
         "MIN($pair)" => ["Minimum $pair", 'DateTimeInterval', 'MIN', $from, $to ],
@@ -278,6 +280,20 @@ our %STATISTICS_META = (
         },
         Display => $duration_to_string_cb,
     },
+    TimeAll => {
+        SubValues => sub { return ('Minimum', 'Average', 'Maximum', 'Summary') },
+        Function => sub {
+            my $self = shift;
+            my $field = shift;
+            return (
+                Minimum => { FUNCTION => "MIN(?)*60", FIELD => $field },
+                Average => { FUNCTION => "AVG(?)*60", FIELD => $field },
+                Maximum => { FUNCTION => "MAX(?)*60", FIELD => $field },
+                Summary => { FUNCTION => "SUM(?)*60", FIELD => $field },
+            );
+        },
+        Display => $duration_to_string_cb,
+    },
     DateTimeInterval => {
         Function => sub {
             my $self = shift;
@@ -292,6 +308,26 @@ our %STATISTICS_META = (
         },
         Display => $duration_to_string_cb,
     },
+    DateTimeIntervalAll => {
+        SubValues => sub { return ('Minimum', 'Average', 'Maximum', 'Summary') },
+        Function => sub {
+            my $self = shift;
+            my ($from, $to) = @_;
+
+            my $interval = $self->_Handle->DateTimeIntervalFunction(
+                From => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $from ) },
+                To   => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $to ) },
+            );
+
+            return (
+                Minimum => { FUNCTION => "MIN($interval)" },
+                Average => { FUNCTION => "AVG($interval)" },
+                Maximum => { FUNCTION => "MAX($interval)" },
+                Summary => { FUNCTION => "SUM($interval)" },
+            );
+        },
+        Display => $duration_to_string_cb,
+    },
 );
 
 sub Groupings {

commit c974e31ca8bf7f60cedd055e0c9b75568cc94953
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Aug 9 01:13:53 2011 +0400

    elementary tests for charts

diff --git a/t/charts/basics.t b/t/charts/basics.t
new file mode 100644
index 0000000..21c723d
--- /dev/null
+++ b/t/charts/basics.t
@@ -0,0 +1,103 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT::Test tests => 13;
+use RT::Ticket;
+
+my $q = RT::Test->load_or_create_queue( Name => 'Default' );
+ok $q && $q->id, 'loaded or created queue';
+my $queue = $q->Name;
+
+my (%test) = (0, ());
+
+my @tickets = add_tix_from_data(
+    { Subject => 'n', Status => 'new' },
+    { Subject => 'o', Status => 'open' },
+    { Subject => 'o', Status => 'open' },
+    { Subject => 's', Status => 'stalled' },
+    { Subject => 's', Status => 'stalled' },
+    { Subject => 's', Status => 'stalled' },
+    { Subject => 'r', Status => 'resolved' },
+    { Subject => 'r', Status => 'resolved' },
+    { Subject => 'r', Status => 'resolved' },
+    { Subject => 'r', Status => 'resolved' },
+);
+
+use_ok 'RT::Report::Tickets';
+
+{
+    my $report = RT::Report::Tickets->new( RT->SystemUser );
+    my %columns = $report->SetupGroupings(
+        Query    => 'Queue = '. $q->id,
+        GroupBy  => ['Status'],
+        Function => ['COUNT'],
+    );
+
+    my $expected = {
+        'thead' => [ {
+                'cells' => [
+                    { 'value' => 'Status', 'type' => 'head' },
+                    { 'rowspan' => 1, 'value' => 'Tickets', 'type' => 'head' },
+                ],
+        } ],
+       'tfoot' => [ {
+            'cells' => [
+                { 'colspan' => 1, 'value' => 'Total', 'type' => 'label' },
+                { 'value' => 10, 'type' => 'value' },
+            ],
+            'even' => 1
+        } ],
+       'tbody' => [
+            {
+                'cells' => [
+                    { 'value' => 'new', 'type' => 'label' },
+                    { 'query' => 'Status = \'new\'', 'value' => '1', 'type' => 'value' },
+                ],
+                'even' => 1
+            },
+            {
+                'cells' => [
+                    { 'value' => 'open', 'type' => 'label' },
+                    { 'query' => 'Status = \'open\'', 'value' => '2', 'type' => 'value' }
+                ],
+                'even' => 0
+            },
+            {
+                'cells' => [
+                    { 'value' => 'resolved', 'type' => 'label' },
+                    { 'query' => 'Status = \'resolved\'', 'value' => '4', 'type' => 'value' }
+                ],
+                'even' => 1
+            },
+            {
+                'cells' => [
+                    { 'value' => 'stalled', 'type' => 'label' },
+                    { 'query' => 'Status = \'stalled\'', 'value' => '3', 'type' => 'value' }
+                ],
+                'even' => 0
+            }
+        ]
+    };
+
+    my %table = $report->FormatTable( %columns );
+    is_deeply( \%table, $expected, "basic table" );
+}
+
+
+sub add_tix_from_data {
+    my @data = @_;
+    my @res = ();
+    while (@data) {
+        my $t = RT::Ticket->new($RT::SystemUser);
+        my ( $id, undef $msg ) = $t->Create(
+            Queue => $q->id,
+            %{ shift(@data) },
+        );
+        ok( $id, "ticket created" ) or diag("error: $msg");
+        push @res, $t;
+    }
+    return @res;
+}
+

commit 69c1a83cd01705cd2320175df0b8d5a00cc42b97
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Aug 9 01:43:23 2011 +0400

    don't call FromSQL if Query is not passed
    
    let caller do UnLimit or custom limits

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 92d28a3..1991633 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -436,7 +436,7 @@ sub SetupGroupings {
         @_
     );
 
-    $self->FromSQL( $args{'Query'} );
+    $self->FromSQL( $args{'Query'} ) if $args{'Query'};
 
     %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
 

commit d16255b29acd933aad8bc112be0e47b7c2ff9ec4
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Aug 30 10:03:06 2011 +0400

    query may have "naked OR", so wrap with ()

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 1991633..7f2922f 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -678,7 +678,9 @@ sub CalculatePostFunction {
     foreach my $item ( @{ $self->{'items'} } ) {
         $item->{'values'}{$column} = $code->(
             $self,
-            Query => join( ' AND ', grep defined && length, $base_query, $item->Query ),
+            Query => join(
+                ' AND ', map "($_)", grep defined && length, $base_query, $item->Query,
+            ),
         );
         $item->{'fetched'}{$column} = 1;
     }
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index f28c058..8978c84 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -140,7 +140,7 @@ sub Query {
         }
     }
     return () unless @parts;
-    return join ' AND ', grep defined && length, @parts;
+    return join ' AND ', map "($_)", grep defined && length, @parts;
 }
 
 sub LabelCode {
diff --git a/share/html/Search/Elements/ChartTable b/share/html/Search/Elements/ChartTable
index f0ad1cf..d9d36e9 100644
--- a/share/html/Search/Elements/ChartTable
+++ b/share/html/Search/Elements/ChartTable
@@ -43,7 +43,7 @@ foreach my $section (qw(thead tbody tfoot)) {
                 if ( my $q = $cell->{'query'} ) {
                     $m->out(
                         '<a href="'. RT->Config->Get('WebPath') .'/Search/Results.html'
-                        .'?Query='. $eu->(join ' AND ', grep defined && length, $Query, $q)
+                        .'?Query='. $eu->(join ' AND ', map "($_)", grep defined && length, $Query, $q)
                         .'&'. $base_query
                         . '">'
                     );

commit c23ab7ed433d795da50a4c0cee1ea081c801348d
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Sep 10 20:06:36 2011 +0400

    rename method, better purpose description
    
    s/CalculatePostFunctions/PostProcessRecords/

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 7f2922f..a4f9ede 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -533,7 +533,7 @@ sub _DoSearch {
         );
     }
     else {
-        $self->CalculatePostFunctions;
+        $self->PostProcessRecords;
     }
 }
 
@@ -647,7 +647,7 @@ sub SortEntries {
     ];
 } }
 
-sub CalculatePostFunctions {
+sub PostProcessRecords {
     my $self = shift;
 
     my $info = $self->{'column_info'};

commit 73396c6333fe954515d28dff4f1a97142c2b93c0
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Sep 11 18:01:09 2011 +0400

    test charting with compound SQL function

diff --git a/t/charts/compound-sql-function.t b/t/charts/compound-sql-function.t
new file mode 100644
index 0000000..b8dffce
--- /dev/null
+++ b/t/charts/compound-sql-function.t
@@ -0,0 +1,121 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT::Test tests => 14;
+use RT::Ticket;
+
+my $q1 = RT::Test->load_or_create_queue( Name => 'One' );
+ok $q1 && $q1->id, 'loaded or created queue';
+
+my $q2 = RT::Test->load_or_create_queue( Name => 'Two' );
+ok $q2 && $q2->id, 'loaded or created queue';
+
+my @tickets = add_tix_from_data(
+    { Queue => $q1->id, Resolved => 3*60 },
+    { Queue => $q1->id, Resolved => 3*60*60 },
+    { Queue => $q1->id, Resolved => 3*24*60*60 },
+    { Queue => $q1->id, Resolved => 3*30*24*60*60 },
+    { Queue => $q1->id, Resolved => 9*30*24*60*60 },
+    { Queue => $q2->id, Resolved => 7*60 },
+    { Queue => $q2->id, Resolved => 7*60*60 },
+    { Queue => $q2->id, Resolved => 7*24*60*60 },
+    { Queue => $q2->id, Resolved => 7*30*24*60*60 },
+    { Queue => $q2->id, Resolved => 24*30*24*60*60 },
+);
+
+use_ok 'RT::Report::Tickets';
+
+{
+    my $report = RT::Report::Tickets->new( RT->SystemUser );
+    my %columns = $report->SetupGroupings(
+        Query    => 'id > 0',
+        GroupBy  => ['Queue'],
+        Function => ['ALL(Created-Resolved)'],
+    );
+
+    my $expected = {
+           'thead' => [
+                        {
+                          'cells' => [
+                               { 'rowspan' => 2, 'value' => 'Queue', 'type' => 'head' },
+                               { 'colspan' => 4, 'value' => 'Created-Resolved', 'type' => 'head' }
+                             ]
+                        },
+                        {
+                          'cells' => [
+                               { 'value' => 'Minimum', 'type' => 'head' },
+                               { 'value' => 'Average', 'type' => 'head' },
+                               { 'value' => 'Maximum', 'type' => 'head' },
+                               { 'value' => 'Summary', 'type' => 'head' }
+                             ]
+                        }
+                      ],
+           'tfoot' => [
+                        {
+                          'cells' => [
+                               { 'colspan' => 1, 'value' => 'Total', 'type' => 'label' },
+                               { 'value' => '10 min', 'type' => 'value' },
+                               { 'value' => '9 months', 'type' => 'value' },
+                               { 'value' => '3 years', 'type' => 'value' },
+                               { 'value' => '4 years', 'type' => 'value' }
+                             ],
+                          'even' => 1
+                        }
+                      ],
+           'tbody' => [
+                        {
+                          'cells' => [
+                               { 'value' => 'One', 'type' => 'label' },
+                               { 'query' => '(Queue = 3)', 'value' => '3 min', 'type' => 'value' },
+                               { 'query' => '(Queue = 3)', 'value' => '2 months', 'type' => 'value' },
+                               { 'query' => '(Queue = 3)', 'value' => '9 months', 'type' => 'value' },
+                               { 'query' => '(Queue = 3)', 'value' => '12 months', 'type' => 'value' }
+                             ],
+                          'even' => 1
+                        },
+                        {
+                          'cells' => [
+                               { 'value' => 'Two', 'type' => 'label' },
+                               { 'query' => '(Queue = 4)', 'value' => '7 min', 'type' => 'value' },
+                               { 'query' => '(Queue = 4)', 'value' => '6 months', 'type' => 'value' },
+                               { 'query' => '(Queue = 4)', 'value' => '2 years', 'type' => 'value' },
+                               { 'query' => '(Queue = 4)', 'value' => '3 years', 'type' => 'value' }
+                             ],
+                          'even' => 0
+                        }
+                      ]
+         };
+
+
+    my %table = $report->FormatTable( %columns );
+    is_deeply( \%table, $expected, "basic table" );
+}
+
+
+sub add_tix_from_data {
+    my @data = @_;
+    my @res = ();
+
+    my $created = RT::Date->new( $RT::SystemUser );
+    $created->SetToNow;
+
+    my $resolved = RT::Date->new( $RT::SystemUser );
+
+    while (@data) {
+        $resolved->Set( Value => $created->Unix );
+        $resolved->AddSeconds( $data[0]{'Resolved'} );
+        my $t = RT::Ticket->new($RT::SystemUser);
+        my ( $id, undef $msg ) = $t->Create(
+            %{ shift(@data) },
+            Created => $created->ISO,
+            Resolved => $resolved->ISO,
+        );
+        ok( $id, "ticket created" ) or diag("error: $msg");
+        push @res, $t;
+    }
+    return @res;
+}
+
+

commit 6b6e7e31445f6f4916a9bdd93485a54c0f67a72e
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Sep 13 18:25:06 2011 +0400

    normalize names of statistics displayed in the UI

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index a4f9ede..4eec9af 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -202,7 +202,7 @@ our @STATISTICS = (
 foreach my $field (qw(TimeWorked TimeEstimated TimeLeft)) {
     my $friendly = lc join ' ', split /(?<=[a-z])(?=[A-Z])/, $field;
     push @STATISTICS, (
-        "ALL($field)" => [ucfirst $friendly,   'TimeAll',     $field ],
+        "ALL($field)" => ["Summary of $friendly",   'TimeAll',     $field ],
         "SUM($field)" => ["Total $friendly",   'Time', 'SUM', $field ],
         "AVG($field)" => ["Average $friendly", 'Time', 'AVG', $field ],
         "MIN($field)" => ["Minimum $friendly", 'Time', 'MIN', $field ],
@@ -221,8 +221,8 @@ foreach my $pair (qw(
 )) {
     my ($from, $to) = split /-/, $pair;
     push @STATISTICS, (
-        "ALL($pair)" => [$pair, 'DateTimeIntervalAll', $from, $to ],
-        "SUM($pair)" => ["Summary of $pair", 'DateTimeInterval', 'SUM', $from, $to ],
+        "ALL($pair)" => ["Summary of $pair", 'DateTimeIntervalAll', $from, $to ],
+        "SUM($pair)" => ["Total $pair", 'DateTimeInterval', 'SUM', $from, $to ],
         "AVG($pair)" => ["Average $pair", 'DateTimeInterval', 'AVG', $from, $to ],
         "MIN($pair)" => ["Minimum $pair", 'DateTimeInterval', 'MIN', $from, $to ],
         "MAX($pair)" => ["Maximum $pair", 'DateTimeInterval', 'MAX', $from, $to ],

commit d0f78eb2efc20018f434dd07b7d70a002ac5aceb
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Sep 21 14:30:11 2011 +0400

    adjust charts' tests for lifecycles and last changes

diff --git a/t/charts/basics.t b/t/charts/basics.t
index 21c723d..118d31b 100644
--- a/t/charts/basics.t
+++ b/t/charts/basics.t
@@ -3,7 +3,7 @@
 use strict;
 use warnings;
 
-use RT::Test tests => 13;
+use RT::Test tests => 9;
 use RT::Ticket;
 
 my $q = RT::Test->load_or_create_queue( Name => 'Default' );
@@ -16,10 +16,6 @@ my @tickets = add_tix_from_data(
     { Subject => 'n', Status => 'new' },
     { Subject => 'o', Status => 'open' },
     { Subject => 'o', Status => 'open' },
-    { Subject => 's', Status => 'stalled' },
-    { Subject => 's', Status => 'stalled' },
-    { Subject => 's', Status => 'stalled' },
-    { Subject => 'r', Status => 'resolved' },
     { Subject => 'r', Status => 'resolved' },
     { Subject => 'r', Status => 'resolved' },
     { Subject => 'r', Status => 'resolved' },
@@ -45,39 +41,32 @@ use_ok 'RT::Report::Tickets';
        'tfoot' => [ {
             'cells' => [
                 { 'colspan' => 1, 'value' => 'Total', 'type' => 'label' },
-                { 'value' => 10, 'type' => 'value' },
+                { 'value' => 6, 'type' => 'value' },
             ],
-            'even' => 1
+            'even' => 0
         } ],
        'tbody' => [
             {
                 'cells' => [
                     { 'value' => 'new', 'type' => 'label' },
-                    { 'query' => 'Status = \'new\'', 'value' => '1', 'type' => 'value' },
+                    { 'query' => '(Status = \'new\')', 'value' => '1', 'type' => 'value' },
                 ],
                 'even' => 1
             },
             {
                 'cells' => [
                     { 'value' => 'open', 'type' => 'label' },
-                    { 'query' => 'Status = \'open\'', 'value' => '2', 'type' => 'value' }
+                    { 'query' => '(Status = \'open\')', 'value' => '2', 'type' => 'value' }
                 ],
                 'even' => 0
             },
             {
                 'cells' => [
                     { 'value' => 'resolved', 'type' => 'label' },
-                    { 'query' => 'Status = \'resolved\'', 'value' => '4', 'type' => 'value' }
+                    { 'query' => '(Status = \'resolved\')', 'value' => '3', 'type' => 'value' }
                 ],
                 'even' => 1
             },
-            {
-                'cells' => [
-                    { 'value' => 'stalled', 'type' => 'label' },
-                    { 'query' => 'Status = \'stalled\'', 'value' => '3', 'type' => 'value' }
-                ],
-                'even' => 0
-            }
         ]
     };
 
@@ -91,7 +80,7 @@ sub add_tix_from_data {
     my @res = ();
     while (@data) {
         my $t = RT::Ticket->new($RT::SystemUser);
-        my ( $id, undef $msg ) = $t->Create(
+        my ( $id, undef, $msg ) = $t->Create(
             Queue => $q->id,
             %{ shift(@data) },
         );
diff --git a/t/charts/compound-sql-function.t b/t/charts/compound-sql-function.t
index b8dffce..a9f4b08 100644
--- a/t/charts/compound-sql-function.t
+++ b/t/charts/compound-sql-function.t
@@ -40,7 +40,7 @@ use_ok 'RT::Report::Tickets';
                         {
                           'cells' => [
                                { 'rowspan' => 2, 'value' => 'Queue', 'type' => 'head' },
-                               { 'colspan' => 4, 'value' => 'Created-Resolved', 'type' => 'head' }
+                               { 'colspan' => 4, 'value' => 'Summary of Created-Resolved', 'type' => 'head' }
                              ]
                         },
                         {

commit 843cf91c638b120f95cfdbf1606ce549e0934b28
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Sep 23 22:14:02 2011 +0400

    take care of encoding when we set value to hash
    
    values as data structures is an experimental feature
    that only required in reports. We don't want to
    put everything into RT::Record::__Value method, so
    if something uses this feature then it should take
    care of upgrading data to perl strings.

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 4eec9af..552807a 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -697,6 +697,7 @@ sub MapSubValues {
         my $dst = $item->{'values'}{ $to } = { };
         while (my ($k, $v) = each %{ $map } ) {
             $dst->{ $k } = delete $item->{'values'}{ $v->{'NAME'} };
+            utf8::decode( $dst->{ $k } ) unless utf8::is_utf8( $dst->{ $k } );
             delete $item->{'fetched'}{ $v->{'NAME'} };
         }
         $item->{'fetched'}{ $to } = 1;

commit e759914ef56ce3223c981e586e7a3eed708bb56d
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Sep 23 22:20:58 2011 +0400

    make COUNT to be stat calculated by default
    
    this fixes Quicksearch box without changing it,
    so we are backwards compatible enough.

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 552807a..691deda 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -471,6 +471,7 @@ sub SetupGroupings {
 
     my @function = grep defined && length,
         ref( $args{'Function'} )? @{ $args{'Function'} } : ($args{'Function'});
+    push @function, 'COUNT' unless @function;
     foreach my $e ( @function ) {
         $e = {
             TYPE => 'statistic',

commit 8f18de4bfc0c77aed85b6a9001349fdd79ea0534
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Mar 8 01:50:32 2013 +0400

    branch TODO

diff --git a/TODO.charts b/TODO.charts
new file mode 100644
index 0000000..4667730
--- /dev/null
+++ b/TODO.charts
@@ -0,0 +1,37 @@
+move abuse protection code from callers to SetupGrouping
+
+protect Function in SetupGrouping from abuse
+
+upgrade script for saved charts
+* switch from space separator to dot
+* switch from PrimaryGroupBy to GroupBy name
+
+it'd be nice if full day and month names worked in 
+"Created.DayOfWeek = 'Thu'" searches.
+
+support more than one calculated function in diagrams,
+at least in bars
+
+support putting grouppings as columns of the table
+support horizontal totals when there are gropings as columns
+
+support links with partial queries in tables, for example
+table:
+
+                          Owner
+    Queue      Status     Nobody [3]
+    General[1] open[2]    10[4]
+
+    [1] - $query AND Queue = 'General'
+    [2] - [1] AND Status = 'open'
+    [3] - $query AND Owner = 'Nobody'
+    [4] - [2] AND [3]
+
+
+On my machine Created-LastUpdated interval is 1 second
+for newely created tickets. It's either bug or just
+operation is long enough to span over two seconds. It
+would be nice to ignore difference up to 3 seconds or
+something like that.
+
+During rebase localization of "group by" was lost

commit 88957db17139e2679ccc1895a79219fa964b7039
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Mar 12 01:27:13 2013 +0400

    make sure status was set correctly

diff --git a/t/charts/basics.t b/t/charts/basics.t
index 118d31b..0b5095d 100644
--- a/t/charts/basics.t
+++ b/t/charts/basics.t
@@ -3,15 +3,13 @@
 use strict;
 use warnings;
 
-use RT::Test tests => 9;
+use RT::Test tests => undef;
 use RT::Ticket;
 
-my $q = RT::Test->load_or_create_queue( Name => 'Default' );
+my $q = RT::Test->load_or_create_queue( Name => 'General' );
 ok $q && $q->id, 'loaded or created queue';
 my $queue = $q->Name;
 
-my (%test) = (0, ());
-
 my @tickets = add_tix_from_data(
     { Subject => 'n', Status => 'new' },
     { Subject => 'o', Status => 'open' },
@@ -74,17 +72,18 @@ use_ok 'RT::Report::Tickets';
     is_deeply( \%table, $expected, "basic table" );
 }
 
+done_testing;
+
 
 sub add_tix_from_data {
     my @data = @_;
     my @res = ();
     while (@data) {
+        my %info = %{ shift(@data) };
         my $t = RT::Ticket->new($RT::SystemUser);
-        my ( $id, undef, $msg ) = $t->Create(
-            Queue => $q->id,
-            %{ shift(@data) },
-        );
+        my ( $id, undef, $msg ) = $t->Create( Queue => $q->id, %info );
         ok( $id, "ticket created" ) or diag("error: $msg");
+        is $t->Status, $info{'Status'}, 'correct status';
         push @res, $t;
     }
     return @res;

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


More information about the Rt-commit mailing list