[Dbix-class] Ordered.pm patch

Neil de Carteret n3dst4 at gmail.com
Sun Nov 12 17:26:38 GMT 2006


The following patch against the trunk allows DBIC::Ordered to group
across more than one column, and updates the position column of all
affected rows when items move between groups. See pod comments for
details :D

It also includes a test file to cover the new bits and some changes to
the test schema to accommodate it (adding a new table).

If these changes to the test schema aren't welcome, let me know and
I'll try and work with what's already there.  Oh yeah, and apologies
in advance if there's anything massively retarded in here, but it
tests okay.

Neil de Carteret


Index: t/lib/sqlite.sql
===================================================================
--- t/lib/sqlite.sql	(revision 2873)
+++ t/lib/sqlite.sql	(working copy)
@@ -253,6 +253,16 @@
   title varchar(100)
 );

+--
+-- Table: todos
+--
+CREATE TABLE todos (
+  id INTEGER PRIMARY KEY NOT NULL,
+  position INTEGER,
+  listname varchar( 100 ),
+  username varchar( 100 ),
+  title varchar( 100 )
+);

 CREATE UNIQUE INDEX tktlnameunique_twokeytreelike on twokeytreelike (name);
 CREATE UNIQUE INDEX cd_artist_title_cd on cd (artist, title);
Index: t/lib/DBICTest/Schema/Todo.pm
===================================================================
--- t/lib/DBICTest/Schema/Todo.pm	(revision 0)
+++ t/lib/DBICTest/Schema/Todo.pm	(revision 0)
@@ -0,0 +1,14 @@
+package DBICTest::Schema::Todo;

+

+use base qw/DBIx::Class/;

+

+__PACKAGE__->load_components(qw/Ordered Core/);

+__PACKAGE__->table('todos');

+__PACKAGE__->add_columns(qw/id position username listname title/);

+__PACKAGE__->set_primary_key('id');

+

+__PACKAGE__->position_column('position');

+

+

+1;

+


Property changes on: t/lib/DBICTest/Schema/Todo.pm
___________________________________________________________________
Name: svn:executable
   + *

Index: t/lib/DBICTest/Schema.pm
===================================================================
--- t/lib/DBICTest/Schema.pm	(revision 2873)
+++ t/lib/DBICTest/Schema.pm	(working copy)
@@ -14,6 +14,7 @@
   #dummy
   Track
   Tag
