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 lt => sub { +{ lt => $_[0] } },
24 gt => sub { +{ gt => $_[0] } },
25 ilike => sub { +{ ilike => $_[0] } },
26 like => sub { +{ like => $_[0] } },
27 enable => sub { ;;;; },
31 my ($filter, %params) = @_;
33 my $hint_objects = $params{with_objects} || [];
35 my ($flattened, $objects) = _pre_parse($filter, $hint_objects, '', %params);
37 my $query = _parse_filter($flattened, %params);
39 _launder_keys($filter, $params{launder_to}) unless $params{no_launder};
42 ($query && @$query ? (query => $query) : ()),
43 ($objects && @$objects ? ( with_objects => [ uniq @$objects ]) : ());
47 my ($filter, $launder_to) = @_;
48 $launder_to ||= $filter;
49 return unless ref $filter eq 'HASH';
50 for my $key (keys %$filter) {
53 if ('' eq ref $filter->{$orig}) {
54 $launder_to->{$key} = $filter->{$orig};
55 } elsif ('ARRAY' eq ref $filter->{$orig}) {
56 $launder_to->{$key} = [ @{ $filter->{$orig} } ];
58 $launder_to->{$key} ||= { };
59 _launder_keys($filter->{$key}, $launder_to->{$key});
65 my ($filter, $with_objects, $prefix, %params) = @_;
67 return (undef, $with_objects) unless 'HASH' eq ref $filter;
72 while (my ($key, $value) = each %$filter) {
73 next if !defined $value || $value eq ''; # 0 is fine
74 if ('HASH' eq ref $value) {
75 my ($query, $more_objects) = _pre_parse($value, $with_objects, _prefix($prefix, $key));
76 push @result, @$query if $query;
77 push @$with_objects, $key, ($more_objects ? @$more_objects : ());
79 push @result, _prefix($prefix, $key) => $value;
83 return \@result, $with_objects;
87 my ($flattened, %params) = @_;
89 return () unless 'ARRAY' eq ref $flattened;
91 my %sorted = ( @$flattened );
93 my @keys = sort { length($b) <=> length($a) } keys %sorted;
95 next unless $key =~ /^(.*\b)::$/;
96 $sorted{$1 . '::' . delete $sorted{$key} } = delete $sorted{$1} if $sorted{$1} && $sorted{$key};
100 while (my ($key, $value) = each %sorted) {
101 ($key, $value) = _apply_all($key, $value, qr/\b:(\w+)/, { %filters, %{ $params{filters} || {} } });
102 ($key, $value) = _apply_all($key, $value, qr/\b::(\w+)/, { %methods, %{ $params{methods} || {} } });
103 $result{$key} = $value;
109 join '.', grep $_, @_;
113 my ($value, $name, $filters) = @_;
114 return $value unless $name && $filters->{$name};
115 return [ map { _apply($_, $name, $filters) } @$value ] if 'ARRAY' eq ref $value;
116 return $filters->{$name}->($value);
120 my ($key, $value, $re, $subs) = @_;
122 while ($key =~ s/$re//) {
123 $value = _apply($value, $1, $subs);
135 SL::Controller::Helper::ParseFilter - Convert a form filter spec into a RDBO get_all filter
139 use SL::Controller::Helper::ParseFilter;
140 SL::DB::Object->get_all(parse_filter($::form->{filter}));
143 SL::DB::Object->get_all(parse_filter($::form->{filter},
144 with_objects => [ qw(part customer) ]));
148 A search filter will usually search for things in relations of the actual
149 search target. A search for sales orders may be filtered by the name of the
150 customer. L<Rose::DB::Object> alloes you to search for these by filtering them prefixed with their table:
153 'customer.name' => 'John Doe',
154 'department.description' => [ ilike => '%Sales%' ],
155 'orddate' => [ lt => DateTime->today ],
158 Unfortunately, if you specify them in you form as these strings, the form
159 parser will convert them into nested structures like this:
169 And the substring match requires you to recognize the ilike, and modify the value.
171 C<parse_filter> tries to ease this by recognizing these structures and
172 providing suffixes for common search patterns.
178 =item C<parse_filter \%FILTER, [ %PARAMS ]>
180 First argument is the filter from form. It is highly recommended that you put
181 all filter attributes into a named container as to not confuse them with the
184 Nested structures will be parsed and interpreted as foreign references. For
185 example if you search for L<Order>s, this input will search for those with a
186 specific L<Salesman>:
188 [% L.select_tag('filter.salesman.id', ...) %]
190 Additionally you can add modifier to the name to set a certain method:
192 [% L.input_tag('filter.department.description:substr::ilike', ...) %]
194 This will add the "% .. %" wildcards for substr matching in SQL, and add an
195 C<< ilike => $value >> block around it to match case insensitively.
197 As a rule all value filters require a single colon and must be placed before
198 match method suffixes, which are appended with 2 colons. See below for a full
201 The reason for the method being last is that it is possible to specify the
202 method in another input. Suppose you want a date input and a separate
203 before/after/equal select, you can use the following:
205 [% L.date_tag('filter.appointed_date:date', ... ) %]
209 [% L.select_tag('filter.appointed_date::', ... ) %]
211 The special empty method will be used to set the method for the previous
218 Unfortunately Template cannot parse the postfixes if you want to
219 rerender the filter. For this reason all colons filter keys are by
220 default laundered into underscores, so you can use them like this:
222 [% L.input_tag('filter.price:number::lt', filter.price_number__lt) %]
224 All of your original entries will stay intactg. If you don't want this to
225 happen pass C<< no_launder => 1 >> as a parameter. Additionally you can pass a
226 different target for the laundered values with the C<launder_to> parameter. It
227 takes an hashref and will deep copy all values in your filter to the target. So
228 if you have a filter that looks liek this:
231 'price:number::lt' => '2,30',
237 parse_filter($filter, launder_to => $laundered_filter = { })
239 the original filter will be unchanged, and C<$laundered_filter> will end up
243 'price_number__lt' => '2,30',
247 =head1 FILTERS (leading with :)
249 The following filters are built in, and can be used.
255 Parses the input string with C<< DateTime->from_lxoffice >>
259 Pasres the input string with C<< Form->parse_amount >>
263 Parses the input string with C<< Form->parse_amount / 100 >>
267 Adds "%" at the end of the string.
271 Adds "%" at the end of the string.
275 Adds "% .. %" around the search string.
279 =head2 METHODS (leading with ::)
291 All these are recognized like the L<Rose::DB::Object> methods.
295 =head1 BUGS AND CAVEATS
297 This will not properly handle multiple versions of the same object in different
300 Suppose you want all L<SL::DB::Order>s which have either themselves a certain
301 customer, or are linked to a L<SL::DB::Invoice> with this customer, the
302 following will not work as you expect:
305 L.input_tag('customer.name:substr::ilike', ...)
306 L.input_tag('invoice.customer.name:substr::ilike', ...)
308 This will sarch for orders whoe invoice has the _same_ customer, which matches
309 both inputs. This is because tables are aliased by their name and not by their
310 position in with_objects.
318 Additional filters shoud be pluggable.
324 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>