ParseFilter auf Objektdispatch erweitert
authorSven Schöling <s.schoeling@linet-services.de>
Fri, 24 May 2013 17:14:07 +0000 (19:14 +0200)
committerSven Schöling <s.schoeling@linet-services.de>
Mon, 27 May 2013 17:46:51 +0000 (19:46 +0200)
SL/Controller/Helper/ParseFilter.pm
t/controllers/helpers/parse_filter.t

index 5a1c93c..8ccd834 100644 (file)
@@ -95,13 +95,46 @@ sub _parse_filter {
   my @result;
   for (my $i = 0; $i < scalar @$flattened; $i += 2) {
     my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]);
+
     ($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;
   }
   return \@result;
 }
 
+sub _dispatch_custom_filters {
+  my ($class, $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;
+
+  if ($manager->can('filter')) {
+    ($key, $value) = $manager->filter($last_token, $value, join '.', @tokens, '');
+  }
+
+  return ($key, $value);
+}
+
 sub _collapse_indirect_filters {
   my ($flattened) = @_;
 
@@ -227,19 +260,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
@@ -273,6 +293,42 @@ like this:
     'closed            => '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<parse_filter> call contains a parameter C<class>, 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<SL::DB::Helper::Filtered>.
+
 =head1 FILTERS (leading with :)
 
 The following filters are built in, and can be used.
@@ -334,7 +390,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.
 
index 1bc3a00..9bcbeb8 100644 (file)
@@ -1,12 +1,14 @@
 use lib 't';
 
-use Test::More tests => 18;
+use Test::More tests => 23;
 use Test::Deep;
 use Data::Dumper;
 
 use_ok 'Support::TestSetup';
 use_ok 'SL::Controller::Helper::ParseFilter';
 
+use SL::DB::OrderItem;
+
 undef *::any; # Test::Deep exports any (for junctions) and MoreCommon exports any (like in List::Moreutils)
 
 Support::TestSetup::login();
@@ -190,3 +192,58 @@ test {
   with_objects => bag('order.customer', 'order'),
 }, 'sub objects have to retain their prefix';
 
+### class filter dispatch
+#
+test {
+  name => 'Test',
+  whut => 'moof',
+}, {
+  query => bag(
+    name => 'Test',
+    whut => 'moof'
+  ),
+}, 'object test simple', class => 'SL::DB::Manager::Part';
+
+test {
+  'type' => 'assembly',
+}, {
+  query => [
+    'assembly' => 1
+  ],
+}, 'object test without prefix', class => 'SL::DB::Manager::Part';
+
+test {
+  'part.type' => 'assembly',
+}, {
+  query => [
+    'part.assembly' => 1
+  ],
+}, 'object test with prefix', class => 'SL::DB::Manager::OrderItem';
+
+test {
+  'type' => [ 'part', 'assembly' ],
+}, {
+  query => [
+    or => [
+     and => [ or => [ assembly => 0, assembly => undef ],
+              "!inventory_accno_id" => 0,
+              "!inventory_accno_id" => undef,
+     ],
+     assembly => 1,
+    ]
+  ],
+}, 'object test without prefix but complex value', class => 'SL::DB::Manager::Part';
+
+test {
+  'part.type' => [ 'part', 'assembly' ],
+}, {
+  query => [
+    or => [
+     and => [ or => [ 'part.assembly' => 0, 'part.assembly' => undef ],
+              "!part.inventory_accno_id" => 0,
+              "!part.inventory_accno_id" => undef,
+     ],
+     'part.assembly' => 1,
+    ]
+  ],
+}, 'object test with prefix but complex value', class => 'SL::DB::Manager::OrderItem';