X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FController%2FHelper%2FParseFilter.pm;h=afe9b7b51b6d8b582f79e56ee12e48334f788fb9;hb=911756998b29079f7f566c6ef70b938f03a5dbb3;hp=ac086f594259f24ee9139cb8b486d9e6b65ebd9b;hpb=c8bdac22de4fbd63c974d76a23fe3393cdc551ed;p=kivitendo-erp.git diff --git a/SL/Controller/Helper/ParseFilter.pm b/SL/Controller/Helper/ParseFilter.pm index ac086f594..afe9b7b51 100644 --- a/SL/Controller/Helper/ParseFilter.pm +++ b/SL/Controller/Helper/ParseFilter.pm @@ -8,6 +8,7 @@ our @EXPORT = qw(parse_filter); use DateTime; use SL::Helper::DateTime; use List::MoreUtils qw(uniq); +use SL::MoreCommon qw(listify); use Data::Dumper; my %filters = ( @@ -20,21 +21,28 @@ my %filters = ( ); my %methods = ( - lt => sub { +{ lt => $_[0] } }, - gt => sub { +{ gt => $_[0] } }, - ilike => sub { +{ ilike => $_[0] } }, - like => sub { +{ like => $_[0] } }, 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. + my $_copy = "$_"; + $_ => sub { +{ $_copy => $_[0] } }, + } qw(similar match imatch regex regexp like ilike rlike is is_not ne eq lt gt le ge), ); 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}; @@ -53,7 +61,7 @@ sub _launder_keys { 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}); @@ -61,20 +69,20 @@ sub _launder_keys { }; } -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, $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; } @@ -84,25 +92,97 @@ sub _pre_parse { } sub _parse_filter { - my ($flattened, %params) = @_; + my ($flattened, $with_objects, %params) = @_; return () unless 'ARRAY' eq ref $flattened; - my %sorted = ( @$flattened ); + $flattened = _collapse_indirect_filters($flattened); - my @keys = sort { length($b) <=> length($a) } keys %sorted; - for my $key (@keys) { - next unless $key =~ /^(.*\b)::$/; - $sorted{$1 . '::' . delete $sorted{$key} } = delete $sorted{$1} if $sorted{$1} && $sorted{$key}; - } + my @result; + for (my $i = 0; $i < scalar @$flattened; $i += 2) { + my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]); - my %result; - while (my ($key, $value) = each %sorted) { ($key, $value) = _apply_all($key, $value, qr/\b:(\w+)/, { %filters, %{ $params{filters} || {} } }); ($key, $value) = _apply_all($key, $value, qr/\b::(\w+)/, { %methods, %{ $params{methods} || {} } }); - $result{$key} = $value; + ($key, $value) = _dispatch_custom_filters($params{class}, $with_objects, $key, $value) if $params{class}; + + push @result, $key, $value if defined $key; } - return [ %result ]; + return \@result; +} + +sub _dispatch_custom_filters { + my ($class, $with_objects, $key, $value) = @_; + + # the key should by now have no filters left + # if it has, catch it here: + die 'unrecognized filters' if $key =~ /:/; + + my @tokens = split /\./, $key; + my $last_token = pop @tokens; + my $curr_class = $class->object_class; + + for my $token (@tokens) { + eval { + $curr_class = $curr_class->meta->relationship($token)->class; + 1; + } or do { + require Carp; + Carp::croak("Could not resolve the relationship '$token' in '$key' while building the filter request"); + } + } + + 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, my $obj) = $manager->filter($last_token, $value, $obj_prefix); + _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) = @_; + + die 'flattened filter array length is uneven, should be possible to use as hash' if @$flattened % 2; + + my (%keys_to_delete, %keys_to_move, @collapsed); + + # search keys matching /::$/; + for (my $i = 0; $i < scalar @$flattened; $i += 2) { + my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]); + + next unless $key =~ /^(.*\b)::$/; + + $keys_to_delete{$key}++; + $keys_to_move{$1} = $1 . '::' . $value; + } + + for (my $i = 0; $i < scalar @$flattened; $i += 2) { + my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]); + + if ($keys_to_move{$key}) { + push @collapsed, $keys_to_move{$key}, $value; + next; + } + if (!$keys_to_delete{$key}) { + push @collapsed, $key, $value; + } + } + + return \@collapsed; } sub _prefix { @@ -198,19 +278,6 @@ As a rule all value filters require a single colon and must be placed before match method suffixes, which are appended with 2 colons. See below for a full list of modifiers. -The reason for the method being last is that it is possible to specify the -method in another input. Suppose you want a date input and a separate -before/after/equal select, you can use the following: - - [% L.date_tag('filter.appointed_date:date', ... ) %] - -and later - - [% L.select_tag('filter.appointed_date::', ... ) %] - -The special empty method will be used to set the method for the previous -method-less input. - =back =head1 LAUNDERING @@ -221,15 +288,21 @@ 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 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: +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 @@ -241,9 +314,46 @@ like this: $filter = { 'price_number__lt' => '2,30', - 'closed => '1', + closed => '1', + 'type_' => { part => 1, assembly => 1 }, } +=head1 INDIRECT FILTER METHODS + +The reason for the method being last is that it is possible to specify the +method in another input. Suppose you want a date input and a separate +before/after/equal select, you can use the following: + + [% L.date_tag('filter.appointed_date:date', ... ) %] + +and later + + [% L.select_tag('filter.appointed_date:date::', ... ) %] + +The special empty method will be used to set the method for the previous +method-less input. + +=head1 CUSTOM FILTERS FROM OBJECTS + +If the L call contains a parameter C, custom filters will +be honored. Suppose you have added a custom filter 'all' for parts which +expands to search both description and partnumber, the following + + $filter = { + 'part.all:substr::ilike' => 'A1', + } + +will expand to: + + query => [ + or => [ + part.description => { ilike => '%A1%' }, + part.partnumber => { ilike => '%A1%' }, + ] + ] + +For more abuot custom filters, see L. + =head1 FILTERS (leading with :) The following filters are built in, and can be used. @@ -274,6 +384,11 @@ Adds "%" at the end of the string. 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 ::) @@ -305,7 +420,7 @@ following will not work as you expect: L.input_tag('customer.name:substr::ilike', ...) L.input_tag('invoice.customer.name:substr::ilike', ...) -This will sarch for orders whoe invoice has the _same_ customer, which matches +This will sarch 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.