LinkedRecords: with_objects erlauben für shipped_qty
[kivitendo-erp.git] / SL / DB / Helper / ActsAsList.pm
index 5734c2e..ee49f4a 100644 (file)
@@ -3,7 +3,8 @@ package SL::DB::Helper::ActsAsList;
 use strict;
 
 use parent qw(Exporter);
 use strict;
 
 use parent qw(Exporter);
-our @EXPORT = qw(move_position_up move_position_down reorder_list configure_acts_as_list);
+our @EXPORT = qw(move_position_up move_position_down add_to_list remove_from_list reorder_list configure_acts_as_list
+                 get_previous_in_list get_next_in_list get_full_list);
 
 use Carp;
 
 
 use Carp;
 
@@ -13,12 +14,13 @@ sub import {
   my ($class, @params)   = @_;
   my $importing = caller();
 
   my ($class, @params)   = @_;
   my $importing = caller();
 
+  configure_acts_as_list($importing, @params);
+
   $importing->before_save(  sub { SL::DB::Helper::ActsAsList::set_position(@_)    });
   $importing->before_delete(sub { SL::DB::Helper::ActsAsList::remove_position(@_) });
 
   $importing->before_save(  sub { SL::DB::Helper::ActsAsList::set_position(@_)    });
   $importing->before_delete(sub { SL::DB::Helper::ActsAsList::remove_position(@_) });
 
-  # Use 'goto' so that Exporter knows which module to import into via
-  # 'caller()'.
-  goto &Exporter::import;
+  # Don't 'goto' to Exporters import, it would try to parse @params
+  __PACKAGE__->export_to_level(1, $class, @EXPORT);
 }
 
 #
 }
 
 #
@@ -35,6 +37,102 @@ sub move_position_down {
   do_move($self, 'down');
 }
 
   do_move($self, 'down');
 }
 
