rails - how to pass changes back to parent model - ruby-on-rails

I need to pass updated paramaters to back to a parent model when saving a series of its children.
For example if a save a bunch of employees to each task through a project, I need to let the project know the title of some of its tasks have changed, then I need to collect all the titles that changed and process them in the ProjectObserver. Is this possible?
I realize there might not be a way to make this work the way I'm trying. If not I'm happy to hear suggestions about how I might be able to get around this.
Here is what I have tried without any success:
class Employee < ActiveRecord::Base
has_many :employee_tasks
has_many :tasks, :through => :employee_tasks
accepts_nested_attributes_For :employee_tasks
accepts_nested_attributes_For :tasks
end
class Project < ActiveRecord::Base
attr_accessor :changed_employees
has_many :tasks
end
class Task < ActiveRecord::Base
has_many :employee_tasks
has_many :employees, :through => :employee_tasks
belongs_to :project
accepts_nested_attributes_For :employee_tasks
end
class EmployeeTask < ActiveRecord::Base
#this is what I want to accomplish
before_save do
if self.employee_id_changed
self.task.project.changed_employees ||= []
self.task.project.changed_employees << self.employee_id_changed
end
end
belongs_to :task
belongs_to :employee
end
class ProjectObserver < ActiveRecord::Observer
observe :project
def after_save(project)
puts project.changed_employees
# should print out the changed attributes loaded from EmployeeTask
#send a single email with all the updated titles (not one email for each change)
end
end

Sounds like you need to use the after_save method described here http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html

Related

Get Changes to ActiveRecord association

I have something like this:
class Post < ActiveRecord::Base
has_many :tag_members
has_many :tags, through: :tag_member
end
class Tag < ActiveRecord::Base
has_many :tag_members
has_many :posts, through: :tag_member
end
class TagMember < ActiveRecord::Base
belongs_to :tag
belongs_to :image
end
I want to track the edits on the post object. The easiest way to do this appears to be something like this:
class Post < ActiveRecord::Base
before_update :save_edits
def save_edits
# Assuming save_edit takes in a hash and persists it somehow
save_edit(self.changes)
end
end
However, from testing I've done, adding a new Tag to the has_many association on a Post does not run the before_update callback, and does not store anything in the hash returned by .changes.
What is the best way to track these types of edit as well? Should I simply overload the .tags= method to do my own storage, or is there a better way?
You could do something like this:
class TagMember < ActiveRecord::Base
after_save { |t| t.post.save }
# ^^^^
belongs_to :tag
belongs_to :image
belongs_to :post
# ^^^^
end

multiple has_many-through to same polymorphic table, but with different source class, fails in tests but work in reality

The model is a User, which has many Contacts. Posts and Images can be published to Contacts through a ContactPublishment.
User has the methods visible_posts and visible_images to allow easy access to Posts and Images published to.
The problem is that while user.visible_images and user.visible_posts work perfectly, specs that rely on these relations are going crazy:
If removing either the test for visible_images or visible_posts from the spec, the remaining tests pass. if I leave both, the 2nd one fails. I can switch the order of the tests, but still the 2nd one fails. weird huh?
This is the code sample, using Rails 3.2.15:
class User < ActiveRecord::Base
...
has_many :visible_posts, through: :contact_publishments, source: :publishable, source_type: 'Post'
has_many :visible_images, through: :contact_publishments, source: :publishable, source_type: 'Image'
end
class Contact < ActiveRecord::Base
...
belongs_to :represented_user, class_name: User.name
has_many :contact_publishments
end
class ContactPublishment < ActiveRecord::Base
...
belongs_to :contact
belongs_to :publishable, polymorphic: true
end
class Post < ActiveRecord::Base
...
has_many :contact_publishments, as: :publishable, dependent: :destroy
has_many :contacts, through: :contact_publishments
end
class Image < ActiveRecord::Base
...
has_many :contact_publishments, as: :publishable, dependent: :destroy
has_many :contacts, through: :contact_publishments
end
describe User do
...
it "#visible_images" do
user = create :user
image = create :image
image.contacts << create(:contact, represented_user: user)
user.visible_images.should == [image]
end
it "#visible_posts" do
user = create :user
post = create :post
post.contacts << create(:contact, represented_user: user)
user.visible_posts.should == [post]
end
end
So I solved it eventually, but not in the way I wanted. I just wrote a manual join query. The funny thing is that this triggers the same exact SQL query that my original solution triggered, but somehow only this one passes specs. Bug in Rails?
class User < ActiveRecord::Base
...
[Post, Image].each do |publishable|
define_method("visible_#{publishable.name.pluralize.underscore}") do
publishable.joins(:users).where('users.id' => self.id)
end
end
end

