X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FController%2FHelper%2FParseFilter.pm;h=0f7314179fcdddd9c31e84f5f6d738993f0cbe00;hb=53593baa211863fbf66540cf1bcc36c8fb37257f;hp=db59408436a5ebdc43b755062f97c1d7d2f45cf0;hpb=fe546cb027a1604ce2ba0f93b1bd241794fb66e4;p=kivitendo-erp.git diff --git a/SL/Controller/Helper/ParseFilter.pm b/SL/Controller/Helper/ParseFilter.pm index db5940843..0f7314179 100644 --- a/SL/Controller/Helper/ParseFilter.pm +++ b/SL/Controller/Helper/ParseFilter.pm @@ -8,16 +8,27 @@ our @EXPORT = qw(parse_filter); use DateTime; use SL::Helper::DateTime; use List::MoreUtils qw(uniq); +use SL::Util qw(trim); use SL::MoreCommon qw(listify); use Data::Dumper; +use Text::ParseWords; + +sub _lazy_bool_eq { + my ($key, $value) = @_; + + return () if ($value // '') eq ''; + return (or => [ $key => undef, $key => 0 ]) if !$value; + return ($key => 1); +} my %filters = ( date => sub { DateTime->from_lxoffice($_[0]) }, number => sub { $::form->parse_amount(\%::myconfig, $_[0]) }, percent => sub { $::form->parse_amount(\%::myconfig, $_[0]) / 100 }, - head => sub { $_[0] . '%' }, - tail => sub { '%' . $_[0] }, - substr => sub { '%' . $_[0] . '%' }, + head => sub { trim($_[0]) . '%' }, + tail => sub { '%' . trim($_[0]) }, + substr => sub { '%' . trim($_[0]) . '%' }, + trim => sub { trim($_[0]) }, ); my %methods = ( @@ -31,6 +42,10 @@ my %methods = ( } qw(similar match imatch regex regexp like ilike rlike is is_not ne eq lt gt le ge), ); +my %complex_methods = ( + lazy_bool_eq => \&_lazy_bool_eq, +); + sub parse_filter { my ($filter, %params) = @_; @@ -42,10 +57,10 @@ sub parse_filter { _add_uniq($objects, $_) for @$auto_objects; } - my $query = _parse_filter($flattened, $objects, %params); - _launder_keys($filter, $params{launder_to}) unless $params{no_launder}; + my $query = _parse_filter($flattened, $objects, %params); + return ($query && @$query ? (query => $query) : ()), ($objects && @$objects ? ( with_objects => [ uniq @$objects ]) : ()); @@ -98,19 +113,58 @@ sub _parse_filter { $flattened = _collapse_indirect_filters($flattened); + my $all_filters = { %filters, %{ $params{filters} || {} } }; + my $all_methods = { %methods, %{ $params{methods} || {} } }; + my $all_complex = { %complex_methods, %{ $params{complex_methods} || {} } }; + my @result; for (my $i = 0; $i < scalar @$flattened; $i += 2) { + my (@args, @filters, $method); + my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]); + my ($type, $op) = $key =~ m{:(.+)::(.+)}; - ($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}; + my $is_multi = $key =~ s/:multi//; + my $is_any = $key =~ s/:any//; + my @value_tokens = $is_multi || $is_any ? parse_line('\s+', 0, $value) : ($value); - push @result, $key, $value; + ($key, $method) = split m{::}, $key, 2; + ($key, @filters) = split m{:}, $key; + + my $orig_key = $key; + + for my $value_token (@value_tokens) { + $key = $orig_key; + + $value_token = _apply($value_token, $_, $all_filters) for @filters; + $value_token = _apply($value_token, $method, $all_methods) if $method && exists $all_methods->{$method}; + ($key, $value_token) = _apply_complex($key, $value_token, $method, $all_complex) if $method && exists $all_complex->{$method}; + ($key, $value_token) = _dispatch_custom_filters($params{class}, $with_objects, $key, $value_token) if $params{class}; + ($key, $value_token) = _apply_value_filters($key, $value_token, $type, $op); + + push @args, $key, $value_token; + } + + next unless defined $key; + + push @result, $is_multi ? (and => [ @args ]) : $is_any ? (or => [ @args ]) : @args; } 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, $with_objects, $key, $value) = @_; @@ -119,25 +173,38 @@ sub _dispatch_custom_filters { die 'unrecognized filters' if $key =~ /:/; my @tokens = split /\./, $key; - my $last_token = pop @tokens; my $curr_class = $class->object_class; - for my $token (@tokens) { + # our key will consist of dot-delimited tokens + # like this: order.part.unit.name + # each of these tokens except the last one is one of: + # - a relationship in the parent object + # - a custom filter + # + # the last token must be + # - a custom filter + # - a column in the parent object + # + # find first token which is not a relationship, + # so we can pass the rest on + my $i = 0; + while ($i < $#tokens) { eval { - $curr_class = $curr_class->meta->relationship($token)->class; - 1; + $curr_class = $curr_class->meta->relationship($tokens[$i])->class; + ++$i; } or do { - require Carp; - Carp::croak("Could not resolve the relationship '$token' in '$key' while building the filter request"); + last; } } my $manager = $curr_class->meta->convention_manager->auto_manager_class_name; - my $obj_path = join '.', @tokens; - my $obj_prefix = join '.', @tokens, ''; + my $obj_path = join '.', @tokens[0..$i-1]; + my $obj_prefix = join '.', @tokens[0..$i-1], ''; + my $key_token = $tokens[$i]; + my @additional_tokens = @tokens[$i+1..$#tokens]; if ($manager->can('filter')) { - ($key, $value, my $obj) = $manager->filter($last_token, $value, $obj_prefix); + ($key, $value, my $obj) = $manager->filter($key_token, $value, $obj_prefix, $obj_path, @additional_tokens); _add_uniq($with_objects, $obj) if $obj; } else { _add_uniq($with_objects, $obj_path) if $obj_path; @@ -196,20 +263,20 @@ sub _apply { return $filters->{$name}->($value); } -sub _apply_all { - my ($key, $value, $re, $subs) = @_; - - while ($key =~ s/$re//) { - $value = _apply($value, $1, $subs); - } - - return $key, $value; +sub _apply_complex { + my ($key, $value, $name, $filters) = @_; + return $key, $value unless $name && $filters->{$name}; + return $filters->{$name}->($key, $value); } 1; __END__ +=pod + +=encoding utf8 + =head1 NAME SL::Controller::Helper::ParseFilter - Convert a form filter spec into a RDBO get_all filter @@ -217,25 +284,25 @@ SL::Controller::Helper::ParseFilter - Convert a form filter spec into a RDBO get =head1 SYNOPSIS use SL::Controller::Helper::ParseFilter; - SL::DB::Object->get_all(parse_filter($::form->{filter})); + SL::DB::Manager::Object->get_all(parse_filter($::form->{filter})); # or more complex - SL::DB::Object->get_all(parse_filter($::form->{filter}, + SL::DB::Manager::Object->get_all(parse_filter($::form->{filter}, with_objects => [ qw(part customer) ])); =head1 DESCRIPTION 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 alloes you to search for these by filtering them prefixed with their table: +customer. L allows you to search for these by filtering them prefixed with their table: query => [ 'customer.name' => 'John Doe', - 'department.description' => [ ilike => '%Sales%' ], - 'orddate' => [ lt => DateTime->today ], + 'department.description' => { ilike => '%Sales%' }, + '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 { @@ -267,7 +334,7 @@ specific L: [% 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', ...) %] @@ -283,7 +350,7 @@ list of modifiers. =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) %] @@ -296,7 +363,7 @@ these will get copied into a _ suffixed version as hashes: 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 +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 = { @@ -352,7 +419,7 @@ will expand to: ] ] -For more abuot custom filters, see L. +For more about custom filters, see L. =head1 FILTERS (leading with :) @@ -372,22 +439,22 @@ Pasres the input string with C<< Form->parse_amount >> Parses the input string with C<< Form->parse_amount / 100 >> +=item trim + +Removes whitespace characters (to be precice, characters with the \p{WSpace} +property from beginning and end of the value. + =item head -Adds "%" at the end of the string. +Adds "%" at the end of the string and applies C. =item tail -Adds "%" at the end of the string. +Adds "%" at the end of the string and applies C. =item substr -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. +Adds "% .. %" around the search string and applies C. =back @@ -405,6 +472,18 @@ standard SQL C<=> operator. All these are recognized like the L methods. +=item lazy_bool_eq + +If the value is undefined or an empty string then this parameter will +be completely removed from the query. Otherwise a falsish filter value +will match for C and C; trueish values will only match +C. + +=item eq_ignore_empty + +Ignores this item if it's empty. Otherwise compares it with the +standard SQL C<=> operator. + =back =head1 BUGS AND CAVEATS @@ -420,7 +499,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 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. @@ -430,7 +509,7 @@ position in with_objects. =item * -Additional filters shoud be pluggable. +Additional filters should be pluggable. =back