Nested association not saving - ruby-on-rails

I have a nested association for a customer. The setup is like this:
A customer has one address
An address has a physical_address
An address has a postal_addrsss
Address model
belongs_to :customer, optional: true # Yes, optional
has_one :physical_address, dependent: :destroy
has_one :postal_address, dependent: :destroy
accepts_nested_attributes_for :physical_address
accepts_nested_attributes_for :postal_address
Postal Address model
# same for physical_address.rb
belongs_to :address
Customer controller:
def create
#customer = current_user.customers.build(customer_params)
if #customer.save
return puts "customer saves"
end
puts #customer.errors.messages
#redirect_to new_customer_path
render :new
end
private
def customer_params
params.require(:customer).permit(
address_attributes: address_params
)
end
def address_params
return ([
postal_address_attributes: shared_address_params,
#physical_address_attributes: shared_address_params
])
end
def shared_address_params
params.fetch(:customer).fetch("address").fetch("postal_address").permit(
:street, etc...
)
end
Customer model:
has_one :address, dependent: :destroy
accepts_nested_attributes_for :address
A customer is created ok but not the address. Here's the form, for example:
<form>
<input name="customer[address][postal_address][street]" value="Foo Street" />
</form>
Logging "params", I see all the values but address is not creating. I believe the error lies in shared_address_params. Any ideas?

I think you just managed to lose yourself in layers of indirection and complexity in that parameters whitelist.
What you basically want is:
def customer_params
params.require(:customer)
.permit(
address_attributes: {
physical_address_attributes: [:street, :foo, :bar, :baz],
postal_address: [:street, :foo, :bar, :baz]
}
)
end
As you can see here you need the param key customer[address_attributes] not just customer[address].
Now lets refactor to cut the duplication:
def customer_params
params.require(:customer)
.permit(
address_attributes: {
physical_address_attributes: address_attributes,
postal_address: address_attributes
}
)
end
def address_attributes
[:street, :foo, :bar, :baz]
end
As you can see there should be very little added complexity here any and if you need to make it more flexible add arguments to the address_attributes method - after all building the whitelist is just simple array and hash manipulation.
If you want to handle mapping some sort of shared attributes to the two address types you really should do it in the model instead of bloating the controller with business logic. Like for example by creating setters and getters for a "virtual attribute":
class Address < ApplicationController
def shared_address_attributes
post_address_attributes.slice("street", "foo", "bar", "baz")
end
def shared_address_attributes=(**attrs)
# #todo map the attributes to the postal and
# visiting address
end
end
That way you would just setup the form and whitelist it like any other attribute and the controller doesn't need to be concerned with the nitty gritty details.
def customer_params
params.require(:customer)
.permit(
address_attributes: {
shared_address_attributes: address_attributes,
physical_address_attributes: address_attributes,
postal_address: address_attributes
}
)
end

Related

Split table into two associated tables

