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.
Related
I've the structure using Cocoon gem, in my app/views/order/_form.html.erb the code below work fine:
<%= f.collection_select(:drink_id, #drinks, :id, :name, :prompt => "Select a drink") %>
But when I using in _drink_fields.html.erb don't work, return this:
undefined method `drink_id' for # Drink:0x007fd30f9799f8
app/views/orders/_drink_fields.html.erb
<div class='nested-fields'>
<div class="field">
<%= f.collection_select(:drink_id, #drinks, :id, :name, prompt: 'Select a drink') %>
</div>
<%= link_to_remove_association "remove", f %>
</div>
app/views/orders/_form.html.erb
<hr>
<h3>Drinks</h3>
<div id='drinks'>
<%= f.fields_for :drinks do |drink| %>
<%= render 'drink_fields', :f => drink %>
<% end %>
<div class='links'>
<%= link_to_add_association 'add', f, :drinks %>
</div>
</div>
<hr>
app/views/models/order.rb
class Order < ApplicationRecord
belongs_to :drink, optional: true
has_many :drinks
accepts_nested_attributes_for :drinks, allow_destroy: true
end
app/controllers/orders_controller.rb
def set_order
#order = Order.find(params[:id])
end
def set_drink
#drinks = Drink.all
end
# Never trust parameters from the scary internet, only allow the white list through.
def order_params
params.require(:order).permit(:number, :drink_id, drinks_attributes: [:id, :name, :_destroy])
end
Why I received it?
How to do work fine?
Thanks guys!!
When checking your association I think you made a mistake there. I see the following
An order belongs to drink
An order has-many drinks
So then follows: A drink belongs-to order then. So a drink can be in only one order?
My guess is you are missing a join table between order and drinks. So add a simple
class OrderedDrink < ActiveRecord::Base
belongs_to :order
belongs_to :drink
end
You also need the table, so you will have to add a migration to make it (obviously).
(you could an amount there: they ordered 5 cola's for instance).
Then in Order you get
class Order
has_many :ordered_drinks, inverse_of: :order
has_many :drinks, through: :ordered_drinks
accepts_nested_attributes_for :ordered_drinks
end
In Drink you write
class Drink
has_many :ordered_drinks
has_many :orders, through: :ordered_drinks
end
(you might not need those relations, to know in how many orders a drink was/is)
And then instead of editing/managing the drinks association, you edit the ordered_drinks and your partial/collection-select will behave as expected.
The reason it doesn't work is because, there is no drink_id method (Which should be correspondent with drink_id column in database) on #drinks object. You probably want to use :id instead of :drink_id
Thank you everyone that helped to solve this trouble, in especially #nathanvda.
My mistake was forget a table to join drink and order, so I recreated the project and besides that drink and order table, I created a ordereddrink table like #nathanvda answered.
app/models/order.rb
has_many :ordered_drinks, inverse_of: :order
has_many :feeds, through: :ordered_drinks
accepts_nested_attributes_for :ordered_drinks, allow_destroy: true
app/controllers/orders_controller.rb
before_action :set_drink, only: [:new, :edit, :update, :destroy]
def set_drink
#drinks = Drink.all
end
def order_params
params.require(:order).permit(:number, ordered_drinks_attributes: [:id, :order_id, :drink_id, :_destroy, drinks_attributes: [:id, :name, :_destroy]])
end
app/views/orders/_form.html.erb
<hr>
<h3>Drinks</h3>
<div id='ordered_drinks'>
<%= f.fields_for :ordered_drinks do |ordered_drink| %>
<%= render 'ordered_drink_fields', :f => ordered_drink %>
<% end %>
<div class='links'>
<%= link_to_add_association 'add drink', f, :ordered_drinks %>
</div>
</div>
<hr>
app/views/orders/_ordered_drink_fields.html.erb
<div class='nested-fields'>
<div class="field">
<%= f.collection_select(:drink_id, #drinks, :id, :name, include_blank: false) %>
</div>
<%= link_to_remove_association "remove drink", f %>
</div>
If anyone had the same trouble, contact me or send a message.
project details -> rails 5.1 and ruby 2.4.1
I'm trying to create a simple todo app. my problem is with nested model forms.
if i create a project without any task and i save it. then if i want to edit the project and add some tasks, tasks fields are not showing up. If i add tasks while creating the projects everything works as expected.in edit page i can see both project and tasks and i can edit as well.
below are my 2 models. I didnt use nested routes. just using the nested model forms.
class Project < ApplicationRecord
has_many :tasks, inverse_of: :project, dependent: :destroy
validates :name, presence: :true
validates :description, presence: :true
accepts_nested_attributes_for :tasks, reject_if: proc { |attributes| attributes[:name].blank? }
end
class Task < ApplicationRecord
belongs_to :project, inverse_of: :tasks
end
Below is my _form partial for new and edit.
<%= form_for #project do |form| %>
<% if #project.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#project.errors.count, "error") %> prohibited this project from being saved:</h2>
<ul>
<% #project.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name, id: :project_name %>
</div>
<div class="field">
<%= form.label :description %>
<%= form.text_field :description, id: :project_description %>
</div>
<%= form.fields_for :tasks do |task_form| %>
<div class="field">
<%= task_form.label :task_name %>
<%= task_form.text_field :name, id: :task_name %>
</div>
<div class="field">
<%= task_form.label :task_description %>
<%= task_form.text_field :description, id: :task_description %>
</div>
<% end %>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
white listing of the project controller is given below.
def project_params
params.require(:project).permit(:name, :description, tasks_attributes: [:id, :name, :description])
end
Any help is appreciated
-Ajith
if i create a project without any task and i save it. then if i want to edit the project and add some tasks, tasks fields are not showing up.
... is how it is actually supposed to be. So your problem most likely is not related to Strong Parameters problem, as I do not see any obvious problems in your view file and your project_params method. Now since you don't want that this behaviour, you'll probably want something like the following which builds up 1 or more task objects by default if there's no task object associated to theproject yet, so that there will always be at least one group of task fields when you're creating or editing a project.
app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def new
#project = Project.new
#project.tasks.build
# the above code just above is the same as below
# #project.tasks << Task.new
# now you can call this again like below, if you want 2 groups of `tasks` fields when you're creating a Project
#project.tasks.build
end
def edit
#project = Project.find(params[:id])
# this project may not yet have a `Task` object associated to it; if so we build a `Task` object like so
# this builds 3 `Task` objects; you can just use one below if you just want to show one in your edit form.
if #project.tasks.count == 0
#project.tasks.build
#project.tasks.build
#project.tasks.build
end
end
end
P.S. you may be interested in cocoon gem if you want to have a form that can build dynamic nested attributes automatically for you (like for example you want 2 buttons like the following) '(Add more task)' and '(Remove this task)`
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
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
I'm attempting to build a recipe-keeper app with three primary models:
Recipe - The recipe for a particular dish
Ingredient - A list of ingredients, validated on uniqueness
Quantity - A join table between Ingredient and Recipe that also reflects the amount of a particular ingredient required for a particular recipe.
I'm using a nested form (see below) that I constructed using an awesome Railscast on Nested Forms (Part 1, Part 2) for inspiration. (My form is in some ways more complex than the tutorial due to the needs of this particular schema, but I was able to make it work in a similar fashion.)
However, when my form is submitted, any and all ingredients listed are created anew—and if the ingredient already exists in the DB, it fails the uniqueness validation and prevents the recipe from being created. Total drag.
So my question is: Is there a way to submit this form so that if an ingredient exists whose name matches one of my ingredient-name fields, it references the existing ingredient instead of attempting to create a new one with the same name?
Code specifics below...
In Recipe.rb:
class Recipe < ActiveRecord::Base
attr_accessible :name, :description, :directions, :quantities_attributes,
:ingredient_attributes
has_many :quantities, dependent: :destroy
has_many :ingredients, through: :quantities
accepts_nested_attributes_for :quantities, allow_destroy: true
In Quantity.rb:
class Quantity < ActiveRecord::Base
attr_accessible :recipe_id, :ingredient_id, :amount, :ingredient_attributes
belongs_to :recipe
belongs_to :ingredient
accepts_nested_attributes_for :ingredient
And in Ingredient.rb:
class Ingredient < ActiveRecord::Base
attr_accessible :name
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
Here's my nested form that displays at Recipe#new:
<%= form_for #recipe do |f| %>
<%= render 'recipe_form_errors' %>
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<h3>Ingredients</h3>
<div id='ingredients'>
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient_attributes do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name %>
<% end %>
<%= ff.label :amount %>
<%= ff.text_field :amount, size: "10" %>
<%= ff.hidden_field :_destroy %>
<%= link_to_function "remove", "remove_fields(this)" %><br>
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
</div><br>
<%= f.label :description %><br>
<%= f.text_area :description, rows: 4, columns: 100 %><br>
<%= f.label :directions %><br>
<%= f.text_area :directions, rows: 4, columns: 100 %><br>
<%= f.submit %>
<% end %>
The link_to and link_to_function are there to allow the addition and removal of quantity/ingredient pairs on the fly, and were adapted from the Railscast mentioned earlier. They could use some refactoring, but work more or less as they should.
Update: Per Leger's request, here's the relevant code from recipes_controller.rb. In the Recipes#new route, 3.times { #recipe.quantities.build } sets up three blank quantity/ingredient pairs for any given recipe; these can be removed or added to on the fly using the "Add ingredient" and "remove" links mentioned above.
class RecipesController < ApplicationController
def new
#recipe = Recipe.new
3.times { #recipe.quantities.build }
#quantity = Quantity.new
end
def create
#recipe = Recipe.new(params[:recipe])
if #recipe.save
redirect_to #recipe
else
render :action => 'new'
end
end
You shouldn't put the logic of ingredients match into view - it's duty of Recipe#create to create proper objects before passing 'em to Model. Pls share the relevant code for controller
Few notes before coming to code:
I use Rails4#ruby2.0, but tried to write Rails3-compatible code.
attr_acessible was deprecated in Rails 4, so strong parameters are used instead. If you ever think to upgrade your app, just go with strong parameters from the beginning.
Recommend to make Ingredient low-cased to provide uniform appearance on top of case-insensitivity
OK, here we go:
Remove attr_accessible string in Recipe.rb, Quantity.rb and Ingredient.rb.
Case-insensitive, low-cased Ingredient.rb:
class Ingredient < ActiveRecord::Base
before_save { self.name.downcase! } # to simplify search and unified view
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
end
<div id='ingredients'> part of adjusted form to create/update Recipe:
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name, size: "10" %>
<% end %>
...
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
We should use :ingredient from Quantity nested_attributes and Rails will add up _attributes-part while creating params-hash for further mass assignment. It allows to use same form in both new and update actions. For this part works properly association should be defined in advance. See adjusted Recipe#new bellow.
and finally recipes_controller.rb:
def new
#recipe = Recipe.new
3.times do
#recipe.quantities.build #initialize recipe -> quantities association
#recipe.quantities.last.build_ingredient #initialize quantities -> ingredient association
end
end
def create
#recipe = Recipe.new(recipe_params)
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
def update
#recipe = Recipe.find(params[:id])
#recipe.attributes = recipe_params
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
private
def prepare_recipe
#recipe.quantities.each do |quantity|
# do case-insensitive search via 'where' and building SQL-request
if ingredient = Ingredient.where('LOWER(name) = ?', quantity.ingredient.name.downcase).first
quantity.ingredient_id = quantity.ingredient.id = ingredient.id
end
end
end
def recipe_params
params.require(:recipe).permit(
:name,
:description,
:directions,
:quantities_attributes => [
:id,
:amount,
:_destroy,
:ingredient_attributes => [
#:id commented bc we pick 'id' for existing ingredients manually and for new we create it
:name
]])
end
In prepare_recipe we do the following things:
Find ID of ingredient with given name
Set foreign_key quantity.ingredient_id to ID
Set quantity.ingredient.id to ID (think what happens if you don't do that and change ingredient name in Recipe)
Enjoy!