Controller-Helfer für das halbautomatische Sortieren von Listenansichten
authorMoritz Bunkus <m.bunkus@linet-services.de>
Fri, 31 Aug 2012 13:50:37 +0000 (15:50 +0200)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Fri, 31 Aug 2012 14:14:55 +0000 (16:14 +0200)
SL/Controller/Helper/GetModels.pm [new file with mode: 0644]
SL/Controller/Helper/Sorted.pm [new file with mode: 0644]
SL/Template/Plugin/L.pm

diff --git a/SL/Controller/Helper/GetModels.pm b/SL/Controller/Helper/GetModels.pm
new file mode 100644 (file)
index 0000000..4658717
--- /dev/null
@@ -0,0 +1,166 @@
+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
diff --git a/SL/Controller/Helper/Sorted.pm b/SL/Controller/Helper/Sorted.pm
new file mode 100644 (file)
index 0000000..7f5f452
--- /dev/null
@@ -0,0 +1,319 @@
+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
index 8523db9..fdb22b3 100644 (file)
@@ -582,6 +582,29 @@ sub truncate {
   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__
@@ -864,6 +887,22 @@ containing the values C<[ 6, 2, 15 ]>.
 
 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