Rails fields_for not working - ruby-on-rails

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| %>

Related

How to make columns from joined tables available in Ransack attribute field?

I want to display belongs_to relationship columns in ransackable attributes list. So that I can display them in the dropdown, and perform an advanced search on the (joined) table.
How can I do that?
Below my model, where each manifest has one consignee. I've adjusted the attribute list, but when I select the consignee name it looks for 'manifest'.'name' and not in 'consignee'.'name' via a JOIN.
When I use the simple search form, it works correctly.
manifest.rb
class Manifest < ApplicationRecord
belongs_to :shipper
belongs_to :consignee
...
def self.ransackable_attributes(auth_object = nil)
super - ['id', 'created_at', 'updated_at', 'consignee_id']
super + ['consignee_name']
end
end
consignee.rb
class Consignee < ApplicationRecord
has_many :manifest, dependent: :destroy
...
end
manifest_controller.rb
...
def index
#search = ransack_params
#search.build_grouping unless #search.groupings.any?
#manifests = #search.result(distinct: true)
#search.build_condition
...
private
def ransack_params
Manifest.includes(:vessel, :pod, :pol, :por, :del, :consignee).ransack(params[:q])
end
end
index.html.erb
<%= search_form_for #search do |f| %>
<%= f.condition_fields do |c| %>
<div class="field">
<%= c.attribute_fields do |a| %>
<%= a.attribute_select %>
<% end %>
<%= c.predicate_select :only => [:cont, :not_cont, :matches]%>
<%= c.value_fields do |v| %>
<%= v.text_field :value %>
<% end %>
</div>
<% end %>
I expect to see Consignee Name in the dropdown list, but only see 'Name' at the bottom. When I select this and press search it returns with an error:
undefined method `type' for nil:NilClass
on line: Manifest.includes(:vessel, :pod, :pol, :por, :del, :consignee).ransack(params[:q])
manifest.rb (define ransackable_attributes only for this model)
class Manifest < ApplicationRecord
belongs_to :shipper
belongs_to :consignee
...
private
def self.ransackable_attributes(auth_object = nil)
super - ['id', 'created_at', 'updated_at', 'consignee_id']
end
end
consignee.rb (define ransackable_attributes for this model)
class Consignee < ApplicationRecord
has_many :manifest, dependent: :destroy
...
private
def self.ransackable_attributes(auth_object = nil)
if auth_object == :manifest_search
['name']
else
super
end
end
end
manifest_controller.rb
...
def index
#search = ransack_params
#search.build_grouping unless #search.groupings.any?
#manifests = #search.result(distinct: true)
#search.build_condition
...
private
def ransack_params
Manifest.ransack(params[:q], auth_object: :manifest_search)
end
index.html.erb (add the associations parameter to attribute_select)
<%= search_form_for #search do |f| %>
<%= f.condition_fields do |c| %>
<div class="field">
<%= c.attribute_fields do |a| %>
<%= a.attribute_select :associations => ["consignee"] %>
<% end %>
<%= c.predicate_select :only => [:cont, :not_cont, :matches]%>
<%= c.value_fields do |v| %>
<%= v.text_field :value %>
<% end %>
</div>
<% end %>
note that the auth_object conditional is optional, but it allows you to have different search pages with different attributes. For example, the consignee page could have its own search form showing all of consignee's attributes, while the manifest search shows only the consignee name as a searchable attribute.
I removed the includes to simplify the code; it's not required to make the search work. If you need those associations pre-loaded, you can put it back in.

Create action by a has many through association to assign a favorite_category

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.

The edit feature on my rails app is duplicating my data

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

Form Objects in Rails

The example code below is a contrived example of an attempt at a form object where it is probably overkill to utilize a form object. Nonetheless: it shows the issue I am having:
I have two models: a User and an Email:
# app/models/user.rb
class User < ApplicationRecord
has_many :emails
end
# app/models/user.rb
class Email < ApplicationRecord
belongs_to :user
end
I want to create a form object which creates a user record, and then creates three associated email records.
Here are my form object classes:
# app/forms/user_form.rb
class UserForm
include ActiveModel::Model
attr_accessor :name, :email_forms
validates :name, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
puts "I would proceed to create all the necessary objects by hand"
user = User.create(name: name)
email_forms.each do |email|
Email.create(user: user, email_text: email.email_text)
end
end
end
# app/forms/email_form.rb
class EmailForm
include ActiveModel::Model
attr_accessor :email_text, :user_id
validates :email_text, presence: true
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
puts "The Form is VALID!"
# DON'T THINK I WOULD PERSIST DATA HERE
# INSTEAD DO IT IN THE user_form
end
end
Notice: the validations on the form objects. A user_form is considered to be invalid if it's name attribute is blank, or if the email_text attribute is left blank for any of the email_form objects inside it's email_forms array.
For brevity: I will just be going through the new and create action of utilizing the user_form:
# app/controllers/user_controller.rb
class UsersController < ApplicationController
def new
#user_form = UserForm.new
#user_form.email_forms = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {email_forms: [:_destroy, :id, :email_text, :user_id]})
end
end
Lastly: the form itself:
# app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
<%= link_to 'Back', users_path %>
# app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
# MESSY, but couldn't think of a better way to do this...
<% unique_index = 0 %>
<% user_form.email_forms.each do |email_form| %>
<div class="field">
<%= label_tag "user_form[email_forms][#{unique_index}][email_text]", "Email Text" %>
<%= text_field_tag "user_form[email_forms][#{unique_index}][email_text]" %>
</div>
<% unique_index += 1 %>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
The form does render:
And here is the form's html:
I go to submit the form. Here is the params hash:
Parameters: {"utf8"=>"✓", "authenticity_token"=>”abc123==", "user_form"=>{"name"=>"neil", "email_forms"=>{"0"=>{"email_text"=>"test_email_1"}, "1"=>{"email_text"=>"test_email_2"}, "2"=>{"email_text"=>""}}}, "commit"=>"Create User form"}
What should happen is the form should be re-rendered and nothing persisted because the form_object is invalid: All three associated emails must NOT be blank. However: the form_object thinks it is valid, and it blows up in the persist! method on the UserForm. It highlights the Email.create(user: user, email_text: email.email_text) line and says:
undefined method `email_text' for ["0", {"email_text"=>"test_email_1"}]:Array
Clearly there are a couple things going on: The nested validations appear to not be working, and I am having trouble rebuilding each of the emails from the params hash.
Resources I have already examined:
This Article seemed promising but I was having trouble getting it to work.
I have attempted an implementation with the virtus gem and the reform-rails gem. I have pending questions posted for both of those implementations as well: virtus attempt here and then reform-rails attempt here.
I have attempted plugging in accepts_nested_attributes, but was having trouble figuring out how to utilize that with a form object, as well as a nested form object (like in this code example). Part of the issue was that has_many and accepts_nested_attributes_for do not appear to be included in ActiveModel::Model.
Any guidance on getting this form object to do what is expected would be very much appreciated! Thanks!
Complete Answer
Models:
#app/models/user.rb
class User < ApplicationRecord
has_many :emails
end
#app/models/email.rb
class Email < ApplicationRecord
belongs_to :user
end
Controller:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
#users = User.all
end
def new
#user_form = UserForm.new
#user_form.emails = [EmailForm.new, EmailForm.new, EmailForm.new]
end
def create
#user_form = UserForm.new(user_form_params)
if #user_form.save
redirect_to users_path, notice: 'User was successfully created.'
else
render :new
end
end
private
def user_form_params
params.require(:user_form).permit(:name, {emails_attributes: [:email_text]})
end
end
Form Objects:
#app/forms/user_form.rb
class UserForm
include ActiveModel::Model
attr_accessor :name, :emails
validates :name, presence: true
validate :all_emails_valid
def emails_attributes=(attributes)
#emails ||= []
attributes.each do |_int, email_params|
email = EmailForm.new(email_params)
#emails.push(email)
end
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
user = User.new(name: name)
new_emails = emails.map do |email_form|
Email.new(email_text: email_form.email_text)
end
user.emails = new_emails
user.save!
end
def all_emails_valid
emails.each do |email_form|
errors.add(:base, "Email Must Be Present") unless email_form.valid?
end
throw(:abort) if errors.any?
end
end
app/forms/email_form.rb
class EmailForm
include ActiveModel::Model
attr_accessor :email_text, :user_id
validates :email_text, presence: true
end
Views:
app/views/users/new.html.erb
<h1>New User</h1>
<%= render 'form', user_form: #user_form %>
<%= link_to 'Back', users_path %>
#app/views/users/_form.html.erb
<%= form_for(user_form, url: users_path) do |f| %>
<% if user_form.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(user_form.errors.count, "error") %> prohibited this User from being saved:</h2>
<ul>
<% user_form.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<%= f.fields_for :emails do |email_form| %>
<div class="field">
<%= email_form.label :email_text %>
<%= email_form.text_field :email_text %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>

