views:

65

answers:

4

I have a "user" model that "has_one" "membership" (active at a time). For auditing and data integrity reasons, I'd like it so that if the membership changes for a user, the old/current record (if existing) has an inactive/active flag swapped, and a new row is added for the new changed record. If there are no changes to the membership, I'd like to just ignore the update. I've tried implementing this with a "before_save" call-back on my user model, but have failed many times. Any help is greatly appreciated.

models:

class User < ActiveRecord::Base
  has_one :membership, :dependent => :destroy
  accepts_nested_attributes_for :membership, :allow_destroy => true  
end

class Membership < ActiveRecord::Base
  default_scope :conditions => {:active => 1}
  belongs_to :user
end
A: 

Did you look at acts_as_versioned? In the before_save of the Membership you could create a new version of the User, which would be acts_as_versioned.

Michiel de Mare
A: 

Why don't you just assume that the latest membership is the active one. This would save you a lot of headache.

class User < ActiveRecord::Base
  has_many :memberships, :dependent => :destroy
end

class Membership < ActiveRecord::Base
  nested_scope :active, :order => "created_at DESC", :limit => 1
  belongs_to :user

  def update(attributes)
    self.class.create attributes if changed?
  end
end

then you can use

@user.memberships.active

to get the active membership, and you can just update any membership to get a new membership, which will become the active membership because it is the latest.

edgerunner
Thanks Edgerunner, but wouldn't that create a new record for every update to the user model if updated through a nested form where the membership fields were referenced (fields_for)?
That's correct, but simple to fix. Just add `if changed?`. Updating answer now.
edgerunner
Edgerunner, I couldn't get it to work with arguments being passed to update, kept getting errors as if the method didn't support the signature being used (1 for 0). I did however get it working with the following: def update if changed? then self.class.update_all( 'deleted_at = now()', [ "id = ?", self.id ] ) self.class.create( self.attributes ) end end
`self.class.update_all` sounds dangerous. It probably will not respect associations
edgerunner
Can you tell me which method throws that error?
edgerunner
A: 
+1  A: 

I have what I think is a pretty elegant solution. Here's your user model:

class User < ActiveRecord::Base
  has_one :membership, :dependent => :destroy
  accepts_nested_attributes_for :membership

  def update_membership_with_history attributes
    self.membership.attributes = attributes
    return true unless self.membership.changed?

    self.membership.update_attribute(:active, false)
    self.build_membership attributes

    self.membership.save
  end
end

This update_membership_with_history method allows us to handle changed or unchanged records. Next the membership model:

class Membership < ActiveRecord::Base
  default_scope :conditions => {:active => true}
  belongs_to :user
end

I changed this slightly, since active should be a boolean, not 1's and 0's. Update your migration to match. Now the update action, which is the only part of your scaffold that needs to change:

  def update
    @user = User.find(params[:id], :include => :membership)
    membership_attributes = params[:user].delete(:membership_attributes)

    if @user.update_attributes(params[:user]) && @user.update_membership_with_history(membership_attributes)
      redirect_to users_path
    else
      render :action => :edit
    end
  end

We're simply parsing out the membership attributes (so you can still use fields_for in your view) and updating them separately, and only if needed.

Jaime Bellmyer