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;
do_move($self, 'down');
}
+sub remove_from_list {
+ my ($self) = @_;
+
+ my $worker = 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);
+ };
+
+ return $self->db->in_transaction ? $worker->() : $self->db->do_transaction($worker);
+}
+
+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
+
+ my $worker = sub {
+ $self->db->dbh->do($query, undef, $new_position - 1, @values);
+ $self->update_attributes($column => $new_position);
+ };
+
+ return $self->db->in_transaction ? $worker->() : $self->db->do_transaction($worker);
+}
+
+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) = @_;
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);
+ 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}
- ${where}
+ WHERE (${column} <> -1)
+ ${group_by}
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;
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
- WHERE (${column} > ${value}) ${group_by}
+ WHERE (${column} > ?)
+ ${group_by}
SQL
- $self->db->dbh->do($sql);
+ $self->db->dbh->do($sql, undef, $value, @values);
return 1;
}
sub do_move {
my ($self, $direction) = @_;
- my $column = column_name($self);
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 $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}
- WHERE (${column} ${comp_sel} ${old_position})
+ WHERE (${column} <> -1)
+ AND (${column} ${comp_sel} ?)
${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}
- SET ${column} = ${old_position}
- WHERE (${column} = ${new_position})
+ SET ${column} = ?
+ WHERE (${column} = ?)
${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);
}
+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');
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