Rails has_many form optionally create multiple records - ruby-on-rails

I have a Profile model and I'm trying to see if that profile has used any other names in the past. I came up with adding a AdditionalName model, and the association is Profile has_many AdditionalName. In the form, I would like for them to fill out up to 5 additional names. I saw from this question I can build it x.times and use f.fields_for :additional_names to show 5 additional names. Problem is, it will save all 5 when I submit the form, even if they don't fill anything. Is it possible that the AdditionalName records only save when they fill out something in the input field?
additional_name.rb
class AdditionalName < ApplicationRecord
belongs_to :profile
end
profile.rb
class Profile < ApplicationRecord
has_many :additional_names
accepts_nested_attributes_for :additional_names
end
controller:
def new
existing_additional_names = #profile.additional_names.count
(5 - existing_additional_names).times do
#current_object.additional_names.build
end
form:
<%= form_with model: #profile, url: wizard_path, method: :patch, local: true do |f| %>
<div class="form-row">
<%= f.fields_for :additional_names do |additional_names_form| %>
<%= additional_names_form.hidden_field :id %>
<div class="form-group col-md-6">
<div class="mx-auto">
<%= additional_names_form.label :name, "Full name" %>
<%= additional_names_form.text_field :name, class: "form-control", id: "ignore-button-disable" %>
</div>
</div>
<% end %>
</div>
<div class="row">
<div class="col-md-6 my-2">
<%= f.submit 'Continue', class: 'btn btn-primary w-100' %>
</div>
</div>
<% end %>

You don't need a custom logic if you just want the nested records to be created only if a certain attribute is set. You can pass option :reject_if to accepts_nested_attributes_for call in the model with a lambda to handle it.
From the definition:
Allows you to specify a Proc or a Symbol pointing to a method that checks whether a record should be built for a certain attribute hash.
accepts_nested_attributes_for :additional_names,
reject_if: -> { |attributes| attributes['name'].blank? }

I just needed to overwrite additional_names_attributes= in the model:
def additional_names_attributes=(additional_name_attributes)
additional_name_attributes.values.each do |additional_name_attribute|
unless additional_name_attribute[:name].empty?
additional_name = AdditionalName.find_or_create_by(additional_name_attribute)
self.additional_names << additional_name
end
end
end

Related

Strong params issue in rails 6

