Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision d820c116

Von Sven Schöling vor mehr als 11 Jahren hinzugefügt

  • ID d820c1162bb08a580dfb4d01800d0406b001e169
  • Vorgänger 061cb2d3
  • Nachfolger a6a6121c

Filtered Plugin für GetModels

Unterschiede anzeigen:

SL/Controller/Helper/Filtered.pm
1
package SL::Controller::Helper::Filtered;
2

  
3
use strict;
4

  
5
use Exporter qw(import);
6
use SL::Controller::Helper::ParseFilter ();
7
use List::MoreUtils qw(uniq);
8
our @EXPORT = qw(make_filtered get_filter_spec get_current_filter_params disable_filtering _save_current_filter_params _callback_handler_for_filtered _get_models_handler_for_filtered);
9

  
10
use constant PRIV => '__filteredhelper_priv';
11

  
12
my %controller_filter_spec;
13

  
14
sub make_filtered {
15
  my ($class, %specs)       = @_;
16

  
17
  $specs{MODEL}           //=  $class->controller_name;
18
  $specs{MODEL}             =~ s{ ^ SL::DB:: (?: .* :: )? }{}x;
19
  $specs{FORM_PARAMS}     //= 'filter';
20
  $specs{LAUNDER_TO}        = '__INPLACE__' unless exists $specs{LAUNDER_TO};
21
  $specs{ONLY}            //= [];
22
  $specs{ONLY}              = [ $specs{ONLY} ] if !ref $specs{ONLY};
23
  $specs{ONLY_MAP}          = @{ $specs{ONLY} } ? { map { ($_ => 1) } @{ $specs{ONLY} } } : { '__ALL__' => 1 };
24

  
25
  $controller_filter_spec{$class} = \%specs;
26

  
27
  my %hook_params           = @{ $specs{ONLY} } ? ( only => $specs{ONLY} ) : ();
28
  $class->run_before('_save_current_filter_params', %hook_params);
29

  
30
  SL::Controller::Helper::GetModels::register_get_models_handlers(
31
    $class,
32
    callback   => '_callback_handler_for_filtered',
33
    get_models => '_get_models_handler_for_filtered',
34
    ONLY       => $specs{ONLY},
35
  );
36

  
37
  # $::lxdebug->dump(0, "CONSPEC", \%specs);
38
}
39

  
40
sub get_filter_spec {
41
  my ($class_or_self) = @_;
42

  
43
  return $controller_filter_spec{ref($class_or_self) || $class_or_self};
44
}
45

  
46
sub get_current_filter_params {
47
  my ($self)   = @_;
48

  
49
  return %{ _priv($self)->{filter_params} } if _priv($self)->{filter_params};
50

  
51
  require Carp;
52
  Carp::confess('It seems a GetModels plugin tries to access filter params before they got calculated. Make sure your make_filtered call comes first.');
53
}
54

  
55
sub _make_current_filter_params {
56
  my ($self, %params)   = @_;
57

  
58
  my $spec              = $self->get_filter_spec;
59
  my $filter            = $params{filter} // _priv($self)->{filter} // {},
60
  my %filter_args       = _get_filter_args($self, $spec);
61
  my %parse_filter_args = (
62
    class        => "SL::DB::Manager::$spec->{MODEL}",
63
    with_objects => $params{with_objects},
64
  );
65
  my $laundered;
66
  if ($spec->{LAUNDER_TO} eq '__INPLACE__') {
67

  
68
  } elsif ($spec->{LAUNDER_TO}) {
69
    $laundered = {};
70
    $parse_filter_args{launder_to} = $laundered;
71
  } else {
72
    $parse_filter_args{no_launder} = 1;
73
  }
74

  
75
  my %calculated_params = SL::Controller::Helper::ParseFilter::parse_filter($filter, %parse_filter_args);
76

  
77
  $calculated_params{query} = [
78
    @{ $calculated_params{query} || [] },
79
    @{ $filter_args{query} || [] },
80
    @{ $params{query} || [] },
81
  ];
82

  
83
  $calculated_params{with_objects} = [
84
    uniq
85
    @{ $calculated_params{with_objects} || [] },
86
    @{ $filter_args{with_objects} || [] },
87
    @{ $params{with_objects} || [] },
88
  ];
89

  
90
  if ($laundered) {
91
    if ($self->can($spec->{LAUNDER_TO})) {
92
      $self->${\ $spec->{LAUNDER_TO} }($laundered);
93
    } else {
94
      $self->{$spec->{LAUNDER_TO}} = $laundered;
95
    }
96
  }
97

  
98
  # $::lxdebug->dump(0, "get_current_filter_params: ", \%calculated_params);
99

  
100
  _priv($self)->{filter_params} = \%calculated_params;
101

  
102
  return %calculated_params;
103
}
104

  
105
sub disable_filtering {
106
  my ($self)               = @_;
107
  _priv($self)->{disabled} = 1;
108
}
109

  
110
#
111
# private functions
112
#
113

  
114
sub _get_filter_args {
115
  my ($self, $spec) = @_;
116

  
117
  $spec ||= $self->get_filter_spec;
118

  
119
  my %filter_args       = ref($spec->{FILTER_ARGS}) eq 'CODE' ? %{ $spec->{FILTER_ARGS}->($self) }
120
                        :     $spec->{FILTER_ARGS}            ? do { my $sub = $spec->{FILTER_ARGS}; %{ $self->$sub() } }
121
                        :                                       ();
122
}
123

  
124
sub _save_current_filter_params {
125
  my ($self)        = @_;
126

  
127
  return if !_is_enabled($self);
128

  
129
  my $filter_spec = $self->get_filter_spec;
130
  $self->{PRIV()}{filter} = $::form->{ $filter_spec->{FORM_PARAMS} };
131

  
132
  # $::lxdebug->message(0, "saving current filter params to " . $self->{PRIV()}->{page} . ' / ' . $self->{PRIV()}->{per_page});
133
}
134

  
135
sub _callback_handler_for_filtered {
136
  my ($self, %params) = @_;
137
  my $priv            = _priv($self);
138

  
139
  if (_is_enabled($self) && $priv->{filter}) {
140
    my $filter_spec                             = $self->get_filter_spec;
141
    my ($flattened) = SL::Controller::Helper::ParseFilter::flatten($priv->{filter}, undef, $filter_spec->{FORM_PARAMS});
142
    %params = (%params, @$flattened);
143
  }
144

  
145
  # $::lxdebug->dump(0, "CB handler for filtered; params after flatten:", \%params);
146

  
147
  return %params;
148
}
149

  
150
sub _get_models_handler_for_filtered {
151
  my ($self, %params)    = @_;
152
  my $spec               = $self->get_filter_spec;
153

  
154
  # $::lxdebug->dump(0,  "params in get_models_for_filtered", \%params);
155

  
156
  my %filter_params;
157
  %filter_params = _make_current_filter_params($self, %params)  if _is_enabled($self);
158

  
159
  # $::lxdebug->dump(0, "GM handler for filtered; params nach modif (is_enabled? " . _is_enabled($self) . ")", \%params);
160

  
161
  return (%params, %filter_params);
162
}
163

  
164
sub _priv {
165
  my ($self)        = @_;
166
  $self->{PRIV()} ||= {};
167
  return $self->{PRIV()};
168
}
169

  
170
sub _is_enabled {
171
  my ($self) = @_;
172
  return !_priv($self)->{disabled} && ($self->get_filter_spec->{ONLY_MAP}->{$self->action_name} || $self->get_filter_spec->{ONLY_MAP}->{'__ALL__'});
173
}
174

  
175

  
176
1;
177

  
178
__END__
179

  
180
=pod
181

  
182
=encoding utf8
183

  
184
=head1 NAME
185

  
186
SL::Controller::Helper::Filtered - A helper for semi-automatic handling
187
of filtered lists of database models in a controller
188

  
189
=head1 SYNOPSIS
190

  
191
In a controller:
192

  
193
  use SL::Controller::Helper::GetModels;
