Pflichtenheftitems: Masken für neue (Unter)Funktionsblöcke
[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) ],
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   push @{ $params{query}        ||= [] }, @{ $self->query || [] };
117   push @{ $params{with_objects} ||= [] }, @{ $self->with_objects || [] };
118
119   %params = $_->finalize(%params) for $self->plugins;
120
121   $self->finalized(1);
122   $self->final_params(\%params);
123
124   return %params;
125 }
126
127 sub register_handlers {
128   my ($self, %additional_handlers) = @_;
129
130   my $handlers    = $self->handlers;
131   map { push @{ $handlers->{$_} }, $additional_handlers{$_} if $additional_handlers{$_} } keys %$handlers;
132 }
133
134 sub get_models_url_params {
135   my ($self, $sub_name_or_code) = @_;
136
137   my $code     = (ref($sub_name_or_code) || '') eq 'CODE' ? $sub_name_or_code : sub { shift->controller->$sub_name_or_code(@_) };
138   my $callback = sub {
139     my ($self, %params)   = @_;
140     my @additional_params = $code->($self);
141     return (
142       %params,
143       (scalar(@additional_params) == 1) && (ref($additional_params[0]) eq 'HASH') ? %{ $additional_params[0] } : @additional_params,
144     );
145   };
146
147   $self->registere_handlers('callback' => $callback);
148 }
149
150 sub get_callback {
151   my ($self, %override_params) = @_;
152
153   my %default_params = $self->_run_handlers('callback', action => $self->controller->action_name);
154
155   return $self->controller->url_for(%default_params, %override_params);
156 }
157
158 sub manager {
159   die "No 'model' to work on" unless $_[0]->model;
160   "SL::DB::Manager::" . $_[0]->model;
161 }
162
163 #
164 # private/internal functions
165 #
166
167 sub _run_handlers {
168   my ($self, $handler_type, %params) = @_;
169
170   foreach my $sub (@{ $self->handlers->{$handler_type} }) {
171     if (ref $sub eq 'CODE') {
172       %params = $sub->($self, %params);
173     } elsif ($self->can($sub)) {
174       %params = $self->$sub(%params);
175     } else {
176       die "SL::Controller::Helper::GetModels::get_callback: Cannot call $sub on " . ref($self) . ")";
177     }
178   }
179
180   return %params;
181 }
182
183 sub init_handlers {
184   {
185     callback => [],
186   }
187 }
188
189 sub init_source {
190   $::form
191 }
192
193 1;
194 __END__
195
196 =pod
197
198 =encoding utf8
199
200 =head1 NAME
201
202 SL::Controller::Helper::GetModels - Base class for a the GetModels system.
203
204 =head1 SYNOPSIS
205
206 In controller:
207
208   use SL::Controller::Helper::GetModels;
209
210   my $get_models = SL::Controller::Helper::GetModels->new(
211     controller   => $self,
212   );
213
214   my $models = $self->get_models->get;
215
216 =head1 OVERVIEW
217
218 Building a CRUD controller would be easy, were it not for those stupid
219 list actions. People unreasonable expect stuff like filtering, sorting,
220 paginating, exporting etc simply to work. Well, lets try to make it simply work
221 a little.
222
223 This class is a proxy between a controller and specialized
224 helper modules that handle these things (sorting, paginating etc) and gives you
225 the means to retrieve the information when needed to display sort headers or
226 paginating footers.
227
228 Information about the requested data query can be stored into the object up to
229 a certain point, from which on the object becomes locked and can only be
230 accessed for information. (See C<STATES>).
231
232 =head1 INTERFACE METHODS
233
234 =over 4
235
236 =item new PARAMS
237
238 Create a new GetModels object. Params must have at least an entry
239 C<controller>, other than that, see C<CONFIGURATION> for options.
240
241 =item get
242
243 Retrieve all models for the current configuration. Will finalize the object.
244
245 =item get_models_url_params SUB
246
247 Register a sub to be called whenever an URL has to be generated (e.g. for sort
248 and pagination links). This is a way for the controller to add additional
249 parameters to the URL (e.g. for filter parameters).
250
251 The parameter can be either a code reference or the name of
252 one of the controller's functions.
253
254 The value returned by C<SUB> must be either a single hash
255 reference or a hash of key/value pairs to add to the URL.
256
257 =item get_callback
258
259 Returns a URL suitable for use as a callback parameter. It maps to the
260 current controller and action. All registered handlers of type
261 'callback' (e.g. the ones by C<Sorted> and C<Paginated>) can inject
262 the parameters they need so that the same list view as is currently
263 visible can be re-rendered.
264
265 Optional C<%params> passed to this function may override any parameter
266 set by the registered handlers.
267
268 =item enable_plugin PLUGIN
269
270 =item disable_plugin PLUGIN
271
272 =item is_enabled_plugin PLUGIN
273
274 Enable or disable the specified plugin. Useful to disable paginating for
275 exports for example. C<is_enabled_plugin> can be used to check the current
276 state fo a plugin.
277
278 Must not be finalized to use this.
279
280 =item finalize
281
282 Forces finalized state. Can be used on finalized objects without error.
283
284 Note that most higher functions will call this themselves to force a finalized
285 state. If you do use it it must come before any other finalizing methods, and
286 will most likely function as a reminder or maintainers where your codes
287 switches from configuration to finalized state.
288
289 =item source HASHREF
290
291 The source for user supplied information. Defaults to $::form. Changing it
292 after C<Base> phase has no effect.
293
294 =item controller CONTROLLER
295
296 A weakened link to the controller that created the GetModels object. Needed for
297 certain plugin methods.
298
299 =back
300
301 =head1 DELEGATION METHODS
302
303 All of these finalize.
304
305 Methods delegating to C<Sorted>:
306
307 =over 4
308
309 =item *
310
311 set_report_generator_sort_options
312
313 =item *
314
315 get_sort_spec
316
317 =item *
318
319 get_current_sort_params
320
321 =back
322
323 Methods delegating to C<Paginated>:
324
325 =over 4
326
327 =item *
328
329 get_paginate_args
330
331 =back
332
333 =head1 STATES
334
335 A GetModels object is in one of 3 states at any given time. Their purpose is to
336 make a class of bugs impossible that orginated from changing the configuration
337 of a GetModels object halfway during the request. This was a huge problem in
338 the old implementation.
339
340 =over 4
341
342 =item Base
343
344 This is the state after creating a new object.
345
346 =item Init
347
348 In this state every information needed from the source ($::form) has been read
349 and subsequent changes to the source have no effect. In the current
350 implementation this will happen during creation, so that the return value of
351 C<new> is already in state C<Init>.
352
353 =item Finalized
354
355 In this state no new configuration will be accepted so that information gotten
356 through the various methods is consistent. Every information retrieval method
357 will trigger finalize.
358
359 =back
360
361
362 =head1 CONFIGURATION
363
364 Most of the configuration will be handed to GetModels on creation via C<new>.
365 This is a list of accepted params.
366
367 =over 4
368
369 =item controller SELF
370
371 The creating controller. Currently this is mandatory.
372
373 =item model MODEL
374
375 The name of the model for this GetModels instance. If none is given, the model
376 is inferred from the name of the controller class.
377
378 =item sorted PARAMS
379
380 =item paginated PARAMS
381
382 =item filtered PARAMS
383
384 Configuration for plugins. If the option for any plugin is omitted, it defaults
385 to enabled and configured by default. Giving a falsish value as first argument
386 will disable the plugin.
387
388 If the value is a hashref, it will be passed to the plugin's C<init> method.
389
390 =item query
391
392 =item with_objects
393
394 Additional static parts for Rose to include into the final query.
395
396 =item source
397
398 Source for plugins to pull their data from. Defaults to $::form.
399
400 =back
401
402 =head1 BUGS AND CAVEATS
403
404 =over 4
405
406 =item *
407
408 Delegation is not as clean as it should be. Most of the methods rely on action
409 at a distance and should be moved out.
410
411 =back
412
413 =head1 AUTHORS
414
415 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
416
417 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
418
419 =cut