%# BEGIN BPS TAGGED BLOCK {{{ %# %# COPYRIGHT: %# %# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC %# %# %# (Except where explicitly superseded by other copyright notices) %# %# %# LICENSE: %# %# This work is made available to you under the terms of Version 2 of %# the GNU General Public License. A copy of that license should have %# been provided with this software, but in any event can be snarfed %# from www.gnu.org. %# %# This work is distributed in the hope that it will be useful, but %# WITHOUT ANY WARRANTY; without even the implied warranty of %# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %# General Public License for more details. %# %# You should have received a copy of the GNU General Public License %# along with this program; if not, write to the Free Software %# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA %# 02110-1301 or visit their web page on the internet at %# http://www.gnu.org/copyleft/gpl.html. %# %# %# CONTRIBUTION SUBMISSION POLICY: %# %# (The following paragraph is not intended to limit the rights granted %# to you to modify and distribute this software under the terms of %# the GNU General Public License and is only of importance to you if %# you choose to contribute your changes and enhancements to the %# community by submitting them to Best Practical Solutions, LLC.) %# %# By intentionally submitting any modifications, corrections or %# derivatives to this work, or any other work intended for use with %# Request Tracker, to Best Practical Solutions, LLC, you confirm that %# you are the copyright holder for those contributions and you grant %# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, %# royalty-free, perpetual, license to use, copy, create derivative %# works based on those contributions, and sublicense and distribute %# those contributions and any derivatives thereof. %# %# END BPS TAGGED BLOCK }}} <& /RTFM/Article/Elements/Tabs, current_tab => "RTFM/Article/Search.html", Title => loc("Search for articles") &> % unless ( keys %ARGS ) { % my $Classes=new RT::FM::ClassCollection($session{'CurrentUser'}); % $Classes->LimitToEnabled();
<& /Elements/TitleBoxStart, title => loc('Saved searches') &> <&|/l&>Load saved search:
<& Elements/SelectSavedSearches, Name => 'LoadSavedSearch', Default => $CurrentSearch &> <& /Elements/TitleBoxEnd &>
% return; % } <& /Elements/ListActions, actions => \@results &>
<&|/l&>Modify this search...
<& Elements/ShowSearchResults, query => \%filtered, articles => $articles &>


<& Elements/ShowSearchCriteria, dates => \%dates, RefersTo => $RefersTo, customfields => $customfields, ReferredToBy => $ReferredToBy, %ARGS &>

