Acts_as_list with scope : position update problem?


#1

Hello,
I’ve tried to set up a class with acts_as_list with a scope argument
that restricts a list to records with the same foreign key.

For example :

database :

CREATE TABLE families (
id int(11) NOT NULL auto_increment,
name varchar(255) NOT NULL default ‘’,
PRIMARY KEY (id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ;

INSERT INTO families VALUES (1, ‘Smith’);
INSERT INTO families VALUES (2, ‘Jones’);

CREATE TABLE people (
id int(11) NOT NULL auto_increment,
name varchar(255) NOT NULL default ‘’,
family_id int(11) default NULL,
position int(5) NOT NULL default ‘0’,
PRIMARY KEY (id)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=6 ;

INSERT INTO people VALUES (1, ‘Robert’, 1, 1);
INSERT INTO people VALUES (2, ‘Brian’, 1, 2);
INSERT INTO people VALUES (3, ‘John-Paul’, 2, 1);
INSERT INTO people VALUES (4, ‘Grace’, 2, 2);

class Family < ActiveRecord::Base
has_many :people
end

class Person < ActiveRecord::Base
belongs_to :family, :order => ‘position’
acts_as_list :scope => ‘family_id’
validates_uniqueness_of :position, :scope => ‘family_id’
end

Now, all this works fine, I can execute commands like
person.move_higher, person.move_to_bottom, etc. So the class behaves as
expected with acts_as_list, updating the position column all by itself :

@person = Person.find_by_id(2)
=> #<Person:0x239cd64 @attributes={“family_id”=>“1”, “name”=>“Brian”,
“id”=>“2”, “position”=>“2”}>

@person.move_higher
=> true

@person.position
=> 1

Except for one thing : when I change a Person’s scope (thus changing the
value for family_id), nothing gets updated except the scope value. The
position column doesn’t change :

@person.family_id = 2
=> 2

@person
=> #<Person:0x239cd64 @attributes={“family_id”=>2, “name”=>“Brian”,
“id”=>“2”, “position”=>1}>

I would expect the “acts_as_list with scope” behavior to ;
a) update all the positions of the objects in the original scope to mend
the gap left by the departure of the updated record moved to another
scope,
b) the moved object to be added to the bottom of the list corresponding
to its new family_id scope.

This appears not to be the case.
The validation method raises an error if the position of the object to
update already exists in the new scope (though not in console).

This is what I have tried, although not working. I somehow can’t access
params() from the callback method, and I can’t figure out what’s wrong.

class Person < ActiveRecord::Base
belongs_to :family, :order => ‘position’
acts_as_list :scope => ‘family_id’
validates_uniqueness_of :position, :scope => ‘family_id’
before_validation_on_update :reorder_positions

private

def reorder_positions
	@updated_family_id = params[:person][:family_id]
	unless @updated_family_id == self.family_id
		self.move_to_bottom # reorder original list
		params[:person][:family_id] = 

Family.find_by_id(@updated_family_id).people.length + 1
end
end
end

When I update a Person by changing its family_id, I get this error
message :

“undefined local variable or method `params’ for #Person:0x22e5060

Can someone help me figure it all out?
Am I making this more complicated than it really is?

Thanks in advance !

Bernard.


#2

Actually, I found a workaround by myself, including the position
reordering directly inside the people controller update method like so :

def update
@person = Person.find(params[:id])
unless params[:person][:family_id] == @person.family_id
@person.move_to_bottom
params[:person][:position] =
Family.find(params[:person][:family_id]).people.length + 1
end
if @person.update_attributes(params[:person])
flash[:notice] = ‘Person was successfully updated.’
redirect_to :action => ‘list’
else
render :action => ‘edit’
end
end

It works fine… but I’m concerned about breaking the MVC model, since
such a kind of rectification should belong to the model, not the
controller, shouldn’t it? Although, I still can’t access the params hash
from a callback method in the model. Still that error :

“undefined local variable or method `params’ for #Person:0x22e5060

Bernard.


#3

Hello Bernard,

I faced the same problem.
I found this solution to solve the problem in my Model :

1/ in my controller, I update the Person object with :
@person.update_attributes(params[:person])

2/ so in my Person model, I have overwritten the update_attributes
method :
def update_attributes(attributes)
# reorder the positions if the category has changed
reorder_positions(attributes[:category_id]) unless
attributes[:category_id].to_s==self.category_id.to_s
super(attributes)
end

3/ the “reorder_positions” method in my Person model :
private
def reorder_positions(new_category_id)
# reorder source list
self.move_to_bottom

# find the position in the destination list
new_position = Category.find(new_category_id, :include =>

“contents”).contents.length + 1
end

Hope this helps.
Thomas B…


#4

Thanks Thomas,

That was indeed a much cleaner solution.
For the record though it didn’t work for me until I added a line to the
rewritten update_attributes method in the model, like so :

def update_attributes(attributes)
# reorder the positions if the category has changed
unless attributes[:category_id].to_s==self.category_id.to_s
reorder_positions(attributes[:category_id])
#this is needed to get rid of the old params[:position]
attributes.delete(“position”)
end
super(attributes)
end

Otherwise, the old position stuck in the params hash was still in the
way upon updating.

Cheers
Bernard.

Thomas B. wrote:

Hello Bernard,

I faced the same problem.
I found this solution to solve the problem in my Model :

1/ in my controller, I update the Person object with :
@person.update_attributes(params[:person])

2/ so in my Person model, I have overwritten the update_attributes
method :
def update_attributes(attributes)
# reorder the positions if the category has changed
reorder_positions(attributes[:category_id]) unless
attributes[:category_id].to_s==self.category_id.to_s
super(attributes)
end

3/ the “reorder_positions” method in my Person model :
private
def reorder_positions(new_category_id)
# reorder source list
self.move_to_bottom

# find the position in the destination list
new_position = Category.find(new_category_id, :include =>

“contents”).contents.length + 1
end

Hope this helps.
Thomas B…