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) unless $params{no_launder};
42 ($query && @$query ? (query => $query) : ()),
43 ($objects && @$objects ? ( with_objects => [ uniq @$objects ]) : ());
48 return unless ref $filter eq 'HASH';
49 my @keys = keys %$filter;
53 $filter->{$key} = $filter->{$orig};
54 _launder_keys($filter->{$key});
61 my ($filter, $with_objects, $prefix, %params) = @_;
63 return () unless 'HASH' eq ref $filter;
68 while (my ($key, $value) = each %$filter) {
69 next if !defined $value || $value eq ''; # 0 is fine
70 if ('HASH' eq ref $value) {
71 my ($query, $more_objects) = _pre_parse($value, $with_objects, _prefix($prefix, $key));
72 push @result, @$query if $query;
73 push @$with_objects, $key, ($more_objects ? @$more_objects : ());
75 push @result, _prefix($prefix, $key) => $value;
79 return \@result, $with_objects;
83 my ($flattened, %params) = @_;
85 return () unless 'ARRAY' eq ref $flattened;
87 my %sorted = ( @$flattened );
89 my @keys = sort { length($b) <=> length($a) } keys %sorted;
91 next unless $key =~ /^(.*\b)::$/;
92 $sorted{$1 . '::' . delete $sorted{$key} } = delete $sorted{$1} if $sorted{$1} && $sorted{$key};
96 while (my ($key, $value) = each %sorted) {
97 ($key, $value) = _apply_all($key, $value, qr/\b:(\w+)/, { %filters, %{ $params{filters} || {} } });
98 ($key, $value) = _apply_all($key, $value, qr/\b::(\w+)/, { %methods, %{ $params{methods} || {} } });
99 $result{$key} = $value;
105 join '.', grep $_, @_;
109 my ($value, $name, $filters) = @_;
110 return $value unless $name && $filters->{$name};
111 return [ map { _apply($_, $name, $filters) } @$value ] if 'ARRAY' eq ref $value;
112 return $filters->{$name}->($value);
116 my ($key, $value, $re, $subs) = @_;
118 while ($key =~ s/$re//) {
119 $value = _apply($value, $1, $subs);
131 SL::Controller::Helper::ParseFilter - Convert a form filter spec into a RDBO get_all filter
135 use SL::Controller::Helper::ParseFilter;
136 SL::DB::Object->get_all(parse_filter($::form->{filter}));
139 SL::DB::Object->get_all(parse_filter($::form->{filter},
140 with_objects => [ qw(part customer) ]));
144 A search filter will usually search for things in relations of the actual
145 search target. A search for sales orders may be filtered by the name of the
146 customer. L<Rose::DB::Object> alloes you to search for these by filtering them prefixed with their table:
149 'customer.name' => 'John Doe',
150 'department.description' => [ ilike => '%Sales%' ],
151 'orddate' => [ lt => DateTime->today ],
154 Unfortunately, if you specify them in you form as these strings, the form
155 parser will convert them into nested structures like this:
165 And the substring match requires you to recognize the ilike, and modify the value.
167 C<parse_filter> tries to ease this by recognizing these structures and
168 providing suffixes for common search patterns.
174 =item C<parse_filter \%FILTER, [ %PARAMS ]>
176 First argument is the filter from form. It is highly recommended that you put
177 all filter attributes into a named container as to not confuse them with the
180 Nested structures will be parsed and interpreted as foreign references. For
181 example if you search for L<Order>s, this input will search for those with a
182 specific L<Salesman>:
184 [% L.select_tag('filter.salesman.id', ...) %]
186 Additionally you can add modifier to the name to set a certain method:
188 [% L.input_tag('filter.department.description:substr::ilike', ...) %]
190 This will add the "% .. %" wildcards for substr matching in SQL, and add an
191 C<< ilike => $value >> block around it to match case insensitively.
193 As a rule all value filters require a single colon and must be placed before
194 match method suffixes, which are appended with 2 colons. See below for a full
197 The reason for the method being last is that it is possible to specify the
198 method in another input. Suppose you want a date input and a separate
199 before/after/equal select, you can use the following:
201 [% L.date_tag('filter.appointed_date:date', ... ) %]
205 [% L.select_tag('filter.appointed_date::', ... ) %]
207 The special empty method will be used to set the method for the previous
210 =item Laundering filter
212 Unfortunately Template cannot parse the postfixes if you want to
213 rerender the filter. For this reason all colons filter keys are by
214 default laundered into underscores. If you don't want this to happen
215 pass C<< no_launder => 1 >> as a parameter. A full select_tag then
218 [% L.input_tag('filter.price:number::lt', filter.price_number__lt) %]
223 =head1 FILTERS (leading with :)
225 The following filters are built in, and can be used.
231 Parses the input string with C<< DateTime->from_lxoffice >>
235 Pasres the input string with C<< Form->parse_amount >>
239 Parses the input string with C<< Form->parse_amount / 100 >>
243 Adds "%" at the end of the string.
247 Adds "%" at the end of the string.
251 Adds "% .. %" around the search string.
255 =head2 METHODS (leading with ::)
267 All these are recognized like the L<Rose::DB::Object> methods.
271 =head1 BUGS AND CAVEATS
273 This will not properly handle multiple versions of the same object in different
276 Suppose you want all L<SL::DB::Order>s which have either themselves a certain
277 customer, or are linked to a L<SL::DB::Invoice> with this customer, the
278 following will not work as you expect:
281 L.input_tag('customer.name:substr::ilike', ...)
282 L.input_tag('invoice.customer.name:substr::ilike', ...)
284 This will sarch for orders whoe invoice has the _same_ customer, which matches
285 both inputs. This is because tables are aliased by their name and not by their
286 position in with_objects.
294 Additional filters shoud be pluggable.
300 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>