<& Elements/ShowSavedSearches, CurrentSearch => $CurrentSearch, Name => ($search ? $search->Name : undef), Privacy => ($search ? $search->Privacy : undef) &>
<&|/l&>Bookmarkable link for this search
<%init> use RT::SavedSearch; my @results; my $articles = RT::FM::ArticleCollection->new( $session{'CurrentUser'} ); # {{{ Quicksearch logic # If it is a number, load the article with that ID. Otherwise, search # on name and summary. if ($ARGS{'q'} && $ARGS{'q'} =~ /^(\d+)$/) { return $m->comp("/RTFM/Article/Display.html", id => $1); } elsif ($ARGS{'q'}) { $articles->Limit( FIELD => 'Name', SUBCLAUSE => 'NameOrSummary', OPERATOR => 'LIKE', ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 0, VALUE => $ARGS{'q'} ); $articles->Limit( FIELD => 'Summary', SUBCLAUSE => 'NameOrSummary', OPERATOR => 'LIKE', ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 0, VALUE => $ARGS{'q'} ); } # }}} # {{{ Saved search logic my $search; # The keys in %ARGS that are not saved and loaded with named searches. # These need to be treated specially. my @metakeys = qw/NewSearchName CurrentSearch SearchPrivacy Save Load Update Delete/; if ($CurrentSearch =~ /^(.*-\d+)-SavedSearch-(\d+)$/) { $search = RT::SavedSearch->new($session{'CurrentUser'}); $search->Load($1, $2); } # Have we been asked to load a search? if ($ARGS{'Load'}) { if ($ARGS{'LoadSavedSearch'} =~ /^(.*-\d+)-SavedSearch-(\d+)$/ ) { my $privacy = $1; my $search_id = $2; $search = RT::SavedSearch->new($session{'CurrentUser'}); my ($ret, $msg) = $search->Load($privacy, $search_id); if ($ret) { my $searchargs = $search->GetParameter('args'); # Clean out ARGS and fill it in with the saved args from the # loaded search. foreach my $key (@metakeys) { $searchargs->{$key} = $ARGS{$key}; } %ARGS = %{$searchargs}; $CurrentSearch = "$privacy-SavedSearch-$search_id"; } else { push(@results, loc("Error: could not load saved search [_1]: [_2]", $ARGS{'LoadSavedSearch'}, $msg)); } } else { push(@results, loc("Invalid [_1] argument", 'LoadSavedSearch')); } } # ...or have we been asked to save, update, or delete a search? if ($ARGS{'Save'}) { my %searchargs = %ARGS; foreach my $key (@metakeys) { delete $searchargs{$key}; } $search = RT::SavedSearch->new($session{'CurrentUser'}); unless ($ARGS{'SearchPrivacy'} =~ /^(.*)-(\d+)$/) { # This shouldn't really happen, but hey. push(@results, loc("WARNING: Saving search to user-level privacy")); $ARGS{'SearchPrivacy'} = 'RT::User-'.$session{'CurrentUser'}->Id; } my ($ret, $msg) = $search->Save(Privacy => $ARGS{'SearchPrivacy'}, Type => 'Article', Name => $ARGS{'NewSearchName'}, SearchParams => {'args' => \%searchargs}); if ($ret) { $CurrentSearch = $ARGS{'SearchPrivacy'} . "-SavedSearch-" . $search->Id; push(@results, loc("Created search [_1]", $search->Name)); } else { undef $search; # if we bomb out creating a search # we don't want to have the empty object hang around push(@results, loc("Could not create search: [_1]", $msg)); } } elsif ($ARGS{'Update'}) { if ($ARGS{'SearchPrivacy'} != $search->Privacy) { push(@results, loc("Error: cannot change privacy value of existing search")); } else { my %searchargs = %ARGS; foreach my $key (@metakeys) { delete $searchargs{$key}; } # We already have a search loaded, because CurrentSearch is set, # or else we would not have gotten here. my ($ret, $msg) = $search->Update(Name => $ARGS{'NewSearchName'}, SearchParams => \%searchargs); if ($ret) { push(@results, loc("Search [_1] updated", $search->Name)); } else { push(@results, loc("Error: search [_1] not updated: [_2]", $search->Name, $msg)); } } } elsif ($ARGS{'Delete'}) { # Keep track of this, as we are about to delete the search. my $searchname = $search->Name; my ($ret, $msg) = $search->Delete; if ($ret) { $ARGS{'CurrentSearch'} = undef; push(@results, loc("Deleted search [_1]", $searchname)); # Get rid of all the state. foreach my $key (keys %ARGS) { delete $ARGS{$key}; } $CurrentSearch = 'new'; $search = undef; $RefersTo = undef; $ReferredToBy = undef; } else { push(@results, loc("Could not delete search [_1]: [_2]", $searchname, $msg)); } } # }}} # Don't want to search for a null class when there is no class specced my $customfields = RT::CustomFields->new( $session{'CurrentUser'} ); $customfields->LimitToLookupType(RT::FM::Article->new($session{'CurrentUser'})->CustomFieldLookupType); if ( $ARGS{'Class'} ) { my @Classes = ( ref $ARGS{'Class'} eq 'ARRAY' ) ? @{ $ARGS{'Class'} } : ( $ARGS{'Class'} ); foreach my $class (@Classes) { $customfields->LimitToGlobalOrObjectId($class); } } else { $customfields->LimitToGlobalOrObjectId(); } my %dates; foreach my $date qw(Created< Created> LastUpdated< LastUpdated>) { next unless ( $ARGS{$date} ); my $seconds = parsedate( $ARGS{$date}, FUZZY => 1, PREFER_PAST => 1 ); my $date_obj = RT::Date->new( $session{'CurrentUser'} ); $date_obj->Set( Format => 'unix', Value => $seconds ); $dates{$date} = $date_obj; if ( $date =~ /^(.*?)<$/i ) { $articles->Limit( FIELD => $1, OPERATOR => "<=", ENTRYAGGREGATOR => "AND", VALUE => $date_obj->ISO ); } if ( $date =~ /^(.*?)>$/i ) { $articles->Limit( FIELD => $1, OPERATOR => ">=", ENTRYAGGREGATOR => "AND", VALUE => $date_obj->ISO ); } } if ($RefersTo) { foreach my $link ( split ( /\s+/, $RefersTo ) ) { next unless ($link); $articles->LimitRefersTo($link); } } if ($ReferredToBy) { foreach my $link ( split ( /\s+/, $ReferredToBy ) ) { next unless ($link); $articles->LimitReferredToBy($link); } } if ($ARGS{'Topics'}) { my @Topics = ( ref $ARGS{'Topics'} eq 'ARRAY' ) ? @{ $ARGS{'Topics'} } : ( $ARGS{'Topics'} ); @Topics = map {split} @Topics; if ($ARGS{'ExpandTopics'}) { my %topics; while (@Topics) { my $id = shift @Topics; next if $topics{$id}; my $Topics = RT::FM::TopicCollection->new($session{'CurrentUser'}); $Topics->Limit(FIELD => 'Parent', VALUE => $id); push @Topics, $_->Id while $_ = $Topics->Next; $topics{$id}++; } @Topics = keys %topics; $ARGS{'Topics'} = \@Topics; } $articles->LimitTopics(@Topics); } my %cfs; while ( my $cf = $customfields->Next ) { $cfs{ $cf->Name } = $cf->Id; } # reset the iterator because we use this to build the UI $customfields->GotoFirstItem; foreach my $field ( keys %cfs ) { my @MatchLike = (ref $ARGS{ $field."~" } eq 'ARRAY' ) ? @{ $ARGS{ $field."~" } } : ( $ARGS{$field."~" } ); my @NoMatchLike = (ref $ARGS{ $field."!~" } eq 'ARRAY' ) ? @{ $ARGS{ $field."!~" } } : ( $ARGS{$field."!~" } ); my @Match = (ref $ARGS{ $field } eq 'ARRAY' ) ? @{ $ARGS{ $field } } : ( $ARGS{$field } ); my @NoMatch = (ref $ARGS{ $field."!" } eq 'ARRAY' ) ? @{ $ARGS{ $field."!" } } : ( $ARGS{$field."!" } ); foreach my $val (@MatchLike) { next unless $val; push @Match, "~".$val; } foreach my $val (@NoMatchLike) { next unless $val; push @NoMatch, "~".$val; } foreach my $value (@Match) { next unless $value; my $op; if ( $value =~ /^~(.*)$/ ) { $value = "%$1%"; $op = 'LIKE'; } else { $op = '='; } $articles->LimitCustomField( FIELD => $cfs{$field}, VALUE => $value, CASESENSITIVE => 0, ENTRYAGGREGATOR => 'OR', OPERATOR => $op ); } foreach my $value (@NoMatch) { next unless $value; my $op; if ( $value =~ /^~(.*)$/ ) { $value = "%$1%"; $op = 'NOT LIKE'; } else { $op = '!='; } $articles->LimitCustomField( FIELD => $cfs{$field}, VALUE => $value, CASESENSITIVE => 0, ENTRYAGGREGATOR => 'OR', OPERATOR => $op ); } } ### Searches for any field if ($ARGS{'Article~'}) { $articles->LimitCustomField( VALUE => $ARGS{'Article~'}, ENTRYAGGREGATOR => 'OR', OPERATOR => 'LIKE', CASESENSITIVE => 0, SUBCLAUSE => 'SearchAll' ); $articles->Limit( SUBCLAUSE => 'SearchAll', FIELD => "Name", VALUE => $ARGS{'Article~'}, ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 0, OPERATOR => 'LIKE' ); $articles->Limit( SUBCLAUSE => 'SearchAll', FIELD => "Summary", VALUE => $ARGS{'Article~'}, ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 0, OPERATOR => 'LIKE' ); } if ($ARGS{'Article!~'}) { $articles->LimitCustomField( VALUE => $ARGS{'Article!~'}, OPERATOR => 'NOT LIKE', CASESENSITIVE => 0, SUBCLAUSE => 'SearchAll' ); $articles->Limit( SUBCLAUSE => 'SearchAll', FIELD => "Name", VALUE => $ARGS{'Article!~'}, ENTRYAGGREGATOR => 'AND', CASESENSITIVE => 0, OPERATOR => 'NOT LIKE' ); $articles->Limit( SUBCLAUSE => 'SearchAll', FIELD => "Summary", VALUE => $ARGS{'Article!~'}, ENTRYAGGREGATOR => 'AND', CASESENSITIVE => 0, OPERATOR => 'NOT LIKE' ); } foreach my $field qw(Name Summary Class) { my @MatchLike = (ref $ARGS{ $field."~" } eq 'ARRAY' ) ? @{ $ARGS{ $field."~" } } : ( $ARGS{$field."~" } ); my @NoMatchLike = (ref $ARGS{ $field."!~" } eq 'ARRAY' ) ? @{ $ARGS{ $field."!~" } } : ( $ARGS{$field."!~" } ); my @Match = (ref $ARGS{ $field } eq 'ARRAY' ) ? @{ $ARGS{ $field } } : ( $ARGS{$field } ); my @NoMatch = (ref $ARGS{ $field."!" } eq 'ARRAY' ) ? @{ $ARGS{ $field."!" } } : ( $ARGS{$field."!" } ); foreach my $val (@MatchLike) { next unless $val; push @Match, "~".$val; } foreach my $val (@NoMatchLike) { next unless $val; push @NoMatch, "~".$val; } my $op; foreach my $value (@Match) { if ( $value && $value =~ /^~(.*)$/ ) { $value = "%$1%"; $op = 'LIKE'; } else { $op = '='; } # preprocess Classes, so we can search on class if ( $field eq 'Class' && $value ) { my $class = RT::FM::Class->new($RT::SystemUser); $class->Load($value); $value = $class->Id; } # now that we've pruned the value, get out if it's different. next unless $value; $articles->Limit( SUBCLAUSE => $field . 'Match', FIELD => $field, OPERATOR => $op, CASESENSITIVE => 0, VALUE => $value, ENTRYAGGREGATOR => 'OR' ); } foreach my $value (@NoMatch) { # preprocess Classes, so we can search on class if ( $value && $value =~ /^~(.*)/ ) { $value = "%$1%"; $op = 'NOT LIKE'; } else { $op = '!='; } if ( $field eq 'Class' ) { my $class = RT::FM::Class->new($RT::SystemUser); $class->Load($value); $value = $class->Id; } # now that we've pruned the value, get out if it's different. next unless $value; $articles->Limit( SUBCLAUSE => $field . 'NoMatch', OPERATOR => $op, VALUE => $value, CASESENSITIVE => 0, FIELD => $field, ENTRYAGGREGATOR => 'AND' ); } } if ( @OrderBy ) { if ( $OrderBy[0] && $OrderBy[0] =~ /\|/ ) { @OrderBy = split '|', @OrderBy; @Order = split '|', @Order; } my @tmp = map {{ FIELD => $OrderBy[$_], ORDER => $Order[$_] }} 0..$#OrderBy; $articles->OrderByCols( @tmp ); } $m->comp('/Elements/Callback', %ARGS, _Search => $articles); my %filtered = %ARGS; delete $filtered{$_} for (@metakeys, "EditTopics", "ExpandTopics"); delete $filtered{$_} for grep {$filtered{$_} !~ /\S/} keys %filtered; @filtered{qw(OrderBy Order)} = (\@OrderBy, \@Order); my $QueryString = "?".$m->comp('/Elements/QueryString', %filtered); <%ARGS> $CreatedBefore => '' $CreatedAfter => '' $LastUpdatedBefore => '' $LastUpdatedAfter => '' $RefersTo => undef $ReferredToBy => undef $CurrentSearch => 'new' @OrderBy => () @Order => ()