import rt 3.4.6
[freeside.git] / rt / html / Search / Build.html
1 %# BEGIN BPS TAGGED BLOCK {{{
2 %# 
3 %# COPYRIGHT:
4 %#  
5 %# This software is Copyright (c) 1996-2005 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., 675 Mass Ave, Cambridge, MA 02139, USA.
26 %# 
27 %# 
28 %# CONTRIBUTION SUBMISSION POLICY:
29 %# 
30 %# (The following paragraph is not intended to limit the rights granted
31 %# to you to modify and distribute this software under the terms of
32 %# the GNU General Public License and is only of importance to you if
33 %# you choose to contribute your changes and enhancements to the
34 %# community by submitting them to Best Practical Solutions, LLC.)
35 %# 
36 %# By intentionally submitting any modifications, corrections or
37 %# derivatives to this work, or any other work intended for use with
38 %# Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 %# you are the copyright holder for those contributions and you grant
40 %# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
41 %# royalty-free, perpetual, license to use, copy, create derivative
42 %# works based on those contributions, and sublicense and distribute
43 %# those contributions and any derivatives thereof.
44 %# 
45 %# END BPS TAGGED BLOCK }}}
46 %#
47 %# Data flow here:
48 %#   The page receives a Query from the previous page, and maybe arguments
49 %#   corresponding to actions.  (If it doesn't get a Query argument, it pulls
50 %#   one out of the session hash.  Also, it could be getting just a raw query from
51 %#   Build/Edit.html (Advanced).)
52 %#
53 %#   After doing some stuff with default arguments and saved searches, the ParseQuery
54 %#   function (which is similar to, but not the same as, _parser in RT/Tickets_Overlay_SQL)
55 %#   converts the Query into a RT::Interface::Web::QueryBuilder::Tree.  This mason file
56 %#   then adds stuff to or modifies the tree based on the actions that had been requested
57 %#   by clicking buttons.  It then calls GetQueryAndOptionList on the tree to generate
58 %#   the SQL query (which is saved as a hidden input) and the option list for the Clauses
59 %#   box in the top right corner.
60 %#
61 %#   Worthwhile refactoring: the tree manipulation code for the actions could use some cleaning
62 %#   up.  The node-adding code is different in the "add" actions from in ParseQuery, which leads
63 %#   to things like ParseQuery correctly not quoting numbers in numerical fields, while the "add"
64 %#   action does quote it (this breaks SQLite).
65 %#
66 <& /Elements/Header, Title => $title &>
67 <& /Ticket/Elements/Tabs, 
68     current_tab => "Search/Build.html".$QueryString, 
69     Title => $title,
70     Format => $Format,
71     Query => $Query,
72     Order => $Order,
73     OrderBy => $OrderBy,
74     Rows => $RowsPerPage
75 &>
76
77 <FORM METHOD="POST" ACTION="Build.html" NAME="BuildQuery">
78 <input type=hidden name=SearchId value="<%$SearchId%>">
79 <input type=hidden name=Query value="<%$Query%>">
80 <input type=hidden name=Format value="<%$Format%>">
81 <table width=100% border="0" cellpadding="5">
82 <tr valign="top">
83 <td class="boxcontainer" rowspan="2" width="65%">
84 <& Elements/PickCriteria, query => $Query, cfqueues => $queues &>
85 <& /Elements/Submit, Caption => loc('Add additional criteria'), Label => loc('Add'), Name => 'AddClause'&>
86 </td>
87
88 <td>
89 <& Elements/EditQuery,
90     %ARGS,
91     actions => \@actions,
92     optionlist => $optionlist,
93     Description => $Description &>
94 </td>
95 </tr>
96
97 <tr valign="top">
98 <td>
99 <& Elements/EditSearches, CurrentSearch => $search_hash, Dirty => $dirty, SearchId => $SearchId &>
100 </td>
101 </tr>
102
103 <tr>
104 <td colspan=2 class="boxcontainer">
105
106 <& Elements/DisplayOptions, %ARGS, Format=> $Format,
107 AvailableColumns => $AvailableColumns,  CurrentFormat => $CurrentFormat, RowsPerPage => $RowsPerPage, OrderBy => $OrderBy, Order => $Order &>
108 </td>
109 </tr>
110 <tr>
111 <td colspan=2 class="boxcontainer">
112 <& /Elements/Submit, Caption => loc("Do the Search"), Label => loc('Search'), Name => 'DoSearch'&>
113 </td>
114 </tr>
115 </table>
116 </FORM>
117
118 <%INIT>
119 use RT::Interface::Web::QueryBuilder;
120 use RT::Interface::Web::QueryBuilder::Tree;
121
122 my $search_hash = {};
123 my $search;
124 my $title = loc("Query Builder");
125
126 # {{{ Clear out unwanted data
127 if ( $NewQuery or $ARGS{'Delete'} ) {
128
129     # Wipe all data-carrying variables clear if we want a new
130     # search, or we're deleting an old one..
131     $Query       = '';
132     $Format      = '';
133     $Description = '';
134     $SearchId    = '';
135     $Order       = '';
136     $OrderBy     = '';
137     $RowsPerPage = '';
138
139     # ($search hasn't been set yet; no need to clear)
140
141     # ..then wipe the session out..
142     undef $session{'CurrentSearchHash'};
143
144     # ..and the search results.
145     $session{'tickets'}->CleanSlate() if defined $session{'tickets'};
146 }
147
148 # }}}
149
150 # {{{ Attempt to load what we can from the session, set defaults
151
152 # We don't read or write to the session again until the end
153 $search_hash = $session{'CurrentSearchHash'};
154
155 # These variables are what define a search_hash; this is also
156 # where we give sane defaults.
157 $Query       ||= $search_hash->{'Query'};
158 $Format      ||= $search_hash->{'Format'};
159 $Description ||= $search_hash->{'Description'};
160 $SearchId    ||= $search_hash->{'SearchId'} || 'new';
161 $Order       ||= $search_hash->{'Order'} || 'ASC';
162 $OrderBy     ||= $search_hash->{'OrderBy'} || 'id';
163 $RowsPerPage = ( $search_hash->{'RowsPerPage'} || 50 )
164   unless defined($RowsPerPage);
165 $search ||= $search_hash->{'Object'};
166
167 # }}}
168
169 my @actions = ();
170
171 # Clean unwanted junk from the format
172 $Format = $m->comp( '/Elements/ScrubHTML', Content => $Format ) if ($Format);
173
174 # {{{ If we're asked to delete the current search, make it go away and reset the search parameters
175 if ( $ARGS{'Delete'} ) {
176
177     # We set $SearchId to 'new' above already, so peek into the %ARGS
178     if ( $ARGS{'SearchId'} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/ ) {
179         my $obj_type  = $1;
180         my $obj_id    = $2;
181         my $search_id = $3;
182
183         my $container_object;
184         if ( $obj_type eq 'RT::User' && $obj_id == $session{'CurrentUser'}->Id )
185         {
186             $container_object = $session{'CurrentUser'}->UserObj;
187         }
188         elsif ( $obj_type eq 'RT::Group' ) {
189             $container_object = RT::Group->new( $session{'CurrentUser'} );
190             $container_object->Load($obj_id);
191         }
192
193         if ( $container_object->id ) {
194
195             # We have the object the entry is an attribute on; delete
196             # the entry..
197             $container_object->Attributes->DeleteEntry(
198                 Name => 'SavedSearch',
199                 id   => $search_id
200             );
201         }
202
203     }
204 }
205
206 # }}}
207
208 # {{{ If the user wants to copy a search, uncouple from the one that this was based on, but don't erase the $Query or $Format
209 if ( $ARGS{'CopySearch'} ) {
210     $SearchId    = 'new';
211     $search      = undef;
212     $Description = loc( "[_1] copy", $Description );
213 }
214
215 # }}}
216
217 # {{{ if we're asked to revert the current search, we just want to load it
218 if ( $ARGS{'Revert'} ) {
219     $ARGS{'LoadSavedSearch'} = $SearchId;
220 }
221
222 # }}}
223
224 # {{{ if we're asked to load a search, load it.
225
226 if ( $ARGS{'LoadSavedSearch'} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/ ) {
227     my $obj_type  = $1;
228     my $obj_id    = $2;
229     my $search_id = $3;
230
231     # We explicitly list out the available types (user and group) and
232     # don't trust user input here
233     if (   ( $obj_type eq 'RT::User' )
234         && ( $obj_id == $session{'CurrentUser'}->id ) )
235     {
236         $search =
237           $session{'CurrentUser'}->UserObj->Attributes->WithId($search_id);
238
239     }
240     elsif ( $obj_type eq 'RT::Group' ) {
241         my $group = RT::Group->new( $session{'CurrentUser'} );
242         $group->Load($obj_id);
243         $search = $group->Attributes->WithId($search_id);
244     }
245
246     # We have a $search and now; import the others
247     $SearchId    = $ARGS{'LoadSavedSearch'};
248     $Description = $search->Description;
249     $Format      = $search->SubValue('Format');
250     $Query       = $search->SubValue('Query');
251     $Order       = $search->SubValue('Order');
252     $OrderBy     = $search->SubValue('OrderBy');
253     $RowsPerPage = $search->SubValue('RowsPerPage');
254 }
255
256 # }}}
257
258 # {{{ Parse the query
259 use Regexp::Common qw /delimited/;
260
261 # States
262 use constant VALUE   => 1;
263 use constant AGGREG  => 2;
264 use constant OP      => 4;
265 use constant PAREN   => 8;
266 use constant KEYWORD => 16;
267
268 my $_match = sub {
269
270     # Case insensitive equality
271     my ( $y, $x ) = @_;
272     return 1 if $x =~ /^$y$/i;
273
274     #  return 1 if ((lc $x) eq (lc $y)); # Why isnt this equiv?
275     return 0;
276 };
277
278 my $ParseQuery = sub {
279     my $string  = shift;
280     my $tree    = shift;
281     my @actions = shift;
282     my $want    = KEYWORD | PAREN;
283     my $last    = undef;
284
285     my $depth = 1;
286
287     # make a tree root
288     $$tree = RT::Interface::Web::QueryBuilder::Tree->new;
289     my $root       = RT::Interface::Web::QueryBuilder::Tree->new( 'AND', $$tree );
290     my $lastnode   = $root;
291     my $parentnode = $root;
292
293     # get the FIELDS from Tickets_Overlay
294     my $tickets = new RT::Tickets( $session{'CurrentUser'} );
295     my %FIELDS  = %{ $tickets->FIELDS };
296
297     # Lower Case version of FIELDS, for case insensitivity
298     my %lcfields = map { ( lc($_) => $_ ) } ( keys %FIELDS );
299
300     my @tokens     = qw[VALUE AGGREG OP PAREN KEYWORD];
301     my $re_aggreg  = qr[(?i:AND|OR)];
302     my $re_value   = qr[$RE{delimited}{-delim=>qq{\'\"}}|\d+];
303     my $re_keyword = qr[$RE{delimited}{-delim=>qq{\'\"}}|(?:\{|\}|\w|\.)+];
304     my $re_op      =
305       qr[=|!=|>=|<=|>|<|(?i:IS NOT)|(?i:IS)|(?i:NOT LIKE)|(?i:LIKE)]
306       ;    # long to short
307     my $re_paren = qr'\(|\)';
308
309     # assume that $ea is AND if it is not set
310     my ( $ea, $key, $op, $value ) = ( "AND", "", "", "" );
311
312     # order of matches in the RE is important.. op should come early,
313     # because it has spaces in it.  otherwise "NOT LIKE" might be parsed
314     # as a keyword or value.
315
316     while (
317         $string =~ /(
318                       $re_aggreg
319                       |$re_op
320                       |$re_keyword
321                       |$re_value
322                       |$re_paren
323                      )/igx
324       )
325     {
326         my $val     = $1;
327         my $current = 0;
328
329         # Highest priority is last
330         $current = OP    if $_match->( $re_op,    $val );
331         $current = VALUE if $_match->( $re_value, $val );
332         $current = KEYWORD
333           if $_match->( $re_keyword, $val ) && ( $want & KEYWORD );
334         $current = AGGREG if $_match->( $re_aggreg, $val );
335         $current = PAREN  if $_match->( $re_paren,  $val );
336
337         unless ( $current && $want & $current ) {
338
339             # Error
340             # FIXME: I will only print out the highest $want value
341             my $token = $tokens[ ( ( log $want ) / ( log 2 ) ) ];
342             push @actions,
343               [
344                 loc(
345 "current: $current, want $want, Error near ->$val<- expecting a "
346                       . $token
347                       . " in '$string'\n"
348                 ),
349                 -1
350               ];
351         }
352
353         # State Machine:
354         my $parentdepth = $depth;
355
356         # Parens are highest priority
357         if ( $current & PAREN ) {
358             if ( $val eq "(" ) {
359                 $depth++;
360
361                 # make a new node that the clauses can be children of
362                 $parentnode = RT::Interface::Web::QueryBuilder::Tree->new( $ea, $parentnode );
363             }
364             else {
365                 $depth--;
366                 $parentnode = $parentnode->getParent();
367                 $lastnode   = $parentnode;
368             }
369
370             $want = KEYWORD | PAREN | AGGREG;
371         }
372         elsif ( $current & AGGREG ) {
373             $ea   = $val;
374             $want = KEYWORD | PAREN;
375         }
376         elsif ( $current & KEYWORD ) {
377             $key  = $val;
378             $want = OP;
379         }
380         elsif ( $current & OP ) {
381             $op   = $val;
382             $want = VALUE;
383         }
384         elsif ( $current & VALUE ) {
385             $value = $val;
386
387             # Remove surrounding quotes from $key, $val
388             # (in future, simplify as for($key,$val) { action on $_ })
389             if ( $key =~ /$RE{delimited}{-delim=>qq{\'\"}}/ ) {
390                 substr( $key, 0,  1 ) = "";
391                 substr( $key, -1, 1 ) = "";
392             }
393             if ( $val =~ /$RE{delimited}{-delim=>qq{\'\"}}/ ) {
394                 substr( $val, 0,  1 ) = "";
395                 substr( $val, -1, 1 ) = "";
396             }
397
398             # Unescape escaped characters
399             $key =~ s!\\(.)!$1!g;
400             $val =~ s!\\(.)!$1!g;
401
402             my $class;
403             if ( exists $lcfields{ lc $key } ) {
404                 $key   = $lcfields{ lc $key };
405                 $class = $FIELDS{$key}->[0];
406             }
407             if ( $class ne 'INT' ) {
408                 $val = "'$val'";
409             }
410
411             push @actions, [ loc("Unknown field: $key"), -1 ] unless $class;
412
413             $want = PAREN | AGGREG;
414         }
415         else {
416             push @actions, [ loc("I'm lost"), -1 ];
417         }
418
419         if ( $current & VALUE ) {
420             if ( $key =~ /^CF./ ) {
421                 $key = "'" . $key . "'";
422             }
423             my $clause = {
424                 Key   => $key,
425                 Op    => $op,
426                 Value => $val
427             };
428
429             # explicity add a child to it
430             $lastnode = RT::Interface::Web::QueryBuilder::Tree->new( $clause, $parentnode );
431             $lastnode->getParent()->setNodeValue($ea);
432
433             ( $ea, $key, $op, $value ) = ( "", "", "", "" );
434         }
435
436         $last = $current;
437     }    # while
438
439     push @actions, [ loc("Incomplete query"), -1 ]
440       unless ( ( $want | PAREN ) || ( $want | KEYWORD ) );
441
442     push @actions, [ loc("Incomplete Query"), -1 ]
443       unless ( $last && ( $last | PAREN ) || ( $last || VALUE ) );
444
445     # This will never happen, because the parser will complain
446     push @actions, [ loc("Mismatched parentheses"), -1 ]
447       unless $depth == 1;
448 };
449
450 my $tree;
451 $ParseQuery->( $Query, \$tree, \@actions );
452
453 # if parsing went poorly, send them to the edit page to fix it
454 if ( $actions[0] ) {
455     $m->comp( "Edit.html", Query => $Query, actions => \@actions );
456     $m->abort();
457 }
458
459 $Query  = "";
460
461 my @options = $tree->GetDisplayedNodes;
462
463 my @current_values = grep { defined } @options[@clauses];
464
465 # {{{ Move things around
466 if ( $ARGS{"Up"} ) {
467     if (@current_values) {
468         foreach my $value (@current_values) {
469             my $index = $value->getIndex();
470             if ( $value->getIndex() > 0 ) {
471                 my $parent = $value->getParent();
472                 $parent->removeChild($index);
473                 $parent->insertChild( $index - 1, $value );
474                 $value = $parent->getChild( $index - 1 );
475             }
476             else {
477                 push( @actions, [ loc("error: can't move up"), -1 ] );
478             }
479         }
480     }
481     else {
482         push( @actions, [ loc("error: nothing to move"), -1 ] );
483     }
484 }
485 elsif ( $ARGS{"Down"} ) {
486     if (@current_values) {
487         foreach my $value (@current_values) {
488             my $index  = $value->getIndex();
489             my $parent = $value->getParent();
490             if ( $value->getIndex() < ( $parent->getChildCount - 1 ) ) {
491                 $parent->removeChild($index);
492                 $parent->insertChild( $index + 1, $value );
493                 $value = $parent->getChild( $index + 1 );
494             }
495             else {
496                 push( @actions, [ loc("error: can't move down"), -1 ] );
497             }
498         }
499     }
500     else {
501         push( @actions, [ loc("error: nothing to move"), -1 ] );
502     }
503 }
504 elsif ( $ARGS{"Left"} ) {
505     if (@current_values) {
506         foreach my $value (@current_values) {
507             my $parent      = $value->getParent();
508             my $grandparent = $parent->getParent();
509             if ( !$grandparent->isRoot ) {
510                 my $index = $parent->getIndex();
511                 $parent->removeChild($value);
512                 $grandparent->insertChild( $index, $value );
513                 if ( $parent->isLeaf() ) {
514                     $grandparent->removeChild($parent);
515                 }
516             }
517             else {
518                 push( @actions, [ loc("error: can't move left"), -1 ] );
519             }
520         }
521     }
522     else {
523         push( @actions, [ loc("error: nothing to move"), -1 ] );
524     }
525 }
526 elsif ( $ARGS{"Right"} ) {
527     if (@current_values) {
528         foreach my $value (@current_values) {
529             my $parent = $value->getParent();
530             my $index  = $value->getIndex();
531             my $newparent;
532             if ( $index > 0 ) {
533                 my $sibling = $parent->getChild( $index - 1 );
534                 if ( ref( $sibling->getNodeValue ) ) {
535                     $parent->removeChild($value);
536                     my $newtree = RT::Interface::Web::QueryBuilder::Tree->new( 'AND', $parent );
537                     $newtree->addChild($value);
538                 }
539                 else {
540                     $parent->removeChild($index);
541                     $sibling->addChild($value);
542                 }
543             }
544             else {
545                 $parent->removeChild($value);
546                 $newparent = RT::Interface::Web::QueryBuilder::Tree->new( 'AND', $parent );
547                 $newparent->addChild($value);
548             }
549         }
550     }
551     else {
552         push( @actions, [ loc("error: nothing to move"), -1 ] );
553     }
554 }
555 elsif ( $ARGS{"DeleteClause"} ) {
556     if (@current_values) {
557         $_->getParent()->removeChild($_) for @current_values;
558                 @current_values = ();
559     }
560     else {
561         push( @actions, [ loc("error: nothing to delete"), -1 ] );
562     }
563 }
564 elsif ( $ARGS{"Toggle"} ) {
565     my $ea;
566     if (@current_values) {
567         foreach my $value (@current_values) {
568             my $parent = $value->getParent();
569
570             if ( $parent->getNodeValue eq 'AND' ) {
571                 $parent->setNodeValue('OR');
572             }
573             else {
574                 $parent->setNodeValue('AND');
575             }
576         }
577     }
578     else {
579         push( @actions, [ loc("error: nothing to toggle"), -1 ] );
580     }
581 }
582
583 # {{{ Try to find if we're adding a clause
584 foreach my $arg ( keys %ARGS ) {
585     if (
586         $arg =~ m/^ValueOf(.+)/
587         && ( ref $ARGS{$arg} eq "ARRAY"
588             ? grep { $_ ne "" } @{ $ARGS{$arg} }
589             : $ARGS{$arg} ne "" )
590       )
591     {
592
593         # We're adding a $1 clause
594         my $field = $1;
595         my ( $keyword, $op, $value );
596
597         #figure out if it's a grouping
598         if ( $ARGS{ $field . "Field" } ) {
599             $keyword = $ARGS{ $field . "Field" };
600         }
601         else {
602             $keyword = $field;
603         }
604
605         my ( @ops, @values );
606         if ( ref $ARGS{ 'ValueOf' . $field } eq "ARRAY" ) {
607
608             # we have many keys/values to iterate over, because there is
609             # more than one CF with the same name.
610             @ops    = @{ $ARGS{ $field . 'Op' } };
611             @values = @{ $ARGS{ 'ValueOf' . $field } };
612         }
613         else {
614             @ops    = ( $ARGS{ $field . 'Op' } );
615             @values = ( $ARGS{ 'ValueOf' . $field } );
616         }
617         $RT::Logger->error("Bad Parameters passed into Query Builder")
618           unless @ops == @values;
619
620         for my $i ( 0 .. @ops - 1 ) {
621             my ( $op, $value ) = ( $ops[$i], $values[$i] );
622             next if $value eq "";
623
624             if ( $value eq 'NULL' && $op =~ /=/ ) {
625                 if ( $op eq '=' ) {
626                     $op = "IS";
627                 }
628                 elsif ( $op eq '!=' ) {
629                     $op = "IS NOT";
630                 }
631
632                 # This isn't "right", but...
633                 # It has to be this way until #5182 is fixed
634                 $value = "'NULL'";
635             }
636             else {
637                 $value = "'$value'";
638             }
639
640             my $clause = {
641                 Key   => $keyword,
642                 Op    => $op,
643                 Value => $value
644             };
645
646             my $newnode = RT::Interface::Web::QueryBuilder::Tree->new($clause);
647             if (@current_values) {
648                 foreach my $value (@current_values) {
649                     my $newindex = $value->getIndex() + 1;
650                     $value->insertSibling( $newindex, $newnode );
651                     $value = $newnode;
652                 }
653             }
654             else {
655                 $tree->getChild(0)->addChild($newnode);
656                 @current_values = $newnode;
657             }
658             $newnode->getParent()->setNodeValue( $ARGS{'AndOr'} );
659         }
660     }
661 }
662
663 # }}}
664
665 $tree->PruneChildlessAggregators;
666
667 # }}}
668
669 # {{{ Rebuild $Query based on the additions / movements
670 $Query      = "";
671 my $optionlist_arrayref;
672
673 ($Query, $optionlist_arrayref) = $tree->GetQueryAndOptionList(\@current_values);
674   
675 my $optionlist = join "\n", map { qq(<option value="$_->{INDEX}" $_->{SELECTED}>) 
676                                   . ("&nbsp;" x (5 * $_->{DEPTH}))
677                                   . $m->interp->apply_escapes($_->{TEXT}, 'h') . qq(</option>) } @$optionlist_arrayref;
678
679
680
681
682 # }}}
683
684 # }}}
685
686 my $queues = $tree->GetReferencedQueues;
687
688 # {{{ Deal with format changes
689 my ( $AvailableColumns, $CurrentFormat );
690 ( $Format, $AvailableColumns, $CurrentFormat ) = $m->comp(
691     'Elements/BuildFormatString',
692     cfqueues => $queues,
693     %ARGS, Format => $Format
694 );
695
696 # }}}
697
698 # {{{ if we're asked to save the current search, save it
699 if ( $ARGS{'Save'} ) {
700
701     if ( $search && $search->id ) {
702
703         # This search is based on a previously loaded search -- so
704         # just update the current search object with new values
705         $search->SetSubValues(
706             Format      => $Format,
707             Query       => $Query,
708             Order       => $Order,
709             OrderBy     => $OrderBy,
710             RowsPerPage => $RowsPerPage,
711         );
712         $search->SetDescription($Description);
713
714     }
715     elsif ( $SearchId eq 'new' && $ARGS{'Owner'} =~ /^(.*?)-(\d+)$/ ) {
716
717         # We're saving a new search
718         my $obj_type = $1;
719         my $obj_id   = $2;
720
721         # Find out if we're saving on the user, or a group
722         my $container_object;
723         if ( $obj_type eq 'RT::User' && $obj_id == $session{'CurrentUser'}->Id )
724         {
725             $container_object = $session{'CurrentUser'}->UserObj;
726         }
727         elsif ( $obj_type eq 'RT::Group' ) {
728             $container_object = RT::Group->new( $session{'CurrentUser'} );
729             $container_object->Load($obj_id);
730         }
731
732         if ( $container_object->id ) {
733
734             # If we got one or the other, add the saerch
735             my ( $search_id, $search_msg ) = $container_object->AddAttribute(
736                 Name        => 'SavedSearch',
737                 Description => $Description,
738                 Content     => {
739                     Format      => $Format,
740                     Query       => $Query,
741                     Order       => $Order,
742                     OrderBy     => $OrderBy,
743                     RowsPerPage => $RowsPerPage,
744                 }
745             );
746             $search =
747               $session{'CurrentUser'}->UserObj->Attributes->WithId($search_id);
748
749             # Build new SearchId
750             $SearchId =
751                 ref( $session{'CurrentUser'}->UserObj ) . '-'
752               . $session{'CurrentUser'}->UserObj->Id
753               . '-SavedSearch-'
754               . $search->Id;
755         }
756         unless ( $search->id ) {
757             push @actions, [ loc("Can't find a saved search to work with"), 0 ];
758         }
759
760     }
761     else {
762         push @actions, [ loc("Can't save this search"), 0 ];
763     }
764
765 }
766
767 # }}}
768
769 # {{{ If we're modifying an old query, check if it has changed
770 my $dirty = 0;
771 $dirty = 1
772   if defined $search
773   and ($search->SubValue('Format') ne $Format
774     or $search->SubValue('Query')       ne $Query
775     or $search->SubValue('Order')       ne $Order
776     or $search->SubValue('OrderBy')     ne $OrderBy
777     or $search->SubValue('RowsPerPage') ne $RowsPerPage );
778
779 # }}}
780
781 # {{{ Push the updates into the session so we don't loose 'em
782 $search_hash->{'SearchId'}    = $SearchId;
783 $search_hash->{'Format'}      = $Format;
784 $search_hash->{'Query'}       = $Query;
785 $search_hash->{'Description'} = $Description;
786 $search_hash->{'Object'}      = $search;
787 $search_hash->{'Order'}       = $Order;
788 $search_hash->{'OrderBy'}     = $OrderBy;
789 $search_hash->{'RowsPerPage'} = $RowsPerPage;
790
791 $session{'CurrentSearchHash'} = $search_hash;
792
793 # }}}
794
795 # {{{ Show the results, if we were asked.
796 if ( $ARGS{"DoSearch"} ) {
797     $m->comp(
798         "Results.html",
799         Query   => $Query,
800         Format  => $Format,
801         Order   => $Order,
802         OrderBy => $OrderBy,
803         Rows    => $RowsPerPage
804     );
805     $m->abort();
806 }
807
808 # }}}
809
810 # {{{ Build a querystring for the tabs
811
812 my $QueryString;
813 if ($NewQuery) {
814     $QueryString = '?NewQuery=1';
815 }
816 else {
817     $QueryString = '?'
818       . $m->comp(
819         '/Elements/QueryString',
820         Query   => $Query,
821         Format  => $Format,
822         Order   => $Order,
823         OrderBy => $OrderBy,
824         Rows    => $RowsPerPage
825       )
826       if ($Query);
827 }
828
829 # }}}
830
831 </%INIT>
832
833 <%ARGS>
834 $NewQuery => 0
835 $SearchId => undef
836 $Query => undef
837 $Format => undef 
838 $Description => undef
839 $Order => undef
840 $OrderBy => undef
841 $RowsPerPage => undef
842 $HideResults => 0
843 @clauses => ()
844 </%ARGS>
845