Rails 5 - find_or_create_by with nested attributes - ruby-on-rails

I'm trying to create a new object with its associated records in the same form but would like the associated records to use find_or_create_by instead of just create (as the associated model records may and most of the time already will exist). I have spent the last two days digging through every post and article that I can find related to this subject trying to get this to work but still my form tries to create the new object only, not search for existing.
Models
#order.rb
has_many :order_owners, dependent: :destroy, inverse_of: :order
has_many :owners, through: :order_owners
accepts_nested_attributes_for :order_owners
#owner.rb
has_many :order_owners, dependent: :destroy
has_many :orders, through: :order_owners
validates :name, presence: true, uniqueness: { case_sensitive: false }
#order_owner.rb
belongs_to :owner
belongs_to :order
accepts_nested_attributes_for :owner
Form
orders/new.html.erb
<%= bootstrap_form_for(#orders, layout: :horizontal, label_col: "col-sm-2", control_col: "col-sm-6") do |f| %>
<%= render 'shared/error_messages', object: f.object %>
...
<%= f.fields_for :order_owners do |orderowner| %>
<%= render 'orders/new_order_owners', f: orderowner, render_partial: 'orders/new_order_owners' %>
<% end %>
...
<%= f.form_group do %>
<%= f.submit "Create Order", class: "btn btn-primary" %>
<%= link_to_add_association fa_icon("plus", text: "Add Owner"), f, :order_owners,
class: "btn btn-outline pull-right #{orderBtnDisable(#properties)}", partial: "orders/new_order_owners", id: "newOrderOwnerAdd" %>
<% end %>
<% end %>
orders/new_order_owners partial
<div class="m-t-md m-b-md border-bottom form-horizontal nested-fields">
<%= link_to_remove_association(fa_icon("remove", text: ""), f, { class: "btn btn-danger btn-outline pull-right" }) %>
<% f.object.build_owner unless f.object.owner %>
<%= f.fields_for :owner do |owner| %>
<%= owner.select :name, options_from_collection_for_select(#owners, "name", "name"),
{ label: "Name:", include_blank: true }, { id: "orderPropOwnerSelect", data: { placeholder: "Select an existing Owner or type a new one.."} } %>
<% end %>
</div>
Controller
orders/new
def new
#order = Order.new
#order.build_property
#order.order_owners.build.build_owner
#properties = Property.find_by_id(params[:property_id])
if #properties
#owners = #properties.owners
else
#owners = []
end
respond_to do |format|
format.html
format.js
end
end
orders/create
def create
#properties = Property.find(params[:order][:property_id])
#order = #properties.orders.create(order_params)
respond_to do |format|
format.html { if #order.save
if params[:order][:owners_attributes]
order_prop_owner_check(#order, #properties)
end
flash[:success] = "Order created successfully!"
redirect_to property_order_path(#properties, #order)
else
#properties
#owner = #properties.owner
render 'new'
end
}
format.js {
if #order.save
flash.now[:success] = "Order Updated Successfully!"
else
flash.now[:danger] = #order.errors.full_messages.join(", ")
end
}
end
end
So as you can see in the new action, I instantiate the new Order, build its associated property (its what the Order belongs_to), build the new order_owner relationship, and build the owner for that relationship. Then on submit it creates the order via #properties.orders.create(order_params).
The error that I get is "Order owners owner name already exists." so clearly its not looking up an owner by name. I have tried:
Redefining autosave_associated_records_for_owner in order.rb and order_owner.rb, using both belongs_to and has_many variations, but it seems like they never get called so I must be doing something wrong. (I have tried variations of almost every answer I could find on SO)
before_add: callback on both has_many :owners, through: :order_owners and has_many :order_owners in order.rb.
Extending has_many :owners and has_many :owners, through: :order_owners in order.rb as well as belongs_to :owner in order_order.rb
I've also tried different variations of calling associations and such within the form so I must be just misunderstanding something. I'm also using Cocoon to manage the nested forms but I've talked to the author on unrelated issues and Cocoon is essentially just a view helper for nested forms so the solution must something in the models/controller.
Any and all ideas welcome. Thanks in advance.
P.s. I left code in the controller actions that may/may not pertain to this exact post but I wanted to show the entire action for completeness. If it matters, I manually set the owners select via AJAX when a property is selected in another field. Basically it just looks up the property and adds existing owners to the owners select.

The owner is nested inside the order. So when you call order.save, it runs all validations (including owner's). If you want to use find_or_create_by you need to do it inside a before_save, so you can make modifications to the owner before the validation hits.
#order.rb
before_save :find_or_create_owner
def find_or_create_owner
self.order_owners.each do |order_owner|
order_owner.owner = Owner.find_or_create_by(name: final_owner.name)
end
end
Further customization may be needed depending on your form and business logic, but that's the main concept.

Related

Simple_form_for many to many with validation

Setup
I have a simple many to many relationship between a Submit and an Answer through SubmitAnswer.
Answers are grouped by a Question (in my case each question has three answers) - think of it as a multiple choice quiz.
I have been trying to use SimpleFormFor to make a form which renders a predetermined set of questions, where each question has a predetermined set of answers.
Something like this:
#form
<%= simple_form_for Submit.new, url: "/questionnaire" do |f| %>
<% #questions.each do |question| %>
<%= f.association :answers, collection: question.answers %>
<% end %>
<%= f.submit :done %>
<% end %>
#controller
def create
#submit = Submit.new(submit_params)
#submit.user = current_user
if #submit.save
redirect_to root_path
else
render :new
end
end
def submit_params
params.require(:submit).permit(answer_ids: [])
end
When I submit the form, Rails creates the join table, SubmitAnswers, automatically.
So here is the crux of the matter: Whats the easiest way to re-render the form, errors and all, if not all questions have been answered, ie if #submit.answers.length != #question.length ?
I can add a custom error with errors.add(:answers, 'error here'), but when I re-render, the correctly selected answers arent repopulated, which is suboptimal.
For completions sacke, here are my models:
class Submit < ApplicationRecord
belongs_to :user
has_many :submit_answers
has_many :answers, through: :submit_answers
end
class SubmitAnswer < ApplicationRecord
belongs_to :submit
belongs_to :answer
end
class Answer < ApplicationRecord
has_many :submit_answers
has_many :submits, through: :submit_answers
end
Alright, after some digging we did find the answer to make the form work, albeit with more pain that we anticipated a simple many-to-many should take.
#model
class Submit < ApplicationRecord
belongs_to :user
has_many :submit_answers
has_many :answers, through: :submit_answers
accepts_nested_attributes_for :submit_answers
end
#controller
def new
#submit = Submit.new
#questions.count.times { #submit.submit_answers.build }
end
def create
#submit = Submit.new(submit_params)
#submit.user = current_user
if #submit.save
redirect_to root_path
else
render :home
end
end
def submit_params
params.require(:submit).permit(submit_answers_attributes:[:answer_id])
end
#form
<%= simple_form_for #submit do |f| %>
<%= f.simple_fields_for :submit_answers do |sa| %>
<%= sa.input :answer_id, collection: #answers[sa.options[:child_index]], input_html: { class: "#{'is-invalid' if sa.object.errors.any?}"}, label: #questions[sa.options[:child_index]].name %>
<div class="invalid-feedback d-block">
<ul>
<% sa.object.errors.full_messages.each do |msg| %>
<li> <%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= f.submit :done %>
<% end %>
The solution is to use simple_fields_for/fields_for. Note that <%= sa.input :answer_id %> must be :answer_id, not :answer, which is something I had tried before.
Also one must allow accepts_nested_attributes_for :submit_answers, where :submit_answers is the join_table.
I prebuild my SubmitAnswers like so: #questions.count.times { #submit.submit_answers.build } which generates an input field for each question, all of which get saved on the form submit, a la build.
For the strong_params one needs to permit the incoming ids:
params.require(:submit).permit(submit_answers_attributes:[:answer_id]), so in this case submit_answers_attributes:[:answer_id].
For anyone wondering what the params look like:
{"authenticity_token"=>"[FILTERED]",
"submit"=>
{"submit_answers_attributes"=>
{"0"=>{"answer_id"=>""}, "1"=>{"answer_id"=>""}, "2"=>{"answer_id"=>""}, "3"=>{"answer_id"=>""}, "4"=>{"answer_id"=>""}, "5"=>{"answer_id"=>""}, "6"=>{"answer_id"=>""}}},
"commit"=>"done"}
As for the errors, im sure there might be a better way, but for now I have just manually added them with input_html: { class: "#{'is-invalid' if sa.object.errors.any?}"}.
On a final note, the sa.object # => SubmitAnswer allows me to retrieve the Model, the errors of that Model or whatever else one might want.

Associate many records to one with has_many_through

I'm trying to associate several dinners to a meal with a has_many: through relationship when the user hits "save". My question is not with the mechanics of has_many: through. I know how to set that up and I have it working in the Rails console, but I just don't know how to set up the view to associate several records at once.
I have models set up like this:
class Dinner < ApplicationRecord
has_one :user
has_many :meals
has_many :meal_plans, through: :meals
end
class MealPlan < ApplicationRecord
belongs_to :user
has_many :meals
has_many :dinners, through: :meals
end
class Meal < ApplicationRecord
belongs_to :dinner
belongs_to :meal_plan
end
With a meal plan controller:
def create
#meal_plan = current_user.meal_plans.build(meal_plan_params)
respond_to do |format|
if #meal_plan.save
format.html { redirect_to root_path, notice: 'Dinner was successfully created.' }
end
end
end
private
def meal_plan_params
params.require(:meal_plan).permit(dinners: [])
end
My question is about the view, in the new view, I create a #meal_plan and I want to pass several different dinners into the meal plan. Below the value: #dinners is just 7 random dinners pulled from the Dinners table.
<%= form_with model: #meal_plan do |f| %>
<%= f.hidden_field(:dinners, value: #dinners)%>
<%= f.submit 'Save'%>
<% end %>
Again, I've gotten this to work by running something like `usr.meal_plans.create(dinners: [d1, d2])`` in the Rails console but I don't
You can use the form option helpers to generate selects or checkboxes:
<%= form_with model: #meal_plan do |f| %>
<%= f.collection_select :dinner_ids, Dinner.all, :id, :name, multiple: true %>
<%= f.collection_checkboxes :dinner_ids, Dinner.all, :id, :name %>
<%= f.submit 'Save'%>
<% end %>
_ids is a special setter / getter generated by ActiveRecord for has many assocations. You pass an array of ids and AR will take care of inserting/removing the join table rows (meals).
You also need to change the name in your params whitelist:
def meal_plan_params
params.require(:meal_plan).permit(dinner_ids: [])
end
If you want to to pass an array through hidden inputs you can do it like so:
<% #dinners.each do |dinner| >
<%= hidden_field_tag "meal_plans[dinner_ids][]", dinner.id %>
<% end %>
See Pass arrays & objects via querystring the Rack/Rails way for an explaination of how this works.

Rails 5.1 add a specific column as foreign key

I have a issue with foreign keys.
I have 3 tables : Users, Members and Groups.
Basically I want the user to be able to create groups. The user provide a name, a description and on create a token is generated. Then other users can join this group via the members table. The members table has user_id and group_id. User can join the group providing the group_id which is not very cool, instead I would like the users to join the group providing the token.
Everything is working except the fact that they can join with the token. My question is how can I use the token instead of the group_id ?
I already tried to add foreign keys like this but nothing is going on in the schema :
add_foreign_key :members, :groups, column: :group_id, primary_key: :auth_token
add_foreign_key :members, :groups, column: :auth_token, primary_key: :auth_token
I also tried to add foreign key in my model :
belongs_to :group, foreign_key: "group_auth_token"
has_many :members, foreign_key: "group_auth_token", class_name: "Group"
Here are my actual models in case :
Group.rb
class Group < ApplicationRecord
has_secure_token :auth_token
has_many :members, :dependent => :destroy
has_many :users, :through => :members
end
User.rb
class User < ApplicationRecord
has_secure_password
has_many :members, :dependent => :destroy
has_many :groups, :through => :members
end
Member.rb
class Member < ApplicationRecord
belongs_to :user
belongs_to :group
validates :user_id, :presence => true
validates :group_id, :presence => true
validates :user_id, :uniqueness => {:scope => [:user_id, :group_id]}
end
I'm really confused with all this, I'm pretty sure the answer is obvious but I'm just lost, anybody to help me out ?
Thanks
EDIT : It looks like it's not clear enough so I'm adding a few things. When the user create the group this is what's happening in my controller :
def create
#group = Group.new group_params
#group.owner_id = current_user.id if current_user
if #group.save
#user = current_user.id if current_user
Member.create(group_id: "#{#group.id}", user_id: "#{#user}")
flash[:success] = "The Group has been created."
redirect_to group_path(#group)
else
render 'new'
end
end
The Member.create is to add the User automatically in the group. If anybody want's to join a group this is what is going on in my member controller :
def create
#member = Member.new member_params
#member.user_id = current_user.id if current_user
if #member.save
flash[:success] = "You Joined the Group."
redirect_to mygroups_path
else
flash.now[:error] = "Something went wrong. Are you in the group already ?"
render 'new'
end
end
And of course there is a form to create a new member (join the group) where the user provide the group_id (member_params) in the controller.
I want to be able to use the token instead of the group_id.
EDIT 2 :
So I have a error with your code jvillian. I paste it in the MembersController. In my form I changed the :group_id by :auth_token and this is my form :
<%= form_for #member do |f| %>
<%= f.label :auth_token %>
<%= f.text_field :auth_token %>
<%= f.submit "Join the Group" %>
<% end %>
This code may not be exactly correct, but it should be in the right direction.
def create
if #group = Group.find_by(auth_token: params[:auth_token]) && current_user
if #group.users << current_user
flash[:success] = "You Joined the Group."
redirect_to mygroups_path
else
flash.now[:error] = "Something went wrong. Are you in the group already ?"
render 'new'
end
end
end
I put in:
params[:token]
Naturally, that may not be quite right. Adjust to fit your own params.
This bit:
if #group.users << current_user
may not be correct as I forget what the shovel operator returns. But, you can look that up in the guide.
TO CORRECT YOUR FORM
Presumably, you're doing something like:
class MembersController < ApplicationController
def new
#member = Member.new
end
...
end
So, when you do this:
<%= form_for #member do |f| %>
<%= f.label :auth_token %>
<%= f.text_field :auth_token %>
<%= f.submit "Join the Group" %>
<% end %>
form_for can access your #member instance.
BUT, when you do this:
<%= f.text_field :auth_token %>
You're getting an error because form_for is trying to access the .auth_token attribute on #member which, naturally, doesn't exist. But remember, all you're trying to do here is submit a form that has a token. So, instead, do something like:
<%= form_for #member do |f| %>
<%= label_tag 'auth_token', 'Token' %>
<%= text_field_tag 'auth_token' %>
<%= f.submit "Join the Group" %>
<% end %>
In this way, the auth_token field is no longer directly derived from #member and you're no longer trying to access a non-existent method on #member.
Your params will look something like:
Parameters: {"auth_token"=>"whatever the user entered"}
If you want the auth_token to be nested inside group, like this:
Parameters: {"group"=>{"auth_token"=>"whatever the user entered"}}
Then do:
<%= form_for #member do |f| %>
<%= label_tag 'group[auth_token]', 'Token' %>
<%= text_field_tag 'group[auth_token]' %>
<%= f.submit "Join the Group" %>
<% end %>
Your text is abit confusing, but maybe this setup of the references are what you are looking for?
add_reference :members, :group_auth_token, foreign_key: { to_table: :groups }, references: :auth_token

How do I save this nested form in Rails 4 with a has many through association?

The problem that I have here is that I have a nested form that won't save to the database and I'm suspecting it's because the proper attributes aren't being passed into the form prior to being saved. Right now I'm trying to pass these values through hidden fields but I'm thinking there's probably a more "Railsy" way to do this. This is the form that I have created to do this:
<%= form_for #topic do |f| %>
<%= render "shared/error_messages", object: f.object %>
<%= f.fields_for :subject do |s| %>
<%= s.label :name, "Subject" %>
<%= collection_select :subject, :id, Subject.all, :id, :name, {prompt:"Select a subject"} %>
<% end %>
<%= f.label :name, "Topic" %>
<%= f.text_field :name %>
<div class="text-center"><%= f.submit class: "button radius" %></div>
<% end %>
This form generates a params hash that looks like this:
{"utf8"=>"✓", "authenticity_token"=>"PdxVyZa3X7Sc6mjjQy1at/Ri7NpR4IPUzW09Fs8I710=", "subject"=>{"id"=>"5"}, "topic"=>{"name"=>"Ruby"}, "commit"=>"Create Topic", "action"=>"create", "controller"=>"topics"}
This my model for user.rb:
class User < ActiveRecord::Base
has_many :topics
has_many :subjects, through: :topics
end
In my subject.rb file:
class Subject < ActiveRecord::Base
has_many :topics
has_many :users, through: :topics, dependent: :destroy
validates :name, presence: true
end
In my topic.rb file:
class Topic < ActiveRecord::Base
belongs_to :subject
belongs_to :user
accepts_nested_attributes_for :subject
validates :name, presence: true
end
class TopicsController < ApplicationController
before_filter :require_login
def new
#topic = Topic.new
#topic.build_subject
end
def create
#topic = Topic.new(topic_params)
#topic.user_id = current_user.id
#topic.subject_id = params[:subject][:id]
if #topic.save
flash[:success] = "Success!"
render :new
else
flash[:error] = "Error!"
render :new
end
end
private
def topic_params
params.require(:topic).permit(:name,:subject_id,:user_id, subjects_attributes: [:name])
end
end
So I'm getting closer to having a successful form submission! I placed the method accepts_nested_attributes_for in the join model, which in this case is in topic.rb. I don't really know why this works but I'm thinking it allows Rails to properly set the ":user_id" and ":subject_id" compared to placing accepts_nested_attributes_for on a model containing the "has_many through" relationship. I saw it on this post btw: http://makandracards.com/makandra/1346-popular-mistakes-when-using-nested-forms
NOW, I still have a problem where the ":subject_id" isn't being properly saved into the database. Would I have to pass in a hidden field to do this or would I have to do something else like nest my routes?
Wow that took forever to figure this one out. Since I have a has_many through relationship and I'm trying to created a nested form involving one of these models the problem I was having was I was placing the "accepts_nested_attributes_for" in the wrong model I was placing it in the has_many through model in the subject.rb file when it should have been placed in the model responsible for the join between two tables.
Also I made a super idiotic mistake on this line when I was trying to save the ":subject_id". I was writing this: #topic.subject_id = params[:subject_id][:id] instead of something like this:
#topic.subject_id = params[:subject][:id]
It was a really dumb mistake (probably because I was copying a pasting code from another controller haha)
Anyways I hope others can learn from my mistake if they ever want to do a nested form on models with a "has_many through" relationship, in certain cases the "accepts_nested_attributes_for" method will go on the JOIN table and NOT on the model with the "has_many through" relationship

Nested form mass assignment error with a has many through relationship

I'm trying to use a nested form at the moment to add category tags to a song as you create the song. At the moment it's throwing a mass assignment error every time I submit the form despite the fact that I've put in what I believe to be the correct attribute accessible characteristics. Obviously I've gone wrong somewhere so it'd be great if someone could point that out for me.
The form is:
<%= nested_form_for(#song) do |f| %>
...
<%= f.fields_for(#song.categorizations.build) do |cat| %>
<%= cat.label :category_id, "TAG" %>
<%= cat.select :category_id, options_for_select(Category.all.collect {|c| [ c.tag, c.id ] },
{ :include_blank => true }), prompt: "" %>
<%end%>
<%= f.submit "Save" %>
<% end %>
The relevant model here is:
class Song < ActiveRecord::Base
attr_accessible :artist, :title, :categorizations_attributes
has_many :categorizations, dependent: :destroy
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categorizations
The song controller looks like this:
def create
#song = Song.new(params[:song])
if #song.save
flash[:success] = "Song successfully added to library"
redirect_to #song
else
#FAIL!
render 'new'
end
end
Finally the error being raised is:
ActiveModel::MassAssignmentSecurity::Error in SongsController#create
Can't mass-assign protected attributes: categorization
Thank you in advance for any help!

Resources