Reputation: 66
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
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
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
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
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