Rails POSTing JSON for accepts_nested_attributes - ruby-on-rails

I am playing around with Rails and Angular and trying to get a simple association to update via a JSON PUT request from the frontend.
Association: Article has_many :categories, through: :article_categories
Article model:
class Article < ActiveRecord::Base
validates :title, presence: true, uniqueness: true
validates :body, presence: true
has_many :article_categories
has_many :categories, through: :article_categories
accepts_nested_attributes_for :categories
end
I've got no issues updating the title and body, but I cannot update the article's categories.
Here are the relevant controller parts
def update
#article = Article.find(params[:id])
if #article.update(article_params)
render json: #article, status: 200
end
end
private
def article_params
params.require(:article).permit(:title, :body,
categories_attributes: [:name, :id])
end
My incoming JSON looks like this, spaced out to make it more readable:
Started PUT "/api/v1/articles/6" for 127.0.0.1 at 2014-06-01 17:53:04 +0900
Processing by Api::V1::ArticlesController#update as HTML
Parameters: {"title"=>"new title", "body"=>"blablabla", "id"=>"6", "categories"=>
[{"name"=>"Ruby", "id"=>1}, {"name"=>"Javascript", "id"=>2}, {"name"=>"HTML", "id"=>3},
{"name"=>"CSS", "id"=>4}],
"categories_attributes"=>[{"name"=>"Ruby", "id"=>1},
{"name"=>"Javascript", "id"=>2}, {"name"=>"HTML", "id"=>3}, {"name"=>"CSS", "id"=>4}],
"article"=>{"id"=>"6", "title"=>"new title", "body"=>"blablabla"}}
The only feedback I get is that article id isn't a whitelisted param. Isn't the categories_attributes what Rails looks for when it takes nested attributes? Why isn't it complaining about the categories params not being whitelisted?

We've had this problem before - you're basically bypassing the join model, which is preventing your application from working correctly.
Nested Association
Basically, you need to pass your associated data to your article_categories model before passing the categories data:
#app/models/article.rb
Class Article < ActiveRecord::Base
...
accepts_nested_attributes_for :article_categories
end
#app/models/article_category.rb
Class ArticleCategory < ActiveRecord::Base
belongs_to :category
belongs_to :article
accepts_nested_attributes_for :category
end
#app/controllers/articles_controller.rb
def new
#article = Article.new
#article.article_categories.build.build_category
end
def create
#article = Article.new(article_params)
#article.save
end
private
def article_params
params.require(:article).permit(:article, :attributes, article_categories_attributes: [categories_attributes: [:category, :options]] )
end
#app/view/articles/new.html.erb
<%= form_for #article do |f| %>
<%= f.fiels_for :article_categories do |ac| %>
<%= ac.fields_For :categories do |c| %>
<%= c.text_field :your_field &>
<% end %>
<% end %>
<% end %>

There were a couple issues here:
My json format was incorrect. The categories were not nested in article and that's why rails wasn't throwing validation errors. I changed the angular frontend to post this:
{"article"=>{"title"=>"sdfsd", "body"=>"sdf", "category_ids"=>[1, 2, 3]}}
My angular $scope contained both the category ID and name, so I had to write a function to parse out the IDs and dump them in an array. Annoying.
Next, creating an article with this JSON format was failing because of the validations on ArticleCategory. I added the inverse_of to my models as described here https://github.com/rails/rails/issues/5178 and then the validations would pass when creating a new article with categories and bypassing the join model. If I understand things correctly, this is an alternative solution to Richard Peck's answer.
The final models looked like this:
class Article < ActiveRecord::Base
validates :title, presence: true, uniqueness: true
validates :body, presence: true
has_many :article_categories, inverse_of: :article
has_many :categories, through: :article_categories
end
class Category < ActiveRecord::Base
validates :name, presence: true, uniqueness: true
has_many :article_categories, inverse_of: :category
has_many :articles, through: :article_categories
end
class ArticleCategory < ActiveRecord::Base
belongs_to :article, inverse_of: :article_categories
belongs_to :category, inverse_of: :article_categories
validates :category, :article, presence: true
end

Related

