Merge 'ordered_handle_updates' into 'DBIx-Class-current'
Matt S Trout [Tue, 2 Jan 2007 20:36:29 +0000 (20:36 +0000)]
multi-col group support for Ordered, from Neil de Carteret

Changes
lib/DBIx/Class.pm
lib/DBIx/Class/Ordered.pm
t/87ordered.t
t/lib/DBICTest/Schema/Employee.pm
t/lib/sqlite.sql

diff --git a/Changes b/Changes
index 31cb403..26aa117 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,5 +1,6 @@
 Revision history for DBIx::Class
 
+        - add support to Ordered for multiple ordering columns
         - mark DB.pm and compose_connection as deprecated
         - switch tests to compose_namespace
 
index 6d75377..91f6677 100644 (file)
@@ -217,6 +217,8 @@ konobi: Scott McWhirter
 
 LTJake: Brian Cassidy <bricas@cpan.org>
 
+ned: Neil de Carteret
+
 nigel: Nigel Metheringham <nigelm@cpan.org>
 
 ningu: David Kamholz <dkamholz@cpan.org>
index 8e2c74d..d5a7a00 100644 (file)
@@ -17,7 +17,26 @@ Create a table for your ordered data.
     name TEXT NOT NULL,
     position INTEGER NOT NULL
   );
-  # Optional: group_id INTEGER NOT NULL
+
+Optionally, add one or more columns to specify groupings, allowing you 
+to maintain independent ordered lists within one table:
+
+  CREATE TABLE items (
+    item_id INTEGER PRIMARY KEY AUTOINCREMENT,
+    name TEXT NOT NULL,
+    position INTEGER NOT NULL,
+    group_id INTEGER NOT NULL
+  );
+
+Or even
+
+  CREATE TABLE items (
+    item_id INTEGER PRIMARY KEY AUTOINCREMENT,
+    name TEXT NOT NULL,
+    position INTEGER NOT NULL,
+    group_id INTEGER NOT NULL,
+    other_group_id INTEGER NOT NULL
+  );
 
 In your Schema or DB class add Ordered to the top 
 of the component list.
@@ -29,7 +48,14 @@ each row.
 
   package My::Item;
   __PACKAGE__->position_column('position');
-  __PACKAGE__->grouping_column('group_id'); # optional
+
+If you are using one grouping column, specify it as follows:
+
+  __PACKAGE__->grouping_column('group_id');
+
+Or if you have multiple grouping columns:
+
+  __PACKAGE__->grouping_column(['group_id', 'other_group_id']);
 
 Thats it, now you can change the position of your objects.
 
@@ -54,6 +80,10 @@ Thats it, now you can change the position of your objects.
   $item->move_first();
   $item->move_last();
   $item->move_to( $position );
