--- /dev/null
+package SL::Controller::Helper::GetModels;
+
+use strict;
+
+use Exporter qw(import);
+our @EXPORT = qw(get_callback get_models);
+
+my $current_action;
+my %registered_handlers = ( callback => [], get_models => [] );
+
+sub register_get_models_handlers {
+ my ($class, %additional_handlers) = @_;
+
+ my $only = delete($additional_handlers{ONLY}) || [];
+ $only = [ $only ] if !ref $only;
+ my %hook_params = @{ $only } ? ( only => $only ) : ();
+
+ $class->run_before(sub { $current_action = $_[1]; }, %hook_params);
+
+ map { push @{ $registered_handlers{$_} }, $additional_handlers{$_} if $additional_handlers{$_} } keys %registered_handlers;
+}
+
+sub get_callback {
+ my ($self, %override_params) = @_;
+
+ my %default_params = _run_handlers($self, 'callback', action => $current_action);
+
+ return $self->url_for(%default_params, %override_params);
+}
+
+sub get_models {
+ my ($self, %override_params) = @_;
+
+ my %default_params = _run_handlers($self, 'get_models');
+
+ my %params = (%default_params, %override_params);
+ my $model = delete($params{model}) || die "No 'model' to work on";
+
+ return "SL::DB::Manager::${model}"->get_all(%params);
+}
+
+#
+# private/internal functions
+#
+
+sub _run_handlers {
+ my ($self, $handler_type, %params) = @_;
+
+ foreach my $sub (@{ $registered_handlers{$handler_type} }) {
+ if (ref $sub eq 'CODE') {
+ %params = $sub->($self, %params);
+ } elsif ($self->can($sub)) {
+ %params = $self->$sub(%params);
+ } else {
+ die "SL::Controller::Helper::GetModels::get_callback: Cannot call $sub on " . ref($self) . ")";
+ }
+ }
+
+ return %params;
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Controller::Helper::GetModels - Base mixin for controller helpers
+dealing with semi-automatic handling of sorting and paginating lists
+
+=head1 SYNOPSIS
+
+For a proper synopsis see L<SL::Controller::Helper::Sorted>.
+
+=head1 OVERVIEW
+
+For a generic overview see L<SL::Controller::Helper::Sorted>.
+
+This base module is the interface between a controller and specialized
+helper modules that handle things like sorting and paginating. The
+specialized helpers register themselves with this module via a call to
+L<register_get_models_handlers> during compilation time (e.g. in the
+case of C<Sorted> this happens when the controller calls
+L<SL::Controller::Helper::Sorted::make_sorted>).
+
+A controller will later usually call the L<get_models>
+function. Templates will call the L<get_callback> function. Both
+functions run the registered handlers handing over control to the
+specialized helpers so that they may inject their parameters into the
+call chain.
+
+The C<GetModels> helper hooks into the controller call to the action
+via a C<run_before> hook. This is done so that it can remember the
+action called by the user. This is used for constructing the callback
+in L<get_callback>.
+
+=head1 PACKAGE FUNCTIONS
+
+=over 4
+
+=item C<register_get_models_handlers $class, %handlers>
+
+This function should only be called from other controller helpers like
+C<Sorted> or C<Paginated>. It is not exported and must therefore be
+called its full name. The first parameter C<$class> must be the actual
+controller's class name.
+
+If C<%handlers> contains a key C<ONLY> then it is passed to the hook
+registration in L<SL::Controller::Base::run_before>.
+
+The C<%handlers> register callback functions in the specialized
+controller helpers that are called during invocation of
+L<get_callback> or L<get_models>. Possible keys are C<callback> and
+C<models>.
+
+Each handler (the value in the hash) can be either a code reference
+(in which case it is called directly) or the name of an instance
+function callable on a controller instance. In both cases the handler
+receives a hash of parameters built during this very call to
+L<get_callback> or L<get_models> respectively. The handler's return
+value must be the new hash to be used in calls to further handlers and
+to the actual database model functions later on.
+
+=back
+
+=head1 INSTANCE FUNCTIONS
+
+=over 4
+
+=item C<get_callback [%params]>
+
+Return an URL suitable for use as a callback parameter. It maps to the
+current controller and action. All registered handlers of type
+'callback' (e.g. the ones by C<Sorted> and C<Paginated>) can inject
+the parameters they need so that the same list view as is currently
+visible can be re-rendered.
+
+Optional C<%params> passed to this function may override any parameter
+set by the registered handlers.
+
+=item C<get_models [%params]>
+
+Query the model manager via C<get_all> and return its result. The
+parameters to C<get_all> are constructed by calling all registered
+handlers of type 'models' (e.g. the ones by C<Sorted> and
+C<Paginated>).
+
+Optional C<%params> passed to this function may override any parameter
+set by the registered handlers.
+
+The return value is the an array reference of C<Rose> models.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
--- /dev/null
+package SL::Controller::Helper::Sorted;
+
+use strict;
+
+use Exporter qw(import);
+our @EXPORT = qw(make_sorted get_sort_spec get_current_sort_params _save_current_sort_params _get_models_handler_for_sorted _callback_handler_for_sorted);
+
+my ($controller_sort_spec, $current_sort_by, $current_sort_dir);
+
+sub make_sorted {
+ my ($class, %specs) = @_;
+
+ $specs{MODEL} ||= $class->_controller_name;
+ $specs{MODEL} =~ s{ ^ SL::DB:: (?: .* :: )? }{}x;
+
+ while (my ($column, $spec) = each %specs) {
+ next if $column =~ m/^[A-Z_]+$/;
+
+ $spec = $specs{$column} = { title => $spec } if !ref $spec;
+
+ $spec->{model} ||= $specs{MODEL};
+ $spec->{model_column} ||= $column;
+ }
+
+ $specs{DEFAULT_DIR} = $specs{DEFAULT_DIR} || !defined($specs{DEFAULT_DIR}) ? 1 : 0;
+ $specs{DEFAULT_BY} ||= "SL::DB::$specs{MODEL}::Manager"->_get_sort_spec($class)->{default}->[0];
+ $specs{FORM_PARAMS} ||= [ qw(sort_by sort_dir) ];
+ $specs{ONLY} ||= [];
+ $specs{ONLY} = [ $specs{ONLY} ] if !ref $specs{ONLY};
+
+ $controller_sort_spec = \%specs;
+
+ my %hook_params = @{ $specs{ONLY} } ? ( only => $specs{ONLY} ) : ();
+ $class->run_before('_save_current_sort_params', %hook_params);
+
+ SL::Controller::Helper::GetModels::register_get_models_handlers(
+ $class,
+ callback => '_callback_handler_for_sorted',
+ get_models => '_get_models_handler_for_sorted',
+ ONLY => $specs{ONLY},
+ );
+
+ # $::lxdebug->dump(0, "CONSPEC", \%specs);
+}
+
+sub get_sort_spec {
+ my ($class_or_self) = @_;
+
+ return $controller_sort_spec;
+}
+
+sub get_current_sort_params {
+ my ($self, %params) = @_;
+
+ my $sort_spec = $self->get_sort_spec;
+
+ if (!$params{sort_by}) {
+ $params{sort_by} = $current_sort_by;
+ $params{sort_dir} = $current_sort_dir;
+ }
+
+ my $by = $params{sort_by} || $sort_spec->{DEFAULT_BY};
+ my %sort_params = (
+ dir => defined($params{sort_dir}) ? $params{sort_dir} * 1 : $sort_spec->{DEFAULT_DIR},
+ by => $sort_spec->{$by} ? $by : $sort_spec->{DEFAULT_BY},
+ );
+
+ return %sort_params;
+}
+
+#
+# private functions
+#
+
+sub _save_current_sort_params {
+ my ($self) = @_;
+
+ my $sort_spec = $self->get_sort_spec;
+ $current_sort_by = $::form->{ $sort_spec->{FORM_PARAMS}->[0] };
+ $current_sort_dir = !!$::form->{ $sort_spec->{FORM_PARAMS}->[1] } * 1;
+
+ # $::lxdebug->message(0, "saving current sort params to $current_sort_by / $current_sort_dir");
+}
+
+sub _callback_handler_for_sorted {
+ my ($self, %params) = @_;
+
+ if ($current_sort_by) {
+ my $sort_spec = $self->get_sort_spec;
+ $params{ $sort_spec->{FORM_PARAMS}->[0] } = $current_sort_by;
+ $params{ $sort_spec->{FORM_PARAMS}->[1] } = $current_sort_dir;
+ }
+
+ # $::lxdebug->dump(0, "CB handler for sorted; params nach modif:", \%params);
+
+ return %params;
+}
+
+sub _get_models_handler_for_sorted {
+ my ($self, %params) = @_;
+
+ my %sort_params = $self->get_current_sort_params;
+ my $sort_spec = $self->get_sort_spec->{ $sort_params{by} };
+
+ $params{model} = $sort_spec->{model};
+ $params{sort_by} = "SL::DB::Manager::$params{model}"->make_sort_string(sort_by => $sort_spec->{model_column}, sort_dir => $sort_params{dir});
+
+ # $::lxdebug->dump(0, "GM handler for sorted; params nach modif:", \%params);
+
+ return %params;
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Controller::Helper::Sorted - A helper for semi-automatic handling
+of sorting lists of database models in a controller
+
+=head1 SYNOPSIS
+
+In a controller:
+
+ use SL::Controller::Helper::GetModels;
+ use SL::Controller::Helper::Sorted;
+
+ __PACKAGE__->make_sorted(
+ DEFAULT_BY => 'run_at',
+ DEFAULT_DIR => 1,
+ MODEL => 'BackgroundJobHistory',
+ ONLY => [ qw(list) ],
+
+ error => $::locale->text('Error'),
+ package_name => $::locale->text('Package name'),
+ run_at => $::locale->text('Run at'),
+ );
+
+ sub action_list {
+ my ($self) = @_;
+
+ my $sorted_models = $self->get_sorted;
+ $self->render('controller/list', ENTRIES => $sorted_models);
+ }
+
+In said template:
+
+ [% USE L %]
+
+ <table>
+ <tr>
+ <th>[% L.sortable_table_header('package_name') %]</th>
+ <th>[% L.sortable_table_header('run_at') %]</th>
+ <th>[% L.sortable_table_header('error') %]</th>
+ </tr>
+
+ [% FOREACH entry = ENTRIES %]
+ <tr>
+ <td>[% HTML.escape(entry.package_name) %]</td>
+ <td>[% HTML.escape(entry.run_at) %]</td>
+ <td>[% HTML.escape(entry.error) %]</td>
+ </tr>
+ [% END %]
+ </table>
+
+=head1 OVERVIEW
+
+This specialized helper module enables controllers to display a
+sortable list of database models with as few lines as possible.
+
+For this to work the controller has to provide the information which
+indexes are eligible for sorting etc. by a call to L<make_sorted> at
+compile time.
+
+The underlying functionality that enables the use of more than just
+the sort helper is provided by the controller helper C<GetModels>. It
+provides mechanisms for helpers like this one to hook into certain
+calls made by the controller (C<get_callback> and C<get_models>) so
+that the specialized helpers can inject their parameters into the
+calls to e.g. C<SL::DB::Manager::SomeModel::get_all>.
+
+A template on the other hand can use the method
+C<sortable_table_header> from the layout helper module C<L>.
+
+The C<Sorted> helper hooks into the controller call to the action via
+a C<run_before> hook. This is done so that it can remember the sort
+parameters that were used in the current view.
+
+=head1 PACKAGE FUNCTIONS
+
+=over 4
+
+=item C<make_sorted %sort_spec>
+
+This function must be called by a controller at compile time. It is
+uesd to set the various parameters required for this helper to do its
+magic.
+
+There are two sorts of keys in the hash C<%sort_spec>. The first kind
+is written in all upper-case. Those parameters are control
+parameters. The second kind are all lower-case and represent indexes
+that can be used for sorting (similar to database column names). The
+second kind are also the indexes you use in a template when calling
+C<[% L.sorted_table_header(...) %]>.
+
+Control parameters include the following (all required parameters
+occur first):
+
+=over 4
+
+=item * C<DEFAULT_BY>
+
+Required. A string: the index to sort by if the user hasn't clicked on
+any column yet (meaning: if the C<$::form> parameters for sorting do
+not contain a valid index).
+
+=item * C<DEFAULT_DIR>
+
+Optional. Default sort direction (ascending for trueish values,
+descrending for falsish values).
+
+Defaults to C<1> if missing.
+
+=item * C<MODEL>
+
+Optional. A string: the name of the Rose database model that is used
+as a default in certain cases. If this parameter is missing then it is
+derived from the controller's package (e.g. for the controller
+C<SL::Controller::BackgroundJobHistory> the C<MODEL> would default to
+C<BackgroundJobHistory>).
+
+=item * C<FORM_PARAMS>
+
+Optional. An array reference with exactly two strings that name the
+indexes in C<$::form> in which the sort index (the first element in
+the array) and sort direction (the second element in the array) are
+stored.
+
+Defaults to the values C<sort_by> and C<sort_dir> if missing.
+
+=item * C<ONLY>
+
+Optional. An array reference containing a list of action names for
+which the sort parameters should be saved. If missing or empty then
+all actions invoked on the controller are monitored.
+
+=back
+
+All keys that are written in all lower-case name indexes that can be
+used for sorting. Each value to such a key can be either a string or a
+hash reference containing certain elements. If the value is only a
+string then such a hash reference is constructed, and the string is
+used as the value for the C<title> key.
+
+These possible elements are:
+
+=over 4
+
+=item * C<title>
+
+Required. A user-displayable title to be used by functions like the
+layout helper's C<sortable_table_header>. Does not have a default
+value.
+
+=item * C<model>
+
+Optional. The name of a Rose database model this sort index refers
+to. If missing then the value of C<$sort_spec{MODEL}> is used.
+
+=item * C<model_column>
+
+Optional. The name of the Rose database model column this sort index
+refers to. It must be one of the columns named by the model's
+C<Sorted> helper (not to be confused with the controller's C<Sorted>
+helper!).
+
+If missing it defaults to the key in C<%sort_spec> for which this hash
+reference is the value.
+
+=back
+
+=back
+
+=head1 INSTANCE FUNCTIONS
+
+These functions are called on a controller instance.
+
+=over 4
+
+=item C<get_sort_spec>
+
+Returns a hash containing the currently active sort parameters.
+
+The key C<by> contains the active sort index referring to the
+C<%sort_spec> given to L<make_sorted>.
+
+The key C<dir> is either C<1> or C<0>.
+
+=item C<get_current_sort_params>
+
+Returns a hash reference to the sort spec structure given in the call
+to L<make_sorted> after normalization (hash reference construction,
+applying default parameters etc).
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
return substr($text, 0, $params{at}) . '...';
}
+sub sortable_table_header {
+ my ($self, $by, @slurp) = @_;
+ my %params = _hashify(@slurp);
+
+ my $controller = $self->{CONTEXT}->stash->get('SELF');
+ my $sort_spec = $controller->get_sort_spec;
+ my $by_spec = $sort_spec->{$by};
+ my %current_sort_params = $controller->get_current_sort_params;
+ my ($image, $new_dir) = ('', $current_sort_params{dir});
+ my $title = delete($params{title}) || $by_spec->{title};
+
+ if ($current_sort_params{by} eq $by) {
+ my $current_dir = $current_sort_params{dir} ? 'up' : 'down';
+ $image = '<img border="0" src="image/' . $current_dir . '.png">';
+ $new_dir = 1 - ($current_sort_params{dir} || 0);
+ }
+
+ $params{ $sort_spec->{FORM_PARAMS}->[0] } = $by;
+ $params{ $sort_spec->{FORM_PARAMS}->[1] } = ($new_dir ? '1' : '0');
+
+ return '<a href="' . $controller->get_callback(%params) . '">' . _H($title) . $image . '</a>';
+}
+
1;
__END__
Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
+=item C<sortable_table_header $by, %params>
+
+Create a link and image suitable for placement in a table
+header. C<$by> must be an index set up by the controller with
+L<SL::Controller::Helper::make_sorted>.
+
+The optional parameter C<$params{title}> can override the column title
+displayed to the user. Otherwise the column title from the
+controller's sort spec is used.
+
+The other parameters in C<%params> are passed unmodified to the
+underlying call to L<SL::Controller::Base::url_for>.
+
+See the documentation of L<SL::Controller::Helper::Sorted> for an
+overview and further usage instructions.
+
=back
=head2 CONVERSION FUNCTIONS