Defining an object in another model in Rails

I have migrated the :bank_name and :bank_account objects in User model.
I want two objects can be define from the Listings model in the listings/view to the User model columns.
I have already done (belongs_to, has_many)relations between two models.
But when I filled the bank_name and bank_account text_fields in Listing/view, I get the following error:
undefined method `bank_name' for #Listing:400123298
Here is my listing/view code:
<%= form_for(#listing, :html => { :multipart => true }) do |f| %>
...
<div class="form-group">
<%= f.label :bank_name %><br>
<%= f.text_field :bank_name, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :bank_account %><br>
<%= f.text_field :bank_account, class: "form-control" %>
</div>
</end>
listing/controller:
def new
#listing = Listing.new
end
def create
#listing = Listing.new(listing_params)
#listing.user_id = current_user.id
#listing.user_id = User.bank_name.build(params[:bank_name])
#listing.user_id = User.bank_account.build(params[:bank_account])
end
Several issues for you
Nested
As mentioned in the comments, what you're looking at is a nested model structure.
Simply, this means you'll be able to create an associative model from your "parent" - giving you the ability to define the attributes you need in your "parent" model, passing them through to the nested. This functionality is handled by accepts_nested_attributes_for in your parent model
The best resource you can use is this Railscast (only the start):
--
Fix
Here's how you can fix the problem:
#app/models/listing.rb
class Listing < ActiveRecord::Base
belongs_to :user
accepts_nested_attributes_for :user
end
#app/models/user.rb
class User < ActiveRecord::Base
has_one :bank_account
accepts_nested_attributes_for :bank_account
end
#app/models/bank_account.rb
class BankAccount < ActiveRecord::Base
belongs_to :user
end
#app/controllers/listings_controller.rb
class ListingsController < ApplicationController
def new
#listing = current_user.listings.new
#listing.user.build_bank_account
end
def create
#listing = Listing.new listing_params
#listing.save
end
private
def listing_params
params.require(:listing).permit(:listing, :params, user_attributes: [ bank_account_attributes: [] ])
end
end
This will help you do the following:
#app/views/listings/new.html.erb
<%= form_for #listing do |f| %>
...
<%= f.fields_for :user do |u| %>
<%= u.fields_for :bank_account do |b| %>
<%= b.text_field :name %>
<%= b.text_field :number %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
There is a slight twist to this tail, in that I'm not sure whether your passing of attributes through to your User model. This would be okay if the user was being created at the same time as your other attributes, but as it isn't, we may need to refactor the process of passing the nested data through
If this does not work, please comment & we can work to fix it!

Resources