194
  use SL::Controller::Helper::Filtered;
195

  
196
  __PACKAGE__->make_filter(
197
    MODEL       => 'Part',
198
    ONLY        => [ qw(list) ],
199
    FORM_PARAMS => [ qw(filter) ],
200
  );
201

  
202
  sub action_list {
203
    my ($self) = @_;
204

  
205
    my $filtered_models = $self->get_models(%addition_filters);
206
    $self->render('controller/list', ENTRIES => $filtered_models);
207
  }
208

  
209

  
210
=head1 OVERVIEW
211

  
212
This helper module enables use of the L<SL::Controller::Helper::ParseFilter>
213
methods in conjunction with the L<SL::Controller::Helper::GetModels> style of
214
plugins. Additional filters can be defined in the database models and filtering
215
can be reduced to a minimum of work.
216

  
217
This plugin can be combined with L<SL::Controller::Sorted> and
218
L<SL::Controller::Paginated> for filtered, sorted and paginated lists.
219

  
220
The controller has to provive information where to look for filter information
221
at compile time. This call is L<make_filtered>.
222

  
223
The underlying functionality that enables the use of more than just
224
the paginate helper is provided by the controller helper
225
C<GetModels>. See the documentation for L<SL::Controller::Sorted> for
226
more information on it.
227

  
228
=head1 PACKAGE FUNCTIONS
229

  
230
=over 4
231

  
232
=item C<make_filtered %filter_spec>
233

  
234
This function must be called by a controller at compile time. It is
235
uesd to set the various parameters required for this helper to do its
236
magic.
237

  
238
Careful: If you want to use this in conjunction with
239
L<SL:Controller::Helper::Paginated>, you need to call C<make_filtered> first,
240
or the paginating will not get all the relevant information to estimate the
241
number of pages correctly. To ensure this does not happen, this module will
242
croak when it detects such a scenario.
243

  
244
The hash C<%filter_spec> can include the following parameters:
245

  
246
=over 4
247

  
248
=item * C<MODEL>
249

  
250
Optional. A string: the name of the Rose database model that is used
251
as a default in certain cases. If this parameter is missing then it is
252
derived from the controller's package (e.g. for the controller
253
C<SL::Controller::BackgroundJobHistory> the C<MODEL> would default to
254
C<BackgroundJobHistory>).
255

  
256
=item * C<FORM_PARAMS>
257

  
258
Optional. Indicates a key in E<$::form> to be used as filter.
259

  
260
Defaults to the values C<filter> if missing.
261

  
262
=item * C<LAUNDER_TO>
263

  
264
Option. Indicates a target for laundered filter arguments in the controller.
265
Can be set to C<undef> to disable laundering, and can be set to method named or
266
hash keys of the controller. In the latter case the laundered structure will be
267
put there.
268

  
269
Defaults to inplace laundering which is not normally settable.
270

  
271
=item * C<ONLY>
272

  
273
Optional. An array reference containing a list of action names for
274
which the paginate parameters should be saved. If missing or empty then
275
all actions invoked on the controller are monitored.
276

  
277
=back
278

  
279
=back
280

  
281
=head1 INSTANCE FUNCTIONS
282

  
283
These functions are called on a controller instance.
284

  
285
=over 4
286

  
287
=item C<get_current_filter_params>
288

  
289
Returns a hash to be used in manager C<get_all> calls or to be passed on to
290
GetModels. Will only work if the get_models chain has been called at least
291
once, because only then the full parameters can get parsed and stored. Will
292
croak otherwise.
293

  
294
=item C<disable_filtering>
295

  
296
Disable filtering for the duration of the current action. Can be used
297
when using the attribute C<ONLY> to L<make_filtered> does not
298
cover all cases.
299

  
300
=back
301

  
302
=head1 BUGS
303

  
304
Nothing here yet.
305

  
306
=head1 AUTHOR
307

  
308
Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
309

  
310
=cut
SL/Controller/Helper/GetModels.pm
48 48
sub get_models {
49 49
  my ($self, %override_params) = @_;
50 50

  
51
  my %default_params           = _run_handlers($self, 'get_models');
51
  my %params                   = _run_handlers($self, 'get_models', %override_params);
52 52

  
53
  my %params                   = (%default_params, %override_params);
54 53
  my $model                    = delete($params{model}) || die "No 'model' to work on";
55 54

  
56 55
  return "SL::DB::Manager::${model}"->get_all(%params);
SL/Controller/Helper/Paginated.pm
18 18
  $specs{MODEL}             =~ s{ ^ SL::DB:: (?: .* :: )? }{}x;
19 19
  $specs{PER_PAGE}        ||= "SL::DB::Manager::$specs{MODEL}"->default_objects_per_page;
20 20
  $specs{FORM_PARAMS}     ||= [ qw(page per_page) ];
21
  $specs{PAGINATE_ARGS}   ||= '__FILTER__';
21 22
  $specs{ONLY}            ||= [];
22 23
  $specs{ONLY}              = [ $specs{ONLY} ] if !ref $specs{ONLY};
23 24
  $specs{ONLY_MAP}          = @{ $specs{ONLY} } ? { map { ($_ => 1) } @{ $specs{ONLY} } } : { '__ALL__' => 1 };
......
58 59
  );
