I am building a recipe book for my rails app. I have ingredients nested under recipes. I can create a recipe and add the ingredients in just fine. However, when I go to edit a recipe, it will duplicate all the ingredients in the form. It will then show all the ingredients duplicated in the views pages and when I delete one of the duplicated ingredient items to only have one, it deletes both items. We are also not allowed to use accepts_nested_forms_for for this project, hence why I have the custom writer
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, length: {minimum: 5}
validates :cooktime, presence: true
def self.alphabetical
self.order(name: :asc)
end
def ingredients_attributes=(ingredients_attributes)
ingredients_attributes.values.each do |ingredients_attribute|
if !ingredients_attribute.empty? &&
new_ingredient =
Ingredient.find_or_create_by(ingredients_attribute)
self.ingredients << new_ingredient
end
end
end
end
Ingredient Model
class Ingredient < ApplicationRecord
has_many :recipe_ingredients
has_many :recipes, through: :recipe_ingredients
def self.alphabetical
self.order(name: :asc)
end
end
Recipe_Ingredient Model
class RecipeIngredient < ApplicationRecord
belongs_to :recipe, required: false
belongs_to :ingredient, required: false
end
Recipe Controller
class RecipesController < ApplicationController
before_action :authenticate_user!
def new
#recipe = Recipe.new
3.times do
ingredient = #recipe.ingredients.build
end
end
def index
#recipes = current_user.recipes.alphabetical
end
def create
#recipe = current_user.recipes.new(recipe_params)
if #recipe.save
redirect_to recipe_path(#recipe)
else
render :new
end
end
def show
#recipe = Recipe.find(params[:id])
#ingredients = #recipe.ingredients.alphabetical
end
def edit
#recipe = Recipe.find(params[:id])
end
def update
#recipe = Recipe.find(params[:id])
if #recipe.update(recipe_params)
redirect_to #recipe
else
render :edit
end
end
def destroy
#recipe = Recipe.find(params[:id])
#recipe.delete
redirect_to recipes_path
end
private
def recipe_params
params.require(:recipe).permit(:name, :instructions, :cooktime,
:ingredient_ids => [], ingredients_attributes: [:recipe_id, :name])
end
end
Recipe Show Page
<h1> Recipe for <%=#recipe.name%></h1>
<h4>Recipe Instructions: <%=#recipe.instructions%></h4>
<h4>Cook Time: <%=#recipe.cooktime%></h4>
</ul>
<h4> Ingredients: </h4>
<% #ingredients.each do |ingredient|%>
<li><%=ingredient.name %></li>
<%end%>
</ul>
<%=link_to "Ingredients", recipe_ingredients_path(#recipe,
#ingredients)%>
<br>
<%=link_to "Delete Recipe", recipe_path(#recipe), :method => "delete"
%>
<br>
<%=link_to "Edit Recipe", edit_recipe_path(#recipe) %>
<br>
<%=link_to "All Recipes", recipes_path %>
<br>
Here are the edit and new form
<%= form_for #recipe do |f| %>
<%=f.label :name, "Name" %>
<%=f.text_field :name %>
<br>
<%=f.label :instructions, "Instructions" %>
<%=f.text_area :instructions, :rows => 3 %>
<br>
<%=f.label :cooktime, "Cook Time" %>
<%=f.text_field :cooktime %>
<br>
<%#= f.collection_check_boxes :ingredient_ids, Ingredient.all, :id,
:name %>
<%= f.fields_for :ingredients, #ingredient do |ingredient_fields| %>
<br>
<div class = >
<%= ingredient_fields.label :name, "Ingredient" %>
<%= ingredient_fields.text_field :name %>
<% end %>
<%=f.submit%>
Any help is greatly appreciated.
This code adds all the ingredients on the form to the recipe's ingredients relationship EVERY time.
def ingredients_attributes=(ingredients_attributes)
ingredients_attributes.values.each do |ingredients_attribute|
if !ingredients_attribute.empty? &&
new_ingredient =
Ingredient.find_or_create_by(ingredients_attribute)
self.ingredients << new_ingredient
end
end
end
You should clear down the ingredients relationship before you start pushing in the returned attributes
def ingredients_attributes=(ingredients_attributes)
self.ingredients.destroy_all # <- this will clear down ingredients
ingredients_attributes.values.each do |ingredients_attribute|
if !ingredients_attribute.empty? &&
new_ingredient =
Ingredient.find_or_create_by(ingredients_attribute)
self.ingredients << new_ingredient
end
end
end
The only thing you need to do is check if the new_ingredient is already included in the recipe's ingridients before (re)adding it.
You could do this like that:
#recipe.rb
def ingredients_attributes=(ingredients_attributes)
ingredients_attributes.values.each do |ingredients_attribute|
if !ingredients_attribute.empty? &&
new_ingredient =
Ingredient.find_or_create_by(ingredients_attribute)
self.ingredients << new_ingredient if !self.ingredients.include?(new_ingredient)
end
end
end
Related
Problem
I'm trying to create a middle table called category_profiles, is a intermediate table to assign favorite categories to my profiles, but I can't access to the category_ids, that I put in my form, always I got the same validation, Category doesn't exist:
Code:
class CategoryProfile < ApplicationRecord
belongs_to :profile
belongs_to :category
end
class Category < ApplicationRecord
has_many :category_profiles
has_many :profiles, through: :category_profiles
class Profile < ApplicationRecord
has_many :category_profiles
has_many :categories, through: :category_profiles
When I'm doing the create action, my controller can't find my category. How do I fix it?
My create action never find the ids of my categories to assign to the category_profiles. It has many through relation:
Module Account
class FavoritesController < Account::ApplicationController
before_action :set_category_profile
def index
#favorites = #profile.categories
end
def new
#categories = Category.all
#category_profile = CategoryProfile.new
end
def create
#category_profile = #profile.category_profiles.new(category_profile_params)
if #category_profile.save
flash[:success] = t('controller.create.success',
resource: CategoryProfile.model_name.human)
redirect_to account_favorites_url
else
flash[:warning] = #category_profile.errors.full_messages.to_sentence
redirect_to account_favorites_url
end
end
def destroy
end
private
def set_category_profile
#category_profile = CategoryProfile.find_by(params[:id])
end
def category_profile_params
params.permit(:profile_id,
category_ids:[])
end
end
end
Form
<%= bootstrap_form_with(model: #category,method: :post , local: true, html: { novalidate: true, class: 'needs-validation' }) do |f| %>
<div class="form-group">
<%= collection_check_boxes(:category_ids, :id, Category.all.kept.children.order(name: :asc), :id, :name, {}, { :multiple => true} ) do |b| %>
<%= b.label class: 'w-1/6 mr-4' %>
<%= b.check_box class: 'w-1/7 mr-4' %>
<%end %>
</div>
<div class="md:flex justify-center">
<%= f.submit 'Guardar categorÃa favorita', class: 'btn btn-primary' %>
</div>
<% end %>
Seems like you just want to update intermediate table. So you can do it like this.
def create
begin
#profile.categories << Category.find(params[:category_ids])
Or
params[:category_ids].each do |category_id|
#profile.category_profiles.create(category_id: category_id)
end
flash[:success] = t('controller.create.success',
resource: CategoryProfile.model_name.human)
redirect_to account_favorites_url
rescue
flash[:warning] = #category_profile.errors.full_messages.to_sentence
redirect_to account_favorites_url
end
end
Need to find other better way for error handling using either transaction block or something.
Upon clicking submit only the Duel attributes are passing - not Dueler.
duels_controller.rb
def new
#duel = Duel.new
#user = User.find(params[:challenge_daddy]) # This pulls in the ID for Challenged User
# Current User
#duel.duelers << Dueler.new(user_id: current_user.id, user_name: current_user.name, user_last_name: current_user.last_name)
#current_user_challenges = current_user.challenges.order(:created_at)
# Challenged User
#duel.duelers << Dueler.new(user_id: #user.id, user_name: #user.name, user_last_name: #user.last_name)
#challenged_user_challenges = #user.challenges.order(:created_at)
respond_with(#duel)
end
I think I have to submerge the dueler info (i.e. full_name and collection_select) within something like <%= simple_form_for(#dueler) do |f| %>, but then I don't want two separate submit buttons. When the user clicks submit the dueler and duel information should both submit since they go hand-in-hand. Right now only the duel information submits and the duelers are never created.
duels/_form.html.erb
<%= simple_form_for(#duel) do |f| %>
<%= current_user.full_name %> WILL <%= collection_select(:dueler, :challenge_id, #current_user_challenges, :id, :full_challenge, include_blank: true) %>
<%= #user.full_name %> WILL <%= collection_select(:dueler, :challenge_id, #challenged_user_challenges, :id, :full_challenge, include_blank: true) %>
THE LOSER WILL <%= f.text_field :consequence %>.
<%= f.submit %>
<% end %>
UPDATE
Originally I had this in the _form:
<%= f.fields_for :duelers do |dueler| %>
<%= render 'dueler_fields', :f => dueler %>
<% end %>
But I took it out because the duels_controller new logic wasn't passing into it so I moved the code directly into the _form, but now I'm not sure what should take the place of <%= f.fields_for :duelers do |dueler| %>
class Dueler < ActiveRecord::Base
belongs_to :user
belongs_to :challenge
belongs_to :duel
end
class Duel < ActiveRecord::Base
belongs_to :user
belongs_to :challenge
has_many :duelers
accepts_nested_attributes_for :duelers, :reject_if => :all_blank, :allow_destroy => true #correct
end
class DuelsController < ApplicationController
before_action :set_duel, only: [:show, :edit, :update, :destroy, :duel_request]
respond_to :html
def index
#duels = Duel.joins(:duelers).all
redirect_to duel(#duel)
end
def duel_request
#dueler = #duel.duelers.where(user_id: current_user)
end
def show
#dueler = Dueler.find_by(user_id: current_user.id)
respond_with(#duel)
end
def user_challenges
#user = User.find_by_name(params[:name])
#challenges = #user.challenges.order(:created_at)
end
def new
#duel = Duel.new
#user = User.find(params[:challenge_daddy])
#duel.duelers << Dueler.new(user_id: current_user.id, user_name: current_user.name, user_last_name: current_user.last_name)
#current_user_challenges = current_user.challenges.order(:created_at)
#duel.duelers << Dueler.new(user_id: #user.id, user_name: #user.name, user_last_name: #user.last_name)
#challenged_user_challenges = #user.challenges.order(:created_at)
respond_with(#duel)
end
def edit
end
def create
#duel = Duel.new(duel_params)
#duel.save
#redirect_to duel_request_url(#duel)
respond_with(#duel)
end
def update
#duel.update(duel_params[:duelers_attributes])
respond_with(#duel)
end
def destroy
#duel.destroy
respond_with(#duel)
end
private
def set_duel
#duel = Duel.find(params[:id])
end
def duel_params
params.require(:duel).permit(:consequence, :reward, duelers_attributes: [:id, :user_id, :challenge_id, :accept])
end
end
If you are using has_many and belongs_to with accepts_nested_attributes you will need to use inverse_of to prevent Rails from attempting to lookup records (which of course don't exist because you haven't yet created them)
Change your Duel model has_many declaration to:
has_many :duelers, inverse_of: :duel
For further details on this and an example of a nested form with has_many relationship using Simple Forms check out:
https://robots.thoughtbot.com/accepts-nested-attributes-for-with-has-many-through
Hoping someone can help out with this. I have two models order and date_order. Each order can have multiple date_orders, and I should be able to create many date_orders as I create an order.
How do I do that? As you can see, my code is working well for creating ONE date_order and relating it to the created order.
UPDATE: I have tried to create many "builders" in my orders/new file. It worked on the view, and created an order when I entered multiple dates and times. But the fields_for did not create any date_orders.
orders_controller.rb
def new
#order = Order.new
#order.date_orders.build
end
def create
#order = Order.new(order_params)
if #order.save
flash[:success] = "blah"
redirect_to #order
else
render 'new'
end
end
private
def order_params
params.require(:order).permit(:user_id, :purpose,
date_orders_attributes: [:id, :order_date, :time_start, :time_end, :order_id])
end
order.rb
class Order < ActiveRecord::Base
has_many :date_orders, :dependent => :destroy
accepts_nested_attributes_for :date_orders, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true
end
date_order.rb
class DateOrder < ActiveRecord::Base
belongs_to :order
end
order/new.html.erb
<%= form_for(#order, :html => {:multipart => true}) do |f| %>
## SOME QUESTIONS ##
<%= f.fields_for :date_orders do |builder| %>
<%= builder.label :date %>
<%= builder.date_field :order_date %>
<%= builder.label :starting_time %>
<%= builder.time_field :time_start %>
<%= builder.label :ending_time %>
<%= builder.time_field :time_end %>
<% end %>
<% end %>
Build more orders_dates:
class OrdersController < ApplicationController
def new
#order = Order.new
5.times { #order.date_orders.build } # < === HERE ===
end
private
def order_params
params.require(:order).permit(:user_id, :purpose,
# |- === HERE ===
date_orders_attributes: [:id, :content, :order_date, :time_start, :time_end, :order_id])
end
end
Update:
Also, add content to your strong params whitelist.
I am trying to use fields_for and create a nested form, however only one text field shows up, blank. I have 3 crewmember records.
crewmember model:
class Crewmember < ActiveRecord::Base
belongs_to :production
belongs_to :callsheet
validates :firstname, presence: true
validates :email, presence: true
def name
"#{firstname} #{lastname}"
end
end
callsheet model
class Callsheet < ActiveRecord::Base
attr_accessible :crewmembers_params
has_many :castmembers
has_many :crewmembers
accepts_nested_attributes_for :crewmembers
end
callsheets controller
class CallsheetsController < ApplicationController
def index
#callsheets = Callsheet.all
#departments = Department.where(production_id: current_user.default_working_production_id)
end
def show
#callsheet = Callsheet.find(params[:id])
end
def new
#callsheet = Callsheet.new
#departments = Department.where(production_id: current_user.default_working_production_id)
end
def edit
#callsheet = Callsheet.find(params[:id])
end
def create
#callsheet = Callsheet.new(callsheets_params)
#Callsheet.production_id = current_user.default_working_production_id
if #callsheets.save
redirect_to callsheet_path
else
render 'new'
end
end
def update
#callsheet = Callsheet.find(params[:id])
if #callsheet.update(callsheets_params)
redirect_to callsheet_path, :notice => "callsheets successfully updated."
else
render 'edit', :notice => "callsheets not updated."
end
end
def destroy
#callsheet = Callsheet.find(params[:id])
#callsheet.destroy
redirect_to callsheets_path
end
private
def callsheets_params
params.require(:callsheet).permit(:crewmembers_params [:id, :firstname])
end
end
form for new callsheet:
<%= form_for #callsheet do |f| %>
<% if #callsheet.errors.any? %>
<div id="error_explanation" class="alert alert-danger">
<strong>
<%= pluralize(#callsheet.errors.count, "error") %> prohibited
this call sheet from being saved:
</strong>
<ul>
<% #callsheet.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%= f.fields_for :crewmember do |crewmember| %>
<fieldset>
<%= crewmember.label :firstname, "First Name" %><br />
<%= crewmember.text_field :firstname %>
</fieldset>
<% end %>
<% end %>
You don't need attr_accessible (that's only for Rails 3).
You should also rename all your models to snake_case, referencing with CamelCase:
#app/models/call_sheet.rb
class CallSheet < ActiveRecord::Base
has_many :cast_members
has_many :crew_members
accepts_nested_attributes_for :crew_members
end
As is the custom with fields_for, you also need to build the associated objects (if you're creating a new record) (you don't need to do this if editing):
#app/controllers/call_sheets_controller.rb
class CallSheetsController < ApplicationController
before_action :set_departments
def new
#callsheet = Callsheet.new
#callsheet.crew_members.build
end
def edit
#callsheet = Callsheet.find params[:id]
end
def update
#callsheet = Callsheet.find params[:id]
#callsheet.update callsheet_params
end
private
def set_departments
#departments = Department.where(production_id: current_user.default_working_production_id)
end
def callsheet_params
params.require(:callsheet).permit(crew_members_attributes: [:id, :firstname])
end
end
This will allow you to use:
<%= form_for #callsheet do |f| %>
<%= f.fields_for :crew_members do |crewmember| %>
<%= crewmember.label :firstname, "First Name" %><br />
<%= crewmember.text_field :firstname %>
<% end %>
<%= f.submit %>
<% end %>
--
When passing nested attributes through fields_for, you need several components:
The correct association in your parent model
An instantiated version of the associated model (#parent.build_child)
Correct fields_for definition
Passing correct parameters through your controller
I've outlined how to achieve the above, all of which you had incorrect.
You can also declare multiple validations in the same call:
#app/models/crew_member.rb
class CrewMember < ActiveRecord::Base
validates :firstname, :email, presence: true
end
Try changing
<%= f.fields_for :crewmember do |crewmember| %>
into
<%= f.fields_for :crewmember, #callsheet.crewmember || #callsheet.build_crewmember do |crewmember| %>
I posted an earlier question about this and was advised to read lots of relevant info. I have read it and tried implementing about 30 different solutions. None of which have worked for me.
Here's what I've got.
I have a Miniatures model.
I have a Manufacturers model.
Miniatures have many manufacturers THROUGH a Productions model.
The associations seem to be set up correctly as I can show them in my views and create them via the console. Where I have a problem is in letting the Miniatures NEW and EDIT views create and update to the Productions table.
In the console the command #miniature.productions.create(manufacturer_id: 1) works, which leads me to believe I should be able to do the same in a form.
I THINK my problem is always in the Miniatures Controller and specifically the CREATE function. I have tried out a ton of other peoples solutions there and none have done the trick. It is also possible that my field_for stuff in my form is wrong but that seems less fiddly.
I've been stuck on this for days and while there are other things I could work on, if this association isn't possible then I'd need to rethink my entire application.
The form now creates a line in the Productions table but doesn't include the all important manufacturer_id.
Any help VERY much appreciated.
My New Miniature form
<% provide(:title, 'Add miniature') %>
<h1>Add a miniature</h1>
<div class="row">
<div class="span6 offset3">
<%= form_for(#miniature) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.fields_for :production do |production_fields| %>
<%= production_fields.label :manufacturer_id, "Manufacturer" %>
<%= production_fields.select :manufacturer_id, options_from_collection_for_select(Manufacturer.all, :id, :name) %>
<% end %>
<%= f.label :release_date %>
<%= f.date_select :release_date, :start_year => Date.current.year, :end_year => 1970, :include_blank => true %>
<%= f.submit "Add miniature", class: "btn btn-large btn-primary" %>
<% end %>
</div>
</div>
Miniatures controller
class MiniaturesController < ApplicationController
before_action :signed_in_user, only: [:new, :create, :edit, :update]
before_action :admin_user, only: :destroy
def productions
#production = #miniature.productions
end
def show
#miniature = Miniature.find(params[:id])
end
def new
#miniature = Miniature.new
end
def edit
#miniature = Miniature.find(params[:id])
end
def update
#miniature = Miniature.find(params[:id])
if #miniature.update_attributes(miniature_params)
flash[:success] = "Miniature updated"
redirect_to #miniature
else
render 'edit'
end
end
def index
#miniatures = Miniature.paginate(page: params[:page])
end
def create
#miniature = Miniature.new(miniature_params)
if #miniature.save
#production = #miniature.productions.create
redirect_to #miniature
else
render 'new'
end
end
def destroy
Miniature.find(params[:id]).destroy
flash[:success] = "Miniature destroyed."
redirect_to miniatures_url
end
private
def miniature_params
params.require(:miniature).permit(:name, :release_date, :material, :scale, :production, :production_attributes)
end
def admin_user
redirect_to(root_url) unless current_user.admin?
end
def signed_in_user
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in."
end
end
end
Miniature model
class Miniature < ActiveRecord::Base
has_many :productions, dependent: :destroy
has_many :manufacturers, :through => :productions
accepts_nested_attributes_for :productions
validates :name, presence: true, length: { maximum: 50 }
validates :material, presence: true
validates :scale, presence: true
validates_date :release_date, :allow_blank => true
def name=(s)
super s.titleize
end
end
Production model
class Production < ActiveRecord::Base
belongs_to :miniature
belongs_to :manufacturer
end
Manufacturer model
class Manufacturer < ActiveRecord::Base
has_many :productions
has_many :miniatures, :through => :productions
validates :name, presence: true, length: { maximum: 50 }
accepts_nested_attributes_for :productions
end
Instead of calling:
#production = #miniature.productions.create
Try Rails' "build" method:
def new
#miniature = Miniature.new(miniature_params)
#miniature.productions.build
end
def create
#miniature = Miniature.new(miniature_params)
if #miniature.save
redirect_to #miniature
else
render 'new'
end
end
Using the build method uses ActiveRecord's Autosave Association functionality.
See http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html
You also need to update your params method, e.g.
def miniature_params
params.require(:miniature).permit(:name, :release_date, :material, :scale, productions_attributes: [:manufacturer_id])
end
Also your fields_for should be plural (I think)...
<%= f.fields_for :productions do |production_fields| %>