Accepting polymorphic associations through checkbox in rails - ruby-on-rails

I have got a Products class,Products are visible to zero or many roles. So, I have created a polymorphic model called content_roles,which stores the id of the role and content_id (which will be product_id,or event_id),and content_type(product,event etc).
I am using nested_form gem for accepting the role id(using check_box) to store the product and role relation in content_role
I get an error no implicit conversion of String into Integer in Products#create function
Parameters: {"utf8"=>"✓", "authenticity_token"=>"xxxxxxxxxxxxxxxxxxxxdLH99ZWLrf8dgT3gcBops=", "product"=>{"product_name"=>"some product", "product_description"=>"some product description", "content_roles_attributes"=>{"role_id"=>["1", "2", ""]}}, "commit"=>"Create Product"}
in my view I have written
= f.simple_fields_for :content_roles_attributes do |role|
= role.input :role_id,label: "visible to", as: :check_boxes,label: "Role",collection: Role.all,:required=>true
the controllers permitted params looks like
def create
#Getting the error at this line
#product = Product.new(product_params)
respond_to do |format|
if #product.save
end
def product_params
params.require(:product).permit(:product_description,:product_name,
content_roles_attributes: [:id,role_id: []],
multimedia_attributes:[:asset,:_destroy,:id])
end
the product model looks like:
class Product
has_many :content_roles, as: :content
has_many :multimedia ,as: :storable
# Nested attributes
accepts_nested_attributes_for :multimedia
accepts_nested_attributes_for :content_roles
end
and this is the content_role model
class ContentRole < ActiveRecord::Base
belongs_to :content, polymorphic: true
belongs_to :role
belongs_to :news
belongs_to :product
end

Related

How do i create a parent and child element at the same time rails

