Double nested association w/ Cocoon and Polymorphism gets rejected - ruby-on-rails

So, I have a model Label which is polymorphic and another model Stuff which is not. A stuff can have many labels (may also called groups), and each and every one of them can also have a label. I am working with the Cocoon gem to try to add these resource on to a single edit/new form (the Stuff one). The problem is that when I try to update the stuff with a new group (with new labels in it), it says this Labels labels labelable must exist. I think that that's an error given because the first label(group) is not yet saved to the database, so it can't give his nested label an id. (not sure though)
Also, is this the best way to do that? I mean, I made the label polymorphic only because I needed to save a only a string, and it would be unpractical and it would have taken database storage for nothing...
Enough talking, here's my code:
<div class="form-group">
<label>Groups:</label>
<div id="group" class="col-md-12 p-0 pl-md-3">
<%= form.fields_for :labels do |groupForm| %>
<%= render 'group_fields', f: groupForm %>
<% end %>
<div class="text-center">
<%= link_to_add_association 'Add Group', form, :labels, partial: 'group_fields', class: 'btn btn-success',
wrap_object: Proc.new { |group| group.labels.build; group } %>
</div>
</div>
</div>
that's in my _form.html.erb
<div class="nested-fields">
<div class="field form-group row">
<%= f.label :name, class: 'col-sm-1 col-form-label' %>
<div class="col-sm-10">
<%= f.text_field :name, class: 'form-control ml-2' %>
</div>
<div class="col-sm-1 p-0">
<%= link_to_remove_association "×".html_safe, f, class: 'badge badge-pill badge-danger mt-2' %>
</div>
<div class="col-12">
<div id="label" class="col-md-12 p-0 pl-md-5 pt-2">
<%= f.fields_for :labels do |labelForm| %>
<%= render 'label_fields', f: labelForm %>
<% end %>
<div class="text-center">
<%= link_to_add_association 'Add Label', f, :labels, class: 'btn btn-success' %>
</div>
</div>
</div>
</div>
</div>
that's in _group_fields.html.erb
<div class="nested-fields">
<div class="field form-group row mb-2">
<%= f.label :name, class: 'col-sm-1 col-form-label' %>
<div class="col-sm-10">
<%= f.text_field :name, class: 'form-control ml-2' %>
</div>
<div class="col-sm-1 p-0">
<%= link_to_remove_association "×".html_safe, f, class: 'badge badge-pill badge-danger mt-2' %>
</div>
</div>
</div>
and that's in my _label_fields.html.erb
class Label < ApplicationRecord
belongs_to :labelable, polymorphic: true
has_many :labels, as: :labelable, dependent: :destroy
accepts_nested_attributes_for :labels, allow_destroy: true, reject_if: proc { |att| att[:name].blank? }
end
this is my Label model
class Stuff< ApplicationRecord
has_many :labels, as: :labelable, dependent: :destroy
accepts_nested_attributes_for :labels, allow_destroy: true, reject_if: proc { |att| att[:name].blank? }
end
and this is my Stuff model
I forgot to mention that if I add only the first "layer" of label (group) without writing anything on the labels (2nd "layer") and I submit the form (which I can do and it updates the database as well) when I come back and edit I can actually modify the 2nd "layer" without any problems.

A belongs_to relation-ship is by default "required". This means it has to be filled in upon saving. However, when saving an item with nested childs, no id's are yet known and while obvious, rails has no way of knowing the belongs-to association will be set when saving them together.
There are two ways to handle this:
make the belongs_to optional, this will skip the validation, and the save will work. Something like belongs_to :labelable, polymorphic: true, optional: true
better, declare the inverse_of so rails knows when saving the labels it corresponds to the labelable relationship.
Like so:
has_many :labels, as: :labelable, dependent: :destroy, inverse_of: :labelable
You can declare the inverse_of on any assocation, so also on the belongs_to, but in most it suffices to just declare it on the has_many. It might not be enough in your specific scenario.

Related

Rails rendering nested comments use eager loading