Rails: Structuring rental orders & order form in ecommerce-like setting

I was wondering if someone could help me out with an application that has some ecommerce characteristics.
Context: Via the application a bike shop chain ('chains') can rent out
bikes ('bikes'),
by picking out a bike type such as mountainbike, city bike etc. ('bike_types) and
bike options, such as helmets etc. ('bike_options')
which are dependent on the individual bike store ('bike_stores')
this rental of the bikes & options will all be captured in an order ('orders')
the relationship between orders and bikes is many-to-many, therefore I created a table to bridge this ('order_bikes')
Final notes:
Before the rental process, the chain owner first created his/her (i) bike_stores, (ii) bike_types, (iii) bikes and (iv) bike_options, this part of the application is working. Therefore, he/she only needs to select bike_types/bikes/options out of the existing inventory previously created.
I limit the scope of the question by leaving out the bike_options, this was mainly to provide some context in order to understand the db schema build up.
Error message: Unpermitted parameter: :bike_id
Code:
models
class Order < ApplicationRecord
belongs_to :bike_store
has_many :bike_types, through: :bike_store
has_many :order_bikes, inverse_of: :order, dependent: :destroy
accepts_nested_attributes_for :order_bikes, allow_destroy: true
end
class OrderBike < ApplicationRecord
belongs_to :bike
belongs_to :order
accepts_nested_attributes_for :bike
end
class Bike < ApplicationRecord
belongs_to :bike_type
validates :name, presence: true
has_many :order_bikes
has_many :orders, through: :order_bikes
end
class BikeType < ApplicationRecord
belongs_to :bike_store
has_many :bikes, dependent: :destroy
accepts_nested_attributes_for :bikes, allow_destroy: true
has_many :bike_options, dependent: :destroy
accepts_nested_attributes_for :bike_options, allow_destroy: true
validates :name, :bike_count, presence: true
end
class BikeStore < ApplicationRecord
has_many :bike_types, dependent: :destroy
has_many :orders, dependent: :destroy
end
Order controller
class OrdersController < ApplicationController
def new
#bike_store = BikeStore.find(params[:bike_store_id])
#order = Order.new
#order.order_bikes.build
#bike_type_list = #bike_store.bike_types
end
def create
#order = Order.new(order_params)
#bike_store = BikeStore.find(params[:bike_store_id])
#order.bike_store = #bike_store
#order.save
redirect_to root_path
end
private
def order_params
params.require(:order).permit(:arrival, :departure,
order_bikes_attributes: [:id, :bike_quantity, :_destroy,
bikes_attributes: [:id, :name,
bike_types_attributes: [:id, :name]]])
end
end
view
<%= simple_form_for [#bike_store, #order] do |f|%>
<%= f.simple_fields_for :order_bikes do |order_bike| %>
<%= order_bike.input :bike_quantity %>
<%= order_bike.association :bike %>
<% end %>
<%= f.input :arrival %>
<%= f.input :departure %>
<%= f.submit %>
<% end %>
If you check coed from simple form here, you will see what actually method association does.
def association(association, options = {}, &block)
# ... simple form code here ...
attribute = build_association_attribute(reflection, association, options)
input(attribute, options.merge(reflection: reflection))
end
We are interested in build_association_attribute method call. here
def build_association_attribute(reflection, association, options)
case reflection.macro
when :belongs_to
(reflection.respond_to?(:options) && reflection.options[:foreign_key]) || :"#{reflection.name}_id"
# ... the rest of code ...
end
end
Your order bike model has belongs_to :bike association. So when you call order_bike.association :bike it builds :bike_id attribute in your form. If you check params hash that comes to your controller, I believe you'll see that attribute coming from your view.
I added bike_id to permitted parameters. I hope it will fix your problem..
def order_params
params.require(:order).permit(:arrival, :departure,
order_bikes_attributes: [:id, :bike_id, :bike_quantity, :_destroy,
bikes_attributes: [:id, :name,
bike_types_attributes: [:id, :name]]])
end

Error when submitting form: Nested attributes unpermitted parameters

I've tried the solutions on this other stack overflow question but they aren't working.
I'm getting this error when submitting my form: Unpermitted parameter: organization_required_fields
Any help would be appreciated.
I have the following models:
class Organization < ActiveRecord::Base
belongs_to :user
has_many :organization_required_fields
has_many :fields, through: :organization_required_fields
accepts_nested_attributes_for :organization_required_fields, allow_destroy: true
end
class OrganizationRequiredField < ActiveRecord::Base
belongs_to :organization
belongs_to :field
end
class Field < ActiveRecord::Base
has_many :organization_required_fields
has_many :organizations, through: :organization_required_fields
end
My controller:
def update
...
#organization.update(organization_params)
...
end
private
def set_organization
#organization = Organization.find_by_id(params[:id])
...
end
def organization_params
params.require(:organization).permit(:name, :user_id, organization_required_fields_attributes: [:id, :organization_id, :field_id, :_destroy])
end
My form view
...
= f.select :organization_required_fields, options_for_select(#fields.collect {|rf| [ rf.name.titleize, rf.id ] }, #organization.fields.collect{ |orf| orf.id }),{ :prompt => "Please select"},{ :multiple => true, :size => 15 }
...
I actually changed a lot by following this rails cast
I also had to change the organization_params to
params.require(:organization).permit(:name, :user_id, { field_ids: []})
You need to use fields_for in your form.

How do I access the extra attribute on join table in my show page?

My models look like this:
class Project < ApplicationRecord
has_many :comments
has_many :contractor_projects
has_many :contractors, through: :contractor_projects
validates_presence_of :title, :contract_number, :category, :project_start_date, :project_end_date, :substantial_completion_date, :category, :solicitation_number, :project_officer, :location
accepts_nested_attributes_for :contractor_projects
end
class Contractor < ApplicationRecord
has_many :contractor_projects
has_many :projects, through: :contractor_projects
validates :name, presence: true
validates :email, presence: true, uniqueness: true
end
class ContractorProject < ApplicationRecord
belongs_to :contractor
belongs_to :project
end
The ContractorProject model has an extra attribute #bid_status that I want to reflect on project's show page but it does not appear even though it's in the params when i raised it.
below is sample method for your case
def show
#project = Project.find(params[:id]
#contractors = #project.contractors
end
inside show.html.erb, you have to loop it, since it may get more than one records
<% #contractors.each do |contractor| %>
<%= contractor.bid_status %>
<% end %>

Rails 4 strong parameters does not work for a polymorphic association

I have set a polymorphic association and added a nested form in the view. Im trying to create the main record and the association at the same time. The main record gets created but the association won't.
Here are the two models in question :
class UnRegistered < ActiveRecord::Base
has_one :vehicle, as: :detailable, dependent: :destroy
belongs_to :dealer
class Vehicle < ActiveRecord::Base
belongs_to :purchase_details, polymorphic: true
belongs_to :brand
belongs_to :model
belongs_to :color
belongs_to :customer
Here's the form definitions :
<%= form_for(#un_registered, url: panel_un_registereds_path, remote: true) do |f| %>
<%= f.fields_for :vehicle do |f_vehicle| %>
Here's a sample params set I get :
{"utf8"=>"✓", "un_registered"=>{"vehicle"=>{"brand_id"=>"", "model_id"=>"", "year"=>"", "engine_number"=>"gdfg", "chassis_number"=>"", "color"=>"", "options"=>""}, "original_price"=>"", "insurance"=>"", "freight"=>"", "tt"=>"", "tt_date"=>"", "duty"=>"", "clearance_fee"=>"", "other_expenses"=>"", "dealer_id"=>"", "landing_date"=>"", "loading_date"=>""}, "controller"=>"panel/un_registereds", "action"=>"create"}
Here's the controller actions :
def create
#un_registered = UnRegistered.create(un_registered_params)
end
def un_registered_params
params.require(:un_registered).permit(:original_price, :insurance, :freight, :tt, :tt_date, :duty, :clearance_fee, :other_expenses, :loading_date, :landing_date, :dealer_id, vehicle_attributes: [:id, :brand_id, :model_id, :engine_number, :chassis_number, :color_id, :year, :options, :selling_price, :customer_id, :purchase_date, :_destroy])
end
Full form code :
https://gist.github.com/THPubs/9665e0e5594e15fcc76a
New method :
def new
#un_registered = UnRegistered.new
end
Your form is fine. You just need to add below changes.
In your un_registered.rb model
class UnRegistered < ActiveRecord::Base
has_one :vehicle, as: :detailable, dependent: :destroy
belongs_to :dealer
accepts_nested_attributes_for :vehicle #this one
end
And in your controller,
def new
#un_registered = UnRegistered.new
#un_registered.build_vehicle #this one
end

How to add quantity to a rails form (create n records when submitting)?

I have a library-like booking system. I want to make a form for adding books in stock, allowing the user to choose a book and choose a library (both are collection_select). Book has a many-to-many relationship with Library, through the stock_items table.
What I can't figure out is how can bring in the quantity, so that the user can add a number of instances of the same book to a chosen university. How should I approach implementing this quantity-type feature. It should create a chosen amount of records in the join table.
Here's my form (currently creates only 1 instance at a time):
<%= form_for(#item) do |f| %>
<%= f.label :choose_book %>
<%= f.collection_select(:book_id, Book.all, :id, :name, prompt: true) %>
<%= f.label :choose_library %>
<%= f.collection_select(:library_id, Library.all, :id, :name, prompt: true) %>
<%= f.submit "Add item in stock", class: "btn btn-info" %>
<% end %>
StockItem model
class StockItem < ActiveRecord::Base
belongs_to :library
belongs_to :book
has_many :bookings, foreign_key: :stock_id, dependent: :destroy
validates :availability, presence: true
validates :library_id, presence: true
end
Library model
class Library < ActiveRecord::Base
has_many :stock_items
has_many :books, through: :stock_items
end
Book model
class Book < ActiveRecord::Base
validates :year_of_publication, presence: true, length: { maximum: 4 }
validates :description, presence: true, length: { minimum: 10 }
validates :name, presence: true
has_many :stock_items, dependent: :destroy
has_many :libraries, through: :stock_items
has_many :contributions, dependent: :destroy
has_many :authors, through: :contributions
has_many :bookings, through: :stock_items
has_many :book_images, dependent: :destroy
accepts_nested_attributes_for :book_images
accepts_nested_attributes_for :authors
accepts_nested_attributes_for :libraries
accepts_nested_attributes_for :stock_items
accepts_nested_attributes_for :contributions
validates :name, presence: true
end
A bit of the StockItemsController
def create
#item = StockItem.new(item_params)
if #item.save
flash[:success] = "Item added to stock"
redirect_to stock_items_path
else
flash[:danger] = "Item has not been added to stock!"
render 'new'
end
end
def new
#item = StockItem.new
end
private
def item_params
params.require(:stock_item).permit(:library_id, :book_id, :availability)
end
I think your answer is systemic, rather than syntaxic, meaning you have to consider your system rather than the specific action syntax
--
M-to-M
You specifically need to look at the many-to-many association you've deployed here
Your population of the StockItem table is basically a way to create a collection for each library, and so you should look at the many collection based methods which ActiveRecord provides (specifically << and .delete)
Bottom line is that instead of creating new records for your StockItem model, I would just add to the collection of both library - giving you the files you need to make it work
--
Collection
As you're using has_many :through, you can add multiple instances of the same record to your collection (as opposed to has_and_belongs_to_many, which only permits single instances of records, as there is no primary_key)
This means you'll be able to just add books individually to the collection, and then use the sum method of SQL to calculate the quantity:
#app/controllers/stock_items_controller.rb
class StockItemsController < ApplicationController
def create
#library = Library.find params[:stock_item][:library_id]
#book = Book.find params[:stock_item][:book_id]
#library << #book
end
end
--
Quantity
This is opposed to the idea that you can add a qty attribute to your StockItem model, and then use the increment! method:
#app/controllers/stock_items_controller.rb
Class StockItemsController < ApplicationController
def create
#item = StockItem.find_by(library_id: params[:stock_item][:library_id],book_id: params[:stock_item][:book_id])
#item ? #item.increment!(:qty) : StockItem.create(stockitem_params)
end
end

Resources