I want to create an Invoice and the regarding InvoiceItems at the same time. While Invoice has_many :invoice_items and an InvoiceItem belongs_to :invoice. How do I perform such action in Rails 7 so that a User can add multiple invoiceItems to their invoice via Turbo? I dont need to know how TurboStreams and stuff work, since I am familiar, but I just cant get the InvoiceItems to be created at the same time as the Invoice.
I already found this post, but could not get any useful information out of it.
Models
Invoice.rb
class Invoice < ApplicationRecord
belongs_to :project
has_many :invoice_items, foreign_key: :invoice_id # not sure if this foreign_key is necessary
accepts_nested_attributes_for :invoice_items
end
invoice_item.rb
class InvoiceItem < ApplicationRecord
belongs_to :invoice
end
Controllers
Invoice_controller.rb
def create
#project = Project.find(params[:project_id])
#client = Client.find(params[:client_id])
#invoice = #project.invoices.new(invoice_params)
#invoice_item = #invoice.invoice_items.new
#invoice.invoice_items_attributes = [:invoice_id, :amount]
#invoice.client_id = #client.id
respond_to do |format|
if #invoice.save
....
def invoice_params
params.require(:invoice).permit(... :invoice_item, invoice_item_attributes: [:id, :invoice_id, :amount, ...])
end
Currently I try using a form_for inside of the Invoice form like:
<%= form.fields_for #invoice.invoice_items.build do |lorem| %>
Which gives me following error in the console (but saves the invoice as expected:
Unpermitted parameter: :invoice_item. Context: { controller: InvoicesController, action: create, request: #<ActionDispatch::Request:0x000000010a0c8d88>, params: {"authenticity_token"=>"[FILTERED]", "invoice"=>{..., "invoice_item"=>{"invoice_id"=>"", "amount"=>"3"}}, "button"=>"", "controller"=>"invoices", "action"=>"create", "user_id"=>"1", "client_id"=>"1", "project_id"=>"1"} }
notice that the invoice_id is not passed to the invoice_item.
Via console something like
#invoice = Invoice.new
#invoice.invoice_items.new(amount: "3", ...)
#invoice.save!
Does work weirdly but it does not translate to my code.
What am I doing wrong here?
# invoice_item_attributes is wrong
def invoice_params
params.require(:invoice).permit(... :invoice_item, invoice_item_attributes: [:id, :invoice_id, :amount, ...])
end
Should be
# invoice_items_attributes is right
def invoice_params
params.require(:invoice).permit(... :invoice_item, invoice_items_attributes: [:id, :invoice_id, :amount, ...])
end
Notice the missing 's'.
https://www.ombulabs.com/blog/learning/rails/nested-forms.html
After following the GoRails screencast on how to properly set nested form attributes in rails, I still came across errors. I eventually could trace them and found this neat post which game the hint to use inverse_of and autosave: true. I am not 100% sure what those do, even though I will read now to find out, but my stuff is working properly now :)
Modified Model
class Invoice < ApplicationRecord
belongs_to :project
has_many :invoice_items, inverse_of: :invoice, autosave: true
accepts_nested_attributes_for :invoice_items
...

Rails 5 is inserting instead of updating when changing the value of attribute in the join table

I have a HABTM relationship using a has_many :through association. I'm having trouble updating the attribute in the join table, because instead of updating the record it just inserts a new record in the table, creating duplicates.
I tried using the UNIQUE constraint when creating the index and adding a validation, but now when I update a record I get a validation error or the error:
ActiveRecord::RecordNotUnique in BatchesController#update
Mysql2::Error: Duplicate entry
To provide some context:
I have 4 tables: manufacturing_orders, order_products, batches and batches_order_products (join table).
A manufacturing_order have many order_products.
Also a manufacturing_order have many batches.
Batches have many order_products.
When I create a batch I copy all the order_products that belongs to the same manufacturing_order, and in the form I can assign a quantity for each of then.
So creating looks like working fine, but when I update any quantity it just inserts the whole relation again instead of updating the existing one.
Model manufacturing_order.rb:
class ManufacturingOrder < ApplicationRecord
has_many :batches, inverse_of: :manufacturing_order, dependent: :destroy
has_many :order_products, inverse_of: :manufacturing_order, dependent: :destroy
accepts_nested_attributes_for :order_products
accepts_nested_attributes_for :batches
end
Model order_product.rb:
class OrderProduct < ApplicationRecord
belongs_to :manufacturing_order
has_many :batches_order_products
has_many :batches, :through => :batches_order_products
end
Model batch.rb:
class Batch < ApplicationRecord
belongs_to :manufacturing_order
has_many :batches_order_products
has_many :order_products, :through => :batches_order_products
accepts_nested_attributes_for :batches_order_products
end
Model batches_order_product.rb:
class BatchesOrderProduct < ApplicationRecord
belongs_to :batch
belongs_to :order_product
validates :batch_id, uniqueness: { scope: :order_product_id }
end
Controller batches_controller.rb:
class BatchesController < ApplicationController
def new
manufacturing_order = ManufacturingOrder.find(params[:manufacturing_order_id])
order_products = manufacturing_order.order_products
#batch = Batch.new({
manufacturing_order: manufacturing_order,
order_products: order_products
})
end
def create
#batch = Batch.new(load_params)
if #batch.save
flash[:notice] = crud_success
redirect_to action: :index
else
flash[:error] = #batch.errors.full_messages.to_sentence
render action: :new
end
end
def edit
#batch = Batch.find(params[:id])
end
def update
#batch = Batch.find(params[:id])
if #batch.update_attributes(load_params)
flash[:notice] = crud_success
redirect_to action: :index
else
flash[:error] = #batch.errors.full_messages.to_sentence
render action: :edit
end
end
private
def load_params
params.require(:batch)
.permit(:name,
:date,
:manufacturing_order_id,
:status,
order_products: [],
order_products_ids: [],
batches_order_products_attributes: [:id, :quantity, :order_product_id]
)
end
end
This is the form in batches:
= bootstrap_form_for([#batch.manufacturing_order, #batch]) do |f|
= f.hidden_field :manufacturing_order_id
= f.text_field :name, label: 'Name'
= f.text_field :date
table
thead
tr
th= "Product"
th= "Quantity"
tbody
= f.fields_for :batches_order_products do |bop|
= bop.hidden_field :order_product_id
tr
td
= bop.object.order_product.name
td
= bop.text_field :quantity
= f.submit 'Save'
Any help will be very much appreciated. Thanks!
UPDATE:
These are the params passed when submitting the edit form. Any clue?
{"utf8"=>"✓",
"_method"=>"patch",
"batch"=>
{"manufacturing_order_id"=>"8",
"name"=>"MAS",
"date"=>"07/05/2020",
"batches_order_products_attributes"=>
{"0"=>{"order_product_id"=>"12", "quantity"=>"77777777", "id"=>""},
"1"=>{"order_product_id"=>"13", "quantity"=>"9.0", "id"=>""},
"2"=>{"order_product_id"=>"14", "quantity"=>"7.0", "id"=>""}}},
"commit"=>"Guardar",
"manufacturing_order_id"=>"8",
"id"=>"7"}
EDIT 2: I updated the nested form to include the id in a hidden field like this:
= f.fields_for :batches_order_products do |bop|
= bop.hidden_field :order_product_id
= bop.hidden_field :id, value: #batch.id
= bop.object.order_product.name
= bop.text_field :quantity, label: ''
BUT now Rails complains of this when updating:
ActiveRecord::StatementInvalid in BatchesController#update
Mysql2::Error: Unknown column 'batches_order_products.' in 'where clause': SELECT `batches_order_products`.* FROM `batches_order_products` WHERE `batches_order_products`.`batch_id` = 9 AND `batches_order_products`.`` IN ('9', '9', '9', '9', '9')
I don't know why Rails adds that last weird part in SQL query.
So I've finally figured it out.
The problem was that the join table needed an ID column to reference to. The table had an index of batch_id and order_product_id but for some reason it didn't work and ActiveRecord was looking for an ID. Adding it solved the problem.
Thanks to #max for giving some points to look at.
class AddIndexToBatchesOrderProductsJoinTable < ActiveRecord::Migration[5.2]
def change
# add_index :batches_order_products, [:batch_id, :order_product_id], unique: true
add_column :batches_order_products, :id, :primary_key
end
end

Update if exists, destroy if empty, create if doesn't exist on array form submit

I am attempting to update multiple records at once but coming to an issue with my validation for uniqueness scope.
When I create a record on a per-record-basis, it does what the title asks. But with the array, it doesn't.
Why?
Because the params don't pass the ID of the nested attribute to update.
Example of param difference of updating on a per record vs array:
Mutli update:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"==", "shop_product_ids"=>["42"], "shop_product"=>{"shop_product_print_files_attributes"=>{"0"=>{"print_location_id"=>"1", "image_file_id"=>"2"}, "1"=>{"print_location_id"=>"2", "image_file_id"=>"2"}, "2"=>{"print_location_id"=>"3", "image_file_id"=>""}, "3"=>{"print_location_id"=>"4", "image_file_id"=>""}, "4"=>{"print_location_id"=>"5", "image_file_id"=>""}, "5"=>{"print_location_id"=>"6", "image_file_id"=>""}}}, "commit"=>"Edit Checked"}
1 Record Update:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"==", "shop_product"=>{... "shop_product_print_files_attributes"=>{"0"=>{"print_location_id"=>"1", "image_file_id"=>"1", "id"=>"145"}, "1"=>{"print_location_id"=>"2", "image_file_id"=>"", "id"=>"151"}, "2"=>{"print_location_id"=>"3", "image_file_id"=>""}, "3"=>{"print_location_id"=>"4", "image_file_id"=>""}, "4"=>{"print_location_id"=>"5", "image_file_id"=>""}, "5"=>{"print_location_id"=>"6", "image_file_id"=>""}},"id"=>"42"}, "commit"=>"Sync", "id"=>"42"}
On the one record update, it passes the shop_product_print_file ID in order to update. On the multi, it doesn't and shouldn't.
Goal: To update if exists, create if it doesn't destroy if empty. I need to somehow do what I do in the model reject_if in my controller but I am boggled on how to.
Models:
class PrintLocation < ApplicationRecord
has_many :shop_products, through: :shop_product_print_files
has_many :shop_product_print_files
end
class ShopProductPrintFile < ApplicationRecord
validates :shop_product, uniqueness: { scope: :print_location }
belongs_to :shop_product
belongs_to :print_location
belongs_to :image_file
end
class ShopProduct < ApplicationRecord
has_many :shop_product_print_files
has_many :print_locations, through: :shop_product_print_files
accepts_nested_attributes_for :shop_product_print_files
...
accepts_nested_attributes_for :shop_product_print_files, reject_if: :reject_file, :allow_destroy => true
def reject_file(attributes)
if attributes[:image_file_id].blank?
if attributes[:id].present?
attributes.merge!({:_destroy => 1}) && false
else
true
end
end
end
end
class ImageFile < ApplicationRecord
# this is where users upload files to
belongs_to :user
has_many :shop_product_print_files
end
Controller methods handling this:
def edit_multiple
#image_files = ImageFile.where(user_id: current_user.id)
#shop_products = ShopProduct.find(params[:shop_product_ids])
#shop_product = ShopProduct.new
#shop_product.shop_product_print_files.build PrintLocation.all.map { |pl| { print_location: pl } }
end
def update_multiple
#shop_products = ShopProduct.find(params[:shop_product_ids])
#image_files = ImageFile.where(user_id: current_user.id)
#shop_products.reject! do |shop_product|
shop_product.update_attributes!(params[:shop_product].permit(:shop_product, shop_product_print_files_attributes: [ :id, :print_file, :print_location_id, :shop_product_id, :image_file_id ]).reject { |k,v| v.blank? })
end
if #shop_products.empty?
redirect_to '/'
else
#shop_product = ShopProduct.new(params[:shop_product])
render "edit_multiple"
end
end
When updating on a record-by-record basis, the :reject_file method handles my goal with the help of the SPPF passing through. But When doing this with the array, no SPPF ID passes through which gives me the error:
> ActiveRecord::RecordInvalid (Validation failed: Shop product print
> files shop product has already been taken):
Does anyone have any conclusions on how I can accomplish this?
I am omitting the form to update array to keep this short as the parameters should explain it enough since what I need is most likely either a controller conditional statement to achieve my goal .

