861834b83c9091748423c69e63fe5fa0d605d0e2
[kivitendo-erp.git] / SL / Controller / Helper / GetModels.pm
1 package SL::Controller::Helper::GetModels;
2
3 use strict;
4
5 use parent 'Rose::Object';
6 use SL::Controller::Helper::GetModels::Filtered;
7 use SL::Controller::Helper::GetModels::Sorted;
8 use SL::Controller::Helper::GetModels::Paginated;
9
10 use Scalar::Util qw(weaken);
11
12 use Rose::Object::MakeMethods::Generic (
13   scalar => [ qw(controller model query with_objects filtered sorted paginated finalized final_params) ],
14   'scalar --get_set_init' => [ qw(handlers source additional_url_params) ],
15   array => [ qw(plugins) ],
16 );
17
18 use constant PRIV => '__getmodelshelperpriv';
19
20
21 # official interface
22
23 sub get {
24   my ($self) = @_;
25   my %params = $self->finalize;
26
27   return $self->manager->get_all(%params);
28 }
29
30 sub count {
31   my ($self) = @_;
32   my %params = $self->finalize;
33
34   return $self->manager->get_all_count(%params);
35 }
36
37 sub disable_plugin {
38   my ($self, $plugin) = @_;
39   die 'cannot change internal state after finalize was called' if $self->finalized;
40   die 'unsupported plugin' unless $self->can($plugin) && $self->$plugin && $self->$plugin->isa('SL::Controller::Helper::GetModels::Base');
41
42   $self->$plugin->disabled(1);
43 }
44
45 sub enable_plugin {
46   my ($self, $plugin) = @_;
47   die 'cannot change internal state after finalize was called' if $self->finalized;
48   die 'unsupported plugin' unless $self->can($plugin) && $self->$plugin && $self->$plugin->isa('SL::Controller::Helper::GetModels::Base');
49   $self->$plugin->disabled(0);
50 }
51
52 sub is_enabled_plugin {
53   my ($self, $plugin) = @_;
54   die 'unsupported plugin' unless $self->can($plugin) && $self->$plugin && $self->$plugin->isa('SL::Controller::Helper::GetModels::Base');
55   $self->$plugin->is_enabled;
56 }
57
58 # TODO: get better delegation
59 sub set_report_generator_sort_options {
60   my ($self, %params) = @_;
61   $self->finalize;
62
63   $self->sorted->set_report_generator_sort_options(%params);
64 }
65
66 sub get_paginate_args {
67   my ($self) = @_;
68   my %params = $self->finalize;
69
70   $self->paginated->get_current_paginate_params(%params);
71 }
72
73 sub get_sort_spec {
74   my ($self) = @_;
75
76   $self->sorted->specs;
77 }
78
79 sub get_current_sort_params {
80   my ($self) = @_;
81
82   $self->sorted->read_params;
83 }
84
85 sub init {
86   my ($self, %params) = @_;
87
88   my $model = delete $params{model};
89   if (!$model && $params{controller} && ref $params{controller}) {
90     $model = ref $params{controller};
91     $model =~ s/.*:://;
92     die 'Need a valid model' unless $model;
93   }
94   $self->model($model);
95
96   my @plugins;
97   for my $plugin (qw(filtered sorted paginated)) {
98     next unless my $spec = delete $params{$plugin} // {};
99     my $plugin_class = "SL::Controller::Helper::GetModels::" . ucfirst $plugin;
100     push @plugins, $self->$plugin($plugin_class->new(%$spec, get_models => $self));
101   }
102   $self->plugins(@plugins);
103
104   $self->SUPER::init(%params);
105
106   $_->read_params for $self->plugins;
107
108   weaken $self->controller if $self->controller;
109 }
110
111 sub finalize {
112   my ($self, %params) = @_;
113
114   return %{ $self->final_params } if $self->finalized;
115
116   $self->register_handlers(callback => sub { shift; (@_, %{ $self->additional_url_params }) }) if %{ $self->additional_url_params };
117
118   push @{ $params{query}        ||= [] }, @{ $self->query || [] };
119   push @{ $params{with_objects} ||= [] }, @{ $self->with_objects || [] };
120
121   %params = $_->finalize(%params) for $self->plugins;
122
123   $self->finalized(1);
124   $self->final_params(\%params);
125
126   return %params;
127 }
128
129 sub register_handlers {
130   my ($self, %additional_handlers) = @_;
131
132   my $handlers    = $self->handlers;
133   map { push @{ $handlers->{$_} }, $additional_handlers{$_} if $additional_handlers{$_} } keys %$handlers;
134 }
135
136 sub add_additional_url_params {
137   my ($self, %params) = @_;
138
139   $self->additional_url_params({ %{ $self->additional_url_params }, %params });
140
141   return $self;
142 }
143
144 sub get_models_url_params {
145   my ($self, $sub_name_or_code) = @_;
146
147   my $code     = (ref($sub_name_or_code) || '') eq 'CODE' ? $sub_name_or_code : sub { shift->controller->$sub_name_or_code(@_) };
148   my $callback = sub {
149     my ($self, %params)   = @_;
150     my @additional_params = $code->($self);
151     return (
152       %params,
153       (scalar(@additional_params) == 1) && (ref($additional_params[0]) eq 'HASH') ? %{ $additional_params[0] } : @additional_params,
154     );
155   };
156
157   $self->register_handlers('callback' => $callback);
158 }
159
160 sub get_callback_params {
161   my ($self, %override_params) = @_;
162
163   my %default_params = $self->_run_handlers('callback', action => $self->controller->action_name);
164 }
165
166 sub get_callback {
167   my ($self, %override_params) = @_;
168
169   my %default_params = $self->get_callback_params(%override_params);
170
171   return $self->controller->url_for(%default_params, %override_params);
172 }
173
174 sub manager {
175   die "No 'model' to work on" unless $_[0]->model;
176   "SL::DB::Manager::" . $_[0]->model;
177 }
178
179 #
180 # private/internal functions
181 #
182
183 sub _run_handlers {
184   my ($self, $handler_type, %params) = @_;
185
186   foreach my $sub (@{ $self->handlers->{$handler_type} }) {
187     if (ref $sub eq 'CODE') {
188       %params = $sub->($self, %params);
189     } elsif ($self->can($sub)) {
190       %params = $self->$sub(%params);
191     } else {
192       die "SL::Controller::Helper::GetModels::get_callback: Cannot call $sub on " . ref($self) . ")";
193     }
194   }
195
196   return %params;
197 }
198
199 sub init_handlers {
200   {
201     callback => [],
202   }
203 }
204
205 sub init_source {
206   $::form
207 }
208
209 sub init_additional_url_params { +{} }
210
211 1;
212 __END__
213
214 =pod
215
216 =encoding utf8
217
218 =head1 NAME
219
220 SL::Controller::Helper::GetModels - Base class for the GetModels system.
221
222 =head1 SYNOPSIS
223
224 In controller:
225
226   use SL::Controller::Helper::GetModels;
227
228   my $get_models = SL::Controller::Helper::GetModels->new(
229     controller   => $self,
230   );
231
232   my $models = $self->get_models->get;
233
234 =head1 OVERVIEW
235
236 Building a CRUD controller would be easy, were it not for those stupid
237 list actions. People unreasonably expect stuff like filtering, sorting,
238 paginating, exporting etc simply to work. Well, lets try to make it simply work
239 a little.
240
241 This class is a proxy between a controller and specialized
242 helper modules that handle these things (sorting, paginating etc) and gives you
243 the means to retrieve the information when needed to display sort headers or
244 paginating footers.
245
246 Information about the requested data query can be stored in the object up to
247 a certain point, from which on the object becomes locked and can only be
248 accessed for information. (See C<STATES>).
249
250 =head1 INTERFACE METHODS
251
252 =over 4
253
254 =item new PARAMS
255
256 Create a new GetModels object. Params must have at least an entry
257 C<controller>, other than that, see C<CONFIGURATION> for options.
258
259 =item get
260
261 Retrieve all models for the current configuration. Will finalize the object.
262
263 =item get_models_url_params SUB
264
265 Register a sub to be called whenever an URL has to be generated (e.g. for sort
266 and pagination links). This is a way for the controller to add additional
267 parameters to the URL (e.g. for filter parameters).
268
269 The parameter can be either a code reference or the name of
270 one of the controller's functions.
271
272 The value returned by C<SUB> must be either a single hash
273 reference or a hash of key/value pairs to add to the URL.
274
275 =item add_additional_url_params C<%params>
276
277 Sets additional parameters that will be added to each URL generated by
278 this model (e.g. for pagination/sorting). This is just sugar for a
279 proper call to L<get_models_url_params> with an anonymous sub adding
280 those parameters.
281
282 =item get_callback
283
284 Returns a URL suitable for use as a callback parameter. It maps to the
285 current controller and action. All registered handlers of type
286 'callback' (e.g. the ones by C<Sorted> and C<Paginated>) can inject
287 the parameters they need so that the same list view as is currently
288 visible can be re-rendered.
289
290 Optional C<%params> passed to this function may override any parameter
291 set by the registered handlers.
292
293 =item enable_plugin PLUGIN
294
295 =item disable_plugin PLUGIN
296
297 =item is_enabled_plugin PLUGIN
298
299 Enable or disable the specified plugin. Useful to disable paginating for
300 exports for example. C<is_enabled_plugin> can be used to check the current
301 state fo a plugin.
302
303 Must not be finalized to use this.
304
305 =item finalize
306
307 Forces finalized state. Can be used on finalized objects without error.
308
309 Note that most higher functions will call this themselves to force a finalized
310 state. If you do use it it must come before any other finalizing methods, and
311 will most likely function as a reminder for maintainers where your code
312 switches from configuration to finalized state.
313
314 =item source HASHREF
315
316 The source for user supplied information. Defaults to $::form. Changing it
317 after C<Base> phase has no effect.
318
319 =item controller CONTROLLER
320
321 A weakened link to the controller that created the GetModels object. Needed for
322 certain plugin methods.
323
324 =back
325
326 =head1 DELEGATION METHODS
327
328 All of these finalize.
329
330 Methods delegating to C<Sorted>:
331
332 =over 4
333
334 =item *
335
336 set_report_generator_sort_options
337
338 =item *
339
340 get_sort_spec
341
342 =item *
343
344 get_current_sort_params
345
346 =back
347
348 Methods delegating to C<Paginated>:
349
350 =over 4
351
352 =item *
353
354 get_paginate_args
355
356 =back
357
358 =head1 STATES
359
360 A GetModels object is in one of 3 states at any given time. Their purpose is to
361 make a class of bugs impossible that orginated from changing the configuration
362 of a GetModels object halfway during the request. This was a huge problem in
363 the old implementation.
364
365 =over 4
366
367 =item Base
368
369 This is the state after creating a new object.
370
371 =item Init
372
373 In this state all the information needed from the source ($::form) has been read
374 and subsequent changes to the source have no effect. In the current
375 implementation this will happen during creation, so that the return value of
376 C<new> is already in state C<Init>.
377
378 =item Finalized
379
380 In this state no new configuration will be accepted so that information gotten
381 through the various methods is consistent. Every information retrieval method
382 will trigger finalize.
383
384 =back
385
386
387 =head1 CONFIGURATION
388
389 Most of the configuration will be handed to GetModels on creation via C<new>.
390 This is a list of accepted params.
391
392 =over 4
393
394 =item controller SELF
395
396 The creating controller. Currently this is mandatory.
397
398 =item model MODEL
399
400 The name of the model for this GetModels instance. If none is given, the model
401 is inferred from the name of the controller class.
402
403 =item sorted PARAMS
404
405 =item paginated PARAMS
406
407 =item filtered PARAMS
408
409 Configuration for plugins. If the option for any plugin is omitted, it defaults
410 to enabled and is configured by default. Giving a falsish value as first argument
411 will disable the plugin.
412
413 If the value is a hashref, it will be passed to the plugin's C<init> method.
414
415 =item query
416
417 =item with_objects
418
419 Additional static parts for Rose to include into the final query.
420
421 =item source
422
423 Source for plugins to pull their data from. Defaults to $::form.
424
425 =back
426
427 =head1 BUGS AND CAVEATS
428
429 =over 4
430
431 =item *
432
433 Delegation is not as clean as it should be. Most of the methods rely on action
434 at a distance and should be moved out.
435
436 =back
437
438 =head1 AUTHORS
439
440 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
441
442 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
443
444 =cut