GetModels-Filtered: Unterstützung für benutzerdefinierte Variablen
[kivitendo-erp.git] / SL / Controller / Helper / ParseFilter.pm
index ea4eba0..759e146 100644 (file)
@@ -8,7 +8,9 @@ our @EXPORT = qw(parse_filter);
 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]) },
@@ -21,6 +23,7 @@ my %filters = (
 
 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.
@@ -32,13 +35,12 @@ my %methods = (
 sub parse_filter {
   my ($filter, %params) = @_;
 
-  my $hint_objects = $params{with_objects} || [];
-  my $auto_objects = [];
+  my $objects      = $params{with_objects} || [];
 
-  my ($flattened, $objects) = flatten($filter, $auto_objects, '', %params);
+  my ($flattened, $auto_objects) = flatten($filter, '', %params);
 
-  if ($params{class}) {
-    $objects = $hint_objects;
+  if (!$params{class}) {
+    _add_uniq($objects, $_) for @$auto_objects;
   }
 
   my $query = _parse_filter($flattened, $objects, %params);
@@ -69,19 +71,19 @@ sub _launder_keys {
 }
 
 sub flatten {
-  my ($filter, $with_objects, $prefix, %params) = @_;
+  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) = flatten($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;
     }
@@ -100,16 +102,45 @@ sub _parse_filter {
   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}, $with_objects, $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, $with_objects, $key, $value) = @_;
 
@@ -118,28 +149,30 @@ 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) {
+  # find first token which is not a relationship
+  my $i = 0;
+   while ($i < $#tokens) {
     eval {
-      $curr_class = $curr_class->meta->relationship($token)->class;
-      1;
+      $curr_class = $curr_class->meta->relationship($tokens[$_])->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);
-    _add_uniq($with_objects, $obj);
+    ($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);
+    _add_uniq($with_objects, $obj_path) if $obj_path;
   }
 
   return ($key, $value);
@@ -149,7 +182,7 @@ sub _add_uniq {
    my ($array, $what) = @_;
 
    $array //= [];
-   $array = [ uniq @$array, $what ];
+   @$array = (uniq @$array, listify($what));
 }
 
 sub _collapse_indirect_filters {
@@ -226,7 +259,7 @@ SL::Controller::Helper::ParseFilter - Convert a form filter spec into a RDBO get
 
 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',
@@ -234,7 +267,7 @@ customer. L<Rose::DB::Object> alloes you to search for these by filtering them p
     '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 {
@@ -266,7 +299,7 @@ specific L<Salesman>:
 
   [% L.select_tag('filter.salesman.id', ...) %]
 
-Additionally you can add modifier to the name to set a certain method:
+Additionally you can add modifier to the name to set a certain method:
 
   [% L.input_tag('filter.department.description:substr::ilike', ...) %]
 
@@ -282,7 +315,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) %]
@@ -295,7 +328,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<launder_to>  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 = {
@@ -351,7 +384,7 @@ will expand to:
     ]
   ]
 
-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 :)
 
@@ -383,6 +416,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 ::)
@@ -414,7 +452,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.
 
@@ -424,7 +462,7 @@ position in with_objects.
 
 =item *
 
-Additional filters shoud be pluggable.
+Additional filters should be pluggable.
 
 =back