I have an app that has a blog feature. Originally when I set this up a post post consisted of a title, body, keywords, and image link. I want to be able to filter posts based on keywords and I think the cleanest way to do this is to move keyword to their own table and associate the two tables. So each posts can have multiple keywords and each keyword can have multiple posts.
I created a migrations and model for keywords.
class CreateKeywords < ActiveRecord::Migration[6.1]
def change
create_table :keywords do |t|
t.string :keyword
t.timestamps
end
end
end
class Keyword < ApplicationRecord
has_and_belongs_to_many :posts
end
I associated that with the posts table and changed the posts model.
class CreatePostsKeywordsJoinTable < ActiveRecord::Migration[6.1]
def change
create_join_table :posts, :keywords do |t|
t.index [:post_id, :keyword_id]
t.index [:keyword_id, :post_id]
end
end
end
class Post < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :keywords
validates :title, presence: true
validates :body, presence: true
accepts_nested_attributes_for :keywords
# validates :keywords, presence: true
end
For now I just commented out the keywords in the Post model. I'm not exactly sure if I need to remove it or not. I already have existing posts that I don't want to lose as part of this switchover so I'm trying to keep that I mind as I figure out how to make this work. Where I'm really confused is what I need to change in the controller.
This is my Post Controller:
require 'pry'
class Api::V1::PostController < ApiController
before_action :authorize_user, except: [:index, :show]
# INDEX /post
def index
render json: Post.all, each_serializer: PostSerializer
end
# SHOW /post/1
def show
render json: Post.find(params[:id]), serializer: PostShowSerializer
end
# CREATE /post/new
def create
binding.pry
post = Post.new(post_params)
post.user = current_user
if post.save
render json: post
else
render json: { errors: post.errors.full_messages }
end
end
# UPDATE /post/update
def update
post = Post.find(params[:id])
if post.update(post_params)
render json: post
else
render json: { errors: post.errors.full_messages }
end
end
# DESTROY /post/destroy
def destroy
post = Post.find(params[:id])
if post.destroy
render json: {destroyed: true}
end
end
protected
def post_params
params.require(:post).permit([:title, :body, :image, :keywords])
end
def authorize_user
if !user_signed_in? || current_user.role != "admin"
redirect_to root_path
end
end
end
In the above state when I get to this part post = Post.new(post_params) I get an error saying NoMethodError (undefined method 'each' for "authorlife":String). If I remove keywords from the post_params I get this error Unpermitted parameter: :keywords
I feel like I am missing one or more steps here but it's been awhile since I've done anything with associated tables like this.
UPDATE:
Followed some of the advice below and I updated the above code to how it currently looks. Current issue is that when I check post_parms in the #create method I'm no longer receiving keywords at all. I checked the frontend and it's sending keywords. I'm assuming it's my post_params that's causing the problem. I've tried adding the keywords nested attribute like this but keywords still isn't showing up in the post_params
def post_params
params.require(:post).permit(:title, :body, :image, :keywords_attributes => [:id, :keyword])
end
This is the WIP for the code I'm trying to implement. I'm not sure what the keywords part is supposed to look like once I get the params situation figured out.
params = { post: {title: post_params["title"], body: post_params["body"], image: post_params["image"], keywords_attributes: [{ keyword: 'keyword title' },]
}}
In the above state when I get to this part post = Post.new(post_params) I get an error saying NoMethodError (undefined method 'each' for "authorlife":String). If I remove keywords from the post_params I get this error Unpermitted parameter: :keywords
You need to setup nested attributes for keywords if you want to update them through a post.
class Post < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :keywords
validates :title, presence: true
validates :body, presence: true
accepts_nested_attributes_for :keywords
end
You can then pass in params structured like this in your controller
params = { post: {
title: 'title', body: "body", keywords_attributes: [
{ text: 'keyword title' },
]
}}
post = Post.create(params[:post])
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
I already have existing posts that I don't want to lose as part of this switchover so I'm trying to keep that I mind as I figure out how to make this work.
It's good practice to remove this not used data anymore. You should write a data migration which moves the existing keywords from the posts table to the keywords table. Something like this
class KeywordsMigrator
def run
Post.all.each do |post|
keyword = Keyword.find_or_create_by(title: post.keyword)
post.keywords << keyword
end
end
end
Finally you can drop the keyword column from post.
You haven't really mentioned the current structure of the posts table so I assume you have a keyword column there. If you have a keywords column you have to name your association different until you remove the column otherwise you will run into troubles. For example you rename it to keywords_v1 and specify the class_name.
class Post < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :keywords_v1, class_name: "Keyword"
validates :title, presence: true
validates :body, presence: true
end
Or you rename the column first to something like deprecated_keywords.

Rails ActiveModel::ForbiddenAttributesError - ActiveModel::ForbiddenAttributesError в simple_form nested_fields

I experience a trouble while saving simple_form.fields_for - forbidden attributes error
'create' action in bookings controller looks so:
def create
...
new_params = params[:booking]
new_params[:user_attributes] = new_params[:user_attributes].merge({"password"=>"osmsmsmsm32"}) # password is temp stuff to bypass User save validation
#booking = Booking.new
#booking.update(params)
# however #booking.user.update(params[:booking][:user_attributes]) gives the same error
...
end
...
def booking_params
params.require(:booking).permit(:arrived_at, :departured_at, :arrival_address,
:departure_address, :arrival_city, :departure_city,
:reservation_cost, :total_additional_cost, :user_attributes, :user_id, :garage_id,
user_attributes: [:id, :name, :surname, :email, :phone],
garage_attributes: [:id]
)
end
===========================
Booking:
belongs_to :user
accepts_nested_attributes_for :user
===========================
##In model User:
has_many :bookings
However #booking.user.save & #booking.save in irb console with same params are successfully saveable and true is passed, without any Forbidden Attribute error.
Where is this Forbidden attribute come from? I am sure I allowed all the attrs I send in the form, and I think I use accepts_nested_attributes_for properly, isn't it?
Just Define your user_attributes inside controller private method as per below:
private
def user_params
params.require(
:user
).permit(
:first_name,
:last_name,
:job_title
)
end
if you are working with nested filed just add nested attributes inside this attributes like below:
private
def user_params
params.require(
:user
).permit(
:first_name,
:last_name,
:job_title,
addresses_attributes: [:address_1, :address_2]
)
end
write nested attributes by take in mind your model associations,
Hope this will work for you. :)

