epic-s6ts
[kivitendo-erp.git] / SL / DB / Helper / Sorted.pm
1 package SL::DB::Helper::Sorted;
2
3 use strict;
4
5 require Exporter;
6 our @ISA    = qw(Exporter);
7 our @EXPORT = qw(get_all_sorted make_sort_string);
8
9 my %sort_spec;
10
11 sub make_sort_string {
12   my ($class, %params) = @_;
13
14   my $sort_spec        = _get_sort_spec($class);
15
16   my $sort_dir         = defined($params{sort_dir}) ? $params{sort_dir} * 1 : $sort_spec->{default}->[1];
17   my $sort_dir_str     = $sort_dir ? 'ASC' : 'DESC';
18
19   my $sort_by          = $params{sort_by} || { };
20   $sort_by             = $sort_spec->{default}->[0] unless $sort_spec->{columns}->{$sort_by};
21
22   my $nulls_str        = '';
23   if ($sort_spec->{nulls}) {
24     $nulls_str = ref($sort_spec->{nulls}) ? ($sort_spec->{nulls}->{$sort_by} || $sort_spec->{nulls}->{default}) : $sort_spec->{nulls};
25     $nulls_str = " NULLS ${nulls_str}" if $nulls_str;
26   }
27
28   my $sort_by_str = $sort_spec->{columns}->{$sort_by};
29   $sort_by_str    = [ $sort_by_str ] unless ref($sort_by_str) eq 'ARRAY';
30
31   # generate tiebreaker
32   push @$sort_by_str, @{ $sort_spec->{tiebreaker} };
33
34   $sort_by_str    = join(', ', map { "${_} ${sort_dir_str}${nulls_str}" } @{ $sort_by_str });
35
36   return wantarray ? ($sort_by, $sort_dir, $sort_by_str) : $sort_by_str;
37 }
38
39 sub get_all_sorted {
40   my ($class, %params) = @_;
41   my $sort_str         = $class->make_sort_string(sort_by => delete($params{sort_by}), sort_dir => delete($params{sort_dir}));
42
43   return $class->get_all(sort_by => $sort_str, %params);
44 }
45
46 sub _get_sort_spec {
47   my ($class) = @_;
48   return $sort_spec{$class} ||= _make_sort_spec($class);
49 }
50
51 sub _make_sort_spec {
52   my ($class) = @_;
53
54   my %sort_spec = defined &{ "${class}::_sort_spec" } ? $class->_sort_spec : ();
55
56   my $meta = $class->object_class->meta;
57   my $table = $meta->table;
58
59   if (!$sort_spec{default}) {
60     my @primary_keys = $meta->primary_key;
61     $sort_spec{default} = [ "" . $primary_keys[0], 1 ];
62   }
63
64   $sort_spec{columns} ||= { SIMPLE => [ map { "$_" } $meta->columns ] };
65
66   if ($sort_spec{columns}->{SIMPLE}) {
67     if (!ref($sort_spec{columns}->{SIMPLE}) && ($sort_spec{columns}->{SIMPLE} eq 'ALL')) {
68       map { $sort_spec{columns}->{"$_"} ||= "${table}.${_}"} @{ $meta->columns };
69       delete $sort_spec{columns}->{SIMPLE};
70     } else {
71       map { $sort_spec{columns}->{$_} = "${table}.${_}" } @{ delete($sort_spec{columns}->{SIMPLE}) };
72     }
73   }
74
75   $sort_spec{tiebreaker} ||= [ map { "${table}.${_}" } $meta->primary_key ];
76
77   return \%sort_spec;
78 }
79
80 1;
81
82 __END__
83
84 =encoding utf8
85
86 =head1 NAME
87
88 SL::DB::Helper::Sorted - Mixin for a manager class that handles
89 sorting of database records
90
91 =head1 SYNOPSIS
92
93   package SL::DB::Manager::Message;
94
95   use SL::DB::Helper::Sorted;
96
97   sub _sort_spec {
98     return ( columns => { recipient_id => [ 'CASE
99                                              WHEN recipient_group_id IS NULL THEN lower(recipient.name)
100                                              ELSE                                 lower(recipient_group.name)
101                                              END',                                      ],
102                           sender_id    => [ 'lower(sender.name)',                       ],
103                           created_at   => [ 'created_at',                               ],
104                           subject      => [ 'lower(subject)',                           ],
105                           status       => [ 'NOT COALESCE(unread, FALSE)', 'created_at' ],
106                         },
107              default => [ 'status', 1 ],
108              nulls   => { default => 'LAST',
109                           subject => 'FIRST',
110                         }
111            );
112   }
113
114   package SL::Controller::Message;
115
116   sub action_list {
117     my $messages = SL::DB::Manager::Message->get_all_sorted(sort_by  => $::form->{sort_by},
118                                                             sort_dir => $::form->{sort_dir});
119   }
120
121 =head1 CLASS FUNCTIONS
122
123 =over 4
124
125 =item C<make_sort_string %params>
126
127 Evaluates C<$params{sort_by}> and C<$params{sort_dir}> and returns an
128 SQL string suitable for sorting. The package this package is mixed
129 into has to provide a method L</_sort_spec> that returns a hash whose
130 structure is explained below. That hash is authoritative in which
131 columns may be sorted, which column to sort by by default and how to
132 handle C<NULL> values.
133
134 Returns the SQL string in scalar context. In array context it returns
135 three values: the actual column it sorts by (suitable for another call
136 to L</make_sort_string>), the actual sort direction (either 0 or 1)
137 and the SQL string.
138
139 =item C<get_all_sorted %params>
140
141 Returns C<< $class->get_all >> with C<sort_by> set to the value
142 returned by c<< $class->make_sort_string(%params) >>.
143
144 =back
145
146 =head1 CLASS FUNCTIONS PROVIDED BY THE MIXING PACKAGE
147
148 =over 4
149
150 =item C<_sort_spec>
151
152 This method is actually not part of this package but can be provided
153 by the package this helper is mixed into. If it isn't then all columns
154 of the corresponding table (as returned by the model's meta data) will
155 be eligible for sorting.
156
157 Returns a hash with the following keys:
158
159 =over 2
160
161 =item C<default>
162
163 A two-element array containing the name and direction by which to sort
164 in default cases. Example:
165
166   default => [ 'name', 1 ],
167
168 Defaults to the table's primary key column (the first column if the
169 primary key is composited).
170
171 =item C<columns>
172
173 A hash reference. Its keys are column names, and its values are SQL
174 strings by which to sort. Example:
175
176   columns => { SIMPLE                  => [ 'transaction_description', 'orddate' ],
177                the_date                => 'CASE WHEN oe.quotation THEN oe.quodate ELSE oe.orddate END',
178                customer_name           => 'lower(customer.name)',
179              },
180
181 If sorting is requested for a column that is not a key in this hash
182 then the default column name will be used.
183
184 The value can be either a scalar or an array reference. If it's the
185 latter then both the sort direction as well as the null handling will
186 be appended to each of its members.
187
188 The special key C<SIMPLE> can be a scalar or an array reference. If it
189 is an array reference then it contains column names that are mapped
190 1:1 onto the table's columns. If it is the scalar 'ALL' then all
191 columns in that model's meta data are mapped 1:1 unless the C<columns>
192 hash already contains a key for that column.
193
194 If C<columns> is missing then all columns of the model will be
195 eligible for sorting. The list of columns is looked up in the model's
196 meta data.
197
198 =item C<nulls>
199
200 Either a scalar or a hash reference determining where C<NULL> values
201 will be sorted. If undefined then the decision is left to the
202 database.
203
204 If it is a scalar then the same value will be used for all
205 classes. The value is either C<FIRST> or C<LAST>.
206
207 If it is a hash reference then its keys are column names (not SQL
208 names). The values are either C<FIRST> or C<LAST>. If a column name is
209 not found in this hash then the special key C<default> will be looked
210 up and used if it is found.
211
212 Example:
213
214   nulls => { transaction_description => 'FIRST',
215              customer_name           => 'FIRST',
216              default                 => 'LAST',
217            },
218
219 =item C<tiebreaker>
220
221 Optional tiebreaker sorting that gets appended to any user requested sorting.
222 Needed to make sorting by non unique columns deterministic.
223
224 If present must be an arrayref of column sort specs (see C<column>).
225
226 Defaults to primary keys.
227
228 =back
229
230 =back
231
232 =head1 BUGS
233
234 Nothing here yet.
235
236 =head1 AUTHOR
237
238 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
239
240 =cut