import rt 3.8.7
[freeside.git] / rt / share / html / Search / Build.html
1 %# BEGIN BPS TAGGED BLOCK {{{
2 %# 
3 %# COPYRIGHT:
4 %# 
5 %# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 %#                                          <jesse@bestpractical.com>
7 %# 
8 %# (Except where explicitly superseded by other copyright notices)
9 %# 
10 %# 
11 %# LICENSE:
12 %# 
13 %# This work is made available to you under the terms of Version 2 of
14 %# the GNU General Public License. A copy of that license should have
15 %# been provided with this software, but in any event can be snarfed
16 %# from www.gnu.org.
17 %# 
18 %# This work is distributed in the hope that it will be useful, but
19 %# WITHOUT ANY WARRANTY; without even the implied warranty of
20 %# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 %# General Public License for more details.
22 %# 
23 %# You should have received a copy of the GNU General Public License
24 %# along with this program; if not, write to the Free Software
25 %# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 %# 02110-1301 or visit their web page on the internet at
27 %# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 %# 
29 %# 
30 %# CONTRIBUTION SUBMISSION POLICY:
31 %# 
32 %# (The following paragraph is not intended to limit the rights granted
33 %# to you to modify and distribute this software under the terms of
34 %# the GNU General Public License and is only of importance to you if
35 %# you choose to contribute your changes and enhancements to the
36 %# community by submitting them to Best Practical Solutions, LLC.)
37 %# 
38 %# By intentionally submitting any modifications, corrections or
39 %# derivatives to this work, or any other work intended for use with
40 %# Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 %# you are the copyright holder for those contributions and you grant
42 %# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 %# royalty-free, perpetual, license to use, copy, create derivative
44 %# works based on those contributions, and sublicense and distribute
45 %# those contributions and any derivatives thereof.
46 %# 
47 %# END BPS TAGGED BLOCK }}}
48 %#
49 %# Data flow here:
50 %#   The page receives a Query from the previous page, and maybe arguments
51 %#   corresponding to actions.  (If it doesn't get a Query argument, it pulls
52 %#   one out of the session hash.  Also, it could be getting just a raw query from
53 %#   Build/Edit.html (Advanced).)
54 %#
55 %#   After doing some stuff with default arguments and saved searches, the ParseQuery
56 %#   function (which is similar to, but not the same as, _parser in RT/Tickets_Overlay_SQL)
57 %#   converts the Query into a RT::Interface::Web::QueryBuilder::Tree.  This mason file
58 %#   then adds stuff to or modifies the tree based on the actions that had been requested
59 %#   by clicking buttons.  It then calls GetQueryAndOptionList on the tree to generate
60 %#   the SQL query (which is saved as a hidden input) and the option list for the Clauses
61 %#   box in the top right corner.
62 %#
63 %#   Worthwhile refactoring: the tree manipulation code for the actions could use some cleaning
64 %#   up.  The node-adding code is different in the "add" actions from in ParseQuery, which leads
65 %#   to things like ParseQuery correctly not quoting numbers in numerical fields, while the "add"
66 %#   action does quote it (this breaks SQLite).
67 %#
68 <& /Elements/Header, Title => $title &>
69 <& /Ticket/Elements/Tabs, 
70     current_tab => "Search/Build.html?".$QueryString, 
71     Title => $title,
72     %query,
73     SavedSearchId => $saved_search{'Id'},
74     SavedChartSearchId => $ARGS{SavedChartSearchId},
75 &>
76
77 <form method="post" action="Build.html" name="BuildQuery">
78 <input type="hidden" class="hidden" name="SavedSearchId" value="<% $saved_search{'Id'} %>" />
79 <input type="hidden" class="hidden" name="SavedChartSearchId" value="<% $ARGS{'SavedChartSearchId'} %>" />
80 <input type="hidden" class="hidden" name="Query" value="<% $query{'Query'} %>" />
81 <input type="hidden" class="hidden" name="Format" value="<% $query{'Format'} %>" />
82
83
84
85
86 <div id="pick-criteria">
87     <& Elements/PickCriteria, query => $query{'Query'}, cfqueues => $queues &>
88 </div>
89 <& /Elements/Submit,  Label => loc('Add these terms'), Name => 'AddClause'&>
90 <& /Elements/Submit, Label => loc('Add these terms and Search'), Name => 'DoSearch'&>
91
92
93 <div id="editquery">
94 <& Elements/EditQuery,
95     %ARGS,
96     actions => \@actions,
97     optionlist => $optionlist,
98     Description => $saved_search{'Description'},
99     &>
100 </div>
101 <div id="editsearches">
102     <& Elements/EditSearches, %saved_search, CurrentSearch => \%query &>
103 </div>
104
105 <span id="display-options">
106 <& Elements/DisplayOptions,
107     %ARGS, %query,
108     AvailableColumns => $AvailableColumns,
109     CurrentFormat    => $CurrentFormat,
110 &>
111 <& /Elements/Submit, Label => loc('Update format and Search'), Name => 'DoSearch', id=>"formatbuttons"&>
112 </span>
113 </form>
114
115 <%INIT>
116 use RT::Interface::Web::QueryBuilder;
117 use RT::Interface::Web::QueryBuilder::Tree;
118
119 $ARGS{SavedChartSearchId} ||= 'new';
120
121 my $title = loc("Query Builder");
122
123 my %query;
124 for( qw(Query Format OrderBy Order RowsPerPage) ) {
125     $query{$_} = $ARGS{$_};
126 }
127
128 my %saved_search;
129 my @actions = $m->comp( 'Elements/EditSearches:Init', %ARGS, Query => \%query, SavedSearch => \%saved_search);
130
131 if ( $NewQuery ) {
132
133     # Wipe all data-carrying variables clear if we want a new
134     # search, or we're deleting an old one..
135     %query = ();
136     %saved_search = ( Id => 'new' );
137
138     # ..then wipe the session out..
139     delete $session{'CurrentSearchHash'};
140
141     # ..and the search results.
142     $session{'tickets'}->CleanSlate if defined $session{'tickets'};
143 }
144
145 { # Attempt to load what we can from the session and preferences, set defaults
146
147     my $current = $session{'CurrentSearchHash'};
148     my $prefs = $session{'CurrentUser'}->UserObj->Preferences("SearchDisplay") || {};
149     my $default = { Query => '', Format => '', OrderBy => 'id', Order => 'ASC', RowsPerPage => 50 };
150
151     for( qw(Query Format OrderBy Order RowsPerPage) ) {
152         $query{$_} = $current->{$_} unless defined $query{$_};
153         $query{$_} = $prefs->{$_} unless defined $query{$_};
154         $query{$_} = $default->{$_} unless defined $query{$_};
155     }
156
157     for( qw(Order OrderBy) ) {
158         if (ref $query{$_} eq "ARRAY") {
159             $query{$_} = join( '|', @{ $query{$_} } );
160         }
161     }
162     if ( $query{'Format'} ) {
163         # Clean unwanted junk from the format
164         $query{'Format'} = $m->comp( '/Elements/ScrubHTML', Content => $query{'Format'} );
165     }
166 }
167
168 my $ParseQuery = sub {
169     my ($string, $results) = @_;
170
171     my $tree = RT::Interface::Web::QueryBuilder::Tree->new('AND');
172     @$results = $tree->ParseSQL( Query => $string, CurrentUser => $session{'CurrentUser'} );
173
174     return $tree;
175 };
176
177 my @parse_results;
178 my $tree = $ParseQuery->( $query{'Query'}, \@parse_results );
179
180 # if parsing went poorly, send them to the edit page to fix it
181 if ( @parse_results ) {
182     return $m->comp( "Edit.html", Query => $query{'Query'}, actions => \@actions );
183 }
184
185 my @options = $tree->GetDisplayedNodes;
186 my @current_values = grep defined, @options[@clauses];
187 my @new_values = ();
188
189 # {{{ Try to find if we're adding a clause
190 foreach my $arg ( keys %ARGS ) {
191     next unless $arg =~ m/^ValueOf(\w+|'CF.{.*?}')$/
192                 && ( ref $ARGS{$arg} eq "ARRAY"
193                      ? grep $_ ne '', @{ $ARGS{$arg} }
194                      : $ARGS{$arg} ne '' );
195
196     # We're adding a $1 clause
197     my $field = $1;
198
199     my ($op, $value);
200
201     #figure out if it's a grouping
202     my $keyword = $ARGS{ $field . "Field" } || $field;
203
204     my ( @ops, @values );
205     if ( ref $ARGS{ 'ValueOf' . $field } eq "ARRAY" ) {
206         # we have many keys/values to iterate over, because there is
207         # more than one CF with the same name.
208         @ops    = @{ $ARGS{ $field . 'Op' } };
209         @values = @{ $ARGS{ 'ValueOf' . $field } };
210     }
211     else {
212         @ops    = ( $ARGS{ $field . 'Op' } );
213         @values = ( $ARGS{ 'ValueOf' . $field } );
214     }
215     $RT::Logger->error("Bad Parameters passed into Query Builder")
216         unless @ops == @values;
217
218     for ( my $i = 0; $i < @ops; $i++ ) {
219         my ( $op, $value ) = ( $ops[$i], $values[$i] );
220         next if !defined $value || $value eq '';
221
222         if ( $value eq 'NULL' && $op =~ /=/ ) {
223             if ( $op eq '=' ) {
224                 $op = "IS";
225             }
226             elsif ( $op eq '!=' ) {
227                 $op = "IS NOT";
228             }
229
230             # This isn't "right", but...
231             # It has to be this way until #5182 is fixed
232             $value = "'NULL'";
233         }
234         else {
235             $value =~ s/'/\\'/g;
236             $value = "'$value'" unless $value =~ /^\d+$/;
237         }
238
239         my $clause = {
240             Key   => $keyword,
241             Op    => $op,
242             Value => $value
243         };
244
245         push @new_values, RT::Interface::Web::QueryBuilder::Tree->new($clause);
246     }
247 }
248
249 # }}}
250
251 push @actions, $m->comp('Elements/EditQuery:Process',
252     %ARGS,
253     Tree     => $tree,
254     Selected => \@current_values,
255     New      => \@new_values,
256 );
257
258 # {{{ Rebuild $Query based on the additions / movements
259
260 my $optionlist_arrayref;
261 ($query{'Query'}, $optionlist_arrayref) = $tree->GetQueryAndOptionList(\@current_values);
262
263 my $optionlist = join "\n", map { qq(<option value="$_->{INDEX}" $_->{SELECTED}>) 
264                                   . ("&nbsp;" x (5 * $_->{DEPTH}))
265                                   . $m->interp->apply_escapes($_->{TEXT}, 'h') . qq(</option>) } @$optionlist_arrayref;
266
267 # }}}
268
269 my $queues = $tree->GetReferencedQueues;
270
271 # {{{ Deal with format changes
272 my ( $AvailableColumns, $CurrentFormat );
273 ( $query{'Format'}, $AvailableColumns, $CurrentFormat ) = $m->comp(
274     'Elements/BuildFormatString',
275     %ARGS,
276     cfqueues => $queues,
277     Format => $query{'Format'},
278 );
279
280 # }}}
281
282 # if we're asked to save the current search, save it
283 push @actions, $m->comp( 'Elements/EditSearches:Save', %ARGS, Query => \%query, SavedSearch => \%saved_search);
284
285 # {{{ Push the updates into the session so we don't loose 'em
286
287 $session{'CurrentSearchHash'} = {
288     %query,
289     SearchId    => $saved_search{'Id'},
290     Object      => $saved_search{'Object'},
291     Description => $saved_search{'Description'},
292 };
293
294 # }}}
295
296 # {{{ Show the results, if we were asked.
297
298 if ( $ARGS{'DoSearch'} ) {
299     $m->comp( 'Results.html', %query, SavedChartSearchId => $ARGS{'SavedChartSearchId'}, );
300     $m->comp( '/Elements/Footer' );
301     $m->abort;
302 }
303
304 # }}}
305
306 # {{{ Build a querystring for the tabs
307
308 my $QueryString = '';
309 if ($NewQuery) {
310     $QueryString = 'NewQuery=1';
311 }
312 elsif ( $query{'Query'} ) {
313     $QueryString = $m->comp('/Elements/QueryString', %query );
314 }
315
316 # }}}
317
318 </%INIT>
319
320 <%ARGS>
321 $NewQuery => 0
322 @clauses => ()
323 </%ARGS>