+sub remove_from_list {
+  my ($self) = @_;
+
+  return $self->db->with_transaction(sub {
+    remove_position($self);
+
+    # Set to -1 manually because $self->update_attributes() would
+    # trigger the before_save() hook from this very plugin assigning a
+    # number at the end of the list again.
+    my $table           = $self->meta->table;
+    my $column          = column_name($self);
+    my $primary_key_col = ($self->meta->primary_key)[0];
+    my $sql             = <<SQL;
+      UPDATE ${table}
+      SET ${column} = -1
+      WHERE ${primary_key_col} = ?
+SQL
+    $self->db->dbh->do($sql, undef, $self->$primary_key_col);
+    $self->$column(undef);
+  });
+}
+
+sub add_to_list {
+  my ($self, %params) = @_;
+
+  croak "Invalid parameter 'position'" unless ($params{position} || '') =~ m/^ (?: before | after | first | last ) $/x;
+
+  my $column = column_name($self);
+
+  $self->remove_from_list if ($self->$column // -1) != -1;
+
+  if ($params{position} eq 'last') {
+    set_position($self);
+    $self->save;
+    return;
+  }
+
+  my $table               = $self->meta->table;
+  my $primary_key_col     = ($self->meta->primary_key)[0];
+  my ($group_by, @values) = get_group_by_where($self);
+  $group_by               = " AND ${group_by}" if $group_by;
+  my $new_position;
+
+  if ($params{position} eq 'first') {
+    $new_position = 1;
+
+  } else {
+    # Can only be 'before' or 'after' -- 'last' has been checked above
+    # already.
+
+    my $reference = $params{reference};
+    croak "Missing parameter 'reference'" if !$reference;
+
+    my $reference_pos;
+    if (ref $reference) {
+      $reference_pos = $reference->$column;
+    } else {
+      ($reference_pos) = $self->db->dbh->selectrow_array(qq|SELECT ${column} FROM ${table} WHERE ${primary_key_col} = ?|, undef, $reference);
+    }
+
+    $new_position = $params{position} eq 'before' ? $reference_pos : $reference_pos + 1;
+  }
+
+  my $query = <<SQL;
+    UPDATE ${table}
+    SET ${column} = ${column} + 1
+    WHERE (${column} > ?)
+      ${group_by}
+SQL
+
+  return $self->db->with_transaction(sub {
+    $self->db->dbh->do($query, undef, $new_position - 1, @values);
+    $self->update_attributes($column => $new_position);
+  });
+}
+
+sub get_next_in_list {
+  my ($self) = @_;
+  return get_previous_or_next($self, 'next');
+}
+
+sub get_previous_in_list {
+  my ($self) = @_;
+  return get_previous_or_next($self, 'previous');
+}
+
+sub get_full_list {
+  my ($self) = @_;
+
+  my $group_by = get_spec(ref $self, 'group_by') || [];
+  $group_by    = [ $group_by ] if $group_by && !ref $group_by;
+  my @where    = map { ($_ => $self->$_) } @{ $group_by };
+
+  return $self->_get_manager_class->get_all(where => \@where, sort_by => column_name($self) . ' ASC');
+}
+
 sub reorder_list {
   my ($class_or_self, @ids) = @_;
 
 sub reorder_list {
   my ($class_or_self, @ids) = @_;
 
@@ -42,7 +140,7 @@ sub reorder_list {
 
   my $self   = ref($class_or_self) ? $class_or_self : $class_or_self->new;
   my $column = column_name($self);
 
   my $self   = ref($class_or_self) ? $class_or_self : $class_or_self->new;
   my $column = column_name($self);
-  my $result = $self->db->do_transaction(sub {
+  my $result = $self->db->with_transaction(sub {
     my $query = qq|UPDATE | . $self->meta->table . qq| SET ${column} = ? WHERE id = ?|;
     my $sth   = $self->db->dbh->prepare($query) || die $self->db->dbh->errstr;
 
     my $query = qq|UPDATE | . $self->meta->table . qq| SET ${column} = ? WHERE id = ?|;
     my $sth   = $self->db->dbh->prepare($query) || die $self->db->dbh->errstr;
 
@@ -51,6 +149,8 @@ sub reorder_list {
     }
 
     $sth->finish;
     }
 
     $sth->finish;
+
+    1;
   });
 
   return $result;
   });
 
   return $result;
@@ -75,27 +175,34 @@ sub get_group_by_where {
   my $group_by = get_spec(ref $self, 'group_by') || [];
   $group_by    = [ $group_by ] if $group_by && !ref $group_by;
 
   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 };
+  my (@where, @values);
+  foreach my $column (@{ $group_by }) {
+    my $value = $self->$column;
+    push @values, $value if defined $value;
+    push @where,  defined($value) ? "(${column} = ?)" : "(${column} IS NULL)";
+  }
 
 
-  return join ' AND ', @where;
+  return (join(' AND ', @where), @values);
 }
 
 sub set_position {
   my ($self) = @_;
   my $column = column_name($self);
 }
 
 sub set_position {
   my ($self) = @_;
   my $column = column_name($self);
+  my $value  = $self->$column;
 
 
-  return 1 if defined $self->$column;
+  return 1 if defined($value) && ($value != -1);
 
 
-  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)
+  my $table               = $self->meta->table;
+  my ($group_by, @values) = get_group_by_where($self);
+  $group_by               = " AND ${group_by}" if $group_by;
+  my $sql                 = <<SQL;
+    SELECT COALESCE(MAX(${column}), 0)
     FROM ${table}
     FROM ${table}
-    ${where}
+    WHERE (${column} <> -1)
+      ${group_by}
 SQL
 
 SQL
 
-  my $max_position = $self->db->dbh->selectrow_arrayref($sql)->[0];
+  my $max_position = $self->db->dbh->selectrow_arrayref($sql, undef, @values)->[0];
   $self->$column($max_position + 1);
 
   return 1;
   $self->$column($max_position + 1);
 
   return 1;
@@ -106,58 +213,85 @@ sub remove_position {
   my $column = column_name($self);
 
   $self->load;
   my $column = column_name($self);
 
   $self->load;
-  return 1 unless defined $self->$column;
+  my $value = $self->$column;
+  return 1 unless defined($value) && ($value != -1);
 
 
-  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;
+  my $table               = $self->meta->table;
+  my ($group_by, @values) = get_group_by_where($self);
+  $group_by               = ' AND ' . $group_by if $group_by;
+  my $sql                 = <<SQL;
     UPDATE ${table}
     SET ${column} = ${column} - 1
     UPDATE ${table}
     SET ${column} = ${column} - 1
-    WHERE (${column} > ${value}) ${group_by}
+    WHERE (${column} > ?)
+     ${group_by}
 SQL
 
 SQL
 
-  $self->db->dbh->do($sql);
+  $self->db->dbh->do($sql, undef, $value, @values);
 
   return 1;
 }
 
 sub do_move {
   my ($self, $direction) = @_;
 
   return 1;
 }
 
 sub do_move {
   my ($self, $direction) = @_;
-  my $column             = column_name($self);
 
   croak "Object has not been saved yet" unless $self->id;
 
   croak "Object has not been saved yet" unless $self->id;
-  croak "No position set yet"           unless defined $self->$column;
+
+  my $column       = column_name($self);
+  my $old_position = $self->$column;
+  croak "No position set yet" unless defined($old_position) && ($old_position != -1);
 
   my $table                                        = $self->meta->table;
 
   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);
+  my ($comp_sel, $comp_upd, $min_max, $plus_minus) = $direction eq 'up' ? ('<', '>=', 'MAX', '+') : ('>', '<=', 'MIN', '-');
+  my ($group_by, @values)                          = get_group_by_where($self);
   $group_by                                        = ' AND ' . $group_by if $group_by;
   my $sql                                          = <<SQL;
     SELECT ${min_max}(${column})
     FROM ${table}
   $group_by                                        = ' AND ' . $group_by if $group_by;
   my $sql                                          = <<SQL;
     SELECT ${min_max}(${column})
     FROM ${table}
-    WHERE (${column} ${comp_sel} ${old_position})
+    WHERE (${column} <>          -1)
+      AND (${column} ${comp_sel} ?)
       ${group_by}
 SQL
 
       ${group_by}
 SQL
 
-  my $new_position = $self->db->dbh->selectrow_arrayref($sql)->[0];
+  my $new_position = $self->db->dbh->selectrow_arrayref($sql, undef, $old_position, @values)->[0];
 
   return undef unless defined $new_position;
 
   $sql = <<SQL;
     UPDATE ${table}
 
   return undef unless defined $new_position;
 
   $sql = <<SQL;
     UPDATE ${table}
-    SET ${column} = ${old_position}
-    WHERE (${column} = ${new_position})
+    SET ${column} = ?
+    WHERE (${column} = ?)
      ${group_by};
 SQL
 
      ${group_by};
 SQL
 
-  $self->db->dbh->do($sql);
+  $self->db->dbh->do($sql, undef, $old_position, $new_position, @values);
 
   $self->update_attributes($column => $new_position);
 }
 
 
   $self->update_attributes($column => $new_position);
 }
 
