This commit was generated by cvs2svn to compensate for changes in r4407,
[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 my $tree;
260 ParseQuery( $Query, \$tree, \@actions );
261
262 # if parsing went poorly, send them to the edit page to fix it
263 if ( $actions[0] ) {
264     $m->comp( "Edit.html", Query => $Query, actions => \@actions );
265     $m->abort();
266 }
267
268 $Query  = "";
269
270 my @options = $tree->GetDisplayedNodes;
271
272 my @current_values = grep { defined } @options[@clauses];
273
274 # {{{ Try to find if we're adding a clause
275 foreach my $arg ( keys %ARGS ) {
276     if (
277         $arg =~ m/^ValueOf(.+)/
278         && ( ref $ARGS{$arg} eq "ARRAY"
279             ? grep { $_ ne "" } @{ $ARGS{$arg} }
280             : $ARGS{$arg} ne "" )
281       )
282     {
283
284         # We're adding a $1 clause
285         my $field = $1;
286         my ( $keyword, $op, $value );
287
288         #figure out if it's a grouping
289         if ( $ARGS{ $field . "Field" } ) {
290             $keyword = $ARGS{ $field . "Field" };
291         }
292         else {
293             $keyword = $field;
294         }
295
296         my ( @ops, @values );
297         if ( ref $ARGS{ 'ValueOf' . $field } eq "ARRAY" ) {
298
299             # we have many keys/values to iterate over, because there is
300             # more than one CF with the same name.
301             @ops    = @{ $ARGS{ $field . 'Op' } };
302             @values = @{ $ARGS{ 'ValueOf' . $field } };
303         }
304         else {
305             @ops    = ( $ARGS{ $field . 'Op' } );
306             @values = ( $ARGS{ 'ValueOf' . $field } );
307         }
308         $RT::Logger->error("Bad Parameters passed into Query Builder")
309           unless @ops == @values;
310
311         for my $i ( 0 .. @ops - 1 ) {
312             my ( $op, $value ) = ( $ops[$i], $values[$i] );
313             next if $value eq "";
314
315             if ( $value eq 'NULL' && $op =~ /=/ ) {
316                 if ( $op eq '=' ) {
317                     $op = "IS";
318                 }
319                 elsif ( $op eq '!=' ) {
320                     $op = "IS NOT";
321                 }
322
323                 # This isn't "right", but...
324                 # It has to be this way until #5182 is fixed
325                 $value = "'NULL'";
326             }
327             else {
328                 $value = "'$value'";
329             }
330
331             my $clause = {
332                 Key   => $keyword,
333                 Op    => $op,
334                 Value => $value
335             };
336
337             my $newnode = RT::Interface::Web::QueryBuilder::Tree->new($clause);
338             if (@current_values) {
339                 foreach my $value (@current_values) {
340                     my $newindex = $value->getIndex() + 1;
341                     $value->insertSibling( $newindex, $newnode );
342                     $value = $newnode;
343                 }
344             }
345             else {
346                 $tree->getChild(0)->addChild($newnode);
347                 @current_values = $newnode;
348             }
349             $newnode->getParent()->setNodeValue( $ARGS{'AndOr'} );
350         }
351     }
352 }
353
354 # }}}
355
356 # {{{ Move things around
357 if ( $ARGS{"Up"} ) {
358     if (@current_values) {
359         foreach my $value (@current_values) {
360             my $index = $value->getIndex();
361             if ( $value->getIndex() > 0 ) {
362                 my $parent = $value->getParent();
363                 $parent->removeChild($index);
364                 $parent->insertChild( $index - 1, $value );
365                 $value = $parent->getChild( $index - 1 );
366             }
367             else {
368                 push( @actions, [ loc("error: can't move up"), -1 ] );
369             }
370         }
371     }
372     else {
373         push( @actions, [ loc("error: nothing to move"), -1 ] );
374     }
375 }
376 elsif ( $ARGS{"Down"} ) {
377     if (@current_values) {
378         foreach my $value (@current_values) {
379             my $index  = $value->getIndex();
380             my $parent = $value->getParent();
381             if ( $value->getIndex() < ( $parent->getChildCount - 1 ) ) {
382                 $parent->removeChild($index);
383                 $parent->insertChild( $index + 1, $value );
384                 $value = $parent->getChild( $index + 1 );
385             }
386             else {
387                 push( @actions, [ loc("error: can't move down"), -1 ] );
388             }
389         }
390     }
391     else {
392         push( @actions, [ loc("error: nothing to move"), -1 ] );
393     }
394 }
395 elsif ( $ARGS{"Left"} ) {
396     if (@current_values) {
397         foreach my $value (@current_values) {
398             my $parent      = $value->getParent();
399             my $grandparent = $parent->getParent();
400             if ( !$grandparent->isRoot ) {
401                 my $index = $parent->getIndex();
402                 $parent->removeChild($value);
403                 $grandparent->insertChild( $index, $value );
404                 if ( $parent->isLeaf() ) {
405                     $grandparent->removeChild($parent);
406                 }
407             }
408             else {
409                 push( @actions, [ loc("error: can't move left"), -1 ] );
410             }
411         }
412     }
413     else {
414         push( @actions, [ loc("error: nothing to move"), -1 ] );
415     }
416 }
417 elsif ( $ARGS{"Right"} ) {
418     if (@current_values) {
419         foreach my $value (@current_values) {
420             my $parent = $value->getParent();
421             my $index  = $value->getIndex();
422             my $newparent;
423             if ( $index > 0 ) {
424                 my $sibling = $parent->getChild( $index - 1 );
425                 if ( ref( $sibling->getNodeValue ) ) {
426                     $parent->removeChild($value);
427                     my $newtree = RT::Interface::Web::QueryBuilder::Tree->new( 'AND', $parent );
428                     $newtree->addChild($value);
429                 }
430                 else {
431                     $parent->removeChild($index);
432                     $sibling->addChild($value);
433                 }
434             }
435             else {
436                 $parent->removeChild($value);
437                 $newparent = RT::Interface::Web::QueryBuilder::Tree->new( 'AND', $parent );
438                 $newparent->addChild($value);
439             }
440         }
441     }
442     else {
443         push( @actions, [ loc("error: nothing to move"), -1 ] );
444     }
445 }
446 elsif ( $ARGS{"DeleteClause"} ) {
447     if (@current_values) {
448         $_->getParent()->removeChild($_) for @current_values;
449     }
450     else {
451         push( @actions, [ loc("error: nothing to delete"), -1 ] );
452     }
453 }
454 elsif ( $ARGS{"Toggle"} ) {
455     my $ea;
456     if (@current_values) {
457         foreach my $value (@current_values) {
458             my $parent = $value->getParent();
459
460             if ( $parent->getNodeValue eq 'AND' ) {
461                 $parent->setNodeValue('OR');
462             }
463             else {
464                 $parent->setNodeValue('AND');
465             }
466         }
467     }
468     else {
469         push( @actions, [ loc("error: nothing to toggle"), -1 ] );
470     }
471 }
472
473 $tree->PruneChildlessAggregators;
474
475 # }}}
476
477 # {{{ Rebuild $Query based on the additions / movements
478 $Query      = "";
479 my $optionlist_arrayref;
480
481 ($Query, $optionlist_arrayref) = $tree->GetQueryAndOptionList(\@current_values);
482   
483 my $optionlist = join "\n", map { qq(<option value="$_->{INDEX}" $_->{SELECTED}>) 
484                                   . ("&nbsp;" x (5 * $_->{DEPTH}))
485                                   . $m->interp->apply_escapes($_->{TEXT}, 'h') . qq(</option>) } @$optionlist_arrayref;
486
487
488
489
490 use Regexp::Common qw /delimited/;
491
492 # States
493 use constant VALUE   => 1;
494 use constant AGGREG  => 2;
495 use constant OP      => 4;
496 use constant PAREN   => 8;
497 use constant KEYWORD => 16;
498
499 sub ParseQuery {
500     my $string  = shift;
501     my $tree    = shift;
502     my @actions = shift;
503     my $want    = KEYWORD | PAREN;
504     my $last    = undef;
505
506     my $depth = 1;
507
508     # make a tree root
509     $$tree = RT::Interface::Web::QueryBuilder::Tree->new;
510     my $root       = RT::Interface::Web::QueryBuilder::Tree->new( 'AND', $$tree );
511     my $lastnode   = $root;
512     my $parentnode = $root;
513
514     # get the FIELDS from Tickets_Overlay
515     my $tickets = new RT::Tickets( $session{'CurrentUser'} );
516     my %FIELDS  = %{ $tickets->FIELDS };
517
518     # Lower Case version of FIELDS, for case insensitivity
519     my %lcfields = map { ( lc($_) => $_ ) } ( keys %FIELDS );
520
521     my @tokens     = qw[VALUE AGGREG OP PAREN KEYWORD];
522     my $re_aggreg  = qr[(?i:AND|OR)];
523     my $re_value   = qr[$RE{delimited}{-delim=>qq{\'\"}}|\d+];
524     my $re_keyword = qr[$RE{delimited}{-delim=>qq{\'\"}}|(?:\{|\}|\w|\.)+];
525     my $re_op      =
526       qr[=|!=|>=|<=|>|<|(?i:IS NOT)|(?i:IS)|(?i:NOT LIKE)|(?i:LIKE)]
527       ;    # long to short
528     my $re_paren = qr'\(|\)';
529
530     # assume that $ea is AND if it is not set
531     my ( $ea, $key, $op, $value ) = ( "AND", "", "", "" );
532
533     # order of matches in the RE is important.. op should come early,
534     # because it has spaces in it.  otherwise "NOT LIKE" might be parsed
535     # as a keyword or value.
536
537     while (
538         $string =~ /(
539                       $re_aggreg
540                       |$re_op
541                       |$re_keyword
542                       |$re_value
543                       |$re_paren
544                      )/igx
545       )
546     {
547         my $val     = $1;
548         my $current = 0;
549
550         # Highest priority is last
551         $current = OP    if _match( $re_op,    $val );
552         $current = VALUE if _match( $re_value, $val );
553         $current = KEYWORD
554           if _match( $re_keyword, $val ) && ( $want & KEYWORD );
555         $current = AGGREG if _match( $re_aggreg, $val );
556         $current = PAREN  if _match( $re_paren,  $val );
557
558         unless ( $current && $want & $current ) {
559
560             # Error
561             # FIXME: I will only print out the highest $want value
562             my $token = $tokens[ ( ( log $want ) / ( log 2 ) ) ];
563             push @actions,
564               [
565                 loc(
566 "current: $current, want $want, Error near ->$val<- expecting a "
567                       . $token
568                       . " in '$string'\n"
569                 ),
570                 -1
571               ];
572         }
573
574         # State Machine:
575         my $parentdepth = $depth;
576
577         # Parens are highest priority
578         if ( $current & PAREN ) {
579             if ( $val eq "(" ) {
580                 $depth++;
581
582                 # make a new node that the clauses can be children of
583                 $parentnode = RT::Interface::Web::QueryBuilder::Tree->new( $ea, $parentnode );
584             }
585             else {
586                 $depth--;
587                 $parentnode = $parentnode->getParent();
588                 $lastnode   = $parentnode;
589             }
590
591             $want = KEYWORD | PAREN | AGGREG;
592         }
593         elsif ( $current & AGGREG ) {
594             $ea   = $val;
595             $want = KEYWORD | PAREN;
596         }
597         elsif ( $current & KEYWORD ) {
598             $key  = $val;
599             $want = OP;
600         }
601         elsif ( $current & OP ) {
602             $op   = $val;
603             $want = VALUE;
604         }
605         elsif ( $current & VALUE ) {
606             $value = $val;
607
608             # Remove surrounding quotes from $key, $val
609             # (in future, simplify as for($key,$val) { action on $_ })
610             if ( $key =~ /$RE{delimited}{-delim=>qq{\'\"}}/ ) {
611                 substr( $key, 0,  1 ) = "";
612                 substr( $key, -1, 1 ) = "";
613             }
614             if ( $val =~ /$RE{delimited}{-delim=>qq{\'\"}}/ ) {
615                 substr( $val, 0,  1 ) = "";
616                 substr( $val, -1, 1 ) = "";
617             }
618
619             # Unescape escaped characters
620             $key =~ s!\\(.)!$1!g;
621             $val =~ s!\\(.)!$1!g;
622
623             my $class;
624             if ( exists $lcfields{ lc $key } ) {
625                 $key   = $lcfields{ lc $key };
626                 $class = $FIELDS{$key}->[0];
627             }
628             if ( $class ne 'INT' ) {
629                 $val = "'$val'";
630             }
631
632             push @actions, [ loc("Unknown field: $key"), -1 ] unless $class;
633
634             $want = PAREN | AGGREG;
635         }
636         else {
637             push @actions, [ loc("I'm lost"), -1 ];
638         }
639
640         if ( $current & VALUE ) {
641             if ( $key =~ /^CF./ ) {
642                 $key = "'" . $key . "'";
643             }
644             my $clause = {
645                 Key   => $key,
646                 Op    => $op,
647                 Value => $val
648             };
649
650             # explicity add a child to it
651             $lastnode = RT::Interface::Web::QueryBuilder::Tree->new( $clause, $parentnode );
652             $lastnode->getParent()->setNodeValue($ea);
653
654             ( $ea, $key, $op, $value ) = ( "", "", "", "" );
655         }
656
657         $last = $current;
658     }    # while
659
660     push @actions, [ loc("Incomplete query"), -1 ]
661       unless ( ( $want | PAREN ) || ( $want | KEYWORD ) );
662
663     push @actions, [ loc("Incomplete Query"), -1 ]
664       unless ( $last && ( $last | PAREN ) || ( $last || VALUE ) );
665
666     # This will never happen, because the parser will complain
667     push @actions, [ loc("Mismatched parentheses"), -1 ]
668       unless $depth == 1;
669 }
670
671 sub _match {
672
673     # Case insensitive equality
674     my ( $y, $x ) = @_;
675     return 1 if $x =~ /^$y$/i;
676
677     #  return 1 if ((lc $x) eq (lc $y)); # Why isnt this equiv?
678     return 0;
679 }
680
681 sub debug {
682     my $message = shift;
683     $m->print( $message . "<br>" );
684 }
685
686 # }}}
687
688 # }}}
689
690 my $queues = $tree->GetReferencedQueues;
691
692 # {{{ Deal with format changes
693 my ( $AvailableColumns, $CurrentFormat );
694 ( $Format, $AvailableColumns, $CurrentFormat ) = $m->comp(
695     'Elements/BuildFormatString',
696     cfqueues => $queues,
697     %ARGS, Format => $Format
698 );
699
700 # }}}
701
702 # {{{ if we're asked to save the current search, save it
703 if ( $ARGS{'Save'} ) {
704
705     if ( $search && $search->id ) {
706
707         # This search is based on a previously loaded search -- so
708         # just update the current search object with new values
709         $search->SetSubValues(
710             Format      => $Format,
711             Query       => $Query,
712             Order       => $Order,
713             OrderBy     => $OrderBy,
714             RowsPerPage => $RowsPerPage,
715         );
716         $search->SetDescription($Description);
717
718     }
719     elsif ( $SearchId eq 'new' && $ARGS{'Owner'} =~ /^(.*?)-(\d+)$/ ) {
720
721         # We're saving a new search
722         my $obj_type = $1;
723         my $obj_id   = $2;
724
725         # Find out if we're saving on the user, or a group
726         my $container_object;
727         if ( $obj_type eq 'RT::User' && $obj_id == $session{'CurrentUser'}->Id )
728         {
729             $container_object = $session{'CurrentUser'}->UserObj;
730         }
731         elsif ( $obj_type eq 'RT::Group' ) {
732             $container_object = RT::Group->new( $session{'CurrentUser'} );
733             $container_object->Load($obj_id);
734         }
735
736         if ( $container_object->id ) {
737
738             # If we got one or the other, add the saerch
739             my ( $search_id, $search_msg ) = $container_object->AddAttribute(
740                 Name        => 'SavedSearch',
741                 Description => $Description,
742                 Content     => {
743                     Format      => $Format,
744                     Query       => $Query,
745                     Order       => $Order,
746                     OrderBy     => $OrderBy,
747                     RowsPerPage => $RowsPerPage,
748                 }
749             );
750             $search =
751               $session{'CurrentUser'}->UserObj->Attributes->WithId($search_id);
752
753             # Build new SearchId
754             $SearchId =
755                 ref( $session{'CurrentUser'}->UserObj ) . '-'
756               . $session{'CurrentUser'}->UserObj->Id
757               . '-SavedSearch-'
758               . $search->Id;
759         }
760         unless ( $search->id ) {
761             push @actions, [ loc("Can't find a saved search to work with"), 0 ];
762         }
763
764     }
765     else {
766         push @actions, [ loc("Can't save this search"), 0 ];
767     }
768
769 }
770
771 # }}}
772
773 # {{{ If we're modifying an old query, check if it has changed
774 my $dirty = 0;
775 $dirty = 1
776   if defined $search
777   and ($search->SubValue('Format') ne $Format
778     or $search->SubValue('Query')       ne $Query
779     or $search->SubValue('Order')       ne $Order
780     or $search->SubValue('OrderBy')     ne $OrderBy
781     or $search->SubValue('RowsPerPage') ne $RowsPerPage );
782
783 # }}}
784
785 # {{{ Push the updates into the session so we don't loose 'em
786 $search_hash->{'SearchId'}    = $SearchId;
787 $search_hash->{'Format'}      = $Format;
788 $search_hash->{'Query'}       = $Query;
789 $search_hash->{'Description'} = $Description;
790 $search_hash->{'Object'}      = $search;
791 $search_hash->{'Order'}       = $Order;
792 $search_hash->{'OrderBy'}     = $OrderBy;
793 $search_hash->{'RowsPerPage'} = $RowsPerPage;
794
795 $session{'CurrentSearchHash'} = $search_hash;
796
797 # }}}
798
799 # {{{ Show the results, if we were asked.
800 if ( $ARGS{"DoSearch"} ) {
801     $m->comp(
802         "Results.html",
803         Query   => $Query,
804         Format  => $Format,
805         Order   => $Order,
806         OrderBy => $OrderBy,
807         Rows    => $RowsPerPage
808     );
809     $m->abort();
810 }
811
812 # }}}
813
814 # {{{ Build a querystring for the tabs
815
816 my $QueryString;
817 if ($NewQuery) {
818     $QueryString = '?NewQuery=1';
819 }
820 else {
821     $QueryString = '?'
822       . $m->comp(
823         '/Elements/QueryString',
824         Query   => $Query,
825         Format  => $Format,
826         Order   => $Order,
827         OrderBy => $OrderBy,
828         Rows    => $RowsPerPage
829       )
830       if ($Query);
831 }
832
833 # }}}
834
835 </%INIT>
836
837 <%ARGS>
838 $NewQuery => 0
839 $SearchId => undef
840 $Query => undef
841 $Format => undef 
842 $Description => undef
843 $Order => undef
844 $OrderBy => undef
845 $RowsPerPage => undef
846 $HideResults => 0
847 @clauses => ()
848 </%ARGS>
849