1 package SL::DB::Helper::CustomVariables;
6 use List::Util qw(first);
7 use List::UtilsBy qw(partition_by);
9 use constant META_CVARS => 'cvars_config';
12 my ($class, %params) = @_;
13 my $caller_package = caller;
15 # TODO: if module is empty, module overloading needs to take effect
16 # certain stuff may have more than one overload, or even more than one type
17 defined $caller_package or croak 'need to be included from a caller reference';
19 $params{module} ||= _calc_modules_from_overloads(%params) if $params{overloads};
20 $params{sub_module} ||= '';
21 $params{id} ||= _get_primary_key_column($caller_package);
23 $params{module} || $params{sub_module} or croak 'need param module or sub_module';
25 return unless save_meta_info($caller_package, %params);
26 make_cvar_accessor($caller_package, %params);
27 make_cvar_alias($caller_package, %params) if $params{cvars_alias};
28 make_cvar_by_configs($caller_package, %params);
29 make_cvar_by_name($caller_package, %params);
30 make_cvar_as_hashref($caller_package, %params);
31 make_cvar_value_parser($caller_package, %params);
32 make_cvar_custom_filter($caller_package, %params);
36 my ($caller_package, %params) = @_;
38 my $meta = $caller_package->meta;
39 return 0 if $meta->{META_CVARS()};
41 $meta->{META_CVARS()} = \%params;
46 sub make_cvar_accessor {
47 my ($caller_package, %params) = @_;
49 my $modules = ('ARRAY' eq ref $params{module}) ?
50 join ',', @{ $params{module} } :
52 my @module_filter = $modules ?
53 ("config_id" => [ \"(SELECT custom_variable_configs.id FROM custom_variable_configs WHERE custom_variable_configs.module IN ( '$modules' ))" ]) : # " make emacs happy
56 $caller_package->meta->add_relationships(
58 type => 'one to many',
59 class => 'SL::DB::CustomVariable',
60 column_map => { $params{id} => 'trans_id' },
61 query_args => [ sub_module => $params{sub_module}, @module_filter ],
67 my ($caller_package) = @_;
69 *{ $caller_package . '::cvars' } = sub {
70 goto &{ $caller_package . '::custom_variables' };
74 # this is used for templates where you need to list every applicable config
75 # auto vivifies non existent cvar objects as necessary.
76 sub make_cvar_by_configs {
77 my ($caller_package, %params) = @_;
80 *{ $caller_package . '::cvars_by_config' } = sub {
82 @_ > 1 and croak "not an accessor";
84 my $configs = _all_configs(%params);
85 my $cvars = $self->custom_variables;
86 my %cvars_by_config = map { $_->config_id => $_ } @$cvars;
87 my $invalids = _all_invalids($self->${\ $self->meta->primary_key_columns->[0]->name }, $configs, %params);
88 my %invalids_by_config = map { $_->config_id => 1 } @$invalids;
93 if ( $cvars_by_config{$_->id} ) {
94 $cvar = $cvars_by_config{$_->id};
97 $cvar = _new_cvar($self, %params, config => $_);
98 $self->add_custom_variables($cvar);
100 $cvar->{is_valid} = !$invalids_by_config{$_->id};
101 $cvar->{config} = $_;
111 # this is used for print templates where you need to refer to a variable by name
112 # TODO typically these were referred as prefix_'cvar'_name
113 sub make_cvar_by_name {
114 my ($caller_package, %params) = @_;
117 *{ $caller_package . '::cvar_by_name' } = sub {
118 my ($self, $name) = @_;
120 my $configs = _all_configs(%params);
121 my $cvars = $self->custom_variables;
122 my $config = first { $_->name eq $name } @$configs;
124 croak "unknown cvar name $name" unless $config;
126 my $cvar = first { $_->config_id eq $config->id } @$cvars;
129 $cvar = _new_cvar($self, %params, config => $config);
130 $self->add_custom_variables($cvar);
137 sub make_cvar_as_hashref {
138 my ($caller_package, %params) = @_;
141 *{ $caller_package . '::cvar_as_hashref' } = sub {
143 @_ > 1 and croak "not an accessor";
145 my $cvars_by_config = $self->cvars_by_config;
148 $_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }
155 sub make_cvar_value_parser {
156 my ($caller_package) = @_;
158 *{ $caller_package . '::parse_custom_variable_values' } = sub {
161 $_->parse_value for @{ $self->custom_variables || [] };
166 $caller_package->before_save('parse_custom_variable_values');
172 require SL::DB::CustomVariableConfig;
174 my $cache = $::request->cache("::SL::DB::Helper::CustomVariables::object_cache");
176 if (!$cache->{all}) {
177 my $configs = SL::DB::Manager::CustomVariableConfig->get_all_sorted;
178 $cache->{all} = $configs;
179 $cache->{module} = { partition_by { $_->module } @$configs };
182 return $params{module} && !ref $params{module} ? $cache->{module}{$params{module}}
183 : $params{module} && ref $params{module} ? [ map { @{ $cache->{module}{$_} // [] } } @{ $params{module} } ]
187 sub _overload_by_module {
188 my ($module, %params) = @_;
190 keys %{ $params{overloads} }; # reset each iterator
191 while (my ($fk, $def) = each %{ $params{overloads} }) {
192 return ($fk, $def->{class}) if $def->{module} eq $module;
195 croak "unknown overload, cannot resolve module $module";
199 my ($self, %params) = @_;
201 # check overloading first
202 if ($params{sub_module}) {
203 my ($fk, $class) = _overload_by_module($params{config}->module, %params);
204 my $base_cvar = $class->new(id => $self->$fk)->load->cvar_by_name($params{config}->name);
205 $inherited_value = $base_cvar->value;
208 my $cvar = SL::DB::CustomVariable->new(
209 config => $params{config},
210 trans_id => $self->${ \ $params{id} },
211 sub_module => $params{sub_module},
215 ? $cvar->value($inherited_value)
216 : $cvar->value($params{config}->type_dependent_default_value);
220 sub _calc_modules_from_overloads {
224 for my $def (values %{ $params{overloads} || {} }) {
225 $modules{$def->{module}} = 1;
228 return [ keys %modules ];
231 sub _get_primary_key_column {
232 my ($caller_package) = @_;
233 my $meta = $caller_package->meta;
236 $column_name = $meta->{primary_key}->{columns}->[0] if $meta->{primary_key} && (ref($meta->{primary_key}->{columns}) eq 'ARRAY') && (1 == scalar(@{ $meta->{primary_key}->{columns} }));
238 croak "Unable to retrieve primary key column name: meta information for package $caller_package not set up correctly" unless $column_name;
243 sub make_cvar_custom_filter {
244 my ($caller_package, %params) = @_;
246 my $manager = $caller_package->meta->convention_manager->auto_manager_class_name;
248 return unless $manager->can('filter');
250 $manager->add_filter_specs(
252 my ($key, $value, $prefix, $config_id) = @_;
253 my $config = SL::DB::Manager::CustomVariableConfig->find_by(id => $config_id);
256 die "invalid config_id in $caller_package\::cvar custom filter: $config_id";
259 if ($config->module != $params{module}) {
260 die "invalid config_id in $caller_package\::cvar custom filter: expected module $params{module} - got @{[ $config->module ]}";
264 if ($config->type eq 'bool') {
265 @filter = $value ? ($config->value_col => 1) : (or => [ $config->value_col => undef, $config->value_col => 0 ]);
267 @filter = ($config->value_col => $value);
270 my (%query, %bind_vals);
271 ($query{customized}, $bind_vals{customized}) = Rose::DB::Object::QueryBuilder::build_select(
273 select => 'trans_id',
274 tables => [ 'custom_variables' ],
275 columns => { custom_variables => [ qw(trans_id config_id text_value number_value bool_value timestamp_value sub_module) ] },
277 config_id => $config_id,
278 sub_module => $params{sub_module},
284 if ($config->type eq 'bool') {
287 '!default_value' => undef,
288 '!default_value' => '',
289 default_value => '1',
295 default_value => '0',
297 default_value => undef,
304 '!default_value' => undef,
305 '!default_value' => '',
306 default_value => $value,
311 my $conversion = $config->type =~ m{^(?:date|timestamp)$} ? $config->type
312 : $config->type =~ m{^(?:customer|vendor|part)$} ? 'integer'
313 : $config->type eq 'number' ? 'numeric'
316 ($query{config}, $bind_vals{config}) = Rose::DB::Object::QueryBuilder::build_select(
319 tables => [ 'custom_variable_configs' ],
320 columns => { custom_variable_configs => [ qw(id default_value) ] },
328 $query{config} =~ s{ (?<! NOT\( ) default_value (?! \s*is\s+not\s+null) }{default_value::${conversion}}x if $conversion;
330 ($query{not_customized}, $bind_vals{not_customized}) = Rose::DB::Object::QueryBuilder::build_select(
332 select => 'trans_id',
333 tables => [ 'custom_variables' ],
334 columns => { custom_variables => [ qw(trans_id config_id sub_module) ] },
336 config_id => $config_id,
337 sub_module => $params{sub_module},
342 foreach my $key (keys %query) {
343 # remove rose aliases. query builder sadly is not reentrant, and will reuse the same aliases. :(
344 $query{$key} =~ s{\bt\d+(?:\.)?\b}{}g;
346 # manually inline the values. again, rose doesn't know how to handle bind params in subqueries :(
347 $query{$key} =~ s{\?}{ $config->dbh->quote(shift @{ $bind_vals{$key} }) }xeg;
349 $query{$key} =~ s{\n}{ }g;
352 my $qry_config = "EXISTS (" . $query{config} . ")";
356 $prefix . 'id' => [ \$query{customized} ],
358 "!${prefix}id" => [ \$query{not_customized} ],
371 my ($trans_id, $configs, %params) = @_;
373 require SL::DB::CustomVariableValidity;
375 # easy 1: no trans_id, all valid by default.
376 return [] unless $trans_id;
378 # easy 2: no module in params? no validity
379 return [] unless $params{module};
381 my %wanted_modules = ref $params{module} ? map { $_ => 1 } @{ $params{module} } : ($params{module} => 1);
382 my @module_configs = grep { $wanted_modules{$_->module} } @$configs;
384 return [] unless @module_configs;
386 # nor find all entries for that and return
387 SL::DB::Manager::CustomVariableValidity->get_all(
389 config_id => [ map { $_->id } @module_configs ],
390 trans_id => $trans_id,
403 SL::DB::Helper::CustomVariables - Mixin to provide custom variable relations
407 # use in a primary class
408 use SL::DB::Helper::CustomVariables (
413 # use overloading in a secondary class
414 use SL::DB::Helper::CustomVariables (
415 sub_module => 'orderitems',
419 class => 'SL::DB::Part',
427 This module provides methods to deal with named custom variables. Two concepts are understood.
429 =head2 Primary CVar Classes
431 Primary classes are those that feature cvars for themselves. Currently those
432 are Part, Contact, Customer and Vendor. cvars for these will get saved directly
435 =head2 Secondary CVar Classes
437 Secondary classes inherit their cvars from member relationships. This is built
438 so that orders can save a copy of the cvars of their parts, customers and the
439 like to be immutable later on.
441 Secondary classes may currently not have cvars of their own.
443 =head1 INSTALLED METHODS
447 =item C<custom_variables [ CUSTOM_VARIABLES ]>
449 This is a Rose::DB::Object::Relationship accessor, generated for cvars. Use it
450 like any other OneToMany relationship.
452 Note that unlike L</cvars_by_config> this accessor only returns
453 variables that have already been created for this object. No variables
454 will be autovivified for configs for which no variable has been
457 =item C<cvars [ CUSTOM_VARIABLES ]>
459 Alias to C<custom_variables>. Will only be installed if C<cvars_alias> was
462 =item C<cvars_by_config>
464 This will return a list of CVars with the following changes over the standard accessor:
470 The list will be returned in the sorted order of the configs.
474 For every config exactly one CVar will be returned.
478 If no cvar was found for a config, a new one will be vivified, set to the
479 correct config, module etc, and registered into the object.
483 Vivified cvars for secondary classes will first try to find their base object
484 and use that value. If no such value or cvar is found the default value from
489 This is useful if you need to list every possible CVar, like in CRUD masks.
491 =item C<cvar_by_name NAME [ VALUE ]>
493 Returns the CVar object for this object which matches the given internal name.
494 Useful for print templates. If the requested cvar is not present, it will be
495 vivified with the same rules as in C<cvars_by_config>.
497 =item C<parse_custom_variable_values>
499 When you want to edit custom variables in a form then you have
500 unparsed values from the user. These should be written to the
501 variable's C<unparsed_value> field.
503 This function then processes all variables and parses their
504 C<unparsed_value> field into the proper field. It returns C<$self> for
507 This is automatically called in a C<before_save> hook so you don't
508 have to do it manually if you save directly after assigning the
511 In an HTML form you could e.g. use something like the following:
513 [%- FOREACH var = SELF.project.cvars_by_config.as_list %]
514 [% HTML.escape(var.config.description) %]:
515 [% L.hidden_tag('project.custom_variables[+].config_id', var.config.id) %]
516 [% PROCESS 'common/render_cvar_input.html' var_name='project.custom_variables[].unparsed_value' %]
519 Later in the controller when you want to save this project you don't
520 have to do anything special:
522 my $project = SL::DB::Project->new;
523 my $params = $::form->{project} || {};
525 $project->assign_attributes(%{ $params });
527 $project->parse_custom_variable_values->save;
529 However, if you need access to a variable's value before saving in
530 some way then you have to call this function manually. For example:
532 my $project = SL::DB::Project->new;
533 my $params = $::form->{project} || {};
535 $project->assign_attributes(%{ $params });
537 $project->parse_custom_variable_values;
539 print STDERR "CVar[0] value: " . $project->custom_variables->[0]->value . "\n";
543 =head1 INSTALLED MANAGER METHODS
547 =item Custom filter for GetModels
549 If the Manager for the calling C<SL::DB::Object> has included the helper L<SL::DB::Helper::Filtered>, a custom filter for cvars will be added to the specs, with the following syntax:
551 filter.cvar.$config_id
555 =head1 BUGS AND CAVEATS
559 =item * Conditional method export
561 Prolonged use has shown that users expect all methods to be present or none.
562 Future versions of this will likely remove the optional aliasing.
564 =item * Semantics need to be updated
566 There are a few transitions that are currently neither supported nor well
567 defined, most of them happening when the config of a cvar gets changed, but
568 whose instances have already been saved. This needs to be cleaned up.
574 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,
575 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>