+sub get_previous_or_next {
+  my ($self, $direction)  = @_;
+
+  my $asc_desc            = $direction eq 'next' ? 'ASC' : 'DESC';
+  my $comparator          = $direction eq 'next' ? '>'   : '<';
+  my $table               = $self->meta->table;
+  my $column              = column_name($self);
+  my $primary_key_col     = ($self->meta->primary_key)[0];
+  my ($group_by, @values) = get_group_by_where($self);
+  $group_by               = " AND ${group_by}" if $group_by;
+  my $sql                 = <<SQL;
+    SELECT ${primary_key_col}
+    FROM ${table}
+    WHERE (${column} ${comparator} ?)
+      ${group_by}
+    ORDER BY ${column} ${asc_desc}
+    LIMIT 1
+SQL
+
+  my $id = ($self->db->dbh->selectrow_arrayref($sql, undef, $self->$column, @values) || [])->[0];
+
+  return $id ? $self->_get_manager_class->find_by(id => $id) : undef;
+}
+
 sub column_name {
   my ($self) = @_;
   my $column = get_spec(ref $self, 'column_name');
 sub column_name {
   my ($self) = @_;
   my $column = get_spec(ref $self, 'column_name');
@@ -187,7 +321,7 @@ column
 =head1 SYNOPSIS
 
   package SL::DB::SomeObject;
 =head1 SYNOPSIS
 
   package SL::DB::SomeObject;
-  use SL::DB::Helper::ActsAsList;
+  use SL::DB::Helper::ActsAsList [ PARAMS ];
 
   package SL::Controller::SomeController;
   ...
 
   package SL::Controller::SomeController;
   ...
@@ -212,7 +346,8 @@ in the table plus one.
 When the object is deleted all positions greater than the object's old
 position are decreased by one.
 
 When the object is deleted all positions greater than the object's old
 position are decreased by one.
 
-The column name to use can be configured via L<configure_acts_as_list>.
+C<PARAMS> will be given to L<configure_acts_as_list> and can be used to
+set the column name.
 
 =head1 CLASS FUNCTIONS
 
 
 =head1 CLASS FUNCTIONS
 
@@ -220,8 +355,8 @@ The column name to use can be configured via L<configure_acts_as_list>.
 
 =item C<configure_acts_as_list %params>
 
 
 =item C<configure_acts_as_list %params>
 
-Configures the mixin's behaviour. C<%params> can contain the following
-values:
+Configures the mixin's behaviour. Will get called automatically with the
+include parameters. C<%params> can contain the following values:
 
 =over 2
 
 
 =over 2
 
@@ -263,6 +398,47 @@ regarding their sort order by exchanging their C<position> values.
 Swaps the object with the object one step below the current one
 regarding their sort order by exchanging their C<position> values.
 
 Swaps the object with the object one step below the current one
 regarding their sort order by exchanging their C<position> values.
 
+=item C<add_to_list %params>
+
+Adds this item to the list. The parameter C<position> is required and
+can be one of C<first>, C<last>, C<before> and C<after>. With C<first>
+the item is inserted as the first item in the list and all other
+item's positions are shifted up by one. For C<position = last> the
+item is inserted at the end of the list.
+
+For C<before> and C<after> an additional parameter C<reference> is
+required. This is either a Rose model instance or the primary key of
+one. The current item will then be inserted either before or after the
+referenced item by shifting all the appropriate item positions up by
+one.
+
+If C<$self>'s positional column is already set when this function is
+called then L</remove_from_list> will be called first before anything
+else is done.
+
+After this function C<$self>'s positional column has been set and
+saved to the database.
+
+=item C<remove_from_list>
+
+Sets this items positional column to C<-1>, saves it and moves all
+following items up by 1.
+
+=item C<get_previous_in_list>
+
+Fetches the previous item in the list. Returns C<undef> if C<$self> is
+already the first one.
+
+=item C<get_next_in_list>
+
+Fetches the next item in the list. Returns C<undef> if C<$self> is
+already the last one.
+
+=item C<get_full_list>
+
+Fetches all items in the same list as C<$self> and returns them as an
+array reference.
+
 =item C<reorder_list @ids>
 
 Re-orders the objects given in C<@ids> by their position in C<@ids> by
 =item C<reorder_list @ids>
 
 Re-orders the objects given in C<@ids> by their position in C<@ids> by