Merge branch 'master' of vc.linet-services.de:public/lx-office-erp
[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 = \%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;
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   $self->{PRIV()} = {
112     by            =>   $::form->{ $sort_spec->{FORM_PARAMS}->[0] },
113     dir           => !!$::form->{ $sort_spec->{FORM_PARAMS}->[1] } * 1,
114   };
115
116   # $::lxdebug->message(0, "saving current sort params to " . $self->{PRIV()}->{by} . ' / ' . $self->{PRIV()}->{dir});
117 }
118
119 sub _callback_handler_for_sorted {
120   my ($self, %params) = @_;
121
122   my $priv = $self->{PRIV()} || {};
123   if ($priv->{by}) {
124     my $sort_spec                             = $self->get_sort_spec;
125     $params{ $sort_spec->{FORM_PARAMS}->[0] } = $priv->{by};
126     $params{ $sort_spec->{FORM_PARAMS}->[1] } = $priv->{dir};
127   }
128
129   # $::lxdebug->dump(0, "CB handler for sorted; params nach modif:", \%params);
130
131   return %params;
132 }
133
134 sub _get_models_handler_for_sorted {
135   my ($self, %params) = @_;
136
137   my %sort_params     = $self->get_current_sort_params;
138   my $sort_spec       = $self->get_sort_spec->{ $sort_params{by} };
139
140   $params{model}      = $sort_spec->{model};
141   $params{sort_by}    = "SL::DB::Manager::$params{model}"->make_sort_string(sort_by => $sort_spec->{model_column}, sort_dir => $sort_params{dir});
142
143   # $::lxdebug->dump(0, "GM handler for sorted; params nach modif:", \%params);
144
145   return %params;
146 }
147
148 1;
149 __END__
150
151 =pod
152
153 =encoding utf8
154
155 =head1 NAME
156
157 SL::Controller::Helper::Sorted - A helper for semi-automatic handling
158 of sorting lists of database models in a controller
159
160 =head1 SYNOPSIS
161
162 In a controller:
163
164   use SL::Controller::Helper::GetModels;
165   use SL::Controller::Helper::Sorted;
166
167   __PACKAGE__->make_sorted(
168     DEFAULT_BY   => 'run_at',
169     DEFAULT_DIR  => 1,
170     MODEL        => 'BackgroundJobHistory',
171     ONLY         => [ qw(list) ],
172
173     error        => $::locale->text('Error'),
174     package_name => $::locale->text('Package name'),
175     run_at       => $::locale->text('Run at'),
176   );
177
178   sub action_list {
179     my ($self) = @_;
180
181     my $sorted_models = $self->get_models;
182     $self->render('controller/list', ENTRIES => $sorted_models);
183   }
184
185 In said template:
186
187   [% USE L %]
188
189   <table>
190    <tr>
191     <th>[% L.sortable_table_header('package_name') %]</th>
192     <th>[% L.sortable_table_header('run_at') %]</th>
193     <th>[% L.sortable_table_header('error') %]</th>
194    </tr>
195
196    [% FOREACH entry = ENTRIES %]
197     <tr>
198      <td>[% HTML.escape(entry.package_name) %]</td>
199      <td>[% HTML.escape(entry.run_at) %]</td>
200      <td>[% HTML.escape(entry.error) %]</td>
201     </tr>
202    [% END %]
203   </table>
204
205 =head1 OVERVIEW
206
207 This specialized helper module enables controllers to display a
208 sortable list of database models with as few lines as possible.
209
210 For this to work the controller has to provide the information which
211 indexes are eligible for sorting etc. by a call to L<make_sorted> at
212 compile time.
213
214 The underlying functionality that enables the use of more than just
215 the sort helper is provided by the controller helper C<GetModels>. It
216 provides mechanisms for helpers like this one to hook into certain
217 calls made by the controller (C<get_callback> and C<get_models>) so
218 that the specialized helpers can inject their parameters into the
219 calls to e.g. C<SL::DB::Manager::SomeModel::get_all>.
220
221 A template on the other hand can use the method
222 C<sortable_table_header> from the layout helper module C<L>.
223
224 This module requires that the Rose model managers use their C<Sorted>
225 helper.
226
227 The C<Sorted> helper hooks into the controller call to the action via
228 a C<run_before> hook. This is done so that it can remember the sort
229 parameters that were used in the current view.
230
231 =head1 PACKAGE FUNCTIONS
232
233 =over 4
234
235 =item C<make_sorted %sort_spec>
236
237 This function must be called by a controller at compile time. It is
238 uesd to set the various parameters required for this helper to do its
239 magic.
240
241 There are two sorts of keys in the hash C<%sort_spec>. The first kind
242 is written in all upper-case. Those parameters are control
243 parameters. The second kind are all lower-case and represent indexes
244 that can be used for sorting (similar to database column names). The
245 second kind are also the indexes you use in a template when calling
246 C<[% L.sorted_table_header(...) %]>.
247
248 Control parameters include the following:
249
250 =over 4
251
252 =item * C<MODEL>
253
254 Optional. A string: the name of the Rose database model that is used
255 as a default in certain cases. If this parameter is missing then it is
256 derived from the controller's package (e.g. for the controller
257 C<SL::Controller::BackgroundJobHistory> the C<MODEL> would default to
258 C<BackgroundJobHistory>).
259
260 =item * C<DEFAULT_BY>
261
262 Optional. A string: the index to sort by if the user hasn't clicked on
263 any column yet (meaning: if the C<$::form> parameters for sorting do
264 not contain a valid index).
265
266 Defaults to the underlying database model's default sort column name.
267
268 =item * C<DEFAULT_DIR>
269
270 Optional. Default sort direction (ascending for trueish values,
271 descrending for falsish values).
272
273 Defaults to the underlying database model's default sort direction.
274
275 =item * C<FORM_PARAMS>
276
277 Optional. An array reference with exactly two strings that name the
278 indexes in C<$::form> in which the sort index (the first element in
279 the array) and sort direction (the second element in the array) are
280 stored.
281
282 Defaults to the values C<sort_by> and C<sort_dir> if missing.
283
284 =item * C<ONLY>
285
286 Optional. An array reference containing a list of action names for
287 which the sort parameters should be saved. If missing or empty then
288 all actions invoked on the controller are monitored.
289
290 =back
291
292 All keys that are written in all lower-case name indexes that can be
293 used for sorting. Each value to such a key can be either a string or a
294 hash reference containing certain elements. If the value is only a
295 string then such a hash reference is constructed, and the string is
296 used as the value for the C<title> key.
297
298 These possible elements are:
299
300 =over 4
301
302 =item * C<title>
303
304 Required. A user-displayable title to be used by functions like the
305 layout helper's C<sortable_table_header>. Does not have a default
306 value.
307
308 Note that this string must be the untranslated English version of the
309 string. The titles will be translated whenever they're requested.
310
311 =item * C<model>
312
313 Optional. The name of a Rose database model this sort index refers
314 to. If missing then the value of C<$sort_spec{MODEL}> is used.
315
316 =item * C<model_column>
317
318 Optional. The name of the Rose database model column this sort index
319 refers to. It must be one of the columns named by the model's
320 C<Sorted> helper (not to be confused with the controller's C<Sorted>
321 helper!).
322
323 If missing it defaults to the key in C<%sort_spec> for which this hash
324 reference is the value.
325
326 =back
327
328 =back
329
330 =head1 INSTANCE FUNCTIONS
331
332 These functions are called on a controller instance.
333
334 =over 4
335
336 =item C<get_sort_spec>
337
338 Returns a hash containing the currently active sort parameters.
339
340 The key C<by> contains the active sort index referring to the
341 C<%sort_spec> given to L<make_sorted>.
342
343 The key C<dir> is either C<1> or C<0>.
344
345 =item C<get_current_sort_params>
346
347 Returns a hash reference to the sort spec structure given in the call
348 to L<make_sorted> after normalization (hash reference construction,
349 applying default parameters etc).
350
351 =item C<set_report_generator_sort_options %params>
352
353 This function does three things with an instance of
354 L<SL::ReportGenerator>:
355
356 =over 4
357
358 =item 1. it sets the sort indicator,
359
360 =item 2. it sets the the links for those column headers that are
361 sortable and
362
363 =item 3. it adds the C<FORM_PARAMS> fields to the list of variables in
364 the report generator's export options.
365
366 =back
367
368 The report generator instance must be passed as the parameter
369 C<report>. The parameter C<sortable_columns> must be an array
370 reference of column names that are sortable.
371
372 The report generator instance must already have its columns and export
373 options set via calls to its L<SL::ReportGenerator::set_columns> and
374 L<SL::ReportGenerator::set_export_options> functions.
375
376 =back
377
378 =head1 BUGS
379
380 Nothing here yet.
381
382 =head1 AUTHOR
383
384 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
385
386 =cut