1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
49 package RT::Report::Tickets;
51 use base qw/RT::Tickets/;
52 use RT::Report::Tickets::Entry;
57 use Scalar::Util qw(weaken);
60 Status => 'Enum', #loc_left_pair
62 Queue => 'Queue', #loc_left_pair
64 InitialPriority => 'Priority', #loc_left_pair
65 FinalPriority => 'Priority', #loc_left_pair
66 Priority => 'Priority', #loc_left_pair
68 Owner => 'User', #loc_left_pair
69 Creator => 'User', #loc_left_pair
70 LastUpdatedBy => 'User', #loc_left_pair
72 Requestor => 'Watcher', #loc_left_pair
73 Cc => 'Watcher', #loc_left_pair
74 AdminCc => 'Watcher', #loc_left_pair
75 Watcher => 'Watcher', #loc_left_pair
77 Created => 'Date', #loc_left_pair
78 Starts => 'Date', #loc_left_pair
79 Started => 'Date', #loc_left_pair
80 Resolved => 'Date', #loc_left_pair
81 Due => 'Date', #loc_left_pair
82 Told => 'Date', #loc_left_pair
83 LastUpdated => 'Date', #loc_left_pair
85 CF => 'CustomField', #loc_left_pair
89 our %GROUPINGS_META = (
95 my $queue = RT::Queue->new( $self->CurrentUser );
96 $queue->Load( $args{'VALUE'} );
102 Sort => 'numeric raw',
105 SubFields => [grep RT::User->_Accessible($_, "public"), qw(
106 Name RealName NickName
109 Lang City Country Timezone
111 Function => 'GenerateUserFunction',
114 SubFields => [grep RT::User->_Accessible($_, "public"), qw(
115 Name RealName NickName
118 Lang City Country Timezone
120 Function => 'GenerateWatcherFunction',
127 DayOfWeek Day DayOfMonth DayOfYear
132 Function => 'GenerateDateFunction',
137 my $raw = $args{'VALUE'};
138 return $raw unless defined $raw;
140 if ( $args{'SUBKEY'} eq 'DayOfWeek' ) {
141 return $self->loc($RT::Date::DAYS_OF_WEEK[ int $raw ]);
143 elsif ( $args{'SUBKEY'} eq 'Month' ) {
144 return $self->loc($RT::Date::MONTHS[ int($raw) - 1 ]);
156 my $queues = $args->{'Queues'};
157 if ( !$queues && $args->{'Query'} ) {
158 require RT::Interface::Web::QueryBuilder::Tree;
159 my $tree = RT::Interface::Web::QueryBuilder::Tree->new('AND');
160 $tree->ParseSQL( Query => $args->{'Query'}, CurrentUser => $self->CurrentUser );
161 $queues = $args->{'Queues'} = $tree->GetReferencedQueues;
163 return () unless $queues;
167 my $CustomFields = RT::CustomFields->new( $self->CurrentUser );
168 foreach my $id (keys %$queues) {
169 my $queue = RT::Queue->new( $self->CurrentUser );
171 next unless $queue->id;
173 $CustomFields->LimitToQueue($queue->id);
175 $CustomFields->LimitToGlobal;
176 while ( my $CustomField = $CustomFields->Next ) {
177 push @res, ["Custom field", $CustomField->Name], "CF.{". $CustomField->id ."}";
181 Function => 'GenerateCustomFieldFunction',
186 my ($cf) = ( $args{'SUBKEY'} =~ /^\{(.*)\}$/ );
187 if ( $cf =~ /^\d+$/ ) {
188 my $obj = RT::CustomField->new( $self->CurrentUser );
193 return 'Custom field [_1]', $cf;
201 # loc'able strings below generated with (s/loq/loc/):
202 # perl -MRT=-init -MRT::Report::Tickets -E 'say qq{\# loq("$_->[0]")} while $_ = splice @RT::Report::Tickets::STATISTICS, 0, 2'
204 # loc("Ticket count")
205 # loc("Summary of time worked")
206 # loc("Total time worked")
207 # loc("Average time worked")
208 # loc("Minimum time worked")
209 # loc("Maximum time worked")
210 # loc("Summary of time estimated")
211 # loc("Total time estimated")
212 # loc("Average time estimated")
213 # loc("Minimum time estimated")
214 # loc("Maximum time estimated")
215 # loc("Summary of time left")
216 # loc("Total time left")
217 # loc("Average time left")
218 # loc("Minimum time left")
219 # loc("Maximum time left")
220 # loc("Summary of Created-Started")
221 # loc("Total Created-Started")
222 # loc("Average Created-Started")
223 # loc("Minimum Created-Started")
224 # loc("Maximum Created-Started")
225 # loc("Summary of Created-Resolved")
226 # loc("Total Created-Resolved")
227 # loc("Average Created-Resolved")
228 # loc("Minimum Created-Resolved")
229 # loc("Maximum Created-Resolved")
230 # loc("Summary of Created-LastUpdated")
231 # loc("Total Created-LastUpdated")
232 # loc("Average Created-LastUpdated")
233 # loc("Minimum Created-LastUpdated")
234 # loc("Maximum Created-LastUpdated")
235 # loc("Summary of Starts-Started")
236 # loc("Total Starts-Started")
237 # loc("Average Starts-Started")
238 # loc("Minimum Starts-Started")
239 # loc("Maximum Starts-Started")
240 # loc("Summary of Due-Resolved")
241 # loc("Total Due-Resolved")
242 # loc("Average Due-Resolved")
243 # loc("Minimum Due-Resolved")
244 # loc("Maximum Due-Resolved")
245 # loc("Summary of Started-Resolved")
246 # loc("Total Started-Resolved")
247 # loc("Average Started-Resolved")
248 # loc("Minimum Started-Resolved")
249 # loc("Maximum Started-Resolved")
252 COUNT => ['Ticket count', 'Count', 'id'],
255 foreach my $field (qw(TimeWorked TimeEstimated TimeLeft)) {
256 my $friendly = lc join ' ', split /(?<=[a-z])(?=[A-Z])/, $field;
258 "ALL($field)" => ["Summary of $friendly", 'TimeAll', $field ],
259 "SUM($field)" => ["Total $friendly", 'Time', 'SUM', $field ],
260 "AVG($field)" => ["Average $friendly", 'Time', 'AVG', $field ],
261 "MIN($field)" => ["Minimum $friendly", 'Time', 'MIN', $field ],
262 "MAX($field)" => ["Maximum $friendly", 'Time', 'MAX', $field ],
267 foreach my $pair (qw(
275 my ($from, $to) = split /-/, $pair;
277 "ALL($pair)" => ["Summary of $pair", 'DateTimeIntervalAll', $from, $to ],
278 "SUM($pair)" => ["Total $pair", 'DateTimeInterval', 'SUM', $from, $to ],
279 "AVG($pair)" => ["Average $pair", 'DateTimeInterval', 'AVG', $from, $to ],
280 "MIN($pair)" => ["Minimum $pair", 'DateTimeInterval', 'MIN', $from, $to ],
281 "MAX($pair)" => ["Maximum $pair", 'DateTimeInterval', 'MAX', $from, $to ],
287 our %STATISTICS_META = (
291 my $field = shift || 'id';
302 my ($function, $field) = @_;
303 return (FUNCTION => $function, FIELD => $field);
309 my ($function, $field) = @_;
310 return (FUNCTION => "$function(?)*60", FIELD => $field);
312 Display => 'DurationAsString',
315 SubValues => sub { return ('Minimum', 'Average', 'Maximum', 'Total') },
320 Minimum => { FUNCTION => "MIN(?)*60", FIELD => $field },
321 Average => { FUNCTION => "AVG(?)*60", FIELD => $field },
322 Maximum => { FUNCTION => "MAX(?)*60", FIELD => $field },
323 Total => { FUNCTION => "SUM(?)*60", FIELD => $field },
326 Display => 'DurationAsString',
328 DateTimeInterval => {
331 my ($function, $from, $to) = @_;
333 my $interval = $self->_Handle->DateTimeIntervalFunction(
334 From => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $from ) },
335 To => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $to ) },
338 return (FUNCTION => "$function($interval)");
340 Display => 'DurationAsString',
342 DateTimeIntervalAll => {
343 SubValues => sub { return ('Minimum', 'Average', 'Maximum', 'Total') },
346 my ($from, $to) = @_;
348 my $interval = $self->_Handle->DateTimeIntervalFunction(
349 From => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $from ) },
350 To => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $to ) },
354 Minimum => { FUNCTION => "MIN($interval)" },
355 Average => { FUNCTION => "AVG($interval)" },
356 Maximum => { FUNCTION => "MAX($interval)" },
357 Total => { FUNCTION => "SUM($interval)" },
360 Display => 'DurationAsString',
370 my @tmp = @GROUPINGS;
371 while ( my ($field, $type) = splice @tmp, 0, 2 ) {
372 my $meta = $GROUPINGS_META{ $type } || {};
373 unless ( $meta->{'SubFields'} ) {
374 push @fields, [$field, $field], $field;
376 elsif ( ref( $meta->{'SubFields'} ) eq 'ARRAY' ) {
377 push @fields, map { ([$field, $_], "$field.$_") } @{ $meta->{'SubFields'} };
379 elsif ( my $code = $self->FindImplementationCode( $meta->{'SubFields'} ) ) {
380 push @fields, $code->( $self, \%args );
384 "$type has unsupported SubFields."
385 ." Not an array, a method name or a code reference"
392 sub IsValidGrouping {
395 return 0 unless $args{'GroupBy'};
397 my ($key, $subkey) = split /\./, $args{'GroupBy'}, 2;
399 %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
400 my $type = $GROUPINGS{$key};
401 return 0 unless $type;
402 return 1 unless $subkey;
404 my $meta = $GROUPINGS_META{ $type } || {};
405 unless ( $meta->{'SubFields'} ) {
408 elsif ( ref( $meta->{'SubFields'} ) eq 'ARRAY' ) {
409 return 1 if grep $_ eq $subkey, @{ $meta->{'SubFields'} };
411 elsif ( my $code = $self->FindImplementationCode( $meta->{'SubFields'}, 'silent' ) ) {
412 return 1 if grep $_ eq "$key.$subkey", $code->( $self, \%args );
419 return map { ref($_)? $_->[0] : $_ } @STATISTICS;
426 my $info = $self->ColumnInfo( $column );
428 $RT::Logger->error("Unknown column '$column'");
429 return $self->CurrentUser->loc('(Incorrect data)');
432 if ( $info->{'META'}{'Label'} ) {
433 my $code = $self->FindImplementationCode( $info->{'META'}{'Label'} );
434 return $self->CurrentUser->loc( $code->( $self, %$info ) )
439 if ( $info->{'TYPE'} eq 'statistic' ) {
440 $res = $info->{'INFO'}[0];
443 $res = join ' ', grep defined && length, @{ $info }{'KEY', 'SUBKEY'};
445 return $self->CurrentUser->loc( $res );
452 return $self->{'column_info'}{$column};
457 return sort { $self->{'column_info'}{$a}{'POSITION'} <=> $self->{'column_info'}{$b}{'POSITION'} }
458 keys %{ $self->{'column_info'} || {} };
470 $self->FromSQL( $args{'Query'} ) if $args{'Query'};
473 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
475 # See if our query is distinct
476 if (not $self->{'joins_are_distinct'} and $self->_isJoined) {
477 # If it isn't, we need to do this in two stages -- first, find
478 # the distinct matching tickets (with no group by), then search
479 # within the matching tickets grouped by what is wanted.
481 $self->Columns( 'id' );
482 while (my $row = $self->Next) {
483 push @match, $row->id;
486 # Replace the query with one that matches precisely those
487 # tickets, with no joins. We then mark it as having been ACL'd,
488 # since it was by dint of being in the search results above
490 while ( @match > 1000 ) {
491 my @batch = splice( @match, 0, 1000 );
492 $self->Limit( FIELD => 'Id', OPERATOR => 'IN', VALUE => \@batch );
494 $self->Limit( FIELD => 'Id', OPERATOR => 'IN', VALUE => \@match );
495 $self->{'_sql_current_user_can_see_applied'} = 1
499 %GROUPINGS = @GROUPINGS unless keys %GROUPINGS;
503 my @group_by = grep defined && length,
504 ref( $args{'GroupBy'} )? @{ $args{'GroupBy'} } : ($args{'GroupBy'});
505 @group_by = ('Status') unless @group_by;
507 foreach my $e ( splice @group_by ) {
508 unless ($self->IsValidGrouping( Query => $args{Query}, GroupBy => $e )) {
509 RT->Logger->error("'$e' is not a valid grouping for reports; skipping");
512 my ($key, $subkey) = split /\./, $e, 2;
513 $e = { $self->_FieldToFunction( KEY => $key, SUBKEY => $subkey ) };
514 $e->{'TYPE'} = 'grouping';
515 $e->{'INFO'} = $GROUPINGS{ $key };
516 $e->{'META'} = $GROUPINGS_META{ $e->{'INFO'} };
517 $e->{'POSITION'} = $i++;
520 $self->GroupBy( map { {
521 ALIAS => $_->{'ALIAS'},
522 FIELD => $_->{'FIELD'},
523 FUNCTION => $_->{'FUNCTION'},
526 my %res = (Groups => [], Functions => []);
529 foreach my $group_by ( @group_by ) {
530 $group_by->{'NAME'} = $self->Column( %$group_by );
531 $column_info{ $group_by->{'NAME'} } = $group_by;
532 push @{ $res{'Groups'} }, $group_by->{'NAME'};
535 %STATISTICS = @STATISTICS unless keys %STATISTICS;
537 my @function = grep defined && length,
538 ref( $args{'Function'} )? @{ $args{'Function'} } : ($args{'Function'});
539 push @function, 'COUNT' unless @function;
540 foreach my $e ( @function ) {
544 INFO => $STATISTICS{ $e },
545 META => $STATISTICS_META{ $STATISTICS{ $e }[1] },
548 unless ( $e->{'INFO'} && $e->{'META'} ) {
549 $RT::Logger->error("'". $e->{'KEY'} ."' is not valid statistic for report");
550 $e->{'FUNCTION'} = 'NULL';
551 $e->{'NAME'} = $self->Column( FUNCTION => 'NULL' );
553 elsif ( $e->{'META'}{'Function'} ) {
554 my $code = $self->FindImplementationCode( $e->{'META'}{'Function'} );
556 $e->{'FUNCTION'} = 'NULL';
557 $e->{'NAME'} = $self->Column( FUNCTION => 'NULL' );
559 elsif ( $e->{'META'}{'SubValues'} ) {
560 my %tmp = $code->( $self, @{ $e->{INFO} }[2 .. $#{$e->{INFO}}] );
561 $e->{'NAME'} = 'postfunction'. $self->{'postfunctions'}++;
562 while ( my ($k, $v) = each %tmp ) {
563 $e->{'MAP'}{ $k }{'NAME'} = $self->Column( %$v );
564 @{ $e->{'MAP'}{ $k } }{'FUNCTION', 'ALIAS', 'FIELD'} =
565 @{ $v }{'FUNCTION', 'ALIAS', 'FIELD'};
569 my %tmp = $code->( $self, @{ $e->{INFO} }[2 .. $#{$e->{INFO}}] );
570 $e->{'NAME'} = $self->Column( %tmp );
571 @{ $e }{'FUNCTION', 'ALIAS', 'FIELD'} = @tmp{'FUNCTION', 'ALIAS', 'FIELD'};
574 elsif ( $e->{'META'}{'Calculate'} ) {
575 $e->{'NAME'} = 'postfunction'. $self->{'postfunctions'}++;
577 push @{ $res{'Functions'} }, $e->{'NAME'};
578 $column_info{ $e->{'NAME'} } = $e;
581 $self->{'column_info'} = \%column_info;
588 Subclass _DoSearch from our parent so we can go through and add in empty
589 columns if it makes sense
595 $self->SUPER::_DoSearch( @_ );
596 if ( $self->{'must_redo_search'} ) {
598 "_DoSearch is not so successful as it still needs redo search, won't call AddEmptyRows"
602 $self->PostProcessRecords;
606 =head2 _FieldToFunction FIELD
608 Returns a tuple of the field or a database function to allow grouping on that
613 sub _FieldToFunction {
617 $args{'FIELD'} ||= $args{'KEY'};
619 my $meta = $GROUPINGS_META{ $GROUPINGS{ $args{'KEY'} } };
620 return ('FUNCTION' => 'NULL') unless $meta;
622 return %args unless $meta->{'Function'};
624 my $code = $self->FindImplementationCode( $meta->{'Function'} );
625 return ('FUNCTION' => 'NULL') unless $code;
627 return $code->( $self, %args );
631 # Gotta skip over RT::Tickets->Next, since it does all sorts of crazy magic we
635 $self->RT::SearchBuilder::Next(@_);
641 my $res = RT::Report::Tickets::Entry->new($self->CurrentUser);
642 $res->{'report'} = $self;
643 weaken $res->{'report'};
647 # This is necessary since normally NewItem (above) is used to intuit the
648 # correct class. However, since we're abusing a subclass, it's incorrect.
649 sub _RoleGroupClass { "RT::Ticket" }
650 sub _SingularClass { "RT::Report::Tickets::Entry" }
655 $self->_DoSearch if $self->{'must_redo_search'};
656 return unless $self->{'items'} && @{ $self->{'items'} };
659 grep $_->{'TYPE'} eq 'grouping',
660 map $self->ColumnInfo($_),
662 return unless @groups;
665 my $by_multiple = sub ($$) {
666 for my $f ( @SORT_OPS ) {
667 my $r = $f->($_[0], $_[1]);
671 my @data = map [$_], @{ $self->{'items'} };
673 for ( my $i = 0; $i < @groups; $i++ ) {
674 my $group_by = $groups[$i];
678 # If this is a CF, traverse the values being used for labels.
679 # If they all look like numbers or undef, flag for a numeric sort
681 my $looks_like_number;
682 if ( $group_by->{'KEY'} eq 'CF' ){
683 $looks_like_number = 1;
685 foreach my $item (@data){
686 my $cf_label = $item->[0]->RawValue($group_by->{'NAME'});
688 $looks_like_number = 0
689 unless (not defined $cf_label)
690 or Scalar::Util::looks_like_number( $cf_label );
694 my $order = $looks_like_number ? 'numeric label' : 'label';
695 $order = $group_by->{'META'}{Sort} if exists $group_by->{'META'}{Sort};
697 if ( $order eq 'label' ) {
698 push @SORT_OPS, sub { $_[0][$idx] cmp $_[1][$idx] };
699 $method = 'LabelValue';
701 elsif ( $order eq 'numeric label' ) {
702 my $nv = $self->loc("(no value)");
703 # Sort the (no value) elements first, by comparing for them
704 # first, and falling back to a numeric sort on all other
706 push @SORT_OPS, sub {
707 (($_[0][$idx] ne $nv) <=> ($_[1][$idx] ne $nv))
708 || ( $_[0][$idx] <=> $_[1][$idx] ) };
709 $method = 'LabelValue';
711 elsif ( $order eq 'raw' ) {
712 push @SORT_OPS, sub { ($_[0][$idx]//'') cmp ($_[1][$idx]//'') };
713 $method = 'RawValue';
715 elsif ( $order eq 'numeric raw' ) {
716 push @SORT_OPS, sub { $_[0][$idx] <=> $_[1][$idx] };
717 $method = 'RawValue';
719 $RT::Logger->error("Unknown sorting function '$order'");
722 $_->[$idx] = $_->[0]->$method( $group_by->{'NAME'} ) for @data;
726 sort $by_multiple @data
730 sub PostProcessRecords {
733 my $info = $self->{'column_info'};
734 foreach my $column ( values %$info ) {
735 next unless $column->{'TYPE'} eq 'statistic';
736 if ( $column->{'META'}{'Calculate'} ) {
737 $self->CalculatePostFunction( $column );
739 elsif ( $column->{'META'}{'SubValues'} ) {
740 $self->MapSubValues( $column );
745 sub CalculatePostFunction {
749 my $code = $self->FindImplementationCode( $info->{'META'}{'Calculate'} );
751 # TODO: fill in undefs
755 my $column = $info->{'NAME'};
757 my $base_query = $self->Query;
758 foreach my $item ( @{ $self->{'items'} } ) {
759 $item->{'values'}{ lc $column } = $code->(
762 ' AND ', map "($_)", grep defined && length, $base_query, $item->Query,
765 $item->{'fetched'}{ lc $column } = 1;
773 my $to = $info->{'NAME'};
774 my $map = $info->{'MAP'};
776 foreach my $item ( @{ $self->{'items'} } ) {
777 my $dst = $item->{'values'}{ lc $to } = { };
778 while (my ($k, $v) = each %{ $map } ) {
779 $dst->{ $k } = delete $item->{'values'}{ lc $v->{'NAME'} };
780 # This mirrors the logic in RT::Record::__Value When that
781 # ceases tp use the UTF-8 flag as a character/byte
782 # distinction from the database, this can as well.
783 utf8::decode( $dst->{ $k } )
784 if defined $dst->{ $k }
785 and not utf8::is_utf8( $dst->{ $k } );
786 delete $item->{'fetched'}{ lc $v->{'NAME'} };
788 $item->{'fetched'}{ lc $to } = 1;
792 sub GenerateDateFunction {
797 if ( RT->Config->Get('ChartsTimezonesInDB') ) {
798 my $to = $self->CurrentUser->UserObj->Timezone
799 || RT->Config->Get('Timezone');
800 $tz = { From => 'UTC', To => $to }
801 if $to && lc $to ne 'utc';
804 $args{'FUNCTION'} = $RT::Handle->DateTimeFunction(
805 Type => $args{'SUBKEY'},
806 Field => $self->NotSetDateToNullFunction,
812 sub GenerateCustomFieldFunction {
816 my ($name) = ( $args{'SUBKEY'} =~ /^\{(.*)\}$/ );
817 my $cf = RT::CustomField->new( $self->CurrentUser );
820 $RT::Logger->error("Couldn't load CustomField #$name");
821 @args{qw(FUNCTION FIELD)} = ('NULL', undef);
823 my ($ticket_cf_alias, $cf_alias) = $self->_CustomFieldJoin($cf->id, $cf);
824 @args{qw(ALIAS FIELD)} = ($ticket_cf_alias, 'Content');
829 sub GenerateUserFunction {
833 my $column = $args{'SUBKEY'} || 'Name';
834 my $u_alias = $self->{"_sql_report_$args{FIELD}_users_$column"}
838 FIELD1 => $args{'FIELD'},
842 @args{qw(ALIAS FIELD)} = ($u_alias, $column);
846 sub GenerateWatcherFunction {
850 my $type = $args{'FIELD'};
851 $type = '' if $type eq 'Watcher';
853 my $column = $args{'SUBKEY'} || 'Name';
855 my $u_alias = $self->{"_sql_report_watcher_users_alias_$type"};
856 unless ( $u_alias ) {
857 my ($g_alias, $gm_alias);
858 ($g_alias, $gm_alias, $u_alias) = $self->_WatcherJoin( Name => $type );
859 $self->{"_sql_report_watcher_users_alias_$type"} = $u_alias;
861 @args{qw(ALIAS FIELD)} = ($u_alias, $column);
866 sub DurationAsString {
869 my $v = $args{'VALUE'};
871 return $self->loc("(no value)") unless defined $v && length $v;
872 return RT::Date->new( $self->CurrentUser )->DurationAsString(
873 $v, Show => 3, Short => 1
877 my $date = RT::Date->new( $self->CurrentUser );
879 foreach my $e ( values %res ) {
880 $e = $date->DurationAsString( $e, Short => 1, Show => 3 )
881 if defined $e && length $e;
882 $e = $self->loc("(no value)") unless defined $e && length $e;
891 my $display = $self->ColumnInfo( $name )->{'META'}{'Display'};
892 return undef unless $display;
893 return $self->FindImplementationCode( $display );
897 sub FindImplementationCode {
904 $RT::Logger->error("Value is not defined. Should be method name or code reference")
908 elsif ( !ref $value ) {
909 $code = $self->can( $value );
911 $RT::Logger->error("No method $value in ". (ref $self || $self) ." class" )
916 elsif ( ref( $value ) eq 'CODE' ) {
920 $RT::Logger->error("$value is not method name or code reference")
931 # current user, handle and column_info
932 delete @clone{'user', 'DBIxHandle', 'column_info'};
933 $clone{'items'} = [ map $_->{'values'}, @{ $clone{'items'} || [] } ];
934 $clone{'column_info'} = {};
935 while ( my ($k, $v) = each %{ $self->{'column_info'} } ) {
936 $clone{'column_info'}{$k} = { %$v };
937 delete $clone{'column_info'}{$k}{'META'};
947 %$self = (%$self, %$data);
950 map { my $r = $self->NewItem; $r->LoadFromHash( $_ ); $r }
951 @{ $self->{'items'} }
953 foreach my $e ( values %{ $self->{column_info} } ) {
954 $e->{'META'} = $e->{'TYPE'} eq 'grouping'
955 ? $GROUPINGS_META{ $e->{'INFO'} }
956 : $STATISTICS_META{ $e->{'INFO'}[1] }
965 my (@head, @body, @footer);
967 @head = ({ cells => []});
968 foreach my $column ( @{ $columns{'Groups'} } ) {
969 push @{ $head[0]{'cells'} }, { type => 'head', value => $self->Label( $column ) };
973 while ( my $entry = $self->Next ) {
974 $body[ $i ] = { even => ($i+1)%2, cells => [] };
977 @footer = ({ even => ++$i%2, cells => []});
980 foreach my $column ( @{ $columns{'Groups'} } ) {
983 while ( my $entry = $self->Next ) {
984 my $value = $entry->LabelValue( $column );
985 if ( !$last || $last->{'value'} ne $value ) {
986 push @{ $body[ $i++ ]{'cells'} }, $last = { type => 'label', value => $value };
987 $last->{even} = $g++ % 2
988 unless $column eq $columns{'Groups'}[-1];
992 $last->{rowspan} = ($last->{rowspan}||1) + 1;
996 push @{ $footer[0]{'cells'} }, {
998 value => $self->loc('Total'),
999 colspan => scalar @{ $columns{'Groups'} },
1002 my $pick_color = do {
1003 my @colors = RT->Config->Get("ChartColors");
1004 sub { $colors[ $_[0] % @colors - 1 ] }
1007 my $function_count = 0;
1008 foreach my $column ( @{ $columns{'Functions'} } ) {
1011 my $info = $self->ColumnInfo( $column );
1014 if ( $info->{'META'}{'SubValues'} ) {
1015 @subs = $self->FindImplementationCode( $info->{'META'}{'SubValues'} )->(
1021 unless ( $info->{'META'}{'NoTotals'} ) {
1022 while ( my $entry = $self->Next ) {
1023 my $raw = $entry->RawValue( $column ) || {};
1024 $raw = { '' => $raw } unless ref $raw;
1025 $total{ $_ } += $raw->{ $_ } foreach grep $raw->{$_}, @subs;
1027 @subs = grep $total{$_}, @subs
1028 unless $info->{'META'}{'NoHideEmpty'};
1031 my $label = $self->Label( $column );
1034 while ( my $entry = $self->Next ) {
1035 push @{ $body[ $i++ ]{'cells'} }, {
1038 query => $entry->Query,
1041 push @{ $head[0]{'cells'} }, {
1044 rowspan => scalar @head,
1045 color => $pick_color->(++$function_count),
1047 push @{ $footer[0]{'cells'} }, { type => 'value', value => undef };
1051 if ( @subs > 1 && @head == 1 ) {
1052 $_->{rowspan} = 2 foreach @{ $head[0]{'cells'} };
1056 push @{ $head[0]{'cells'} }, {
1059 rowspan => scalar @head,
1060 color => $pick_color->(++$function_count),
1063 push @{ $head[0]{'cells'} }, { type => 'head', value => $label, colspan => scalar @subs };
1064 push @{ $head[1]{'cells'} }, { type => 'head', value => $_, color => $pick_color->(++$function_count) }
1068 while ( my $entry = $self->Next ) {
1069 my $query = $entry->Query;
1070 my $value = $entry->LabelValue( $column ) || {};
1071 $value = { '' => $value } unless ref $value;
1072 foreach my $e ( @subs ) {
1073 push @{ $body[ $i ]{'cells'} }, {
1075 value => $value->{ $e },
1082 unless ( $info->{'META'}{'NoTotals'} ) {
1083 my $total_code = $self->LabelValueCode( $column );
1084 foreach my $e ( @subs ) {
1085 my $total = $total{ $e };
1086 $total = $total_code->( $self, %$info, VALUE => $total )
1088 push @{ $footer[0]{'cells'} }, { type => 'value', value => $total };
1092 foreach my $e ( @subs ) {
1093 push @{ $footer[0]{'cells'} }, { type => 'value', value => undef };
1098 return thead => \@head, tbody => \@body, tfoot => \@footer;
1101 RT::Base->_ImportOverlays();