]>
Commit | Line | Data |
---|---|---|
84fb5b46 MKG |
1 | # BEGIN BPS TAGGED BLOCK {{{ |
2 | # | |
3 | # COPYRIGHT: | |
4 | # | |
320f0092 | 5 | # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC |
84fb5b46 MKG |
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 | ||
49 | package RT::Interface::Web::QueryBuilder::Tree; | |
50 | ||
51 | use strict; | |
52 | use warnings; | |
53 | ||
54 | use Tree::Simple qw/use_weak_refs/; | |
55 | use base qw/Tree::Simple/; | |
56 | ||
57 | =head1 NAME | |
58 | ||
59 | RT::Interface::Web::QueryBuilder::Tree - subclass of Tree::Simple used in Query Builder | |
60 | ||
61 | =head1 DESCRIPTION | |
62 | ||
63 | This class provides support functionality for the Query Builder (Search/Build.html). | |
64 | It is a subclass of L<Tree::Simple>. | |
65 | ||
66 | =head1 METHODS | |
67 | ||
68 | =head2 TraversePrePost PREFUNC POSTFUNC | |
69 | ||
70 | Traverses the tree depth-first. Before processing the node's children, | |
71 | calls PREFUNC with the node as its argument; after processing all of the | |
72 | children, calls POSTFUNC with the node as its argument. | |
73 | ||
74 | (Note that unlike Tree::Simple's C<traverse>, it actually calls its functions | |
75 | on the root node passed to it.) | |
76 | ||
77 | =cut | |
78 | ||
79 | sub TraversePrePost { | |
80 | my ($self, $prefunc, $postfunc) = @_; | |
81 | ||
82 | # XXX: if pre or post action changes siblings (delete or adds) | |
83 | # we could have problems | |
84 | $prefunc->($self) if $prefunc; | |
85 | ||
86 | foreach my $child ($self->getAllChildren()) { | |
87 | $child->TraversePrePost($prefunc, $postfunc); | |
88 | } | |
89 | ||
90 | $postfunc->($self) if $postfunc; | |
91 | } | |
92 | ||
93 | =head2 GetReferencedQueues | |
94 | ||
b5747ff2 MKG |
95 | Returns a hash reference; each queue referenced with an '=' operation |
96 | will appear as a key whose value is 1. | |
84fb5b46 MKG |
97 | |
98 | =cut | |
99 | ||
100 | sub GetReferencedQueues { | |
101 | my $self = shift; | |
102 | ||
103 | my $queues = {}; | |
104 | ||
105 | $self->traverse( | |
106 | sub { | |
107 | my $node = shift; | |
108 | ||
109 | return if $node->isRoot; | |
110 | return unless $node->isLeaf; | |
111 | ||
112 | my $clause = $node->getNodeValue(); | |
b5747ff2 MKG |
113 | return unless $clause->{Key} eq 'Queue'; |
114 | return unless $clause->{Op} eq '='; | |
84fb5b46 | 115 | |
af59614d | 116 | $queues->{ $clause->{RawValue} } = 1; |
84fb5b46 MKG |
117 | } |
118 | ); | |
119 | ||
120 | return $queues; | |
121 | } | |
122 | ||
123 | =head2 GetQueryAndOptionList SELECTED_NODES | |
124 | ||
125 | Given an array reference of tree nodes that have been selected by the user, | |
126 | traverses the tree and returns the equivalent SQL query and a list of hashes | |
127 | representing the "clauses" select option list. Each has contains the keys | |
128 | TEXT, INDEX, SELECTED, and DEPTH. TEXT is the displayed text of the option | |
129 | (including parentheses, not including indentation); INDEX is the 0-based | |
130 | index of the option in the list (also used as its CGI parameter); SELECTED | |
131 | is either 'SELECTED' or '', depending on whether the node corresponding | |
132 | to the select option was in the SELECTED_NODES list; and DEPTH is the | |
133 | level of indentation for the option. | |
134 | ||
135 | =cut | |
136 | ||
137 | sub GetQueryAndOptionList { | |
138 | my $self = shift; | |
139 | my $selected_nodes = shift; | |
140 | ||
141 | my $list = $self->__LinearizeTree; | |
142 | foreach my $e( @$list ) { | |
143 | $e->{'DEPTH'} = $e->{'NODE'}->getDepth; | |
144 | $e->{'SELECTED'} = (grep $_ == $e->{'NODE'}, @$selected_nodes)? qq[ selected="selected"] : ''; | |
145 | } | |
146 | ||
147 | return (join ' ', map $_->{'TEXT'}, @$list), $list; | |
148 | } | |
149 | ||
150 | =head2 PruneChildLessAggregators | |
151 | ||
152 | If tree manipulation has left it in a state where there are ANDs, ORs, | |
153 | or parenthesizations with no children, get rid of them. | |
154 | ||
155 | =cut | |
156 | ||
157 | sub PruneChildlessAggregators { | |
158 | my $self = shift; | |
159 | ||
160 | $self->TraversePrePost( | |
161 | undef, | |
162 | sub { | |
163 | my $node = shift; | |
164 | return unless $node->isLeaf; | |
165 | ||
166 | # We're only looking for aggregators (AND/OR) | |
167 | return if ref $node->getNodeValue; | |
168 | ||
169 | return if $node->isRoot; | |
170 | ||
171 | # OK, this is a childless aggregator. Remove self. | |
172 | $node->getParent->removeChild($node); | |
173 | $node->DESTROY; | |
174 | } | |
175 | ); | |
176 | } | |
177 | ||
178 | =head2 GetDisplayedNodes | |
179 | ||
180 | This function returns a list of the nodes of the tree in depth-first | |
181 | order which correspond to options in the "clauses" multi-select box. | |
182 | In fact, it's all of them but the root and its child. | |
183 | ||
184 | =cut | |
185 | ||
186 | sub GetDisplayedNodes { | |
187 | return map $_->{NODE}, @{ (shift)->__LinearizeTree }; | |
188 | } | |
189 | ||
190 | ||
191 | sub __LinearizeTree { | |
192 | my $self = shift; | |
193 | ||
194 | my ($list, $i) = ([], 0); | |
195 | ||
196 | $self->TraversePrePost( sub { | |
197 | my $node = shift; | |
198 | return if $node->isRoot; | |
199 | ||
200 | my $str = ''; | |
201 | if( $node->getIndex > 0 ) { | |
202 | $str .= " ". $node->getParent->getNodeValue ." "; | |
203 | } | |
204 | ||
205 | unless( $node->isLeaf ) { | |
206 | $str .= '( '; | |
207 | } else { | |
208 | ||
209 | my $clause = $node->getNodeValue; | |
210 | $str .= $clause->{Key}; | |
211 | $str .= " ". $clause->{Op}; | |
212 | $str .= " ". $clause->{Value}; | |
213 | ||
214 | } | |
215 | $str =~ s/^\s+|\s+$//; | |
216 | ||
217 | push @$list, { | |
218 | NODE => $node, | |
219 | TEXT => $str, | |
220 | INDEX => $i, | |
221 | }; | |
222 | ||
223 | $i++; | |
224 | }, sub { | |
225 | my $node = shift; | |
226 | return if $node->isRoot; | |
227 | return if $node->isLeaf; | |
228 | $list->[-1]->{'TEXT'} .= ' )'; | |
229 | }); | |
230 | ||
231 | return $list; | |
232 | } | |
233 | ||
234 | sub ParseSQL { | |
235 | my $self = shift; | |
236 | my %args = ( | |
237 | Query => '', | |
238 | CurrentUser => '', #XXX: Hack | |
239 | @_ | |
240 | ); | |
241 | my $string = $args{'Query'}; | |
242 | ||
243 | my @results; | |
244 | ||
245 | my %field = %{ RT::Tickets->new( $args{'CurrentUser'} )->FIELDS }; | |
246 | my %lcfield = map { ( lc($_) => $_ ) } keys %field; | |
247 | ||
248 | my $node = $self; | |
249 | ||
250 | my %callback; | |
251 | $callback{'OpenParen'} = sub { | |
252 | $node = __PACKAGE__->new( 'AND', $node ); | |
253 | }; | |
254 | $callback{'CloseParen'} = sub { $node = $node->getParent }; | |
255 | $callback{'EntryAggregator'} = sub { $node->setNodeValue( $_[0] ) }; | |
256 | $callback{'Condition'} = sub { | |
257 | my ($key, $op, $value) = @_; | |
af59614d | 258 | my $rawvalue = $value; |
84fb5b46 MKG |
259 | |
260 | my ($main_key) = split /[.]/, $key; | |
261 | ||
262 | my $class; | |
263 | if ( exists $lcfield{ lc $main_key } ) { | |
264 | $key =~ s/^[^.]+/ $lcfield{ lc $main_key } /e; | |
265 | ($main_key) = split /[.]/, $key; # make the case right | |
266 | $class = $field{ $main_key }->[0]; | |
267 | } | |
268 | unless( $class ) { | |
269 | push @results, [ $args{'CurrentUser'}->loc("Unknown field: [_1]", $key), -1 ] | |
270 | } | |
271 | ||
272 | if ( lc $op eq 'is' || lc $op eq 'is not' ) { | |
273 | $value = 'NULL'; # just fix possible mistakes here | |
274 | } elsif ( $value !~ /^[+-]?[0-9]+$/ ) { | |
275 | $value =~ s/(['\\])/\\$1/g; | |
276 | $value = "'$value'"; | |
277 | } | |
278 | ||
279 | if ($key =~ s/(['\\])/\\$1/g or $key =~ /[^{}\w\.]/) { | |
280 | $key = "'$key'"; | |
281 | } | |
282 | ||
af59614d | 283 | my $clause = { Key => $key, Op => $op, Value => $value, RawValue => $rawvalue }; |
84fb5b46 MKG |
284 | $node->addChild( __PACKAGE__->new( $clause ) ); |
285 | }; | |
286 | $callback{'Error'} = sub { push @results, @_ }; | |
287 | ||
288 | require RT::SQL; | |
289 | RT::SQL::Parse($string, \%callback); | |
290 | return @results; | |
291 | } | |
292 | ||
293 | RT::Base->_ImportOverlays(); | |
294 | ||
295 | 1; |