Very basic tests for DBIC:Positioned.
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Positioned.pm
CommitLineData
118e6b96 1# vim: ts=8:sw=4:sts=4:et
2package DBIx::Class::Positioned;
3use strict;
4use warnings;
5use base qw( DBIx::Class );
6
7=head1 NAME
8
9DBIx::Class::Positioned - Modify the position of objects in an ordered list.
10
11=head1 SYNOPSIS
12
13Create a table for your positionable data.
14
15 CREATE TABLE employees (
16 employee_id INTEGER PRIMARY KEY AUTOINCREMENT,
17 name TEXT NOT NULL,
18 position INTEGER NOT NULL
19 );
20
21In your Schema or DB class add Positioned to the top
22of the component list.
23
24 __PACKAGE__->load_components(qw( Positioned ... ));
25
26Specify the column that stores the position number for
27each row.
28
29 package My::Employee;
30 __PACKAGE__->position_column('position');
31
32Thats it, now you can change the position of your objects.
33
34 #!/use/bin/perl
35 use My::Employee;
36
37 my $employee = My::Employee->create({ name=>'Matt S. Trout' });
38
39 my $rs = $employee->siblings();
40 my @siblings = $employee->siblings();
41
42 my $sibling;
43 $sibling = $employee->first_sibling();
44 $sibling = $employee->last_sibling();
45 $sibling = $employee->previous_sibling();
46 $sibling = $employee->next_sibling();
47
48 $employee->move_previous();
49 $employee->move_next();
50 $employee->move_first();
51 $employee->move_last();
52 $employee->move_to( $position );
53
54=head1 DESCRIPTION
55
56This module provides a simple interface for modifying the position
57of DBIx::Class objects.
58
59=head1 METHODS
60
61=head2 position_column
62
63 __PACKAGE__->position_column('position');
64
65Sets and retrieves the name of the column that stores the
66positional value of each record. Default to "position".
67
68=cut
69
70__PACKAGE__->mk_classdata( 'position_column' => 'position' );
71
72=head2 siblings
73
74 my $rs = $employee->siblings();
75 my @siblings = $employee->siblings();
76
77Returns either a result set or an array of all other objects
78excluding the one you called it on.
79
80=cut
81
82sub siblings {
83 my( $self ) = @_;
84 my $position_column = $self->position_column;
85 my $rs = $self->search(
86 { $position_column => { '!=' => $self->get_column($position_column) } },
87 { order_by => $self->position_column },
88 );
89 if (wantarray()) { return $rs->all(); }
90 else { return $rs; }
91}
92
93=head2 first_sibling
94
95 my $sibling = $employee->first_sibling();
96
97Returns the first sibling object.
98
99=cut
100
101sub first_sibling {
102 my( $self ) = @_;
103 return ($self->search(
104 {},
105 { rows=>1, order_by => $self->position_column },
106 )->all())[0];
107}
108
109=head2 last_sibling
110
111 my $sibling = $employee->last_sibling();
112
113Return the last sibling.
114
115=cut
116
117sub last_sibling {
118 my( $self ) = @_;
119 return ($self->search(
120 {},
121 { rows=>1, order_by => $self->position_column.' DESC' },
122 )->all())[0];
123}
124
125=head2 previous_sibling
126
127 my $sibling = $employee->previous_sibling();
128
129Returns the sibling that resides one position higher. Undef
130is returned if the current object is the first one.
131
132=cut
133
134sub previous_sibling {
135 my( $self ) = @_;
136 my $position_column = $self->position_column;
137 return ($self->search(
138 { $position_column => { '<' => $self->get_column($position_column) } },
139 { rows=>1, order_by => $position_column.' DESC' },
140 )->all())[0];
141}
142
143=head2 next_sibling
144
145 my $sibling = $employee->next_sibling();
146
147Returns the sibling that resides one position lower. Undef
148is returned if the current object is the last one.
149
150=cut
151
152sub next_sibling {
153 my( $self ) = @_;
154 my $position_column = $self->position_column;
155 return ($self->search(
156 { $position_column => { '>' => $self->get_column($position_column) } },
157 { rows=>1, order_by => $position_column },
158 )->all())[0];
159}
160
161=head2 move_up
162
163 $employee->move_up();
164
165Swaps position with the sibling on position higher. 1 is returned on
166success, and 0 is returned if the objects is already the first one.
167
168=cut
169
170sub move_up {
171 my( $self ) = @_;
172 my $previous = $self->previous_sibling();
173 return undef if (!$previous);
174 my $position_column = $self->position_column;
175 my $self_position = $self->get_column( $position_column );
176 $self->set_column( $position_column, $previous->get_column($position_column) );
177 $previous->set_column( $position_column, $self_position );
178 $self->update();
179 $previous->update();
180 return 1;
181}
182
183=head2 move_down
184
185 $employee->move_down();
186
187Swaps position with the sibling on position lower. 1 is returned on
188success, and 0 is returned if the object is already at the last position.
189
190=cut
191
192sub move_down {
193 my( $self ) = @_;
194 my $next = $self->next_sibling();
195 return undef if (!$next);
196 my $position_column = $self->position_column;
197 my $self_position = $self->get_column( $position_column );
198 $self->set_column( $position_column, $next->get_column($position_column) );
199 $next->set_column( $position_column, $self_position );
200 $self->update();
201 $next->update();
202 return 1;
203}
204
205=head2 move_first
206
207 $employee->move_first();
208
209Moves the object to the first position. 1 is returned on
210success, and 0 is returned if the object is already the first.
211
212=cut
213
214sub move_first {
215 my( $self ) = @_;
216 return $self->move_to( 1 );
217}
218
219=head2 move_last
220
221 $employee->move_last();
222
223Moves the object to the very last position. 1 is returned on
224success, and 0 is returned if the object is already the last one.
225
226=cut
227
228sub move_last {
229 my( $self ) = @_;
230 my $count = $self->search()->count();
231 return $self->move_to( $count );
232}
233
234=head2 move_to
235
236 $employee->move_to( $position );
237
238Moves the object to the specified position. 1 is returned on
239success, and 0 is returned if the object is already at the
240specified position.
241
242=cut
243
244sub move_to {
245 my( $self, $to_position ) = @_;
246 my $position_column = $self->position_column;
247 my $from_position = $self->get_column( $position_column );
248 return undef if ( $from_position==$to_position );
249 my $rs = $self->search({
250 -and => [
251 $position_column => { ($from_position>$to_position?'<':'>') => $from_position },
252 $position_column => { ($from_position>$to_position?'>=':'<=') => $to_position },
253 ]
254 });
255 my $op = ($from_position>$to_position) ? '+' : '-';
256 $rs->update({
257 $position_column => \"$position_column $op 1",
258 });
259 $self->set_column( $position_column => $to_position );
260 $self->update();
261 return 1;
262}
263
264=head2 insert
265
266Overrides the DBIC insert() method by providing a default
267position number. The default will be the number of rows in
268the table +1, thus positioning the new record at the last position.
269
270=cut
271
272sub insert {
273 my $self = shift;
274 my $position_column = $self->position_column;
275 $self->set_column( $position_column => $self->count()+1 )
276 if (!$self->get_column($position_column));
277 $self->next::method( @_ );
278}
279
280=head2 delete
281
282Overrides the DBIC delete() method by first moving the object
283to the last position, then deleting it, thus ensuring the
284integrity of the positions.
285
286=cut
287
288sub delete {
289 my $self = shift;
290 $self->move_last;
291 $self->next::method( @_ );
292}
293
2941;
295__END__
296
297=head1 TODO
298
299Support foreign keys that cause rows to be members of mini
300positionable sets.
301
302=head1 BUGS
303
304If a position is not specified for an insert than a position
305will be chosen based on COUNT(*)+1. But, it first selects the
306count then inserts the record. The space of time between select
307and insert introduces a race condition. To fix this we need the
308ability to lock tables in DBIC. I've added an entry in the TODO
309about this.
310
311=head1 AUTHOR
312
313Aran Deltac <bluefeet@cpan.org>
314
315=head1 LICENSE
316
317You may distribute this code under the same terms as Perl itself.
318