Update deeply nested model upon save - ruby-on-rails

I'm attempting to update a deeply nested model from a model that is itself nested using cocoon. (Don't worry, I'll clarify)
I have three Models: Stock Stockholder and Folder. Stock has_many :stockholders, which are all nested via cocoon in a form_for #stock. Stockholder has_one :folder.
Within my stock form, I have a table that lists out stockholders where I can add new ones (to reiterate, via cocoon). To create a Folder for each new Stockholder, I have a before_create filter in my Stockholder model that creates a new Folder for each new Stockholder (see below)
before_create :build_default_folder
def build_default_folder
logger.debug "Inside build_default_folder"
build_folder(name: "#{self.holder_index}. #{self.holder_name}", company_id: self.warrant.company.id, parent_id: self.warrant.company.folders.find_by_name("#{self.warrant.security_series} #{self.warrant.security_class} Warrant").id)
true
end
All of that works very well.
The problem
Is that I would like to add a method that updates the Folder attributes according to any changes in the stockholder information (say they change the name or something). To this end, I've attempted to add the following.
before_save :update_default_folder
def update_default_folder
logger.debug "Inside update_default_folder"
self.folder.update_attributes(name: "#{self.holder_index}. #{self.holder_name}", company_id: self.warrant.company.id, parent_id: self.warrant.company.folders.find_by_name("#{self.warrant.security_series} #{self.warrant.security_class} Warrant").id)
true
end
This however doesn't work and (particularly puzzling to me) is that before_save doesn't even seem to be firing. My best guess would be that this is because stockholder is itself nested using cocoon (but I could be completely wrong about this).
Anyway, how might I achieve the desired functionality? Thanks for any suggestions.

Related

Saving multiple instances of models in a rails form using CRUD

