I have to be dead tired because I really can't figure out such simple task as this.
Having:
class Account < ActiveRecord::Base
has_one :subscription, :dependent => :destroy
after_save :append_subscription
private
def append_subscription
# TODO
end
end
# Subscription(id: integer, account_id: integer, level: integer (: 1), starts_at: date, ends_at:date, created_at: datetime, updated_at: datetime)
class Subscription < ActiveRecord::Base
belongs_to :account
end
I'm trying resolve the TODO part, or am I going about it the wrong way? Here's the test.
describe Account do
include AccountSpecHelper
it "should have a subscription at least at level one on creation" do
account = Account.create
account.subscription.level.should be(1)
end
end
Why after_save and not before_create and let ActiveRecord worry about creating associated model and assigning account_id correctly?
I haven't checked, but this should work:
class Account
before_create {|account| account.build_subscription(params)} # or move it to method
end
Related
When working with many-to-many relationships, I need to maintain a log file recording the changed values.
Using the before_save and after_save callbacks works fine for the main (has_many) model itself but in the before_save callback the associated (belongs_to) records appear to be updated already!
It seems rather odd to see that some parts of the data already have been updated before the 'before_save' callback is called.
Also, using callbacks in the associated model reveals that there is no before_destroy executed. Only the before_save gets called and shows the new values.
I also tried the :prepend => :true option but that didn't gave other results.
When turning on SQL logging before the actual save in the main (has_many) model, I can see Rails is fetching the associated records, determines the differences and deletes the surplus record(s). The before_destroy of the associated model is not called.
It then calls the before_save of the associated model and inserts the new ones (if any) and commits the transaction. This is all done JUST BEFORE the before_save of the main model.
Does anyone know how to fetch the associated records before they are changed?
I would expect the before_destroy of the associated model would get called and let me handle it.
Use before_update and you can access old values using _was
before_update :record_values
def record_values
p "oldvalue: #{self.field_was} Newvalue: #{self.field}"
end
Your question is a bit unclear, but let me provide you with an example of what I think you try to do.
The code below works fine for me:
class Person < ApplicationRecord
has_many :addresses
validates_presence_of :name
before_save { puts "before_save of person - changes: #{changes}" }
before_destroy { puts "before_destroy of person with id: #{id}" }
end
class Address < ApplicationRecord
belongs_to :person, required: true
validates_presence_of :name
before_save { puts "before_save of address - changes: #{changes}" }
before_destroy { puts "before_destroy of address with id: #{id}" }
end
This results in the following output when interacting:
person = Person.create(name: 'Johan Wentholt')
# before_save of person - changes: {"name" =>[nil, "Johan Wentholt"]}
#=> #<Person id: 2, name: "Johan Wentholt", created_at: "2017-10-25 15:04:27", updated_at: "2017-10-25 15:04:27">
person.addresses.create(name: 'Address #1')
# before_save of address - changes: {"person_id"=>[nil, 2], "name" =>[nil, "Address #1"]}
#=> #<Address id: 7, person_id: 2, name: "Address #1", created_at: "2017-10-25 15:06:38", updated_at: "2017-10-25 15:06:38">
person.addresses.last.update(name: 'Address without typo')
# before_save of address - changes: {"name"=>["Address #1", "Address without typo"]}
#=> true
person.update(name: 'Kaasboer')
# before_save of person - changes: {"name"=>["Johan Wentholt", "Kaasboer"]}
#=> true
person.addresses.last.destroy
# before_destroy of address with id: 7
#=> #<Address id: 7, person_id: 2, name: "Address without typo", created_at: "2017-10-25 15:06:38", updated_at: "2017-10-25 15:08:51">
person.destroy
# before_destroy of person with id: 2
#=> #<Person id: 2, name: "Kaasboer", created_at: "2017-10-25 15:04:27", updated_at: "2017-10-25 15:10:46">
As you can see this logs all changes. Like I said, the question is a bit unclear, but I hope this helps you further.
Keep in mind that some Rails methods don't trigger callbacks. For example: delete, update_all, update_column and some others.
For more about changes take a look at: ActiveModel::Dirty
For clarity sake, lets give some extended information:
class Book < ActiveRecord::Base
unloadable
has_many :titles, dependent: :destroy
has_many :authors, :through => :titles
accepts_nested_attributes_for :authors
before_save :pre_save
after_save :post_save
before_destroy :pre_delete
def pre_save
#nr = self.new_record?
end
def pre_save
changed_values = []
if #nr
changed_values.push "New record created"
else
self.changes.each do |field, cvs|
changes.push("#{field} : #{cvs[0]} => #{cvs[1]}")
end
end
if changes.length > 0
BookLog.create(:book_id => self.id, :changed_values => changes.join(', '))
end
end
def pre_delete
BookLog.create(:book_id => self.id, :changed_values => "Deleted: #{self.name}")
end
end
class Title < ActiveRecord::Base
unloadable
belongs_to :book
belongs_to :author
end
class Author < ActiveRecord::Base
unloadable
has_many :titles, dependent: :destroy
has_many :books, :through => :titles
accepts_nested_attributes_for :books
end
class BooksController < ApplicationController
def edit
book = Book.find(params[:book][:id])
book.name = .....
===> Here the old values are still available <====
book.author_ids = params[:book][:author_ids]
===> Now the new values are written to the database! <====
book.save!
end
end
Changes to the Book record are perfectly logged.
But there is no way to fetch the changed associated values for author_ids.
A before_destroy callback in Title was not called, the after_save was.
I checked this with enabling the SQL logging just before the assignment of the new author_ids to the edited record. I could see that Rails determines the differences between the existing and new associated values, deletes the surplus form the Titles table and insert the extra ones (if any)
I solved it by moving the logging for the changes in Titles to the Books controller by comparing the old with the new values:
o_authors = book.author_ids
n_authors = params[:book][:author_ids].collect {|c| c.to_i}
diff = o_authors - n_authors | n_authors - o_authors
if !diff.empty?
changed_values = []
(o_authors - n_authors).each do |d|
changed_values.push("Removed Author: #{Author.find(d).name}")
end
(n_authors - o_authors).each do |d|
changed_values.push("Added Author: #{Author.find(d).name}")
end
BookLog.create(:book_id => book.id, :changed_values => changed_values)
end
book.author_ids = params[:book][:author_ids]
book.save!
Like I said, it works but IMHO it does not reflect the Rails way of doing things. I would have expected to get the previous author_ids in the same way as any other Book attribute.
class Program < ActiveRecord::Base
has_many :contacts
class Contact < ActiveRecord::Base
belongs_to :program
FactoryGirl.define do
factory :contact do
sequence(....
...
program_id 1 #foreign key
program #association
(byebug) contact
Contact id: 949, display_name: "Contact-3", business_phone: "1234567894", fax_number: "1234567894", created_at: "2017-03-05 00:43:24", updated_at: "2017-03-05 00:43:24", first_name: "First-4", last_name: "Last-4", middle_initial: "4", email: "Email4#Something.Com", program_id: 1193, 287g: nil, active: true, call_office_id: 4
The program_id is 1193 in the contact record created with the contact factory but the program table has only four records with ids 1-4. Not sure where 1193 originates from. At this point the rspec test more or less succeeds. But once the validation code below gets added to the contact model the rspec test fails.
Contact Model with Association Validation Added For Program
class ProgramValidator < ActiveModel::Validator
def validate(record)
if record.program.nil?
record.errors[:base] << "Program cannot be blank"
end
end
end
class Contact < ActiveRecord::Base
belongs_to :program
validates_with ProgramValidator
Running rspec now it complains that "Program cannot be blank". Question: how can the contact factory get created to satisfy the validation? Why are associations so friggin difficult, much harder than creating the associations in ROR. Thanks for reading.
According to Factory Girl's documentation try something like:
factory :contact do
# ...
association :program, factory: :program
end
For more ifnromation about Factory Girl's associations please follow the link:
https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md#associations
This:
FactoryGirl.define do
program #association
creates new Program record, which is attached as association (with some other id, which can be also 1193 or any other id).
If you don't want any new program record to be created, just leave program_id 1 only in your factory class. Also, remember that you run your tests in empty database.
This factory class definition will work if you create Program record, for example, before your test suite and specify explicitly its ID as 1.
I have a model Order which is like
# app/models/order.rb
class Order< ApplicationRecord
has_one :detail
has_one :extra
..
end
I have two orders
order1 = Order.first
order1.detail #<OrderDetail:0x00 name: "abc", remark: 'test1'>
order1.extra #<OrderExtra:0x00 email: nil, recipent: nil>
order2 = Order.second
order1.detail #<OrderDetail:0x00 name: "abc", remark: 'test1'>
order1.extra #<OrderExtra:0x00 email: nil, recipent: "xyz">
When I call order1.valid? or order1.save! it will not check OrderExtra validation and returns true. But when I call order2.valid? or order2.save! it checks OrderExtra validation.
order1.save! # true
order2.save! # ActiveRecord Invalid OrderExtra
I want to know how rails checks if they want to check associated validation when call save! and the reason behind that.
Please let me know if any additional requirement needed on this.
use the validates_associated for enforcing associated model validations
class Book < ActiveRecord::Base
has_many :pages
belongs_to :library
validates_associated :pages, :library
end
This validation will not fail if the association hasn’t been assigned. If you want to ensure that the association is both present and guaranteed to be valid, you also need to use validates_presence_of.
class Library < ActiveRecord::Base
has_many :books
validates_presence_of :name
end
I need to create a todo list with any objects type. Objects that will be part of the list: Diet, Prescription and VeterinaryConsult.
Per hour i made: Keep the object class on object_type column, and when i will get the object, use the .send(TodoItem.x.object_type) method. This is the best way? I thought to use serialize option, but i don't how.
And for this, i create this structure:
Diet(id: integer, name: string, todo_item_id: integer, created_at: datetime, updated_at: datetime)
class Diet < ActiveRecord::Base
belongs_to :todo_item
end
Prescription(id: integer, name: string, todo_item_id: integer, created_at: datetime, updated_at: datetime)
class Prescription < ActiveRecord::Base
belongs_to :todo_item
end
TodoItem(id: integer, name: string, done_date: date, is_done: boolean, created_at: datetime, updated_at: datetime, object_type: string)
class TodoItem < ActiveRecord::Base
has_one :diet
has_one :prescription
def related
self.send(self.object_type)
end
end
On controllers i do:
class PrescriptionsController < ApplicationController
before_filter :create_todo_item, only: [:create]
def create
#prescription = Prescription.new(prescription_params)
#prescription.todo_item = #todo_item
...
end
class ApplicationController < ActionController::Base
def create_todo_item
#todo_item = TodoItem.new(object_type: params[:controller].singularize)
#todo_item.save
end
end
Sorry for my poor english :|
Maybe you could try a different approach:
Diet.rb
has_many :todo_items, as: :todoable
after_create :create_todo_item
def create_todo_item
todo_items.create
end
Prescription.rb
has_many :todo_items, as: :todoable
after_create :create_todo_item
def create_todo_item
todo_items.create
end
and on every other model you need todos you can use the above code
In TodoItem.rb all you have to do is
belongs_to :todoable, polymorphic: true
And create the fields on TodoItem todoable_type and todoable_id
Looks a lot better IMO. Even this way it is possible to further refactoring it creating a module "todoable" and load it on every model you need, but it is a next step
I followed the post http://techspry.com/ruby_and_rails/multiple-table-inheritance-in-rails-3/ to implement the multiple table inheritance with Rail 4. I have three models: user, applicant and tutor. Here is my code:
class User < ActiveRecord::Base
belongs_to :student, :polymorphic => true
end
class Tutor < ActiveRecord::Base
acts_as_student
end
class Applicant < ActiveRecord::Base
acts_as_student
end
# in the /lib/student_module.rb
module Student
def acts_as_student
include InstanceMethods
has_one :user, :as => :student, :autosave => true, :dependent => :destroy
alias_method_chain :user, :build
user_attributes = User.content_columns.map(&:name) #<-- gives access to all columns of Business
# define the attribute accessor method
def student_attr_accessor(*attribute_array)
attribute_array.each do |att|
define_method(att) do
user.send(att)
end
define_method("#{att}=") do |val|
user.send("#{att}=",val)
end
end
end
student_attr_accessor *user_attributes #<- delegating the attributes
end
module InstanceMethods
def user_with_build
user_without_build || build_user
end
end
end
The User Table has username, email attributes.The Tutor table has first_name,last_name,intro,program,entry_year attributes.
In the rails console, I got
tutor = Tutor.new => #<Tutor id: nil, first_name: nil, last_name: nil, intro: nil, created_at: nil, updated_at: nil, entry_year: nil, program: nil>
tutor.username
=> ActiveRecord::UnknownAttributeError: unknown attribute: student_id
I found the error was from the student_attr_accessor method. How should I fix it? Thanks!
I found I forgot to declare a foreign key column and a type column in the User Model. To fix this, just run a migration like:
def change
add_column :users, :student_id,:integer
add_column :users, :student_type,:string
end