+  Todo
   /,
   { 'DBICTest::Schema' => [qw/
     LinerNotes
Index: t/95position.t
===================================================================
--- t/95position.t	(revision 0)
+++ t/95position.t	(revision 0)
@@ -0,0 +1,57 @@
+# vim: filetype=perl
+use strict;
+use warnings;
+
+use Test::More;
+use lib qw(t/lib);
+use DBICTest;
+
+my $schema = DBICTest->init_schema(no_populate => 1);
+
+plan tests => 18;
+
+my $todos = $schema->resultset('Todo');
+$todos->delete();
+
+
+
+DBICTest::Todo->grouping_column(['username', 'listname']);
+$todos->delete();
+foreach my $username (1..3) {
+    foreach my $listname (1..3) {
+        foreach (1..6) {
+            $todos->create({ title=>'temp', username=>$username,
listname=>$listname });
+        }
+    }
+}
+foreach my $username (1..3) {
+    foreach my $listname (1..3) {
+        my $len = $todos->search({username=>$username,
listname=>$listname})->count();
+        my $position = int(rand($len))+1;
+        my $to_user = int(rand(3))+1;
+        my $to_list = int(rand(3))+1;
+
+        my $victim = $todos->search({username=>$username,
listname=>$listname, position=>$position})->first;
+        $victim->update({username=>$to_user, listname=>$to_list});
+
+        my $old_list = $todos->search({username=>$username,
listname=>$listname}, {order_by=>'position'});
+        ok( check_rs($old_list), "positions of old list after moving items" );
+        my $new_list = $todos->search({username=>$to_user,
listname=>$to_list}, {order_by=>'position'});
+        ok( check_rs($new_list), "positions of new_list after moving items" );
+
+    }
+}
+
+sub check_rs {
+    my( $rs ) = @_;
+    $rs->reset();
+    my $position_column = $rs->result_class->position_column();
+    my $expected_position = 0;
+    while (my $row = $rs->next()) {
+        $expected_position ++;
+        if ($row->get_column($position_column)!=$expected_position) {
+            return 0;
+        }
+    }
+    return 1;
+}

Property changes on: t/95position.t
___________________________________________________________________
Name: svn:executable
   + *

Index: lib/DBIx/Class/Ordered.pm
===================================================================
--- lib/DBIx/Class/Ordered.pm	(revision 2873)
+++ lib/DBIx/Class/Ordered.pm	(working copy)
@@ -6,7 +6,7 @@

 =head1 NAME

-DBIx::Class::Ordered - Modify the position of objects in an ordered list.
+DBIx::Class::Ordered - Modify the positions of objects in ordered lists.

 =head1 SYNOPSIS

@@ -29,8 +29,15 @@

   package My::Item;
   __PACKAGE__->position_column('position');
-  __PACKAGE__->grouping_column('group_id'); # optional

+Optional: specify one or more columns whose values will be used to
group the row, effectively creating multiple indepenent lists within
one table:
+
+  __PACKAGE__->grouping_column('group_id');
+
+If you want grouping based on more than one column:
+
+  __PACKAGE__->grouping_column(['group_id', 'other_group_id', ... ]);
+
 Thats it, now you can change the position of your objects.

   #!/use/bin/perl
@@ -48,6 +55,8 @@
   $sibling = $item->last_sibling();
   $sibling = $item->previous_sibling();
   $sibling = $item->next_sibling();
+
+  my $rs = $item->siblings();

   $item->move_previous();
   $item->move_next();
@@ -55,6 +64,37 @@
   $item->move_last();
   $item->move_to( $position );

+Furthermore, if you change the values in any of the columns used for
grouping, the position column will be updated to match.
+
+  #!/use/bin/perl
+  use My::Item;
+
+  my $item1 = My::Item->create({ name=>'Matt S. Trout', group_id=>1 });
+  my $item2 = My::Item->create({ name=>'Neil de Carteret', group_id=>1 });
+  my $item3 = My::Item->create({ name=>'Doctor Zoidberg', group_id=>1 });
+  my $item4 = My::Item->create({ name=>'Philip J. Fry', group_id=>2 });
+  my $item5 = My::Item->create({ name=>'Turanga Leela', group_id=>2 });
+
+  # item1, item2 and item3 have positions 1, 2 and 3 respectively;
+  # item4 and item5 are in a seperate group and so have position 1
and 2 respectively.
+  #
+  #    GROUP 1               GROUP 2
+  # 1: Matt S. Trout         Philip J. Fry
+  # 2: Neil de Carteret      Turanga Leela
+  # 3: Doctor Zoidberg
+  #
+  # Then, If we do this:
+
+  $item2->update({group_id=>2});
+
+  # Now I've moved into Fry and Leela's group. I get a new position
of 3, to put me at the
+  # end of the new group. And Dr. Zoidberg has changed to position 2
to fill the gap I occupied.
+  #
+  #    GROUP 1               GROUP 2
+  # 1: Matt S. Trout         Philip J. Fry
+  # 2: Doctor Zoidberg       Turanga Leela
+  #                          Neil de Carteret
+
 =head1 DESCRIPTION

 This module provides a simple interface for modifying the ordered
@@ -324,6 +364,90 @@
     return $self->next::method( @_ );
 }

+=head2 store_column
+
+Overrides DBIC::Row::store_column. If the colum being updated is used for
+grouping, we hang on to the old values for when update() saves to to
the database.
+
+=cut
+
+sub store_column {
+    my ( $self, $name, $value ) = @_;
+
+    if ($self->grouping_column()) {
+        my @groups = (ref $self->grouping_column() eq 'ARRAY')?
+                @{$self->grouping_column()}:
+                ($self->grouping_column());
+        my $pos    = $self->position_column();
+
+        # if the column being stored is one of our grouping columns
+        if (   grep {$_ eq $name} @groups) {
+
+            unless ($self->{old_group}{$name} ||
+                    (defined $self->get_column($name) &&
$self->get_column($name) eq $value)) {
+                $self->{old_group}{$name} = $self->get_column($name);
+            }
+        }
+    }
+    $self->next::method($name, $value);
+}
+
+=head2 update
+
+Overrides DBIx::Class::RoW::update.
+If any of the columns being saved is used for grouping, the position column
+will be updated to put the item at the end of its new host list, and the old
+host list will be shuffled up to fill the gap.
+
+=cut
+
+sub update {
+    my ($self, $upd) = @_;
+
+    if ($self->grouping_column() && $self->in_storage) {
+        my @groups = (ref $self->grouping_column() eq 'ARRAY')?
+                @{$self->grouping_column()}:
+                ($self->grouping_column());
+
+        my $pos_col    = $self->position_column();
+
+        # set our grouping column values if they're in $upd
+        # do this rather than set_columns($upd) to avoid problems
with inflation, maybe
+        if ($upd) {
+            for my $col (@groups) {
+                if ($upd->{$col}) {
+                    $self->set_column($col, $upd->{$col});
+                    delete $upd->{$col};
+                }
+            }
+        }
+
+        # if any of the grouping columns have changed
+        if (  grep {$_} map {$self->is_column_changed($_)} @groups  ) {
+
+            # fill in the blanks in %old_group
+            for my $group (@groups) {
+                $self->{old_group}{$group} =
$self->{old_group}{$group} || $self->get_column($group);
+            }
+
+            # fix up the old group
+            my $rs = $self->result_source->resultset->search({
+                $pos_col => { '>' => $self->get_column($pos_col) },
+                %{$self->{old_group}},
+            });
+            $rs->update({ $pos_col => \"$pos_col - 1" });
+
+            # set our new position
+            my $count =
$self->result_source->resultset->search({$self->_grouping_clause()})->count();
+            $self->set_column($pos_col, $count+1);
+
+        }
+    }
+
+    $self->next::method($upd);
+}
+
+
 =head1 PRIVATE METHODS

 These methods are used internally.  You should never have the
@@ -331,8 +455,8 @@

 =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 return name=>value pairs for limiting a search
+by the grouping columns.  If the grouping columns are not
 defined then this will return an empty list.

 =cut
@@ -340,12 +464,16 @@
 sub _grouping_clause {
     my( $self ) = @_;
     my $col = $self->grouping_column();
-    if ($col) {
+    if (ref $col eq 'ARRAY') {
+        my @clause = map {  $_ => $self->get_column($_)  } @$col;
+        return (@clause);
+    } elsif ($col) {
         return ( $col => $self->get_column($col) );
     }
     return ();
 }

+
 1;
 __END__



More information about the Dbix-class mailing list