any help would be most appreciated, I am rather new to Rails.
I have two models a Shopping List and a Product. I'd like to save/update multiple products to a shopping list at a time.
The suggested changes are not updating the models. I've been googling and is "attr_accessor" or find_or_create_by the answer(s)?
Attempt 1 - Existing code
Error
> unknown attribute 'products_attributes' for Product.
Request
Parameters:
{"_method"=>"patch",
"authenticity_token"=>"3BgTQth38d5ykd3EHiuV1hkUqBZaTmedaJai3p9AR1N2bPlHraVANaxxe5lQYaVcWNoydA3Hb3ooMZxx15YnOQ==",
"list"=>
{"products_attributes"=>
{"0"=>{"title"=>"ten", "id"=>"12"},
"1"=>{"title"=>"two", "id"=>"13"},
"2"=>{"title"=>"three", "id"=>"14"},
"3"=>{"title"=>"four", "id"=>"15"},
"4"=>{"title"=>"five", "id"=>"16"},
"5"=>{"title"=>""},
"6"=>{"title"=>""},
"7"=>{"title"=>""},
"8"=>{"title"=>""},
"9"=>{"title"=>""},
"10"=>{"title"=>""}}},
"commit"=>"Save Products",
"id"=>"7"}
Attempt 2 - no errors the page reloads and none of the expected fields are updated. In earnest, I am Googling around and copying and pasting code snippets in the vain hope of unlocking the right combo.
Added to Products mode
class Product < ApplicationRecord
attr_accessor :products_attributes
belongs_to :list, optional: true
end
<%= content_tag(:h1, 'Add Products To This List') %>
<%= form_for(#list) do |f| %>
<%= f.fields_for :products do |pf| %>
<%= pf.text_field :title %><br>
<% end %>
<p>
<%= submit_tag "Save Products" %>
</p>
<% end %>
<%= link_to "Back To List", lists_path %>
list controller
def update
#render plain: params[:list].inspect
#list = List.find(params[:id])
if #list.products.update(params.require(:list).permit(:id, products_attributes: [:id, :title]))
redirect_to list_path(#list)
else
render 'show'
end
list model
class List < ApplicationRecord
has_many :products
accepts_nested_attributes_for :products
end
original do nothing - product model
class Product < ApplicationRecord
belongs_to :list, optional: true
end
If you just want a user to be able to select products and place them on a list you want a many to many association:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
end
class ListItem < ApplicationRecord
belongs_to :list
belongs_to :product
end
class Product < ApplicationRecord
has_many :list_items
has_many :lists, through: :list_products
end
This avoids creating vast numbers of duplicates on the products table and is known as normalization.
You can then select existing products by simply using a select:
<%= form_for(#list) do |f| %>
<%= f.label :product_ids %>
<%= f.collection_select(:product_ids, Product.all, :name, :id) %>
# ...
<% end %>
Note that this has nothing to with nested routes or nested attributes. Its just a select that uses the product_ids setter that's created by the association. This form will still submit to /lists or /lists/:id
You can whitelist an array of ids by:
def list_params
params.require(:list)
.permit(:foo, :bar, product_ids: [])
end
To add create/update/delete a bunch of nested records in one form you can use accepts_nested_attributes_for together with fields_for:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
accepts_nested_attributes_for :products
end
<%= form_for(#list) do |f| %>
<%= form.fields_for :products do |pf| %>
<%= pf.label :title %><br>
<%= pf.text_field :title %>
<% end %>
# ...
<% end %>
Of course fields_for won't show anything if you don't seed the association with records. That's where that loop that you completely misplaced comes in.
class ListsController < ApplicationController
# ...
def new
#list = List.new
5.times { #list.products.new } # seeds the form
end
def edit
#list = List.find(params[:id])
5.times { #list.products.new } # seeds the form
end
# ...
def update
#list = List.find(params[:id])
if #list.update(list_params)
redirect_to #list
else
render :new
end
end
private
def list_params
params.require(:list)
.permit(
:foo, :bar,
product_ids: [],
products_attrbutes: [ :title ]
)
end
end
Required reading:
Rails Guides: Nested forms
ActiveRecord::NestedAttributes
fields_for
Related
I would like to create an association to another a model when creating a record.
The models use the has_many through association.
Models
Recipe
class Recipe < ApplicationRecord
attribute :name
attribute :published
has_many :ingridients, dependent: :destroy
has_many :instructions, dependent: :destroy
has_many :recipe_seasons
has_many :seasons, through: :recipe_seasons
accepts_nested_attributes_for :recipe_seasons
validates_presence_of :name
end
Season
class Season < ApplicationRecord
has_many :recipe_seasons
has_many :recipes, through: :recipe_seasons
end
RecipeSeason
class RecipeSeason < ApplicationRecord
belongs_to :recipe
belongs_to :season
validates_presence_of :recipe
validates_presence_of :season
accepts_nested_attributes_for :season
end
Controller
def new
#month = 1
#recipe = Recipe.new
#recipe.recipe_seasons.build(season_id: #month).build_recipe
end
def create
#recipe = Recipe.new(recipe_params)
#recipe.save
redirect_to recipes_path
flash[:notice] = I18n.t("recipe.created")
end
private
def recipe_params
params.require(:recipe)
.permit(:name, :published, recipe_seasons_attributes:[:recipe_id, :season_id ])
end
When the Recipe is created, I'd like a defauly value of #month to be inserted into a record on the table recipe_seasons using the id of the newly created Recipe.
Form
<%= form_with(model: #recipe) do |f| %>
<%= f.label :name %>
<%= f.text_field :name, required: true %>
<%= f.label :published %>
<%= f.check_box :published, class: "form-control", placeholder: "Tick if done" %>
<%= f.submit %>
<% end %>
<%=link_to t("back"), recipes_path %>
When I create a recipe, I would like a record to be inserted into recipe_seasons at the same time, using the id that is created on the recipe as the recipe_id on the table recipe_seasons. For now I will hard code a value for #month that is used for the season_id.
You're actually overdoing and overcomplicating it here. You don't need anything in your RecipeSeason class except:
class RecipeSeason < ApplicationRecord
belongs_to :recipe
belongs_to :season
end
The presence validations are added by default to belongs_to assocations since Rails 5. You do not need nested attributes to just assign assocatiated items.
Normally when dealing with join tables you do not need to explicitly create the join models as they are created implicitly through the assocation:
def create
#recipe = Recipe.new(recipe_params)
# Always check if the record was valid and saved
if #recipe.save
redirect_to recipes_path, status: :created
flash[:notice] = I18n.t("recipe.created")
else
render :new
end
end
def recipe_params
params.require(:recipe)
.permit(:name, :published, season_ids: [])
end
<%= form_with(model: :recipe) do |form| %>
<%= form.collection_select(
:season_ids, # name of the attribute
Season.all, # the collection which should be available as options
:id, # value method
:name # label method
) %>
<% end %>
This uses the setter created by has_many :seasons, through: :recipe_seasons and will automatically create/delete rows in the recipe_seasons table.
If you want to create a default you just set the selected attribute on the select element.
<%= form.collection_select :season_ids, #seasons, :id, :name, selected: #seasons.first.id %>
The only time you need to explicitly create the intermediate model is when its not just used as a simple join table and your passing properties for that table. For example if your have an order form and you want to add a product to the order together with a quantity:
# order.products << #product won't let us pass a quantity
order.line_items.create(
product: #product,
quantity: 50
)
That's when nested attributes actually becomes relevant.
I have a form that has some prebuilt tags that the user can select on a post. These tags are set up with a has_many through: relationship. Everything seems to be working but when I save (the post does save) there is an Unpermitted parameter: :tags from the controller's save method.
Tag model:
class Tag < ApplicationRecord
has_many :post_tags
has_many :posts, through: :post_tags
end
PostTag model:
class PostTag < ApplicationRecord
belongs_to :tag
belongs_to :post
end
Post model:
class Post < ApplicationRecord
...
has_many :post_tags
has_many :tags, through: :post_tags
Post controller methods:
def update
# saves tags
save_tags(#post, params[:post][:tags])
# updates params (not sure if this is required but I thought that updating the tags might be causing problems for the save.
params[:post][:tags] = #post.tags
if #post.update(post_params)
...
end
end
...
private
def post_params
params.require(:post).permit(:name, :notes, tags: [])
end
def save_tags(post, tags)
tags.each do |tag|
# tag.to_i, is because the form loads tags as just the tag id.
post.post_tags.build(tag: Tag.find_by(id: tag.to_i))
end
end
View (tags are checkboxes displayed as buttons):
<%= form.label :tags %>
<div class="box">
<% #tags.each do |tag| %>
<div class="check-btn">
<label>
<%= check_box_tag('dinner[tags][]', tag.id) %><span> <%= tag.name %></span>
</label>
</div>
<% end %>
</div>
Again this saves, and works fine, but I'd like to get rid of the Unpermitted parameter that is thrown in the console.
Your whole solution is creative but extremely redundant. Instead use the collection helpers:
<%= form_with(model: #post) |f| %>
<%= f.collection_check_boxes :tag_ids, Tag.all, :id, :name %>
<% end %>
tags_ids= is a special setter created by has_many :tags, through: :post_tags (they are created for all has_many and HABTM assocations). It takes an array of ids and will automatically create/delete join rows for you.
All you have to do in your controller is whitelist post[tag_ids] as an array:
class PostsController < ApplicationController
# ...
def create
#post = Post.new(post_params)
if #post.save
redirect_to #post
else
render :new
end
end
def update
if #post.update(post_params)
redirect_to #post
else
render :edit
end
end
private
# ...
def post_params
params.require(:post)
.permit(:name, :notes, tag_ids: [])
end
end
So I am working on a recipe book rails application. A user can create a recipe, view the recipe, and update the recipe. However, when I update a particular ingredient's quantity (i.e. Pasta "2 Cups" ) it changes all the other recipes that contain pasta to that new quantity ("2 Cups"). I can see in my rails console and server that it recognizes the change and updates, but when I display the view it looks as if it displays the first instance of the ingredient and not the one that I just updated. I have a strong feeling that it is an error with my quantity method in ingredients, but i'm not sure how to fix it.
Recipe Model
class Recipe < ApplicationRecord
belongs_to :user, required: false
has_many :recipe_ingredients
has_many :ingredients, through: :recipe_ingredients
validates :name, presence: true
validates :instructions, presence: true
validates :cooktime, presence: true
def self.alphabetize
self.order(name: :asc)
end
def ingredients_attributes=(ingredients_attributes)
self.ingredients = []
ingredients_attributes.values.each do |ingredients_attribute|
if !ingredients_attribute[:name].empty?
new_ingredient = Ingredient.find_or_create_by(name:
ingredients_attribute[:name])
self.recipe_ingredients.build(ingredient_id: new_ingredient.id,
quantity: ingredients_attribute[:quantity])
end
end
end
end
Ingredients Model
class Ingredient < ApplicationRecord
has_many :recipe_ingredients
has_many :recipes, through: :recipe_ingredients
def self.alphabetize
self.order(name: :asc)
end
def quantity
recipe_ingredient = RecipeIngredient.find_by(recipe_id:
self.recipes.first.id, ingredient_id: self.id)
recipe_ingredient.quantity
end
end
Recipe Show, Edit and Update Actions:
def show
#recipe = Recipe.find(params[:id])
#ingredients = #recipe.ingredients.alphabetize
end
def edit
#recipe = Recipe.find(params[:id])
end
def update
#recipe = Recipe.find(params[:id])
if #recipe.user = current_user
if #recipe.update(recipe_params)
redirect_to #recipe
else
render :edit
end
end
end
views/recipes/show (ingredients - quantity) listing:
<% #recipe.ingredients.each do |ingredient|%>
<li><%=ingredient.name %> - <%=ingredient.quantity%></li>
<%end%>
Just to riff on abax's answer a bit. I think I would do something like:
class Ingredient < ApplicationRecord
has_many :recipe_ingredients
has_many :recipes, through: :recipe_ingredients
def self.alphabetize
self.order(name: :asc)
end
def quantity_for(recipe)
recipe_ingredients.where(recipe: recipe).first.quantity
end
end
Which you would use something like:
<% #recipe.ingredients.each do |ingredient|%>
<li>
<%= ingredient.name %> - <%= ingredient.quantity_for(#recipe) %>
</li>
<%end%>
IMO, ingredient.quantity_for(#recipe) indicates very clearly what's going on: the ingredient is returning the quantity_for a particular #recipe.
BTW, this:
def quantity
recipe_ingredient = RecipeIngredient.find_by(
recipe_id: self.recipes.first.id,
ingredient_id: self.id
)
recipe_ingredient.quantity
end
Is all sorts of wrong. In addition to the problem identified by abax, you don't need to do
RecipeIngredient.find_by(ingredient_id: self.id)
That's why you did has_many recipe_ingredients! So that you can simply do:
recipe_ingredients.where(recipe: recipe)
And, note that when you do associations, you don't have to say recipe_id: recipe.id. Rails allows you to simply do recipe: recipe. Less typing. Fewer typos.
Finally, this assignment:
recipe_ingredient = RecipeIngredient.find_by(...)
Isn't necessary. You could just do:
RecipeIngredient.find_by(
recipe: recipe,
ingredient: self
).quantity
Which, of course, you would never do. Because, as above, it's much nicer to do:
recipe_ingredients.where(recipe: recipe).first.quantity
It looks like this is the problem area:
class Ingredient < ApplicationRecord
def quantity
recipe_ingredient = RecipeIngredient.find_by(recipe_id:
self.recipes.first.id, ingredient_id: self.id)
recipe_ingredient.quantity
end
<%=ingredient.quantity%> will return the quantity of the first recipe to use the ingredient no matter the recipe. I think you should be accessing the recipe_ingerdient.quantity instead.
<% #recipe.recipe_ingredients.each do |recipe_ingredient|%>
<li><%=recipe_ingredient.name %> - <%=recipe_ingredient.quantity%></li>
<%end%>
as it looks like the quantity for each recipe is saved in the recipe_ingredient join table.
Edit: This approach would require adding delegate :name, to: :ingredient to class RecipeIngredient
I would start with a more modest goal to start with - just to let users create nested RecipeIngedients:
<%= form_for(#recipe) do |f| %>
<%= fields_for(:recipe_ingredients) do |ri| %>
<%= ri.collection_select(:ingredient_id, Ingredient.all, :id, :name) %>
<%= ri.text_field(:quantity) %>
<% end %>
<% end %>
class Recipe < ApplicationRecord
# ...
accepts_nested_attributes_for :recipe_ingredients
end
def recipe_attributes
params.require(:recipe).permit(:foo, :bar, recipe_ingredient_attributes: [:ingredient_id, :quantity])
end
You can then expand this by using an additional level of nested attributes:
<%= form_for(#recipe) do |f| %>
<%= f.fields_for(:recipe_ingredients) do |ri| %>
<div class="recipe-ingredient">
<%= ri.collection_select(:ingredient_id, Ingredient.all, :id, :name) %>
<%= ri.fields_for(:ingredient) |ingredient| %>
<%= ingredient.text_field :name %>
<% end %>
</div>
<% end %>
<% end %>
class RecipeIngredient < ApplicationRecord
# ...
accepts_nested_attributes_for :ingredient, reject_if: [:ingredient_exists?,
private
def ingredient_exists?(attributes)
Ingredient.exists?(name: attributes[:name])
end
def ingredient_set?(attributes)
self.ingredient.nil?
end
end
def recipe_attributes
params.require(:recipe)
.permit(:foo, :bar,
recipe_ingredients_attributes: [:ingredient_id, :quantity, { ingredient_attributes: [:name] }]
)
end
But using nested attributes more than one level down usually turns into a real mess since you´re stuffing so much functionality down into a single controller. A better alternative both from the the programming and UX perspective might be to use ajax to setup an autocomplete and create the records on the fly by POST'ing to /ingredients.
I'm kinda new to ruby on rails, I've been reading documentation on assosiations and I've been having an easy time (and usually a quick google search solves most of my doubts) however recently I'm having problems with a seemingly easy thing to do.
What I'm trying to do is to create an Event, linked to an existing Category.
Event model
class Event < ApplicationRecord
has_many :categorizations
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categorizations
.
.
.
end
Category model
class Category < ApplicationRecord
has_many :categorizations
has_many :events, through: :categorizations
end
Categorization model
class Categorization < ApplicationRecord
belongs_to :event
belongs_to :category
end
Event controller
class EventsController < ApplicationController
def new
#event = Event.new
end
def create
#user = User.find(current_user.id)
#event = #user.events.create(event_params)
if #event.save
redirect_to root_path
else
redirect_to root_path
end
end
private
def event_params
params.require(:event).permit(:name, category_ids:[])
end
Here is the form, which is where I think the problem lies:
<%= form_for #event, :html => {:multipart => true} do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.fields_for :categorizations do |categories_fields|%>
<% categories = [] %>
<% Category.all.each do |category| %>
<% categories << category.name %>
<% end %>
<%= categories_fields.label :category_id, "Category" %>
<%= categories_fields.select ( :category_id, categories) %>
<% end %>
.
.
.
<%= f.submit "Create"%>
<% end %>
I previously populate the Category db with some categories, so what's left to do is to while creating an event, also create a categorization that is linked both to the new event and the chosen Categorization. but the things I've tried don't seem to be working.
Other things seem to be working ok, whenever I try to submit the event all things are populated as expected except the categorization.
As you mentioned that you are new to rails, you'll find this cocoon gem very interesting. You can achieve what you wanted. And the code will cleaner.
I don't have the points to comment, that's why I am giving this as an answer.
I have three model classes related to each other.
class Student < ActiveRecord::Base
has_many :marks
belongs_to :group
accepts_nested_attributes_for :marks,
reject_if: proc { |attributes| attributes['rate'].blank?},
allow_destroy: true
end
This class describes a student that has many marks and I want to create a Student record along with his marks.
class Mark < ActiveRecord::Base
belongs_to :student, dependent: :destroy
belongs_to :subject
end
Marks are related both to the Subject and a Student.
class Subject < ActiveRecord::Base
belongs_to :group
has_many :marks
end
When I try to create the nested fields of marks in loop labeling them with subject names and passing into in it's subject_id via a loop a problem comes up - only the last nested field of marks is saved correctly, whilst other fields are ignored. Here's my form view code:
<%= form_for([#group, #student]) do |f| %>
<%= f.text_field :student_name %>
<%=f.label 'Student`s name'%><br>
<%= f.text_field :student_surname %>
<%=f.label 'Student`s surname'%><br>
<%=f.check_box :is_payer%>
<%=f.label 'Payer'%>
<%= f.fields_for :marks, #student.marks do |ff|%>
<%#group.subjects.each do |subject| %><br>
<%=ff.label subject.subject_full_name%><br>
<%=ff.text_field :rate %>
<%=ff.hidden_field :subject_id, :value => subject.id%><br>
<%end%>
<% end %>
<%= f.submit 'Add student'%>
<% end %>
Here`s my controller code:
class StudentsController<ApplicationController
before_action :authenticate_admin!
def new
#student = Student.new
#student.marks.build
#group = Group.find(params[:group_id])
#group.student_sort
end
def create
#group = Group.find(params[:group_id])
#student = #group.students.new(student_params)
if #student.save
redirect_to new_group_student_path
flash[:notice] = 'Студента успішно додано!'
else
redirect_to new_group_student_path
flash[:alert] = 'При створенні були деякі помилки!'
end
end
private
def student_params
params.require(:student).permit(:student_name, :student_surname, :is_payer, marks_attributes: [:id, :rate, :subject_id, :_destroy])
end
end
How can I fix it?
#student.marks.build
This line will reserve an object Mark.
If you want multi marks, May be you need something like this in new action :
#group.subjects.each do |subject|
#student.marks.build(:subject=> subject)
end
Hope useful for you.