[Rt-commit] rt branch, 4.2/charts-improvments, created. rt-4.0.2-116-gfd955ad

Ruslan Zakirov ruz at bestpractical.com
Thu Sep 22 15:38:21 EDT 2011


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

- Log -----------------------------------------------------------------
commit fe13519b2bad231f2627f77c0d2376b30b19b44c
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 32eb4cc..db5a0f3 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 2b78764621269b856aa62877c2cff29485e34ece
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 db5a0f3..5b6fa5c 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -82,9 +82,9 @@ sub Groupings {
 
 
     for my $field (qw(Due Resolved Created LastUpdated Started Starts)) {
-        for my $frequency (qw(Hourly Daily Monthly Annually)) {
+        for my $frequency (@{ $GROUPINGS{'Date'} }) {
             my $item = $field.$frequency;
-            push @fields,  $item,  $item;
+            push @fields, "$field $frequency", "$field.$frequency";
         }
     }
 

commit c22925e46bf1845d2d79dcfaad3bfe17c365bd27
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 5b6fa5c..b1d3a52 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -201,53 +201,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 c27b98ad11162979a1964c7e6f30972a195fc5d1
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu May 26 02:21:42 2011 +0400

    @rest is an hash actually, we'll need access by key

diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 5401373..461d43c 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -530,7 +530,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 =~ /^(=|>|<|>=|<=)$/;
@@ -559,14 +559,14 @@ sub _DateLimit {
             FIELD    => $meta->[1],
             OPERATOR => ">=",
             VALUE    => $daystart,
-            @rest,
+            %rest,
         );
 
         $sb->_SQLLimit(
             FIELD    => $meta->[1],
             OPERATOR => "<",
             VALUE    => $dayend,
-            @rest,
+            %rest,
             ENTRYAGGREGATOR => 'AND',
         );
 
@@ -578,7 +578,7 @@ sub _DateLimit {
             FIELD    => $meta->[1],
             OPERATOR => $op,
             VALUE    => $date->ISO,
-            @rest,
+            %rest,
         );
     }
 }

commit ee581424c4292ec30dbf6b3c9d8a5a07fe497616
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu May 26 03:22:28 2011 +0400

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

diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 461d43c..a374098 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -539,6 +539,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 );
 

