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