Rails: Unpermitted parameter in Rails 5

First of all I want simply get an object inside the current object that I'm sending to my backend.
I have this simple JSON (generated from a form):
{
"name": "Project 1",
"project_criteria": [
{
"name": "Criterium 1",
"type": "Type 1",
"benefit": "1"
},
{
"name": "Criterium 2",
"type": "Type 2",
"benefit": "3"
}
]
}
My classes:
class Project < ApplicationRecord
has_many :project_criteria
accepts_nested_attributes_for :project_criteria
end
class ProjectCriterium < ApplicationRecord
belongs_to :project
end
ProjectsController:
def project_params
params.require(:project).permit(:name, project_criteria: [] )
end
But I still can't access project_criteria parameter as you can see below:
Started POST "/projects" for 127.0.0.1 at 2016-08-19 16:24:03 -0300
Processing by ProjectsController#create as HTML
Parameters: {"project"=>{"name"=>"Project 1", "project_criteria"=>{"0"=>{"benefit"=>"1", "name"=>"Criterium 1", "type"=>"Type 1"}, "1"=>{"benefit"=>"3", "name"=>"Criterium 2", "type"=>"Type 2"}}}}
Unpermitted parameter: project_criteria # <-----------
Note:
By the way, I already tried to use criterium instead of criteria(which - in my opinion - is the correct since it should be pluralized) in has_many and accepts_nested_attributes_for, but it also doesn't work.
Does someone have a solution for this?
It's not the inflection of the word "criteria" that's giving you problems (although you can add a custom inflector to get the singular and plural versions you prefer if you really want).
The issue is that you have to explicitly permit the fields of nested objects.
Change your current params:
params.require(:project).permit(:name, project_criteria: [] )
To this (for a single nested object):
params.require(:project).permit(:name, project_criteria: [:name, :type, :benefit] )
Your case is somewhat compounded by the fact that you're dealing with multiple nested objects, so you'll have to pass a hash instead:
params.require(:project).permit(:name, { project_criteria: [:name, :type, :benefit]} )
I had this issue when working on a Rails 6 application.
My application consists of a User model that has a one-to-one relationship a Personal_Info model
My original code was this:
User Model
class User < ApplicationRecord
has_one :personal_info, class_name: 'PersonalInfo', dependent: :destroy
accepts_nested_attributes_for :personal_info, allow_destroy: true
end
Personal Info Model
class PersonalInfo < ApplicationRecord
belongs_to :user
end
User Controller
class UsersController < ApplicationController
def index
#users = User.all
end
.
.
def user_params
params.require(:user).permit(:email, :password, :password_confirmation,
personal_info_attributes: [:first_name,
:last_name, :phone, :gender, :dob,
:address, :city, :state, :country])
end
end
The issue was that I did not add the Personal_Info id to the accepted user params (parameters).
Here's how I fixed it:
I simply had to add the Personal_Info id to the UsersController params this way:
User Controller
class UsersController < ApplicationController
def index
#users = User.all
end
.
.
def user_params
params.require(:user).permit(:email, :password, :password_confirmation,
personal_info_attributes: [:id, :first_name,
:last_name, :phone, :gender, :dob,
:address, :city, :state, :country])
end
end
Another way is to add the update_only option to the Users Model this way:
class User < ApplicationRecord
has_one :personal_info, class_name: 'PersonalInfo', dependent: :destroy
accepts_nested_attributes_for :personal_info, update_only: true, allow_destroy: true
end
That's all.
I hope this helps

Rails accepts_nested_attributes_for associated models not created

