C/Project: filtered eingeführt; db_args, pre_parse_filter und flat_filter entfernt
[kivitendo-erp.git] / SL / Controller / Helper / Sorted.pm
1 package SL::Controller::Helper::Sorted;
2
3 use strict;
4
5 use Carp;
6 use List::MoreUtils qw(uniq);
7
8 use Exporter qw(import);
9 our @EXPORT = qw(make_sorted get_sort_spec get_current_sort_params set_report_generator_sort_options
10                  _save_current_sort_params _get_models_handler_for_sorted _callback_handler_for_sorted);
11
12 use constant PRIV => '__sortedhelperpriv';
13
14 my %controller_sort_spec;
15
16 sub make_sorted {
17   my ($class, %specs) = @_;
18
19   $specs{MODEL} ||=  $class->controller_name;
20   $specs{MODEL}   =~ s{ ^ SL::DB:: (?: .* :: )? }{}x;
21
22   while (my ($column, $spec) = each %specs) {
23     next if $column =~ m/^[A-Z_]+$/;
24
25     $spec = $specs{$column} = { title => $spec } if (ref($spec) || '') ne 'HASH';
26
27     $spec->{model}        ||= $specs{MODEL};
28     $spec->{model_column} ||= $column;
29   }
30
31   my %model_sort_spec   = "SL::DB::Manager::$specs{MODEL}"->_sort_spec;
32   $specs{DEFAULT_DIR}   = $specs{DEFAULT_DIR} ? 1 : defined($specs{DEFAULT_DIR}) ? $specs{DEFAULT_DIR} * 1 : $model_sort_spec{default}->[1];
33   $specs{DEFAULT_BY}  ||= $model_sort_spec{default}->[0];
34   $specs{FORM_PARAMS} ||= [ qw(sort_by sort_dir) ];
35   $specs{ONLY}        ||= [];
36   $specs{ONLY}          = [ $specs{ONLY} ] if !ref $specs{ONLY};
37
38   $controller_sort_spec{$class} = \%specs;
39
40   my %hook_params = @{ $specs{ONLY} } ? ( only => $specs{ONLY} ) : ();
41   $class->run_before('_save_current_sort_params', %hook_params);
42
43   SL::Controller::Helper::GetModels::register_get_models_handlers(
44     $class,
45     callback   => '_callback_handler_for_sorted',
46     get_models => '_get_models_handler_for_sorted',
47     ONLY       => $specs{ONLY},
48   );
49
50   # $::lxdebug->dump(0, "CONSPEC", \%specs);
51 }
52
53 sub get_sort_spec {
54   my ($class_or_self) = @_;
55
56   return $controller_sort_spec{ref($class_or_self) || $class_or_self};
57 }
58
59 sub get_current_sort_params {
60   my ($self, %params) = @_;
61
62   my $sort_spec = $self->get_sort_spec;
63
64   if (!$params{sort_by}) {
65     my $priv          = $self->{PRIV()} || {};
66     $params{sort_by}  = $priv->{by};
67     $params{sort_dir} = $priv->{dir};
68   }
69
70   my $by          = $params{sort_by} || $sort_spec->{DEFAULT_BY};
71   my %sort_params = (
72     dir => defined($params{sort_dir}) ? $params{sort_dir} * 1 : $sort_spec->{DEFAULT_DIR},
73     by  => $sort_spec->{$by} ? $by : $sort_spec->{DEFAULT_BY},
74   );
75
76   return %sort_params;
77 }
78
79 sub set_report_generator_sort_options {
80   my ($self, %params) = @_;
81
82   $params{$_} or croak("Missing parameter '$_'") for qw(report sortable_columns);
83
84   my %current_sort_params = $self->get_current_sort_params;
85
86   foreach my $col (@{ $params{sortable_columns} }) {
87     $params{report}->{columns}->{$col}->{link} = $self->get_callback(
88       sort_by  => $col,
89       sort_dir => ($current_sort_params{by} eq $col ? 1 - $current_sort_params{dir} : $current_sort_params{dir}),
90     );
91   }
92
93   $params{report}->set_sort_indicator($current_sort_params{by}, 1 - $current_sort_params{dir});
94
95   if ($params{report}->{export}) {
96     $params{report}->{export}->{variable_list} = [ uniq(
97       @{ $params{report}->{export}->{variable_list} },
98       @{ $self->get_sort_spec->{FORM_PARAMS} }
99     )];
100   }
101 }
102
103 #
104 # private functions
105 #
106
107 sub _save_current_sort_params {
108   my ($self)      = @_;
109
110   my $sort_spec   = $self->get_sort_spec;
111   my $dir_idx     = $sort_spec->{FORM_PARAMS}->[1];
112   $self->{PRIV()} = {
113     by            =>   $::form->{ $sort_spec->{FORM_PARAMS}->[0] },
114     dir           => defined($::form->{$dir_idx}) ? $::form->{$dir_idx} * 1 : undef,
115   };
116
117   # $::lxdebug->message(0, "saving current sort params to " . $self->{PRIV()}->{by} . ' / ' . $self->{PRIV()}->{dir});
118 }
119
120 sub _callback_handler_for_sorted {
121   my ($self, %params) = @_;
122
123   my $priv = $self->{PRIV()} || {};
124   if ($priv->{by}) {
125     my $sort_spec                             = $self->get_sort_spec;
126     $params{ $sort_spec->{FORM_PARAMS}->[0] } = $priv->{by};
127     $params{ $sort_spec->{FORM_PARAMS}->[1] } = $priv->{dir};
128   }
129
130   # $::lxdebug->dump(0, "CB handler for sorted; params nach modif:", \%params);
131
132   return %params;
133 }
134
135 sub _get_models_handler_for_sorted {
136   my ($self, %params) = @_;
137
138   my %sort_params     = $self->get_current_sort_params;
139   my $sort_spec       = $self->get_sort_spec->{ $sort_params{by} };
140
141   $params{model}      = $sort_spec->{model};
142   $params{sort_by}    = "SL::DB::Manager::$params{model}"->make_sort_string(sort_by => $sort_spec->{model_column}, sort_dir => $sort_params{dir});
143
144   # $::lxdebug->dump(0, "GM handler for sorted; params nach modif:", \%params);
145
146   return %params;
147 }
148
149 1;
150 __END__
151
152 =pod
153
154 =encoding utf8
155
156 =head1 NAME
157
158 SL::Controller::Helper::Sorted - A helper for semi-automatic handling
159 of sorting lists of database models in a controller
160
161 =head1 SYNOPSIS
162
163 In a controller:
164
165   use SL::Controller::Helper::GetModels;
166   use SL::Controller::Helper::Sorted;
167
168   __PACKAGE__->make_sorted(
169     DEFAULT_BY   => 'run_at',
170     DEFAULT_DIR  => 1,
171     MODEL        => 'BackgroundJobHistory',
172     ONLY         => [ qw(list) ],
173
174     error        => $::locale->text('Error'),
175     package_name => $::locale->text('Package name'),
176     run_at       => $::locale->text('Run at'),
177   );
178
179   sub action_list {
180     my ($self) = @_;
181
182     my $sorted_models = $self->get_models;
183     $self->render('controller/list', ENTRIES => $sorted_models);
184   }
185
186 In said template:
187
188   [% USE L %]
189
190   <table>
191    <tr>
192     <th>[% L.sortable_table_header('package_name') %]</th>
193     <th>[% L.sortable_table_header('run_at') %]</th>
194     <th>[% L.sortable_table_header('error') %]</th>
195    </tr>
196
197    [% FOREACH entry = ENTRIES %]
198     <tr>
199      <td>[% HTML.escape(entry.package_name) %]</td>
200      <td>[% HTML.escape(entry.run_at) %]</td>
201      <td>[% HTML.escape(entry.error) %]</td>
202     </tr>
203    [% END %]
204   </table>
205
206 =head1 OVERVIEW
207
208 This specialized helper module enables controllers to display a
209 sortable list of database models with as few lines as possible.
210
211 For this to work the controller has to provide the information which
212 indexes are eligible for sorting etc. by a call to L<make_sorted> at
213 compile time.
214
215 The underlying functionality that enables the use of more than just
216 the sort helper is provided by the controller helper C<GetModels>. It
217 provides mechanisms for helpers like this one to hook into certain
218 calls made by the controller (C<get_callback> and C<get_models>) so
219 that the specialized helpers can inject their parameters into the
220 calls to e.g. C<SL::DB::Manager::SomeModel::get_all>.
221
222 A template on the other hand can use the method
223 C<sortable_table_header> from the layout helper module C<L>.
224
225 This module requires that the Rose model managers use their C<Sorted>
226 helper.
227
228 The C<Sorted> helper hooks into the controller call to the action via
229 a C<run_before> hook. This is done so that it can remember the sort
230 parameters that were used in the current view.
231
232 =head1 PACKAGE FUNCTIONS
233
234 =over 4
235
236 =item C<make_sorted %sort_spec>
237
238 This function must be called by a controller at compile time. It is
239 uesd to set the various parameters required for this helper to do its
240 magic.
241
242 There are two sorts of keys in the hash C<%sort_spec>. The first kind
243 is written in all upper-case. Those parameters are control
244 parameters. The second kind are all lower-case and represent indexes
245 that can be used for sorting (similar to database column names). The
246 second kind are also the indexes you use in a template when calling
247 C<[% L.sorted_table_header(...) %]>.
248
249 Control parameters include the following:
250
251 =over 4
252
253 =item * C<MODEL>
254
255 Optional. A string: the name of the Rose database model that is used
256 as a default in certain cases. If this parameter is missing then it is
257 derived from the controller's package (e.g. for the controller
258 C<SL::Controller::BackgroundJobHistory> the C<MODEL> would default to
259 C<BackgroundJobHistory>).
260
261 =item * C<DEFAULT_BY>
262
263 Optional. A string: the index to sort by if the user hasn't clicked on
264 any column yet (meaning: if the C<$::form> parameters for sorting do
265 not contain a valid index).
266
267 Defaults to the underlying database model's default sort column name.
268
269 =item * C<DEFAULT_DIR>
270
271 Optional. Default sort direction (ascending for trueish values,
272 descrending for falsish values).
273
274 Defaults to the underlying database model's default sort direction.
275
276 =item * C<FORM_PARAMS>
277
278 Optional. An array reference with exactly two strings that name the
279 indexes in C<$::form> in which the sort index (the first element in
280 the array) and sort direction (the second element in the array) are
281 stored.
282
283 Defaults to the values C<sort_by> and C<sort_dir> if missing.
284
285 =item * C<ONLY>
286
287 Optional. An array reference containing a list of action names for
288 which the sort parameters should be saved. If missing or empty then
289 all actions invoked on the controller are monitored.
290
291 =back
292
293 All keys that are written in all lower-case name indexes that can be
294 used for sorting. Each value to such a key can be either a string or a
295 hash reference containing certain elements. If the value is only a
296 string then such a hash reference is constructed, and the string is
297 used as the value for the C<title> key.
298
299 These possible elements are:
300
301 =over 4
302
303 =item * C<title>
304
305 Required. A user-displayable title to be used by functions like the
306 layout helper's C<sortable_table_header>. Does not have a default
307 value.
308
309 Note that this string must be the untranslated English version of the
310 string. The titles will be translated whenever they're requested.
311
312 =item * C<model>
313
314 Optional. The name of a Rose database model this sort index refers
315 to. If missing then the value of C<$sort_spec{MODEL}> is used.
316
317 =item * C<model_column>
318
319 Optional. The name of the Rose database model column this sort index
320 refers to. It must be one of the columns named by the model's
321 C<Sorted> helper (not to be confused with the controller's C<Sorted>
322 helper!).
323
324 If missing it defaults to the key in C<%sort_spec> for which this hash
325 reference is the value.
326
327 =back
328
329 =back
330
331 =head1 INSTANCE FUNCTIONS
332
333 These functions are called on a controller instance.
334
335 =over 4
336
337 =item C<get_sort_spec>
338
339 Returns a hash containing the currently active sort parameters.
340
341 The key C<by> contains the active sort index referring to the
342 C<%sort_spec> given to L<make_sorted>.
343
344 The key C<dir> is either C<1> or C<0>.
345
346 =item C<get_current_sort_params>
347
348 Returns a hash reference to the sort spec structure given in the call
349 to L<make_sorted> after normalization (hash reference construction,
350 applying default parameters etc).
351
352 =item C<set_report_generator_sort_options %params>
353
354 This function does three things with an instance of
355 L<SL::ReportGenerator>:
356
357 =over 4
358
359 =item 1. it sets the sort indicator,
360
361 =item 2. it sets the the links for those column headers that are
362 sortable and
363
364 =item 3. it adds the C<FORM_PARAMS> fields to the list of variables in
365 the report generator's export options.
366
367 =back
368
369 The report generator instance must be passed as the parameter
370 C<report>. The parameter C<sortable_columns> must be an array
371 reference of column names that are sortable.
372
373 The report generator instance must already have its columns and export
374 options set via calls to its L<SL::ReportGenerator::set_columns> and
375 L<SL::ReportGenerator::set_export_options> functions.
376
377 =back
378
379 =head1 BUGS
380
381 Nothing here yet.
382
383 =head1 AUTHOR
384
385 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
386
387 =cut