Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / share / html / Articles / Article / Search.html
1 %# BEGIN BPS TAGGED BLOCK {{{
2 %#
3 %# COPYRIGHT:
4 %#
5 %# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
6 %#                                          <sales@bestpractical.com>
7 %#
8 %# (Except where explicitly superseded by other copyright notices)
9 %#
10 %#
11 %# LICENSE:
12 %#
13 %# This work is made available to you under the terms of Version 2 of
14 %# the GNU General Public License. A copy of that license should have
15 %# been provided with this software, but in any event can be snarfed
16 %# from www.gnu.org.
17 %#
18 %# This work is distributed in the hope that it will be useful, but
19 %# WITHOUT ANY WARRANTY; without even the implied warranty of
20 %# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 %# General Public License for more details.
22 %#
23 %# You should have received a copy of the GNU General Public License
24 %# along with this program; if not, write to the Free Software
25 %# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 %# 02110-1301 or visit their web page on the internet at
27 %# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 %#
29 %#
30 %# CONTRIBUTION SUBMISSION POLICY:
31 %#
32 %# (The following paragraph is not intended to limit the rights granted
33 %# to you to modify and distribute this software under the terms of
34 %# the GNU General Public License and is only of importance to you if
35 %# you choose to contribute your changes and enhancements to the
36 %# community by submitting them to Best Practical Solutions, LLC.)
37 %#
38 %# By intentionally submitting any modifications, corrections or
39 %# derivatives to this work, or any other work intended for use with
40 %# Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 %# you are the copyright holder for those contributions and you grant
42 %# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 %# royalty-free, perpetual, license to use, copy, create derivative
44 %# works based on those contributions, and sublicense and distribute
45 %# those contributions and any derivatives thereof.
46 %#
47 %# END BPS TAGGED BLOCK }}}
48 <& /Elements/Header, Title => loc("Search for articles") &>
49 <& /Elements/Tabs &>
50
51 % unless ( keys %ARGS ) {
52 <table width="100%" border="0">
53 <tr>
54 <td valign="top" width="50%">
55 % if (not $classes_configured) {
56 <& /Articles/Elements/NeedsSetup &>
57 % } elsif (not @classes) {
58 <i><&|/l&>You don't have permission to view Articles in any Class</&></i>
59 % } else {
60 <ul>
61 % for my $class (@classes) {
62 <li><a href="<%RT->Config->Get('WebPath')%>/Articles/Article/Search.html?<% $m->comp('/Elements/QueryString', %filtered, Class => $class->id) %>"><&|/l, $class->Name&>in class [_1]</&></a></li>
63 % }
64 </ul>
65 % }
66 </td>
67 <td valign="top" width="50%">
68 <form action="Search.html" method="get">
69 <& /Widgets/TitleBoxStart, title => loc('Saved searches') &>
70 <&|/l&>Load saved search:</&><br />
71 <& Elements/SelectSavedSearches, Name => 'LoadSavedSearch', Default => $CurrentSearch &>
72 <input value="<%loc('Load')%>" name="Load" type="submit" />
73 <& /Widgets/TitleBoxEnd &>
74 </form>
75 </td>
76 </tr>
77 </table>
78 %  return;
79 % }
80
81 <& /Elements/ListActions, actions => \@results &>
82 <div style="float:right;margin-top:15px"><a href="#criteria"><&|/l&>Modify this search...</&></a></div>
83
84 % if ($articles->BuildSelectCountQuery =~ /WHERE/i) {
85 <h2><&|/l&>Search results</&></h2>
86 <& /Elements/CollectionList,
87     Collection => $articles, 
88     AllowSorting => 1,
89     OrderBy => \@OrderBy,
90     Order => \@Order,
91     Format => $format,
92     GenericQueryArgs => { %filtered, Format => $format, },
93     &>
94 % }
95
96 <br />
97 <br />
98 <br />
99 <a name="criteria"></a>
100 <form action="Search.html" method="get">
101 <& Elements/ShowSearchCriteria, dates => \%dates, RefersTo => $RefersTo, customfields => $customfields,  ReferredToBy => $ReferredToBy, %ARGS &>
102 <br />
103 <br />
104 <& Elements/ShowSavedSearches, CurrentSearch => $CurrentSearch, 
105     Name => ($search ? $search->Name : undef),
106     Privacy => ($search ? $search->Privacy : undef) &>
107
108 </form>
109
110 <div align=right>
111 <a href="<%RT->Config->Get('WebPath')%>/Articles/Article/Search.html<%$QueryString%>"><&|/l&>Bookmarkable link for this search</&></a><br />
112 </div>
113 <%init>
114 my $Classes = RT::Classes->new($session{'CurrentUser'});
115 $Classes->LimitToEnabled();
116
117 # This is a COUNT(), which doesn't apply ACLs; as such, we don't display
118 # the warning if there are classes, but the user can't see them.
119 my $classes_configured = $Classes->Count;
120 my @classes = @{ $Classes->ItemsArrayRef };
121 $ARGS{Class} = $classes[0]->id if @classes == 1;
122
123 use RT::SavedSearch;
124 my @results;
125 my $articles = RT::Articles->new( $session{'CurrentUser'} );
126 my $format = q{
127     '<a href="__WebPath__/Articles/Article/Display.html?id=__id__">__id__</a>/TITLE:#',
128     '<a href="__WebPath__/Articles/Article/Display.html?id=__id__">__Name__</a>/TITLE:Name',
129     '__ClassName__',
130     '__CreatedRelative__',
131     '__LastUpdatedRelative__',
132     '__Summary__',
133     '__Topics__', };
134
135 # {{{ Quicksearch logic
136
137 # If it is a number, load the article with that ID.  Otherwise, search
138 # on name and summary.
139 if ($ARGS{'q'} && $ARGS{'q'} =~ /^(\d+)$/) {
140     return $m->comp("/Articles/Article/Display.html", id => $1);
141 }
142 # }}}
143
144 # {{{ Saved search logic
145
146 my $search;
147
148 # The keys in %ARGS that are not saved and loaded with named searches.
149 # These need to be treated specially.
150 my @metakeys = qw/NewSearchName CurrentSearch SearchPrivacy Save Load
151     Update Delete/;
152
153 if ($CurrentSearch =~ /^(.*-\d+)-SavedSearch-(\d+)$/) {
154     $search = RT::SavedSearch->new($session{'CurrentUser'});
155     $search->Load($1, $2);
156 }
157
158 # Have we been asked to load a search?
159
160 if ($ARGS{'Load'}) {
161     if ($ARGS{'LoadSavedSearch'} =~ /^(.*-\d+)-SavedSearch-(\d+)$/ ) {
162         my $privacy = $1;
163         my $search_id = $2;
164         
165         $search = RT::SavedSearch->new($session{'CurrentUser'});
166         my ($ret, $msg) = $search->Load($privacy, $search_id);
167         if ($ret) {
168             my $searchargs = $search->GetParameter('args');
169             # Clean out ARGS and fill it in with the saved args from the 
170             # loaded search.
171             foreach my $key (@metakeys) {
172                 $searchargs->{$key} = $ARGS{$key};
173             }
174             %ARGS = %{$searchargs};
175             $CurrentSearch = "$privacy-SavedSearch-$search_id";
176         } else {
177             push(@results, loc("Error: could not load saved search [_1]: [_2]",
178                                $ARGS{'LoadSavedSearch'}, $msg));
179         }
180     } else {
181         push(@results, loc("Invalid [_1] argument", 'LoadSavedSearch'));
182     }
183 }
184
185 # ...or have we been asked to save, update, or delete a search?
186
187 if ($ARGS{'Save'}) {
188     my %searchargs = %ARGS;
189     foreach my $key (@metakeys) {
190         delete $searchargs{$key};
191     }
192
193     $search = RT::SavedSearch->new($session{'CurrentUser'});
194     unless ($ARGS{'SearchPrivacy'} =~ /^(.*)-(\d+)$/) {
195         # This shouldn't really happen, but hey.
196         push(@results, loc("WARNING: Saving search to user-level privacy"));
197         $ARGS{'SearchPrivacy'} = 'RT::User-'.$session{'CurrentUser'}->Id;
198     }
199     my ($ret, $msg) = $search->Save(Privacy => $ARGS{'SearchPrivacy'},
200                                     Type => 'Article',
201                                     Name => $ARGS{'NewSearchName'},
202                                     SearchParams => {'args' => \%searchargs});
203     if ($ret) {
204         $CurrentSearch = $ARGS{'SearchPrivacy'} . "-SavedSearch-" .
205             $search->Id;
206         push(@results, loc("Created search [_1]", $search->Name));
207     } else {
208         undef $search; # if we bomb out creating a search
209                         # we don't want to have the empty object hang around
210         push(@results, loc("Could not create search: [_1]", $msg));
211     }
212 } elsif ($ARGS{'Update'}) {
213     if ($ARGS{'SearchPrivacy'} != $search->Privacy) {
214         push(@results,
215              loc("Error: cannot change privacy value of existing search"));
216     } else {
217         my %searchargs = %ARGS;
218         foreach my $key (@metakeys) {
219             delete $searchargs{$key};
220         }
221
222         # We already have a search loaded, because CurrentSearch is set,
223         # or else we would not have gotten here.
224         my ($ret, $msg) = $search->Update(Name => $ARGS{'NewSearchName'},
225                                           SearchParams => \%searchargs);
226         if ($ret) {
227             push(@results, loc("Search [_1] updated", $search->Name));
228         } else {
229             push(@results, loc("Error: search [_1] not updated: [_2]",
230                                $search->Name, $msg));
231         }
232     }
233 } elsif ($ARGS{'Delete'}) {
234     # Keep track of this, as we are about to delete the search.
235     my $searchname = $search->Name;
236     my ($ret, $msg) = $search->Delete;
237     if ($ret) {
238         $ARGS{'CurrentSearch'} = undef;
239         push(@results, loc("Deleted search [_1]", $searchname));
240         # Get rid of all the state.
241         foreach my $key (keys %ARGS) {
242             delete $ARGS{$key};
243         }
244         $CurrentSearch = 'new';
245         $search = undef;
246         $RefersTo = undef;
247         $ReferredToBy = undef;
248     } else {
249         push(@results, loc("Could not delete search [_1]: [_2]",
250              $searchname, $msg));
251     }
252 }
253
254 # }}}
255
256     # Don't want to search for a null class when there is no class specced
257 my $customfields = RT::CustomFields->new( $session{'CurrentUser'} );
258 my %dates;
259
260 $articles->Search( %ARGS, CustomFields => $customfields, Dates => \%dates,
261     OrderBy => \@OrderBy, Order => \@Order, );
262
263 $m->callback( %ARGS, _Search => $articles );
264
265 my %filtered = %ARGS;
266 delete $filtered{$_} for (@metakeys, "EditTopics", "ExpandTopics");
267 delete $filtered{$_} for grep {$filtered{$_} !~ /\S/} keys %filtered;
268 @filtered{qw(OrderBy Order)} = (\@OrderBy, \@Order);
269 my $QueryString = "?".$m->comp('/Elements/QueryString', %filtered);
270 </%init>
271
272 <%ARGS>
273 $CreatedBefore => ''
274 $CreatedAfter => ''
275 $LastUpdatedBefore => ''
276 $LastUpdatedAfter => ''
277 $RefersTo => undef
278 $ReferredToBy => undef
279 $CurrentSearch => 'new'
280 @OrderBy => ()
281 @Order   => ()
282 </%ARGS>