1 package SL::DB::Helper::CustomVariables;
 
   6 use List::Util qw(first);
 
   8 use constant META_CVARS => 'cvars_config';
 
  11   my ($class, %params) = @_;
 
  12   my $caller_package = caller;
 
  14   # TODO: if module is empty, module overloading needs to take effect
 
  15   # certain stuff may have more than one overload, odr even more than one type
 
  16   defined $caller_package     or croak 'need to be included from a caller reference';
 
  18   $params{module}     ||= _calc_modules_from_overloads(%params) if $params{overloads};
 
  19   $params{sub_module} ||= '';
 
  20   $params{id}         ||= _get_primary_key_column($caller_package);
 
  22   $params{module} || $params{sub_module}  or croak 'need param module or sub_module';
 
  24   return unless save_meta_info($caller_package, %params);
 
  25   make_cvar_accessor($caller_package, %params);
 
  26   make_cvar_alias($caller_package, %params)      if $params{cvars_alias};
 
  27   make_cvar_by_configs($caller_package, %params);
 
  28   make_cvar_by_name($caller_package, %params);
 
  29   make_cvar_as_hashref($caller_package, %params);
 
  30   make_cvar_value_parser($caller_package, %params);
 
  31   make_cvar_custom_filter($caller_package, %params);
 
  35   my ($caller_package, %params) = @_;
 
  37   my $meta = $caller_package->meta;
 
  38   return 0 if $meta->{META_CVARS()};
 
  40   $meta->{META_CVARS()} = \%params;
 
  45 sub make_cvar_accessor {
 
  46   my ($caller_package, %params) = @_;
 
  48   my $modules = ('ARRAY' eq ref $params{module}) ?
 
  49       join ',', @{ $params{module} } :
 
  51   my @module_filter = $modules ?
 
  52     ("config_id" => [ \"(SELECT custom_variable_configs.id FROM custom_variable_configs WHERE custom_variable_configs.module IN ( '$modules' ))" ]) : # " make emacs happy
 
  55   $caller_package->meta->add_relationships(
 
  57       type         => 'one to many',
 
  58       class        => 'SL::DB::CustomVariable',
 
  59       column_map   => { $params{id} => 'trans_id' },
 
  60       query_args   => [ sub_module => $params{sub_module}, @module_filter ],
 
  66   my ($caller_package) = @_;
 
  68   *{ $caller_package . '::cvars' } =  sub {
 
  69     goto &{ $caller_package . '::custom_variables' };
 
  73 # this is used for templates where you need to list every applicable config
 
  74 # auto vivifies non existent cvar objects as necessary.
 
  75 sub make_cvar_by_configs {
 
  76   my ($caller_package, %params) = @_;
 
  79   *{ $caller_package . '::cvars_by_config' } = sub {
 
  81     @_ > 1 and croak "not an accessor";
 
  83     my $configs     = _all_configs(%params);
 
  84     my $cvars       = $self->custom_variables;
 
  85     my %cvars_by_config = map { $_->config_id => $_ } @$cvars;
 
  89         if ( $cvars_by_config{$_->id} ) {
 
  90           $cvars_by_config{$_->id};
 
  93           my $cvar = _new_cvar($self, %params, config => $_);
 
  94           $self->add_custom_variables($cvar);
 
 105 # this is used for print templates where you need to refer to a variable by name
 
 106 # TODO typically these were referred as prefix_'cvar'_name
 
 107 sub make_cvar_by_name {
 
 108   my ($caller_package, %params) = @_;
 
 111   *{ $caller_package . '::cvar_by_name' } = sub {
 
 112     my ($self, $name) = @_;
 
 114     my $configs = _all_configs(%params);
 
 115     my $cvars   = $self->custom_variables;
 
 116     my $config  = first { $_->name eq $name } @$configs;
 
 118     croak "unknown cvar name $name" unless $config;
 
 120     my $cvar    = first { $_->config_id eq $config->id } @$cvars;
 
 123       $cvar = _new_cvar($self, %params, config => $config);
 
 124       $self->add_custom_variables($cvar);
 
 131 sub make_cvar_as_hashref {
 
 132   my ($caller_package, %params) = @_;
 
 135   *{ $caller_package . '::cvar_as_hashref' } = sub {
 
 137     @_ > 1 and croak "not an accessor";
 
 139     my $cvars_by_config = $self->cvars_by_config;
 
 142       $_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }
 
 149 sub make_cvar_value_parser {
 
 150   my ($caller_package) = @_;
 
 152   *{ $caller_package . '::parse_custom_variable_values' } =  sub {
 
 155     $_->parse_value for @{ $self->custom_variables || [] };
 
 160   $caller_package->before_save('parse_custom_variable_values');
 
 166   require SL::DB::CustomVariableConfig;
 
 168   SL::DB::Manager::CustomVariableConfig->get_all_sorted($params{module} ? (query => [ module => $params{module} ]) : ());
 
 171 sub _overload_by_module {
 
 172   my ($module, %params) = @_;
 
 174   keys %{ $params{overloads} }; # reset each iterator
 
 175   while (my ($fk, $def) = each %{ $params{overloads} }) {
 
 176     return ($fk, $def->{class}) if $def->{module} eq $module;
 
 179   croak "unknown overload, cannot resolve module $module";
 
 183   my ($self, %params) = @_;
 
 185   # check overloading first
 
 186   if ($params{sub_module}) {
 
 187     my ($fk, $class) = _overload_by_module($params{config}->module, %params);
 
 188     my $base_cvar = $class->new(id => $self->$fk)->load->cvar_by_name($params{config}->name);
 
 189     $inherited_value = $base_cvar->value;
 
 192   my $cvar = SL::DB::CustomVariable->new(
 
 193     config     => $params{config},
 
 194     trans_id   => $self->${ \ $params{id} },
 
 195     sub_module => $params{sub_module},
 
 199    ? $cvar->value($inherited_value)
 
 200    : $cvar->value($params{config}->type_dependent_default_value);
 
 204 sub _calc_modules_from_overloads {
 
 208   for my $def (values %{ $params{overloads} || {} }) {
 
 209     $modules{$def->{module}} = 1;
 
 212   return [ keys %modules ];
 
 215 sub _get_primary_key_column {
 
 216   my ($caller_package) = @_;
 
 217   my $meta             = $caller_package->meta;
 
 220   $column_name = $meta->{primary_key}->{columns}->[0] if $meta->{primary_key} && (ref($meta->{primary_key}->{columns}) eq 'ARRAY') && (1 == scalar(@{ $meta->{primary_key}->{columns} }));
 
 222   croak "Unable to retrieve primary key column name: meta information for package $caller_package not set up correctly" unless $column_name;
 
 227 sub make_cvar_custom_filter {
 
 228   my ($caller_package, %params) = @_;
 
 230   my $manager    = $caller_package->meta->convention_manager->auto_manager_class_name;
 
 232   return unless $manager->can('filter');
 
 234   $manager->add_filter_specs(
 
 236       my ($key, $value, $prefix, $config_id) = @_;
 
 237       my $config = SL::DB::Manager::CustomVariableConfig->find_by(id => $config_id);
 
 240         die "invalid config_id in $caller_package\::cvar custom filter: $config_id";
 
 243       if ($config->module != $params{module}) {
 
 244         die "invalid config_id in $caller_package\::cvar custom filter: expected module $params{module} - got @{[ $config->module ]}";
 
 248       if ($config->type eq 'bool') {
 
 249         @filter = $value ? ($config->value_col => 1) : (or => [ $config->value_col => undef, $config->value_col => 0 ]);
 
 251         @filter = ($config->value_col => $value);
 
 254       my (%query, %bind_vals);
 
 255       ($query{customized}, $bind_vals{customized}) = Rose::DB::Object::QueryBuilder::build_select(
 
 257         select               => 'trans_id',
 
 258         tables               => [ 'custom_variables' ],
 
 259         columns              => { custom_variables => [ qw(trans_id config_id text_value number_value bool_value timestamp_value sub_module) ] },
 
 261           config_id          => $config_id,
 
 262           sub_module         => $params{sub_module},
 
 268       if ($config->type eq 'bool') {
 
 271             '!default_value' => undef,
 
 272             '!default_value' => '',
 
 273             default_value    => '1',
 
 279               default_value => '0',
 
 281               default_value => undef,
 
 288           '!default_value' => undef,
 
 289           '!default_value' => '',
 
 290           default_value    => $value,
 
 295       my $conversion  = $config->type =~ m{^(?:date|timestamp)$}       ? $config->type
 
 296                       : $config->type =~ m{^(?:customer|vendor|part)$} ? 'integer'
 
 297                       : $config->type eq 'number'                      ? 'numeric'
 
 298                       # : $config->type eq 'bool'                        ? 'boolean'
 
 301       ($query{config}, $bind_vals{config}) = Rose::DB::Object::QueryBuilder::build_select(
 
 304         tables             => [ 'custom_variable_configs' ],
 
 305         columns            => { custom_variable_configs => [ qw(id default_value) ] },
 
 313       $query{config} =~ s{ \bdefault_value\b \s*=\s* (?!'') }{default_value::${conversion} = }x if $conversion;
 
 315       ($query{not_customized}, $bind_vals{not_customized}) = Rose::DB::Object::QueryBuilder::build_select(
 
 317         select       => 'trans_id',
 
 318         tables       => [ 'custom_variables' ],
 
 319         columns      => { custom_variables => [ qw(trans_id config_id sub_module) ] },
 
 321           config_id  => $config_id,
 
 322           sub_module => $params{sub_module},
 
 327       foreach my $key (keys %query) {
 
 328         # remove rose aliases. query builder sadly is not reentrant, and will reuse the same aliases. :(
 
 329         $query{$key} =~ s{\bt\d+(?:\.)?\b}{}g;
 
 331         # manually inline the values. again, rose doen't know how to handly bind params in subqueries :(
 
 332         $query{$key} =~ s{\?}{ $config->dbh->quote($_) }xe for @{ $bind_vals{$key} };
 
 334         $query{$key} =~ s{\n}{ }g;
 
 337       my $qry_config = "EXISTS (" . $query{config} . ")";
 
 341           $prefix . 'id'   => [ \$query{customized} ],
 
 343             "!${prefix}id" => [ \$query{not_customized}  ],
 
 362 SL::DB::Helper::CustomVariables - Mixin to provide custom variables relations
 
 366   # use in a primary class
 
 367   use SL::DB::Helper::CustomVariables (
 
 372   # use overloading in a secondary class
 
 373   use SL::DB::Helper::CustomVariables (
 
 374     sub_module  => 'orderitems',
 
 378         class => 'SL::DB::Part',
 
 386 This module provides methods to deal with named custom variables. Two concepts are understood.
 
 388 =head2 Primary CVar Classes
 
 390 Primary classes are those that feature cvars for themselves. Currently those
 
 391 are Part, Contact, Customer and Vendor. cvars for these will get saved directly
 
 394 =head2 Secondary CVar Classes
 
 396 Secondary classes inherit their cvars from member relationships. This is built
 
 397 so that orders can save a copy of the cvars of their parts, customers and the
 
 398 like to be immutable later on.
 
 400 Secondary classes may currently not have cvars of their own.
 
 402 =head1 INSTALLED METHODS
 
 406 =item C<custom_variables [ CUSTOM_VARIABLES ]>
 
 408 This is a Rose::DB::Object::Relationship accessor, generated for cvars. Use it
 
 409 like any other OneToMany relationship.
 
 411 Note that unlike L</cvars_by_config> this accessor only returns
 
 412 variables that have already been created for this object. No variables
 
 413 will be autovivified for configs for which no variable has been
 
 416 =item C<cvars [ CUSTOM_VARIABLES ]>
 
 418 Alias to C<custom_variables>. Will only be installed if C<cvars_alias> was
 
 421 =item C<cvars_by_config>
 
 423 Thi will return a list of CVars with the following changes over the standard accessor:
 
 429 The list will be returned in the sorted order of the configs.
 
 433 For every config exactly one CVar will be returned.
 
 437 If no cvar was found for a config, a new one will be vivified, set to the
 
 438 correct config, module etc, and registered into the object.
 
 442 Vivified cvars for secondary classes will first try to find their base object
 
 443 and use that value. If no such value or cvar is found the default value from
 
 448 This is useful if you need to list every possible CVar, like in CRUD masks.
 
 450 =item C<cvar_by_name NAME [ VALUE ]>
 
 452 Returns the CVar object for this object which matches the given internal name.
 
 453 Useful for print templates. If the requested cvar is not present, it will be
 
 454 vivified with the same rules as in C<cvars_by_config>.
 
 456 =item C<parse_custom_variable_values>
 
 458 When you want to edit custom variables in a form then you have
 
 459 unparsed values from the user. These should be written to the
 
 460 variable's C<unparsed_value> field.
 
 462 This function then processes all variables and parses their
 
 463 C<unparsed_value> field into the proper field. It returns C<$self> for
 
 466 This is automatically called in a C<before_save> hook so you don't
 
 467 have to do it manually if you save directly after assigning the
 
 470 In an HTML form you could e.g. use something like the following:
 
 472   [%- FOREACH var = SELF.project.cvars_by_config.as_list %]
 
 473     [% HTML.escape(var.config.description) %]:
 
 474     [% L.hidden_tag('project.custom_variables[+].config_id', var.config.id) %]
 
 475     [% PROCESS 'common/render_cvar_input.html' var_name='project.custom_variables[].unparsed_value' %]
 
 478 Later in the controller when you want to save this project you don't
 
 479 have to do anything special:
 
 481   my $project = SL::DB::Project->new;
 
 482   my $params  = $::form->{project} || {};
 
 484   $project->assign_attributes(%{ $params });
 
 486   $project->parse_custom_variable_values->save;
 
 488 However, if you need access to a variable's value before saving in
 
 489 some way then you have to call this function manually. For example:
 
 491   my $project = SL::DB::Project->new;
 
 492   my $params  = $::form->{project} || {};
 
 494   $project->assign_attributes(%{ $params });
 
 496   $project->parse_custom_variable_values;
 
 498   print STDERR "CVar[0] value: " . $project->custom_variables->[0]->value . "\n";
 
 502 =head1 INSTALLED MANAGER METHODS
 
 506 =item Custom filter for GetModels
 
 508 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:
 
 510   filter.cvar.$config_id
 
 516 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>,
 
 517 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>