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