Using Delegate With has_many In Rails?

We've got 2 models & a join model:
#app/models/message.rb
Class Message < ActiveRecord::Base
has_many :image_messages
has_many :images, through: :image_messages
end
#app/models/image.rb
Class Image < ActiveRecord::Base
has_many :image_messages
has_many :messages, through: :image_messages
end
#app/models/image_message.rb
Class ImageMessage < ActiveRecord::Base
belongs_to :image
belongs_to :message
end
Extra Attributes
We're looking to extract the extra attributes from the join model (ImageMessage) and have them accessible in the Message model:
#message.image_messages.first.caption # -> what happens now
#message.images.first.caption #-> we want
We've already achieved this using the select method when declaring the association:
#app/models/message.rb
has_many :images, -> { select("#{Image.table_name}.*", "#{ImageMessage.table_name}.caption AS caption") }, class_name: 'Image', through: :image_messages, dependent: :destroy
Delegate
We've just found the delegate method, which does exactly what this needs. However, it only seems to work for has_one and belongs_to associations
We just got this working with a single association, but it seems it does not work for collections (just takes you to a public method)
Question
Do you know any way we could return the .caption attribute from the ImageMessage join model through the Image model?
We have this currently:
#app/models/image.rb
Class Message < ActiveRecord::Base
has_many :image_messages
has_many :messages, through: :image_messages
delegate :caption, to: :image_messages, allow_nil: true
end
#app/models/image_message.rb
Class ImageMessage < ActiveRecord::Base
belongs_to :image
belongs_to :message
def self.caption # -> only works with class method
#what do we put here?
end
end
Update
Thanks to Billy Chan (for the instance method idea), we have got it working very tentatively:
#app/models/image.rb
Class Image < ActiveRecord::Base
#Caption
def caption
self.image_messages.to_a
end
end
#app/views/messages/show.html.erb
<%= #message.images.each_with_index do |i, index| %>
<%= i.caption[index][:caption] %> #-> works, but super sketchy
<% end %>
Any way to refactor, specifically to get it so that each time .caption is called, it returns the image_message.caption value for that particular record?
delegate is just a shorthand as equivalent instance method. It's not a solution for all, and there are even some debate that it's not so explicit.
You can use an instance method when simple delegate can't fit.
I reviewed and found any association is unnecessary is this case. The ImageMessage's class method caption is more like a constant, you can refer it directly.
def image_message_caption
ImageMessage.caption
end

Failing to save attributes of associated object in rails application

I have three models defined as follows
class Student < ActiveRecord::Base
belongs_to :user
has_many :placements
has_many :companys , through: :placements
end
class Company < ActiveRecord::Base
has_many :placements
has_many :students , through: :placements
end
class Placement < ActiveRecord::Base
belongs_to :student
belongs_to :company
before_save :set_placed
def set_placed
s = self.student
s.is_placed = true
s.save
end
end
Each time i add data for placement object i want to update a field in its corresponding student object. But when i use rails_admin to add data , i am getting the error Placement failed to be created .
When i remove the before_save call , data can be added.
I am using better_errors gem for debugging. I am getting the following from it
#_already_called
{[:autosave_associated_records_for_student, :student]=>false,
[:autosave_associated_records_for_company, :company]=>false}
i am hoping this could be the reason for error.
How can i solve this error??
You have a s.save in your set_placed callback. You don't save an ActiveRecord object in a callback, and especially not in a before_save callback.
try this,
def set_placed
self.student.is_placed = true
end

How to delete nested objects in Rails3?

How can I delete nested objects in a form? I found out that I need to add :allow_destroy in the parent model at the accepts_nested_attributes_for directive.
Further, I want to restrict the deletion. A nested object only should be deleted, if the parent object is the only one that retains the association.
Example:
class Internship < ActiveRecord::Base
belongs_to :company
accepts_nested_attributes_for :company, allow_destroy => true
end
class Company < ActiveRecord::Base
has_many :internships
end
Explanation: A company can host many internships. Therefore, I do not want to delete the company record as long as there is at least one other internship associated with it.
You could use dependent => :destroy
class Internship < ActiveRecord::Base
belongs_to :company
accepts_nested_attributes_for :company, allow_destroy => true
end
class Company < ActiveRecord::Base
has_many :internships, :dependent => :destroy
end
If you return false in a before_destroy filter, then the destroy action will be blocked. So we can check to see if there are any internships associated to the company, and block it if so. This is done in the company model.
class Company < ActiveRecord::Base
has_many :internships
before_destroy :ensure_no_internships
private
def ensure_no_internships
return false if self.internships.count > 0
end
end

Resources