I wanted to be able to allow users to add comments to recipes. I also wanted users to be able to comment on those comments. It appears to be working, but maybe there are too many queries going on. I have researched and seen a lot of closely related articles, but none seem to be helping the issue. No matter how I change it up, it doesn't work. I can make as many comments on comments as I want, but as soon as there are multiple comments to a recipe it crashes. It's like I can have one or the other, but not both or it will crash. Here is what I have so far:
comment.rb
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
has_many :comments, as: :commentable
end
recipe.rb
class Recipe < ApplicationRecord
has_many :comments, as: :commentable
recipes/show.html.erb
<div class="">
<h5>Comments:</h5>
<div class="comment-form">
<hr />
<h3 class="subtitle is-3">Leave a reply</h3>
<%= render #recipe.comments %>
</div>
<%= simple_form_for([#recipe, #recipe.comments.build]) do |f| %>
<div class="field">
<div class="control">
<%= f.input :content, input_html: { class: 'input' }, wrapper: false, label_html: { class: 'label' } %>
</div>
</div>
<%= f.button :submit, 'Leave a reply', class: "button is-primary" %>
<% end %>
</div>
_comments.html.erb
<div class="box">
<article class="media">
<div class="media-content">
<div class="content">
<p>
<strong><%= comment.content %></strong>
</p>
</div>
</div>
</article>
</div>
<div>
<div class="">
<%= form_for([comment, comment.comments.build]) do |f| %>
<%= f.hidden_field :recipe_id, value: #recipe.id %>
<%= f.text_area :content, placeholder: "Add a Reply" %><br/>
<%= f.submit "Reply" %>
<% end %>
</div>
<div>
<%= render comment.comments %>
</div>
</div>
If I remove <%= render comment.comments %> It works, but obviously will not show me the comments on other comments. If I make only 1 comment on a recipe, I can comment on that comment as many times as I want with no issues. If I add just one more comment on the recipe, it crashes. If I pry in, it works and shows every comment is there until it is done going through each comment, then crashes. I know there are gems for this, but I am learning and really wanted to build from scratch and understand what is going on. Thanks in advance!
I think if you made a self referential relationship on your Comment model it would work. You'd need to add a parent_id or whatever you want to call it. You can keep the polymorphic owner in case you have other models that will be able to have comments.
# Migration
def change
add_column :comments, :parent_id, :integer, foreign_key: true
add_index :comments, :parent_id
end
And then in your Comment model:
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
belongs_to :parent, class_name: 'Comment', inverse_of: :replies
has_many :replies, foreign_key: :parent_id, class_name: 'Comment', inverse_of: :parent
end
If you're using Rails 5, you'd need to add optional: true to the belongs_to :parent relationship.

Unpermitted parameters: offices in a has_many :through association in Rails 5

I have a has_many through association. When I go to the Store form I need to save Owner data in the same form. But I keep getting Unpermitted parameters: :offices. I tried with inverse_of as well. I tried changing the models structures like trying to accept attributes for all models.
Office Model:
class Office < ApplicationRecord
has_many :owner_offices, :dependent => :destroy
has_many :owners, through: :owner_offices
accepts_nested_attributes_for :owner_offices
#accepts_nested_attributes_for :offices
end
Owner Model:
class Owner < ApplicationRecord
has_many :owner_offices
has_many :offices, through: :owner_offices
accepts_nested_attributes_for :owner_offices
end
Owner_Office Model:
class OwnerOffice < ApplicationRecord
belongs_to :office
belongs_to :owner
accepts_nested_attributes_for :owner
end
Office Controller:
def new
#office = Office.new
#office.owners.build
end
def office_params
params.require(:office).permit(:office_name, :office_slug, :office_email, :phone, :office_type, :status, :mf_member, :comment,
:owners_attributes => [:office_id, :owner_id, :first_name, :last_name, :owner_email])
end
Office Form:
<%= form_with(model: office, local: true, html: {class: "form-office"}) do |form| %>
<div class="row">
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="form-group">
<span><%= form.label :office_email %></span>
<%= form.text_field :office_email, class: 'form-control' %>
</div>
</div>
</div>
<hr>
<h3>Owner Information</h3>
<hr>
<%= form.fields_for :owners do |owner_form| %>
<div class="row">
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="form-group">
<span><%= owner_form.label :first_name %></span>
<%= owner_form.text_field :first_name, class: 'form-control' %>
</div>
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="form-group">
<span><%= owner_form.label :last_name %></span>
<%= owner_form.text_field :last_name, class: 'form-control' %>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 col-sm-6 col-xs-12">
<div class="form-group">
<span><%= owner_form.label :owner_email %></span>
<%= owner_form.text_field :username, class: 'form-control' %>
</div>
</div>
</div>
<% end %>
<% end %>
I just placed most of the part where I'm calling the fields_for for owner. So I'm stuck, not sure what I'm missing at the moment and I've been checking other resources as well, implementing a different logic.
Also, I do not want to use cocoon because is important to me to learn how to implement from scratch.
Thanks in advance.

Rails - Cocoon/simple form nested fields not showing

I'm trying to create nested fields in the edit/create section of a recipe book for ingredients, directions, and utensils, but only the outline of the fields show up, without any of the nested bits.
Models
class Recipe < ApplicationRecord
has_many :ingredients
has_many :directions
has_many :utensils
accepts_nested_attributes_for :ingredients, reject_if: proc { |attributes|
attributes['name'].blank? }, allow_destroy: true
accepts_nested_attributes_for :directions, reject_if: proc { |attributes|
attributes['step'].blank? }, allow_destroy: true
accepts_nested_attributes_for :utensils, reject_if: proc { |attributes|
attributes['name'].blank? }, allow_destroy: true
end
class Ingredient < ApplicationRecord
belongs_to :recipe
end
class Direction < ApplicationRecord
belongs_to :recipe
end
class Utensil < ApplicationRecord
belongs_to :recipe
end
Recipes Controller (params)
def recipe_params
params.require(:recipe).permit(:title, :description, :image,
ingredients_attributes: [:id, :name, :_destroy], directions_attributes:
[:id, :step, :_destroy], utensils_attributes: [:id, :name, :_destroy])
end
View
<div class="row">
<div class="col-md-4">
<h3>Ingredients</h3>
<div id="ingredients">
<%= f.simple_fields_for :ingredients do |ingredient| %>
<%= render "ingredients_fields", f: ingredient %>
<div class="links">
<%= link_to_add_association "Add Ingredient", f, :ingredients, class: "btn btn-default add-button" %>
</div>
<% end %>
</div>
</div>
<div class="col-md-4">
<h3>Directions</h3>
<div id="directions">
<%= f.simple_fields_for :directions do |direction| %>
<%= render "directions_fields", f: direction %>
<div class="links">
<%= link_to_add_association "Add Step", f, :directions, class: "btn btn-default add-button" %>
</div>
<% end %>
</div>
</div>
<div class="col-md-4">
<h3>Utensils</h3>
<div id="utensils">
<%= f.simple_fields_for :utensils do |utensil| %>
<%= render "utensils_fields", f: utensil %>
<div class="links">
<%= link_to_add_association "Add Utensil", f, :utensils, class: "btn btn-default add-button" %>
</div>
<% end %>
</div>
</div>
</div>
<%= f.button :submit, class: "btn btn-primary" %>
</div>
Partial (for directions)
<div class="form-inline.clearfix">
<div class="nested-fields">
<%= f.input :step, input_html: { class: "form-input form-control" } %>
<%= link_to_remove_association "Remove", f, class: "form-button btn btn-default" %>
</div>
</div>
The outline for utensils also isn't showing up and I'm not sure why.
Here's what it looks like: nested forms not showing
Any help would be appreciated, thank you!
Are there any nested items?
Because if there are not it would explain the behaviour: your link_to_add_association is inside the simple_fields_for loop and so it will be shown for each nested item, but it will also never be shown if there are none (note: this could be desired behaviour, but I am guessing in most cases it is not).
All examples on the cocoon documentation place the link_to_add_association outside the simple_fields_for loop. If you have trouble reading haml, check the ERB examples.

How can I avoid ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection errors in my setup?

I have the following models in my rails app:
class SaleContact < ActiveRecord::Base
validates :key_contact_id, presence: true, uniqueness: { scope: :sales_opportunity_id, message: "Contact already added!" }
validates :sales_opportunity_id, presence: true
belongs_to :key_contact, inverse_of: :sale_contacts
belongs_to :sales_opportunity, inverse_of: :sale_contacts
has_many :phone_numbers, :through => :key_contact
accepts_nested_attributes_for :phone_numbers
end
I'm trying to create a sale_contact from the sales_opportunity screen by selecting a key_contact (assume a key_contact already exists). I would also like the ability to add a phone_number at the same time using fields_for and nested attributes.
class KeyContact < ActiveRecord::Base
validates :first_name, :last_name, :company_id, presence: true
has_many :phone_numbers, dependent: :destroy
belongs_to :company
has_many :sales_opportunities, :through => :sale_contacts
has_many :sale_contacts, dependent: :destroy
end
Assume I've already created the key_contact and assigned it to the company that owns it.
class PhoneNumber < ActiveRecord::Base
validates :number, :key_contact_id, presence: true
belongs_to :key_contact
end
Nothing magical here - just a very basic model for a phone number.
On the sales_opportunity page I can load a modal that adds a new sale_contact. It's a bootstrap modal loaded by AJAX, but I don't think that matters much (I've only included the form parts for brevity):
<%= form_for(#sale_contact, :html => {role: :form, 'data-model' => 'sale_contact'}, remote: true) do |f| %>
<% if #sale_contact.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#sale_contact.errors.count, "error") %> prohibited this sale_contact from being saved:</h2>
<ul>
<% #sale_contact.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-group" id= "contact_error">
<%= f.label :key_contact_id, :class => "control-label" %>
<div id="contact_select">
<%= f.collection_select :key_contact_id, #sales_opportunity.company.key_contacts(:full_name), :id, :full_name %>
</div>
<span class="warning-block"></span>
<span class="help-block"></span>
</div>
<div class="form-group">
<%= f.label :role, :class => "control-label" %>
</br>
<%= f.select(:role, options_for_select(#roles.collect { |r| [r[0].humanize, r[0]] }, selected: #sale_contact.role), {}) %>
<span class="help-block"></span>
</div>
<div class="form-group">
<%= f.label :preference, :class => "control-label" %>
</br>
<%= f.select(:preference, options_for_select(#preferences.collect { |r| [r[0].humanize, r[0]] }, selected: #sale_contact.preference), {}) %>
<span class="help-block"></span>
</div>
<%= f.fields_for(:phone_numbers) do |phone| %>
<div class="form-group">
<%= phone.label :number, "Phone Number", :class => "control-label" %>
</br>
<%= phone.text_field :number, :placeholder => "Enter phone number (optional)" %>
<span class="help-block"></span>
</div>
<div>
<%= phone.hidden_field :key_contact_id %>
</div>
<% end %>
<div class="form-group">
<%= f.hidden_field :sales_opportunity_id, :value => #sales_opportunity.id %>
</div>
<%= f.submit "Save", class: "btn btn-large btn-success", data: { disable_with: "Submitting..." }%>
<% end %>
And from my sale_contact_controller new action:
def new
#sale_contact = SaleContact.new
#sale_contact.phone_numbers.build
#sales_opportunity = SalesOpportunity.find(params[:sales_opportunity_id])
#company = #sales_opportunity.company
#roles = SaleContact.roles
#preferences = SaleContact.preferences
render :modal_form
end
def sale_contact_params
params.require(:sale_contact).permit(:key_contact_id, :sales_opportunity_id, :role, :preference, phone_numbers_attributes: [:number, :id])
end
I run a javascript snippet when the modal is being retrieved via AJAX or the key_contact select dropdown changes to bring in the key_contact_id to the phone_numbers_attributes; if I don't do that I get a 422 unprocessable entity error for not sending through my key_contact_id.
Using this setup the modal pops into view and has the correct fields (both for sale_contact and phone_number), but will not save either of these models - I get a 500 error:
Completed 500 Internal Server Error in 17ms
ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection (Cannot modify association 'SaleContact#phone_numbers' because the source reflection class 'PhoneNumber' is associated to 'KeyContact' via :has_many.):
app/controllers/sale_contacts_controller.rb:50:in `block in create'
app/controllers/sale_contacts_controller.rb:49:in `create'
I've tried other methods, such as setting the key_contact_id in the sale_contact create method, but I get the same result. I can't work out why this setup complains when I don't pass a key_contact_id (422 error) and complains when I do (500 internal server error as the key_contact_id is being set internally by the rails associations).
What does Rails want me to do here?
I had a similar issue with my form, but I was able to get passed it by using nested field_for tags
Proposal.rb
has_many :proposal_section_reviews, :through => :proposal_review_status
has_one :proposal_review_status, :dependent => :destroy
attr_accessible :proposal_review_status_attributes, :proposal_review_status_attributes, :proposal_section_reviews_attributes
accepts_nested_attributes_for :proposal_review_status, :proposal_section_reviews
ProposalSectionReview.rb
belongs_to :proposal_review_status
ProposalReviewStatus.rb
has_many :proposal_section_reviews, :dependent => :destroy
_form.html.haml
= form_for proposal do |f|
= f.fields_for :proposal_review_status do |ff|
= ff.fields_for :proposal_section_reviews
So in your instance I would try
<%= f.fields_for(:key_contact) do |key| %>
<%= key.fields_for(:phone_numbers) do |phone| %>
<div class="form-group">
<%= phone.label :number, "Phone Number", :class => "control-label" %>
</br>
<%= phone.text_field :number, :placeholder => "Enter phone number (optional)" %>
<span class="help-block"></span>
</div>
<div>
<%= phone.hidden_field :key_contact_id %>
</div>
<% end %>
<% end %>
Hope this helps

Works for 'edit', fails for 'new'... undefined method `association' for #<ActionView::Helpers::FormBuilder:

Following the railscast #196 on nested forms... I have the following models:
class DealContact < ActiveRecord::Base
belongs_to :deal
belongs_to :contact
class Contact < ActiveRecord::Base
has_many :deal_contacts
has_many :deals, through: :deal_contacts
accepts_nested_attributes_for :deal_contacts, :allow_destroy => true
class Deal < ActiveRecord::Base
has_many :deal_contacts
has_many :contacts, through: :deal_contacts
accepts_nested_attributes_for :deal_contacts, :allow_destroy => true
In my deals form I have this
<div class="row">
<div class="span12"><h4>Contacts Associated with this Deal</h4></div>
<%= f.fields_for :deal_contacts do |builder| %>
<%= render 'deal_contact_fields', f: builder %>
<% end %>
<div class="span1"><%= link_to_add_contact "Add", f, :deal_contacts %></div>
</div>
</div>
And deal_contact_fields just contains:
<fieldset>
<div class="span4">
<%= f.association :contact, collection: Contact.all(order: 'contact_name'), label_method: :full_desc %>
</div>
<div class="span6">
<%= f.label :details, "Details " %>
<%= f.text_field :details %>
</div>
<div class="span1" style="margin-top: 30px">
<%= f.hidden_field :_destroy %>
<%= link_to "remove", '#', class: "remove_fields" %>
</div>
</fieldset>
This all works great for editing existing deals, however when I create a new one I get the following error:
undefined method `association' for #<ActionView::Helpers::FormBuilder:0x007fe6fba55840>
:-(
.association is only available in simple_form gem
You'll have to use collection_select to achieve what you want without that gem:
<%= f.collection_select :contact, Contact.all(order: 'contact_name'), :id, :name %>

Resources