From 9deadd1d1a37ce24b807132c00f4890785400683 Mon Sep 17 00:00:00 2001 From: Moritz Bunkus Date: Fri, 31 Aug 2012 15:50:37 +0200 Subject: [PATCH] =?utf8?q?Controller-Helfer=20f=C3=BCr=20das=20halbautomat?= =?utf8?q?ische=20Sortieren=20von=20Listenansichten?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- SL/Controller/Helper/GetModels.pm | 166 ++++++++++++++++ SL/Controller/Helper/Sorted.pm | 319 ++++++++++++++++++++++++++++++ SL/Template/Plugin/L.pm | 39 ++++ 3 files changed, 524 insertions(+) create mode 100644 SL/Controller/Helper/GetModels.pm create mode 100644 SL/Controller/Helper/Sorted.pm diff --git a/SL/Controller/Helper/GetModels.pm b/SL/Controller/Helper/GetModels.pm new file mode 100644 index 000000000..4658717f6 --- /dev/null +++ b/SL/Controller/Helper/GetModels.pm @@ -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. + +=head1 OVERVIEW + +For a generic overview see L. + +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 during compilation time (e.g. in the +case of C this happens when the controller calls +L). + +A controller will later usually call the L +function. Templates will call the L 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 helper hooks into the controller call to the action +via a C hook. This is done so that it can remember the +action called by the user. This is used for constructing the callback +in L. + +=head1 PACKAGE FUNCTIONS + +=over 4 + +=item C + +This function should only be called from other controller helpers like +C or C. 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 then it is passed to the hook +registration in L. + +The C<%handlers> register callback functions in the specialized +controller helpers that are called during invocation of +L or L. Possible keys are C and +C. + +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 or L 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 + +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 and C) 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 + +Query the model manager via C and return its result. The +parameters to C are constructed by calling all registered +handlers of type 'models' (e.g. the ones by C and +C). + +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 models. + +=back + +=head1 BUGS + +Nothing here yet. + +=head1 AUTHOR + +Moritz Bunkus Em.bunkus@linet-services.deE + +=cut diff --git a/SL/Controller/Helper/Sorted.pm b/SL/Controller/Helper/Sorted.pm new file mode 100644 index 000000000..7f5f4526d --- /dev/null +++ b/SL/Controller/Helper/Sorted.pm @@ -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 %] + + + + + + + + + [% FOREACH entry = ENTRIES %] + + + + + + [% END %] +
[% L.sortable_table_header('package_name') %][% L.sortable_table_header('run_at') %][% L.sortable_table_header('error') %]
[% HTML.escape(entry.package_name) %][% HTML.escape(entry.run_at) %][% HTML.escape(entry.error) %]
+ +=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 at +compile time. + +The underlying functionality that enables the use of more than just +the sort helper is provided by the controller helper C. It +provides mechanisms for helpers like this one to hook into certain +calls made by the controller (C and C) so +that the specialized helpers can inject their parameters into the +calls to e.g. C. + +A template on the other hand can use the method +C from the layout helper module C. + +The C helper hooks into the controller call to the action via +a C 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 + +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 + +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 + +Optional. Default sort direction (ascending for trueish values, +descrending for falsish values). + +Defaults to C<1> if missing. + +=item * C + +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 the C would default to +C). + +=item * C + +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 and C if missing. + +=item * C + +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 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 diff --git a/SL/Template/Plugin/L.pm b/SL/Template/Plugin/L.pm index 8523db9e0..fdb22b31a 100644 --- a/SL/Template/Plugin/L.pm +++ b/SL/Template/Plugin/L.pm @@ -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 -- 2.20.1