Dev -> 4.0.6. Clean upgrade from 4.0.5-5.
[usit-rt.git] / lib / RT / Report / Tickets.pm
CommitLineData
84fb5b46
MKG
1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
6# <sales@bestpractical.com>
7#
8# (Except where explicitly superseded by other copyright notices)
9#
10#
11# LICENSE:
12#
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
16# from www.gnu.org.
17#
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.
22#
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.
28#
29#
30# CONTRIBUTION SUBMISSION POLICY:
31#
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.)
37#
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.
46#
47# END BPS TAGGED BLOCK }}}
48
49package RT::Report::Tickets;
50
51use base qw/RT::Tickets/;
52use RT::Report::Tickets::Entry;
53
54use strict;
55use warnings;
56
57sub Groupings {
58 my $self = shift;
59 my %args = (@_);
60 my @fields = map {$_, $_} qw(
61 Status
62 Queue
63 );
64
65 foreach my $type ( qw(Owner Creator LastUpdatedBy Requestor Cc AdminCc Watcher) ) {
66 push @fields, $type.' '.$_, $type.'.'.$_ foreach qw(
67 Name EmailAddress RealName NickName Organization Lang City Country Timezone
68 );
69 }
70
71
72 for my $field (qw(Due Resolved Created LastUpdated Started Starts Told)) {
73 for my $frequency (qw(Hourly Daily Monthly Annually)) {
74 my $item = $field.$frequency;
75 push @fields, $item, $item;
76 }
77 }
78
79 my $queues = $args{'Queues'};
80 if ( !$queues && $args{'Query'} ) {
81 require RT::Interface::Web::QueryBuilder::Tree;
82 my $tree = RT::Interface::Web::QueryBuilder::Tree->new('AND');
83 $tree->ParseSQL( Query => $args{'Query'}, CurrentUser => $self->CurrentUser );
84 $queues = $tree->GetReferencedQueues;
85 }
86
87 if ( $queues ) {
88 my $CustomFields = RT::CustomFields->new( $self->CurrentUser );
89 foreach my $id (keys %$queues) {
90 my $queue = RT::Queue->new( $self->CurrentUser );
91 $queue->Load($id);
b5747ff2 92 $CustomFields->LimitToQueue($queue->Id) if $queue->Id;
84fb5b46
MKG
93 }
94 $CustomFields->LimitToGlobal;
95 while ( my $CustomField = $CustomFields->Next ) {
96 push @fields, "Custom field '". $CustomField->Name ."'", "CF.{". $CustomField->id ."}";
97 }
98 }
99 return @fields;
100}
101
102sub Label {
103 my $self = shift;
104 my $field = shift;
105 if ( $field =~ /^(?:CF|CustomField)\.{(.*)}$/ ) {
106 my $cf = $1;
107 return $self->CurrentUser->loc( "Custom field '[_1]'", $cf ) if $cf =~ /\D/;
108 my $obj = RT::CustomField->new( $self->CurrentUser );
109 $obj->Load( $cf );
110 return $self->CurrentUser->loc( "Custom field '[_1]'", $obj->Name );
111 }
112 return $self->CurrentUser->loc($field);
113}
114
115sub SetupGroupings {
116 my $self = shift;
117 my %args = (Query => undef, GroupBy => undef, @_);
118
119 $self->FromSQL( $args{'Query'} );
120 my @group_by = ref( $args{'GroupBy'} )? @{ $args{'GroupBy'} } : ($args{'GroupBy'});
121 $self->GroupBy( map { {FIELD => $_} } @group_by );
122
123 # UseSQLForACLChecks may add late joins
124 my $joined = ($self->_isJoined || RT->Config->Get('UseSQLForACLChecks')) ? 1 : 0;
125
126 my @res;
127 push @res, $self->Column( FUNCTION => ($joined? 'DISTINCT COUNT' : 'COUNT'), FIELD => 'id' );
128 push @res, map $self->Column( FIELD => $_ ), @group_by;
129 return @res;
130}
131
132sub GroupBy {
133 my $self = shift;
134 my @args = ref $_[0]? @_ : { @_ };
135
136 @{ $self->{'_group_by_field'} ||= [] } = map $_->{'FIELD'}, @args;
137 $_ = { $self->_FieldToFunction( %$_ ) } foreach @args;
138
139 $self->SUPER::GroupBy( @args );
140}
141
142sub Column {
143 my $self = shift;
144 my %args = (@_);
145
146 if ( $args{'FIELD'} && !$args{'FUNCTION'} ) {
147 %args = $self->_FieldToFunction( %args );
148 }
149
150 return $self->SUPER::Column( %args );
151}
152
153=head2 _DoSearch
154
155Subclass _DoSearch from our parent so we can go through and add in empty
156columns if it makes sense
157
158=cut
159
160sub _DoSearch {
161 my $self = shift;
162 $self->SUPER::_DoSearch( @_ );
163 if ( $self->{'must_redo_search'} ) {
164 $RT::Logger->crit(
165"_DoSearch is not so successful as it still needs redo search, won't call AddEmptyRows"
166 );
167 }
168 else {
169 $self->AddEmptyRows;
170 }
171}
172
173=head2 _FieldToFunction FIELD
174
175Returns a tuple of the field or a database function to allow grouping on that
176field.
177
178=cut
179
180sub _FieldToFunction {
181 my $self = shift;
182 my %args = (@_);
183
184 my $field = $args{'FIELD'};
185
186 if ($field =~ /^(.*)(Hourly|Daily|Monthly|Annually)$/) {
187 my ($field, $grouping) = ($1, $2);
188 my $alias = $args{'ALIAS'} || 'main';
189
190 my $func = "$alias.$field";
191
192 my $db_type = RT->Config->Get('DatabaseType');
193 if ( RT->Config->Get('ChartsTimezonesInDB') ) {
194 my $tz = $self->CurrentUser->UserObj->Timezone
195 || RT->Config->Get('Timezone')
196 || 'UTC';
197 if ( lc $tz eq 'utc' ) {
198 # do nothing
199 }
200 elsif ( $db_type eq 'Pg' ) {
201 $func = "timezone('UTC', $func)";
202 $func = "timezone(". $self->_Handle->dbh->quote($tz) .", $func)";
203 }
204 elsif ( $db_type eq 'mysql' ) {
205 $func = "CONVERT_TZ($func, 'UTC', "
206 . $self->_Handle->dbh->quote($tz)
207 .")";
208 }
209 else {
210 $RT::Logger->warning(
211 "ChartsTimezonesInDB config option"
212 ." is not supported on $db_type."
213 );
214 }
215 }
216
217 # Pg 8.3 requires explicit casting
218 $func .= '::text' if $db_type eq 'Pg';
219
220 if ( $grouping eq 'Hourly' ) {
221 $func = "SUBSTR($func,1,13)";
222 }
223 if ( $grouping eq 'Daily' ) {
224 $func = "SUBSTR($func,1,10)";
225 }
226 elsif ( $grouping eq 'Monthly' ) {
227 $func = "SUBSTR($func,1,7)";
228 }
229 elsif ( $grouping eq 'Annually' ) {
230 $func = "SUBSTR($func,1,4)";
231 }
232 $args{'FUNCTION'} = $func;
233 } elsif ( $field =~ /^(?:CF|CustomField)\.{(.*)}$/ ) { #XXX: use CFDecipher method
234 my $cf_name = $1;
235 my $cf = RT::CustomField->new( $self->CurrentUser );
236 $cf->Load($cf_name);
237 unless ( $cf->id ) {
238 $RT::Logger->error("Couldn't load CustomField #$cf_name");
239 } else {
240 my ($ticket_cf_alias, $cf_alias) = $self->_CustomFieldJoin($cf->id, $cf->id, $cf_name);
241 @args{qw(ALIAS FIELD)} = ($ticket_cf_alias, 'Content');
242 }
243 } elsif ( $field =~ /^(?:(Owner|Creator|LastUpdatedBy))(?:\.(.*))?$/ ) {
244 my $type = $1 || '';
245 my $column = $2 || 'Name';
246 my $u_alias = $self->{"_sql_report_${type}_users_${column}"}
247 ||= $self->Join(
248 TYPE => 'LEFT',
249 ALIAS1 => 'main',
250 FIELD1 => $type,
251 TABLE2 => 'Users',
252 FIELD2 => 'id',
253 );
254 @args{qw(ALIAS FIELD)} = ($u_alias, $column);
255 } elsif ( $field =~ /^(?:Watcher|(Requestor|Cc|AdminCc))(?:\.(.*))?$/ ) {
256 my $type = $1 || '';
257 my $column = $2 || 'Name';
258 my $u_alias = $self->{"_sql_report_watcher_users_alias_$type"};
259 unless ( $u_alias ) {
260 my ($g_alias, $gm_alias);
261 ($g_alias, $gm_alias, $u_alias) = $self->_WatcherJoin( $type );
262 $self->{"_sql_report_watcher_users_alias_$type"} = $u_alias;
263 }
264 @args{qw(ALIAS FIELD)} = ($u_alias, $column);
265 }
266 return %args;
267}
268
269
270# Override the AddRecord from DBI::SearchBuilder::Unique. id isn't id here
271# wedon't want to disambiguate all the items with a count of 1.
272sub AddRecord {
273 my $self = shift;
274 my $record = shift;
275 push @{$self->{'items'}}, $record;
276 $self->{'rows'}++;
277}
278
2791;
280
281
282
283# Gotta skip over RT::Tickets->Next, since it does all sorts of crazy magic we
284# don't want.
285sub Next {
286 my $self = shift;
287 $self->RT::SearchBuilder::Next(@_);
288
289}
290
291sub NewItem {
292 my $self = shift;
293 return RT::Report::Tickets::Entry->new(RT->SystemUser); # $self->CurrentUser);
294}
295
296
297=head2 AddEmptyRows
298
299If we're grouping on a criterion we know how to add zero-value rows
300for, do that.
301
302=cut
303
304sub AddEmptyRows {
305 my $self = shift;
306 if ( @{ $self->{'_group_by_field'} || [] } == 1 && $self->{'_group_by_field'}[0] eq 'Status' ) {
307 my %has = map { $_->__Value('Status') => 1 } @{ $self->ItemsArrayRef || [] };
308
309 foreach my $status ( grep !$has{$_}, RT::Queue->new($self->CurrentUser)->StatusArray ) {
310
311 my $record = $self->NewItem;
312 $record->LoadFromHash( {
313 id => 0,
314 status => $status
315 } );
316 $self->AddRecord($record);
317 }
318 }
319}
320
321RT::Base->_ImportOverlays();
322
3231;