Rails 3.2.1 associations no callbacks? - ruby-on-rails

First off, i'm using Rails 3.2.1 and ruby 1.9.3p392
I have two models, ad_user and device
An ad_user has many devices, and a device belongs to an ad_user.
My models are as follow:
class AdUser < ActiveRecord::Base
has_many :devices
class Device < ActiveRecord::Base
belongs_to :device_type
belongs_to :device_status
belongs_to :ad_user
validates_presence_of :name
validates_uniqueness_of :name
validates_presence_of :serial
validates_uniqueness_of :serial
validates_presence_of :device_type_id
validates_presence_of :device_status_id
validates_presence_of :ad_user_id
before_update :before_update_call
before_save :before_save_call
before_create :before_create_call
before_validation :before_validation_call
protected
def before_update_call
p self.name
p self.ad_user_id
p "-=-=-=-=-=-=-=-=-"
p "before_update_call"
self.ad_user_id = 1 if self.ad_user_id.nil? || self.ad_user_id.blank?
end
def before_save_call
p self.name
p self.ad_user_id
p "-=-=-=-=-=-=-=-=-"
p "before_save_call"
self.ad_user_id = 1 if self.ad_user_id.nil? || self.ad_user_id.blank?
end
def before_create_call
p self.name
p self.ad_user_id
p "-=-=-=-=-=-=-=-=-"
p "before_create_call"
self.ad_user_id = 1 if self.ad_user_id.nil? || self.ad_user_id.blank?
end
def before_validation_call
p self.name
p self.ad_user_id
p "-=-=-=-=-=-=-=-=-"
p "before_validation_call"
self.ad_user_id = 1 if self.ad_user_id.nil? || self.ad_user_id.blank?
end
When i assign devices to a user using
u = AdUser.first
u.device_ids=[1,2]
I can see the before_validation_call, before_save_call and before_update_call printing to the console, however when I unassign these devices from the user with:
u.device_ids=[]
It results in a simple:
SQL (2.0ms) UPDATE "devices" SET "ad_user_id" = NULL WHERE "devices"."ad_user_id" = 405 AND "devices"."id" IN (332, 333)
None of the callbacks are called and my devices end up with having a nil ad_user_id despite the fact that the model should validate the presence. I planned to use the callbacks to check that ad_user_id is not nil before saving or updating but they are not even called.
Am I doing anything wrong here ?

Unfortunately, I think this is an expected behavior. You shouldn't rely on callbacks being fired when you update associations, so underlying model is being updated implicitly.
What you can do though, try to access devices field instead. device_ids performs differently, on a lower level.
Also consider using association-callbacks, if it fits.
P.S. just a small note: in Rails you can use self.ad_user.present? instead of self.ad_user_id.nil? || self.ad_user_id.blank?. Also, you can merge validates_presence_of statements.

Allright then, in my update method of the ad_user controller, before the update_attributes I added:
old_device_ids = #ad_user.device_ids
the after the update_attributes I added:
(old_device_ids - AdUser.find(params[:id]).device_ids).each do |device|
Device.find(device).update_attributes(:ad_user_id => 1)
end
I had already done that earlier, but I wanted to find a proper "Rails way" of doing it.

Related

Active Record callbacks throw "undefined method" error in production with classes using STI