I have two models (Company and User) that have a belongs_to/has_many relationship.
class Company < ActiveRecord::Base
attr_accessor :users_attributes
has_many :users
accepts_nested_attributes_for :users, allow_destroy: true
end
class User < ActiveRecord::Base
belongs_to :company
end
In my CompaniesController I want to create a new instance of Company along with a group of Users.
class Cms::CompaniesController < ApplicationController
def create
company = Company.new(company_params)
respond_to do |format|
if company.save
format.json { render json: company, status: :ok }
else
format.json { render json: company.errors.messages, status: :bad_request }
end
end
end
private
def company_params
params.require(:company).permit(
:id,
:name,
users_attributes: [
:id,
:_destroy,
:first_name,
:last_name,
:email
]
)
end
end
When I call company.save, I would expect a new instance of Company along with several new instances of User to be saved, depending on how many users I have in my params, however no users are persisted.
Here is a sample of what company_params looks like:
{"id"=>nil, "name"=>"ABC", "users_attributes"=>[{"first_name"=>"Foo", "last_name"=>"Bar", "email"=>"foo#bar.com"}]}
What am I missing here?
Remove attr_accessor:
class Company < ActiveRecord::Base
has_many :users
accepts_nested_attributes_for :users, allow_destroy: true
end
Everything else should work.
--
attr_accessor creates getter/setter methods in your class.
It's mostly used for virtual attributes (ones which aren't saved to the database). Your current setup is preventing you from being able to save the users_attributes param, thus your users are not saving.

Rails 4 strong params get permit from nested model

There are several questions for strong params, but I couldn't find any answer for achieving my goal. Please excuse any duplicates (and maybe point me in the right direction).
I'm using strong params in a model that has several 'has_one' associations and nested attributes with 'accepts_attributes_for'.
In my routes I have: (updated for better understanding)
resources :organisations do
resources :contact_details
end
So, i.e. for one associated model I have to use
def organisation_params
params.require(:organisation).permit(:org_reference, :supplier_reference, :org_type, :name, :org_members, :business, :contact_person, contact_detail_attributes: [:id, :contactable_id, :contactable_type, :phone, :fax, :mail, :state, :province, :zip_code, :street, :po_box, :salutation, :title, :last_name, :first_name, :description])
end
This works, but I have to retype all my permitted params for each associated model. When I modify my permitted attributes for contact_details , I have to change it in several locations (every model that has the polymorphic association).
Is there a way to get the parameter whitelist of contact_details and include it into the parent whitelist?
Something like:
def organisation_params
my_params = [:org_reference, :supplier_reference, :org_type, :name, :org_members, :business, :contact_person]
contact_params = #get permitted params, that are defined in contact_details_controller
params.require(:organisation).permit(my_params, contact_params)
end
I don't want to workaround security, but I had already defined the permitted attributes for the contact_details and don't want to repeat it in every associated "parent" model (because it's exhausting and very prone to stupid mistakes like omitting one attribute in one of several parent models).
Use a method defined inside ApplicationController, or a shared module:
ApplicationController:
class ApplicationController
def contact_details_permitted_attributes
[:id, :contactable_id, :contactable_type, ...]
end
end
class ContactDetailsController < ApplicationController
def contact_details_params
params
.require(contact_details)
.permit(*contact_details_permitted_attributes)
end
end
class OrganisationsController < ApplicationController
def organisation_params
params
.require(:organisation)
.permit(:org_reference, ...,
contact_detail_attributes: contact_details_permitted_attributes)
end
end
Shared module:
module ContactDetailsPermittedAttributes
def contact_details_permitted_attributes
[:id, :contactable_id, :contactable_type, ...]
end
end
class ContactDetailsController < ApplicationController
include ContactDetailsPermittedAttributes
def contact_details_params
params
.require(contact_details)
.permit(*contact_details_permitted_attributes)
end
end
class OrganisationsController < ApplicationController
include ContactDetailsPermittedAttributes
def organisation_params
params
.require(:organisation)
.permit(:org_reference, ...,
contact_detail_attributes: contact_details_permitted_attributes)
end
end
Rails has even dedicated directories for shared modules, concerns inside app/controllers and app/models; indeed, in your case you should use app/controllers/concerns
I don't see why not. In your ApplicationController you could have
def contact_attributes
[:id, :contactable_id, :contactable_type, :phone, :fax,
:mail, :state, :province, :zip_code, :street, :po_box,
:salutation, :title, :last_name, :first_name, :description]
end
Then in your organisation_params
def organisation_params
my_params = [:org_reference, :supplier_reference, :org_type, :name, :org_members, :business, :contact_person]
params.require(:organisation).permit(*my_params, contact_detail_attributes: contact_attributes)
end
In some other location you might do...
def contact_params
params.require(:contact).permit(*contact_attributes)
end

Resources