ActsAsList: Möglichkeit, Positionsgruppen nach anderen Spalten anzugeben
[kivitendo-erp.git] / SL / DB / Helper / ActsAsList.pm
index ce927bb..5734c2e 100644 (file)
@@ -3,10 +3,12 @@ package SL::DB::Helper::ActsAsList;
 use strict;
 
 use parent qw(Exporter);
-our @EXPORT = qw(move_position_up move_position_down reorder_list);
+our @EXPORT = qw(move_position_up move_position_down reorder_list configure_acts_as_list);
 
 use Carp;
 
+my %list_spec;
+
 sub import {
   my ($class, @params)   = @_;
   my $importing = caller();
@@ -54,18 +56,47 @@ sub reorder_list {
   return $result;
 }
 
+sub configure_acts_as_list {
+  my ($class, %params) = @_;
+
+  $list_spec{$class} = {
+    group_by    => $params{group_by},
+    column_name => $params{column_name},
+  };
+}
+
 #
 # Helper functions
 #
 
+sub get_group_by_where {
+  my ($self)   = @_;
+
+  my $group_by = get_spec(ref $self, 'group_by') || [];
+  $group_by    = [ $group_by ] if $group_by && !ref $group_by;
+
+  my @where    = map { my $value = $self->$_; defined($value) ? "(${_} = " . $value . ")" : "(${_} IS NULL)" } @{ $group_by };
+
+  return join ' AND ', @where;
+}
+
 sub set_position {
   my ($self) = @_;
   my $column = column_name($self);
 
-  if (!defined $self->$column) {
-    my $max_position = $self->db->dbh->selectrow_arrayref(qq|SELECT COALESCE(max(${column}), 0) FROM | . $self->meta->table)->[0];
-    $self->$column($max_position + 1);
-  }
+  return 1 if defined $self->$column;
+
+  my $table        = $self->meta->table;
+  my $where        = get_group_by_where($self);
+  $where           = " WHERE ${where}" if $where;
+  my $sql = <<SQL;
+    SELECT COALESCE(max(${column}), 0)
+    FROM ${table}
+    ${where}
+SQL
+
+  my $max_position = $self->db->dbh->selectrow_arrayref($sql)->[0];
+  $self->$column($max_position + 1);
 
   return 1;
 }
@@ -75,10 +106,19 @@ sub remove_position {
   my $column = column_name($self);
 
   $self->load;
-  if (defined $self->$column) {
-    $self->_get_manager_class->update_all(set   => { $column => \"${column} - 1" },
-                                          where => [ $column => { gt => $self->$column } ]);
-  }
+  return 1 unless defined $self->$column;
+
+  my $table    = $self->meta->table;
+  my $value    = $self->$column;
+  my $group_by = get_group_by_where($self);
+  $group_by    = ' AND ' . $group_by if $group_by;
+  my $sql      = <<SQL;
+    UPDATE ${table}
+    SET ${column} = ${column} - 1
+    WHERE (${column} > ${value}) ${group_by}
+SQL
+
+  $self->db->dbh->do($sql);
 
   return 1;
 }
@@ -90,22 +130,48 @@ sub do_move {
   croak "Object has not been saved yet" unless $self->id;
   croak "No position set yet"           unless defined $self->$column;
 
-  my ($comp_sql, $comp_rdbo, $min_max, $plus_minus) = $direction eq 'up' ? ('<', 'ge', 'max', '+') : ('>', 'le', 'min', '-');
+  my $table                                        = $self->meta->table;
+  my $old_position                                 = $self->$column;
+  my ($comp_sel, $comp_upd, $min_max, $plus_minus) = $direction eq 'up' ? ('<', '>=', 'max', '+') : ('>', '<=', 'min', '-');
+  my $group_by                                     = get_group_by_where($self);
+  $group_by                                        = ' AND ' . $group_by if $group_by;
+  my $sql                                          = <<SQL;
+    SELECT ${min_max}(${column})
+    FROM ${table}
+    WHERE (${column} ${comp_sel} ${old_position})
+      ${group_by}
+SQL
 
-  my $new_position = $self->db->dbh->selectrow_arrayref(qq|SELECT ${min_max}(${column}) FROM | . $self->meta->table . qq| WHERE ${column} ${comp_sql} | . $self->$column)->[0];
+  my $new_position = $self->db->dbh->selectrow_arrayref($sql)->[0];
 
   return undef unless defined $new_position;
 
-  $self->_get_manager_class->update_all(set   => { $column => $self->$column },
-                                        where => [ $column => $new_position ]);
+  $sql = <<SQL;
+    UPDATE ${table}
+    SET ${column} = ${old_position}
+    WHERE (${column} = ${new_position})
+     ${group_by};
+SQL
+
+  $self->db->dbh->do($sql);
+
   $self->update_attributes($column => $new_position);
 }
 
 sub column_name {
   my ($self) = @_;
+  my $column = get_spec(ref $self, 'column_name');
+  return $column if $column;
   return $self->can('sortkey') ? 'sortkey' : 'position';
 }
 
+sub get_spec {
+  my ($class, $key) = @_;
+
+  return undef unless $list_spec{$class};
+  return $list_spec{$class}->{$key};
+}
+
 1;
 __END__
 
@@ -116,7 +182,7 @@ __END__
 =head1 NAME
 
 SL::DB::Helper::ActsAsList - Mixin for managing ordered items by a
-column I<position> or I<sortkey>
+column
 
 =head1 SYNOPSIS
 
@@ -146,7 +212,44 @@ in the table plus one.
 When the object is deleted all positions greater than the object's old
 position are decreased by one.
 
-=head1 FUNCTIONS
+The column name to use can be configured via L<configure_acts_as_list>.
+
+=head1 CLASS FUNCTIONS
+
+=over 4
+
+=item C<configure_acts_as_list %params>
+
+Configures the mixin's behaviour. C<%params> can contain the following
+values:
+
+=over 2
+
+=item C<column_name>
+
+The name of the column containing the position. If not set explicitly
+then the mixin will use C<sortkey> if the model contains such a column
+(only for legacy tables) and C<position> otherwise.
+
+=item C<group_by>
+
+An optional column name (or array reference of column names) by which
+to group. If a table contains items for several distinct sets and each
+set has its own sorting then this can be used.
+
+An example would be requirement spec text blocks. They have a column
+called C<output_position> that selects where to output the text blocks
+(either before or after the sections). Furthermore these text blocks
+each belong to a single requirement spec document. So each combination
+of C<requirement_spec_id> and C<output_position> should have its own
+set of C<position> values, which can be achieved by configuring this
+mixin with C<group_by = [qw(requirement_spec_id output_position)]>.
+
+=back
+
+=back
+
+=head1 INSTANCE FUNCTIONS
 
 =over 4