From d820c1162bb08a580dfb4d01800d0406b001e169 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Sven=20Sch=C3=B6ling?= Date: Mon, 27 May 2013 19:43:26 +0200 Subject: [PATCH] =?utf8?q?Filtered=20Plugin=20f=C3=BCr=20GetModels?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- SL/Controller/Helper/Filtered.pm | 310 ++++++++++++++++++++++++++++ SL/Controller/Helper/GetModels.pm | 3 +- SL/Controller/Helper/Paginated.pm | 2 + SL/Controller/Helper/ParseFilter.pm | 3 +- SL/DB/Helper/Filtered.pm | 186 +++++++++++++++++ 5 files changed, 501 insertions(+), 3 deletions(-) create mode 100644 SL/Controller/Helper/Filtered.pm create mode 100644 SL/DB/Helper/Filtered.pm diff --git a/SL/Controller/Helper/Filtered.pm b/SL/Controller/Helper/Filtered.pm new file mode 100644 index 000000000..597a04e04 --- /dev/null +++ b/SL/Controller/Helper/Filtered.pm @@ -0,0 +1,310 @@ +package SL::Controller::Helper::Filtered; + +use strict; + +use Exporter qw(import); +use SL::Controller::Helper::ParseFilter (); +use List::MoreUtils qw(uniq); +our @EXPORT = qw(make_filtered get_filter_spec get_current_filter_params disable_filtering _save_current_filter_params _callback_handler_for_filtered _get_models_handler_for_filtered); + +use constant PRIV => '__filteredhelper_priv'; + +my %controller_filter_spec; + +sub make_filtered { + my ($class, %specs) = @_; + + $specs{MODEL} //= $class->controller_name; + $specs{MODEL} =~ s{ ^ SL::DB:: (?: .* :: )? }{}x; + $specs{FORM_PARAMS} //= 'filter'; + $specs{LAUNDER_TO} = '__INPLACE__' unless exists $specs{LAUNDER_TO}; + $specs{ONLY} //= []; + $specs{ONLY} = [ $specs{ONLY} ] if !ref $specs{ONLY}; + $specs{ONLY_MAP} = @{ $specs{ONLY} } ? { map { ($_ => 1) } @{ $specs{ONLY} } } : { '__ALL__' => 1 }; + + $controller_filter_spec{$class} = \%specs; + + my %hook_params = @{ $specs{ONLY} } ? ( only => $specs{ONLY} ) : (); + $class->run_before('_save_current_filter_params', %hook_params); + + SL::Controller::Helper::GetModels::register_get_models_handlers( + $class, + callback => '_callback_handler_for_filtered', + get_models => '_get_models_handler_for_filtered', + ONLY => $specs{ONLY}, + ); + + # $::lxdebug->dump(0, "CONSPEC", \%specs); +} + +sub get_filter_spec { + my ($class_or_self) = @_; + + return $controller_filter_spec{ref($class_or_self) || $class_or_self}; +} + +sub get_current_filter_params { + my ($self) = @_; + + return %{ _priv($self)->{filter_params} } if _priv($self)->{filter_params}; + + require Carp; + Carp::confess('It seems a GetModels plugin tries to access filter params before they got calculated. Make sure your make_filtered call comes first.'); +} + +sub _make_current_filter_params { + my ($self, %params) = @_; + + my $spec = $self->get_filter_spec; + my $filter = $params{filter} // _priv($self)->{filter} // {}, + my %filter_args = _get_filter_args($self, $spec); + my %parse_filter_args = ( + class => "SL::DB::Manager::$spec->{MODEL}", + with_objects => $params{with_objects}, + ); + my $laundered; + if ($spec->{LAUNDER_TO} eq '__INPLACE__') { + + } elsif ($spec->{LAUNDER_TO}) { + $laundered = {}; + $parse_filter_args{launder_to} = $laundered; + } else { + $parse_filter_args{no_launder} = 1; + } + + my %calculated_params = SL::Controller::Helper::ParseFilter::parse_filter($filter, %parse_filter_args); + + $calculated_params{query} = [ + @{ $calculated_params{query} || [] }, + @{ $filter_args{query} || [] }, + @{ $params{query} || [] }, + ]; + + $calculated_params{with_objects} = [ + uniq + @{ $calculated_params{with_objects} || [] }, + @{ $filter_args{with_objects} || [] }, + @{ $params{with_objects} || [] }, + ]; + + if ($laundered) { + if ($self->can($spec->{LAUNDER_TO})) { + $self->${\ $spec->{LAUNDER_TO} }($laundered); + } else { + $self->{$spec->{LAUNDER_TO}} = $laundered; + } + } + + # $::lxdebug->dump(0, "get_current_filter_params: ", \%calculated_params); + + _priv($self)->{filter_params} = \%calculated_params; + + return %calculated_params; +} + +sub disable_filtering { + my ($self) = @_; + _priv($self)->{disabled} = 1; +} + +# +# private functions +# + +sub _get_filter_args { + my ($self, $spec) = @_; + + $spec ||= $self->get_filter_spec; + + my %filter_args = ref($spec->{FILTER_ARGS}) eq 'CODE' ? %{ $spec->{FILTER_ARGS}->($self) } + : $spec->{FILTER_ARGS} ? do { my $sub = $spec->{FILTER_ARGS}; %{ $self->$sub() } } + : (); +} + +sub _save_current_filter_params { + my ($self) = @_; + + return if !_is_enabled($self); + + my $filter_spec = $self->get_filter_spec; + $self->{PRIV()}{filter} = $::form->{ $filter_spec->{FORM_PARAMS} }; + + # $::lxdebug->message(0, "saving current filter params to " . $self->{PRIV()}->{page} . ' / ' . $self->{PRIV()}->{per_page}); +} + +sub _callback_handler_for_filtered { + my ($self, %params) = @_; + my $priv = _priv($self); + + if (_is_enabled($self) && $priv->{filter}) { + my $filter_spec = $self->get_filter_spec; + my ($flattened) = SL::Controller::Helper::ParseFilter::flatten($priv->{filter}, undef, $filter_spec->{FORM_PARAMS}); + %params = (%params, @$flattened); + } + + # $::lxdebug->dump(0, "CB handler for filtered; params after flatten:", \%params); + + return %params; +} + +sub _get_models_handler_for_filtered { + my ($self, %params) = @_; + my $spec = $self->get_filter_spec; + + # $::lxdebug->dump(0, "params in get_models_for_filtered", \%params); + + my %filter_params; + %filter_params = _make_current_filter_params($self, %params) if _is_enabled($self); + + # $::lxdebug->dump(0, "GM handler for filtered; params nach modif (is_enabled? " . _is_enabled($self) . ")", \%params); + + return (%params, %filter_params); +} + +sub _priv { + my ($self) = @_; + $self->{PRIV()} ||= {}; + return $self->{PRIV()}; +} + +sub _is_enabled { + my ($self) = @_; + return !_priv($self)->{disabled} && ($self->get_filter_spec->{ONLY_MAP}->{$self->action_name} || $self->get_filter_spec->{ONLY_MAP}->{'__ALL__'}); +} + + +1; + +__END__ + +=pod + +=encoding utf8 + +=head1 NAME + +SL::Controller::Helper::Filtered - A helper for semi-automatic handling +of filtered lists of database models in a controller + +=head1 SYNOPSIS + +In a controller: + + use SL::Controller::Helper::GetModels; + use SL::Controller::Helper::Filtered; + + __PACKAGE__->make_filter( + MODEL => 'Part', + ONLY => [ qw(list) ], + FORM_PARAMS => [ qw(filter) ], + ); + + sub action_list { + my ($self) = @_; + + my $filtered_models = $self->get_models(%addition_filters); + $self->render('controller/list', ENTRIES => $filtered_models); + } + + +=head1 OVERVIEW + +This helper module enables use of the L +methods in conjunction with the L style of +plugins. Additional filters can be defined in the database models and filtering +can be reduced to a minimum of work. + +This plugin can be combined with L and +L for filtered, sorted and paginated lists. + +The controller has to provive information where to look for filter information +at compile time. This call is L. + +The underlying functionality that enables the use of more than just +the paginate helper is provided by the controller helper +C. See the documentation for L for +more information on it. + +=head1 PACKAGE FUNCTIONS + +=over 4 + +=item C + +This function must be called by a controller at compile time. It is +uesd to set the various parameters required for this helper to do its +magic. + +Careful: If you want to use this in conjunction with +L, you need to call C first, +or the paginating will not get all the relevant information to estimate the +number of pages correctly. To ensure this does not happen, this module will +croak when it detects such a scenario. + +The hash C<%filter_spec> can include the following parameters: + +=over 4 + +=item * C + +Optional. A string: the name of the Rose database model that is used +as a default in certain cases. If this parameter is missing then it is +derived from the controller's package (e.g. for the controller +C the C would default to +C). + +=item * C + +Optional. Indicates a key in E<$::form> to be used as filter. + +Defaults to the values C if missing. + +=item * C + +Option. Indicates a target for laundered filter arguments in the controller. +Can be set to C to disable laundering, and can be set to method named or +hash keys of the controller. In the latter case the laundered structure will be +put there. + +Defaults to inplace laundering which is not normally settable. + +=item * C + +Optional. An array reference containing a list of action names for +which the paginate parameters should be saved. If missing or empty then +all actions invoked on the controller are monitored. + +=back + +=back + +=head1 INSTANCE FUNCTIONS + +These functions are called on a controller instance. + +=over 4 + +=item C + +Returns a hash to be used in manager C calls or to be passed on to +GetModels. Will only work if the get_models chain has been called at least +once, because only then the full parameters can get parsed and stored. Will +croak otherwise. + +=item C + +Disable filtering for the duration of the current action. Can be used +when using the attribute C to L does not +cover all cases. + +=back + +=head1 BUGS + +Nothing here yet. + +=head1 AUTHOR + +Sven Schöling Es.schoeling@linet-services.deE + +=cut diff --git a/SL/Controller/Helper/GetModels.pm b/SL/Controller/Helper/GetModels.pm index e35365717..6b408b3f8 100644 --- a/SL/Controller/Helper/GetModels.pm +++ b/SL/Controller/Helper/GetModels.pm @@ -48,9 +48,8 @@ sub get_callback { sub get_models { my ($self, %override_params) = @_; - my %default_params = _run_handlers($self, 'get_models'); + my %params = _run_handlers($self, 'get_models', %override_params); - my %params = (%default_params, %override_params); my $model = delete($params{model}) || die "No 'model' to work on"; return "SL::DB::Manager::${model}"->get_all(%params); diff --git a/SL/Controller/Helper/Paginated.pm b/SL/Controller/Helper/Paginated.pm index 15997b8e9..8d572ecbd 100644 --- a/SL/Controller/Helper/Paginated.pm +++ b/SL/Controller/Helper/Paginated.pm @@ -18,6 +18,7 @@ sub make_paginated { $specs{MODEL} =~ s{ ^ SL::DB:: (?: .* :: )? }{}x; $specs{PER_PAGE} ||= "SL::DB::Manager::$specs{MODEL}"->default_objects_per_page; $specs{FORM_PARAMS} ||= [ qw(page per_page) ]; + $specs{PAGINATE_ARGS} ||= '__FILTER__'; $specs{ONLY} ||= []; $specs{ONLY} = [ $specs{ONLY} ] if !ref $specs{ONLY}; $specs{ONLY_MAP} = @{ $specs{ONLY} } ? { map { ($_ => 1) } @{ $specs{ONLY} } } : { '__ALL__' => 1 }; @@ -58,6 +59,7 @@ sub get_current_paginate_params { ); my %paginate_args = ref($spec->{PAGINATE_ARGS}) eq 'CODE' ? %{ $spec->{PAGINATE_ARGS}->($self) } + : $spec->{PAGINATE_ARGS} eq '__FILTER__' ? $self->get_current_filter_params : $spec->{PAGINATE_ARGS} ? do { my $sub = $spec->{PAGINATE_ARGS}; %{ $self->$sub() } } : (); my $calculated_params = "SL::DB::Manager::$spec->{MODEL}"->paginate(%paginate_params, args => \%paginate_args); diff --git a/SL/Controller/Helper/ParseFilter.pm b/SL/Controller/Helper/ParseFilter.pm index ea4eba0a0..ee5a740d1 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 = ( @@ -149,7 +150,7 @@ sub _add_uniq { my ($array, $what) = @_; $array //= []; - $array = [ uniq @$array, $what ]; + $array = [ uniq @$array, listify($what) ]; } sub _collapse_indirect_filters { diff --git a/SL/DB/Helper/Filtered.pm b/SL/DB/Helper/Filtered.pm new file mode 100644 index 000000000..15e098527 --- /dev/null +++ b/SL/DB/Helper/Filtered.pm @@ -0,0 +1,186 @@ +package SL::DB::Helper::Filtered; + +use strict; +use SL::Controller::Helper::ParseFilter (); + +require Exporter; +our @ISA = qw(Exporter); +our @EXPORT = qw (filter add_filter_specs); + +my %filter_spec; + +sub filter { + my ($class, $key, $value, $prefix) = @_; + + my $filters = _get_filters($class); + + return ($key, $value) unless $filters->{$key}; + + return $filters->{$key}->($key, $value, $prefix); +} + +sub _get_filters { + my ($class) = @_; + return $filter_spec{$class} ||= {}; +} + +sub add_filter_specs { + my $class = shift; + + my $filters = _get_filters($class); + + while (@_ > 1) { + my $key = shift; + $filters->{$key} = shift; + } +} + +1; + +__END__ + +=encoding utf-8 + +=head1 NAME + +SL::Helper::Sorted - Manager mixin for filtered results. + +=head1 SYNOPSIS + +In the manager: + + use SL::Helper::Filtered; + + __PACKAGE__->add_filter_specs( + custom_filter_name => sub { + my ($key, $value, $prefix) = @_; + # code to handle this + return ($key, $value, $with_objects); + }, + another_filter_name => \&_sub_to_handle_this, + ); + +In consuming code: + + ($key, $value, $with_objects) = $manager_class->filter($key, $value, $prefix); + +=head1 FUNCTIONS + +=over 4 + +=item C + +Adds new filters to this package as key value pairs. The key will be the new +filters name, the value is expected to be a coderef to an implementation of +this filter. See L for details on this. + +You can add multiple filters in one call, but only one filter per key. + +=item C + +Tells the manager to pply custom filters. If none is registered for C<$key>, +returns C<$key, $value>. + +Otherwise the filter code is called. + +=back + +=head1 INTERFACE OF A CUSTOM FILTER + +Lets look at an example of a working filter. Suppose your model has a lot of +notes fields, and you need to search in all of them. A working filter would be: + + __PACKAGE__->add_filter_specs( + all_notes => sub { + my ($key, $value, $prefix) = @_; + + return or => [ + $prefix . notes1 => $value, + $prefix . notes2 => $value, + ]; + } + ); + +If someone filters for C, your +filter will get called with: + + ->filter('all_notes', { ilike => '%telephone%' }, '') + +and the result will be: + + or => [ + notes1 => { notes1 => '%telephone%' }, + notes2 => { notes1 => '%telephone%' }, + ] + +The prefix is to make sure this also works when called on submodels: + + C + +will pass C as prefix so that the resulting query will be: + + or => [ + customer.notes1 => { notes1 => '%telephone%' }, + customer.notes2 => { notes1 => '%telephone%' }, + ] + +which is pretty much what you would expect. + +As a final touch consider a filter that needs to search somewhere else to work, +like this one: + + __PACKAGE__->add_filter_specs( + name => sub { + my ($key, $value, $prefix) = @_; + + return $prefix . person.name => $value, + $prefix . 'person'; + }, + }; + +Now you can search for C in your model without ever knowing that the real +name lies in the table C. Unfortunately Rose has to know about it to +get the joins right, and so you need to tell it to include C into its +C. That's the reason for the third return value. + + +To summarize: + +=over 4 + +=item * + +You will get passed the name of your filter as C<$key> stripped of all filters +and escapes. + +=item * + +You will get passed the C<$value> processed with all filters and escapes. + +=item * + +You will get passed a C<$prefix> that can be prepended to all database columns +to make sense to Rose. + +=item * + +You are expeceted to return exactly one key and one value. That can mean you +have to encapsulate your arguments into C<< or => [] >> or C<< and => [] >> blocks. + +=item * + +If your filter needs relationships that are not always loaded, you need to +return them in C style. If you need to return more than one, use +an arrayref. + +=back + +=head1 BUGS + +None yet. + +=head1 AUTHOR + +Sven Schöling Es.schoeling@linet-services.deE + +=cut -- 2.20.1