+  $item->move_to_group( 'groupname' );
+  $item->move_to_group( 'groupname', $position );
+  $item->move_to_group( {group_id=>'groupname', 'other_group_id=>'othergroupname'} );
+  $item->move_to_group( {group_id=>'groupname', 'other_group_id=>'othergroupname'}, $position );
 
 =head1 DESCRIPTION
 
@@ -127,6 +157,7 @@ is this sibliing.
 sub first_sibling {
     my( $self ) = @_;
     return 0 if ($self->get_column($self->position_column())==1);
+
     return ($self->result_source->resultset->search(
         {
             $self->position_column => 1,
@@ -289,11 +320,72 @@ sub move_to {
         $self->_grouping_clause(),
     });
     my $op = ($from_position>$to_position) ? '+' : '-';
-    $rs->update({ $position_column => \"$position_column $op 1" });
+    $rs->update({ $position_column => \"$position_column $op 1" });  #" Sorry, GEdit bug
+    $self->{_ORDERED_INTERNAL_UPDATE} = 1;
     $self->update({ $position_column => $to_position });
     return 1;
 }
 
+
+
+=head2 move_to_group
+
+  $item->move_to_group( $group, $position );
+
+Moves the object to the specified position of the specified
+group, or to the end of the group if $position is undef.
+1 is returned on success, and 0 is returned if the object is
+already at the specified position of the specified group.
+
+$group may be specified as a single scalar if only one 
+grouping column is in use, or as a hashref of column => value pairs
+if multiple grouping columns are in use.
+
+=cut
+
+sub move_to_group {
+    my( $self, $to_group, $to_position ) = @_;
+
+    # if we're given a string, turn it into a hashref
+    unless (ref $to_group eq 'HASH') {
+        $to_group = {($self->_grouping_columns)[0] => $to_group};
+    }
+
+    my $position_column = $self->position_column;
+    #my @grouping_columns = $self->_grouping_columns;
+
+    return 0 if ( ! defined($to_group) );
+    return 0 if ( defined($to_position) and $to_position < 1 );
+    return 0 if ( $self->_is_in_group($to_group) 
+                    and ((not defined($to_position)) 
+                            or (defined($to_position) and $self->$position_column==$to_position)
+                        )
+                    );
+
+    # Move to end of current group and adjust siblings
+    $self->move_last;
+
+    $self->set_columns($to_group);
+    my $new_group_count = $self->result_source->resultset->search({$self->_grouping_clause()})->count();
+    if (!defined($to_position) or $to_position > $new_group_count) {
+        $self->{_ORDERED_INTERNAL_UPDATE} = 1;
+        $self->update({ $position_column => $new_group_count + 1 });
+    }
+    else {
+        my @between = ($to_position, $new_group_count);
+
+        my $rs = $self->result_source->resultset->search({
+            $position_column => { -between => [ @between ] },
+            $self->_grouping_clause(),
+        });
+        $rs->update({ $position_column => \"$position_column + 1" }); #"
+        $self->{_ORDERED_INTERNAL_UPDATE} = 1;
+        $self->update({ $position_column => $to_position });
+    }
+
+    return 1;
+}
+
 =head2 insert
 
 Overrides the DBIC insert() method by providing a default 
@@ -310,6 +402,53 @@ sub insert {
     return $self->next::method( @_ );
 }
 
+=head2 update
+
+Overrides the DBIC update() method by checking for a change
+to the position and/or group columns.  Movement within a
+group or to another group is handled by repositioning
+the appropriate siblings.  Position defaults to the end
+of a new group if it has been changed to undef.
+
+=cut
+
+sub update {
+    my $self = shift;
+
+    if ($self->{_ORDERED_INTERNAL_UPDATE}) {
+        delete $self->{_ORDERED_INTERNAL_UPDATE};
+        return $self->next::method( @_ );
+    }
+
+    $self->set_columns($_[0]) if @_ > 0;
+    my %changes = $self->get_dirty_columns;
+    $self->discard_changes;
+
+    my $pos_col = $self->position_column;
+
+    # if any of our grouping columns have been changed
+    if (grep {$_} map {exists $changes{$_}} $self->_grouping_columns ) {
+
+        # create new_group by taking the current group and inserting changes
+        my $new_group = {$self->_grouping_clause};
+        foreach my $col (keys %$new_group) {
+            if (exists $changes{$col}) {
+                $new_group->{$col} = $changes{$col};
+                delete $changes{$col}; # don't want to pass this on to next::method
+            }
+        }
+
+        $self->move_to_group(
+            $new_group,
+            exists($changes{$pos_col}) ? delete($changes{$pos_col}) : $self->$pos_col
+        );
+    }
+    elsif (exists $changes{$pos_col}) {
+        $self->move_to(delete $changes{$pos_col});
+    }
+    return $self->next::method( \%changes );
+}
+
 =head2 delete
 
 Overrides the DBIC delete() method by first moving the object 
@@ -331,21 +470,57 @@ need to use them.
 
 =head2 _grouping_clause
 
-This method returns a name=>value pare for limiting a search 
-by the collection column.  If the collection column is not 
+This method returns one or more name=>value pairs for limiting a search 
+by the grouping column(s).  If the grouping column is not 
 defined then this will return an empty list.
 
 =cut
-
 sub _grouping_clause {
     my( $self ) = @_;
+    return map {  $_ => $self->get_column($_)  } $self->_grouping_columns();
+}
+
+
+
+=head2 _get_grouping_columns
+
+Returns a list of the column names used for grouping, regardless of whether
+they were specified as an arrayref or a single string, and returns ()
+if there is no grouping.
+
+=cut
+sub _grouping_columns {
+    my( $self ) = @_;
     my $col = $self->grouping_column();
-    if ($col) {
-        return ( $col => $self->get_column($col) );
+    if (ref $col eq 'ARRAY') {
+        return @$col;
+    } elsif ($col) {
+        return ( $col );
+    } else {
+        return ();
     }
-    return ();
 }
 
+
+
+=head2 _is_in_group($other)
+
+    $item->_is_in_group( {user => 'fred', list => 'work'} )
+
+Returns true if the object is in the group represented by hashref $other
+=cut
+sub _is_in_group {
+    my ($self, $other) = @_;
+    my $current = {$self->_grouping_clause};
+    return 0 unless (ref $other eq 'HASH') and (keys %$current == keys %$other);
+    for my $key (keys %$current) {
+        return 0 unless exists $other->{$key};
+        return 0 if $current->{$key} ne $other->{$key};
+    }
+    return 1;
+}
+
+
 1;
 __END__
 
index b1d484c..7bc1bed 100644 (file)
@@ -6,9 +6,11 @@ use Test::More;
 use lib qw(t/lib);
 use DBICTest;
 
+use POSIX qw(ceil);
+
 my $schema = DBICTest->init_schema();
 
-plan tests => 321;
+plan tests => 879;
 
 my $employees = $schema->resultset('Employee');
 $employees->delete();
@@ -23,20 +25,168 @@ hammer_rs( $employees );
 
 DBICTest::Employee->grouping_column('group_id');
 $employees->delete();
-foreach my $group_id (1..3) {
+foreach my $group_id (1..4) {
     foreach (1..6) {
         $employees->create({ name=>'temp', group_id=>$group_id });
     }
 }
 $employees = $employees->search(undef,{order_by=>'group_id,position'});
 
-foreach my $group_id (1..3) {
+foreach my $group_id (1..4) {
     my $group_employees = $employees->search({group_id=>$group_id});
     $group_employees->all();
     ok( check_rs($group_employees), "group intial positions" );
     hammer_rs( $group_employees );
 }
 
+my $group_3 = $employees->search({group_id=>3});
+my $to_group = 1;
+my $to_pos = undef;
+while (my $employee = $group_3->next) {
+       $employee->move_to_group($to_group, $to_pos);
+       $to_pos++;
+       $to_group = $to_group==1 ? 2 : 1;
+}
+foreach my $group_id (1..4) {
+    my $group_employees = $employees->search({group_id=>$group_id});
+    $group_employees->all();
+    ok( check_rs($group_employees), "group positions after move_to_group" );
+}
+
+my $employee = $employees->search({group_id=>4})->first;
+$employee->position(2);
+$employee->update;
+ok( check_rs($employees->search_rs({group_id=>4})), "overloaded update 1" );
+$employee = $employees->search({group_id=>4})->first;
+$employee->update({position=>3});
+ok( check_rs($employees->search_rs({group_id=>4})), "overloaded update 2" );
+$employee = $employees->search({group_id=>4})->first;
+$employee->group_id(1);
+$employee->update;
+ok(
+       check_rs($employees->search_rs({group_id=>1})) && check_rs($employees->search_rs({group_id=>4})),
+       "overloaded update 3"
+);
+$employee = $employees->search({group_id=>4})->first;
+$employee->update({group_id=>2});
+ok(
+       check_rs($employees->search_rs({group_id=>2})) && check_rs($employees->search_rs({group_id=>4})),
+       "overloaded update 4"
+);
+$employee = $employees->search({group_id=>4})->first;
+$employee->group_id(1);
+$employee->position(3);
+$employee->update;
+ok(
+       check_rs($employees->search_rs({group_id=>1})) && check_rs($employees->search_rs({group_id=>4})),
+       "overloaded update 5"
+);
+$employee = $employees->search({group_id=>4})->first;
+$employee->group_id(2);
+$employee->position(undef);
+$employee->update;
+ok(
+       check_rs($employees->search_rs({group_id=>2})) && check_rs($employees->search_rs({group_id=>4})),
+       "overloaded update 6"
+);
+$employee = $employees->search({group_id=>4})->first;
+$employee->update({group_id=>1,position=>undef});
+ok(
+       check_rs($employees->search_rs({group_id=>1})) && check_rs($employees->search_rs({group_id=>4})),
+       "overloaded update 7"
+);
+
+# multicol tests begin here
+DBICTest::Employee->grouping_column(['group_id', 'group_id_2']);
+$employees->delete();
+foreach my $group_id (1..4) {
+    foreach my $group_id_2 (1..4) {
+        foreach (1..4) {
+            $employees->create({ name=>'temp', group_id=>$group_id, group_id_2=>$group_id_2 });
+        }
+    }
+}
+$employees = $employees->search(undef,{order_by=>'group_id,group_id_2,position'});
+
+foreach my $group_id (1..3) {
+    foreach my $group_id_2 (1..3) {
+        my $group_employees = $employees->search({group_id=>$group_id, group_id_2=>$group_id_2});
+        $group_employees->all();
+        ok( check_rs($group_employees), "group intial positions" );
+        hammer_rs( $group_employees );
+    }
+}
+
+# move_to_group, specifying group by hash
+my $group_4 = $employees->search({group_id=>4});
+$to_group = 1;
+my $to_group_2_base = 7;
+my $to_group_2 = 1;
+$to_pos = undef;
+while (my $employee = $group_4->next) {
+       $employee->move_to_group({group_id=>$to_group, group_id_2=>$to_group_2}, $to_pos);
+       $to_pos++;
+    $to_group = ($to_group % 3) + 1;
+    $to_group_2_base++;
+    $to_group_2 = (ceil($to_group_2_base/3.0) %3) +1
+}
+foreach my $group_id (1..4) {
+    foreach my $group_id_2 (1..4) {
+        my $group_employees = $employees->search({group_id=>$group_id,group_id_2=>$group_id_2});
+        $group_employees->all();
+        ok( check_rs($group_employees), "group positions after move_to_group" );
+    }
+}
+
+$employees->delete();
+foreach my $group_id (1..4) {
+    foreach my $group_id_2 (1..4) {
+        foreach (1..4) {
+            $employees->create({ name=>'temp', group_id=>$group_id, group_id_2=>$group_id_2 });
+        }
+    }
+}
+$employees = $employees->search(undef,{order_by=>'group_id,group_id_2,position'});
+
+$employee = $employees->search({group_id=>4, group_id_2=>1})->first;
+$employee->group_id(1);
+$employee->update;
+ok( 
+    check_rs($employees->search_rs({group_id=>4, group_id_2=>1}))
+    && check_rs($employees->search_rs({group_id=>1, group_id_2=>1})), 
+    "overloaded multicol update 1" 
+);
+
+$employee = $employees->search({group_id=>4, group_id_2=>1})->first;
+$employee->update({group_id=>2});
+ok( check_rs($employees->search_rs({group_id=>4, group_id_2=>1}))
+    && check_rs($employees->search_rs({group_id=>2, group_id_2=>1})), 
+    "overloaded multicol update 2" 
+);
+
+$employee = $employees->search({group_id=>3, group_id_2=>1})->first;
+$employee->group_id(1);
+$employee->group_id_2(3);
+$employee->update();
+ok( check_rs($employees->search_rs({group_id=>3, group_id_2=>1}))
+    && check_rs($employees->search_rs({group_id=>1, group_id_2=>3})),
+    "overloaded multicol update 3" 
+);
+
+$employee = $employees->search({group_id=>3, group_id_2=>1})->first;
+$employee->update({group_id=>2, group_id_2=>3});
+ok( check_rs($employees->search_rs({group_id=>3, group_id_2=>1}))
+    && check_rs($employees->search_rs({group_id=>2, group_id_2=>3})), 
+    "overloaded multicol update 4" 
+);
+
+$employee = $employees->search({group_id=>3, group_id_2=>2})->first;
+$employee->update({group_id=>2, group_id_2=>4, position=>2});
+ok( check_rs($employees->search_rs({group_id=>3, group_id_2=>2}))
+    && check_rs($employees->search_rs({group_id=>2, group_id_2=>4})), 
+    "overloaded multicol update 5" 
+);
+
 sub hammer_rs {
     my $rs = shift;
     my $employee;
index 78b3d16..7beb833 100644 (file)
@@ -19,6 +19,10 @@ __PACKAGE__->add_columns(
         data_type => 'integer',
         is_nullable => 1,
     },
+    group_id_2 => {
+        data_type => 'integer',
+        is_nullable => 1,
+    },
     name => {
         data_type => 'varchar',
         size      => 100,
index 2ce5dad..a5f4084 100644 (file)
@@ -11,6 +11,7 @@ CREATE TABLE employee (
   employee_id INTEGER PRIMARY KEY NOT NULL,
   position integer NOT NULL,
   group_id integer,
+  group_id_2 integer,  
   name varchar(100)
 );