--- /dev/null
+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<SL::Controller::Helper::ParseFilter>
+methods in conjunction with the L<SL::Controller::Helper::GetModels> 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<SL::Controller::Sorted> and
+L<SL::Controller::Paginated> 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<make_filtered>.
+
+The underlying functionality that enables the use of more than just
+the paginate helper is provided by the controller helper
+C<GetModels>. See the documentation for L<SL::Controller::Sorted> for
+more information on it.
+
+=head1 PACKAGE FUNCTIONS
+
+=over 4
+
+=item C<make_filtered %filter_spec>
+
+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<SL:Controller::Helper::Paginated>, you need to call C<make_filtered> 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<MODEL>
+
+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<SL::Controller::BackgroundJobHistory> the C<MODEL> would default to
+C<BackgroundJobHistory>).
+
+=item * C<FORM_PARAMS>
+
+Optional. Indicates a key in E<$::form> to be used as filter.
+
+Defaults to the values C<filter> if missing.
+
+=item * C<LAUNDER_TO>
+
+Option. Indicates a target for laundered filter arguments in the controller.
+Can be set to C<undef> 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<ONLY>
+
+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<get_current_filter_params>
+
+Returns a hash to be used in manager C<get_all> 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>
+
+Disable filtering for the duration of the current action. Can be used
+when using the attribute C<ONLY> to L<make_filtered> does not
+cover all cases.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut
--- /dev/null
+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<add_filter_specs %PARAMS>
+
+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<INTERFACE OF A CUSTOM FILTER> for details on this.
+
+You can add multiple filters in one call, but only one filter per key.
+
+=item C<filter $key, $value, $prefix>
+
+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<filter.model.all_notes:substr::ilike=telephone>, 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<filter.customer.model.all_notes:substr::ilike=telephone>
+
+will pass C<customer.> 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<name> in your model without ever knowing that the real
+name lies in the table C<person>. Unfortunately Rose has to know about it to
+get the joins right, and so you need to tell it to include C<person> into its
+C<with_objects>. 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<with_objects> style. If you need to return more than one, use
+an arrayref.
+
+=back
+
+=head1 BUGS
+
+None yet.
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut