C/Project: filtered eingeführt; db_args, pre_parse_filter und flat_filter entfernt
[kivitendo-erp.git] / SL / Controller / Helper / Filtered.pm
1 package SL::Controller::Helper::Filtered;
2
3 use strict;
4
5 use Exporter qw(import);
6 use SL::Controller::Helper::ParseFilter ();
7 use List::MoreUtils qw(uniq);
8 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);
9
10 use constant PRIV => '__filteredhelper_priv';
11
12 my %controller_filter_spec;
13
14 sub make_filtered {
15   my ($class, %specs)             = @_;
16
17   $specs{MODEL}                 //=  $class->controller_name;
18   $specs{MODEL}                   =~ s{ ^ SL::DB:: (?: .* :: )? }{}x;
19   $specs{FORM_PARAMS}           //= 'filter';
20   $specs{LAUNDER_TO}              = '__INPLACE__' unless exists $specs{LAUNDER_TO};
21   $specs{ONLY}                  //= [];
22   $specs{ONLY}                    = [ $specs{ONLY} ] if !ref $specs{ONLY};
23   $specs{ONLY_MAP}                = @{ $specs{ONLY} } ? { map { ($_ => 1) } @{ $specs{ONLY} } } : { '__ALL__' => 1 };
24
25   $controller_filter_spec{$class} = \%specs;
26
27   my %hook_params                 = @{ $specs{ONLY} } ? ( only => $specs{ONLY} ) : ();
28   $class->run_before('_save_current_filter_params', %hook_params);
29
30   SL::Controller::Helper::GetModels::register_get_models_handlers(
31     $class,
32     callback   => '_callback_handler_for_filtered',
33     get_models => '_get_models_handler_for_filtered',
34     ONLY       => $specs{ONLY},
35   );
36
37   # $::lxdebug->dump(0, "CONSPEC", \%specs);
38 }
39
40 sub get_filter_spec {
41   my ($class_or_self) = @_;
42
43   return $controller_filter_spec{ref($class_or_self) || $class_or_self};
44 }
45
46 sub get_current_filter_params {
47   my ($self)   = @_;
48
49   return %{ _priv($self)->{filter_params} } if _priv($self)->{filter_params};
50
51   require Carp;
52   Carp::confess('It seems a GetModels plugin tries to access filter params before they got calculated. Make sure your make_filtered call comes first.');
53 }
54
55 sub _make_current_filter_params {
56   my ($self, %params)   = @_;
57
58   my $spec              = $self->get_filter_spec;
59   my $filter            = $params{filter} // _priv($self)->{filter} // {},
60   my %filter_args       = _get_filter_args($self, $spec);
61   my %parse_filter_args = (
62     class        => "SL::DB::Manager::$spec->{MODEL}",
63     with_objects => $params{with_objects},
64   );
65   my $laundered;
66   if ($spec->{LAUNDER_TO} eq '__INPLACE__') {
67
68   } elsif ($spec->{LAUNDER_TO}) {
69     $laundered = {};
70     $parse_filter_args{launder_to} = $laundered;
71   } else {
72     $parse_filter_args{no_launder} = 1;
73   }
74
75   my %calculated_params = SL::Controller::Helper::ParseFilter::parse_filter($filter, %parse_filter_args);
76
77   $calculated_params{query} = [
78     @{ $calculated_params{query} || [] },
79     @{ $filter_args{      query} || [] },
80     @{ $params{           query} || [] },
81   ];
82
83   $calculated_params{with_objects} = [
84     uniq
85     @{ $calculated_params{with_objects} || [] },
86     @{ $filter_args{      with_objects} || [] },
87     @{ $params{           with_objects} || [] },
88   ];
89
90   if ($laundered) {
91     if ($self->can($spec->{LAUNDER_TO})) {
92       $self->${\ $spec->{LAUNDER_TO} }($laundered);
93     } else {
94       $self->{$spec->{LAUNDER_TO}} = $laundered;
95     }
96   }
97
98   # $::lxdebug->dump(0, "get_current_filter_params: ", \%calculated_params);
99
100   _priv($self)->{filter_params} = \%calculated_params;
101
102   return %calculated_params;
103 }
104
105 sub disable_filtering {
106   my ($self)               = @_;
107   _priv($self)->{disabled} = 1;
108 }
109
110 #
111 # private functions
112 #
113
114 sub _get_filter_args {
115   my ($self, $spec) = @_;
116
117   $spec           ||= $self->get_filter_spec;
118
119   my %filter_args   = ref($spec->{FILTER_ARGS}) eq 'CODE' ? %{ $spec->{FILTER_ARGS}->($self) }
120                     :     $spec->{FILTER_ARGS}            ? do { my $sub = $spec->{FILTER_ARGS}; %{ $self->$sub() } }
121                     :                                       ();
122 }
123
124 sub _save_current_filter_params {
125   my ($self)        = @_;
126
127   return if !_is_enabled($self);
128
129   my $filter_spec = $self->get_filter_spec;
130   $self->{PRIV()}{filter} = $::form->{ $filter_spec->{FORM_PARAMS} };
131
132   # $::lxdebug->message(0, "saving current filter params to " . $self->{PRIV()}->{page} . ' / ' . $self->{PRIV()}->{per_page});
133 }
134
135 sub _callback_handler_for_filtered {
136   my ($self, %params) = @_;
137   my $priv            = _priv($self);
138
139   if (_is_enabled($self) && $priv->{filter}) {
140     my $filter_spec = $self->get_filter_spec;
141     my ($flattened) = SL::Controller::Helper::ParseFilter::flatten($priv->{filter}, $filter_spec->{FORM_PARAMS});
142     %params         = (%params, @$flattened);
143   }
144
145   # $::lxdebug->dump(0, "CB handler for filtered; params after flatten:", \%params);
146
147   return %params;
148 }
149
150 sub _get_models_handler_for_filtered {
151   my ($self, %params)    = @_;
152   my $spec               = $self->get_filter_spec;
153
154   # $::lxdebug->dump(0,  "params in get_models_for_filtered", \%params);
155
156   my %filter_params;
157   %filter_params = _make_current_filter_params($self, %params)  if _is_enabled($self);
158
159   # $::lxdebug->dump(0, "GM handler for filtered; params nach modif (is_enabled? " . _is_enabled($self) . ")", \%params);
160
161   return (%params, %filter_params);
162 }
163
164 sub _priv {
165   my ($self)        = @_;
166   $self->{PRIV()} ||= {};
167   return $self->{PRIV()};
168 }
169
170 sub _is_enabled {
171   my ($self) = @_;
172   return !_priv($self)->{disabled} && ($self->get_filter_spec->{ONLY_MAP}->{$self->action_name} || $self->get_filter_spec->{ONLY_MAP}->{'__ALL__'});
173 }
174
175 1;
176
177 __END__
178
179 =pod
180
181 =encoding utf8
182
183 =head1 NAME
184
185 SL::Controller::Helper::Filtered - A helper for semi-automatic handling
186 of filtered lists of database models in a controller
187
188 =head1 SYNOPSIS
189
190 In a controller:
191
192   use SL::Controller::Helper::GetModels;
193   use SL::Controller::Helper::Filtered;
194
195   __PACKAGE__->make_filter(
196     MODEL       => 'Part',
197     ONLY        => [ qw(list) ],
198     FORM_PARAMS => [ qw(filter) ],
199   );
200
201   sub action_list {
202     my ($self) = @_;
203
204     my $filtered_models = $self->get_models(%addition_filters);
205     $self->render('controller/list', ENTRIES => $filtered_models);
206   }
207
208
209 =head1 OVERVIEW
210
211 This helper module enables use of the L<SL::Controller::Helper::ParseFilter>
212 methods in conjunction with the L<SL::Controller::Helper::GetModels> style of
213 plugins. Additional filters can be defined in the database models and filtering
214 can be reduced to a minimum of work.
215
216 This plugin can be combined with L<SL::Controller::Sorted> and
217 L<SL::Controller::Paginated> for filtered, sorted and paginated lists.
218
219 The controller has to provive information where to look for filter information
220 at compile time. This call is L<make_filtered>.
221
222 The underlying functionality that enables the use of more than just
223 the paginate helper is provided by the controller helper
224 C<GetModels>. See the documentation for L<SL::Controller::Sorted> for
225 more information on it.
226
227 =head1 PACKAGE FUNCTIONS
228
229 =over 4
230
231 =item C<make_filtered %filter_spec>
232
233 This function must be called by a controller at compile time. It is
234 uesd to set the various parameters required for this helper to do its
235 magic.
236
237 Careful: If you want to use this in conjunction with
238 L<SL:Controller::Helper::Paginated>, you need to call C<make_filtered> first,
239 or the paginating will not get all the relevant information to estimate the
240 number of pages correctly. To ensure this does not happen, this module will
241 croak when it detects such a scenario.
242
243 The hash C<%filter_spec> can include the following parameters:
244
245 =over 4
246
247 =item * C<MODEL>
248
249 Optional. A string: the name of the Rose database model that is used
250 as a default in certain cases. If this parameter is missing then it is
251 derived from the controller's package (e.g. for the controller
252 C<SL::Controller::BackgroundJobHistory> the C<MODEL> would default to
253 C<BackgroundJobHistory>).
254
255 =item * C<FORM_PARAMS>
256
257 Optional. Indicates a key in C<$::form> to be used as filter.
258
259 Defaults to the values C<filter> if missing.
260
261 =item * C<LAUNDER_TO>
262
263 Option. Indicates a target for laundered filter arguments in the controller.
264 Can be set to C<undef> to disable laundering, and can be set to method named or
265 hash keys of the controller. In the latter case the laundered structure will be
266 put there.
267
268 Defaults to inplace laundering which is not normally settable.
269
270 =item * C<ONLY>
271
272 Optional. An array reference containing a list of action names for
273 which the paginate parameters should be saved. If missing or empty then
274 all actions invoked on the controller are monitored.
275
276 =back
277
278 =back
279
280 =head1 INSTANCE FUNCTIONS
281
282 These functions are called on a controller instance.
283
284 =over 4
285
286 =item C<get_current_filter_params>
287
288 Returns a hash to be used in manager C<get_all> calls or to be passed on to
289 GetModels. Will only work if the get_models chain has been called at least
290 once, because only then the full parameters can get parsed and stored. Will
291 croak otherwise.
292
293 =item C<disable_filtering>
294
295 Disable filtering for the duration of the current action. Can be used
296 when using the attribute C<ONLY> to L<make_filtered> does not
297 cover all cases.
298
299 =back
300
301 =head1 BUGS
302
303 Nothing here yet.
304
305 =head1 AUTHOR
306
307 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
308
309 =cut