Revision d820c116
Von Sven Schöling vor mehr als 11 Jahren hinzugefügt
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
Filtered Plugin für GetModels