59 60

  
60 61
  my %paginate_args     = ref($spec->{PAGINATE_ARGS}) eq 'CODE' ? %{ $spec->{PAGINATE_ARGS}->($self) }
62
                        :     $spec->{PAGINATE_ARGS}  eq '__FILTER__' ? $self->get_current_filter_params
61 63
                        :     $spec->{PAGINATE_ARGS}            ? do { my $sub = $spec->{PAGINATE_ARGS}; %{ $self->$sub() } }
62 64
                        :                                         ();
63 65
  my $calculated_params = "SL::DB::Manager::$spec->{MODEL}"->paginate(%paginate_params, args => \%paginate_args);
SL/Controller/Helper/ParseFilter.pm
8 8
use DateTime;
9 9
use SL::Helper::DateTime;
10 10
use List::MoreUtils qw(uniq);
11
use SL::MoreCommon qw(listify);
11 12
use Data::Dumper;
12 13

  
13 14
my %filters = (
......
149 150
   my ($array, $what) = @_;
150 151

  
151 152
   $array //= [];
152
   $array = [ uniq @$array, $what ];
153
   $array = [ uniq @$array, listify($what) ];
153 154
}
154 155

  
155 156
sub _collapse_indirect_filters {
SL/DB/Helper/Filtered.pm
1
package SL::DB::Helper::Filtered;
2

  
3
use strict;
4
use SL::Controller::Helper::ParseFilter ();
5

  
6
require Exporter;
7
our @ISA    = qw(Exporter);
8
our @EXPORT = qw (filter add_filter_specs);
9

  
10
my %filter_spec;
11

  
12
sub filter {
13
  my ($class, $key, $value, $prefix) = @_;
14

  
15
  my $filters = _get_filters($class);
16

  
17
  return ($key, $value) unless $filters->{$key};
18

  
19
  return $filters->{$key}->($key, $value, $prefix);
20
}
21

  
22
sub _get_filters {
23
  my ($class) = @_;
24
  return $filter_spec{$class} ||= {};
25
}
26

  
27
sub add_filter_specs {
28
  my $class = shift;
29

  
30
  my $filters = _get_filters($class);
31

  
32
  while (@_ > 1) {
33
    my $key          = shift;
34
    $filters->{$key} = shift;
35
  }
36
}
37

  
38
1;
39

  
40
__END__
41

  
42
=encoding utf-8
43

  
44
=head1 NAME
45

  
46
SL::Helper::Sorted - Manager mixin for filtered results.
47

  
48
=head1 SYNOPSIS
49

  
50
In the manager:
51

  
52
  use SL::Helper::Filtered;
53

  
54
  __PACKAGE__->add_filter_specs(
55
    custom_filter_name => sub {
56
      my ($key, $value, $prefix) = @_;
57
      # code to handle this
58
      return ($key, $value, $with_objects);
59
    },
60
    another_filter_name => \&_sub_to_handle_this,
61
  );
62

  
63
In consuming code:
64

  
65
  ($key, $value, $with_objects) = $manager_class->filter($key, $value, $prefix);
66

  
67
=head1 FUNCTIONS
68

  
69
=over 4
70

  
71
=item C<add_filter_specs %PARAMS>
72

  
73
Adds new filters to this package as key value pairs. The key will be the new
74
filters name, the value is expected to be a coderef to an implementation of
75
this filter. See L<INTERFACE OF A CUSTOM FILTER> for details on this.
76

  
77
You can add multiple filters in one call, but only one filter per key.
78

  
79
=item C<filter $key, $value, $prefix>
80

  
81
Tells the manager to pply custom filters. If none is registered for C<$key>,
82
returns C<$key, $value>.
83

  
84
Otherwise the filter code is called.
85

  
86
=back
87

  
88
=head1 INTERFACE OF A CUSTOM FILTER
89

  
90
Lets look at an example of a working filter. Suppose your model has a lot of
91
notes fields, and you need to search in all of them. A working filter would be:
92

  
93
  __PACKAGE__->add_filter_specs(
94
    all_notes => sub {
95
      my ($key, $value, $prefix) = @_;
96

  
97
      return or => [
98
        $prefix . notes1 => $value,
99
        $prefix . notes2 => $value,
100
      ];
101
    }
102
  );
103

  
104
If someone filters for C<filter.model.all_notes:substr::ilike=telephone>, your
105
filter will get called with:
106

  
107
  ->filter('all_notes', { ilike => '%telephone%' }, '')
108

  
109
and the result will be:
110

  
111
  or => [
112
    notes1 => { notes1 => '%telephone%' },
113
    notes2 => { notes1 => '%telephone%' },
114
  ]
115

  
116
The prefix is to make sure this also works when called on submodels:
117

  
118
  C<filter.customer.model.all_notes:substr::ilike=telephone>
119

  
120
will pass C<customer.> as prefix so that the resulting query will be:
121

  
122
  or => [
123
    customer.notes1 => { notes1 => '%telephone%' },
124
    customer.notes2 => { notes1 => '%telephone%' },
125
  ]
126

  
127
which is pretty much what you would expect.
128

  
129
As a final touch consider a filter that needs to search somewhere else to work,
130
like this one:
131

  
132
  __PACKAGE__->add_filter_specs(
133
    name => sub {
134
      my ($key, $value, $prefix) = @_;
135

  
136
      return $prefix . person.name => $value,
137
             $prefix . 'person';
138
    },
139
  };
140

  
141
Now you can search for C<name> in your model without ever knowing that the real
142
name lies in the table C<person>. Unfortunately Rose has to know about it to
143
get the joins right, and so you need to tell it to include C<person> into its
144
C<with_objects>. That's the reason for the third return value.
145

  
146

  
147
To summarize:
148

  
149
=over 4
150

  
151
=item *
152

  
153
You will get passed the name of your filter as C<$key> stripped of all filters
154
and escapes.
155

  
156
=item *
157

  
158
You will get passed the C<$value> processed with all filters and escapes.
159

  
160
=item *
161

  
162
You will get passed a C<$prefix> that can be prepended to all database columns
163
to make sense to Rose.
164

  
165
=item *
166

  
167
You are expeceted to return exactly one key and one value. That can mean you
168
have to encapsulate your arguments into C<< or => [] >> or C<< and => [] >> blocks.
169

  
170
=item *
171

  
172
If your filter needs relationships that are not always loaded, you need to
173
return them in C<with_objects> style. If you need to return more than one, use
174
an arrayref.
175

  
176
=back
177

  
178
=head1 BUGS
179

  
180
None yet.
181

  
182
=head1 AUTHOR
183

  
184
Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
185

  
186
=cut

Auch abrufbar als: Unified diff