I have many instances in my application where I use single table inheritance and everything works fine in my development environment. But when I release to production (using passenger) I get the following error:
undefined method `before_save' for InventoryOrder:Class
(NoMethodError)
Why would this work in my dev environment and not work in production? Both are using Rails 4.2 and Ruby 2.1.5. Could this be a problem with passenger?
Here is the InventoryOrder class:
class InventoryOrder < Order
def self.model_name
Order.model_name
end
before_save :ensure_only_feed_types
def ensure_only_feed_types
order_products.each do |op|
if !ProductTypes::is_mix?(op.product_type.type)
raise Exceptions::FailedValidations, _("Can't have an inventory order for anything but mixes")
end
end
end
def self.check_if_replenishment_order_is_needed(product_type_id)
prod_type = ProductType.find(product_type_id)
return if prod_type.nil? || prod_type.min_system_should_have_on_hand.nil? || prod_type.min_system_should_have_on_hand == 0
amount_free = Inventory::inventory_free_for_type(product_type_id)
if prod_type.min_system_should_have_on_hand > amount_free
if prod_type.is_mix?
InventoryOrder::create_replenishment_order(product_type_id, prod_type.min_system_should_have_on_hand - amount_free)
else
OrderMoreNotification.create({subject: "Running low on #{prod_type.name}", body: "Should have #{prod_type.min_system_should_have_on_hand} of unreserved #{prod_type.name} but only #{amount_free} is left"})
end
end
end
def self.create_replenishment_order(product_type_id, amount)
# first check for current inventory orders
orders = InventoryOrder.joins(:order_products).where("order_products.product_type_id = ? and status <> ? and status <> ?", product_type_id, OrderStatuses::ready[:id], OrderStatuses::completed[:id])
amount_in_current_orders = orders.map {|o| o.order_products.map {|op| op.amount }.sum }.sum
amount_left_to_add = amount - amount_in_current_orders
if amount_left_to_add > 0
InventoryOrder.create({pickup_time: 3.days.from_now, location_id: Location::get_default_location.id, order_products: [OrderProduct.new({product_type_id: product_type_id, amount: amount_left_to_add})]})
end
end
def self.create_order_from_cancelled_order_product(order_product)
InventoryOrder.create({
pickup_time: DateTime.now.change({ min: 0, sec: 0 }) + 1.days,
location_id: Location::get_default_location.id,
order_products: [OrderProduct.new({
product_type_id: order_product.product_type_id,
feed_mill_job_id: order_product.feed_mill_job_id,
ration_id: order_product.ration_id,
amount: order_product.amount
})],
description: "Client Order for #{order_product.amount}kg of #{order_product.product_type.name} was cancelled after the feed mill job started."
})
end
end
And here is it's parent class:
class Order < ActiveRecord::Base
#active record concerns
include OrderProcessingInfo
belongs_to :client
belongs_to :location
has_many :order_products
before_destroy :clear_order_products
after_save :after_order_saved
before_save :on_before_save
accepts_nested_attributes_for :order_products, allow_destroy: true
after_initialize :init #used to set default values
validate :client_order_validations
def client_order_validations
if self.type == OrderTypes::client[:id] && self.client_id.nil?
errors.add(:client_id, _("choose a client"))
end
end
...
end
Thanks,
Eric
After doing some more digging and with the help of Roman's comment I was able to figure out that this issue was a result of me using an older convention for ActiveRecord::Concerns that works fine on windows but not on unix based systems.
According to this RailsCasts you can define your concerns like this:
In ../models/concerns/order/order_processing_info.rb
class Order
module OrderProcessingInfo
extend ActiveSupport::Concern
included do
end
...
end
But according to this the right way to define the concern would be to
1) Put it in ../models/concerns/[FILENAMEHERE] instead of ../models/concerns/[CLASSNAMEHERE]/[FILENAMEHERE]
2) Define the module without wrapping it in the class like this:
module OrderProcessingInfo
extend ActiveSupport::Concern
included do
end
end
Took some digging to get to the bottom of it but hopefully this might help someone else out there.

Rails 4 query, scoped by condition from has many through attributes

The question is : "given a company, how should I build the query that returns all events created by all employees during their respective employments periods ?"
Ex:
#company = Company.create
# Welcome John
#john = User.create
#event_a = #john.events.create date: Date.new(2013,6,1)
#event_b = #john.events.create date: Date.new(2014,1,1)
#company.employments.create user:#john, since: Date.new(2013,12,20)
# Welcome Jack
#jack = User.create
#event_c = #jack.events.create date: Date.new(2012,1,1)
#event_d = #jack.events.create date: Date.new(2013,1,1)
#company.employments.create user:#jack,
since: Date.new(2011,12,20), till: Date.new(2012,12,31)
#company.events
=> [#event_b, #event_c]
# #event_a is not returned because it was created prior to John's hiring
# #event_d is not returned because it was created after Jack's departure
I came up with a solution but I would like to know if there are any ways to improve it.
class Event
belongs_to :user
# attributes
# date: datetime
# …
end
class Employment
belongs_to :user
belongs_to :event
# attributes
# since: date
# till: date
# …
end
class Company
has_many :employments
def events
Event.where employments.map do |e|
if e.since && e.till
"(user_id = '#{e.user_id}' AND date BETWEEN '#{e.since}' AND '#{e.till}')"
elsif !e.since.nil?
"(user_id = '#{e.user_id}' AND date > '#{e.since}')"
else
"(user_id = '#{e.user_id}')"
end
end.join(' OR ')
end
end
Do you see any other way to do that ?
Events.joins(user: :employments).where(company_id: id)I believe you could use something like this:
def events
Event.where employments.map do |e|
user=User.find(e.user_id)
since = e.since
till = e.till || Date.parse('3100-12-31') # a day in the distant future if nil
user.events.where('(date is null) or (date > ? and date < ?)', since, till)
end
end
2nd try, after using the appropriate joins to connect Event and Employment model, you can use the final where clause to implement your filter.
The full method, after you've added the right set of joins and filters (thanks), is as follows.
def events
Events.joins(user: :employments).where(company_id: id). # <-This comes from the author, not me.
where('employments.since is null OR
(employments.since < events.date AND employments.till is null) OR
(employments.since < events.date AND employments.till > events.date)')
end
how can I query all events created by all employees during their
employment for a given company
You may wish to use an ActiveRecord Association Extension, which basically appends a new method to your association, allowing you to define filtration or some other specifications:
#app/models/company.rb
Class Company < ActiveRecord::Base
has_many :employments
has_many :events do
def by_date(since, till)
... logic here
end
end
end
This will allow you to call:
#app/controllers/events_controller.rb
Class EventsController < ApplicationController
def action
company = Company.find params[:id]
#events = company.events.by_date("01-01-2014", "04-01-2014")
end
end
If you let me know in the comments, I'll have a go at populating the method for you

Rails ActiveRecord callback messup

This is driving nuts. I have a dead simple callback functions to initialize and validate a class children as such:
class A < ActiveRecord::Base
has_many :bs
after_initialize :add_t_instance
validate :has_only_one_t
protected
def add_t_instance
bs << B.new(:a => self, :type => "T") unless bs.map(&:type).count("T") > 0
end
def has_only_one_t
unless bs.map(&:type).count("T") < 2
errors.add(:bs, 'has too many Ts")
end
end
end
and now, here comes the magic at runtime:
a = A.new
>>[#<A>]
a.bs
>> [#<T>]
a.save
>> true
a.id
>> 15
so far it's all going great, but:
s = A.find(15)
s.bs
>>[#<T>,#<T>]
s.bs.count
>> 2
s.valid?
>> false
s.errors.full_messages
>> "Too many Ts"
What the heck am I missing here?!?! What in the world could be adding the second #T?
Confusingly (to me at least) after_initialize is called whenever an active record object is instantiated, not only after creating a new instance, but also after loading an existing one from the database. So you create the second B when you run A.find(15).
You could solve the problem by checking whether you are dealing with a new record in your callback, e.g.
def add_t_instance
if new_record?
bs << B.new(:a => self, :type => "T") unless bs.map(&:type).count("T") > 0
end
end
or you could place a condition on the before_initialize declaration itself, or perhaps try using a before_create callback.

Rails: why isn't this marked_for_destruction not destroyed?

** update **
it all seems to be related to a custom validator: if I remove it, it works as expected. see code at the end
**
I have a model budget that has many multi_year_impacts
in the console, if I run:
b = Budget.find(4)
b.multi_year_impacts.size #=> 2
b.update_attributes({multi_year_impacts_attributes: {id: 20, _destroy: true} } ) #=> true
b.multi_year_impacts.size #=> 1 (so far so good)
b.reload
b.multi_year_impacts.size #=> 2 What???
and if before b.reload I do b.save (which shouldn't be needed anyway), it's the same.
Any idea why my child record doesn't get destroyed?
Some additional information, just in case:
Rails 3.2.12
in budget.rb
attr_accessible :multi_year_impacts_attributes
has_many :multi_year_impacts, as: :impactable, :dependent => :destroy
accepts_nested_attributes_for :multi_year_impacts, :allow_destroy => true
validates_with MultiYearImpactValidator # problem seems to com from here
in multi_year_impact.rb
belongs_to :impactable, polymorphic: true
in multi_year_impact_validator.rb
class MultiYearImpactValidator < ActiveModel::Validator
def validate(record)
return false unless record.amount_before && record.amount_after && record.savings
lines = record.multi_year_impacts.delete_if{|x| x.marked_for_destruction?}
%w[amount_before amount_after savings].each do |val|
if lines.inject(0){|s,e| s + e.send(val).to_f} != record.send(val)
record.errors.add(val.to_sym, " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts")
end
end
end
end
it might depend on your rails version, however, comparing your code to the current docs:
Now, when you add the _destroy key to the attributes hash, with a
value that evaluates to true, you will destroy the associated model:
member.avatar_attributes = { :id => '2', :_destroy => '1' }
member.avatar.marked_for_destruction? # => true
member.save
member.reload.avatar # => nil
Note that the model will not be destroyed until the parent is saved.
you could try with:
b.multi_year_impacts_attributes = {id: 20, _destroy: true}
b.save
So it looks like the culprit was here
if lines.inject(0){|s,e| s + e.send(val).to_f} != record.send(val)
record.errors.add(val.to_sym, " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts")
end
changing this to the slightly more complex
total = 0
lines.each do |l|
total += l.send(val).to_f unless l.marked_for_destruction?
end
if total != record.send(val)
record.errors[:amount_before] << " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts"
end
solved the problem.

Why are associated objects through a belongs_to not equal to themselves?

I have two classes with a has_many and belongs_to association:
class Employee < ActiveRecord::Base
has_many :contracts
end
class Contract < ActiveRecord::Base
belongs_to :employee
end
I expect that the employee returned by the #employee method of the Contract class would be equal to itself, which means that the following unit test would pass.
class EmployeeTest < ActiveSupport::TestCase
test "an object retrieved via a belongs_to association should be equal to itself" do
e = Employee.new
e.contracts << Contract.new
assert e.save
a = e.contracts[0].employee
assert a.equal? a
end
end
However, it fails. I do not understand. Is this a bug in ActiveRecord?
Thanks for helping out.
This has to do with object equality. consider this IRB session
irb(main):010:0> Foo = Class.new
=> Foo
irb(main):011:0> f = Foo.new
=> #<Foo:0x16c128>
irb(main):012:0> b = Foo.new
=> #<Foo:0x1866a8>
irb(main):013:0> f == b
=> false
By default, == will test that the two objects have the same type, and same object_id. In activerecord, it is hitting up the database for the first employee, and hitting it up again for the employee through the referencial method, but those are two different objects. Since the object_ids are different, it doesn't matter if they have all the same values, == will return false. To change this behavior, consider this second IRB session
irb(main):050:0> class Bar
irb(main):051:1> attr_accessor :id
irb(main):052:1> def ==(compare)
irb(main):053:2> compare.respond_to?(:id) && #id == compare.id
irb(main):054:2> end
irb(main):055:1> end
=> nil
irb(main):056:0> a = Bar.new
=> #<Bar:0x45c8b50>
irb(main):057:0> b = Bar.new
=> #<Bar:0x45c2430>
irb(main):058:0> a.id = 1
=> 1
irb(main):059:0> b.id = 1
=> 1
irb(main):060:0> a == b
=> true
irb(main):061:0> a.id = 2
=> 2
irb(main):062:0> a == b
=> false
Here I defined the == operator to compare the .id methods on the two objects (or to just return false if the object we are comparing doesn't have an id method). If you want to compare Employees by value like this, you will have to define your own == method to implement it.
That is probably because the rails internal cached differently in two association calls. try to do
a.reload.equal? a.reload
this will get rid of the caching and should return true

Resources