Rails 4, saving attributes nested - ruby-on-rails

I have a model Job. A job require many skills. But when I'm trying to save my job it fails. I'm not sure I understand what I'm doing wrong.
My models:
class Skill < ActiveRecord::Base
has_many :job_skills
has_many :jobs, through: :job_skills
end
class JobSkill < ActiveRecord::Base
belongs_to :skill
belongs_to :job
end
class Job < ActiveRecord::Base
has_many :job_skills, :inverse_of => :job
has_many :skills, through: :job_skills
accepts_nested_attributes_for :job_skills
end
My view:
<%= form_for #job do |f| %>
<div class="row">
<div class="col-md-8">
<h4>General informations</h4>
<br />
<div class="form-group">
<%= f.label :title %>
<%= f.text_field :title, :autofocus => true, class:'form-control' %>
</div><br />
<%= f.fields_for :job_skills do |s| %>
<%= s.text_field :id %>
<% end %>
</div>
</div>
<div class="submit" style="position:relative;">
<%= f.submit "Save", class: 'button button-small' %>
</div>
<% end %>
My controller:
class JobsController < ApplicationController
before_filter :authenticate_user!, :has_company?, :except => [:index, :show]
def create
#job = Job.new(job_params)
#job.company_id = current_user.company_id
#job.user_id = current_user.id
if #job.save
flash[:notice] = "This job offer has been saved."
return redirect_to job_path(#job)
else
flash[:error] = #job.errors.full_messages.to_sentence
render action: :new
end
end
def new
if current_user.company.present?
#job = Job.new(email:current_user.email)
#job.job_skill.build
else
flash[:error] = "You need to create a company before posting a job"
return redirect_to new_company_path()
end
end
private
def job_params
params.require(:job).permit(:status, :title, :description, :remote ,:job_type, :visa_sponsor, :email, :salary_max, :salary_min, :country, :state, :city, job_skill_attributes: [:id])
end
end
So, I'm not sure what I'm doing wrong, when I'm trying to save I get the following error:
#job = Job.new(job_params)
ActiveRecord::RecordNotFound Exception:
Couldn't find Skill with ID=4 for Job with ID= nil

Your pieces of code are a bit confusing for me.
It seems, that you want to create a job and define, what skills are needed for this job.
Why do you need nested attributes?
Normally, you
either edit a list of all the skills, that are probably needed for a job and then assign the propper skills to that job, than you have a has_and_belongs_to_many relationship and can use form helpers for collections. In this case, you don't need a model JobSkill (but a table jobs_skills to store the relationship and is handles transparently by Rails)
or add random skills to a job, then your job has_may :skills and every skill belongs to exactly one job. Here you can use nested attributes. And then you need a way to add nested skill instances i.e. with cocoon. Again, you don't need a model JobSkill.
Which one is your usecase, so I can explain it in more detail.

Related

Bulk add item rails 6

I have a little project management app.
In the app I have a Project, Item and Delivery Model.
class Project < ApplicationRecord
has_many :locations, dependent: :destroy
has_many :items, dependent: :destroy
has_many :deliveries, dependent: :destroy
end
class Item < ApplicationRecord
belongs_to :project
belongs_to :location, optional: true
has_many :delivery_items, dependent: :destroy
has_many :deliveries, through: :delivery_items
enum status: [:unscheduled, :scheduled, :delivered]
end
class Delivery < ApplicationRecord
belongs_to :project
has_many :delivery_items, dependent: :destroy
has_many :items, through: :delivery_items
enum status: [ :unapproved, :approved, :scheduled ]
end
I also have a delivery_item join table
class DeliveryItem < ApplicationRecord
belongs_to :delivery
belongs_to :item
end
I have added a new Model called location, which is a way of classifying the items into a group on the project.
class Location < ApplicationRecord
belongs_to :project
has_many :items
has_many :part_numbers, through: :items
def bulkadd(delivery)
self.items.each do |row|
batch << Product.new(row)
end
end
end
At the moment the user individually adds items to deliveries via a form on the page
<h6>Add to Delivery</h6>
<%= form_for #delivery_item, html: {class: 'form-inline'} do |form| %>
<div class="form-group">
<%= form.collection_select :delivery_id, #project.deliveries.all, :id, :date, placeholder: 'Add to Delivery', class: 'form-control' %>
</div>
<%= form.hidden_field :item_id, value: item.id %>
<div class="form-group">
<%= form.submit "Add",class: 'btn btn-primary' %>
</div>
<% end %>
I would like to simplify the process by adding a bulk add button to each location which would add all of the associated items to the delivery selected has many items.
I know that I will need the delivery_item(delivery, item).
I just cant seem to get the final part to work in my brain
When you create a has_many or has_and_belongs_to_many assocation the macro creates an others_ids setter/getter. In this case item_ids= which will automatically add/remove rows from the join table.
Its really easy to use this together with the form option helpers to create a select where the user can choose multiple records:
<%= form_for(#delivery) do |form| %>
<div class="field">
<%= f.label :item_ids, 'Select the items' %>
<%= f.collection_select :item_ids, #items, :id, :name, multiple: true %>
</div>
<% end %>
Or if you prefer checkboxes:
<%= form_for(#delivery) do |form| %>
<div class="field">
<%= f.label :item_ids, 'Select the items' %>
<%= f.collection_check_boxes :item_ids, #items, :id, :name %>
</div>
<% end %>
Replace :name with whatever attribute you want to use for the option text.
class DeliveriesController < ApplicationController
before_action :set_delivery, only: [:show, :edit, :update, :destroy]
# This avoids a database query in the view
before_action :set_items, only: [:new, :edit]
# POST /deliveries
def create
#delivery = Delivery.new(delivery_params)
if #delivery.save
redirect_to #delivery, notice: 'Delivery created'
else
set_items
render :new
end
end
# PUT|PATCH /deliveries/1
def update
if #delivery.update(delivery_params)
redirect_to #delivery, notice: 'Delivery updated'
else
set_items
render :edit
end
end
private
def set_delivery
#delivery = Delivery.find(params[:id])
end
def set_items
#items = Item.all
end
def delivery_item_params
# Passing the hash `item_ids: []` allows an array of permitted scalar types.
params.require(:delivery)
.permit(:foo, :bar, :baz, item_ids: [])
end
end

Repeating form fields and updating to database

any help would be most appreciated, I am rather new to Rails.
I have two models a Shopping List and a Product. I'd like to save/update multiple products to a shopping list at a time.
The suggested changes are not updating the models. I've been googling and is "attr_accessor" or find_or_create_by the answer(s)?
Attempt 1 - Existing code
Error
> unknown attribute 'products_attributes' for Product.
Request
Parameters:
{"_method"=>"patch",
"authenticity_token"=>"3BgTQth38d5ykd3EHiuV1hkUqBZaTmedaJai3p9AR1N2bPlHraVANaxxe5lQYaVcWNoydA3Hb3ooMZxx15YnOQ==",
"list"=>
{"products_attributes"=>
{"0"=>{"title"=>"ten", "id"=>"12"},
"1"=>{"title"=>"two", "id"=>"13"},
"2"=>{"title"=>"three", "id"=>"14"},
"3"=>{"title"=>"four", "id"=>"15"},
"4"=>{"title"=>"five", "id"=>"16"},
"5"=>{"title"=>""},
"6"=>{"title"=>""},
"7"=>{"title"=>""},
"8"=>{"title"=>""},
"9"=>{"title"=>""},
"10"=>{"title"=>""}}},
"commit"=>"Save Products",
"id"=>"7"}
Attempt 2 - no errors the page reloads and none of the expected fields are updated. In earnest, I am Googling around and copying and pasting code snippets in the vain hope of unlocking the right combo.
Added to Products mode
class Product < ApplicationRecord
attr_accessor :products_attributes
belongs_to :list, optional: true
end
<%= content_tag(:h1, 'Add Products To This List') %>
<%= form_for(#list) do |f| %>
<%= f.fields_for :products do |pf| %>
<%= pf.text_field :title %><br>
<% end %>
<p>
<%= submit_tag "Save Products" %>
</p>
<% end %>
<%= link_to "Back To List", lists_path %>
list controller
def update
#render plain: params[:list].inspect
#list = List.find(params[:id])
if #list.products.update(params.require(:list).permit(:id, products_attributes: [:id, :title]))
redirect_to list_path(#list)
else
render 'show'
end
list model
class List < ApplicationRecord
has_many :products
accepts_nested_attributes_for :products
end
original do nothing - product model
class Product < ApplicationRecord
belongs_to :list, optional: true
end
If you just want a user to be able to select products and place them on a list you want a many to many association:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
end
class ListItem < ApplicationRecord
belongs_to :list
belongs_to :product
end
class Product < ApplicationRecord
has_many :list_items
has_many :lists, through: :list_products
end
This avoids creating vast numbers of duplicates on the products table and is known as normalization.
You can then select existing products by simply using a select:
<%= form_for(#list) do |f| %>
<%= f.label :product_ids %>
<%= f.collection_select(:product_ids, Product.all, :name, :id) %>
# ...
<% end %>
Note that this has nothing to with nested routes or nested attributes. Its just a select that uses the product_ids setter that's created by the association. This form will still submit to /lists or /lists/:id
You can whitelist an array of ids by:
def list_params
params.require(:list)
.permit(:foo, :bar, product_ids: [])
end
To add create/update/delete a bunch of nested records in one form you can use accepts_nested_attributes_for together with fields_for:
class List < ApplicationRecord
has_many :list_items
has_many :products, through: :list_products
accepts_nested_attributes_for :products
end
<%= form_for(#list) do |f| %>
<%= form.fields_for :products do |pf| %>
<%= pf.label :title %><br>
<%= pf.text_field :title %>
<% end %>
# ...
<% end %>
Of course fields_for won't show anything if you don't seed the association with records. That's where that loop that you completely misplaced comes in.
class ListsController < ApplicationController
# ...
def new
#list = List.new
5.times { #list.products.new } # seeds the form
end
def edit
#list = List.find(params[:id])
5.times { #list.products.new } # seeds the form
end
# ...
def update
#list = List.find(params[:id])
if #list.update(list_params)
redirect_to #list
else
render :new
end
end
private
def list_params
params.require(:list)
.permit(
:foo, :bar,
product_ids: [],
products_attrbutes: [ :title ]
)
end
end
Required reading:
Rails Guides: Nested forms
ActiveRecord::NestedAttributes
fields_for

Rails nested fields creation in loop

I have three model classes related to each other.
class Student < ActiveRecord::Base
has_many :marks
belongs_to :group
accepts_nested_attributes_for :marks,
reject_if: proc { |attributes| attributes['rate'].blank?},
allow_destroy: true
end
This class describes a student that has many marks and I want to create a Student record along with his marks.
class Mark < ActiveRecord::Base
belongs_to :student, dependent: :destroy
belongs_to :subject
end
Marks are related both to the Subject and a Student.
class Subject < ActiveRecord::Base
belongs_to :group
has_many :marks
end
When I try to create the nested fields of marks in loop labeling them with subject names and passing into in it's subject_id via a loop a problem comes up - only the last nested field of marks is saved correctly, whilst other fields are ignored. Here's my form view code:
<%= form_for([#group, #student]) do |f| %>
<%= f.text_field :student_name %>
<%=f.label 'Student`s name'%><br>
<%= f.text_field :student_surname %>
<%=f.label 'Student`s surname'%><br>
<%=f.check_box :is_payer%>
<%=f.label 'Payer'%>
<%= f.fields_for :marks, #student.marks do |ff|%>
<%#group.subjects.each do |subject| %><br>
<%=ff.label subject.subject_full_name%><br>
<%=ff.text_field :rate %>
<%=ff.hidden_field :subject_id, :value => subject.id%><br>
<%end%>
<% end %>
<%= f.submit 'Add student'%>
<% end %>
Here`s my controller code:
class StudentsController<ApplicationController
before_action :authenticate_admin!
def new
#student = Student.new
#student.marks.build
#group = Group.find(params[:group_id])
#group.student_sort
end
def create
#group = Group.find(params[:group_id])
#student = #group.students.new(student_params)
if #student.save
redirect_to new_group_student_path
flash[:notice] = 'Студента успішно додано!'
else
redirect_to new_group_student_path
flash[:alert] = 'При створенні були деякі помилки!'
end
end
private
def student_params
params.require(:student).permit(:student_name, :student_surname, :is_payer, marks_attributes: [:id, :rate, :subject_id, :_destroy])
end
end
How can I fix it?
#student.marks.build
This line will reserve an object Mark.
If you want multi marks, May be you need something like this in new action :
#group.subjects.each do |subject|
#student.marks.build(:subject=> subject)
end
Hope useful for you.

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!

Rails has_many through form with checkboxes and extra field in the join model

I'm trying to solve a pretty common (as I thought) task.
There're three models:
class Product < ActiveRecord::Base
validates :name, presence: true
has_many :categorizations
has_many :categories, :through => :categorizations
accepts_nested_attributes_for :categorizations
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
validates :description, presence: true # note the additional field here
end
class Category < ActiveRecord::Base
validates :name, presence: true
end
My problems begin when it comes to Product new/edit form.
When creating a product I need to check categories (via checkboxes) which it belongs to. I know it can be done by creating checkboxes with name like 'product[category_ids][]'. But I also need to enter a description for each of checked relations which will be stored in the join model (Categorization).
I saw those beautiful Railscasts on complex forms, habtm checkboxes, etc. I've been searching StackOverflow hardly. But I haven't succeeded.
I found one post which describes almost exactly the same problem as mine. And the last answer makes some sense to me (looks like it is the right way to go). But it's not actually working well (i.e. if validation fails). I want categories to be displayed always in the same order (in new/edit forms; before/after validation) and checkboxes to stay where they were if validation fails, etc.
Any thougts appreciated.
I'm new to Rails (switching from CakePHP) so please be patient and write as detailed as possible. Please point me in the right way!
Thank you. : )
Looks like I figured it out! Here's what I got:
My models:
class Product < ActiveRecord::Base
has_many :categorizations, dependent: :destroy
has_many :categories, through: :categorizations
accepts_nested_attributes_for :categorizations, allow_destroy: true
validates :name, presence: true
def initialized_categorizations # this is the key method
[].tap do |o|
Category.all.each do |category|
if c = categorizations.find { |c| c.category_id == category.id }
o << c.tap { |c| c.enable ||= true }
else
o << Categorization.new(category: category)
end
end
end
end
end
class Category < ActiveRecord::Base
has_many :categorizations, dependent: :destroy
has_many :products, through: :categorizations
validates :name, presence: true
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
validates :description, presence: true
attr_accessor :enable # nice little thingy here
end
The form:
<%= form_for(#product) do |f| %>
...
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<%= f.fields_for :categorizations, #product.initialized_categorizations do |builder| %>
<% category = builder.object.category %>
<%= builder.hidden_field :category_id %>
<div class="field">
<%= builder.label :enable, category.name %>
<%= builder.check_box :enable %>
</div>
<div class="field">
<%= builder.label :description %><br />
<%= builder.text_field :description %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
And the controller:
class ProductsController < ApplicationController
# use `before_action` instead of `before_filter` if you are using rails 5+ and above, because `before_filter` has been deprecated/removed in those versions of rails.
before_filter :process_categorizations_attrs, only: [:create, :update]
def process_categorizations_attrs
params[:product][:categorizations_attributes].values.each do |cat_attr|
cat_attr[:_destroy] = true if cat_attr[:enable] != '1'
end
end
...
# all the rest is a standard scaffolded code
end
From the first glance it works just fine. I hope it won't break somehow.. :)
Thanks all. Special thanks to Sandip Ransing for participating in the discussion. I hope it will be useful for somebody like me.
use accepts_nested_attributes_for to insert into intermediate table i.e. categorizations
view form will look like -
# make sure to build product categorizations at controller level if not already
class ProductsController < ApplicationController
before_filter :build_product, :only => [:new]
before_filter :load_product, :only => [:edit]
before_filter :build_or_load_categorization, :only => [:new, :edit]
def create
#product.attributes = params[:product]
if #product.save
flash[:success] = I18n.t('product.create.success')
redirect_to :action => :index
else
render_with_categorization(:new)
end
end
def update
#product.attributes = params[:product]
if #product.save
flash[:success] = I18n.t('product.update.success')
redirect_to :action => :index
else
render_with_categorization(:edit)
end
end
private
def build_product
#product = Product.new
end
def load_product
#product = Product.find_by_id(params[:id])
#product || invalid_url
end
def build_or_load_categorization
Category.where('id not in (?)', #product.categories).each do |c|
#product.categorizations.new(:category => c)
end
end
def render_with_categorization(template)
build_or_load_categorization
render :action => template
end
end
Inside view
= form_for #product do |f|
= f.fields_for :categorizations do |c|
%label= c.object.category.name
= c.check_box :category_id, {}, c.object.category_id, nil
%label Description
= c.text_field :description
I just did the following. It worked for me..
<%= f.label :category, "Category" %>
<%= f.select :category_ids, Category.order('name ASC').all.collect {|c| [c.name, c.id]}, {} %>

Resources