Mailing List Archive

rt branch 5.0/asset-search-chart created. rt-5.0.5-99-g4497a54780
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "rt".

The branch, 5.0/asset-search-chart has been created
at 4497a547807f077964f8a068c010abd92f3e3932 (commit)

- Log -----------------------------------------------------------------
commit 4497a547807f077964f8a068c010abd92f3e3932
Author: sunnavy <sunnavy@bestpractical.com>
Date: Tue Dec 5 10:10:12 2023 -0500

Increase "Group By" rows to 5 to group by 2 more fields

diff --git a/share/html/Search/Chart.html b/share/html/Search/Chart.html
index 0733856126..14774cdb37 100644
--- a/share/html/Search/Chart.html
+++ b/share/html/Search/Chart.html
@@ -196,6 +196,28 @@ $m->callback( ARGSRef => \%ARGS, QueryArgsRef => \%query );
Class => $Class,
&>
</fieldset>
+ <fieldset><legend><% loc('and then') %></legend>
+ <& Elements/SelectGroupBy,
+ Name => 'GroupBy',
+ Query => $query{Query},
+ Default => $query{'GroupBy'}[3] // q{},
+ ShowEmpty => 1,
+ Stacked => $query{'GroupBy'}[3] && ($query{'GroupBy'}[3] eq ($query{StackedGroupBy} // '')) ? 1 : 0,
+ StackedId => 'StackedGroupBy-4',
+ Class => $Class,
+ &>
+ </fieldset>
+ <fieldset><legend><% loc('and then') %></legend>
+ <& Elements/SelectGroupBy,
+ Name => 'GroupBy',
+ Query => $query{Query},
+ Default => $query{'GroupBy'}[4] // q{},
+ ShowEmpty => 1,
+ Stacked => $query{'GroupBy'}[4] && ($query{'GroupBy'}[4] eq ($query{StackedGroupBy} // '')) ? 1 : 0,
+ StackedId => 'StackedGroupBy-5',
+ Class => $Class,
+ &>
+ </fieldset>
</&>

<&| /Widgets/TitleBox, title => loc("Calculate"), class => "chart-calculate" &>

commit 3eceb99fb4f5fdf9c3ce58f1d60fcbe5cbe1f4aa
Author: sunnavy <sunnavy@bestpractical.com>
Date: Mon Dec 4 14:30:28 2023 -0500

Test asset charts

diff --git a/t/charts/asset.t b/t/charts/asset.t
new file mode 100644
index 0000000000..767e53a13d
--- /dev/null
+++ b/t/charts/asset.t
@@ -0,0 +1,102 @@
+use strict;
+use warnings;
+
+use RT::Test::Assets tests => undef;
+use RT::Report::Assets;
+
+for my $status (qw/new in-use in-use allocated/) { # 2 in-use assets
+ create_asset( Catalog => 'General assets', Name => 'test', Status => $status );
+}
+
+my $report = RT::Report::Assets->new( RT->SystemUser );
+my %columns = $report->SetupGroupings(
+ Query => q{Catalog = 'General assets'},
+ GroupBy => ['Status'],
+ Function => ['COUNT'],
+);
+$report->SortEntries;
+
+my @colors = RT->Config->Get("ChartColors");
+my $expected = {
+ 'thead' => [.
+ {
+ 'cells' => [.
+ {
+ 'type' => 'head',
+ 'value' => 'Status'
+ },
+ {
+ 'color' => $colors[0],
+ 'rowspan' => 1,
+ 'type' => 'head',
+ 'value' => 'Asset count'
+ }
+ ]
+ }
+ ],
+ 'tbody' => [.
+ {
+ 'cells' => [.
+ {
+ 'type' => 'label',
+ 'value' => 'allocated',
+ },
+ {
+ 'query' => "(Status = 'allocated')",
+ 'type' => 'value',
+ 'value' => '1',
+ }
+ ],
+ 'even' => 1
+ },
+ {
+ 'even' => 0,
+ 'cells' => [.
+ {
+ 'type' => 'label',
+ 'value' => 'in-use',
+ },
+ {
+ 'query' => "(Status = 'in-use')",
+ 'type' => 'value',
+ 'value' => '2',
+ }
+ ]
+ },
+ {
+ 'even' => 1,
+ 'cells' => [.
+ {
+ 'type' => 'label',
+ 'value' => 'new',
+ },
+ {
+ 'query' => "(Status = 'new')",
+ 'type' => 'value',
+ 'value' => '1',
+ }
+ ]
+ }
+ ],
+ 'tfoot' => [.
+ {
+ 'cells' => [.
+ {
+ 'colspan' => 1,
+ 'type' => 'label',
+ 'value' => 'Total'
+ },
+ {
+ 'type' => 'value',
+ 'value' => 4
+ }
+ ],
+ 'even' => 0
+ }
+ ],
+};
+
+my %table = $report->FormatTable(%columns);
+is_deeply( \%table, $expected, "basic table" );
+
+done_testing;
diff --git a/t/web/charting.t b/t/web/charting.t
index 1cf8170942..a9c8454cf9 100644
--- a/t/web/charting.t
+++ b/t/web/charting.t
@@ -141,4 +141,16 @@ $m->get_ok("/Search/Chart?Class=RT::Transactions&Query=Type=Create");
is( $m->content_type, "image/png" );
ok( length( $m->content ), "Has content" );

+# Test asset charts
+my $asset = RT::Asset->new( RT->SystemUser );
+$asset->Create( Name => 'test', Catalog => 'General assets', Status => 'new' );
+ok( $asset->Id, 'Created test asset' );
+$m->get_ok("/Search/Chart.html?Class=RT::Assets&Query=id>0");
+$m->content_like( qr{<th[^>]*>Status\s*</th>\s*<th[^>]*>Asset count\s*</th>}, "Grouped by status" );
+$m->content_like( qr{new\s*</th>\s*<td[^>]*>\s*<a[^>]*>1</a>}, "Found results in table" );
+$m->content_like( qr{<img src="/Search/Chart\?}, "Found image" );
+$m->get_ok("/Search/Chart?Class=RT::Assets&Query=id>0");
+is( $m->content_type, "image/png" );
+ok( length( $m->content ), "Has content" );
+
done_testing;
diff --git a/t/web/custom_frontpage.t b/t/web/custom_frontpage.t
index 5ed6b4964c..a634b9f200 100644
--- a/t/web/custom_frontpage.t
+++ b/t/web/custom_frontpage.t
@@ -205,12 +205,40 @@ $m->submit_form(
);
$m->content_contains("Chart first txn chart saved", 'saved first txn chart' );

+# Add asset saved searches
+$m->get_ok( $url . "/Search/Build.html?Class=RT::Assets&Query=" . 'id>0' );
+
+$m->submit_form(
+ form_name => 'BuildQuery',
+ fields => {
+ SavedSearchDescription => 'first asset search',
+ SavedSearchOwner => 'RT::System-1',
+ },
+ button => 'SavedSearchSave',
+);
+# We don't show saved message on page :/
+$m->content_contains("Save as New", 'saved first asset search' );
+
+$m->get_ok( $url . "/Search/Chart.html?Class=RT::Assets&Query=" . 'id>0' );
+
+$m->submit_form(
+ form_name => 'SaveSearch',
+ fields => {
+ SavedSearchDescription => 'first asset chart',
+ SavedSearchOwner => 'RT::System-1',
+ },
+ button => 'SavedSearchSave',
+);
+$m->content_contains("Chart first asset chart saved", 'saved first txn chart' );
+
$m->get_ok( $url . "Dashboards/Queries.html?id=$id" );
push(
@{$args->{body}},
"saved-" . $m->dom->find('[data-description="first chart"]')->first->attr('data-name'),
"saved-" . $m->dom->find('[data-description="first txn search"]')->first->attr('data-name'),
"saved-" . $m->dom->find('[data-description="first txn chart"]')->first->attr('data-name'),
+ "saved-" . $m->dom->find('[data-description="first asset search"]')->first->attr('data-name'),
+ "saved-" . $m->dom->find('[data-description="first asset chart"]')->first->attr('data-name'),
);

$res = $m->post(
@@ -226,5 +254,8 @@ $m->text_contains('first chart');
$m->text_contains('first txn search');
$m->text_contains('first txn chart');
$m->text_contains('Transaction count', 'txn chart content');
+$m->text_contains('first asset search');
+$m->text_contains('first asset chart');
+$m->text_contains('Asset count', 'asset chart content');

done_testing;
diff --git a/t/web/saved_search_chart.t b/t/web/saved_search_chart.t
index 26366cc101..93fb297bd2 100644
--- a/t/web/saved_search_chart.t
+++ b/t/web/saved_search_chart.t
@@ -261,4 +261,34 @@ diag 'testing transaction saved searches';
is( $search->Name, 'txn chart 1', 'loaded search' );
}

+
+diag 'testing asset saved searches';
+{
+ $m->get_ok("/Search/Chart.html?Class=RT::Assets&Query=id>0");
+ $m->submit_form(
+ form_name => 'SaveSearch',
+ fields => {
+ SavedSearchDescription => 'asset chart 1',
+ SavedSearchOwner => $owner,
+ },
+ button => 'SavedSearchSave',
+ );
+ $m->form_name('SaveSearch');
+ @saved_search_ids = $m->current_form->find_input('SavedSearchLoad')->possible_values;
+ shift @saved_search_ids; # first value is blank
+ my $chart_without_updates_id = $saved_search_ids[0];
+ ok( $chart_without_updates_id, 'got a saved chart id' );
+ is( scalar @saved_search_ids, 1, 'got only one saved chart id' );
+
+ my ( $privacy, $user_id, $search_id ) = $chart_without_updates_id =~ /^(RT::User-(\d+))-SavedSearch-(\d+)$/;
+ my $user = RT::User->new( RT->SystemUser );
+ $user->Load($user_id);
+ is( $user->Name, 'root', 'loaded user' );
+ my $currentuser = RT::CurrentUser->new($user);
+
+ my $search = RT::SavedSearch->new($currentuser);
+ $search->Load( $privacy, $search_id );
+ is( $search->Name, 'asset chart 1', 'loaded search' );
+}
+
done_testing;

commit 5be73b5c830ac8fd7a9378ceb334b8151863167b
Author: sunnavy <sunnavy@bestpractical.com>
Date: Fri Dec 1 11:58:08 2023 -0500

Add asset charts support

Here we also add Creator/LastUpdatedBy/Watcher search support to make
links on charts work. E.g. if you group by Watcher, the AssetSQL on bars
will be like:

... AND (Watcher.Name = 'root')

diff --git a/lib/RT/Assets.pm b/lib/RT/Assets.pm
index 516869f9cc..30ab53629c 100644
--- a/lib/RT/Assets.pm
+++ b/lib/RT/Assets.pm
@@ -71,6 +71,8 @@ our %FIELD_METADATA = (
Catalog => [ 'ENUM' => 'Catalog', ], #loc_left_pair
LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
Created => [ 'DATE' => 'Created', ], #loc_left_pair
+ Creator => [ 'ENUM' => 'User', ], #loc_left_pair
+ LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair

Linked => [ 'LINK' ], #loc_left_pair
LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
@@ -90,6 +92,7 @@ our %FIELD_METADATA = (
Contact => [ 'WATCHERFIELD' => 'Contact', ], #loc_left_pair
ContactGroup => [ 'MEMBERSHIPFIELD' => 'Contact', ], #loc_left_pair
CustomRole => [ 'WATCHERFIELD' ], # loc_left_pair
+ Watcher => [ 'WATCHERFIELD', ], #loc_left_pair

CustomFieldValue => [ 'CUSTOMFIELD' => 'Asset' ], #loc_left_pair
CustomField => [ 'CUSTOMFIELD' => 'Asset' ], #loc_left_pair
@@ -396,7 +399,10 @@ sub AddRecord {
my $asset = shift;
return unless $asset->CurrentUserCanSee;

- return if $asset->__Value('Status') eq 'deleted'
+ # No need to check "deleted" if it's from AssetSQL(_sql_query is set). This
+ # also short circuits Status check for RT::Report::Assets::Entry, which
+ # doesn't have Status column
+ return if !$self->{_sql_query} and $asset->__Value('Status') eq 'deleted'
and not $self->{'allow_deleted_search'};

$self->SUPER::AddRecord($asset, @_);
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 82f67b0018..6964b7b315 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -759,6 +759,7 @@ sub BuildMainNav {
}
elsif ( $class eq 'RT::Assets' ) {
$current_search_menu->child( bulk => title => loc('Bulk Update'), path => "/Asset/Search/Bulk.html$args" );
+ $current_search_menu->child( chart => title => loc('Chart'), path => "/Search/Chart.html$args" );
}
elsif ( $class eq 'RT::Transactions' ) {
$current_search_menu->child( chart => title => loc('Chart'), path => "/Search/Chart.html$args" );
diff --git a/lib/RT/Report.pm b/lib/RT/Report.pm
index 52b1b1c602..57b721c30f 100644
--- a/lib/RT/Report.pm
+++ b/lib/RT/Report.pm
@@ -1445,11 +1445,17 @@ sub _DoSearchInPerl {
my @groups = grep { $_->{TYPE} eq 'grouping' } map { $self->ColumnInfo($_) } $self->ColumnsList;
my %info;

- my %bh_class = map { $_ => 'business_hours_' . HTML::Mason::Commands::CSSClass( lc $_ ) }
- keys %{ RT->Config->Get('ServiceBusinessHours') || {} };
+ my %bh_class;
+
+ # Can't use ->can('SLA') as SLA is an autoloaded method of RT::Ticket
+ if ( $self->_SingularClass->ObjectType->_ClassAccessible->{SLA} ) {
+ %bh_class = map { $_ => 'business_hours_' . HTML::Mason::Commands::CSSClass( lc $_ ) }
+ keys %{ RT->Config->Get('ServiceBusinessHours') || {} };
+ }

while ( my $object = $objects->Next ) {
- my $bh = $object->SLA ? RT->Config->Get('ServiceAgreements')->{Levels}{ $object->SLA }{BusinessHours} : '';
+ my $bh = %bh_class
+ && $object->SLA ? RT->Config->Get('ServiceAgreements')->{Levels}{ $object->SLA }{BusinessHours} : '';

my @keys;
my @extra_keys;
@@ -1802,8 +1808,15 @@ sub GetReferencedObjects {
my $self = shift;
my %args = @_;

- my $class = 'RT::Queue';
- my $method = 'GetReferencedQueues';
+ my ( $class, $method );
+ if ( $self->isa('RT::Report::Assets') ) {
+ $class = 'RT::Catalog';
+ $method = 'GetReferencedCatalogs';
+ }
+ else {
+ $class = 'RT::Queue';
+ $method = 'GetReferencedQueues';
+ }

my $objects;
if ( $args{Query} ) {
diff --git a/lib/RT/Report/Assets.pm b/lib/RT/Report/Assets.pm
new file mode 100644
index 0000000000..a1590c3526
--- /dev/null
+++ b/lib/RT/Report/Assets.pm
@@ -0,0 +1,137 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC
+# <sales@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 }}}
+
+package RT::Report::Assets;
+
+use base qw/RT::Report RT::Assets/;
+use RT::Report::Assets::Entry;
+
+use strict;
+use warnings;
+use 5.010;
+
+=head1 NAME
+
+RT::Report::Assets - Asset search charts
+
+=head1 DESCRIPTION
+
+This is the backend class for asset search charts.
+
+=cut
+
+our @GROUPINGS = (
+ Status => 'Enum', #loc_left_pair
+ Catalog => 'Catalog', #loc_left_pair
+ Creator => 'User', #loc_left_pair
+ LastUpdatedBy => 'User', #loc_left_pair
+ Owner => 'Watcher', #loc_left_pair
+ HeldBy => 'Watcher', #loc_left_pair
+ Contact => 'Watcher', #loc_left_pair
+ Watcher => 'Watcher', #loc_left_pair
+ CustomRole => 'Watcher',
+ Created => 'Date', #loc_left_pair
+ LastUpdated => 'Date', #loc_left_pair
+ CF => 'CustomField', #loc_left_pair
+);
+
+# loc'able strings below generated with (s/loq/loc/):
+# perl -MRT=-init -MRT::Report::Assets -E 'say qq{\# loq("$_->[0]")} while $_ = splice @RT::Report::Assets::STATISTICS, 0, 2'
+#
+# loc("Asset count")
+# loc("Summary of Created to LastUpdated")
+# loc("Total Created to LastUpdated")
+# loc("Average Created to LastUpdated")
+# loc("Minimum Created to LastUpdated")
+# loc("Maximum Created to LastUpdated")
+
+our @STATISTICS = (
+ COUNT => ['Asset count', 'Count', 'id'],
+);
+
+foreach my $pair (
+ 'Created to LastUpdated',
+) {
+ my ($from, $to) = split / to /, $pair;
+ push @STATISTICS, (
+ "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 ],
+ );
+ push @GROUPINGS, $pair => 'Duration';
+}
+
+sub _DoSearch {
+ my $self = shift;
+
+ # When groupby/calculation can't be done at SQL level, do it at Perl level
+ return $self->_DoSearchInPerl(@_) if $self->{_query};
+
+ $self->SUPER::_DoSearch( @_ );
+ $self->_PostSearch();
+}
+
+sub new {
+ my $self = shift;
+ $self->_SetupCustomDateRanges;
+ return $self->SUPER::new(@_);
+}
+
+sub _Init {
+ my $self = shift;
+ $self->SUPER::_Init(@_);
+
+ # Reset OrderBy to not order by name by default
+ $self->OrderByCols();
+}
+
+RT::Base->_ImportOverlays();
+
+1;
diff --git a/lib/RT/Report/Assets/Entry.pm b/lib/RT/Report/Assets/Entry.pm
new file mode 100644
index 0000000000..1a22edd29d
--- /dev/null
+++ b/lib/RT/Report/Assets/Entry.pm
@@ -0,0 +1,58 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC
+# <sales@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 }}}
+
+package RT::Report::Assets::Entry;
+
+use warnings;
+use strict;
+
+use base qw/RT::Report::Entry/;
+
+RT::Base->_ImportOverlays();
+
+1;

commit 1ce3ff95a5b1cbfa624697239284445c304ec85a
Author: sunnavy <sunnavy@bestpractical.com>
Date: Tue Dec 5 11:25:52 2023 -0500

Move a few more general code from RT::Report::Tickets to RT::Report

We are going to reuse them in the upcoming asset charts.

diff --git a/lib/RT/Report.pm b/lib/RT/Report.pm
index 3db792e31f..52b1b1c602 100644
--- a/lib/RT/Report.pm
+++ b/lib/RT/Report.pm
@@ -559,6 +559,71 @@ sub SetupGroupings {
@_
);

+ $self->FromSQL( $args{'Query'} ) if $args{'Query'};
+
+ # Apply ACL checks
+ $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
+
+ # See if our query is distinct
+ if (not $self->{'joins_are_distinct'} and $self->_isJoined) {
+ # If it isn't, we need to do this in two stages -- first, find
+ # the distinct matching tickets (with no group by), then search
+ # within the matching tickets grouped by what is wanted.
+ $self->Columns( 'id' );
+ if ( RT->Config->Get('UseSQLForACLChecks') ) {
+ my $query = $self->BuildSelectQuery( PreferBind => 0 );
+ $self->CleanSlate;
+ $self->Limit( FIELD => 'Id', OPERATOR => 'IN', VALUE => "($query)", QUOTEVALUE => 0 );
+ }
+ else {
+ # ACL is done in Next call
+ my @match = (0);
+ while ( my $row = $self->Next ) {
+ push @match, $row->id;
+ }
+
+ # Replace the query with one that matches precisely those
+ # tickets, with no joins. We then mark it as having been ACL'd,
+ # since it was by dint of being in the search results above
+ $self->CleanSlate;
+ while ( @match > 1000 ) {
+ my @batch = splice( @match, 0, 1000 );
+ $self->Limit( FIELD => 'Id', OPERATOR => 'IN', VALUE => \@batch );
+ }
+ $self->Limit( FIELD => 'Id', OPERATOR => 'IN', VALUE => \@match );
+ }
+ $self->{'_sql_current_user_can_see_applied'} = 1
+ }
+
+ my %res = $self->_SetupGroupings(%args);
+
+ if ($args{Query}
+ && ( grep( { $_->{INFO} =~ /Duration|CustomDateRange/ } map { $self->{column_info}{$_} } @{ $res{Groups} } )
+ || grep( { $_->{TYPE} eq 'statistic' && ref $_->{INFO} && $_->{INFO}[1] =~ /CustomDateRange/ }
+ values %{ $self->{column_info} } )
+ || grep( { $_->{TYPE} eq 'statistic' && ref $_->{INFO} && ref $_->{INFO}[-1] && $_->{INFO}[-1]{business_time} }
+ values %{ $self->{column_info} } ) )
+ )
+ {
+ # Need to do the groupby/calculation at Perl level
+ $self->{_query} = $args{'Query'};
+ }
+ else {
+ delete $self->{_query};
+ }
+
+ return %res;
+}
+
+sub _SetupGroupings {
+ my $self = shift;
+ my %args = (
+ Query => undef,
+ GroupBy => undef,
+ Function => undef,
+ @_
+ );
+
my $i = 0;

my @group_by = grep defined && length,
@@ -1366,6 +1431,324 @@ sub DefaultGroupBy {

# The following methods are more collection related

+=head2 _DoSearchInPerl
+
+For complicated reports that can't be calculated in SQL, do them in Perl.
+
+=cut
+
+sub _DoSearchInPerl {
+ my $self = shift;
+
+ my $objects = $self->_CollectionClass->new( $self->CurrentUser );
+ $objects->FromSQL( $self->{_query} );
+ my @groups = grep { $_->{TYPE} eq 'grouping' } map { $self->ColumnInfo($_) } $self->ColumnsList;
+ my %info;
+
+ my %bh_class = map { $_ => 'business_hours_' . HTML::Mason::Commands::CSSClass( lc $_ ) }
+ keys %{ RT->Config->Get('ServiceBusinessHours') || {} };
+
+ while ( my $object = $objects->Next ) {
+ my $bh = $object->SLA ? RT->Config->Get('ServiceAgreements')->{Levels}{ $object->SLA }{BusinessHours} : '';
+
+ my @keys;
+ my @extra_keys;
+ my %css_class;
+ for my $group ( @groups ) {
+ my $value;
+
+ if ( $object->_Accessible($group->{KEY}, 'read' )) {
+ if ( $group->{SUBKEY} ) {
+ my $method = "$group->{KEY}Obj";
+ if ( my $obj = $object->$method ) {
+ if ( $group->{INFO} eq 'Date' ) {
+ if ( $obj->Unix > 0 ) {
+ $value = $obj->Strftime( $self->_GroupingsMeta()->{Date}{StrftimeFormat}{ $group->{SUBKEY} },
+ Timezone => 'user' );
+ }
+ else {
+ $value = $self->loc('(no value)')
+ }
+ }
+ else {
+ $value = $obj->_Value($group->{SUBKEY});
+ }
+ $value //= $self->loc('(no value)');
+ }
+ }
+ $value //= $object->_Value( $group->{KEY} ) // $self->loc('(no value)');
+ }
+ elsif ( $group->{INFO} eq 'Watcher' ) {
+ my @values;
+ if ( $object->can($group->{KEY}) ) {
+ my $method = $group->{KEY};
+ push @values, map { $_->MemberId } @{$object->$method->MembersObj->ItemsArrayRef};
+ }
+ elsif ( $group->{KEY} eq 'Watcher' ) {
+ push @values, map { $_->MemberId } @{$object->$_->MembersObj->ItemsArrayRef} for /Requestor Cc AdminCc/;
+ }
+ else {
+ RT->Logger->error("Unsupported group by $group->{KEY}");
+ next;
+ }
+
+ @values = $self->loc('(no value)') unless @values;
+ $value = \@values;
+ }
+ elsif ( $group->{INFO} eq 'CustomField' ) {
+ my ($id) = $group->{SUBKEY} =~ /{(\d+)}/;
+ my $values = $object->CustomFieldValues($id);
+ if ( $values->Count ) {
+ $value = [ map { $_->Content } @{ $values->ItemsArrayRef } ];
+ }
+ else {
+ $value = $self->loc('(no value)');
+ }
+ }
+ elsif ( $group->{INFO} =~ /^Duration(InBusinessHours)?/ ) {
+ my $business_time = $1;
+
+ if ( $group->{FIELD} =~ /^(\w+) to (\w+)(\(Business Hours\))?$/ ) {
+ my $start = $1;
+ my $end = $2;
+ my $start_method = $start . 'Obj';
+ my $end_method = $end . 'Obj';
+ if ( $object->$end_method->Unix > 0 && $object->$start_method->Unix > 0 ) {
+ my $seconds;
+
+ if ($business_time) {
+ $seconds = $object->CustomDateRange(
+ '',
+ { value => "$end - $start",
+ business_time => 1,
+ format => sub { $_[0] },
+ }
+ );
+ }
+ else {
+ $seconds = $object->$end_method->Unix - $object->$start_method->Unix;
+ }
+
+ if ( $group->{SUBKEY} eq 'Default' ) {
+ $value = RT::Date->new( $self->CurrentUser )->DurationAsString(
+ $seconds,
+ Show => $group->{META}{Show},
+ Short => $group->{META}{Short},
+ MaxUnit => $business_time ? 'hour' : 'year',
+ );
+ }
+ else {
+ $value = RT::Date->new( $self->CurrentUser )->DurationAsString(
+ $seconds,
+ Show => $group->{META}{Show} // 3,
+ Short => $group->{META}{Short} // 1,
+ MaxUnit => lc $group->{SUBKEY},
+ MinUnit => lc $group->{SUBKEY},
+ Unit => lc $group->{SUBKEY},
+ );
+ }
+ }
+
+ if ( $business_time ) {
+ push @extra_keys, join ' => ', $group->{FIELD}, $bh_class{$bh} || 'business_hours_none';
+ }
+ }
+ else {
+ my %ranges = $self->_RoleGroupClass->CustomDateRanges;
+ if ( my $spec = $ranges{$group->{FIELD}} ) {
+ if ( $group->{SUBKEY} eq 'Default' ) {
+ $value = $object->CustomDateRange( $group->{FIELD}, $spec );
+ }
+ else {
+ my $seconds = $object->CustomDateRange( $group->{FIELD},
+ { ref $spec ? %$spec : ( value => $spec ), format => sub { $_[0] } } );
+
+ if ( defined $seconds ) {
+ $value = RT::Date->new( $self->CurrentUser )->DurationAsString(
+ $seconds,
+ Show => $group->{META}{Show} // 3,
+ Short => $group->{META}{Short} // 1,
+ MaxUnit => lc $group->{SUBKEY},
+ MinUnit => lc $group->{SUBKEY},
+ Unit => lc $group->{SUBKEY},
+ );
+ }
+ }
+ if ( ref $spec && $spec->{business_time} ) {
+ # 1 means the corresponding one in SLA, which $bh already holds
+ $bh = $spec->{business_time} unless $spec->{business_time} eq '1';
+ push @extra_keys, join ' => ', $group->{FIELD}, $bh_class{$bh} || 'business_hours_none';
+ }
+ }
+ }
+
+ $value //= $self->loc('(no value)');
+ }
+ else {
+ RT->Logger->error("Unsupported group by $group->{KEY}");
+ next;
+ }
+ push @keys, $value;
+ }
+ push @keys, @extra_keys;
+
+ # @keys could contain arrayrefs, so we need to expand it.
+ # e.g. "open", [ "root", "foo" ], "General" )
+ # will be expanded to:
+ # "open", "root", "General"
+ # "open", "foo", "General"
+
+ my @all_keys;
+ for my $key (@keys) {
+ if ( ref $key eq 'ARRAY' ) {
+ if (@all_keys) {
+ my @new_all_keys;
+ for my $keys ( @all_keys ) {
+ push @new_all_keys, [ @$keys, $_ ] for @$key;
+ }
+ @all_keys = @new_all_keys;
+ }
+ else {
+ push @all_keys, [$_] for @$key;
+ }
+ }
+ else {
+ if (@all_keys) {
+ @all_keys = map { [ @$_, $key ] } @all_keys;
+ }
+ else {
+ push @all_keys, [$key];
+ }
+ }
+ }
+
+ my @fields = grep { $_->{TYPE} eq 'statistic' }
+ map { $self->ColumnInfo($_) } $self->ColumnsList;
+
+ while ( my $field = shift @fields ) {
+ for my $keys (@all_keys) {
+ my $key = join ';;;', @$keys;
+ if ( $field->{NAME} =~ /^id/ && $field->{FUNCTION} eq 'COUNT' ) {
+ $info{$key}{ $field->{NAME} }++;
+ }
+ elsif ( $field->{NAME} =~ /^postfunction/ ) {
+ if ( $field->{MAP} ) {
+ my ($meta_type) = $field->{INFO}[1] =~ /^(\w+)All$/;
+ for my $item ( values %{ $field->{MAP} } ) {
+ push @fields,
+ {
+ NAME => $item->{NAME},
+ FIELD => $item->{FIELD},
+ INFO => [.
+ '', $meta_type,
+ $item->{FUNCTION} =~ /^(\w+)/ ? $1 : '',
+ @{ $field->{INFO} }[ 2 .. $#{ $field->{INFO} } ],
+ ],
+ };
+ }
+ }
+ }
+ elsif ( $field->{INFO}[1] eq 'Time' ) {
+ if ( $field->{NAME} =~ /^(TimeWorked|TimeEstimated|TimeLeft)$/ ) {
+ my $method = $1;
+ my $type = $field->{INFO}[2];
+ my $name = lc $field->{NAME};
+
+ $info{$key}{$name}
+ = $self->_CalculateTime( $type, $object->$method * 60, $info{$key}{$name} ) || 0;
+ }
+ else {
+ RT->Logger->error("Unsupported field $field->{NAME}");
+ }
+ }
+ elsif ( $field->{INFO}[1] eq 'DateTimeInterval' ) {
+ my ( undef, undef, $type, $start, $end, $extra_info ) = @{ $field->{INFO} };
+ my $name = lc $field->{NAME};
+ $info{$key}{$name} ||= 0;
+
+ my $start_method = $start . 'Obj';
+ my $end_method = $end . 'Obj';
+ next unless $object->$end_method->Unix > 0 && $object->$start_method->Unix > 0;
+
+ my $value;
+ if ($extra_info->{business_time}) {
+ $value = $object->CustomDateRange(
+ '',
+ { value => "$end - $start",
+ business_time => $extra_info->{business_time},
+ format => sub { return $_[0] },
+ }
+ );
+ }
+ else {
+ $value = $object->$end_method->Unix - $object->$start_method->Unix;
+ }
+
+ $info{$key}{$name} = $self->_CalculateTime( $type, $value, $info{$key}{$name} );
+ }
+ elsif ( $field->{INFO}[1] eq 'CustomDateRange' ) {
+ my ( undef, undef, $type, $range_name ) = @{ $field->{INFO} };
+ my $name = lc $field->{NAME};
+ $info{$key}{$name} ||= 0;
+
+ my $value;
+ my %ranges = $self->_RoleGroupClass->CustomDateRanges;
+ if ( my $spec = $ranges{$range_name} ) {
+ $value = $object->CustomDateRange(
+ $range_name,
+ {
+ ref $spec eq 'HASH' ? %$spec : ( value => $spec ),
+ format => sub { $_[0] },
+ }
+ );
+ }
+ $info{$key}{$name} = $self->_CalculateTime( $type, $value, $info{$key}{$name} );
+ }
+ else {
+ RT->Logger->error("Unsupported field $field->{INFO}[1]");
+ }
+ }
+ }
+
+ for my $keys (@all_keys) {
+ my $key = join ';;;', @$keys;
+ push @{ $info{$key}{ids} }, $object->id;
+ }
+ }
+
+ # Make generated results real SB results
+ for my $key ( keys %info ) {
+ my @keys = split /;;;/, $key;
+ my $row;
+ for my $group ( @groups ) {
+ $row->{lc $group->{NAME}} = shift @keys;
+ }
+ for my $field ( keys %{ $info{$key} } ) {
+ my $value = $info{$key}{$field};
+ if ( ref $value eq 'HASH' && $value->{calculate} ) {
+ $row->{$field} = $value->{calculate}->($value);
+ }
+ else {
+ $row->{$field} = $info{$key}{$field};
+ }
+ }
+ my $item = $self->NewItem();
+
+ # Has extra css info
+ for my $key (@keys) {
+ if ( $key =~ /(.+) => (.+)/ ) {
+ $row->{_css_class}{$1} = $2;
+ }
+ }
+
+ $item->LoadFromHash($row);
+ $self->AddRecord($item);
+ }
+ $self->{must_redo_search} = 0;
+ $self->{is_limited} = 1;
+ $self->PostProcessRecords;
+}
+
sub _PostSearch {
my $self = shift;
if ( $self->{'must_redo_search'} ) {
@@ -1378,6 +1761,14 @@ sub _PostSearch {
}
}

+
+# Gotta skip over customized Next, since it does all sorts of crazy magic we don't want.
+sub Next {
+ my $self = shift;
+ $self->RT::SearchBuilder::Next(@_);
+
+}
+
sub NewItem {
my $self = shift;
my $res = $self->_SingularClass->new($self->CurrentUser);
@@ -1478,6 +1869,13 @@ sub GetCustomRoles {
return $custom_roles;
}

+sub _CollectionClass {
+ my $self = shift;
+ my $class = ref $self || $self;
+ $class =~ s!::Report!!;
+ return $class;
+}
+
RT::Base->_ImportOverlays();

1;
diff --git a/lib/RT/Report/Tickets.pm b/lib/RT/Report/Tickets.pm
index 53d9424b55..6fdd51a755 100644
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@ -210,71 +210,6 @@ foreach my $pair (
}
}

-sub SetupGroupings {
- my $self = shift;
- my %args = (
- Query => undef,
- GroupBy => undef,
- Function => undef,
- @_
- );
-
- $self->FromSQL( $args{'Query'} ) if $args{'Query'};
-
- # Apply ACL checks
- $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
-
- # See if our query is distinct
- if (not $self->{'joins_are_distinct'} and $self->_isJoined) {
- # If it isn't, we need to do this in two stages -- first, find
- # the distinct matching tickets (with no group by), then search
- # within the matching tickets grouped by what is wanted.
- $self->Columns( 'id' );
- if ( RT->Config->Get('UseSQLForACLChecks') ) {
- my $query = $self->BuildSelectQuery( PreferBind => 0 );
- $self->CleanSlate;
- $self->Limit( FIELD => 'Id', OPERATOR => 'IN', VALUE => "($query)", QUOTEVALUE => 0 );
- }
- else {
- # ACL is done in Next call
- my @match = (0);
- while ( my $row = $self->Next ) {
- push @match, $row->id;
- }
-
- # Replace the query with one that matches precisely those
- # tickets, with no joins. We then mark it as having been ACL'd,
- # since it was by dint of being in the search results above
- $self->CleanSlate;
- while ( @match > 1000 ) {
- my @batch = splice( @match, 0, 1000 );
- $self->Limit( FIELD => 'Id', OPERATOR => 'IN', VALUE => \@batch );
- }
- $self->Limit( FIELD => 'Id', OPERATOR => 'IN', VALUE => \@match );
- }
- $self->{'_sql_current_user_can_see_applied'} = 1
- }
-
- my %res = $self->SUPER::SetupGroupings(%args);
-
- if ($args{Query}
- && ( grep( { $_->{INFO} =~ /Duration|CustomDateRange/ } map { $self->{column_info}{$_} } @{ $res{Groups} } )
- || grep( { $_->{TYPE} eq 'statistic' && ref $_->{INFO} && $_->{INFO}[1] =~ /CustomDateRange/ }
- values %{ $self->{column_info} } )
- || grep( { $_->{TYPE} eq 'statistic' && ref $_->{INFO} && ref $_->{INFO}[-1] && $_->{INFO}[-1]{business_time} }
- values %{ $self->{column_info} } ) )
- )
- {
- # Need to do the groupby/calculation at Perl level
- $self->{_query} = $args{'Query'};
- }
- else {
- delete $self->{_query};
- }
-
- return %res;
-}
-
=head2 _DoSearch

Subclass _DoSearch from our parent so we can go through and add in empty
@@ -290,317 +225,7 @@ sub _DoSearch {
my $self = shift;

# When groupby/calculation can't be done at SQL level, do it at Perl level
- if ( $self->{_query} ) {
- my $tickets = RT::Tickets->new( $self->CurrentUser );
- $tickets->FromSQL( $self->{_query} );
- my @groups = grep { $_->{TYPE} eq 'grouping' } map { $self->ColumnInfo($_) } $self->ColumnsList;
- my %info;
-
- my %bh_class = map { $_ => 'business_hours_' . HTML::Mason::Commands::CSSClass( lc $_ ) }
- keys %{ RT->Config->Get('ServiceBusinessHours') || {} };
-
- while ( my $ticket = $tickets->Next ) {
- my $bh = $ticket->SLA ? RT->Config->Get('ServiceAgreements')->{Levels}{ $ticket->SLA }{BusinessHours} : '';
-
- my @keys;
- my @extra_keys;
- my %css_class;
- for my $group ( @groups ) {
- my $value;
-
- if ( $ticket->_Accessible($group->{KEY}, 'read' )) {
- if ( $group->{SUBKEY} ) {
- my $method = "$group->{KEY}Obj";
- if ( my $obj = $ticket->$method ) {
- if ( $group->{INFO} eq 'Date' ) {
- if ( $obj->Unix > 0 ) {
- $value = $obj->Strftime( $self->_GroupingsMeta()->{Date}{StrftimeFormat}{ $group->{SUBKEY} },
- Timezone => 'user' );
- }
- else {
- $value = $self->loc('(no value)')
- }
- }
- else {
- $value = $obj->_Value($group->{SUBKEY});
- }
- $value //= $self->loc('(no value)');
- }
- }
- $value //= $ticket->_Value( $group->{KEY} ) // $self->loc('(no value)');
- }
- elsif ( $group->{INFO} eq 'Watcher' ) {
- my @values;
- if ( $ticket->can($group->{KEY}) ) {
- my $method = $group->{KEY};
- push @values, map { $_->MemberId } @{$ticket->$method->MembersObj->ItemsArrayRef};
- }
- elsif ( $group->{KEY} eq 'Watcher' ) {
- push @values, map { $_->MemberId } @{$ticket->$_->MembersObj->ItemsArrayRef} for /Requestor Cc AdminCc/;
- }
- else {
- RT->Logger->error("Unsupported group by $group->{KEY}");
- next;
- }
-
- @values = $self->loc('(no value)') unless @values;
- $value = \@values;
- }
- elsif ( $group->{INFO} eq 'CustomField' ) {
- my ($id) = $group->{SUBKEY} =~ /{(\d+)}/;
- my $values = $ticket->CustomFieldValues($id);
- if ( $values->Count ) {
- $value = [ map { $_->Content } @{ $values->ItemsArrayRef } ];
- }
- else {
- $value = $self->loc('(no value)');
- }
- }
- elsif ( $group->{INFO} =~ /^Duration(InBusinessHours)?/ ) {
- my $business_time = $1;
-
- if ( $group->{FIELD} =~ /^(\w+) to (\w+)(\(Business Hours\))?$/ ) {
- my $start = $1;
- my $end = $2;
- my $start_method = $start . 'Obj';
- my $end_method = $end . 'Obj';
- if ( $ticket->$end_method->Unix > 0 && $ticket->$start_method->Unix > 0 ) {
- my $seconds;
-
- if ($business_time) {
- $seconds = $ticket->CustomDateRange(
- '',
- { value => "$end - $start",
- business_time => 1,
- format => sub { $_[0] },
- }
- );
- }
- else {
- $seconds = $ticket->$end_method->Unix - $ticket->$start_method->Unix;
- }
-
- if ( $group->{SUBKEY} eq 'Default' ) {
- $value = RT::Date->new( $self->CurrentUser )->DurationAsString(
- $seconds,
- Show => $group->{META}{Show},
- Short => $group->{META}{Short},
- MaxUnit => $business_time ? 'hour' : 'year',
- );
- }
- else {
- $value = RT::Date->new( $self->CurrentUser )->DurationAsString(
- $seconds,
- Show => $group->{META}{Show} // 3,
- Short => $group->{META}{Short} // 1,
- MaxUnit => lc $group->{SUBKEY},
- MinUnit => lc $group->{SUBKEY},
- Unit => lc $group->{SUBKEY},
- );
- }
- }
-
- if ( $business_time ) {
- push @extra_keys, join ' => ', $group->{FIELD}, $bh_class{$bh} || 'business_hours_none';
- }
- }
- else {
- my %ranges = RT::Ticket->CustomDateRanges;
- if ( my $spec = $ranges{$group->{FIELD}} ) {
- if ( $group->{SUBKEY} eq 'Default' ) {
- $value = $ticket->CustomDateRange( $group->{FIELD}, $spec );
- }
- else {
- my $seconds = $ticket->CustomDateRange( $group->{FIELD},
- { ref $spec ? %$spec : ( value => $spec ), format => sub { $_[0] } } );
-
- if ( defined $seconds ) {
- $value = RT::Date->new( $self->CurrentUser )->DurationAsString(
- $seconds,
- Show => $group->{META}{Show} // 3,
- Short => $group->{META}{Short} // 1,
- MaxUnit => lc $group->{SUBKEY},
- MinUnit => lc $group->{SUBKEY},
- Unit => lc $group->{SUBKEY},
- );
- }
- }
- if ( ref $spec && $spec->{business_time} ) {
- # 1 means the corresponding one in SLA, which $bh already holds
- $bh = $spec->{business_time} unless $spec->{business_time} eq '1';
- push @extra_keys, join ' => ', $group->{FIELD}, $bh_class{$bh} || 'business_hours_none';
- }
- }
- }
-
- $value //= $self->loc('(no value)');
- }
- else {
- RT->Logger->error("Unsupported group by $group->{KEY}");
- next;
- }
- push @keys, $value;
- }
- push @keys, @extra_keys;
-
- # @keys could contain arrayrefs, so we need to expand it.
- # e.g. "open", [ "root", "foo" ], "General" )
- # will be expanded to:
- # "open", "root", "General"
- # "open", "foo", "General"
-
- my @all_keys;
- for my $key (@keys) {
- if ( ref $key eq 'ARRAY' ) {
- if (@all_keys) {
- my @new_all_keys;
- for my $keys ( @all_keys ) {
- push @new_all_keys, [ @$keys, $_ ] for @$key;
- }
- @all_keys = @new_all_keys;
- }
- else {
- push @all_keys, [$_] for @$key;
- }
- }
- else {
- if (@all_keys) {
- @all_keys = map { [ @$_, $key ] } @all_keys;
- }
- else {
- push @all_keys, [$key];
- }
- }
- }
-
- my @fields = grep { $_->{TYPE} eq 'statistic' }
- map { $self->ColumnInfo($_) } $self->ColumnsList;
-
- while ( my $field = shift @fields ) {
- for my $keys (@all_keys) {
- my $key = join ';;;', @$keys;
- if ( $field->{NAME} =~ /^id/ && $field->{FUNCTION} eq 'COUNT' ) {
- $info{$key}{ $field->{NAME} }++;
- }
- elsif ( $field->{NAME} =~ /^postfunction/ ) {
- if ( $field->{MAP} ) {
- my ($meta_type) = $field->{INFO}[1] =~ /^(\w+)All$/;
- for my $item ( values %{ $field->{MAP} } ) {
- push @fields,
- {
- NAME => $item->{NAME},
- FIELD => $item->{FIELD},
- INFO => [.
- '', $meta_type,
- $item->{FUNCTION} =~ /^(\w+)/ ? $1 : '',
- @{ $field->{INFO} }[ 2 .. $#{ $field->{INFO} } ],
- ],
- };
- }
- }
- }
- elsif ( $field->{INFO}[1] eq 'Time' ) {
- if ( $field->{NAME} =~ /^(TimeWorked|TimeEstimated|TimeLeft)$/ ) {
- my $method = $1;
- my $type = $field->{INFO}[2];
- my $name = lc $field->{NAME};
-
- $info{$key}{$name}
- = $self->_CalculateTime( $type, $ticket->$method * 60, $info{$key}{$name} ) || 0;
- }
- else {
- RT->Logger->error("Unsupported field $field->{NAME}");
- }
- }
- elsif ( $field->{INFO}[1] eq 'DateTimeInterval' ) {
- my ( undef, undef, $type, $start, $end, $extra_info ) = @{ $field->{INFO} };
- my $name = lc $field->{NAME};
- $info{$key}{$name} ||= 0;
-
- my $start_method = $start . 'Obj';
- my $end_method = $end . 'Obj';
- next unless $ticket->$end_method->Unix > 0 && $ticket->$start_method->Unix > 0;
-
- my $value;
- if ($extra_info->{business_time}) {
- $value = $ticket->CustomDateRange(
- '',
- { value => "$end - $start",
- business_time => $extra_info->{business_time},
- format => sub { return $_[0] },
- }
- );
- }
- else {
- $value = $ticket->$end_method->Unix - $ticket->$start_method->Unix;
- }
-
- $info{$key}{$name} = $self->_CalculateTime( $type, $value, $info{$key}{$name} );
- }
- elsif ( $field->{INFO}[1] eq 'CustomDateRange' ) {
- my ( undef, undef, $type, $range_name ) = @{ $field->{INFO} };
- my $name = lc $field->{NAME};
- $info{$key}{$name} ||= 0;
-
- my $value;
- my %ranges = RT::Ticket->CustomDateRanges;
- if ( my $spec = $ranges{$range_name} ) {
- $value = $ticket->CustomDateRange(
- $range_name,
- {
- ref $spec eq 'HASH' ? %$spec : ( value => $spec ),
- format => sub { $_[0] },
- }
- );
- }
- $info{$key}{$name} = $self->_CalculateTime( $type, $value, $info{$key}{$name} );
- }
- else {
- RT->Logger->error("Unsupported field $field->{INFO}[1]");
- }
- }
- }
-
- for my $keys (@all_keys) {
- my $key = join ';;;', @$keys;
- push @{ $info{$key}{ids} }, $ticket->id;
- }
- }
-
- # Make generated results real SB results
- for my $key ( keys %info ) {
- my @keys = split /;;;/, $key;
- my $row;
- for my $group ( @groups ) {
- $row->{lc $group->{NAME}} = shift @keys;
- }
- for my $field ( keys %{ $info{$key} } ) {
- my $value = $info{$key}{$field};
- if ( ref $value eq 'HASH' && $value->{calculate} ) {
- $row->{$field} = $value->{calculate}->($value);
- }
- else {
- $row->{$field} = $info{$key}{$field};
- }
- }
- my $item = $self->NewItem();
-
- # Has extra css info
- for my $key (@keys) {
- if ( $key =~ /(.+) => (.+)/ ) {
- $row->{_css_class}{$1} = $2;
- }
- }
-
- $item->LoadFromHash($row);
- $self->AddRecord($item);
- }
- $self->{must_redo_search} = 0;
- $self->{is_limited} = 1;
- $self->PostProcessRecords;
-
- return;
- }
+ return $self->_DoSearchInPerl(@_) if $self->{_query};

$self->SUPER::_DoSearch( @_ );
$self->_PostSearch();
diff --git a/lib/RT/Report/Transactions.pm b/lib/RT/Report/Transactions.pm
index 3d285befc2..64557057a4 100644
--- a/lib/RT/Report/Transactions.pm
+++ b/lib/RT/Report/Transactions.pm
@@ -115,7 +115,7 @@ sub SetupGroupings {
$self->Limit( FIELD => 'Id', OPERATOR => 'IN', VALUE => \@match );
}

- return $self->SUPER::SetupGroupings(%args);
+ return $self->_SetupGroupings(%args);
}

sub _DoSearch {

commit 73b1fb96a09835dac2a36826f251566cef2eef18
Author: sunnavy <sunnavy@bestpractical.com>
Date: Fri Dec 1 13:49:49 2023 -0500

Abstract procedures to get queue-specific custom fields and roles

With this abstraction, we will be able to add corresponding support to
asset charts easily, where we need to get catalog-specific custom fields
and roles.

diff --git a/lib/RT/Report.pm b/lib/RT/Report.pm
index 916ae47a0c..3db792e31f 100644
--- a/lib/RT/Report.pm
+++ b/lib/RT/Report.pm
@@ -124,27 +124,7 @@ our %GROUPINGS_META = (

my @res;
if ( $args->{key} =~ /^CustomRole/ ) {
- 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, Class => ref $self );
- $queues = $args->{'Queues'} = $tree->GetReferencedQueues( CurrentUser => $self->CurrentUser );
- }
- return () unless $queues;
-
- my $crs = RT::CustomRoles->new( $self->CurrentUser );
- $crs->LimitToLookupType( $self->RecordClass->CustomFieldLookupType );
- # Adding this to avoid returning all records when no queues are available.
- $crs->LimitToObjectId(0);
-
- for my $id ( keys %$queues ) {
- my $queue = RT::Queue->new( $self->CurrentUser );
- $queue->Load($id);
- next unless $queue->id;
-
- $crs->LimitToObjectId( $queue->id );
- }
+ my $crs = $self->GetCustomRoles(%$args);
while ( my $cr = $crs->Next ) {
for my $field ( @{ $fields{ $cr->MaxValues ? 'user' : 'principal' } } ) {
push @res, [ $cr->Name, $field ], "CustomRole.{" . $cr->id . "}.$field";
@@ -256,28 +236,8 @@ our %GROUPINGS_META = (
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, Class => ref $self );
- $queues = $args->{'Queues'} = $tree->GetReferencedQueues( CurrentUser => $self->CurrentUser );
- }
- return () unless $queues;
-
my @res;
-
- my $CustomFields = RT::CustomFields->new( $self->CurrentUser );
- $CustomFields->LimitToLookupType( $self->RecordClass->CustomFieldLookupType );
- $CustomFields->LimitToObjectId(0);
- foreach my $id (keys %$queues) {
- my $queue = RT::Queue->new( $self->CurrentUser );
- $queue->Load($id);
- next unless $queue->id;
- $CustomFields->SetContextObject( $queue ) if keys %$queues == 1;
- $CustomFields->LimitToObjectId($queue->id);
- }
+ my $CustomFields = $self->GetCustomFields(%$args);
while ( my $CustomField = $CustomFields->Next ) {
push @res, ["Custom field", $CustomField->Name], "CF.{". $CustomField->id ."}";
}
@@ -1336,23 +1296,7 @@ sub _SetupCustomDateRanges {
sub _NumericCustomFields {
my $self = shift;
my %args = @_;
- my $custom_fields = RT::CustomFields->new( $self->CurrentUser );
- $custom_fields->LimitToLookupType( $self->RecordClass->CustomFieldLookupType );
- $custom_fields->LimitToObjectId(0);
-
- if ( $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, Class => ref $self );
- my $queues = $tree->GetReferencedQueues( CurrentUser => $self->CurrentUser );
- foreach my $id ( keys %$queues ) {
- my $queue = RT::Queue->new( $self->CurrentUser );
- $queue->Load($id);
- next unless $queue->id;
- $custom_fields->SetContextObject($queue) if keys %$queues == 1;
- $custom_fields->LimitToObjectId( $queue->id );
- }
- }
+ my $custom_fields = $self->GetCustomFields(%args);

my @items;
while ( my $custom_field = $custom_fields->Next ) {
@@ -1454,6 +1398,85 @@ sub _SingularClass {
return (ref $self || $self) . '::Entry';
}

+=head2 GetReferencedObjects Query => QUERY
+
+This is generally an abstraction of GetReferenced... methods in
+L<RT::Interface::Web::QueryBuilder::Tree>, based on what current report is for.
+
+Returns a tuple of the class and referenced objects.
+
+=cut
+
+sub GetReferencedObjects {
+ my $self = shift;
+ my %args = @_;
+
+ my $class = 'RT::Queue';
+ my $method = 'GetReferencedQueues';
+
+ my $objects;
+ if ( $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, Class => ref $self );
+ $objects = $tree->$method( CurrentUser => $self->CurrentUser );
+ }
+ return ( $class, $objects );
+}
+
+=head2 GetCustomFields Query => QUERY
+
+Returns an L<RT::CustomFields> object that contains all possible custom
+fields the given query can refer to.
+
+=cut
+
+sub GetCustomFields {
+ my $self = shift;
+ my %args = @_;
+
+ my $custom_fields = RT::CustomFields->new( $self->CurrentUser );
+ $custom_fields->LimitToLookupType( $self->RecordClass->CustomFieldLookupType );
+ $custom_fields->LimitToObjectId(0);
+
+ if ( $args{'Query'} ) {
+ my ( $referenced_class, $referenced_objects ) = $self->GetReferencedObjects(%args);
+ foreach my $id ( keys %{$referenced_objects} ) {
+ my $object = $referenced_class->new( $self->CurrentUser );
+ $object->Load($id);
+ next unless $object->id;
+ $custom_fields->SetContextObject($object) if keys %{$referenced_objects} == 1;
+ $custom_fields->LimitToObjectId( $object->id );
+ }
+ }
+ return $custom_fields;
+}
+
+=head2 GetCustomRoles Query => QUERY
+
+Returns an L<RT::CustomRoles> object that contains all possible custom
+roles the given query can refer to.
+
+=cut
+
+sub GetCustomRoles {
+ my $self = shift;
+ my %args = @_;
+
+ my $custom_roles = RT::CustomRoles->new( $self->CurrentUser );
+ $custom_roles->LimitToLookupType( $self->RecordClass->CustomFieldLookupType );
+ # Adding this to avoid returning all records when no queues are available.
+ $custom_roles->LimitToObjectId(0);
+
+ my ( $referenced_class, $referenced_objects ) = $self->GetReferencedObjects(%args);
+ foreach my $id ( keys %{$referenced_objects} ) {
+ my $object = $referenced_class->new( $self->CurrentUser );
+ $object->Load($id);
+ next unless $object->id;
+ $custom_roles->LimitToObjectId( $object->id );
+ }
+ return $custom_roles;
+}

RT::Base->_ImportOverlays();


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


hooks/post-receive
--
rt
_______________________________________________
rt-commit mailing list
rt-commit@lists.bestpractical.com
https://lists.bestpractical.com/mailman/listinfo/rt-commit