I'm having a problem with saved_changes ActiveRecord method returning blank values when the change was triggered by a child updated from nested attributes.
Given these two models:
class Customer < ApplicationRecord
enum status: {inactive: 0, active: 1, paused: 2}, _prefix: :status
has_many :customer_pauses
has_many :contracts
after_save :handle_status_change
protected
# UNEXPECTED BEHAVIOR HAPPENS HERE!!!
def handle_status_change
puts "CHANGED STATUS??? #{saved_change_to_status?}"
puts saved_changes
puts "--------------------"
if saved_change_to_status?
contracts.where(status: saved_change_to_status.first).update status: status
end
end
end
class CustomerPause < ApplicationRecord
enum status: {disabled: 0, scheduled: 1, ongoing: 2, finished: 3}, _prefix: :status
belongs_to :customer
after_save :update_customer_status
protected
#update customer status to match the status of the pause accordingly
def update_customer_status
if (saved_change_to_status? || new_record?)
if status_ongoing? && customer.status_active?
customer.update status: Customer::STATUS_PAUSED
end
end
end
end
These differences in behavior happen:
customer = Customer.create status: :active
pause = customer.customer_pauses.create status: :disabled
pause.status = :ongoing
#this works as expected, and customer.saved_changes will contain the status change
pause.save
#This however will result in saved_changes to be empty on the customer callback "handle_status_change"
#Note that the customer status is still changed but not listed in saved_changes
customer.update customer_pauses_attributes: [pause.attributes]
I'm not sure why this happens, is this the expected behavior or a bug in rails?
Should a callback on a nested child not cause changes to its parent?
Related
Running into some performance issues with the following code (stripped out irrelevant parts).
This is the CardsController#index code:
def index
cards = cards.paginate(page: index_params[:page], per_page: limit)
# Assign bumped attribute
cards.each do |card|
if current_user
card.bumped = card.bump_by?(current_user)
card.bump = card.get_bump(current_user)
else
card.bumped = false
card.bump = nil
end
end
end
Card.rb:
class Card < ActiveRecord::Base
belongs_to :cardable, polymorphic: true, touch: true
belongs_to :user
has_many :card_comments, autosave: true
has_many :card_bumps
has_many :card_bumpers, through: :card_bumps, class_name: 'User', source: :user
def bump_by?(user)
self.card_bumpers.include? user
end
def get_bump(user)
CardBump.find_by(user_id: user.id, card_id: self.id)
end
end
How can I avoid and optimize the second loop on each card where I do the associations of card.bumped and card.bump ?
Thanks in advance
In the model level, since the method bump_by? equals to bump existence, so
card.bumped = !card.bump.empty?
So the whole includes check in method bump_by? can be avoided, which in turn avoid fetching all associated bumps.
First of all, I would optimize your controller code a little and would update all cards with a single query if current_user is not present:
def index
cards = cards.paginate(page: index_params[:page], per_page: limit)
# Assign bumped attribute
if current_user
cards.each do |card|
card.bump = card.get_bump(current_user)
card.bumped = card.bump_by?(current_user)
end
else
cards.update_all(bump: nil, bumped: false)
end
end
There is also a possibility to optimize model code:
class Card < ActiveRecord::Base
belongs_to :cardable, polymorphic: true, touch: true
belongs_to :user
has_many :card_comments, autosave: true
has_many :card_bumps
has_many :card_bumpers, through: :card_bumps, class_name: 'User', source: :user
def bump_by?(user)
# use #exists? to check whether a record is present in the database.
# This will make a `SELECT 1 as count...` query and,
# therefore, perform a lookup on database level.
# #include? in opposite will load ALL associated items from DB,
# turn them into a ruby objects array and perform a lookup in the
# obtained array which is much slower than simple lookup performed by #exists?
get_bump(user) == user || self.card_bumpers.exists?(user.id)
end
def get_bump(user)
#_bump ||= self.card_bumpers.find_by(user_id: user.id)
end
end
Since Card#get_bump is also looking in card_bumpers association we can memorize its result and later use memorized value in Card#bump_by? without hitting database again. If there is no memorized value then fast check for record existence will be performed by a database.
Notice, that I changed lines order in controller to get benefit of memorizing:
card.bump = card.get_bump(current_user)
card.bumped = card.bump_by?(current_user)
How can I get the data from an associated form and insert it to the associated table from the main model?
class Supplier < ActiveRecord::Base
has_one :account, foreign_key: "acc_sup_id", :autosave => true
self.primary_key = 'sup_id'
end
class Account < ActiveRecord::Base
belongs_to :supplier, foreign_key: "acc_sup_id"
self.primary_key = 'acc_id'
self.table_name = 'accounts'
end
I am having a combined form for Supplier and Account. When I submit I need to find a way to insert the corresponding values to Supplier and Account. The problem is Supplier values is inserting properly but not Account.
I have asked the same question in several forums, groups and even in stack but nobody seems to give a convincing answer.
The basic strategy is to first look at what params are being submitted when the form is submitted. You could add a line in the controller action such as raise params.inspect to see that. Make sure that those paras contain all the data you need; if not then there is some problem in the view that generates that form.
Once you have all the data getting to the controller action, then you need to change the controller action so that is properly interprets all the data and puts it into the correct models.
I cannot give any more specific advice unless you show the code for your view, the result from doing params.inspect, and the code for the controller action that takes the data.
Try this.
Let's assume that there are orders and customers tables and that you want to perform CRUD operations on customers from orders form.
Customer model is very simple
class Customer < ActiveRecord::Base
attr_accessible :name
end
Order model must provide virtual attributes for all customer's attributes (attr_accessor construct). CRUD for customers is provided through callbacks. Validations can be used as well.
class Order < ActiveRecord::Base
attr_accessor :customer_name
attr_accessible :description, :number, :customer_name
belongs_to :customer
validates_presence_of :number
validates_presence_of :description
validates_presence_of :customer_name
before_save :save_customer
after_find :find_customer
after_destroy :destroy_customer
protected
def save_customer
if self.customer
self.customer.name = self.customer_name
else
self.customer = Customer.create(name: self.customer_name)
end
self.customer.save
end
def find_customer
self.customer_name = self.customer.name
end
def destroy_customer
self.customer.destroy
end
end
Example grid for Order model.
class Orders < Netzke::Basepack::Grid
def configure(c)
super
c.model = 'Order'
c.items = [
:description,
:number,
:customer_name
]
c.enable_edit_inline = false
c.enable_add_inline = false
end
def preconfigure_record_window(c)
super
c.form_config.klass = OrderForm
end
end
Example form for Order model.
class OrderForm< Netzke::Basepack::Form
def configure(c)
super
c.model = 'Order'
c.items = [
:description,
:number,
:customer_name
]
end
end
Here's my test:
require 'spec_helper'
describe League do
it 'should default weekly to false' do
league = Factory.create(:league, :weekly => nil)
league.weekly.should == false
end
end
end
And here's my model:
class League < ActiveRecord::Base
validates :weekly, :inclusion => { :in => [true, false] }
before_create :default_values
protected
def default_values
self.weekly ||= false
end
end
When I run my test, I get the following error message:
Failure/Error: league = Factory.create(:league, :weekly => nil)
ActiveRecord::RecordInvalid:
Validation failed: Weekly is not included in the list
I've tried a couple different approaches to trying to create a league record and trigger the callback, but I haven't had any luck. Is there something that I am missing about testing callbacks using RSpec?
I believe that what you are saying is, before create, set weekly to false, then create actually sets weekly to nil, overwriting the false.
Just do
require 'spec_helper'
describe League do
it 'should default weekly to false' do
league = Factory.create(:league) # <= this line changed
league.weekly.should == false
end
end
end
in your test. No need to explicitly set nil.
Errors are added to error object of record but associations are still saved.
class Parent < ActiveRecord::Base
validate :valid_child?
#validation methods
protected
def valid_child?
#child_names = Hash.new
self.children.each do |curr_child|
if #child_names[curr_child.name].nil?
#child_names[curr_child.name] = curr_child.name
else
errors.add(:base, "child name should be unique for children associated to the parent")
end
end
end
#associations
has_and_belongs_to_many :children, :join_table => 'map__parents__children'
end
#query on rails console
#parent = Parent.find(1)
#parent.children_ids = [1, 2]
#parent.save
The problem is that, for an existing record, #parent.children_ids = [1, 2] will take effect a change in the database before the call to #parent.save.
Try using validates_associated to validate the children rather than rolling your own validation.
To make sure that the children's names are unique within the context of the parent, use validates_uniqueness_of with the :scope option to scope the uniqueness to the parent id. Something like:
class Child < ActiveRecord::Base
belongs_to :parent
validates_uniqueness_of :name, :scope => :parent
end
I have a tiny logical error in my code somewhere and I can't figure out exactly what the problem is. Let's start from the beginning. I have the following extension that my order class uses.
class ActiveRecord::Base
def self.has_statuses(*status_names)
validates :status,
:presence => true,
:inclusion => { :in => status_names}
status_names.each do |status_name|
scope "all_#{status_name}", where(status: status_name)
end
status_names.each do |status_name|
define_method "#{status_name}?" do
status == status_name
end
end
end
end
This works great for the queries and initial setting of "statuses".
require "#{Rails.root}/lib/active_record_extensions"
class Order < ActiveRecord::Base
has_statuses :created, :in_progress, :approved, :rejected, :shipped
after_initialize :init
attr_accessible :store_id, :user_id, :order_reference, :sales_person
private
def init
if new_record?
self.status = :created
end
end
end
Now I set a status initially and that works great. No problems at all and I can save my new order as expected. Updating the order on the other hand is not working. I get a message saying:
"Status is not included in the list"
When I check it seems that order.status == 'created' and it's trying to match against :created. I tried setting the has_statuses 'created', 'in_progress' etc but couldn't get some of the other things to work.
Anyway to automatically map between string/attribute?
from your description, looks like you're comparing a string to a symbol. Probably need to add:
define_method "#{status_name}=" do
self.status = status_name.to_sym
end
or do a #to_s on the status_names