1 package SL::Controller::Helper::ParseFilter;
5 use Exporter qw(import);
6 our @EXPORT = qw(parse_filter);
9 use SL::Helper::DateTime;
10 use List::MoreUtils qw(uniq);
14 date => sub { DateTime->from_lxoffice($_[0]) },
15 number => sub { $::form->parse_amount(\%::myconfig, $_[0]) },
16 percent => sub { $::form->parse_amount(\%::myconfig, $_[0]) / 100 },
17 head => sub { $_[0] . '%' },
18 tail => sub { '%' . $_[0] },
19 substr => sub { '%' . $_[0] . '%' },
23 enable => sub { ;;;; },
25 # since $_ is an alias it can't be used in a closure. even "".$_ or "$_"
26 # does not work, we need a real copy.
28 $_ => sub { +{ $_copy => $_[0] } },
29 } qw(similar match imatch regex regexp like ilike rlike is is_not ne eq lt gt le ge),
33 my ($filter, %params) = @_;
35 my $hint_objects = $params{with_objects} || [];
37 my ($flattened, $objects) = _pre_parse($filter, $hint_objects, '', %params);
39 my $query = _parse_filter($flattened, %params);
41 _launder_keys($filter, $params{launder_to}) unless $params{no_launder};
44 ($query && @$query ? (query => $query) : ()),
45 ($objects && @$objects ? ( with_objects => [ uniq @$objects ]) : ());
49 my ($filter, $launder_to) = @_;
50 $launder_to ||= $filter;
51 return unless ref $filter eq 'HASH';
52 for my $key (keys %$filter) {
55 if ('' eq ref $filter->{$orig}) {
56 $launder_to->{$key} = $filter->{$orig};
57 } elsif ('ARRAY' eq ref $filter->{$orig}) {
58 $launder_to->{$key} = [ @{ $filter->{$orig} } ];
60 $launder_to->{$key} ||= { };
61 _launder_keys($filter->{$key}, $launder_to->{$key});
67 my ($filter, $with_objects, $prefix, %params) = @_;
69 return (undef, $with_objects) unless 'HASH' eq ref $filter;
74 while (my ($key, $value) = each %$filter) {
75 next if !defined $value || $value eq ''; # 0 is fine
76 if ('HASH' eq ref $value) {
77 my ($query, $more_objects) = _pre_parse($value, $with_objects, _prefix($prefix, $key));
78 push @result, @$query if $query;
79 push @$with_objects, _prefix($prefix, $key), ($more_objects ? @$more_objects : ());
81 push @result, _prefix($prefix, $key) => $value;
85 return \@result, $with_objects;
89 my ($flattened, %params) = @_;
91 return () unless 'ARRAY' eq ref $flattened;
93 $flattened = _collapse_indirect_filters($flattened);
96 for (my $i = 0; $i < scalar @$flattened; $i += 2) {
97 my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]);
98 ($key, $value) = _apply_all($key, $value, qr/\b:(\w+)/, { %filters, %{ $params{filters} || {} } });
99 ($key, $value) = _apply_all($key, $value, qr/\b::(\w+)/, { %methods, %{ $params{methods} || {} } });
100 push @result, $key, $value;
105 sub _collapse_indirect_filters {
106 my ($flattened) = @_;
108 die 'flattened filter array length is uneven, should be possible to use as hash' if @$flattened % 2;
110 my (%keys_to_delete, %keys_to_move, @collapsed);
112 # search keys matching /::$/;
113 for (my $i = 0; $i < scalar @$flattened; $i += 2) {
114 my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]);
116 next unless $key =~ /^(.*\b)::$/;
118 $keys_to_delete{$key}++;
119 $keys_to_move{$1} = $1 . '::' . $value;
122 for (my $i = 0; $i < scalar @$flattened; $i += 2) {
123 my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]);
125 if ($keys_to_move{$key}) {
126 push @collapsed, $keys_to_move{$key}, $value;
129 if (!$keys_to_delete{$key}) {
130 push @collapsed, $key, $value;
138 join '.', grep $_, @_;
142 my ($value, $name, $filters) = @_;
143 return $value unless $name && $filters->{$name};
144 return [ map { _apply($_, $name, $filters) } @$value ] if 'ARRAY' eq ref $value;
145 return $filters->{$name}->($value);
149 my ($key, $value, $re, $subs) = @_;
151 while ($key =~ s/$re//) {
152 $value = _apply($value, $1, $subs);
164 SL::Controller::Helper::ParseFilter - Convert a form filter spec into a RDBO get_all filter
168 use SL::Controller::Helper::ParseFilter;
169 SL::DB::Object->get_all(parse_filter($::form->{filter}));
172 SL::DB::Object->get_all(parse_filter($::form->{filter},
173 with_objects => [ qw(part customer) ]));
177 A search filter will usually search for things in relations of the actual
178 search target. A search for sales orders may be filtered by the name of the
179 customer. L<Rose::DB::Object> alloes you to search for these by filtering them prefixed with their table:
182 'customer.name' => 'John Doe',
183 'department.description' => [ ilike => '%Sales%' ],
184 'orddate' => [ lt => DateTime->today ],
187 Unfortunately, if you specify them in you form as these strings, the form
188 parser will convert them into nested structures like this:
198 And the substring match requires you to recognize the ilike, and modify the value.
200 C<parse_filter> tries to ease this by recognizing these structures and
201 providing suffixes for common search patterns.
207 =item C<parse_filter \%FILTER, [ %PARAMS ]>
209 First argument is the filter from form. It is highly recommended that you put
210 all filter attributes into a named container as to not confuse them with the
213 Nested structures will be parsed and interpreted as foreign references. For
214 example if you search for L<Order>s, this input will search for those with a
215 specific L<Salesman>:
217 [% L.select_tag('filter.salesman.id', ...) %]
219 Additionally you can add modifier to the name to set a certain method:
221 [% L.input_tag('filter.department.description:substr::ilike', ...) %]
223 This will add the "% .. %" wildcards for substr matching in SQL, and add an
224 C<< ilike => $value >> block around it to match case insensitively.
226 As a rule all value filters require a single colon and must be placed before
227 match method suffixes, which are appended with 2 colons. See below for a full
230 The reason for the method being last is that it is possible to specify the
231 method in another input. Suppose you want a date input and a separate
232 before/after/equal select, you can use the following:
234 [% L.date_tag('filter.appointed_date:date', ... ) %]
238 [% L.select_tag('filter.appointed_date::', ... ) %]
240 The special empty method will be used to set the method for the previous
247 Unfortunately Template cannot parse the postfixes if you want to
248 rerender the filter. For this reason all colons filter keys are by
249 default laundered into underscores, so you can use them like this:
251 [% L.input_tag('filter.price:number::lt', filter.price_number__lt) %]
253 All of your original entries will stay intactg. If you don't want this to
254 happen pass C<< no_launder => 1 >> as a parameter. Additionally you can pass a
255 different target for the laundered values with the C<launder_to> parameter. It
256 takes an hashref and will deep copy all values in your filter to the target. So
257 if you have a filter that looks liek this:
260 'price:number::lt' => '2,30',
266 parse_filter($filter, launder_to => $laundered_filter = { })
268 the original filter will be unchanged, and C<$laundered_filter> will end up
272 'price_number__lt' => '2,30',
276 =head1 FILTERS (leading with :)
278 The following filters are built in, and can be used.
284 Parses the input string with C<< DateTime->from_lxoffice >>
288 Pasres the input string with C<< Form->parse_amount >>
292 Parses the input string with C<< Form->parse_amount / 100 >>
296 Adds "%" at the end of the string.
300 Adds "%" at the end of the string.
304 Adds "% .. %" around the search string.
308 =head2 METHODS (leading with ::)
320 All these are recognized like the L<Rose::DB::Object> methods.
324 =head1 BUGS AND CAVEATS
326 This will not properly handle multiple versions of the same object in different
329 Suppose you want all L<SL::DB::Order>s which have either themselves a certain
330 customer, or are linked to a L<SL::DB::Invoice> with this customer, the
331 following will not work as you expect:
334 L.input_tag('customer.name:substr::ilike', ...)
335 L.input_tag('invoice.customer.name:substr::ilike', ...)
337 This will sarch for orders whoe invoice has the _same_ customer, which matches
338 both inputs. This is because tables are aliased by their name and not by their
339 position in with_objects.
347 Additional filters shoud be pluggable.
353 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>