I'm trying to create a form that has multiple instances of different models at once.
I have my main model visualizations. A Visualization (:title, :cover_image) has_many Rows. A Row has_many Panes (:text_field, :image)
Basically when a user tries to create a Visualization, they can choose the cover image and title easily enough. But I get a bit confused when I come to the next two levels.
The user is prompted to create a new Row in the form and they can choose either 1, 2, or 3 Panes per Row. Each pane can take in text and an image, but Row doesn't necessarily have any attributes itself.
How can I generate multiple Rows with multiple Panes in this form? The end result will need to possess a bunch of rows consisting of many panes. Can I even do this in rails?
Thanks for any help!
You can do anything in rails! The best approach in my opinion is to create what is known as a Form Model since this form will have a lot going on and you don't want to bog down several models with validations and such for one view of your app. To do this you're basically going to create a class that will take all of this information in, run whatever validations you need, and then create whatever records you need in whatever models you have. To do this lets create a new file in your model folder called so_much.rb (You can make any filename you want just make sure you name the class the same as the file so Rails finds it automagically!)
Then in your so_much.rb file do:
class SoMuch
include ActiveModel::Model #This gives us rails validations & model helpers
attr_accessor :visual_title
attr_accessor :visual_cover #These are virtual attributes so you can make as many as needed to handle all of your form fields. Obviously these aren't tied to a database table so we'll run our validations and then save them to their proper models as needed below!
#Add whatever other form fields youll have
validate :some_validator_i_made
def initialize(params={})
self.visual_title = params[:visual_title]
self.visual_cover = params[:visual_cover]
#Assign whatever fields you added here
end
def some_validator_i_made
if self.visual_title.blank?
errors.add(:visual_title, "This can't be blank!")
end
end
end
Now you can go into your controller that is processing this form and do something like:
def new
#so_much = SoMuch.new
end
def create
user_input = SoMuch.new(form_params)
if user_input.valid? #This runs our validations before we try to save
#Save the params to their appropriate models
else
#errors = user_input.errors
end
end
private
def form_params
params.require(#so_much).permit(all your virtual attributes we just made here)
end
Then in your view you would set your form_for up with #so_much like:
<%= form_for #so_much do %>
whatever virtual attributes etc
<% end %>
Form Models are a bit advanced in Rails but are a life saver when it comes to larger apps where you have many different types of forms for one model and you don't want all of the clutter.

Rails preview update associations without saving to database

I want to preview what the model will look like when saved without currently saving to the database.
I am using #event.attributes = because that assigns but does not save attributes for #event to the database.
However, when I also try to assign the audiences association, Rails inserts new records into the audiences_events join table. Not cool. Is there a way to preview what these new associations will look like without inserting into the join table?
Model
class Event < ActiveRecord::Base
has_and_belongs_to_many :audiences # And vice versa for the Audience model.
end
Controller
class EventsController < ApplicationController
def preview
#event = Event.find(params[:id])
#event.attributes = event_params
end
private
def event_params
params[:event].permit(:name, :start_time, :audiences => [:id, :name]
end
end
Possible Solutions?
Possible solutions that I thought of, but don't know how to do:
Using some sort of method that assigns associations, but does not persist them.
disabling all database writes for this one action (I dont know how to do that).
Rolling back all database changes at the end of this action
Any help with these would be great!
UPDATE:
After the reading the great answers below, I ended up writing this service class that assigns the non-nested attributes to the Event model, then calls collection.build on each of the nested params. I made a little gist. Happy to receive comments/suggestions.
https://gist.github.com/jameskerr/69cedb2f30c95342f64a
In these docs you have:
When are Objects Saved?
When you assign an object to a has_and_belongs_to_many association, that object is automatically saved (in order to update the join table). If you assign multiple objects in one statement, then they are all saved.
If you want to assign an object to a has_and_belongs_to_many association without saving the object, use the collection.build method.
Here is a good answer for Rails 3 that goes over some of the same issues
Rails 3 has_and_belongs_to_many association: how to assign related objects without saving them to the database
Transactions
Creating transactions is pretty straight forward:
Event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Or
#event.transaction do
#event.audiences.create!
#event.audiences.first.destroy!
end
Notice the use of the "bang" methods create! and destroy!, unlike create which returns false create! will raise an exception if it fails and cause the transaction to rollback.
You can also manually trigger a rollback anywhere in the a transaction by raising ActiveRecord::Rollback.
Build
build instantiates a new related object without saving.
event = Event.new(name: 'Party').audiences.build(name: 'Party People')
event.save # saves both event and audiences
I know that this is a pretty old question, but I found a solution that works perfectly for me and hope it could save time to someone else:
class A
has_many :bs, class_name 'B'
end
class B
belongs_to :a, class_name: 'A'
end
a.bs.target.clear
new_bs.each {|new_b| a.bs.build new_b.attributes.except('created_at', 'updated_at', 'id') }
you will avoid autosave that Rails does when you do a.bs = new_bs

Updating an ActiveAdmin nested resource using inputs not located on the table

I currently have a form for a nested resource in one of my activeadmin pages:
f.inputs "Courses" do
f.has_many :registrations, :allow_destroy => true, new_record: true do |tc|
tc.input :course
tc.input :semester
end
end
This is for a student resource. A student has many course_offerings through registrations. However, I didn't want users to select the course_offering directly from a menu (Since there will be many repeats of the same course each year). Instead there is an input for course and semester. course and semester are instance variables on the registration model. They are set in the form, then the correct course_offering is found in an after save hook and associated with the registration. The code is as follows:
def semester=(s)
#semester = s.to_i
end
def semester
self.course_offering.semester
end
def course=(co)
#course = co.to_i
end
def course
self.course_offering.course
end
before_save :set_course_offering
def set_course_offering
self.course_offering = CourseOffering.where(semester_id: #semester, course_id: #course).first
#TODO: Handle case where no course offering is found
end
I am having two problems. The first is that I get a nil pointer error when registrations table is empty.
undefined method `course' for nil:NilClass
I have accepts_nested_attributes call in my student model.
accepts_nested_attributes_for :registrations, :allow_destroy => true
which is the only suggestion I get when looking up the error but still get it despite having that piece of code. It seems to work fine when I remove the course and semester and replace it with a course_offering instead.
The next problem I have is that the student record does not save after hitting update. I assume this is because I don't make any changes that write to the database when I only update the two course and semester instance variables only. I either need to update another input or add the call to the semester= method.
You get an undefined method error because when the registration table is empty there are no course offerings associated with your student, so in the accessor method for course you get nil for self.course_offerings. You could try this instead which takes the nil value into account:
def course
self.course_offering.try(:course)
end
You don't need the accept_nested_attributes_for because you don't want to create or modify course offerings through students.
For your second problem: you're right about the dirty tracking. Your model is not saved because your student model has not been modified from the perspective of ActiveRecord. You need to flag an attribute (eg course_offering) as dirty by hand with the course_attribute_will_change! method before saving the model.
Although this situation looks like a good example to introduce form objects. There is a great library for that called reform.

How to use `first_or_initialize` step with `accepts_nested_attributes_for` - Mongoid

I'd like to incorporate a step to check for an existing relation object as part of my model creation/form submission process. For example, say I have a Paper model that has_and_belongs_to_many :authors. On my "Create Paper" form, I'd like to have a authors_attributes field for :name, and then, in my create method, I'd like to first look up whether this author exists in the "database"; if so, then add that author to the paper's authors, if not, perform the normal authors_attributes steps of initializing a new author.
Basically, I'd like to do something like:
# override authors_attributes
def authors_attributes(attrs)
attrs.map!{ |attr| Author.where(attr).first_or_initialize.attributes }
super(attrs)
end
But this doesn't work for a number of reasons (it messes up Mongoid's definition of the method, and you can't include an id in the _attributes unless it's already registered with the model).
I know a preferred way of handling these types of situations is to use a "Form Object" (e.g., with Virtus). However, I'm somewhat opposed to this pattern because it requires duplicating field definitions and validations (at least as I understand it).
Is there a simple way to handle this kind of behavior? I feel like it must be a common situation, so I must be missing something...
The way I've approached this problem in the past is to allow existing records to be selected from some sort of pick list (either a search dialog for large reference tables or a select box for smaller ones). Included in the dialog or dropdown is a way to create a new reference instead of picking one of the existing items.
With that approach, you can detect whether the record already exists or needs to be created. It avoids the need for the first_or_initialize since the user's intent should be clear from what is submitted to the controller.
This approach struggles when users don't want to take the time to find what they want in the list though. If a validation error occurs, you can display something friendly for the user like, "Did you mean to pick [already existing record]?" That might help some as well.
If I have a model Paper:
class Paper
include Mongoid::Document
embeds_many :authors
accepts_nested_attributes_for :authors
field :title, type: String
end
And a model Author embedded in Paper:
class Author
include Mongoid::Document
embedded_in :paper, inverse_of: :authors
field :name, type: String
end
I can do this in the console:
> paper = Paper.create(title: "My Paper")
> paper.authors_attributes = [ {name: "Raviolicode"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Raviolicode">]
> paper.authors_attributes = [ {id: paper.authors.first, name: "Lucia"}, {name: "Kardeiz"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Lucia">, #<Author _id: 531cd95931302ea603010000, name: "Kardeiz">]
As you can see, I can update and add authors in the same authors_attributes hash.
For more information see Mongoid nested_attributes article
I followed the suggestion of the accepted answer for this question and implemented a reject_if guard on the accepts_nested_attributes_for statement like:
accepts_nested_attributes_for :authors, reject_if: :check_author
def check_author(attrs)
if existing = Author.where(label: attrs['label']).first
self.authors << existing
true
else
false
end
end
This still seems like a hack, but it works in Mongoid as well...

Rails: How can I access the parent model of a new record's nested associations?

Suppose we have the standard Post & Comment models, with Post having accepts_nested_attributes_for :commments and :autosave => true set.
We can create a new post together with some new comments, e.g.:
#post = Post.new :subject => 'foo'
#post.comments.build :text => 'bar'
#post.comments.first # returns the new comment 'bar'
#post.comments.first.post # returns nil :(
#post.save # saves both post and comments simultaneously, in a transaction etc
#post.comments.first # returns the comment 'bar'
#post.comments.first.post # returns the post 'foo'
However, I need to be able to distinguish from within Comment (e.g. from its before_save or validation functions) between
this comment is not attached to a post (which is invalid)
this comment is attached to an unsaved post (which is valid)
Unfortunately, merely calling self.post from Comment doesn't work, because per above, it returns nil until after save happens. In a callback of course, I don't (and shouldn't) have access to #post, only to self of the comment in question.
So: how can I access the parent model of a new record's nested associations, from the perspective of that nested association model?
(FWIW, the actual sample I'm using this with allows people to create a naked "comment" and will then automatically create a "post" to contain it if there isn't one already. I've simplified this example so it's not specific to my code in irrelevant ways.)
I think it is strange that Rails does not let you do this. It also affects validations in the child model.
There's a ticket with much discussion and no resolution in the Rails bug tracker about this:
Nested attributes validations
circular
dependency
And a proposed resolution:
nested models: build should directly
assign the
parent
Basically, the deal is, the nested attributes code doesn't set the parent association in the child record.
There's some work-arounds mentioned in the second ticket I linked to.
I don't think you can do this. On the other hand, your validations shouldn't be failing, as the order of the transaction will create the post record before saving the comment.

Resources