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

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.

Related

Rails How make a conditional inside a method on the model

I'm trying to make that just the user can feedback on a feedback method in the feedback model. but in the logs shows
NoMethodError (undefined method `total_feedbacks' for nil:NilClass)
so show to make a conditional in the model?
def feedback_product
user = User.find_by_id(attributes['user_id'])
if user
product.total_feedbacks += 1
product.average_rating = product.feedbacks.where('buyer_feedback_date IS NOT NULL').rated(Feedback::FROM_BUYERS).average(:buyer_rating)
product.save
end
end
the feedback
belongs_to :user
and user
has_many :feedbacks
Your question is not totally clear. You didn't mention in which model this code is inside and your condition seems okay.
It seems product is nil, so maybe you are missing something that was not mentioned in the question, like instantiating that product. Or maybe you want your condition to be:
if user && product
The problem is that this method assumes there is an existing product for the model in question. However if no record has been created at, then product will be nil.
This can be addressed in a few ways, which you use depends on your use-case.
making sure that this model creates a product upon save, e.g. through before_save:
before_save :ensure_product_exists
def ensure_product_exists
product ||= Product.create(foo_id: self.id) # replace with correct foreign key
end
making this method create the product if it does not yet exist, e.g.
product ||= Product.new(foo_id: self.id) # replace with correct foreign key
making the method return early if the product does not exist, e.g.
return unless product
raising an error unless the product exists
raise(RuntimeError, "product doesn't exist") unless product

How to build objects that avoid a ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection erros in rails?

I am experiencing the ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection error when trying to build a new object in my rails app. It does not fit any of the standard errors I've seen, and can't be fixed with inverse_of associations.
I presume I need to run a callback to help this work - can anyone help fix the issue below:
def PhoneNumber do
belongs_to :key_contact
end
def KeyContact do
has_many :phone_numbers
has_many :sale_contacts
end
def SaleContact do
belongs_to :key_contact
belongs_to :sales_opportunity
has_many :phone_numbers, through: :key_contact
accepts_nested_attributes_for :phone_numbers
end
As you can see, SaleContact is the join table where key_contacts and sales_opportunities meet - basically I'm picking existing key_contacts and displaying them on a sales_opportunity page with some additional details (role, preference etc - I've excluded this for brevity).
When adding a new sale_contact I want to offer users the ability to also add phone_numbers at the same time. This is throwing my activerecord error.
My SaleContact Controller:
def new
#sale_contact = SaleContact.new
#phone_number = #sale_contact.phone_numbers.build
end
This works to show the fields_for phone_number on the input form, and passed the right attributes through the params hash for adding a new phone_number, but that's when I get the error:
ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection (Cannot modify association 'SaleContact#phone_numbers' because the source reflection class 'PhoneNumber' is associated to 'KeyContact' via :has_many.):
From what I can see:
My new controller action builds a phone_number, but because the sale_contact does not yet know which key_contact it's associated with I presume ActiveRecord gets confused
If I try and remove the #sale_contact.phone_number.build line (replacing it with PhoneNumber.new for example) the fields no longer appear on the SaleContact new form
As such I was thinking of creating a callback to strip out the phone_number_attributes from the sale_contact hash, destroy the newly built phone_number and all associations, then start fresh by passing the phone_numbers_attributes to a PhoneNumber.new(phone_number_attributes) action and saving as a separate transaction. Would that work?
You could try this instead:
delegate :phone_numbers, to: :key_contact

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

how can i validate the nested attributes field in rails 4?

I have two models
class Information < ActiveRecord::Base
belongs_to :study
validates_presence_of :email
end
and
class Study < ActiveRecord::Base
has_many :informations
accepts_nested_attributes_for :informations
end
I show up a form of study which contains few fields for the informations and i want to validate presence of those fields. Only on validation success i wanted to save the study field values as well and i wanted to show errors if the validation fails. How can i do this? Thanks in advance.
You write validations in the models that you require, as normal. So if you need to validate presence of field foo in the Information class you'd just write validates_presence_of :foo in that class. Likewise validations for Study fields just go in the Study class. With nested attributes, when you update a Study instance from a params hash that contains nested attributes, it'll update the Information instance(s) too, running validations in passing. That's what the accepts_nested_attributes_for call is doing - it's giving "permission" for the appropriate bits of a params hash to be used in this way.
You can use reject_if to only reject new nested records should they fail to meet criteria. So I might let someone create a Study and only create one or more nested Information instances associated with that Study if they'd filled in field(s) in the form, but if they left them blank, the nested stuff wouldn't be created and saved (so you don't get pointless blank associated records). The Study would still be saved. For example:
accepts_nested_attributes_for(
:informations,
reject_if: proc() { | attrs | attrs[ 'title' ] .blank? }
)
This and more is covered in the API documentation here:
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Beware that nested fields are intended for existing records only. If you were creating a new Study instance in a new/create action with no Information instances associated, you won't see any nested form fields for your Information class at all - when you might be expecting just one, for a blank new item. This can be very confusing if you aren't ready for it! You'll need to manually add a new Information instance to your Study instance in the controller or similar for the 'new' and 'create' actions, e.g. using before_filter :create_blank_object, only: [ :new, :create ], with, say:
def create_blank_object
#study = Study.new
#study.informations << Information.new
end
you can use validates_presence validation available in rails other wise you can write before_create or before_save callback method. write validation logic inside the before_create or before_save callback method.
Check out the API Doc for validates_associated:
Validates whether the associated object or objects are all valid. Works with any kind of association.
If you call a method on the parent object which runs validations (e.g. save), the validation on the associated objects will be called as well.

Use rails nested model to *create* outer object and simultaneously *edit* existing nested object?

Using Rails 2.3.8
Goal is to create a Blogger while simultaneously updating the nested User model (in case info has changed, etc.), OR create a brand new user if it doesn't exist yet.
Model:
class Blogger < ActiveRecord::Base
belongs_to :user
accepts_nested_attributes_for :user
end
Blogger controller:
def new
#blogger = Blogger.new
if user = self.get_user_from_session
#blogger.user = user
else
#blogger.build_user
end
# get_user_from_session returns existing user
# saved in session (if there is one)
end
def create
#blogger = Blogger.new(params[:blogger])
# ...
end
Form:
<% form_for(#blogger) do |blogger_form| %>
<% blogger_form.fields_for :user do |user_form| %>
<%= user_form.label :first_name %>
<%= user_form.text_field :first_name %>
# ... other fields for user
<% end %>
# ... other fields for blogger
<% end %>
Works fine when I'm creating a new user via the nested model, but fails if the nested user already exists and has and ID (in which case I'd like it to simply update that user).
Error:
Couldn't find User with ID=7 for Blogger with ID=
This SO question deals with a similar issue, and only answer suggests that Rails simply won't work that way. The answer suggests simply passing the ID of the existing item rather than showing the form for it -- which works fine, except I'd like to allow edits to the User attributes if there are any.
Deeply nested Rails forms using belong_to not working?
Suggestions? This doesn't seem like a particularly uncommon situation, and seems there must be a solution.
I'm using Rails 3.2.8 and running into the exact same problem.
It appears that what you are trying to do (assign/update an existing saved record to a belongs_to association (user) of a new unsaved parent model (Blogger) is simply not possible in Rails 3.2.8 (or Rails 2.3.8, for that matter, though I hope you've upgraded to 3.x by now)... not without some workarounds.
I found 2 workarounds that appear to work (in Rails 3.2.8). To understand why they work, you should first understand the code where it was raising the error.
Understanding why ActiveRecord is raising the error...
In my version of activerecord (3.2.8), the code that handles assigning nested attributes for a belongs_to association can be found in lib/active_record/nested_attributes.rb:332 and looks like this:
def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {})
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
(options[:update_only] || record.id.to_s == attributes['id'].to_s)
assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy], assignment_opts) unless call_reject_if(association_name, attributes)
elsif attributes['id'].present? && !assignment_opts[:without_protection]
raise_nested_attributes_record_not_found(association_name, attributes['id'])
elsif !reject_new_record?(association_name, attributes)
method = "build_#{association_name}"
if respond_to?(method)
send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts)
else
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
end
end
end
In the if statement, if it sees that you passed a user ID (!attributes['id'].blank?), it tries to get the existing user record from the blogger's user association (record = send(association_name) where association_name is :user).
But since this is a newly built Blogger object, blogger.user is going to initially be nil, so it won't get to the assign_to_or_mark_for_destruction call in that branch that handles updating the existing record. This is what we need to work around (see the next section).
So it moves on to the 1st else if branch, which again checks if a user ID is present (attributes['id'].present?). It is present, so it checks the next condition, which is !assignment_opts[:without_protection].
Since you are initializing your new Blogger object with Blogger.new(params[:blogger]) (that is, without passing as: :role or without_protection: true), it uses the default assignment_opts of {}. !{}[:without_protection] is true, so it proceeds to raise_nested_attributes_record_not_found, which is the error that you saw.
Finally, if neither of the other 2 if branches were taken, it checks if it should reject the new record and (if not) proceeds to build a new record. This is the path it follows in the "create a brand new user if it doesn't exist yet" case you mentioned.
Workaround 1 (not recommended): without_protection: true
The first workaround I thought of -- but wouldn't recommend -- was be to assign the attributes to the Blogger object using without_protection: true (Rails 3.2.8).
Blogger.new(params[:blogger], without_protection: true)
This way it skips the 1st elsif and goes to the last elsif, which builds up a new user with all the attributes from the params, including :id. Actually, I don't know if that will cause it to update the existing user record like you were wanting (probably not—haven't really tested that option much), but at least it avoids the error... :)
Workaround 2 (recommended): set self.user in user_attributes=
But the workaround that I would recommend more than that is to actually initialize/set the user association from the :id param so that the first if branch is used and it updates the existing record in memory like you want...
accepts_nested_attributes_for :user
def user_attributes=(attributes)
if attributes['id'].present?
self.user = User.find(attributes['id'])
end
super
end
In order to be able to override the nested attributes accessor like that and call super, you'll need to either be using edge Rails or include the monkey patch that I posted at https://github.com/rails/rails/pull/2945. Alternatively, you can just call assign_nested_attributes_for_one_to_one_association(:user, attributes) directly from your user_attributes= setter instead of calling super.
If you want to make it always create a new user record and not update existing user...
In my case, I ended up deciding that I didn't want people to be able to update existing user records from this form, so I ended up using a slight variation of the workaround above:
accepts_nested_attributes_for :user
def user_attributes=(attributes)
if user.nil? && attributes['id'].present?
attributes.delete('id')
end
super
end
This approach also prevents the error from occurring, but does so a little differently.
If an id is passed in the params, instead of using it to initialize the user association, I just delete the passed-in id so that it will fall back to building a new user from the rest of the submitted user params.
I ran into the same error in rails 3.2. The error occurred when using a nested form to create a new object with a belongs to relationship for an existing object. Tyler Rick's approach did not work for me. What I found to work was to set the relationship following the initialization of the object and then setting the objects attributes. An example of this is as follows ...
#report = Report.new()
#report.user = current_user
#report.attributes = params[:report]
assuming params looks something like ...
{:report => { :name => "name", :user_attributes => {:id => 1, { :things_attributes => { "1" => {:name => "thing name" }}}}}}
Try adding a hidden field for the user's id in the nested form:
<%=user_form.hidden_field :id%>
The nested save will use this to determine if it is a create or an update for the User.

Resources