commit 052822e9fdc9dc496f9a86777d80034f13a8f3e3
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 b652d6d..25d3926 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -247,10 +247,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 17057678d704e108902ab20c79b12d4eed88cb57
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun May 29 02:45:01 2011 +0400

    handle not set dates with CASE before applying functions

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index b1d3a52..78702f8 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -214,7 +214,7 @@ sub _FieldToFunction {
 
         $args{'FUNCTION'} = $RT::Handle->DateTimeFunction(
             Type => $subkey,
-            Field => "?",
+            Field => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
             Timezone => $tz,
         );
         $args{'FIELD'} = $key;
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 770269f..025c9a3 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( @_ );
 }
 
 RT::Base->_ImportOverlays();
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index a374098..4b93929 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -549,7 +549,7 @@ sub _DateLimit {
         }
         my $function = $RT::Handle->DateTimeFunction(
             Type     => $subkey,
-            Field    => '?',
+            Field    => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
             Timezone => $tz,
         );
         return $sb->_SQLLimit(
@@ -597,6 +597,7 @@ sub _DateLimit {
     }
     else {
         $sb->_SQLLimit(
+            FUNCTION => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
             FIELD    => $meta->[1],
             OPERATOR => $op,
             VALUE    => $date->ISO,

commit 6189c933831f1f14d05be9b613c3bba62c9aee35
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 78702f8..317d3c9 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
@@ -75,9 +81,7 @@ sub Groupings {
     );
 
     foreach my $type ( qw(Owner Creator LastUpdatedBy Requestor Cc AdminCc Watcher) ) {
-        push @fields, $type.' '.$_, $type.'.'.$_ foreach qw(
-            Name EmailAddress RealName NickName Organization Lang City Country Timezone
-        );
+        push @fields, map { ("$type $_", "$type.$_") } @{ $GROUPINGS{'User'} };
     }
 
 

commit 41dcb1b0e5fab85326c4f860baa76fb9b34149af
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 31 03:14:08 2011 +0400

    drop unused code

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 317d3c9..e39eab8 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -84,10 +84,8 @@ sub Groupings {
         push @fields, map { ("$type $_", "$type.$_") } @{ $GROUPINGS{'User'} };
     }
 
-
     for my $field (qw(Due Resolved Created LastUpdated Started Starts)) {
         for my $frequency (@{ $GROUPINGS{'Date'} }) {
-            my $item = $field.$frequency;
             push @fields, "$field $frequency", "$field.$frequency";
         }
     }

commit 0972491b4de42960ec912e44745d9cad395a84ca
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 4b93929..f5992dd 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -540,6 +540,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
@@ -547,11 +566,13 @@ sub _DateLimit {
             $tz = { From => 'UTC', To => $to }
                 if $to && lc $to ne 'utc';
         }
+
         my $function = $RT::Handle->DateTimeFunction(
             Type     => $subkey,
             Field    => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
             Timezone => $tz,
         );
+
         return $sb->_SQLLimit(
             FUNCTION => $function,
             FIELD    => $meta->[1],

commit df3b10fc0edc31c0ef9531c6a9ec4081afdf35d7
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 e39eab8..a9e2897 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -137,15 +137,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 98fb07ddf23430c956cf88c1f21c8e3ba902792a
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 a9e2897..683ca39 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -293,7 +293,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;
 }
 
 

commit fe79865150f958c22085280f36d0696c1d6e31a2
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 025c9a3..929bb72 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( @_ );
 }
 
 RT::Base->_ImportOverlays();

commit cb7f4faac84cd0eb8a232a74da59269c8d34e577
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 1 02:53:17 2011 +0400

    GroupBy in SB doesn't combine function with field
    
    GroupBy supports functions for a while, but it's
    not yet compatible with Column and new Limit API,
    so we combine ourself

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 683ca39..126a74c 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -167,7 +167,12 @@ sub GroupBy {
     my @args = ref $_[0]? @_ : { @_ };
 
     @{ $self->{'_group_by_field'} ||= [] } = map $_->{'FIELD'}, @args;
-    $_ = { $self->_FieldToFunction( %$_ ) } foreach @args;
+
+    foreach my $e ( @args ) {
+        $e = { $self->_FieldToFunction( %$e ) };
+        $e->{'FUNCTION'} = $self->CombineFunctionWithField( %$e )
+            if $e->{'FUNCTION'};
+    }
 
     $self->SUPER::GroupBy( @args );
 }

commit 0c0879c2fcc866c8704a4b76a1416e551a8486fb
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 9ff8c762dfb2f323303d1c00c1cd6ec212bc1430
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 cf56c24..f1fc175 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 b036565..01d3349 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>
@@ -89,8 +90,11 @@ my @actions = $m->comp( '/Widgets/SavedSearch:process', args => \%ARGS, self =>
 <input type="hidden" class="hidden" name="Query" value="<% $ARGS{Query} %>" />
 <input type="hidden" class="hidden" name="SavedChartSearchId" value="<% $saved_search->{SearchId} || 'new' %>" />
 
-<&|/l, $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,
+  $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 28ffa72..81d88b5 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 0106b1845a97c678681067f240cf5b93a500e384
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 126a74c..62bbc1c 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -134,7 +134,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'} );
 
@@ -144,12 +149,15 @@ sub SetupGroupings {
     # UseSQLForACLChecks may add late joins
     my $joined = ($self->_isJoined || RT->Config->Get('UseSQLForACLChecks')) ? 1 : 0;
 
-    my %column_type;
-
-    my @res;
+    my (@res, %column_type);
 
-    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 ba8f821057fc74b1003a7b5fffb5a7278fe8bdc7
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 1 19:38:46 2011 +0400

    branch specific todo

diff --git a/TODO.charts b/TODO.charts
new file mode 100644
index 0000000..f3a36d2
--- /dev/null
+++ b/TODO.charts
@@ -0,0 +1,7 @@
+expand GROUPINGS to list fields and a few callbacks
+
+move abuse protection code from callers to SetupGrouping
+
+protect Function in SetupGrouping from abuse
+
+extend GroupBy in DBIx::SB with CombineFunctionWithField

commit fe65af5fd2b361a8b75fdb3a21c9354e82f020e1
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 1 23:03:43 2011 +0400

    update branch's TODO

diff --git a/TODO.charts b/TODO.charts
index f3a36d2..18a0276 100644
--- a/TODO.charts
+++ b/TODO.charts
@@ -5,3 +5,12 @@ move abuse protection code from callers to SetupGrouping
 protect Function in SetupGrouping from abuse
 
 extend GroupBy in DBIx::SB with CombineFunctionWithField
+
+Y-axis in charts still always labeled as 'Tickets' even
+if calculate different function
+
+upgrade for saved charts to switch from space separator
+to dot
+
+it'd be nice if full day and month names worked in 
+"Created.DayOfWeek = 'Thu'" searches.

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

    no need in CombineFunctionWithField

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

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

commit 5a2cab692c762f8e57d63b875676ce2d4ec05f7a
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 f1f2e32..7e92156 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 929bb72..890612a 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -79,12 +79,26 @@ sub LabelValue {
     my $raw = $self->RawValue( $name, @_ );
 
     my $type = $self->ColumnType( $name );
-    return $raw unless $type;
+    my $meta = $self->FieldMeta( $type->{'FIELD'} );
+    return $raw unless $meta && $meta->{'Display'};
+
+    my $code;
+    unless ( ref $meta->{'Display'} ) {
+        $code = $self->can( $meta->{'Display'} );
+        unless ( $code ) {
+            $RT::Logger->error("No method ". $meta->{'Display'} );
+            return $raw;
+        }
+    }
+    elsif ( ref( $meta->{'Display'} ) eq 'CODE' ) {
+        $code = $meta->{'Display'};
+    }
+    else {
+        return $raw;
+    }
 
-    my $field = $type->{'FIELD'};
-    return $raw unless $field;
+    return $code->( $self, %$type, VALUE => $raw );
 
-    my ($key, $subkey) = split /\./, $field, 2;
     if ( $subkey && grep $_ eq $subkey, @{ $RT::Report::Tickets::GROUPINGS{'Date'} } ) {
         return $raw unless defined $raw;
         if ( $subkey eq 'DayOfWeek' ) {
@@ -102,6 +116,21 @@ sub RawValue {
     return (shift)->__Value( @_ );
 }
 
+sub FieldMeta {
+    my $self = shift;
+    my $field = shift or return undef;
+
+    ($field) = split /\./, $field, 2;
+
+    %RT::Report::Tickets::GROUPINGS
+        = @RT::Report::Tickets::GROUPINGS
+        unless keys %RT::Report::Tickets::GROUPINGS;
+
+    return $RT::Report::Tickets::GROUPINGS_META{
+        $RT::Report::Tickets::GROUPINGS{ $field }
+    };
+}
+
 RT::Base->_ImportOverlays();
 
 1;

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

    delete code we never reach

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

commit f64c9dedef13c961785aa92852ce22d0998b80f6
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 f91b4c1..dc45dac 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -221,10 +221,14 @@ sub SetupGroupings {
 
     $self->FromSQL( $args{'Query'} );
 
+    %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
+
     my @group_by = ref( $args{'GroupBy'} )? @{ $args{'GroupBy'} } : ($args{'GroupBy'});
     foreach my $e ( @group_by ) {
         my ($key, $subkey) = split /\./, $e, 2;
         $e = { $self->_FieldToFunction( KEY => $key, SUBKEY => $subkey ) };
+        $e->{'TYPE'} = $GROUPINGS{ $key };
+        $e->{'META'} = $GROUPINGS{ $e->{'TYPE'} };
     }
     $self->GroupBy( @group_by );
 
@@ -285,8 +289,6 @@ sub _FieldToFunction {
 
     $args{'FIELD'} ||= $args{'KEY'};
 
-    %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
-
     my $meta = $GROUPINGS_META{ $GROUPINGS{ $args{'KEY'} } };
     return ('FUNCTION' => 'NULL') unless $meta;
 
diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index c7ec4dc..50863c2 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,26 +98,13 @@ sub LabelValue {
         return $raw;
     }
 
-    return $code->( $self, %$type, VALUE => $raw );
+    return $code->( $self, %$meta, VALUE => $raw );
 }
 
 sub RawValue {
     return (shift)->__Value( @_ );
 }
 
-sub FieldMeta {
-    my $self = shift;
-    my $field = shift or return undef;
-
-    %RT::Report::Tickets::GROUPINGS
-        = @RT::Report::Tickets::GROUPINGS
-        unless keys %RT::Report::Tickets::GROUPINGS;
-
-    return $RT::Report::Tickets::GROUPINGS_META{
-        $RT::Report::Tickets::GROUPINGS{ $field }
-    };
-}
-
 RT::Base->_ImportOverlays();
 
 1;

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

    typo

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

commit 3066d47a131f45bc1bb96c10a4ac92cd240e3f7a
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 23c8f96..fd95549 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 d314df8786946022b1dfa9afc063ef1696b04328
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 16:31:53 2011 +0400

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

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index f1fc175..369d371 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -74,28 +74,12 @@ my ($count_name, $value_name) = $tix->SetupGroupings(
     Function => $ChartFunction,
 );
 
-my %class = (
-    Queue => 'RT::Queue',
-    Owner => 'RT::User',
-    Creator => 'RT::User',
-    LastUpdatedBy => 'RT::User',
-);
-my $class = $class{ $PrimaryGroupBy };
-
 my %data;
 my $max_value = 0;
 my $max_key_length = 0;
 while ( my $entry = $tix->Next ) {
-    my $key;
-    if ( $class ) {
-        my $q = $class->new( $session{'CurrentUser'} );
-        $q->Load( $entry->LabelValue( $value_name ) );
-        $key = $q->Name;
-    }
-    else {
-        $key = $entry->LabelValue($value_name);
-    }
-    $key ||= '(no value)';
+    my $key = $entry->LabelValue($value_name)
+        || '(no value)';
     
     my $value = $entry->__Value( $count_name );
     if ($chart_class eq 'GD::Graph::pie') {
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 81d88b5..0c730ca 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 27c5362aeb9426904fbe0be1a7e0c61b6361aafd
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 8 17:25:56 2011 +0400

    update TODO

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

commit 58aaeddfed0fb4b4ba90e15426e1f9e3eeeeabb1
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 fd95549..1e8d1f2 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 d6b7eee163c75177e8ba6d93b6160e7cabf55fc2
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 1e8d1f2..9876cc3 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -236,7 +236,7 @@ sub SetupGroupings {
         my ($key, $subkey) = split /\./, $e, 2;
         $e = { $self->_FieldToFunction( KEY => $key, SUBKEY => $subkey ) };
         $e->{'TYPE'} = $GROUPINGS{ $key };
-        $e->{'META'} = $GROUPINGS{ $e->{'TYPE'} };
+        $e->{'META'} = $GROUPINGS_META{ $e->{'TYPE'} };
     }
     $self->GroupBy( @group_by );
 

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

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

    use new IsValidGrouping

diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 369d371..5776215 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -66,8 +66,10 @@ if ($ChartStyle eq 'pie') {
 
 use RT::Report::Tickets;
 my $tix = RT::Report::Tickets->new( $session{'CurrentUser'} );
-my %AllowedGroupings = reverse $tix->Groupings( Query => $Query );
-$PrimaryGroupBy = 'Queue' unless exists $AllowedGroupings{$PrimaryGroupBy};
+
+$PrimaryGroupBy = 'Queue'
+    unless $tix->IsValidGrouping( Query => $Query, GroupBy => $PrimaryGroupBy );
+
 my ($count_name, $value_name) = $tix->SetupGroupings(
     Query => $Query,
     GroupBy => $PrimaryGroupBy,
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 0c730ca..8b10c8e 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 8596d8f7dc5679bb911d301d662eff8ed697ade3
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 00:57:14 2011 +0400

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

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index cd309db..bea89c8 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -417,7 +417,7 @@ sub GenerateDateFunction {
 
     $args{'FUNCTION'} = $RT::Handle->DateTimeFunction(
         Type     => $args{'SUBKEY'},
-        Field    => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
+        Field    => $self->NotSetDateToNullFunction,
         Timezone => $tz,
     );
     return %args;
diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 25d3926..230805f 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -344,6 +344,11 @@ sub ColumnMapClassName {
     return $Class;
 }
 
+sub NotSetDateToNullFunction {
+    my $self = shift;
+    return "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END";
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index f5992dd..090102e 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -569,7 +569,7 @@ sub _DateLimit {
 
         my $function = $RT::Handle->DateTimeFunction(
             Type     => $subkey,
-            Field    => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
+            Field    => $self->NotSetDateToNullFunction,
             Timezone => $tz,
         );
 
@@ -618,7 +618,7 @@ sub _DateLimit {
     }
     else {
         $sb->_SQLLimit(
-            FUNCTION => "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END",
+            FUNCTION => $self->NotSetDateToNullFunction,
             FIELD    => $meta->[1],
             OPERATOR => $op,
             VALUE    => $date->ISO,

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

    typo, $self called $sb in this particular class

diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 090102e..3112a69 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -569,7 +569,7 @@ sub _DateLimit {
 
         my $function = $RT::Handle->DateTimeFunction(
             Type     => $subkey,
-            Field    => $self->NotSetDateToNullFunction,
+            Field    => $sb->NotSetDateToNullFunction,
             Timezone => $tz,
         );
 
@@ -618,7 +618,7 @@ sub _DateLimit {
     }
     else {
         $sb->_SQLLimit(
-            FUNCTION => $self->NotSetDateToNullFunction,
+            FUNCTION => $sb->NotSetDateToNullFunction,
             FIELD    => $meta->[1],
             OPERATOR => $op,
             VALUE    => $date->ISO,

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

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

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

commit 1c63702e6aa237768f1d3122cc75e01355deff64
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 7261883c806a750f0760427e7097890d2c859b72
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 18:56:46 2011 +0400

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

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

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

    DateTimeIntervalFunction, two dates -> seconds interval

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

commit fac2747e31b114a2c69957721caa00a22be78dcd
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 10 23:54:27 2011 +0400

    make NotSet date to NULL conversion more accurate

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 268d45c..354df52 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -348,7 +348,7 @@ sub NotSetDateToNullFunction {
     my $self = shift;
     my %args = ( FIELD => undef, @_ );
 
-    my $res = "CASE WHEN ? < '1970-01-02 00:00:00' THEN NULL ELSE ? END";
+    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 );
     }

commit 82ea0ef89ae18eb91986d8ef220da89b3f27970d
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 f945d5d..b480142 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -346,27 +346,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 79efaed..55399c8 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 d6d1469504d9ad5c2616c81534ecf63f5ba308b7
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 b480142..84cb30b 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -172,6 +172,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 => {
     },
@@ -230,7 +243,6 @@ our %STATISTICS_META = (
 
             return (FUNCTION => $function, FIELD => 'id');
         },
-
     },
     Simple => {
         Function => sub {
@@ -252,7 +264,6 @@ our %STATISTICS_META = (
             return (FUNCTION => "$function($interval)");
         },
     },
-
 );
 
 sub Groupings {
@@ -318,15 +329,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 5776215..a8337e9 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 8b10c8e..a163cae 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 f54e30157502075e7dd1b7e70fcf6a5fb83e6b59
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jun 11 00:49:08 2011 +0400

    update TODO

diff --git a/TODO.charts b/TODO.charts
index e555d32..7c7362a 100644
--- a/TODO.charts
+++ b/TODO.charts
@@ -1,12 +1,7 @@
-expand GROUPINGS to list fields and a few callbacks
-
 move abuse protection code from callers to SetupGrouping
 
 protect Function in SetupGrouping from abuse
 
-Y-axis in charts still always labeled as 'Tickets' even
-if calculate different function
-
 upgrade for saved charts to switch from space separator
 to dot
 
@@ -16,3 +11,4 @@ it'd be nice if full day and month names worked in
 we shouldn't sort "day of week" bars according to translated
 values. Now we get Mon, Fri, Wed, Sat, Thu... in russian, so
 it's very annoying to look at.
+

commit 817aa425b4ff963c1de52154733333b369ba8637
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 55399c8..730fc7e 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -63,6 +63,10 @@ sub ColumnInfo {
     return $self->{'column_info'}{$column};
 }
 
+sub ColumnsList {
+    return keys %{ $self->{'column_info'} || {} };
+}
+
 
 =head2 LabelValue
 

commit 4e77f87f02fc89f5741d8bd12d8cba44e353686c
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 730fc7e..b7146f7 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -109,6 +109,36 @@ sub RawValue {
     return (shift)->__Value( @_ );
 }
 
+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 7efc4720226018791c6a40bf1963e95595d4d207
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 b7146f7..5234e03 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -109,6 +109,37 @@ sub RawValue {
     return (shift)->__Value( @_ );
 }
 
+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 54ab97b25c6187fb301deedb98bc347b1c7b6eaf
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 84cb30b..ab8b77a 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -281,14 +281,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;
@@ -313,11 +313,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;
 }
@@ -337,33 +334,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 {
@@ -457,21 +441,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 );
 }
@@ -622,6 +593,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 5234e03..a9f174e 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -87,20 +87,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 );
 }
@@ -141,33 +129,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 ffeca06494cd24c37286a38279e97713a72bffb6
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 14 23:54:18 2011 +0400

    use strict and fix missing var declaration

diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index a9f174e..fc8e850 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -46,6 +46,9 @@
 #
 # END BPS TAGGED BLOCK }}}
 
+use strict;
+use warnings;
+
 package RT::Report::Tickets::Entry;
 
 use warnings;
@@ -64,6 +67,7 @@ sub ColumnInfo {
 }
 
 sub ColumnsList {
+    my $self = shift;
     return keys %{ $self->{'column_info'} || {} };
 }
 

commit ec2a5829937f267aab15ac6bfe9b267c7a863f16
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 ab8b77a..788debc 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 {
@@ -356,6 +357,10 @@ sub ColumnInfo {
 
     return $self->{'column_info'}{$column};
 }
+sub ColumnsList {
+    my $self = shift;
+    return keys %{ $self->{'column_info'} || {} };
+}
 
 sub SetupGroupings {
     my $self = shift;
@@ -389,12 +394,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;
     }
 
@@ -519,6 +526,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 0cc54fda2ab2e86415edc0e2630381d41dc517a7
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 a163cae..78b390d 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('WebURL') %>Search/Results.html?<%$QueryString%>><%$key%></a>
-</td>
-<td class="value collection-as-table">
-<a href=<% RT->Config->Get('WebURL') %>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 b62a2ccee541e38fe01ec204a1a30a8e22b497f8
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 78b390d..d8ab6b7 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 85511cd8468a3c39046494ea74bcce5319b4d07b
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 788debc..2aa0b65 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;
         },
@@ -188,6 +189,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 fc8e850..2e4a28c 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -85,16 +85,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 a8337e9..07858f8 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 d8ab6b7..9435063 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 2ae3f6d4dd6513c7819ab42bc95c389769148c83
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 07858f8..1955617 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 1d1c3b4cbaa1fafb699170b04e832ac67a783d1f
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 1955617..03de74f 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 ),
@@ -165,7 +169,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 a9917b793dd5228e0d33e03eb83c4c284151a39a
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jun 15 13:07:03 2011 +0400

    update TODO

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

commit 74dfb44c63bf103cf6b40e45a5f7edec6a8f8bdc
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/TODO.charts b/TODO.charts
index 646d6a3..bd79628 100644
--- a/TODO.charts
+++ b/TODO.charts
@@ -8,3 +8,13 @@ to dot
 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 multiple groupings
+
+  support putting grouppings as columns of the table
+
+  support horizontal totals when there are gropings as columns
+
+
diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 2aa0b65..9562c39 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -377,7 +377,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 ) };
@@ -387,29 +388,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 03de74f..35faffb1 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 01d3349..f654576 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>
@@ -85,18 +85,34 @@ my @actions = $m->comp( '/Widgets/SavedSearch:process', args => \%ARGS, self =>
 
 <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,
-  $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' &>
 </&>
+
+<&| /Widgets/TitleBox, title => loc('Table') &>
+</&>
+
+<& /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 9435063..bf408fd 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 e380ee9e1bf5ed62ad054591fe22dafe5c757aa5
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jun 17 19:40:23 2011 +0400

    use $self instead of RT::SearchBuilder
    
    echo of original place of the function, it was in RT::Handle
    at first

diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm
index 354df52..5d3e7c8 100644
--- a/lib/RT/SearchBuilder.pm
+++ b/lib/RT/SearchBuilder.pm
@@ -371,8 +371,8 @@ sub DateTimeIntervalFunction {
     }
     elsif ( $db_type eq 'Pg' ) {
         $res = 'EXTRACT(EPOCH FROM AGE('
-                . RT::SearchBuilder->CombineFunctionWithField( %{ $args{'From'} } )
-                .', '. RT::SearchBuilder->CombineFunctionWithField( %{ $args{'To'} } )
+                . $self->CombineFunctionWithField( %{ $args{'From'} } )
+                .', '. $self->CombineFunctionWithField( %{ $args{'To'} } )
             .'))'
         ;
     }

commit 88e4248543f757e78d403dbb6776d8957d74ec9e
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 2e4a28c..12fe990 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -122,7 +122,7 @@ sub Query {
             my $op = '=';
             if ( defined $value ) {
                 unless ( $value =~ /^\d+$/ ) {
-                    $value =~ s/(['\\])/\\$1/g;
+                    $value =~ s/'/\\'/g;
                     $value = "'$value'";
                 }
             }

commit 229e2e20ca8c8c877976117a0959e58c69b938cf
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 12fe990..2e4a28c 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -122,7 +122,7 @@ sub Query {
             my $op = '=';
             if ( defined $value ) {
                 unless ( $value =~ /^\d+$/ ) {
-                    $value =~ s/'/\\'/g;
+                    $value =~ s/(['\\])/\\$1/g;
                     $value = "'$value'";
                 }
             }

commit 3c6aeb06d5597fc477b958b3a05eac287a2b80d1
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 35faffb1..673865a 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 dc30eeb40e9f0e5d2f7a2831a184e553de96a6dd
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 a19b54e..3372b7c 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 fe22ab5d321484ded41bafe1263829dde4a6e49e
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/TODO.charts b/TODO.charts
index bd79628..c3e3cfe 100644
--- a/TODO.charts
+++ b/TODO.charts
@@ -2,8 +2,9 @@ move abuse protection code from callers to SetupGrouping
 
 protect Function in SetupGrouping from abuse
 
-upgrade for saved charts to switch from space separator
-to dot
+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.
diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index 673865a..c3f8c0e 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 f654576..6ca6043 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 );
 
@@ -92,9 +77,21 @@ my @actions = $m->comp( '/Widgets/SavedSearch:process', args => \%ARGS, self =>
 
 <&| /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 bf408fd..99630b6 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 fbf19ad..928de12 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%>><% loc($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 %>><% loc($text) %></option>
 % }
 </select>
 <%init>

commit 1e6a922f70e4b599673cbcf690314d89bae7ecd4
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jun 18 00:08:20 2011 +0400

    update todo for charts

diff --git a/TODO.charts b/TODO.charts
index c3e3cfe..541caa5 100644
--- a/TODO.charts
+++ b/TODO.charts
@@ -12,10 +12,24 @@ it'd be nice if full day and month names worked in
 support more than one calculated function in diagrams,
 at least in bars
 
-support multiple groupings
+support putting grouppings as columns of the table
+support horizontal totals when there are gropings as columns
 
-  support putting grouppings as columns of the table
+support links with partial queries in tables, for example
+table:
 
-  support horizontal totals when there are gropings as columns
+                          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.

commit c5b8697cf4bfbd4d23ebd6f5b5df8b8c1da8fcad
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 c3f8c0e..b4532cd 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 99630b6..2767d65 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 779259f28dd1ded367fdee7783a75d0d9aa7facc
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 b4532cd..e94ad42 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 170017f5e01f16384dfc303398466df781ba40aa
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 e94ad42..f352911 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 ba7c7121bc5e4af3224078835465f148486f24e1
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 9562c39..b063ab8 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -530,52 +530,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 c5925ff5759b4040b46e9b45c8ef425dc04d6d94
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 b063ab8..2a1d4b7 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -386,7 +386,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;
@@ -404,7 +408,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 56c3a1dff35c4e4836d31640da44c628d000aeb8
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 2767d65..ef626c7 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 876181be49eeb2ed55e316a87d07491ca4361a5e
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 2a1d4b7..62b93ca 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -696,6 +696,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 5d2b8a7cb813a41243b43837dad76f6d629e5aa4
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 f352911..b315251 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 ef626c7..0522e76 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 c7cb121bcb305a990ef45307ab8d027846d036be
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 62b93ca..1ed19a5 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -667,6 +667,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 2e4a28c..b7c24f4 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -85,18 +85,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;
 }
 
@@ -135,6 +130,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 8d037a3560294f88698c06d83dc3bfa1aac957a7
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 1ed19a5..ad811f4 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -266,6 +266,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 7c6476124b25fa1fa7ad9d4e2373e853a7a94051
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 b315251..0f9a430 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 0522e76..2a2fac6 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 ea2f9ca08dc6a94ff6e23d0aee2d54bffaa74bf3
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 0f9a430..8f34a3d 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 2a2fac6..16d85a2 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 7b74a7b309a87b2cd2eab7fb9bf755a088b099fa
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 2821f07..adceba3 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 5cfa65e58c806f41a3707b73b60b78b7f2d985c7
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 8f34a3d..71dd41f 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 87bb997ece0b2f51b0c68c8e45e3ea0d5a2220f5
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 ad811f4..61e836f 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -174,6 +174,7 @@ our %GROUPINGS_META = (
             }
             return @res;
         },
+        Function => 'GenerateCustomFieldFunction',
         Label => sub {
             my $self = shift;
             my %args = (@_);

commit 458b97645eb1c6f663262f156f8e294406f8c6a3
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 61e836f..ca7380a 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -197,20 +197,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' ],
@@ -255,6 +255,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 42baf735cbfbd54463bcb95a8b536929e41d3721
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 ca7380a..7ecd4bb 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -266,7 +266,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 551df90d248156e85a0176266fb2d1c8ba3c58c1
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 16d85a2..704df20 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 570f889157228819e8c144aea44a85232335dcb4
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 7ecd4bb..9f393e7 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -381,6 +381,7 @@ sub ColumnInfo {
 
     return $self->{'column_info'}{$column};
 }
+
 sub ColumnsList {
     my $self = shift;
     return keys %{ $self->{'column_info'} || {} };
@@ -423,20 +424,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;
@@ -488,25 +511,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 eaa01a35abbfd6809c5835f3c4e1c3631cab3410
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 9f393e7..96580fa 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -483,7 +483,6 @@ sub _DoSearch {
         );
     }
     else {
-        $self->AddEmptyRows;
     }
 }
 
@@ -539,31 +538,6 @@ sub NewItem {
     return $res;
 }
 
-
-=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 eb2e80c87185b4f1b877860a1080b2911bd52fa2
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 96580fa..b97b1e1 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -741,6 +741,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 704df20..5afa529 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 68ad94d06da5189e7533bb856d3b17e7674e4158
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 5afa529..9c80957 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..62ebf3d
--- /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(rawspan 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 3a5fa991572cec6c0d8b159c4b5b78beed34582b
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 b97b1e1..f147ea1 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -454,9 +454,7 @@ sub SetupGroupings {
             }
         }
         elsif ( $e->{'META'}{'Calculate'} ) {
-            # ....
-        }
-        else {
+            $e->{'NAME'} = 'postfunction'. $self->{'postfunctions'}++;
         }
         push @{ $res{'Functions'} }, $e->{'NAME'};
         $column_info{ $e->{'NAME'} } = $e;
@@ -483,6 +481,7 @@ sub _DoSearch {
         );
     }
     else {
+        $self->CalculatePostFunctions;
     }
 }
 
@@ -593,6 +592,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 b7c24f4..52a38f8 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -127,6 +127,7 @@ sub Query {
             push @parts, "$field $op $value";
         }
     }
+    return () unless @parts;
     return join ' AND ', grep defined && length, @parts;
 }
 

commit 7f980d387b5d307620bbcd0b028b4b0cac74e75b
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 f147ea1..28c9cd9 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -787,27 +787,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 762053b84b335b4bbd0f70841c1b7fad13779624
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 28c9cd9..dd31cd5 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -779,20 +779,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;
@@ -803,12 +802,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'} }, {
@@ -817,33 +822,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 5520513f48ade2b75b2e846cab933905525fe0b7
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 28 04:50:27 2011 +0400

    it's rOwspan, not rAwspan

diff --git a/share/html/Search/Elements/ChartTable b/share/html/Search/Elements/ChartTable
index 62ebf3d..6ef0db7 100644
--- a/share/html/Search/Elements/ChartTable
+++ b/share/html/Search/Elements/ChartTable
@@ -34,7 +34,7 @@ foreach my $section (qw(thead tbody tfoot)) {
             push @class, ($cell->{'type'}) unless $cell->{'type'} eq 'head';
             $m->out(' class="'. $eh->( join ' ', @class ) .'"');
 
-            foreach my $dir ( grep $cell->{$_}, qw(rawspan colspan) ) {
+            foreach my $dir ( grep $cell->{$_}, qw(rowspan colspan) ) {
                 my $value = int $cell->{ $dir };
                 $m->out(qq{ $dir="$value"});
             }

commit 53d29c8015584e7eaf59d9a6238f773cfe1d2a46
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 0365c8952542db9c85ef5f0e75111dad4adb0eb0
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Jun 28 04:51:55 2011 +0400

    we should use SB's method as local breaks references

diff --git a/lib/RT/Report/Tickets/Entry.pm b/lib/RT/Report/Tickets/Entry.pm
index 52a38f8..f67983b 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -96,7 +96,7 @@ sub LabelValue {
 }
 
 sub RawValue {
-    return (shift)->__Value( @_ );
+    return (shift)->DBIx::SearchBuilder::Record::__Value( @_ );
 }
 
 sub Query {

commit e0efba326632ea9c87c41f528aa740fd7553560a
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 dd31cd5..4b50c24 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -384,7 +384,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 {
@@ -400,6 +401,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 ) {
@@ -408,6 +411,7 @@ sub SetupGroupings {
         $e->{'TYPE'} = 'grouping';
         $e->{'INFO'} = $GROUPINGS{ $key };
         $e->{'META'} = $GROUPINGS_META{ $e->{'INFO'} };
+        $e->{'POSITION'} = $i++;
     }
     $self->GroupBy( map { {
         ALIAS    => $_->{'ALIAS'},
@@ -434,7 +438,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 3df5c626a4ae04f58def16943b5c7ea7a88fdf54
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 f67983b..76aa34d 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -90,9 +90,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 ea854f6bc002ff3eaf094f5f24f7c35c97f4fdc3
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 4b50c24..3bb1abc 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -786,8 +786,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;
@@ -814,23 +814,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 bb454cc423264b19853ca0b3ada5cfbb375a993e
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 3bb1abc..1099191 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -799,11 +799,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 21113e98fb7f801717d1acfbe1981abd12c50723
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 71dd41f..64a6281 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 1b7a6d9f988c0680364a26ddade62e8e3dbeaade
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 1099191..52840f8 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -830,12 +830,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 );
 
@@ -886,12 +889,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 4b7d5fb47cd314ee7422092982ef6c1cfff97618
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 52840f8..57b661f 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -196,40 +196,35 @@ 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-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 9c79fe807fbd02120f94f0f2d3d37eb766690817
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 57b661f..0c2e71c 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -227,6 +227,22 @@ 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 );
+    foreach my $e ( values %$v ) {
+        $e = $date->DurationAsString( $e ) if defined $e && length $e;
+        $e = $self->loc("(no value)") unless defined $e && length $e;
+    }
+};
+
 our %STATISTICS_META = (
     Count => {
         Function => sub {
@@ -254,15 +270,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 {
@@ -276,13 +286,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 69e70675d3d34939ae42ebed582db1269ab3d4dc
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 0c2e71c..cd5a429 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -450,6 +450,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 );
@@ -601,9 +610,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 );
+        }
     }
 }
 
@@ -629,6 +641,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 e9d4bbb5baf1743a6c19931906b407b5f5937058
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 cd5a429..c7868ca 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -201,6 +201,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 ],
@@ -218,6 +219,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 ],
@@ -274,6 +276,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;
@@ -288,6 +304,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->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 dcb001ba7d294bb0f45fe1cd52da34568089c986
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Aug 9 00:59:06 2011 +0400

    elementary tests for new charts

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

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

    elementary tests for charts, for real

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 e5428067825e82a24a744617caf2497754b61834
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Aug 9 01:41:05 2011 +0400

    Revert "elementary tests for new charts"
    
    This reverts commit fe8533332222b5ee35943c1bc234ebb84de7274a.
    
    Wrong commit message

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

commit dc30e8b5132f024a7a2728e75d023f9372dc5e0e
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 c7868ca..62ad4d8 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -432,7 +432,7 @@ sub SetupGroupings {
         @_
     );
 
-    $self->FromSQL( $args{'Query'} );
+    $self->FromSQL( $args{'Query'} ) if $args{'Query'};
 
     %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
 

commit 9cf9b112a3c10b00006753c5d3eee3656284b2fb
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 62ad4d8..95985f3 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -671,7 +671,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 76aa34d..ee43e4d 100644
--- a/lib/RT/Report/Tickets/Entry.pm
+++ b/lib/RT/Report/Tickets/Entry.pm
@@ -139,7 +139,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 3aef95185bfd8a6479e5af6f352c722976a48f81
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 95985f3..9c71d96 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -529,7 +529,7 @@ sub _DoSearch {
         );
     }
     else {
-        $self->CalculatePostFunctions;
+        $self->PostProcessRecords;
     }
 }
 
@@ -640,7 +640,7 @@ sub SortEntries {
     ];
 } }
 
-sub CalculatePostFunctions {
+sub PostProcessRecords {
     my $self = shift;
 
     my $info = $self->{'column_info'};

commit e27f53346a3d7bf94778c2fbfed58326ebe81902
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Sep 10 20:09:38 2011 +0400

    display function shouldn't change argument

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 9c71d96..6a326a3 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -239,10 +239,12 @@ my $duration_to_string_cb = sub {
     }
 
     my $date = RT::Date->new( $self->CurrentUser );
-    foreach my $e ( values %$v ) {
+    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 = (

commit 89ce82514a4e148bfe2ead93cd0d09dd17388b93
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 5d6d60e4a6816f175559738fef1651ae94c08b59
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Sep 13 18:24:31 2011 +0400

    fix splitting CamelCase words

diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 6a326a3..0870822 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -199,7 +199,7 @@ our @STATISTICS = (
 );
 
 foreach my $field (qw(TimeWorked TimeEstimated TimeLeft)) {
-    my $friendly = lc join ' ', split /(?<=[a-z])(?=A-Z)/, $field;
+    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 ],

commit 9452548a3c01363889ed992b4e17581a7e602822
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 0870822..cae93ff 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -201,7 +201,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 ],
@@ -219,8 +219,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 efa0c2bc4bf09d79d9ad71ba3db720845aea67ea
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Sep 13 18:25:37 2011 +0400

    we don't have Table options, delete the box

diff --git a/share/html/Search/Chart.html b/share/html/Search/Chart.html
index 6ca6043..b185793 100644
--- a/share/html/Search/Chart.html
+++ b/share/html/Search/Chart.html
@@ -104,9 +104,6 @@ my @actions = $m->comp( '/Widgets/SavedSearch:process', args => \%ARGS, self =>
 <% loc('Style') %> <& Elements/SelectChartType, Default => $ChartStyle, Name => 'ChartStyle' &>
 </&>
 
-<&| /Widgets/TitleBox, title => loc('Table') &>
-</&>
-
 <& /Elements/Submit, Label => loc('Update Chart'), Name => 'Update' &>
 </form>
 

commit fd955ad46c6871275bade12a8de638e6a7aea05e
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' }
                              ]
                         },
                         {

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


More information about the Rt-commit mailing list