diff options
Diffstat (limited to 'rt/share/html/Search')
30 files changed, 1031 insertions, 603 deletions
diff --git a/rt/share/html/Search/Build.html b/rt/share/html/Search/Build.html index eea5f81ca..8ce404a31 100644 --- a/rt/share/html/Search/Build.html +++ b/rt/share/html/Search/Build.html @@ -53,7 +53,7 @@ %# Build/Edit.html (Advanced).) %# %# After doing some stuff with default arguments and saved searches, the ParseQuery -%# function (which is similar to, but not the same as, _parser in lib/RT/Tickets_SQL.pm) +%# function (which is similar to, but not the same as, _parser in lib/RT/Tickets.pm) %# converts the Query into a RT::Interface::Web::QueryBuilder::Tree. This mason file %# then adds stuff to or modifies the tree based on the actions that had been requested %# by clicking buttons. It then calls GetQueryAndOptionList on the tree to generate @@ -140,7 +140,11 @@ if ( $NewQuery ) { my $current = $session{'CurrentSearchHash'}; my $prefs = $session{'CurrentUser'}->UserObj->Preferences("SearchDisplay") || {}; - my $default = { Query => '', Format => '', OrderBy => 'id', Order => 'ASC', RowsPerPage => 50 }; + my $default = { Query => '', + Format => '', + OrderBy => RT->Config->Get('DefaultSearchResultOrderBy'), + Order => RT->Config->Get('DefaultSearchResultOrder'), + RowsPerPage => 50 }; for( qw(Query Format OrderBy Order RowsPerPage) ) { $query{$_} = $current->{$_} unless defined $query{$_}; @@ -188,9 +192,15 @@ my @options = $tree->GetDisplayedNodes; my @current_values = grep defined, @options[@clauses]; my @new_values = (); +my $cf_field_names = + join "|", + map quotemeta, + grep { $RT::Tickets::FIELD_METADATA{$_}->[0] eq 'CUSTOMFIELD' } + sort keys %RT::Tickets::FIELD_METADATA; + # Try to find if we're adding a clause foreach my $arg ( keys %ARGS ) { - next unless $arg =~ m/^ValueOf([\w\.]+|'\w*CF\.\{.*?\}')$/ + next unless $arg =~ m/^ValueOf(\w+|($cf_field_names).\{.*?\})$/ && ( ref $ARGS{$arg} eq "ARRAY" ? grep $_ ne '', @{ $ARGS{$arg} } : $ARGS{$arg} ne '' ); @@ -220,6 +230,7 @@ foreach my $arg ( keys %ARGS ) { for ( my $i = 0; $i < @ops; $i++ ) { my ( $op, $value ) = ( $ops[$i], $values[$i] ); next if !defined $value || $value eq ''; + my $rawvalue = $value; if ( $value =~ /^NULL$/i && $op =~ /=/ ) { if ( $op eq '=' ) { @@ -234,16 +245,15 @@ foreach my $arg ( keys %ARGS ) { $value = "'$value'"; } - if ($keyword =~ /^'(\w*CF)\.\{(.*)\}'/) { - my ($field, $cf) = ($1, $2); - $cf =~ s/(['\\])/\\$1/g; - $keyword = "'$field.{$cf}'"; + if ($keyword =~ s/(['\\])/\\$1/g or $keyword =~ /[^{}\w\.]/) { + $keyword = "'$keyword'"; } my $clause = { Key => $keyword, Op => $op, - Value => $value + Value => $value, + RawValue => $rawvalue, }; push @new_values, RT::Interface::Web::QueryBuilder::Tree->new($clause); diff --git a/rt/share/html/Search/Bulk.html b/rt/share/html/Search/Bulk.html index 89e850bfa..90fec6848 100755 --- a/rt/share/html/Search/Bulk.html +++ b/rt/share/html/Search/Bulk.html @@ -50,13 +50,13 @@ <& /Elements/ListActions, actions => \@results &> <form method="post" action="<% RT->Config->Get('WebPath') %>/Search/Bulk.html" enctype="multipart/form-data" name="BulkUpdate" id="BulkUpdate"> -% foreach my $var (qw(Query Format OrderBy Order Rows Page SavedChartSearchId)) { +% foreach my $var (qw(Query Format OrderBy Order Rows Page SavedSearchId SavedChartSearchId Token)) { <input type="hidden" class="hidden" name="<%$var%>" value="<%$ARGS{$var} || ''%>" /> %} <& /Elements/CollectionList, Query => $Query, - DisplayFormat => $Format, - Format => $ARGS{'Format'}, + DisplayFormat => $DisplayFormat, + Format => $Format, Verbatim => 1, AllowSorting => 1, OrderBy => $OrderBy, @@ -71,7 +71,7 @@ <hr /> -<& /Elements/Submit, Label => loc('Update'), CheckboxNameRegex => '/^UpdateTicket\d+$/', CheckAll => 1, ClearAll => 1 &> +<& /Elements/Submit, Label => loc('Update'), CheckboxNameRegex => '/^UpdateTicket(All)?$/', CheckAll => 1, ClearAll => 1 &> <br /> <&|/Widgets/TitleBox, title => $title &> <table> @@ -80,20 +80,20 @@ <table> <tr><td class="label"> <&|/l&>Make Owner</&>: </td> <td class="value"> <& /Elements/SelectOwner, Name => "Owner", Default => $ARGS{Owner} || '' &> -(<input type="checkbox" class="checkbox" name="ForceOwnerChange" - <% $ARGS{ForceOwnerChange} ? 'checked="checked"' : '' %> /> <&|/l&>Force change</&>) </td></tr> +<label>(<input type="checkbox" class="checkbox" name="ForceOwnerChange" +<% $ARGS{ForceOwnerChange} ? 'checked="checked"' : '' %> /> <&|/l&>Force change</&>)</label></td></tr> <tr><td class="label"> <&|/l&>Add Requestor</&>: </td> -<td class="value"> <input name="AddRequestor" size="20" value="<% $ARGS{AddRequestor} || '' %>" /> </td></tr> +<td class="value"> <& /Elements/EmailInput, Name => "AddRequestor", Size=> 20, Default => $ARGS{AddRequestor} &> </td></tr> <tr><td class="label"> <&|/l&>Remove Requestor</&>: </td> -<td class="value"> <input name="DeleteRequestor" size="20" value="<% $ARGS{DeleteRequestor} || '' %>"/> </td></tr> +<td class="value"> <& /Elements/EmailInput, Name => "DeleteRequestor", Size=> 20, Default => $ARGS{DeleteRequestor} &> </td></tr> <tr><td class="label"> <&|/l&>Add Cc</&>: </td> -<td class="value"> <input name="AddCc" size="20" value="<% $ARGS{AddCc} || '' %>" /> </td></tr> +<td class="value"> <& /Elements/EmailInput, Name => "AddCc", Size=> 20, Default => $ARGS{AddCc} &> </td></tr> <tr><td class="label"> <&|/l&>Remove Cc</&>: </td> -<td class="value"> <input name="DeleteCc" size="20" value="<% $ARGS{DeleteCc} || '' %>" /> </td></tr> +<td class="value"> <& /Elements/EmailInput, Name => "DeleteCc", Size=> 20, Default => $ARGS{DeleteCc} &> </td></tr> <tr><td class="label"> <&|/l&>Add AdminCc</&>: </td> -<td class="value"> <input name="AddAdminCc" size="20" value="<% $ARGS{AddAdminCc} || '' %>" /> </td></tr> +<td class="value"> <& /Elements/EmailInput, Name => "AddAdminCc", Size=> 20, Default => $ARGS{AddAdminCc} &> </td></tr> <tr><td class="label"> <&|/l&>Remove AdminCc</&>: </td> -<td class="value"> <input name="DeleteAdminCc" size="20" value="<% $ARGS{DeleteAdminCc} || '' %>" /> </td></tr> +<td class="value"> <& /Elements/EmailInput, Name => "DeleteAdminCc", Size=> 20, Default => $ARGS{DeleteAdminCc} &> </td></tr> </table> </td> <td valign="top"> @@ -111,7 +111,7 @@ <tr><td class="label"> <&|/l&>Make queue</&>: </td> <td class="value"> <& /Elements/SelectQueue, Name => "Queue", Default => $ARGS{Queue} &> </td></tr> <tr><td class="label"> <&|/l&>Make Status</&>: </td> -<td class="value"> <& /Elements/SelectStatus, Name => "Status", Default => $ARGS{Status}, Queues => $seen_queues &> </td></tr> +<td class="value"> <& /Ticket/Elements/SelectStatus, Name => "Status", Default => $ARGS{Status}, Queues => $seen_queues &> </td></tr> <tr><td class="label"> <&|/l&>Make date Starts</&>: </td> <td class="value"> <& /Elements/SelectDate, Name => "Starts_Date", Default => $ARGS{Starts_Date} || '' &> </td></tr> <tr><td class="label"> <&|/l&>Make date Started</&>: </td> @@ -120,8 +120,6 @@ <td class="value"> <& /Elements/SelectDate, Name => "Told_Date", Default => $ARGS{Told_Date} || '' &> </td></tr> <tr><td class="label"> <&|/l&>Make date Due</&>: </td> <td class="value"> <& /Elements/SelectDate, Name => "Due_Date", Default => $ARGS{Due_Date} || '' &> </td></tr> -<tr><td class="label"> <&|/l&>Make date Resolved</&>: </td> -<td class="value"> <& /Elements/SelectDate, Name => "Resolved_Date", Default => $ARGS{Resolved_Date} || '' &> </td></tr> </table> </td> @@ -131,28 +129,34 @@ <&| /Widgets/TitleBox, title => loc('Add comments or replies to selected tickets') &> <table> <tr><td align="right"><&|/l&>Update Type</&>:</td> -<td><select name="UpdateType"> +<td><select name="UpdateType" id="UpdateType"> <option value="private" <% $ARGS{UpdateType} && $ARGS{UpdateType} eq 'private' ? 'selected="selected"' : '' %> ><&|/l&>Comments (Not sent to requestors)</&></option> <option value="response" <% $ARGS{UpdateType} && $ARGS{UpdateType} eq 'response' ? 'selected="selected"' : '' %>><&|/l&>Reply to requestors</&></option> </select> </td></tr> -<tr><td align="right"><&|/l&>Subject</&>:</td><td> <input name="UpdateSubject" -size="60" value="<% $ARGS{UpdateSubject} || "" %>" /></td></tr> +<tr> + <td align="right"><&|/l&>Subject</&>:</td> + <td> + <input name="UpdateSubject" size="60" value="<% $ARGS{UpdateSubject} || "" %>" /> +% $m->callback( %ARGS, CallbackName => 'AfterUpdateSubject' ); + </td> +</tr> +% $m->callback( CallbackName => 'BeforeTransactionCustomFields', CustomFields => $TxnCFs ); % while (my $CF = $TxnCFs->Next()) { <tr> <td align="right"><% $CF->Name %>:</td> -<td><& /Elements/EditCustomField, - CustomField => $CF, - NamePrefix => "Object-RT::Transaction--CustomField-", - Default => $ARGS{"Object-RT::Transaction--CustomField-" . $CF->id . '-Values'} || - $ARGS{"Object-RT::Transaction--CustomField-" . $CF->id . '-Value'}, +<td><& /Elements/EditCustomField, + CustomField => $CF, + Object => RT::Transaction->new( $session{'CurrentUser'} ), &><em><% $CF->FriendlyType %></em></td> </td></tr> % } # end if while <& /Ticket/Elements/AddAttachments, %ARGS &> - <tr><td class="labeltop"><&|/l&>Message</&>:</td><td> + <tr><td class="labeltop"><&|/l&>Message</&>:</td> + <td class="messagebox-container action-<% $ARGS{UpdateType} || 'private' %>"> +% $m->callback( %ARGS, CallbackName => 'BeforeMessageBox' ); %# Currently, bulk update always starts with Comment not Reply selected, so we check this unconditionally % my $IncludeSignature = RT->Config->Get('MessageBoxIncludeSignatureOnComment'); <& /Elements/MessageBox, Name => "UpdateContent", @@ -168,59 +172,22 @@ size="60" value="<% $ARGS{UpdateSubject} || "" %>" /></td></tr> my $cfs = RT::CustomFields->new($session{'CurrentUser'}); $cfs->LimitToGlobal(); $cfs->LimitToQueue($_) for keys %$seen_queues; +$cfs->SetContextObject( values %$seen_queues ) if keys %$seen_queues == 1; </%perl> -% if ($cfs->Count) { -<&|/Widgets/TitleBox, title => loc('Edit Custom Fields'), color => "#336633"&> -<table> -<tr> -<th><&|/l&>Name</&></th> -<th><&|/l&>Add values</&></th> -<th><&|/l&>Delete values</&></th> -</tr> -% while (my $cf = $cfs->Next()) { -<tr> -<td class="label"><% loc($cf->Name) %><br /> -<em>(<%$cf->FriendlyType%>)</em></td> -% my $rows = 5; -% my $cf_id = $cf->id; -% my @add = (NamePrefix => 'Bulk-Add-CustomField-', CustomField => $cf, Rows => $rows, -% Multiple => ($cf->MaxValues ==1 ? 0 : 1) , Cols => 25, -% Default => $ARGS{"Bulk-Add-CustomField-$cf_id-Values"} || $ARGS{"Bulk-Add-CustomField-$cf_id-Value"}, ); -% my @del = (NamePrefix => 'Bulk-Delete-CustomField-', CustomField => $cf, -% Rows => $rows, Multiple => 1, Cols => 25, -% Default => $ARGS{"Bulk-Delete-CustomField-$cf_id-Values"} || $ARGS{"Bulk-Delete-CustomField-$cf_id-Value"}, ); -% if ($cf->Type eq 'Select') { -<td><& /Elements/EditCustomFieldSelect, @add &></td> -<td><& /Elements/EditCustomFieldSelect, @del &></td> -% } elsif ($cf->Type eq 'Combobox') { -<td><& /Elements/EditCustomFieldCombobox, @add &></td> -<td><& /Elements/EditCustomFieldCombobox, @del &></td> -% } elsif ($cf->Type eq 'Freeform') { -<td><& /Elements/EditCustomFieldFreeform, @add &></td> -<td><& /Elements/EditCustomFieldFreeform, @del &></td> -% } elsif ($cf->Type eq 'Text') { -<td><& /Elements/EditCustomFieldText, @add &></td> -<td> </td> -% } elsif ($cf->Type eq 'Date') { -<td><& /Elements/EditCustomFieldDate, @add, Default => undef &></td> -<td><& /Elements/EditCustomFieldDate, @del, Default => undef &></td> -% } elsif ($cf->Type eq 'DateTime') { -% # Pass datemanip format to prevent another tz date conversion -<td><& /Elements/EditCustomFieldDateTime, @add, Default => undef, Format => 'datemanip' &></td> -<td><& /Elements/EditCustomFieldDateTime, @del, Default => undef, Format => 'datemanip' &></td> -% } else { -% $RT::Logger->crit("Unknown CustomField type: " . $cf->Type); -% } -</tr> -% } -</table> +% if ( $cfs->Count ) { +<&|/Widgets/TitleBox, title => loc('Edit Custom Fields') &> +<& /Elements/BulkCustomFields, $ARGS{'AddMoreAttach'} ? %ARGS : (), CustomFields => $cfs &> </&> % } <&|/Widgets/TitleBox, title => loc('Edit Links'), color => "#336633"&> <em><&|/l&>Enter tickets or URIs to link tickets to. Separate multiple entries with spaces.</&></em><br /> -<& /Ticket/Elements/BulkLinks, Tickets => $Tickets, $ARGS{'AddMoreAttach'} ? %ARGS : () &> +<& /Elements/BulkLinks, Collection => $Tickets, $ARGS{'AddMoreAttach'} ? %ARGS : () &> +</&> + +<&| /Widgets/TitleBox, title => loc('Merge'), color => '#336633' &> +<& /Ticket/Elements/EditMerge, Tickets => $Tickets, %ARGS &> </&> <& /Elements/Submit, Label => loc('Update') &> @@ -248,7 +215,10 @@ $Page ||= 1; $Format ||= RT->Config->Get('DefaultSearchResultFormat'); # inject _CHECKBOX to the first field. -$Format =~ s/'?([^']+)'?,/'___CHECKBOX__$1',/; #' +my $DisplayFormat = "'__CheckBox.{UpdateTicket}__',". $Format; +$DisplayFormat =~ s/\s*,\s*('?__NEWLINE__'?)/,$1,''/gi; + +$DECODED_ARGS->{'UpdateTicketAll'} = 1 unless @UpdateTicket; my $Tickets = RT::Tickets->new( $session{'CurrentUser'} ); $Tickets->FromSQL($Query); @@ -276,7 +246,8 @@ Abort( loc("No search to operate on.") ) unless ($Tickets); my $fields = {}; my $seen_queues = {}; while ( my $ticket = $Tickets->Next ) { - next if $seen_queues->{ $ticket->Queue }++; + next if $seen_queues->{ $ticket->Queue }; + $seen_queues->{ $ticket->Queue } ||= $ticket->QueueObj; my $custom_fields = $ticket->CustomFields; while ( my $field = $custom_fields->Next ) { @@ -289,13 +260,6 @@ my @linkresults; $Tickets->RedoSearch(); -# pull out the labels for any custom fields we want to update - -my $cf_del_keys; -@$cf_del_keys = grep { /^Bulk-Delete-CustomField/ } keys %ARGS; -my $cf_add_keys; -@$cf_add_keys = grep { /^Bulk-Add-CustomField/ } keys %ARGS; - if ( defined($ARGS{'Priority'}) and ($ARGS{'Priority-Mode'} || '') eq 'relative' ) { # magic in Ticket::SetPriority @@ -304,19 +268,19 @@ if ( defined($ARGS{'Priority'}) delete $ARGS{'Priority-Mode'}; unless ( $ARGS{'AddMoreAttach'} ) { - # Add session attachments if any to be processed by ProcessUpdateMessage - $ARGS{'UpdateAttachments'} = $session{'Attachments'} if ( $session{'Attachments'} ); while ( my $Ticket = $Tickets->Next ) { - next unless ( $ARGS{ "UpdateTicket" . $Ticket->Id } ); + my $tid = $Ticket->id; + next unless grep $tid == $_, @UpdateTicket; #Update the links $ARGS{'id'} = $Ticket->id; my @updateresults = ProcessUpdateMessage( - TicketObj => $Ticket, - ARGSRef => \%ARGS, - ); + TicketObj => $Ticket, + ARGSRef => \%ARGS, + KeepAttachments => 1, + ); #Update the basics. my @basicresults = @@ -328,86 +292,11 @@ unless ( $ARGS{'AddMoreAttach'} ) { my @watchresults = ProcessTicketWatchers( TicketObj => $Ticket, ARGSRef => \%ARGS ); - foreach my $type (qw(MergeInto DependsOn MemberOf RefersTo)) { - $ARGS{ $Ticket->id . "-" . $type } = $ARGS{"Ticket-$type"}; - $ARGS{ $type . "-" . $Ticket->id } = $ARGS{"$type-Ticket"}; - } - @linkresults = - ProcessTicketLinks( TicketObj => $Ticket, ARGSRef => \%ARGS ); - foreach my $type (qw(MergeInto DependsOn MemberOf RefersTo)) { - delete $ARGS{ $type . "-" . $Ticket->id }; - delete $ARGS{ $Ticket->id . "-" . $type }; - } - - my @cfresults; - - foreach my $list ( $cf_add_keys, $cf_del_keys ) { - next unless $list->[0]; - - - my $op; - if ( $list->[0] =~ /Add/ ) { - $op = 'add'; - - } - elsif ( $list->[0] =~ /Del/ ) { - $op = 'del'; - } - else { - $RT::Logger->crit( - "Got an op that was neither add nor delete. can never happen" - . $list->[0] ); - last; - } - - foreach my $key (@$list) { - my ( $cfid, $cf ); - next if $key =~ /CustomField-(\d+)-Category$/; - if ( $key =~ /CustomField-(\d+)-/ ) { - $cfid = $1; - $cf = RT::CustomField->new( $session{'CurrentUser'} ); - $cf->Load($cfid); - } - else {next} - my @values = - ref( $ARGS{$key} ) eq 'ARRAY' - ? @{ $ARGS{$key} } - : ( $ARGS{$key} ); - map { s/(\r\n|\r)/\n/g; } @values; # fix the newlines - # now break the multiline values into multivalues - @values = map { split( /\n/, $_ ) } @values - unless ( $cf->SingleValue ); - - my $current_values = $Ticket->CustomFieldValues($cfid); - - if ( $cf->Type eq 'DateTime' || $cf->Type eq 'Date' ){ - # Clear out empty string submissions to avoid - # Not set changed to Not set - @values = grep length, @values; - } - - foreach my $value (@values) { - - if ( $op eq 'del' ) { - if ( my $entry = $current_values->HasEntry($value) ) { - my ( $id, $msg ) = $Ticket->DeleteCustomFieldValue( - Field => $cfid, - ValueId => $entry->id, - ); - push @cfresults, $msg; - } - } - - elsif ( $op eq 'add' && !$current_values->HasEntry($value) ) { - my ( $id, $msg ) = $Ticket->AddCustomFieldValue( - Field => $cfid, - Value => $value - ); - push @cfresults, $msg; - } - } - } - } + @linkresults = + ProcessTicketLinks( TicketObj => $Ticket, TicketId => 'Ticket', ARGSRef => \%ARGS ); + + my @cfresults = ProcessRecordBulkCustomFields( RecordObj => $Ticket, ARGSRef => \%ARGS ); + my @statusresults = ProcessTicketStatus( TicketObj => $Ticket, ARGSRef => \%ARGS ); @@ -426,8 +315,7 @@ unless ( $ARGS{'AddMoreAttach'} ) { @results = ( @results, @tempresults ); } - # Cleanup WebUI - delete $session{'Attachments'}; + delete $session{'Attachments'}{ $ARGS{'Token'} }; $Tickets->RedoSearch(); } @@ -435,6 +323,7 @@ unless ( $ARGS{'AddMoreAttach'} ) { my $TxnCFs = RT::CustomFields->new( $session{CurrentUser} ); $TxnCFs->LimitToLookupType( RT::Transaction->CustomFieldLookupType ); $TxnCFs->LimitToGlobalOrObjectId( keys %$seen_queues ); +$TxnCFs->SetContextObject( values %$seen_queues ) if keys %$seen_queues == 1; </%INIT> <%args> @@ -445,6 +334,5 @@ $RowsPerPage => undef $Order => 'ASC' $OrderBy => 'id' $Query => undef -$SavedSearchId => undef -$SavedChartSearchId => undef +@UpdateTicket => () </%args> diff --git a/rt/share/html/Search/Chart b/rt/share/html/Search/Chart index 2a28d62c4..881a3d6d9 100644 --- a/rt/share/html/Search/Chart +++ b/rt/share/html/Search/Chart @@ -46,132 +46,403 @@ %# %# END BPS TAGGED BLOCK }}} <%args> +$Cache => undef $Query => "id > 0" -$PrimaryGroupBy => 'Queue' -$ChartStyle => 'bar' +@GroupBy => () +$ChartStyle => 'bar+table+sql' +@ChartFunction => 'COUNT' +$Width => undef +$Height => undef </%args> <%init> -my $chart_class; use GD; use GD::Text; -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'}; + +s/\D//g for grep defined, $Width, $Height; +$Width ||= 600; +$Height ||= ($ChartStyle =~ /\bpie\b/ ? $Width : 400); +$Height = $Width if $ChartStyle =~ /\bpie\b/; + +my $plot_error = sub { + my $text = shift; + my ($plot, $error); + + my $create_plot = sub { + my ($width, $height) = @_; + + my $plot = GD::Image->new($width => $height); + $plot->colorAllocate(255, 255, 255); # background + my $black = $plot->colorAllocate(0, 0, 0); + + require GD::Text::Wrap; + my $error = GD::Text::Wrap->new($plot, + color => $black, + text => $text, + align => "left", + width => $width - 20, + preserve_nl => 1, + ); + $error->set_font( $font, 16 ); + return ($plot, $error); + }; + + ($plot, $error) = $create_plot->($Width, $Height); + my $text_height = ($error->get_bounds(0, 0))[3]; + + # GD requires us to replot it all with the new height + ($plot, $error) = $create_plot->($Width, $text_height + 20); + + $error->draw(10, 10); + $m->comp( 'SELF:Plot', plot => $plot, %ARGS ); +}; 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}; -my ($count_name, $value_name) = $tix->SetupGroupings( - Query => $Query, GroupBy => $PrimaryGroupBy, -); +my $report = RT::Report::Tickets->new( $session{'CurrentUser'} ); -my %class = ( - Queue => 'RT::Queue', - Owner => 'RT::User', - Creator => 'RT::User', - LastUpdatedBy => 'RT::User', -); -my $class = $class{ $PrimaryGroupBy }; +my %columns; +if ( $Cache and my $data = delete $session{'charts_cache'}{ $Cache } ) { + %columns = %{ $data->{'columns'} }; + $report->Deserialize( $data->{'report'} ); + $session{'i'}++; +} else { + %columns = $report->SetupGroupings( + Query => $Query, + GroupBy => \@GroupBy, + Function => \@ChartFunction, + ); -my %data; + $report->SortEntries; +} + +my @data = ([],[]); my $max_value = 0; +my $min_value; 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 $value = $entry->__Value( $count_name ); - if ($chart_class eq 'GD::Graph::pie') { - $key = loc($key) ." - ". $value; - } else { - $key = loc($key); +while ( my $entry = $report->Next ) { + push @{ $data[0] }, [ map $entry->LabelValue( $_ ), @{ $columns{'Groups'} } ]; + + 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; } - $data{ $key } = $value; - $max_value = $value if $max_value < $value; - $max_key_length = length $key if $max_key_length < length $key; -} -unless (keys %data) { - $data{''} = 0; + my $i = 0; + push @{ $data[++$i] }, $_ foreach @values; + + foreach my $v ( @values ) { + $max_value = $v if $max_value < $v; + $min_value = $v if !defined $min_value || $min_value > $v; + } } +$ChartStyle =~ s/\bpie\b/bar/ if @data > 2; -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'); +my $chart_class; +if ($ChartStyle =~ /\bpie\b/) { + require GD::Graph::pie; + $chart_class = "GD::Graph::pie"; +} else { + require GD::Graph::bars; + $chart_class = "GD::Graph::bars"; +} # 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) { - my $plot = GD::Image->new(600 => 400); - $plot->colorAllocate(255, 255, 255); # background - my $black = $plot->colorAllocate(0, 0, 0); - - require GD::Text::Wrap; - my $error = GD::Text::Wrap->new($plot, - color => $black, - text => loc("No tickets found."), - ); - $error->set_font( $font, 16 ); - $error->draw(0, 0); - - $m->comp( 'SELF:Plot', plot => $plot, %ARGS ); +unless ( $report->Count ) { + return $plot_error->(loc("No tickets found.")); } +my $chart = $chart_class->new( $Width => $Height ); + +my %chart_options; if ($chart_class eq "GD::Graph::bars") { - my $count = keys %data; - $chart->set( - x_label => $tix->Label( $PrimaryGroupBy ), - y_label => loc('Tickets'), - show_values => 1, + my $count = @{ $data[0] }; + $chart_options{'bar_spacing'} = + $count > 30 ? 1 + : $count > 20 ? 2 + : $count > 10 ? 3 + : 5 + ; + if ( my $code = $report->LabelValueCode( $columns{'Functions'}[0] ) ) { + my %info = %{ $report->ColumnInfo( $columns{'Functions'}[0] ) }; + $chart_options{'values_format'} = $chart_options{'y_number_format'} = sub { + return $code->($report, %info, VALUE => shift ); + }; + } + $report->GotoFirstItem; + + # normalize min/max values to graph boundaries + { + my $integer = 1; + $integer = 0 for grep $_ ne int $_, $min_value, $max_value; + + $max_value *= $max_value > 0 ? 1.1 : 0.9 + if $max_value; + $min_value *= $min_value > 0 ? 0.9 : 1.1 + if $min_value; + + if ($integer) { + $max_value = int($max_value + ($max_value > 0? 1 : 0) ); + $min_value = int($min_value + ($min_value < 0? -1 : 0) ); + + my $span = abs($max_value - $min_value); + $max_value += 5 - ($span % 5); + } + $chart_options{'y_label_skip'} = 2; + $chart_options{'y_tick_number'} = 10; + } + my $text_size = sub { + my ($size, $text) = (@_); + my $font_handle = GD::Text::Align->new( + $chart->get('graph'), valign => 'top', 'halign' => 'center', + ); + $font_handle->set_font($font, $size); + $font_handle->set_text($text); + return $font_handle; + }; + + my $fitter = sub { + my %args = @_; + + foreach my $font_size ( @{$args{'sizes'}} ) { + my $line_height = $text_size->($font_size, 'Q')->get('height'); + + my $keyset_height = $line_height; + if ( ref $args{data}->[0] ) { + $keyset_height = $text_size->($font_size, join "\n", ('Q')x scalar @{ $args{data}->[0] }) + ->get('height'); + } + + my $status = 1; + foreach my $e ( @{ $args{data} } ) { + $status = $args{'cb'}->( + element => $e, + size => $font_size, + line_height => $line_height, + keyset_height => $keyset_height, + ); + last unless $status; + } + next unless $status; + + return $font_size; + } + return 0; + }; + + # try to fit in labels on X axis values, aka key + { + # we have several labels layouts: + # 1) horizontal, one line per label + # 2) horizontal, multi-line - doesn't work, GD::Chart bug + # 3) vertical, one line + # 4) vertical, multi-line + my %can = ( + 'horizontal, one line' => 1, + 'vertical, one line' => 1, + 'vertical, multi line' => @{$data[0][0]} > 1, + ); + + my $x_space_for_label = $Width*0.8/($count+1.5); + my $y_space_for_label = $Height*0.4; + + my $found_solution = $fitter->( + sizes => [12,11,10], + data => $data[0], + cb => sub { + my %args = @_; + + # if horizontal space doesn't allow us to fit one vertical line, + # then we need smaller font + return 0 if $args{'line_height'} > $x_space_for_label; + + my $width = $text_size->( $args{'size'}, join ' - ', @{ $args{'element'} } ) + ->get('width'); + + if ( $width > $x_space_for_label ) { + $can{'horizontal, one line'} = 0; + } + if ( $width > $y_space_for_label ) { + $can{'vertical, one line'} = 0; + } + if ( $args{'keyset_height'} >= $x_space_for_label ) { + $can{'vertical, multi line'} = 0; + } + if ( $can{'vertical, multi line'} ) { + my $width = $text_size->( $args{'size'}, join "\n", @{ $args{'element'} } ) + ->get('width'); + if ( $width > $y_space_for_label ) { + $can{'vertical, multi line'} = 0; + } + } + return 0 unless grep $_, values %can; + return 1; + }, + ); + if ( $found_solution ) { + $chart_options{'x_axis_font'} = [$font, $found_solution]; + + if ( $can{'horizontal, one line'} ) { + $chart_options{'x_labels_vertical'} = 0; + $_ = join ' - ', @$_ foreach @{$data[0]}; + } + elsif ( $can{'vertical, multi line'} ) { + $chart_options{'x_labels_vertical'} = 1; + $_ = join "\n", @$_ foreach @{$data[0]}; + } + else { + $chart_options{'x_labels_vertical'} = 1; + $_ = join " - ", @$_ foreach @{$data[0]}; + } + } + else { + my $font_handle = $text_size->(10, 'Q'); + my $line_height = $font_handle->get('height'); + if ( $line_height > $x_space_for_label ) { + $Width *= $line_height/$x_space_for_label; + $Width = int( $Width+1 ); + } + + $_ = join " - ", @$_ foreach @{$data[0]}; + + my $max_text_width = 0; + foreach (@{$data[0]}) { + $font_handle->set_text($_); + my $width = $font_handle->get('width'); + $max_text_width = $width if $width > $max_text_width; + } + if ( $max_text_width > $Height*0.4 ) { + $Height = int($max_text_width / 0.4 + 1); + } + + $chart_options{'x_labels_vertical'} = 1; + $chart_options{'x_axis_font'} = [$font, 10]; + } + } + + # use the same size for y axis labels + { + $chart_options{'y_axis_font'} = $chart_options{'x_axis_font'}; + } + + # try to fit in values above bars + { + # 0.8 is guess, labels for ticks on Y axis can be wider + # 1.5 for paddings around bars that GD::Graph adds + my $x_space_for_label = $Width*0.8/($count*(@data - 1)+1.5); + + my %can = ( + 'horizontal, one line' => 1, + 'vertical, one line' => 1, + ); + + my %seen; + my $found_solution = $fitter->( + sizes => [ grep $_ <= $chart_options{'x_axis_font'}[1], 12, 11, 10, 9 ], + data => [ map {@$_} @data[1..(@data-1)] ], + cb => sub { + my %args = @_; + + # if horizontal space doesn't allow us to fit one vertical line, + # then we need smaller font + return 0 if $args{'line_height'} > $x_space_for_label; + + my $value = $args{'element'}; + $value = $chart_options{'values_format'}->($value) + if $chart_options{'values_format'}; + return 1 if $seen{$value}++; + + my $width = $text_size->( $args{'size'}, $value )->get('width'); + if ( $width > $x_space_for_label ) { + $can{'horizontal, one line'} = 0; + } + my $y_space_for_label = $Height * 0.6 + *( 1 - ($args{'element'}-$min_value)/($max_value-$min_value) ); + if ( $width > $y_space_for_label ) { + $can{'vertical, one line'} = 0; + } + return 0 unless grep $_, values %can; + return 1; + }, + ); + $chart_options{'show_values'} = 1; + $chart_options{'hide_overlapping_values'} = 1; + if ( $found_solution ) { + $chart_options{'values_font'} = [ $font, $found_solution ], + $chart_options{'values_space'} = 2; + $chart_options{'values_vertical'} = + $can{'horizontal, one line'} ? 0 : 1; + } else { + $chart_options{'values_font'} = [ $font, 9 ], + $chart_options{'values_space'} = 1; + $chart_options{'values_vertical'} = 1; + } + } + + %chart_options = ( + %chart_options, + x_label => join( ' - ', map $report->Label( $_ ), @{ $columns{'Groups'} } ), x_label_position => 0.6, + y_label => $report->Label( $columns{'Functions'}[0] ), y_label_position => 0.6, - values_space => -1, # 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, + y_min_value => $min_value, # 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 ) ) - ), + bargroup_spacing => $chart_options{'bar_spacing'}*5, ); } +else { + my $i = 0; + while ( my $entry = $report->Next ) { + push @{ $data[0][$i++] }, $entry->LabelValue( $columns{'Functions'}[0] ); + } + $_ = join ' - ', @$_ foreach @{$data[0]}; +} + +if ($chart->get('width') != $Width || $chart->get('height') != $Height ) { + $chart = $chart_class->new( $Width => $Height ); +} -# refine values' colors, with both Color::Scheme's help and my own tweak -$chart->{dclrs} = [ - '66cc66', 'ff6666', 'ffcc66', '663399', - '3333cc', '339933', '993333', '996633', - '33cc33', 'cc3333', 'cc9933', '6633cc', -]; +%chart_options = ( + '3d' => 0, + title_font => [ $font, 16 ], + legend_font => [ $font, 16 ], + x_label_font => [ $font, 14 ], + y_label_font => [ $font, 14 ], + label_font => [ $font, 14 ], + y_axis_font => [ $font, 12 ], + values_font => [ $font, 12 ], + value_font => [ $font, 12 ], + %chart_options, +); + +foreach my $opt ( grep /_font$/, keys %chart_options ) { + my $v = delete $chart_options{$opt}; + next unless my $can = $chart->can("set_$opt"); + + $can->($chart, @$v); +} +$chart->set(%chart_options) if keys %chart_options; + +$chart->{dclrs} = [ RT->Config->Get("ChartColors") ]; { no warnings 'redefine'; @@ -182,8 +453,12 @@ $chart->{dclrs} = [ }; } -my $plot = $chart->plot( [ [sort keys %data], [map $data{$_}, sort keys %data] ] ) or die $chart->error; -$m->comp( 'SELF:Plot', plot => $plot, %ARGS ); +if (my $plot = eval { $chart->plot( \@data ) }) { + $m->comp( 'SELF:Plot', plot => $plot, %ARGS ); +} else { + my $error = join "\n", grep defined && length, $chart->error, $@; + $plot_error->(loc("Error plotting chart: [_1]", $error)); +} </%init> <%METHOD Plot> diff --git a/rt/share/html/Search/Chart.html b/rt/share/html/Search/Chart.html index 2cde5135a..e08616aac 100644 --- a/rt/share/html/Search/Chart.html +++ b/rt/share/html/Search/Chart.html @@ -45,36 +45,23 @@ %# those contributions and any derivatives thereof. %# %# END BPS TAGGED BLOCK }}} -<%args> -$PrimaryGroupBy => 'Queue' -$ChartStyle => 'bar' -$Description => undef -</%args> <%init> +my $default_value = { + Query => 'id > 0', + GroupBy => ['Status'], + ChartStyle => ['bar+table+sql'], + ChartFunction => ['COUNT'], +}; + $m->callback( ARGSRef => \%ARGS, CallbackName => 'Initial' ); -$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 @search_fields = qw(Query GroupBy ChartStyle ChartFunction Width Height); my $saved_search = $m->comp( '/Widgets/SavedSearch:new', SearchType => 'Chart', - SearchFields => [qw(Query PrimaryGroupBy ChartStyle)] ); + SearchFields => [@search_fields], +); my @actions = $m->comp( '/Widgets/SavedSearch:process', args => \%ARGS, self => $saved_search ); @@ -113,7 +100,21 @@ my %query; } +foreach (@search_fields) { + if ( ref $default_value->{$_} ) { + $query{$_} = ref $ARGS{$_} ? $ARGS{$_} : [ $ARGS{$_} ]; + $query{$_} = $default_value->{$_} + unless defined $query{$_} && defined $query{$_}[0]; + } + else { + $query{$_} = ref $ARGS{$_} ? $ARGS{$_} : $ARGS{$_}; + $query{$_} = $default_value->{$_} + unless defined $query{$_}; + } +} + $m->callback( ARGSRef => \%ARGS, QueryArgsRef => \%query ); + </%init> <& /Elements/Header, Title => $title &> <& /Elements/Tabs, QueryArgs => \%query &> @@ -127,15 +128,94 @@ $m->callback( ARGSRef => \%ARGS, QueryArgsRef => \%query ); <div class="chart-meta"> <div class="chart-type"> -<&| /Widgets/TitleBox, title => loc('Chart Properties')&> -<form method="get" action="<%RT->Config->Get('WebPath')%>/Search/Chart.html"> -<input type="hidden" class="hidden" name="Query" value="<% $ARGS{Query} %>" /> + +<form method="get" action="<% RT->Config->Get('WebPath') %>/Search/Chart.html"> +<input type="hidden" class="hidden" name="Query" value="<% $query{Query} %>" /> <input type="hidden" class="hidden" name="SavedChartSearchId" value="<% $saved_search->{SearchId} || 'new' %>" /> -<&|/l_unsafe, $m->scomp('Elements/SelectChartType', Name => 'ChartStyle', Default => $ChartStyle), $m->scomp('Elements/SelectGroupBy', Name => 'PrimaryGroupBy', Query => $ARGS{Query}, Default => $PrimaryGroupBy) -&>[_1] chart by [_2]</&><input type="submit" class="button" value="<%loc('Update Chart')%>" /> -</form> +<&| /Widgets/TitleBox, title => loc('Group by'), class => "chart-group-by" &> +<fieldset><legend><% loc('Group tickets by') %></legend> +<& Elements/SelectGroupBy, + Name => 'GroupBy', + Query => $query{Query}, + Default => $query{'GroupBy'}[0], + &> +</fieldset> +<fieldset><legend><% loc('and then') %></legend> +<& Elements/SelectGroupBy, + Name => 'GroupBy', + Query => $query{Query}, + Default => $query{'GroupBy'}[1] // q{}, + ShowEmpty => 1, + &> +</fieldset> +<fieldset><legend><% loc('and then') %></legend> +<& Elements/SelectGroupBy, + Name => 'GroupBy', + Query => $query{Query}, + Default => $query{'GroupBy'}[2] // q{}, + ShowEmpty => 1, + &> +</fieldset> +</&> + +<&| /Widgets/TitleBox, title => loc("Calculate"), class => "chart-calculate" &> + +<fieldset><legend><% loc('Calculate values of') %></legend> +<& Elements/SelectChartFunction, Default => $query{'ChartFunction'}[0] &> +</fieldset> +<fieldset><legend><% loc('and then') %></legend> +<& Elements/SelectChartFunction, Default => $query{'ChartFunction'}[1] // q{}, ShowEmpty => 1 &> +</fieldset> +<fieldset><legend><% loc('and then') %></legend> +<& Elements/SelectChartFunction, Default => $query{'ChartFunction'}[2] // q{}, ShowEmpty => 1 &> +</fieldset> + </&> + +<&| /Widgets/TitleBox, title => loc('Picture'), class => "chart-picture" &> +<input name="ChartStyle" type="hidden" value="<% $query{ChartStyle}[0] %>" /> +<label><% loc('Style') %>: <& Elements/SelectChartType, Default => $query{ChartStyle}[0] =~ /^(pie|bar|table)\b/ ? $1 : undef &></label> +<span class="width"> +<label><% loc("Width") %>: <input type="text" name="Width" value="<% $query{'Width'} || q{} %>"> <% loc("px") %></label> +</span> +<span class="height"> + × + <label><% loc("Height") %>: <input type="text" name="Height" value="<% $query{'Height'} || q{} %>"> <% loc("px") %></label> +</span> +<div class="include-table"> + <input type="checkbox" name="ChartStyleIncludeTable" <% $query{ChartStyle}[0] =~ /\btable\b/ ? 'checked="checked"' : '' |n %>> <% loc('Include data table') %> +</div> +<div class="include-sql"> + <input type="checkbox" name="ChartStyleIncludeSQL" <% $query{ChartStyle}[0] =~ /\bsql\b/ ? 'checked="checked"' : '' |n %>> <% loc('Include TicketSQL query') %> +</div> +</&> +<script type="text/javascript"> +var updateChartStyle = function() { + var val = jQuery(".chart-picture [name=ChartType]").val(); + if ( val != 'table' && jQuery(".chart-picture [name=ChartStyleIncludeTable]").is(':checked') ) { + val += '+table'; + } + if ( jQuery(".chart-picture [name=ChartStyleIncludeSQL]").is(':checked') ) { + val += '+sql'; + } + jQuery(".chart-picture [name=ChartStyle]").val(val); +}; +jQuery(".chart-picture [name=ChartType]").change(function(){ + var t = jQuery(this); + t.closest("form").find("[name=Height]").closest(".height").toggle( t.val() == 'bar' ); + t.closest("form").find("[name=Width]").closest(".width").toggle( t.val() !== 'table' ); + t.closest("form .chart-picture").find("div.include-table").toggle( t.val() !== 'table' ); + updateChartStyle(); +}).change(); + +jQuery(".chart-picture [name=ChartStyleIncludeTable]").change( updateChartStyle ); +jQuery(".chart-picture [name=ChartStyleIncludeSQL]").change( updateChartStyle ); +</script> + +<& /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/rt/share/html/Search/Elements/BuildFormatString b/rt/share/html/Search/Elements/BuildFormatString index 9a3ba1e75..10ac1afbe 100644 --- a/rt/share/html/Search/Elements/BuildFormatString +++ b/rt/share/html/Search/Elements/BuildFormatString @@ -115,10 +115,12 @@ $m->callback( CallbackOnce => 1, CallbackName => 'SetFieldsOnce', Fields => \@fi my $CustomFields = RT::CustomFields->new( $session{'CurrentUser'}); foreach my $id (keys %queues) { - # Gotta load up the $queue object, since queues get stored by name now. my $id + # Gotta load up the $queue object, since queues get stored by name now. my $queue = RT::Queue->new($session{'CurrentUser'}); $queue->Load($id); - $CustomFields->LimitToQueue($queue->Id) if $queue->Id; + next unless $queue->Id; + $CustomFields->LimitToQueue($queue->Id); + $CustomFields->SetContextObject( $queue ) if keys %queues == 1; } $CustomFields->LimitToGlobal; @@ -140,8 +142,7 @@ foreach my $field (@format) { if ( $RemoveCol ) { # we do this regex match to avoid a non-numeric warning - my ($index) = $CurrentDisplayColumns =~ /^(\d+)/; - my $column = $seen[$index]; + my ($index) = ($CurrentDisplayColumns // '') =~ /^(\d+)/; if ( defined($index) ) { delete $seen[$index]; my @temp = @seen; @@ -206,7 +207,7 @@ elsif ( $AddCol ) { } } elsif ( $ColUp ) { - my $index = $CurrentDisplayColumns; + my ($index) = ($CurrentDisplayColumns // '') =~ /^(\d+)/; if ( defined $index && ( $index - 1 ) >= 0 ) { my $column = $seen[$index]; $seen[$index] = $seen[ $index - 1 ]; @@ -215,7 +216,7 @@ elsif ( $ColUp ) { } } elsif ( $ColDown ) { - my $index = $CurrentDisplayColumns; + my ($index) = ($CurrentDisplayColumns // '') =~ /^(\d+)/; if ( defined $index && ( $index + 1 ) < scalar @seen ) { my $column = $seen[$index]; $seen[$index] = $seen[ $index + 1 ]; diff --git a/rt/share/html/Search/Elements/Chart b/rt/share/html/Search/Elements/Chart index 38c15f6ef..6285fac2b 100644 --- a/rt/share/html/Search/Elements/Chart +++ b/rt/share/html/Search/Elements/Chart @@ -47,107 +47,44 @@ %# END BPS TAGGED BLOCK }}} <%args> $Query => "id > 0" -$PrimaryGroupBy => 'Queue' -$ChartStyle => 'bar' +@GroupBy => () +$ChartStyle => 'bar+table+sql' +@ChartFunction => 'COUNT' </%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, -); +my $report = RT::Report::Tickets->new( $session{'CurrentUser'} ); -my %class = ( - Queue => 'RT::Queue', - Owner => 'RT::User', - Creator => 'RT::User', - LastUpdatedBy => 'RT::User', +my %columns = $report->SetupGroupings( + Query => $Query, + GroupBy => \@GroupBy, + Function => \@ChartFunction, ); -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 @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); +$report->SortEntries; -my ($i,$total); +my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy ); </%init> <div class="chart-wrapper"> -<span class="chart image"> +% if ( ($ChartStyle || '') =~ /\b(pie|bar)\b/ ) { +<span class="chart image <% $1 %>"> % 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"> -<tr> -<th class="collection-as-table"><% loc($tix->Label($PrimaryGroupBy)) %> -</th> -<th class="collection-as-table"><&|/l&>Tickets</&> -</th> -</tr> -<%perl> - while (my $key = shift @sorted_keys) { - $i++; - my $value = shift @sorted_values; - $total += $value; -</%perl> -<tr class="<% $i%2 ? 'evenline' : 'oddline' %>"> -<%perl> -# TODO sadly we don't have "creator.city is null" or alike support yet -# so no link if the key is undef for now - if ( $PrimaryGroupBy !~ /(Hourly|Daily|Monthly|Annually)$/ - && $key ne loc('(no value)') ) { - my $group = $PrimaryGroupBy; $group =~ s! !.!; - my %orig_keys = reverse %loc_keys; - my $QueryString = $m->comp('/Elements/QueryString', - Query => "$Query and $group = '$orig_keys{$key}'", - Format => $ARGS{Format}, - Rows => $ARGS{Rows}, - OrderBy => $ARGS{OrderBy}, - Order => $ARGS{Order}, - ); -</%perl> -<td class="label collection-as-table"> -<a href=<% RT->Config->Get('WebPath') %>/Search/Results.html?<%$QueryString%>><%$key%></a> -</td> -<td class="value collection-as-table"> -<a href=<% RT->Config->Get('WebPath') %>/Search/Results.html?<%$QueryString%>><%$value%></a> -</td> -% } else { -<td class="label collection-as-table"><% $key %></td> -<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> -</tr> +% if ( ($ChartStyle || '') =~ /\btable\b/ ) { +<& ChartTable, %ARGS, Table => { $report->FormatTable( %columns ) } &> +% } -</table> +% if ( ($ChartStyle || '') =~ /\bsql\b/ ) { <div class="query"><span class="label"><% loc('Query') %>:</span><span class="value"><% $Query %></span></div> +% } </div> diff --git a/rt/share/html/Search/Elements/ChartTable b/rt/share/html/Search/Elements/ChartTable new file mode 100644 index 000000000..045653ae8 --- /dev/null +++ b/rt/share/html/Search/Elements/ChartTable @@ -0,0 +1,119 @@ +%# BEGIN BPS TAGGED BLOCK {{{ +%# +%# COPYRIGHT: +%# +%# This software is Copyright (c) 1996-2015 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 }}} +<%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'; + push @class, $cell->{'even'} ? 'evenline' : 'oddline' + if defined $cell->{'even'}; + $m->out(' class="'. $eh->( join ' ', @class ) .'"'); + + foreach my $dir ( grep $cell->{$_}, qw(rowspan colspan) ) { + my $value = int $cell->{ $dir }; + $m->out(qq{ $dir="$value"}); + } + $m->out(' style="background-color: #'. $m->interp->apply_escapes($cell->{color}) .'"') + if $cell->{color}; + + $m->out('>'); + if ( defined $cell->{'value'} ) { + if ( my $q = $cell->{'query'} ) { + $m->out( + '<a href="'. $eh->(RT->Config->Get('WebPath')) .'/Search/Results.html' + .'?Query='. $eu->(join ' AND ', map "($_)", grep defined && length, $Query, $q) + . $eh->('&') . $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> diff --git a/rt/share/html/Search/Elements/ConditionRow b/rt/share/html/Search/Elements/ConditionRow index edf738107..80ecd97b4 100644 --- a/rt/share/html/Search/Elements/ConditionRow +++ b/rt/share/html/Search/Elements/ConditionRow @@ -74,9 +74,11 @@ $handle_block = sub { return $m->scomp( $box->{'Path'}, %{ $box->{'Arguments'} }, Name => $name ); } if ( $box->{'Type'} eq 'text' ) { - my $default = $box->{'Default'} || ''; - my $size = $box->{'Size'}? qq{size="$box->{'Size'}"} : ''; - return qq{<input id="$name" name="$name" value="$default" $size />}; + $box->{id} ||= $box->{name} ||= $name; + $box->{value} ||= delete($box->{Default}) || ''; + return "<input ".join(" ", map{$m->interp->apply_escapes(lc($_),'h') + .q{="}.$m->interp->apply_escapes($box->{$_},'h').q{"}} + sort keys %$box)." />"; } if ( $box->{'Type'} eq 'select' ) { my $res = ''; diff --git a/rt/share/html/Search/Elements/EditFormat b/rt/share/html/Search/Elements/EditFormat index a78fa0574..fffec5c9d 100644 --- a/rt/share/html/Search/Elements/EditFormat +++ b/rt/share/html/Search/Elements/EditFormat @@ -59,7 +59,8 @@ <td valign="top"><select size="6" name="SelectDisplayColumns" multiple="multiple"> % my %seen; % foreach my $field ( grep !$seen{lc $_}++, @$AvailableColumns) { -<option value="<% $field %>"><% loc($field) %></option> +<option value="<% $field %>" <% $selected{$field} ? 'selected="selected"' : '' |n%>>\ +<% $field =~ /^(?:CustomField|CF)\./ ? $field : loc($field) %></option> % } </select></td> <td> @@ -105,8 +106,10 @@ <td valign="top"> <select size="4" name="CurrentDisplayColumns"> % my $i=0; +% my $current = $ARGS{CurrentDisplayColumns} || ''; $current =~ s/^\d+>//; % foreach my $field ( @$CurrentFormat ) { -<option value="<% $i++ %>><% $field->{Column} %>"><% loc( $field->{Column} ) %></option> +<option value="<% $i++ %>><% $field->{Column} %>" <% $field->{Column} eq $current ? 'selected="selected"' : '' |n%>>\ +<% $field->{Column} =~ /^(?:CustomField|CF)\./ ? $field->{Column} : loc( $field->{Column} ) %></option> % } </select> <br /> @@ -120,6 +123,12 @@ </tr> </table> +<%init> +my $selected = $ARGS{SelectDisplayColumns}; +$selected = [ $selected ] unless ref $selected; +my %selected; +$selected{$_}++ for grep {defined} @{ $selected }; +</%init> <%ARGS> $CurrentFormat => undef $AvailableColumns => undef diff --git a/rt/share/html/Search/Elements/EditSearches b/rt/share/html/Search/Elements/EditSearches index f5be486b8..0a55e0dff 100644 --- a/rt/share/html/Search/Elements/EditSearches +++ b/rt/share/html/Search/Elements/EditSearches @@ -146,7 +146,7 @@ $SavedSearch => {} $SavedSearch->{'Id'} = ( $ARGS{Type} && $ARGS{Type} eq 'Chart' ? $ARGS{'SavedChartSearchId'} : $ARGS{'SavedSearchId'} ) || 'new'; -$SavedSearch->{'Description'} = $ARGS{'SavedSearchDescription'} || undef; +$SavedSearch->{'Description'} = $ARGS{'SavedSearchDescription'} || ''; $SavedSearch->{'Privacy'} = $ARGS{'SavedSearchOwner'} || undef; my @results; @@ -158,7 +158,8 @@ if ( $ARGS{'SavedSearchRevert'} ) { if ( $ARGS{'SavedSearchLoad'} ) { my ($container, $id ) = _parse_saved_search ($ARGS{'SavedSearchLoad'}); if ( $container ) { - my $search = $container->Attributes->WithId( $id ); + my $search = RT::Attribute->new( $session{'CurrentUser'} ); + $search->Load( $id ); $SavedSearch->{'Id'} = $ARGS{'SavedSearchLoad'}; $SavedSearch->{'Object'} = $search; $SavedSearch->{'Description'} = $search->Description; @@ -194,7 +195,8 @@ elsif ( $ARGS{'SavedSearchDelete'} ) { } elsif ( $ARGS{'SavedSearchCopy'} ) { my ($container, $id ) = _parse_saved_search( $ARGS{'SavedSearchId'} ); - $SavedSearch->{'Object'} = $container->Attributes->WithId( $id ); + $SavedSearch->{'Object'} = RT::Attribute->new( $session{'CurrentUser'} ); + $SavedSearch->{'Object'}->Load( $id ); if ( $ARGS{'SavedSearchDescription'} && $ARGS{'SavedSearchDescription'} ne $SavedSearch->{'Object'}->Description ) { $SavedSearch->{'Description'} = $ARGS{'SavedSearchDescription'}; } else { @@ -208,7 +210,8 @@ if ( $SavedSearch->{'Id'} && $SavedSearch->{'Id'} ne 'new' && !$SavedSearch->{'Object'} ) { my ($container, $id ) = _parse_saved_search( $ARGS{'SavedSearchId'} ); - $SavedSearch->{'Object'} = $container->Attributes->WithId( $id ); + $SavedSearch->{'Object'} = RT::Attribute->new( $session{'CurrentUser'} ); + $SavedSearch->{'Object'}->Load( $id ); $SavedSearch->{'Description'} ||= $SavedSearch->{'Object'}->Description; } @@ -290,7 +293,7 @@ if ( $obj && $obj->id ) { } push @results, loc('Updated saved search "[_1]"', $desc); } -elsif ( $id eq 'new' ) { +elsif ( $id eq 'new' and defined $desc and length $desc ) { my $saved_search = RT::SavedSearch->new( $session{'CurrentUser'} ); my ($status, $msg) = $saved_search->Save( Privacy => $privacy, @@ -300,8 +303,8 @@ elsif ( $id eq 'new' ) { ); if ( $status ) { - $SavedSearch->{'Object'} = - $session{'CurrentUser'}->UserObj->Attributes->WithId( $saved_search->Id ); + $SavedSearch->{'Object'} = RT::Attribute->new( $session{'CurrentUser'} ); + $SavedSearch->{'Object'}->Load( $saved_search->Id ); # Build new SearchId $SavedSearch->{'Id'} = ref( $session{'CurrentUser'}->UserObj ) . '-' @@ -313,6 +316,9 @@ elsif ( $id eq 'new' ) { push @results, loc("Can't find a saved search to work with").': '.loc($msg); } } +elsif ( $id eq 'new' ) { + push @results, loc("Can't save a search without a Description"); +} else { push @results, loc("Can't save this search"); } diff --git a/rt/share/html/Search/Elements/EditSort b/rt/share/html/Search/Elements/EditSort index de5d2d818..43ae7292d 100644 --- a/rt/share/html/Search/Elements/EditSort +++ b/rt/share/html/Search/Elements/EditSort @@ -68,7 +68,7 @@ % if (defined $OrderBy[$o] and $fieldval eq $OrderBy[$o]) { selected="selected" % } -><% loc($field) %></option> +><% $field =~ /^(?:CustomField|CF)\./ ? $field : loc($field) %></option> % } </select> <select name="Order"> diff --git a/rt/share/html/Search/Elements/PickBasics b/rt/share/html/Search/Elements/PickBasics index 3aae96589..29eea7e3f 100644 --- a/rt/share/html/Search/Elements/PickBasics +++ b/rt/share/html/Search/Elements/PickBasics @@ -70,10 +70,10 @@ my @lines = ( Type => 'component', Path => '/Elements/SelectBoolean', Arguments => { - True => loc("matches"), - False => loc("doesn't match"), - TrueVal => 'LIKE', - FalseVal => 'NOT LIKE', + True => loc("matches"), + False => loc("doesn't match"), + TrueVal => 'LIKE', + FalseVal => 'NOT LIKE', }, }, Value => { Type => 'text', Size => 20 }, @@ -89,7 +89,7 @@ my @lines = ( Value => { Type => 'component', Path => '/Elements/SelectQueue', - Arguments => { NamedValues => 1, CheckQueueRight => 'ShowTicket' }, + Arguments => { NamedValues => 1, }, }, }, { @@ -102,7 +102,7 @@ my @lines = ( }, Value => { Type => 'component', - Path => '/Elements/SelectStatus', + Path => '/Ticket/Elements/SelectStatus', Arguments => { SkipDeleted => 1, Queues => \%queues }, }, }, @@ -114,6 +114,7 @@ my @lines = ( Owner => loc('Owner'), Creator => loc('Creator'), LastUpdatedBy => loc('Last updated by'), + UpdatedBy => loc('Updated by'), ], }, Op => { @@ -141,6 +142,19 @@ my @lines = ( Value => { Type => 'text', Size => 20 } }, { + Name => 'WatcherGroup', + Field => { + Type => 'component', + Path => 'SelectPersonType', + Arguments => { Default => 'Owner', Suffix => 'Group' }, + }, + Op => { + Type => 'select', + Options => [ '=' => loc('is') ], + }, + Value => { Type => 'text', Size => 20, "data-autocomplete" => "Groups" } + }, + { Name => 'Date', Field => { Type => 'component', diff --git a/rt/share/html/Search/Elements/PickCFs b/rt/share/html/Search/Elements/PickCFs index cf8c92a27..e8d9c71e2 100644 --- a/rt/share/html/Search/Elements/PickCFs +++ b/rt/share/html/Search/Elements/PickCFs @@ -58,7 +58,7 @@ $m->callback( my @lines; while ( my $CustomField = $CustomFields->Next ) { my %line; - $line{'Name'} = "'$TicketSQLField.{" . $CustomField->Name . "}'"; + $line{'Name'} = "$TicketSQLField.{" . $CustomField->Name . "}"; $line{'Field'} = $CustomField->Name; # Op @@ -88,20 +88,11 @@ while ( my $CustomField = $CustomFields->Next ) { } # Value - if ($CustomField->Type =~ /^Date(Time)?$/) { - my $is_datetime = $1 ? 1 : 0; - $line{'Value'} = { - Type => 'component', - Path => '/Elements/SelectDate', - Arguments => { $is_datetime ? (ShowTime => 1) : (ShowTime => 0), }, - }; - } else { - $line{'Value'} = { - Type => 'component', - Path => '/Elements/SelectCustomFieldValue', - Arguments => { CustomField => $CustomField }, - }; - } + $line{'Value'} = { + Type => 'component', + Path => '/Elements/SelectCustomFieldValue', + Arguments => { CustomField => $CustomField }, + }; push @lines, \%line; } diff --git a/rt/share/html/Search/Elements/PickCriteria b/rt/share/html/Search/Elements/PickCriteria index b2e84cae9..e55e27085 100644 --- a/rt/share/html/Search/Elements/PickCriteria +++ b/rt/share/html/Search/Elements/PickCriteria @@ -54,6 +54,8 @@ <& PickBasics &> <& PickCustomerFields &> <& PickTicketCFs, queues => \%queues &> +<& PickObjectCFs, Class => 'Transaction', queues => \%queues &> +<& PickObjectCFs, Class => 'Queue', queues => \%queues &> % $m->callback( %ARGS, CallbackName => "AfterCFs" ); <tr class="separator"><td colspan="3"><hr /></td></tr> diff --git a/rt/share/html/Search/Elements/PickObjectCFs b/rt/share/html/Search/Elements/PickObjectCFs new file mode 100644 index 000000000..1a67338c5 --- /dev/null +++ b/rt/share/html/Search/Elements/PickObjectCFs @@ -0,0 +1,76 @@ +%# BEGIN BPS TAGGED BLOCK {{{ +%# +%# COPYRIGHT: +%# +%# This software is Copyright (c) 1996-2015 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 }}} +<%ARGS> +$Class +%queues => () +</%ARGS> +<%init> +my $CustomFields = RT::CustomFields->new( $session{'CurrentUser'} ); +$CustomFields->ApplySortOrder; +$CustomFields->LimitToLookupType( "RT::$Class"->CustomFieldLookupType ); +$CustomFields->LimitToObjectId(0); + +foreach my $name (keys %queues) { + my $queue = RT::Queue->new($session{'CurrentUser'}); + $queue->Load($name); + next unless $queue->Id; + $CustomFields->LimitToObjectId($queue->Id); + $CustomFields->SetContextObject( $queue ) if keys %queues == 1; +} + +my $has_cf = $CustomFields->First ? 1 : 0; +$CustomFields->GotoFirstItem; +</%init> +% if ($has_cf) { +<tr class="separator"> + <td colspan="3"> + <hr><em><% loc("[_1] CFs", loc($Class)) %></em> + </td> +</tr> +% } +<& PickCFs, %ARGS, TicketSQLField => "${Class}CF", CustomFields => $CustomFields &> diff --git a/rt/share/html/Search/Elements/PickTicketCFs b/rt/share/html/Search/Elements/PickTicketCFs index ac52049c3..ae3a4a286 100644 --- a/rt/share/html/Search/Elements/PickTicketCFs +++ b/rt/share/html/Search/Elements/PickTicketCFs @@ -54,8 +54,11 @@ foreach my $id (keys %queues) { # Gotta load up the $queue object, since queues get stored by name now. my $queue = RT::Queue->new($session{'CurrentUser'}); $queue->Load($id); - $CustomFields->LimitToQueue($queue->Id) if $queue->Id; + next unless $queue->Id; + $CustomFields->LimitToQueue($queue->Id); + $CustomFields->SetContextObject( $queue ) if keys %queues == 1; } $CustomFields->LimitToGlobal; +$CustomFields->OrderBy( FIELD => 'Name', ORDER => 'ASC' ); </%init> <& PickCFs, %ARGS, TicketSQLField => 'CF', CustomFields => $CustomFields &> diff --git a/rt/share/html/Search/Elements/ResultsRSSView b/rt/share/html/Search/Elements/ResultsRSSView index 0bce7ec45..f392369a8 100644 --- a/rt/share/html/Search/Elements/ResultsRSSView +++ b/rt/share/html/Search/Elements/ResultsRSSView @@ -46,41 +46,7 @@ %# %# END BPS TAGGED BLOCK }}} <%INIT> -my $current_user = $session{CurrentUser}; - -if ( $m->request_comp->path =~ RT->Config->Get('WebNoAuthRegex') ) { - my $path = $m->dhandler_arg; - - my $notfound = sub { - my $mesg = shift; - $r->headers_out->{'Status'} = '404 Not Found'; - $RT::Logger->info("Error encountered in rss generation: $mesg"); - $m->clear_and_abort; - }; - - $notfound->("Invalid path: $path") unless $path =~ m!^([^/]+)/([^/]+)/?!; - - my ( $name, $auth ) = ( $1, $2 ); - - # Unescape parts - $name =~ s/\%([0-9a-z]{2})/chr(hex($1))/gei; - - # Decode from bytes to characters - $name = Encode::decode( "UTF-8", $name ); - - my $user = RT::User->new(RT->SystemUser); - $user->Load($name); - $notfound->("Invalid user: $user") unless $user->id; - - $notfound->("Invalid authstring") - unless $user->ValidateAuthString( $auth, - $ARGS{Query} . $ARGS{Order} . $ARGS{OrderBy} ); - - $current_user = RT::CurrentUser->new; - $current_user->Load($user); -} - -my $Tickets = RT::Tickets->new($current_user); +my $Tickets = RT::Tickets->new($session{'CurrentUser'}); $Tickets->FromSQL($ARGS{'Query'}); if ($OrderBy =~ /\|/) { # Multiple Sorts @@ -92,48 +58,58 @@ if ($OrderBy =~ /\|/) { } else { $Tickets->OrderBy(FIELD => $OrderBy, ORDER => $Order); } -$r->content_type('application/rss+xml'); - - +$r->content_type('application/rss+xml; charset=utf-8'); - # create an RSS 1.0 file (http://purl.org/rss/1.0/) - use XML::RSS; - my $rss = XML::RSS->new(version => '1.0'); - $rss->channel( - title => RT->Config->Get('rtname').": Search " . $ARGS{'Query'}, - link => RT->Config->Get('WebURL'), - description => "", - dc => { - }, - generator => "RT v" . $RT::VERSION, - syn => { - updatePeriod => "hourly", - updateFrequency => "1", - updateBase => "1901-01-01T00:00+00:00", - }, - ); +use XML::RSS; +my $rss = XML::RSS->new(version => '1.0'); +my $url; +if ( RT->Config->Get('CanonicalizeURLsInFeeds') ) { + $url = RT->Config->Get('WebURL'); +} else { + $url = RT::Interface::Web::GetWebURLFromRequest(); +} - while ( my $Ticket = $Tickets->Next()) { - my $creator_str = $m->scomp('/Elements/ShowUser', User => $Ticket->CreatorObj); - $creator_str =~ s/[\r\n]//g; - - # Get the plain-text content; it is interpreted as HTML by RSS - # readers, so it must be escaped (and is escaped _again_ when - # inserted into the XML). - my $content = $Ticket->Transactions->First->Content; - $content = $m->interp->apply_escapes( $content, 'h'); - $rss->add_item( - title => $Ticket->Subject || loc('No Subject'), - link => RT->Config->Get('WebURL')."Ticket/Display.html?id=".$Ticket->id, - description => $content, - dc => { creator => $creator_str, - date => $Ticket->CreatedObj->RFC2822, - }, - guid => $Ticket->Queue . '_' . $Ticket->id, - ); - } +my $base_date = RT::Date->new( RT->SystemUser ); +$base_date->SetToNow; +$base_date->SetToMidnight; + +$rss->channel( + title => RT->Config->Get('rtname').": Search " . $ARGS{'Query'}, + link => $url, + description => "", + dc => { }, + generator => "RT v" . $RT::VERSION, + syn => { + updatePeriod => "hourly", + updateFrequency => "1", + updateBase => $base_date->W3CDTF, + }, +); + + +while ( my $Ticket = $Tickets->Next()) { + my $creator_str = $Ticket->CreatorObj->Format; + $creator_str =~ s/[\r\n]//g; + + # Get the plain-text content; it is interpreted as HTML by RSS + # readers, so it must be escaped (and is escaped _again_ when + # inserted into the XML). + my $content = $Ticket->Transactions->First->Content; + $content = $m->interp->apply_escapes( $content, 'h'); + + $rss->add_item( + title => $Ticket->Subject || loc('No Subject'), + link => $url . "Ticket/Display.html?id=".$Ticket->id, + description => $content, + dc => { + creator => $creator_str, + date => $Ticket->CreatedObj->W3CDTF, + }, + guid => $Ticket->Queue . '_' . $Ticket->id, + ); +} $m->out($rss->as_string); $m->abort(); diff --git a/rt/share/html/Search/Elements/SearchPrivacy b/rt/share/html/Search/Elements/SearchPrivacy index dd7ef3b2f..1e43dfd39 100644 --- a/rt/share/html/Search/Elements/SearchPrivacy +++ b/rt/share/html/Search/Elements/SearchPrivacy @@ -53,9 +53,9 @@ my $label; if (ref($Object) eq 'RT::User') { $label = $Object->id == $session{'CurrentUser'}->Id ? loc("My saved searches") - : loc("[_1]'s saved searches", $m->scomp('/Elements/ShowUser', User => $Object)); + : loc("[_1]'s saved searches", $Object->Format); } else { - $label = loc("[_1]'s saved searches", $m->interp->apply_escapes($Object->Name, 'h')); + $label = loc("[_1]'s saved searches", $Object->Name); } </%init> -<% $label |n %>\ +<% $label %>\ diff --git a/rt/share/html/Search/Elements/SearchesForObject b/rt/share/html/Search/Elements/SearchesForObject index 397a0d95c..f58752d00 100644 --- a/rt/share/html/Search/Elements/SearchesForObject +++ b/rt/share/html/Search/Elements/SearchesForObject @@ -55,10 +55,10 @@ my @result; while (my $search = $Object->Attributes->Next) { my $desc; if ($search->Name eq 'SavedSearch') { - push @result, [$search->Description, $search->Description, $search]; + push @result, [$search->Description, $search->Description, $search]; } elsif ($search->Name =~ m/^Search - (.*)/) { - push @result, [$1, loc($1), $search]; + push @result, [$1, loc($1), $search]; } } return @result; diff --git a/rt/share/html/Search/Elements/SelectAndOr b/rt/share/html/Search/Elements/SelectAndOr index cbea34fc2..d506ef785 100644 --- a/rt/share/html/Search/Elements/SelectAndOr +++ b/rt/share/html/Search/Elements/SelectAndOr @@ -45,8 +45,8 @@ %# those contributions and any derivatives thereof. %# %# END BPS TAGGED BLOCK }}} -<input type="radio" class="radio" name="<%$Name%>" checked="checked" value="AND" /><&|/l&>AND</&> -<input type="radio" class="radio" name="<%$Name%>" value="OR" /><&|/l&>OR</&> +<label><input type="radio" class="radio" name="<%$Name%>" checked="checked" value="AND" /><&|/l&>AND</&></label> +<label><input type="radio" class="radio" name="<%$Name%>" value="OR" /><&|/l&>OR</&></label> <%ARGS> $Name => "Operator" diff --git a/rt/share/html/Search/Elements/SelectChartFunction b/rt/share/html/Search/Elements/SelectChartFunction new file mode 100644 index 000000000..dad6b781a --- /dev/null +++ b/rt/share/html/Search/Elements/SelectChartFunction @@ -0,0 +1,79 @@ +%# BEGIN BPS TAGGED BLOCK {{{ +%# +%# COPYRIGHT: +%# +%# This software is Copyright (c) 1996-2015 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 }}} +<select name="<% $Name %>" class="cascade-by-optgroup"> +% if ( $ShowEmpty ) { +<option value=""> </option> +% } +<%perl> +my $in_optgroup = ""; +while ( my ($value, $display) = splice @functions, 0, 2 ) { + my $optgroup = $value =~ /\((.+)\)$/ ? $1 : $display; + if ($in_optgroup ne $optgroup) { + $m->out("</optgroup>\n") if $in_optgroup; + + my $name = $m->interp->apply_escapes(loc($optgroup), 'h'); + $m->out(qq[<optgroup label="$name">\n]); + + $in_optgroup = $optgroup; + } +</%perl> +<option value="<% $value %>"<% $value eq $Default ? qq[ selected="selected"] : '' |n %>><% loc( $display ) %></option> +% } +% if ($in_optgroup) { + </optgroup> +% } +</select> +<%ARGS> +$Name => 'ChartFunction' +$Default => 'COUNT' +$ShowEmpty => 0 +</%ARGS> +<%INIT> +my @functions = RT::Report::Tickets->Statistics; +$Default = '' unless defined $Default; +</%INIT> diff --git a/rt/share/html/Search/Elements/SelectChartType b/rt/share/html/Search/Elements/SelectChartType index 266885f0d..c4d95d00c 100644 --- a/rt/share/html/Search/Elements/SelectChartType +++ b/rt/share/html/Search/Elements/SelectChartType @@ -50,9 +50,10 @@ $Name => 'ChartType' $Default => 'bar' </%args> <select id="<%$Name%>" name="<%$Name%>"> -% foreach my $option (qw(bar pie)) { +% foreach my $option ('bar', 'pie', 'table') { % # 'bar' # loc % # 'pie' # loc +% # 'table' # loc <option value="<%$option%>"<% $option eq $Default ? qq[ selected="selected"] : '' |n %>><%loc($option)%></option> % } </select> diff --git a/rt/share/html/Search/Elements/SelectGroup b/rt/share/html/Search/Elements/SelectGroup index 907c88e27..27d6a762e 100644 --- a/rt/share/html/Search/Elements/SelectGroup +++ b/rt/share/html/Search/Elements/SelectGroup @@ -56,7 +56,7 @@ <%INIT> my $groups = RT::Groups->new($session{'CurrentUser'}); -$groups->Limit(FIELD => 'Domain', OPERATOR => '=', VALUE => $Domain); +$groups->Limit(FIELD => 'Domain', OPERATOR => '=', VALUE => $Domain, CASESENSITIVE => 0); </%INIT> <%ARGS> diff --git a/rt/share/html/Search/Elements/SelectGroupBy b/rt/share/html/Search/Elements/SelectGroupBy index 8daab6daa..99f0f47eb 100644 --- a/rt/share/html/Search/Elements/SelectGroupBy +++ b/rt/share/html/Search/Elements/SelectGroupBy @@ -49,11 +49,29 @@ $Name => 'GroupBy' $Default => 'Status' $Query => '' +$ShowEmpty => 0 </%args> -<select id="<% $Name %>" name="<% $Name %>"> -% while (@options) { -% my ($text, $value) = (shift @options, shift @options); -<option value="<% $value %>" <% $value eq $Default ? 'selected="selected"' : '' |n%>><% $text %></option> +<select name="<% $Name %>" class="cascade-by-optgroup"> +% if ( $ShowEmpty ) { +<option value=""> </option> +% } +<%perl> +my $in_optgroup = ""; +while ( my ($label, $value) = splice @options, 0, 2 ) { + my ($optgroup, $text) = @$label; + if ($in_optgroup ne $optgroup) { + $m->out("</optgroup>\n") if $in_optgroup; + + my $name = $m->interp->apply_escapes(loc($optgroup), 'h'); + $m->out(qq[<optgroup label="$name">\n]); + + $in_optgroup = $optgroup; + } +</%perl> +<option value="<% $value %>" <% $value eq ($Default||'') ? 'selected="selected"' : '' |n %>><% loc($text) %></option> +% } +% if ($in_optgroup) { + </optgroup> % } </select> <%init> diff --git a/rt/share/html/Search/Elements/SelectLinks b/rt/share/html/Search/Elements/SelectLinks index 3759a58d1..1b8509461 100644 --- a/rt/share/html/Search/Elements/SelectLinks +++ b/rt/share/html/Search/Elements/SelectLinks @@ -47,7 +47,7 @@ %# END BPS TAGGED BLOCK }}} <select name="<%$Name%>"> % foreach (@fields) { -<option value="<%$_%>"><% loc($_) %></option> +<option value="<%$_->[0]%>"><% $_->[1] %></option> % } </select> <%ARGS> @@ -55,12 +55,13 @@ $Name => 'LinksField' </%ARGS> <%INIT> -my @fields = ('HasMember', - 'MemberOf', - 'DependsOn', - 'DependedOnBy', - 'RefersTo', - 'ReferredToBy', - 'LinkedTo', - ); +my @fields = ( + [ HasMember => loc("Child") ], + [ MemberOf => loc("Parent") ], + [ DependsOn => loc("Depends on") ], + [ DependedOnBy => loc("Depended on by") ], + [ RefersTo => loc("Refers to") ], + [ ReferredToBy => loc("Referred to by") ], + [ LinkedTo => loc("Links to") ], +); </%INIT> diff --git a/rt/share/html/Search/Elements/SelectPersonType b/rt/share/html/Search/Elements/SelectPersonType index 7ec875a8d..0fc541b07 100644 --- a/rt/share/html/Search/Elements/SelectPersonType +++ b/rt/share/html/Search/Elements/SelectPersonType @@ -51,7 +51,7 @@ % } % for my $option (@types) { % if ($Suffix) { -<option value="<% $option %><% $Suffix %>"<%$option eq $Default && qq[ selected="selected"] |n %> ><%loc($option)%></option> +<option value="<% $option %><% $Suffix %>"<%$option eq $Default && qq[ selected="selected"] |n %> ><% loc($option) %> <% loc('Group') %></option> % next; % } % foreach my $subtype (@subtypes) { @@ -66,7 +66,7 @@ if ($Scope =~ /queue/) { @types = qw(Cc AdminCc); } elsif ($Suffix eq 'Group') { - @types = qw(Requestor Cc AdminCc Watcher); + @types = qw(Owner Requestor Cc AdminCc Watcher); } else { @types = qw(Requestor Cc AdminCc Watcher Owner QueueCc QueueAdminCc QueueWatcher); diff --git a/rt/share/html/Search/Results.html b/rt/share/html/Search/Results.html index 6125c3642..b6b337946 100755 --- a/rt/share/html/Search/Results.html +++ b/rt/share/html/Search/Results.html @@ -50,11 +50,12 @@ LinkRel => \%link_rel &> <& /Elements/Tabs &> -% $m->callback( ARGSRef => \%ARGS, CallbackName => 'BeforeResults' ); +% my $DisplayFormat; +% $m->callback( ARGSRef => \%ARGS, Format => \$Format, DisplayFormat => \$DisplayFormat, CallbackName => 'BeforeResults' ); % unless ($ok) { % $msg =~ s{ at .*? line .*}{}s; -<&| /Widgets/TitleBox, title => loc("Error"), class => "error" &> +<&| /Widgets/TitleBox, title => loc("Error"), class => "error-titlebox" &> <&|/l_unsafe, "<i>".$m->interp->apply_escapes($msg, "h")."</i>" &>There was an error parsing your search query: [_1]. Your RT admin can find more information in the error logs.</&> </&> % } else { @@ -68,10 +69,13 @@ Rows => $Rows, Page => $Page, Format => $Format, + DisplayFormat => $DisplayFormat, # in case we set it in callbacks Class => 'RT::Tickets', - BaseURL => $BaseURL - - &> + BaseURL => $BaseURL, + SavedSearchId => $ARGS{'SavedSearchId'}, + SavedChartSearchId => $ARGS{'SavedChartSearchId'}, + PassArguments => [qw(Query Format Rows Page Order OrderBy SavedSearchId SavedChartSearchId)], +&> % } % $m->callback( ARGSRef => \%ARGS, CallbackName => 'AfterResults' ); @@ -94,8 +98,8 @@ my $prefs = $session{'CurrentUser'}->UserObj->Preferences("SearchDisplay") || {} # These variables are what define a search_hash; this is also # where we give sane defaults. $Format ||= $prefs->{'Format'} || RT->Config->Get('DefaultSearchResultFormat'); -$Order ||= $prefs->{'Order'} || 'ASC'; -$OrderBy ||= $prefs->{'OrderBy'} || 'id'; +$Order ||= $prefs->{'Order'} || RT->Config->Get('DefaultSearchResultOrder'); +$OrderBy ||= $prefs->{'OrderBy'} || RT->Config->Get('DefaultSearchResultOrderBy'); # Some forms pass in "RowsPerPage" rather than "Rows" # We call it RowsPerPage everywhere else. @@ -140,10 +144,10 @@ $session{'CurrentSearchHash'} = { }; -my ($title, $ticketcount) = (loc("Found tickets"), 0); +my ($title, $ticketcount) = (loc("Find tickets"), 0); if ( $session{'tickets'}->Query()) { $ticketcount = $session{tickets}->CountAll(); - $title = loc('Found [quant,_1,ticket]', $ticketcount); + $title = loc('Found [quant,_1,ticket,tickets]', $ticketcount); } my $QueryString = "?".$m->comp('/Elements/QueryString', @@ -156,7 +160,7 @@ my $QueryString = "?".$m->comp('/Elements/QueryString', my $ShortQueryString = "?".$m->comp('/Elements/QueryString', Query => $Query); if ($ARGS{'TicketsRefreshInterval'}) { - $session{'tickets_refresh_interval'} = $ARGS{'TicketsRefreshInterval'}; + $session{'tickets_refresh_interval'} = $ARGS{'TicketsRefreshInterval'}; } my $refresh = $session{'tickets_refresh_interval'} diff --git a/rt/share/html/Search/Results.tsv b/rt/share/html/Search/Results.tsv index b28ea4816..1e45a33cf 100644 --- a/rt/share/html/Search/Results.tsv +++ b/rt/share/html/Search/Results.tsv @@ -54,56 +54,8 @@ $PreserveNewLines => 0 </%ARGS> <%INIT> -$r->content_type('text/tab-separated-values'); $r->header_out('Content-Disposition' => 'attachment;filename="Results.tsv"'); -my $DisplayFormat = $m->comp('/Elements/ScrubHTML', Content => $Format); - -my @Format = $m->comp('/Elements/CollectionAsTable/ParseFormat', Format => $DisplayFormat); - -my @columns; - -my $should_loc = { map { $_ => 1 } qw(Status) }; - -my $col_entry = sub { - my $col = shift; - # in tsv output, "#" is often a comment character but we use it for "id" - delete $col->{title} - if $col->{title} and $col->{title} =~ /^\s*#\s*$/; - return { - header => loc($col->{title} || $col->{attribute}), - map => $m->comp( - "/Elements/ColumnMap", - Name => $col->{attribute}, - Attr => 'value' - ), - should_loc => $should_loc->{$col->{attribute}}, - } -}; - -if ($PreserveNewLines) { - my $col = []; - push @columns, $col; - for (@Format) { - if ($_->{title} eq 'NEWLINE') { - $col = []; - push @columns, $col; - } - else { - push @$col, $col_entry->($_); - } - } -} -else { - push @columns, [map { $_->{attribute} - ? $col_entry->($_) - : () } @Format]; -} - -for (@columns) { - $m->out(join("\t", map { $_->{header} } @$_)."\n"); -} - my $Tickets = RT::Tickets->new( $session{'CurrentUser'} ); $Tickets->FromSQL( $Query ); if ( $OrderBy =~ /\|/ ) { @@ -119,21 +71,5 @@ else { $Tickets->OrderBy( FIELD => $OrderBy, ORDER => $Order ); } -my $i = 0; -my $ii = 0; -while (my $row = $Tickets->Next) { - for my $col (@columns) { - $m->out(join("\t", map { - my $val = ProcessColumnMapValue($_->{map}, Arguments => [$row, $ii++], Escape => 0); - $val = loc($val) if $_->{should_loc}; - # remove tabs from all field values, they screw up the tsv - $val = '' unless defined $val; - $val =~ s/(?:\n|\r)//g; $val =~ s{\t}{ }g; - $val; - } @$col)."\n"); - } - $m->flush_buffer unless ++$i % 10; -} -$m->abort(); - +$m->comp( "/Elements/TSVExport", Collection => $Tickets, Format => $Format, PreserveNewLines => $PreserveNewLines ); </%INIT> diff --git a/rt/share/html/Search/Simple.html b/rt/share/html/Search/Simple.html index fbbffde3c..f65ad5dc4 100644 --- a/rt/share/html/Search/Simple.html +++ b/rt/share/html/Search/Simple.html @@ -98,7 +98,7 @@ <%INIT> my $title = loc("Search for tickets"); -use RT::Search::Googleish; +use RT::Search::Simple; if ($q) { my $tickets = RT::Tickets->new( $session{'CurrentUser'} ); @@ -115,7 +115,7 @@ if ($q) { $m->callback( %ARGS, CallbackName => 'SearchArgs', args => \%args); - my $search = RT::Search::Googleish->new(%args); + my $search = RT::Search::Simple->new(%args); $m->comp( "Results.html", Query => $search->QueryToSQL() ); $m->comp( "/Elements/Footer" ); diff --git a/rt/share/html/Search/Graph.html b/rt/share/html/Search/index.html index 185278ac0..ffb7a5b6f 100644 --- a/rt/share/html/Search/Graph.html +++ b/rt/share/html/Search/index.html @@ -45,6 +45,6 @@ %# those contributions and any derivatives thereof. %# %# END BPS TAGGED BLOCK }}} -<%INIT> -return $m->comp('/Ticket/Graphs/index.html', %ARGS ); -</%INIT> +<& /Admin/Elements/Header, Title => loc('Searches') &> +<& /Elements/Tabs &> +<& /Elements/ListMenu, menu => Menu()->child('search'), show_children => 1 &> |