use Carp;
use Data::Dumper;
use List::Util qw(first);
+use List::UtilsBy qw(sort_by);
use constant META_CVARS => 'cvars_config';
my $caller_package = caller;
# TODO: if module is empty, module overloading needs to take effect
- # certain stuff may have more than one overload, odr even more than one type
+ # certain stuff may have more than one overload, or even more than one type
defined $caller_package or croak 'need to be included from a caller reference';
$params{module} ||= _calc_modules_from_overloads(%params) if $params{overloads};
make_cvar_by_configs($caller_package, %params);
make_cvar_by_name($caller_package, %params);
make_cvar_as_hashref($caller_package, %params);
+ make_cvar_value_parser($caller_package, %params);
+ make_cvar_custom_filter($caller_package, %params);
}
sub save_meta_info {
@$configs
);
+ @return = sort_by { $_->config->sortkey } @return;
+
return \@return;
}
}
}
}
+sub make_cvar_value_parser {
+ my ($caller_package) = @_;
+ no strict 'refs';
+ *{ $caller_package . '::parse_custom_variable_values' } = sub {
+ my ($self) = @_;
+
+ $_->parse_value for @{ $self->custom_variables || [] };
+
+ return $self;
+ };
+
+ $caller_package->before_save('parse_custom_variable_values');
+}
+
sub _all_configs {
my (%params) = @_;
require SL::DB::CustomVariableConfig;
- $params{module}
- ? SL::DB::Manager::CustomVariableConfig->get_all(query => [ module => $params{module} ])
- : SL::DB::Manager::CustomVariableConfig->get_all;
+ SL::DB::Manager::CustomVariableConfig->get_all_sorted($params{module} ? (query => [ module => $params{module} ]) : ());
}
sub _overload_by_module {
# value needs config
$inherited_value
? $cvar->value($inherited_value)
- : $cvar->value($params{config}->default_value);
+ : $cvar->value($params{config}->type_dependent_default_value);
return $cvar;
}
return $column_name;
}
+sub make_cvar_custom_filter {
+ my ($caller_package, %params) = @_;
+
+ my $manager = $caller_package->meta->convention_manager->auto_manager_class_name;
+
+ return unless $manager->can('filter');
+
+ $manager->add_filter_specs(
+ cvar => sub {
+ my ($key, $value, $prefix, $config_id) = @_;
+ my $config = SL::DB::Manager::CustomVariableConfig->find_by(id => $config_id);
+
+ if (!$config) {
+ die "invalid config_id in $caller_package\::cvar custom filter: $config_id";
+ }
+
+ if ($config->module != $params{module}) {
+ die "invalid config_id in $caller_package\::cvar custom filter: expected module $params{module} - got @{[ $config->module ]}";
+ }
+
+ my @filter;
+ if ($config->type eq 'bool') {
+ @filter = $value ? ($config->value_col => 1) : (or => [ $config->value_col => undef, $config->value_col => 0 ]);
+ } else {
+ @filter = ($config->value_col => $value);
+ }
+
+ my (%query, %bind_vals);
+ ($query{customized}, $bind_vals{customized}) = Rose::DB::Object::QueryBuilder::build_select(
+ dbh => $config->dbh,
+ select => 'trans_id',
+ tables => [ 'custom_variables' ],
+ columns => { custom_variables => [ qw(trans_id config_id text_value number_value bool_value timestamp_value sub_module) ] },
+ query => [
+ config_id => $config_id,
+ sub_module => $params{sub_module},
+ @filter,
+ ],
+ query_is_sql => 1,
+ );
+
+ if ($config->type eq 'bool') {
+ if ($value) {
+ @filter = (
+ '!default_value' => undef,
+ '!default_value' => '',
+ default_value => '1',
+ );
+
+ } else {
+ @filter = (
+ or => [
+ default_value => '0',
+ default_value => '',
+ default_value => undef,
+ ],
+ );
+ }
+
+ } else {
+ @filter = (
+ '!default_value' => undef,
+ '!default_value' => '',
+ default_value => $value,
+ );
+ }
+
+
+ my $conversion = $config->type =~ m{^(?:date|timestamp)$} ? $config->type
+ : $config->type =~ m{^(?:customer|vendor|part)$} ? 'integer'
+ : $config->type eq 'number' ? 'numeric'
+ : '';
+
+ ($query{config}, $bind_vals{config}) = Rose::DB::Object::QueryBuilder::build_select(
+ dbh => $config->dbh,
+ select => 'id',
+ tables => [ 'custom_variable_configs' ],
+ columns => { custom_variable_configs => [ qw(id default_value) ] },
+ query => [
+ id => $config->id,
+ @filter,
+ ],
+ query_is_sql => 1,
+ );
+
+ $query{config} =~ s{ (?<! NOT\( ) default_value (?! \s*is\s+not\s+null) }{default_value::${conversion}}x if $conversion;
+
+ ($query{not_customized}, $bind_vals{not_customized}) = Rose::DB::Object::QueryBuilder::build_select(
+ dbh => $config->dbh,
+ select => 'trans_id',
+ tables => [ 'custom_variables' ],
+ columns => { custom_variables => [ qw(trans_id config_id sub_module) ] },
+ query => [
+ config_id => $config_id,
+ sub_module => $params{sub_module},
+ ],
+ query_is_sql => 1,
+ );
+
+ foreach my $key (keys %query) {
+ # remove rose aliases. query builder sadly is not reentrant, and will reuse the same aliases. :(
+ $query{$key} =~ s{\bt\d+(?:\.)?\b}{}g;
+
+ # manually inline the values. again, rose doesn't know how to handle bind params in subqueries :(
+ $query{$key} =~ s{\?}{ $config->dbh->quote(shift @{ $bind_vals{$key} }) }xeg;
+
+ $query{$key} =~ s{\n}{ }g;
+ }
+
+ my $qry_config = "EXISTS (" . $query{config} . ")";
+
+ my @result = (
+ 'or' => [
+ $prefix . 'id' => [ \$query{customized} ],
+ and => [
+ "!${prefix}id" => [ \$query{not_customized} ],
+ \$qry_config,
+ ]
+ ],
+ );
+
+ return @result;
+ }
+ );
+}
+
1;
__END__
=head1 NAME
-SL::DB::Helper::CustomVariables - Mixin to provide custom variables relations
+SL::DB::Helper::CustomVariables - Mixin to provide custom variable relations
=head1 SYNOPSIS
This is a Rose::DB::Object::Relationship accessor, generated for cvars. Use it
like any other OneToMany relationship.
+Note that unlike L</cvars_by_config> this accessor only returns
+variables that have already been created for this object. No variables
+will be autovivified for configs for which no variable has been
+created yet.
+
=item C<cvars [ CUSTOM_VARIABLES ]>
Alias to C<custom_variables>. Will only be installed if C<cvars_alias> was
Useful for print templates. If the requested cvar is not present, it will be
vivified with the same rules as in C<cvars_by_config>.
+=item C<parse_custom_variable_values>
+
+When you want to edit custom variables in a form then you have
+unparsed values from the user. These should be written to the
+variable's C<unparsed_value> field.
+
+This function then processes all variables and parses their
+C<unparsed_value> field into the proper field. It returns C<$self> for
+easy chaining.
+
+This is automatically called in a C<before_save> hook so you don't
+have to do it manually if you save directly after assigning the
+values.
+
+In an HTML form you could e.g. use something like the following:
+
+ [%- FOREACH var = SELF.project.cvars_by_config.as_list %]
+ [% HTML.escape(var.config.description) %]:
+ [% L.hidden_tag('project.custom_variables[+].config_id', var.config.id) %]
+ [% PROCESS 'common/render_cvar_input.html' var_name='project.custom_variables[].unparsed_value' %]
+ [%- END %]
+
+Later in the controller when you want to save this project you don't
+have to do anything special:
+
+ my $project = SL::DB::Project->new;
+ my $params = $::form->{project} || {};
+
+ $project->assign_attributes(%{ $params });
+
+ $project->parse_custom_variable_values->save;
+
+However, if you need access to a variable's value before saving in
+some way then you have to call this function manually. For example:
+
+ my $project = SL::DB::Project->new;
+ my $params = $::form->{project} || {};
+
+ $project->assign_attributes(%{ $params });
+
+ $project->parse_custom_variable_values;
+
+ print STDERR "CVar[0] value: " . $project->custom_variables->[0]->value . "\n";
+
+=back
+
+=head1 INSTALLED MANAGER METHODS
+
+=over 4
+
+=item Custom filter for GetModels
+
+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:
+
+ filter.cvar.$config_id
+
+=back
+
+=head1 BUGS AND CAVEATS
+
+=over 4
+
+=item * Conditional method export
+
+Prolonged use has shown that users expect all methods to be present or none.
+Future versions of this will likely remove the optional aliasing.
+
+=item * Sematics need to be updated
+
+There are a few transitions that are currently neither supported nor well
+defined, most of the happening when the config of a cvar gets changed which
+instances are already saved. This needs to be cleaned up.
+
=back
=head1 AUTHOR
-Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
=cut