Ruby: How do I assign values in the controller when using nested forms?

I have 3 models: Employers, Partners and Collaborations.
As an Employer, I want to add a record to my Partner model and to my Collaboration model to be able to indicate a collaboration between a Partner and a Employer. I therefore have the following columns in my database/tabels.
Models
class Employer < ActiveRecord::Base
has_many :collaborations
has_many :partners, :through => :collaborations
end
class Partner < ActiveRecord::Base
has_many :collaborations
has_many :employers, :through => :collaborations
accepts_nested_attributes_for :collaborations
end
class Collaboration < ActiveRecord::Base
belongs_to :employer
belongs_to :partner
end
Tables
Collaborations
employer_id:integer
partner_id:integer
tarive:string
Partners
added_by:integer
name:string
Because I want to be able to add a Partner/Collaboration within 1 form, I use nested forms. So I can add a partner (name, etc) and a collaboration (tarive, etc) in one go.
My (simple_form) form looks like this (I have named_space resource).
Te reduce clutter, I removed as much HTML mark_up as I could, this is not the issue.
Form
/views/employer/partners/_form
= simple_form_for [:employer, #partner], html: { multipart: true } do |f|
Partner
= f.input :name, input_html: { class: 'form-control' }
= f.simple_fields_for :collaborations do |ff|
Tarive
= ff.input :tarive, input_html: { class: 'form-control' }
= f.button :submit, "Save"
My controller looks like
class Employer::PartnersController < ActionController::Base
def new
#partner = Partner.new
#partner.collaborations.build
end
def create
#partner = Partner.new(partner_params)
#partner.collaborations.build
#partner.added_by = current_employer.id
#partner.collaborations.employer_id = current_employer.employer_id
#partner.collaborations.partner_id = #partner.id
#partner.collaborations.added_by = current_employer.id
if #partner.save
redirect_to employer_partner_path(#partner), notice: "Succes!"
else
render 'new'
end
end
def partner_params
params.require(:partner).permit(:id, :name, collaborations_attributes: [:id, :employer_id, :partner_id, :tarive])
end
end
Problem
The problem/question I have is this. The attributes are assigned nicely and added in the model. But I want to add a employer_id as well, which I have in current_employer.employer.id (Devise). I do not want to work with hidden forms, just to avoid this issue.
I assigned 'parent' models always like #partner.added_by = current_employer.id and that works beautifully.
When I use:
#partner.collaborations.employer_id = current_employer.employer_id
I get an error, saying #partner.collaborations.employer_id is empty.
Question
How can I assign a variable to the nested_form (Collaboration) in my controller#create?
Or more specifically: how can I assign current_employer.employer_id to #partner.collaborations.employer_id?
There are several ways:
Merge the params
Deal with objects, not foreign keys
Personally, I feel your create method looks really inefficient. Indeed, you should know about fat model skinny controller - most of your associative logic should be kept in the model.
It could be improved using the following:
#app/controllers/employers/partners_controller.rb
class Employers::PartnersController < ApplicationController
def new
#partner = current_employer.partners.new #-> this *should* build the associated collaborations object
end
def create
#partner = current_employer.partners.new partner_params
#partner.save ? redirect_to(employer_partner_path(#partner), notice: "Succes!") : render('new')
end
private
def partner_params
params.require(:partner).permit(:id, :name, collaborations_attributes: [:tarive]) #when dealing with objects, foreign keys are set automatically
end
end
This would allow you to use:
#app/views/employers/partners/new.html.erb
= simple_form_for #partner do |f| #-> #partner is built off the current_employer object
= f.input :name
= f.simple_fields_for :collaborations do |ff|
= ff.input :tarive
= f.submit
... and the models:
#app/models/partner.rb
class Partner < ActiveRecord::Base
belongs_to :employer, foreign_key: :added_by
has_many :collaborations
has_many :employers, through: :collaborations
accepts_nested_attributes_for :collaborations
end
#app/models/collaboration.rb
class Collaboration < ActiveRecord::Base
belongs_to :employer
belongs_to :partner
belongs_to :creator, foreign_key: :added_by
before_create :set_creator
private
def set_creator
self.creator = self.employer_id #-> will probably need to change
end
end
#app/models/employer.rb
class Employer < ActiveRecord::Base
has_many :collaborations
has_many :employers, through: :collaborations
end
This may not give you the ability to set tarive, however if you cut down the manual declarations in your model, we should be able to look at getting that sorted.
The main thing you need to do is slim down your code in the controller. You're being very specific, and as a consequence, you're encountering problems like that which you mentioned.

accept data from checkboxes in nested_form and create records using the data from checkboxes

I have got a Products class,Products are visible to zero or many roles . so i have created a polymorphic model called content_roles,which stores the id of the role and content_id (which will be product_id,or event_id),and content_type(product,event etc).
I am using nested_form gem for accepting the role id(using check_box) to store the product and role relation in content_role
the Issue I am facing is I am not able to create a content_role record . in my logs i get unpermitted parameters : role_id
Parameters: {"utf8"=>"✓", "authenticity_token"=>"xxxxxxxxxxxxxxxxxxxxdLH99ZWLrf8dgT3gcBops=", "product"=>{"product_name"=>"some product", "product_description"=>"some product description", "content_roles_attributes"=>{"role_id"=>["1", "2", ""]}}, "commit"=>"Create Product"}
in my view I have written
= f.simple_fields_for :content_roles_attributes do |role|
= role.input :role_id,label: "visible to", as: :check_boxes,label: "Role",collection: Role.all,:required=>true
the controllers permitted params looks like
def product_params
params.require(:product).permit(:product_description,:product_name,
content_roles_attributes: [:role_id,:id],
multimedia_attributes:[:asset,:_destroy,:id])
end
the product model looks like
class Product
has_many :content_roles, as: :content
has_many :multimedia ,as: :storable
# Nested attributes
accepts_nested_attributes_for :multimedia
accepts_nested_attributes_for :content_roles
end
and this is the content_role model
class ContentRole < ActiveRecord::Base
belongs_to :content, polymorphic: true
belongs_to :role
belongs_to :news
belongs_to :product
end
Try changing your product_params to:
def product_params
params.require(:product).permit(
:product_description,
:product_name,
content_roles_attributes: [:id, role_id: []],
multimedia_attributes: [:asset, :_destroy,:id]
)
end

Resources