This module provides a simple interface for modifying the position
of DBIx::Class objects.
+=head1 AUTO UPDATE
+
+All of the move_* methods automatically update the rows involved in
+the query. This is not configurable and is due to the fact that if you
+move a record it always causes other records in the list to be updated.
+
=head1 METHODS
=head2 position_column
__PACKAGE__->mk_classdata( 'position_column' => 'position' );
+=head2 collection_column
+
+ __PACKAGE__->collection_column('thing_id');
+
+This method specified a column to limit all queries in
+this module by. This effectively allows you to have multiple
+positioned lists within the same table.
+
+=cut
+
+__PACKAGE__->mk_classdata( 'collection_column' );
+
=head2 siblings
my $rs = $employee->siblings();
my $rs = $self->search(
{
$position_column => { '!=' => $self->get_column($position_column) },
- $self->_parent_clause(),
+ $self->_collection_clause(),
},
{ order_by => $self->position_column },
);
sub first_sibling {
my( $self ) = @_;
return ($self->search(
- { $self->_parent_clause() },
+ { $self->_collection_clause() },
{ rows=>1, order_by => $self->position_column },
)->all())[0];
}
sub last_sibling {
my( $self ) = @_;
return ($self->search(
- { $self->_parent_clause() },
+ { $self->_collection_clause() },
{ rows=>1, order_by => $self->position_column.' DESC' },
)->all())[0];
}
return ($self->search(
{
$position_column => { '<' => $self->get_column($position_column) },
- $self->_parent_clause(),
+ $self->_collection_clause(),
},
{ rows=>1, order_by => $position_column.' DESC' },
)->all())[0];
sub next_sibling {
my( $self ) = @_;
my $position_column = $self->position_column;
- return ($self->search(
+ return ($self->result_source->resultset->search(
{
$position_column => { '>' => $self->get_column($position_column) },
- $self->_parent_clause(),
+ $self->_collection_clause(),
},
{ rows=>1, order_by => $position_column },
)->all())[0];
sub move_previous {
my( $self ) = @_;
- my $previous = $self->previous_sibling();
- return undef if (!$previous);
- my $position_column = $self->position_column;
- my $self_position = $self->get_column( $position_column );
- $self->set_column( $position_column, $previous->get_column($position_column) );
- $previous->set_column( $position_column, $self_position );
- $self->update();
- $previous->update();
- return 1;
+ my $position = $self->get_column( $self->position_column() );
+ return $self->move_to( $position - 1 );
}
=head2 move_next
sub move_next {
my( $self ) = @_;
- my $next = $self->next_sibling();
- return undef if (!$next);
- my $position_column = $self->position_column;
- my $self_position = $self->get_column( $position_column );
- $self->set_column( $position_column, $next->get_column($position_column) );
- $next->set_column( $position_column, $self_position );
- $self->update();
- $next->update();
- return 1;
+ my $position = $self->get_column( $self->position_column() );
+ my $count = $self->result_source->resultset->search({$self->_collection_clause()})->count();
+ return 0 if ($position==$count);
+ return $self->move_to( $position + 1 );
}
=head2 move_first
sub move_last {
my( $self ) = @_;
- my $count = $self->search({$self->_parent_clause()})->count();
+ my $count = $self->result_source->resultset->search({$self->_collection_clause()})->count();
return $self->move_to( $count );
}
my( $self, $to_position ) = @_;
my $position_column = $self->position_column;
my $from_position = $self->get_column( $position_column );
- return undef if ( $from_position==$to_position );
- my $rs = $self->search({
+ return 0 if ( $to_position < 1 );
+ return 0 if ( $from_position==$to_position );
+ my $rs = $self->result_source->resultset->search({
-and => [
$position_column => { ($from_position>$to_position?'<':'>') => $from_position },
$position_column => { ($from_position>$to_position?'>=':'<=') => $to_position },
],
- $self->_parent_clause(),
+ $self->_collection_clause(),
});
my $op = ($from_position>$to_position) ? '+' : '-';
$rs->update({
sub insert {
my $self = shift;
my $position_column = $self->position_column;
- $self->set_column( $position_column => $self->search( {$self->_parent_clause()} )->count()+1 )
+ $self->set_column( $position_column => $self->result_source->resultset->search( {$self->_collection_clause()} )->count()+1 )
if (!$self->get_column($position_column));
$self->next::method( @_ );
}
These methods are used internally. You should never have the
need to use them.
-=head2 _parent_clause
-
- sub _parent_clause {
- my( $self ) = @_;
- return ( parent_id => $self->parent_id );
- }
+=head2 _collection_clause
-This method is a placeholder for you, or another component, to
-provide additional limits for all the various queries in this
-module. This allows for more than one positionable list within
-the same table since any move_* method will adhere to the clause
-that you specify.
+This method returns a name=>value pare for limiting a search
+by the collection column. If the collection column is not
+defined then this will return an empty list.
=cut
-sub _parent_clause {
+sub _collection_clause {
+ my $self = shift;
+ if ($self->collection_column()) {
+ return ( $self->collection_column() => $self->get_column($self->collection_column()) );
+ }
return ();
}
=head1 BUGS
+=head2 Race Condition on Insert
+
If a position is not specified for an insert than a position
will be chosen based on COUNT(*)+1. But, it first selects the
count then inserts the record. The space of time between select
ability to lock tables in DBIC. I've added an entry in the TODO
about this.
+=head2 Multiple Moves
+
+Be careful when issueing move_* methods to multiple objects. If
+you've pre-loaded the objects then when you move one of the objects
+the position of the other object will not reflect their new value
+until you reload them from the database.
+
+The are times when you will want to move objects as groups, such
+as changeing the parent of several objects at once - this directly
+conflicts with this problem. One solution is for us to write a
+ResultSet class that supports a parent() method, for example. Another
+solution is to somehow automagically modify the objects that exist
+in the current object's result set to have the new position value.
+
=head1 AUTHOR
Aran Deltac <bluefeet@cpan.org>
plan tests => 96;
my $schema = shift;
- foreach my $class ( 'Positioned', 'PositionedAdjacencyList', 'AdjacencyList' ) {
+ my $employees = $schema->resultset('Employee::Positioned');
- my $employees = $schema->resultset('Employee::'.$class);
+ if ($employees->result_class->can('position_column')) {
- if ($employees->result_class->can('position_column')) {
+ $employees->delete();
+ foreach (1..5) {
+ $employees->create({ name=>'temp' });
+ }
- $employees->delete();
- foreach (1..5) {
- $employees->create({ name=>'temp' });
- }
+ $employees = $employees->search(undef,{order_by=>'position'});
+ ok( check_positions($employees), "$class: intial positions" );
- $employees = $employees->search(undef,{order_by=>'position'});
- ok( check_positions($employees), "$class: intial positions" );
+ my $employee;
- my $employee;
+ foreach my $position (1..$employees->count()) {
- foreach my $position (1..$employees->count()) {
+ $employee = $employees->find({ position=>$position });
+ $employee->move_previous();
+ ok( check_positions($employees), "$class: move_previous( $position )" );
- $employee = $employees->find({ position=>$position });
- $employee->move_previous();
- ok( check_positions($employees), "$class: move_previous( $position )" );
+ $employee = $employees->find({ position=>$position });
+ $employee->move_next();
+ ok( check_positions($employees), "$class: move_next( $position )" );
- $employee = $employees->find({ position=>$position });
- $employee->move_next();
- ok( check_positions($employees), "$class: move_next( $position )" );
+ $employee = $employees->find({ position=>$position });
+ $employee->move_first();
+ ok( check_positions($employees), "$class: move_first( $position )" );
- $employee = $employees->find({ position=>$position });
- $employee->move_first();
- ok( check_positions($employees), "$class: move_first( $position )" );
+ $employee = $employees->find({ position=>$position });
+ $employee->move_last();
+ ok( check_positions($employees), "$class: move_last( $position )" );
+ foreach my $to_position (1..$employees->count()) {
$employee = $employees->find({ position=>$position });
- $employee->move_last();
- ok( check_positions($employees), "$class: move_last( $position )" );
-
- foreach my $to_position (1..$employees->count()) {
- $employee = $employees->find({ position=>$position });
- $employee->move_to($to_position);
- ok( check_positions($employees), "$class: move_to( $position => $to_position )" );
- }
-
- }
- }
- if ($employees->result_class->can('parent_column')) {
-
- $employees->delete();
- my $mom = $employees->create({ name=>'temp' });
- foreach (1..14) {
- $employees->create({ name=>'temp', parent_id=>$mom->id() });
- }
-
- my $children = $mom->children();
- ok( ($children->count()==14), 'correct number of children' );
-
- my $grandma = $mom;
- my @children = $children->all();
- $mom = pop(@children);
- foreach my $child (@children) {
- $child->parent( $mom );
+ $employee->move_to($to_position);
+ ok( check_positions($employees), "$class: move_to( $position => $to_position )" );
}
- ok( ($mom->children->count() == 13), 'correct number of grandchildren' );
-
- if ($employees->result_class->can('position_column')) {
- # TODO: Test positioning within a tree.
- }
}
}
-
}
sub check_positions {
my( $employees ) = @_;
$employees->reset();
my $expected_position = 0;
- my $is_ok = 1;
while (my $employee = $employees->next()) {
$expected_position ++;
if ($employee->position()!=$expected_position) {
- $is_ok = 0;
- last;
+ return 0;
}
}
- return $is_ok;
+ return 1;
}
1;