devnoel
devnoel

Reputation: 66

ActiveRecord (Rails 2.3.8) - Update existing, add new record when updating nested attributes

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

Upvotes: 0

Views: 1907

Answers (4)

edgerunner
edgerunner

Reputation: 14983

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.

Upvotes: 0

Jaime Bellmyer
Jaime Bellmyer

Reputation: 23317

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.

Upvotes: 1

devnoel
devnoel

Reputation: 66

Got it working. While it's probably not the best implementation, all my tests are passing. Thanks for the input guys.


  before_save :soft_delete_changed_membership

  def soft_delete_changed_membership
    if !membership.nil? then
      if !membership.new_record? && membership.trial_expire_at_changed? then
        Membership.update_all( "active = 0", [ "id = ?", self.membership.id ] )
        trial_expire_at = self.membership.trial_expire_at
        self.membership = nil
        Membership.create!( 
          :user_id => self.id, 
          :trial_expire_at => trial_expire_at, 
          :active => true 
        )
        self.reload
      end
    end
  end

Upvotes: 0

Michiel de Mare
Michiel de Mare

Reputation: 42460

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.

Upvotes: 0

Related Questions