From: Matt S Trout Date: Tue, 2 Jan 2007 20:36:29 +0000 (+0000) Subject: Merge 'ordered_handle_updates' into 'DBIx-Class-current' X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=77e7e47d3279ed047c1c72a9587a9ac73797c251;hp=664a70fad7d9aaf7d38579cdafe934663ee4a9c3;p=dbsrgits%2FDBIx-Class-Historic.git Merge 'ordered_handle_updates' into 'DBIx-Class-current' multi-col group support for Ordered, from Neil de Carteret --- diff --git a/Changes b/Changes index 31cb403..26aa117 100644 --- 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 diff --git a/lib/DBIx/Class.pm b/lib/DBIx/Class.pm index 6d75377..91f6677 100644 --- a/lib/DBIx/Class.pm +++ b/lib/DBIx/Class.pm @@ -217,6 +217,8 @@ konobi: Scott McWhirter LTJake: Brian Cassidy +ned: Neil de Carteret + nigel: Nigel Metheringham ningu: David Kamholz diff --git a/lib/DBIx/Class/Ordered.pm b/lib/DBIx/Class/Ordered.pm index 8e2c74d..d5a7a00 100644 --- a/lib/DBIx/Class/Ordered.pm +++ b/lib/DBIx/Class/Ordered.pm @@ -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__ diff --git a/t/87ordered.t b/t/87ordered.t index b1d484c..7bc1bed 100644 --- a/t/87ordered.t +++ b/t/87ordered.t @@ -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; diff --git a/t/lib/DBICTest/Schema/Employee.pm b/t/lib/DBICTest/Schema/Employee.pm index 78b3d16..7beb833 100644 --- a/t/lib/DBICTest/Schema/Employee.pm +++ b/t/lib/DBICTest/Schema/Employee.pm @@ -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, diff --git a/t/lib/sqlite.sql b/t/lib/sqlite.sql index 2ce5dad..a5f4084 100644 --- a/t/lib/sqlite.sql +++ b/t/lib/sqlite.sql @@ -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) );