use DateTime;
use SL::Helper::DateTime;
use List::MoreUtils qw(uniq);
+use SL::MoreCommon qw(listify);
use Data::Dumper;
+use Text::ParseWords;
my %filters = (
date => sub { DateTime->from_lxoffice($_[0]) },
my %methods = (
enable => sub { ;;;; },
+ eq_ignore_empty => sub { ($_[0] // '') eq '' ? () : +{ eq => $_[0] } },
map {
# since $_ is an alias it can't be used in a closure. even "".$_ or "$_"
# does not work, we need a real copy.
sub parse_filter {
my ($filter, %params) = @_;
- my $hint_objects = $params{with_objects} || [];
+ my $objects = $params{with_objects} || [];
- my ($flattened, $objects) = _pre_parse($filter, $hint_objects, '', %params);
+ my ($flattened, $auto_objects) = flatten($filter, '', %params);
- my $query = _parse_filter($flattened, %params);
+ if (!$params{class}) {
+ _add_uniq($objects, $_) for @$auto_objects;
+ }
+
+ my $query = _parse_filter($flattened, $objects, %params);
_launder_keys($filter, $params{launder_to}) unless $params{no_launder};
if ('' eq ref $filter->{$orig}) {
$launder_to->{$key} = $filter->{$orig};
} elsif ('ARRAY' eq ref $filter->{$orig}) {
- $launder_to->{$key} = [ @{ $filter->{$orig} } ];
+ $launder_to->{"${key}_"} = { map { $_ => 1 } @{ $filter->{$orig} } };
} else {
$launder_to->{$key} ||= { };
_launder_keys($filter->{$key}, $launder_to->{$key});
};
}
-sub _pre_parse {
- my ($filter, $with_objects, $prefix, %params) = @_;
+sub flatten {
+ my ($filter, $prefix, %params) = @_;
- return (undef, $with_objects) unless 'HASH' eq ref $filter;
- $with_objects ||= [];
+ return (undef, []) unless 'HASH' eq ref $filter;
+ my $with_objects = [];
my @result;
while (my ($key, $value) = each %$filter) {
next if !defined $value || $value eq ''; # 0 is fine
if ('HASH' eq ref $value) {
- my ($query, $more_objects) = _pre_parse($value, $with_objects, _prefix($prefix, $key));
- push @result, @$query if $query;
- push @$with_objects, _prefix($prefix, $key), ($more_objects ? @$more_objects : ());
+ my ($query, $more_objects) = flatten($value, _prefix($prefix, $key));
+ push @result, @$query if $query;
+ _add_uniq($with_objects, $_) for _prefix($prefix, $key), @$more_objects;
} else {
push @result, _prefix($prefix, $key) => $value;
}
}
sub _parse_filter {
- my ($flattened, %params) = @_;
+ my ($flattened, $with_objects, %params) = @_;
return () unless 'ARRAY' eq ref $flattened;
my @result;
for (my $i = 0; $i < scalar @$flattened; $i += 2) {
my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]);
+ my ($type, $op) = $key =~ m{:(.+)::(.+)};
+
+ if ($key =~ s/:multi//) {
+ my @multi;
+ my $orig_key = $key;
+ for my $value (parse_line('\s+', 0, $value)) {
+ ($key, $value) = _apply_all($key, $value, qr/\b:(\w+)/, { %filters, %{ $params{filters} || {} } });
+ ($key, $value) = _apply_all($key, $value, qr/\b::(\w+)/, { %methods, %{ $params{methods} || {} } });
+ ($key, $value) = _dispatch_custom_filters($params{class}, $with_objects, $key, $value) if $params{class};
+ ($key, $value) = _apply_value_filters($key, $value, $type, $op);
+ push @multi, $key, $value;
+ $key = $orig_key;
+ }
+ ($key, $value) = (and => \@multi);
+ } else {
+ ($key, $value) = _apply_all($key, $value, qr/\b:(\w+)/, { %filters, %{ $params{filters} || {} } });
+ ($key, $value) = _apply_all($key, $value, qr/\b::(\w+)/, { %methods, %{ $params{methods} || {} } });
+ ($key, $value) = _dispatch_custom_filters($params{class}, $with_objects, $key, $value) if $params{class};
+ ($key, $value) = _apply_value_filters($key, $value, $type, $op);
+ }
- ($key, $value) = _apply_all($key, $value, qr/\b:(\w+)/, { %filters, %{ $params{filters} || {} } });
- ($key, $value) = _apply_all($key, $value, qr/\b::(\w+)/, { %methods, %{ $params{methods} || {} } });
- ($key, $value) = _dispatch_custom_filters($params{class}, $key, $value) if $params{class};
-
- push @result, $key, $value;
+ push @result, $key, $value if defined $key;
}
return \@result;
}
+sub _apply_value_filters {
+ my ($key, $value, $type, $op) = @_;
+
+ return ($key, $value) unless $key && $value && $type && $op && (ref($value) eq 'HASH');
+
+ if (($type eq 'date') && ($op eq 'le')) {
+ my $date = delete $value->{le};
+ $value->{lt} = $date->add(days => 1);
+ }
+
+ return ($key, $value);
+}
+
sub _dispatch_custom_filters {
- my ($class, $key, $value) = @_;
+ my ($class, $with_objects, $key, $value) = @_;
# the key should by now have no filters left
# if it has, catch it here:
}
}
- my $manager = $curr_class->meta->convention_manager->auto_manager_class_name;
+ my $manager = $curr_class->meta->convention_manager->auto_manager_class_name;
+ my $obj_path = join '.', @tokens;
+ my $obj_prefix = join '.', @tokens, '';
if ($manager->can('filter')) {
- ($key, $value) = $manager->filter($last_token, $value, join '.', @tokens, '');
+ ($key, $value, my $obj) = $manager->filter($last_token, $value, $obj_prefix, $obj_path);
+ _add_uniq($with_objects, $obj) if $obj;
+ } else {
+ _add_uniq($with_objects, $obj_path) if $obj_path;
}
return ($key, $value);
}
+sub _add_uniq {
+ my ($array, $what) = @_;
+
+ $array //= [];
+ @$array = (uniq @$array, listify($what));
+}
+
sub _collapse_indirect_filters {
my ($flattened) = @_;
A search filter will usually search for things in relations of the actual
search target. A search for sales orders may be filtered by the name of the
-customer. L<Rose::DB::Object> alloes you to search for these by filtering them prefixed with their table:
+customer. L<Rose::DB::Object> allows you to search for these by filtering them prefixed with their table:
query => [
'customer.name' => 'John Doe',
'orddate' => [ lt => DateTime->today ],
]
-Unfortunately, if you specify them in you form as these strings, the form
+Unfortunately, if you specify them in your form as these strings, the form
parser will convert them into nested structures like this:
$::form = bless {
[% L.select_tag('filter.salesman.id', ...) %]
-Additionally you can add modifier to the name to set a certain method:
+Additionally you can add a modifier to the name to set a certain method:
[% L.input_tag('filter.department.description:substr::ilike', ...) %]
=head1 LAUNDERING
Unfortunately Template cannot parse the postfixes if you want to
-rerender the filter. For this reason all colons filter keys are by
+rerender the filter. For this reason all colon filter keys are by
default laundered into underscores, so you can use them like this:
[% L.input_tag('filter.price:number::lt', filter.price_number__lt) %]
-All of your original entries will stay intactg. If you don't want this to
+Also Template has trouble when looking up the contents of arrays, so
+these will get copied into a _ suffixed version as hashes:
+
+ [% L.checkbox_tag('filter.ids[]', value=15, checked=filter.ids_.15) %]
+
+All of your original entries will stay intact. If you don't want this to
happen pass C<< no_launder => 1 >> as a parameter. Additionally you can pass a
different target for the laundered values with the C<launder_to> parameter. It
-takes an hashref and will deep copy all values in your filter to the target. So
-if you have a filter that looks liek this:
+takes a hashref and will deep copy all values in your filter to the target. So
+if you have a filter that looks like this:
$filter = {
'price:number::lt' => '2,30',
- 'closed => '1',
+ closed => '1',
+ type => [ 'part', 'assembly' ],
}
and parse it with
$filter = {
'price_number__lt' => '2,30',
- 'closed => '1',
+ closed => '1',
+ 'type_' => { part => 1, assembly => 1 },
}
=head1 INDIRECT FILTER METHODS
]
]
-For more abuot custom filters, see L<SL::DB::Helper::Filtered>.
+For more about custom filters, see L<SL::DB::Helper::Filtered>.
=head1 FILTERS (leading with :)
Adds "% .. %" around the search string.
+=item eq_ignore_empty
+
+Ignores this item if it's empty. Otherwise compares it with the
+standard SQL C<=> operator.
+
=back
=head2 METHODS (leading with ::)
L.input_tag('customer.name:substr::ilike', ...)
L.input_tag('invoice.customer.name:substr::ilike', ...)
-This will sarch for orders whose invoice has the _same_ customer, which matches
+This will search for orders whose invoice has the _same_ customer, which matches
both inputs. This is because tables are aliased by their name and not by their
position in with_objects.
=item *
-Additional filters shoud be pluggable.
+Additional filters should be pluggable.
=back