I have a many-to-many model ProductCategory product_category (joint-table) and
I'm having issue with nesting the parameter in the ProductsController. The error I keep getting is that its unpermitted params category_ids but I have nested it in the strong product params.
I took a picture of the important parts of the code. Please take a look and let me know thank you. Here is the most important part of the code I think:
<%= form_with(model: [:user, #product], local: true) do |f|%>
<h4>Category</h4>
<div class="dropdown-trigger btn">
<%= f.collection_select(:category_ids, Category.all, :id, :name) %>
</div>
<h4>Product Name:</h4>
<%= f.text_field :name %><br/>
<h4>Product Price:</h4>
<%= f.number_field :price, value: #product.price ? '%.2f' % #product.price : nil, min: 0, step: 0.01 %>$<br/>
<h4>Product Description:</h4>
<%= f.text_field :description %><br/>
<h4>Product Image (recommended)</h4>
<%= f.file_field :image %><br/>
The require in ProductsController:
def product_params
params.require(:product).permit(:name, :price, :description, :image, category_ids: [])
end
And the relevant parts of Product and ProductCategory model.
class Product < ApplicationRecord
belongs_to :user
has_many :product_categories
has_many :categories, though: :product_categories
has_one_attached :image
end
class ProductCategory < ApplicationRecord
belongs_to :product
belongs_to :category
end
class Category < ApplicationRecord
belongs_to :user
has_many :product_categories
has_many :products, though: :product_categories
end
code screenshot
You are receiving an "unpermitted params category_ids" error, because you first need to declare in your Product model the following:
accepts_nested_attributes_for :categories , allow_destroy: true
Once that is done, you should start receiving all the category_ids info, really nested inside your params.
However, I fully recommend DO NOT perform on your views and partials an ActiveRecord query over your DB. For example:
<div class="dropdown-trigger btn">
<%= f.collection_select(:category_ids, Category.all, :id, :name) %>
</div>
That is not advisable. Instead, you should receive from your controller the whole set of categories. The only function on the view in this case is to fill the data by the user, to select the categories, and then after a submit to send all that information back to the controller. That's all. Not performing any kind of query. It's true that you can do it. I mean, it is physically possible to do it there on that view, or even to do it on a helper (also wrong, a helper is to perform additional actions over resources already loaded or received from controllers), but MVC means the separation of duties for several reasons.
Anyway, in your case I would choose to go more or less with something like this:
On products_controller.rb:
def edit
#categories_to_assign = product_service.get_categories_to_assign(#product)
end
def product_service
ProductService
end
def product_params
params.require(:product).permit(:name, :price, :description, :image, categories_to_assign: [])
end
On product_service.rb it gets the categories:
def self.get_categories_to_assign(product)
categories_scope.where.not(id: product.categories.map(&:id)).map do |category|
["#{category.name}", category.id]
end
end
def self.categories_scope()
Category
end
Then on the edit/new view:
<%
categories_to_assign = #categories_to_assign || []
%>
<% content_for :products_main_content do %>
<div id="edit_product_content">
<%= render partial: 'products/form', locals: {
product: product,
return_to: return_to,
categories_to_assign: categories_to_assign
} %>
</div>
<% end %>
Then on the _form.html.erb partial:
<%
categories_to_assign = local_assigns.fetch(:categories_to_assign, [])
%>
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title"><%= t('products.categories.title') %></h2>
</div>
<div class="panel-body">
<div class="form-horizontal" id="categories_container" data-sjr-placeholder>
<%= render partial: 'products/categories', locals: {f: f, categories_to_assign: categories_to_assign} %>
</div>
</div>
</div>
And finally on the _categories.html.erb partial:
<%
categories_to_assign = local_assigns.fetch(:categories_to_assign, [])
%>
<% if categories_to_assign.present? %>
<%= select_tag "#{f.object_name}[categories_to_assign][]", options_for_select(categories_to_assign), {id: "#{f.object_name}_categories_to_assign", include_blank: true, multiple: true, class: 'form-control', data: {placeholder: t('products.form.select_category')}} %>
<% end %>
As you can see, the general idea is passing the pertinent information from the controller, after been properly retrieved on the product_service (you should add it), and then it goes to the edit/new view and then it finally goes down into the nested partials. That way everything is separated in its respective areas of responsibilities.

Unable to add rows to join table in rails 5 app

I have three tables - result, feedback_qs, and feedback_qs_results (join table). Given an existing result and feedback_qs, I want to populate the join table with the result_id, feedback_q_id and the subsequent answer (text_area).
I believe the relationships are correct....
# result.rb
has_many :feedback_q_results
has_many :feedback_qs, :through => :feedback_q_results
# feedback_q.rb
has_many :feedback_q_results
has_many :results, :through => :feedback_q_results
# feedback_q_result.rb
belongs_to :result
belongs_to :feedback_q
I have access to the result_id (result/result.id/feedback) via config/routes.rb below...
resources :results do
member do
get 'feedback'
post 'feedback_create'
end
end
What would this form look like to populate the join table? Something similar to this (I realize this isn't correct)...? How do I take in the data through the post_controller to populate the join table? Or should it go through another controller?
<%= form_for :result, url: feedback_create_result_path(result), method: :post do |form| %>
<% for q in FeedbackQ.all %>
<div class="col field">
<p>
<%= form.label q.question_text %>
</p>
<%= text_area_tag id: q.id %>
</div>
<% end %>
<div class="actions">
<%= form.submit "Submit", class: "btn btn-primary" %>
</div>
<% end %>
What would my feedback_create method look like? While I've seen several questions regarding rails' many to many relationships and populating join tables, none of those use cases seems to suit my need. Thanks in advance.
Update - My use case is this: user takes an exam (called a result), and they then review the proctor by answering feedback questions. I want to record the feedback in the join table
If your has_many associations are working correctly, then in the rails console you should be able to do:
> result.feedback_qs
=> []
and
> feedback_q.results
=> []
Where result and feedback_q are are Result and FeedbackQ objects, respectively.
To associate a feedback_q to a result, simply:
> result.feedback_qs << feedback_q
This should create a record in the feedback_q_result table.
Now, when you do
> result.feedback_qs
It should return the feedback_q record that you just added.
I altered my original models. One issue I ran into was simple naming convention in rails, so I simplified the table names to eliminate that from the equation. New models:
# result.rb
has_many :feedbacks
has_many :questions, :through => :feedbacks
# question.rb
has_many :feedbacks
has_many :results, :through => :feedbacks
# feedback.rb
belongs_to :question
belongs_to :result
validates_uniqueness_of :question_id, scope: :result_id
form in my view:
<%= form_for :result, url: feedback_create_result_path(result), method: :post do |form| %>
<% Question.all.each do |question| %>
<div class="col field">
<p>
<%= form.label :question, question.question_text %>
</p>
<%= hidden_field_tag 'question[][id]', question.id %>
<%= text_area_tag 'question[][answer]', "", id: "question_" + question.id.to_s, class: "stretch_textarea" %>
</div>
<% end %>
<div class="actions">
<%= form.submit "Submit", class: "btn btn-primary" %>
</div>
This SO page helped quite a bit as well.

has_many through access join table attribute in form

I have the following models:
class RandomExam < ActiveRecord::Base
has_many :random_exam_sections
has_many :sections, :through => :random_exam_sections
end
class Section < ActiveRecord::Base
has_many :random_exam_sections
has_many :random_exams, :through => :random_exam_sections
class RandomExamSection < ActiveRecord::Base
belongs_to :random_exam
belongs_to :section
end
The idea is to have certain configurations to create random exams, so this tables help to select which sections do you need and then also select the number of questions per section, here are the attributes of each table:
RandomExam: name(string), created_at(datetime), updated_at(datetime)
Section: name(string), created_at(datetime), updated_at(datetime)
RandomExamSection: random_exam_id(integer), section_id(integer), questions_number(integer)
As you can see the number of questions per section attribute is inside the RandomExamSections table and I want to access it in a form that is displayed from the RandomExam controller, here is my form:
<%= form_for (#random_exam) do |f| %>
<div class="row">
<div class="input-field col s12">
<%= f.label :name, 'Name' %>
<%= f.text_field :name, placeholder: 'Enter the name of the configuration' %>
</div>
</div>
<% #sections.each do |section| %>
<div class="row <%= dom_id(section) %>">
<div class="col s4">
<%= check_box_tag 'random_exam[section_ids][]', section.id,
#random_exam.section_ids.include?(section.id), id: dom_id(section), class: "section-checkbox #{dom_id(section)}" %>
<%= label_tag dom_id(section), (raw sanitize section.name, tags: %w(h2 p strong em a br b i small u ul ol li blockquote), attributes: %w(id class href)),
class: "name #{dom_id(section)}" %>
</div class="col s4">
<div>
<%= text_field_tag "random_exam[random_questions_numbers][#{section.id}][]", nil,
:placeholder => 'Enter the number of questions' %>
</div>
</div>
<% end %>
<div class="form-group">
<%= f.submit class: "btn waves-effect waves-light green" %>
</div>
<% end %>
My controller:
def create
#random_exam = RandomExam.new(random_exam_params)
if #random_exam.save
assign_random_questions_number
flash[:success] = 'Random configuration created successfully'
redirect_to #random_exam
else
flash.now[:danger] = #random_exam.errors.full_messages.to_sentence
render 'new'
end
def assign_random_questions_number
if params[:random_exam][:'random_questions_numbers'] == nil
return
end
params[:random_exam][:'section_ids'].each do |key, value|
record = RandomExamSection.search_ids(#random_exam.id, key)
record.each do |random_exam_section_record|
number_of_questions = params[:random_exam][:'random_questions_numbers'][key].first.to_i
random_exam_section_record.update(questions_number: number_of_questions)
end
end
end
I'm getting a TypeError: TypeError: nil is not a symbol nor a string when I update the record in the method assign_random_questions_number
This error even appears when I run this on the console
random = RandomExamSection.first
random.update(questions_number: 10)
Or when I run:
random = RandomExamSection.first
random.questions_number = 10
random.save
EDIT
I ended up deleting the association in RandomExamSection and recreating it inside 'assign_random_questions_number' with the questions_number
Thanks.
If you use nested_attributes you can do something like this:
#form
<h4>Selected exams</h4>
<%= f.fields_for :random_exam_sections do |b| %>
<%= b.hidden_field :section_id %>
<%= b.label :selected, b.object.section.name %>
<%= b.check_box :selected, { checked: !b.object.id.blank? } %>
<br>
<%= b.label :question_numbers %>
<%= b.text_field :questions_number %>
<% end %>
#RandomExamModel
class RandomExam < ApplicationRecord
has_many :random_exam_sections, inverse_of: :random_exam
has_many :sections, :through => :random_exam_sections
accepts_nested_attributes_for :random_exam_sections, reject_if: :is_not_selected
private
def is_not_selected(attr)
attr["selected"] == '0'
end
end
# RandomExam
class RandomExamSection < ApplicationRecord
belongs_to :random_exam
belongs_to :section
attr_accessor :selected
end
# Controller
# GET /random_exams/new
def new
#random_exam = RandomExam.new
#random_exam.random_exam_sections.build(Section.all.map{|s| {section_id: s.id}})
end
The idea basically is
- Build on controller the random_exam_sections to be selected
- Write a form that allows to you 'select' one option and assign the number
- Then, validate if the random_exam_section of a sections was selected (this why i made that `attr_accessor :selected`, i need a place to write if user select the exam_section)
- If was selected, save.
The trick here is build on the controller, then select on the view and validate the selected on the model. Here i made an example if you need help: https://github.com/afromankenobi/nested_attr_demo
To add sections when the random_exam_sections is already created you should probably use javascript. Maybe this railscasts can help you http://railscasts.com/episodes/196-nested-model-form-part-1

Rails 4 - checkboxes for has_and_belongs_to_many association

I recently had a problem getting checkboxes to work for a has_and_belongs_to_many (HABTM) association in Rails 4. I was able to find the information on how to get it working correctly in a few disparate places, but thought it would be good to document the few simple steps necessary to get it working correctly in one place here on StackOverflow.
As a setup assume a model of Kennel with a HABTM association to Handler.
class Kennel
has_and_belongs_to_many :handlers
end
This is all you need to do for the form: Don't do it manually when there is a built in helper.
<%= form_for #kennel do |f| %>
<%= f.collection_check_boxes(:handler_ids, Handler.all, :id, :to_s) %>
<% end %>
The form should have something like this:
<%= form_for(#kennel) do |form| %>
...
<div class="field">
<div class="field_head">Handlers</div>
<%= hidden_field_tag("kennel[handler_ids][]", nil) %>
<% Handler.order(:name).each do |handler| %>
<label><%= check_box_tag("kennel[handler_ids][]", id, id.in?(#kennel.handlers.collect(&:id))) %> <%= handler.name %></label>
<% end %>
</div>
...
<% end %>
The hidden_field_tag allows the user to uncheck all the boxes and successfully remove all the associations.
The controller needs to allow the parameter through strong parameters in the permitted_params method:
params.permit(kennel: [:city, :state
{handler_ids: []},
:description, ...
])
References:
http://railscasts.com/episodes/17-habtm-checkboxes
https://coderwall.com/p/_1oejq
I implement has_and_belongs_to_many association this way:
model/role
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
end
model/user
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
end
users/_form.html.erb
---
----
-----
<div class="field">
<% for role in Role.all %>
<div>
<%= check_box_tag "user[role_ids][]", role.id, #user.roles.include?(role) %>
<%= role.name %>
</div>
<% end %>
</div>
users_controller.rb
def user_params
params.require(:user).permit(:name, :email, { role_ids:[] })
end
Intermediate table_name should be roles_users and there should be two fields:
role_id
user_id

Adding empty record for nested attribute in rails

I'm working in Rails 4/Ruby 2.0.0. I have a two models - Articles and Graphics. Articles has_many Graphics. So, in my code I am trying to add an empty record to the graphics collection on the article so that in the form, there will be an empty set of fields to let a new record be added. I cannot figure out why the fields do not show up on the form though.
I've tried multiple methods of building the graphics collection but none seem to do the trick. Surely I must be missing something insanely small.
Article.rb
class Article < ActiveRecord::Base
has_many :graphics, :dependent => :destroy, :foreign_key => 'article_id'
accepts_nested_attributes_for :graphics,
:allow_destroy => true,
:reject_if => :all_blank
end
Graphic.rb
class Graphic < ActiveRecord::Base
belongs_to :article
validates_presence_of :path, :caption
end
_form.html.erb
...
<% f.fields_for :graphics do |g| %>
<div class="clear clearfix pad-b-20">
<div class="w-1-2 left f-left">
<div class="field">
<%= g.label :path %><br>
<%= g.text_field :path %>
</div>
</div>
<div class="w-1-2 left f-left">
<div class="field">
<%= g.label :caption %><br>
<%= g.text_field :caption %>
</div>
</div>
</div>
<% end %>
...
Building it in a form helper method
articles/_form.html.erb
<%= form_for(setup_article(#article)) do |f| %>
form_helper.rb
module FormHelper
def setup_article(article)
article.graphics.build
article
end
end
Using an ActiveRecord callback
Article.rb
...
after_initialize :build_graphics
private
def build_graphics
self.graphics.build
end
Building it in the controller
ArticleController.rb
...
def new
#article = Article.new
#article.graphics.build
end
...
The problem is that both for form_for and for fields_for you need to use <%=, because they render the contents of the form.
So, to solve your problem, you need to write
...
<%= f.fields_for :graphics do |g| %>
Your content
<% end %>
...

Resources