Merge branch 'b-3.6.1' of ../kivitendo-erp_20220811
[kivitendo-erp.git] / SL / Controller / Helper / ParseFilter.pm
index cdd3eb7..0f73141 100644 (file)
@@ -8,17 +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 = (
@@ -32,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) = @_;
 
@@ -99,31 +113,41 @@ 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{:(.+)::(.+)};
 
-    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);
+    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);
+
+    ($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;
     }
 
-    push @result, $key, $value if defined $key;
+    next unless defined $key;
+
+    push @result, $is_multi ? (and => [ @args ]) : $is_any ? (or => [ @args ]) : @args;
   }
   return \@result;
 }
@@ -239,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
@@ -260,10 +284,10 @@ 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
@@ -274,8 +298,8 @@ customer. L<Rose::DB::Object> allows you to search for these by filtering them p
 
   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 your form as these strings, the form
@@ -415,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<trim>.
 
 =item tail
 
-Adds "%" at the end of the string.
+Adds "%" at the end of the string and applies C<trim>.
 
 =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<trim>.
 
 =back
 
@@ -448,6 +472,18 @@ standard SQL C<=> operator.
 
 All these are recognized like the L<Rose::DB::Object> 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<NULL> and C<FALSE>; trueish values will only match
+C<TRUE>.
+
+=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