Bulk add item rails 6 - ruby-on-rails

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

Related

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 5: Nested params not nested with has_many :through

I'm trying to create a form in Rails 5.2 for a model with a has_many :through relationship to another model. The form needs to include nested attributes for the other model. However, the params are not nesting properly. I've created the following minimal example.
Here are my models:
class Order < ApplicationRecord
has_many :component_orders, dependent: :restrict_with_exception
has_many :components, through: :component_orders
accepts_nested_attributes_for :components
end
class Component < ApplicationRecord
has_many :component_orders, dependent: :restrict_with_exception
has_many :orders, through: :component_orders
end
class ComponentOrder < ApplicationRecord
belongs_to :component
belongs_to :order
end
The Component and Order models each have one attribute: :name.
Here is my form code:
<%= form_with model: #order do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= fields_for :components do |builder| %>
<%= builder.label :name %>
<%= builder.text_field :name %>
<% end %>
<%= f.submit %>
<% end %>
When I fill out the form, I get the following params:
{"utf8"=>"✓", "authenticity_token"=>"ztA1D9MBp1IRPsiZnnSAIl2sEYjFeincKxivoq0/pUO+ptlcfi6VG+ibBnSREqGq3VzckyRfkQtkCTDqvnTDjg==", "order"=>{"name"=>"Hello"}, "components"=>{"name"=>"World"}, "commit"=>"Create Order", "controller"=>"orders", "action"=>"create"}
Specifically, note that instead of a param like this:
{
"order" => {
"name" => "Hello",
"components_attributes" => {
"0" => {"name" => "World"}
}
}
}
There are separate keys for "order" and "components" at the same level. How can I cause these attributes to nest properly? Thank you!
EDIT: Here is my controller code:
class OrdersController < ApplicationController
def new
#order = Order.new
end
def create
#order = Order.new(order_params)
if #order.save
render json: #order, status: :created
else
render :head, status: :unprocessable_entity
end
end
private
def order_params
params.require(:order).permit(:name, components_attributes: [:name])
end
end
You should include accepts_nested_attributes_for :components in the Order model.
class Order < ApplicationRecord
has_many :component_orders, dependent: :restrict_with_exception
has_many :components, through: :component_orders
accepts_nested_attributes_for :components
end
And change
<%= fields_for :components do |builder| %>
to
<%= f.fields_for :components do |builder| %>
to get the desired params. accepts_nested_attributes_for :components creates a method namely components_attributes
More Info here

Create 3rd level child in a single form - Rails 5

I'm building a simple top-to-bottom Workout Routine app on ROR. I'm able to create a Workout Day (parent) and an Exercise (child) on the same form. But I can't seem to save the Weighted Set (grandchild) when I submit the form. The interesting thing is that since the Exercise is saved, I can go to that exercise edit page, add a Weighted Set, and the Weighted Set will show up in the Workout Day show page. I think it has to do with the Weighted Set not being associated with the Exercise at the time of creation. How cam I tie wll three models together? I know I'm close!
I have the whole app on github. I the link isn't working, try this URL https://github.com/j-acosta/routine/tree/association
Models
class WorkoutDay < ApplicationRecord
has_many :exercises, dependent: :destroy
has_many :weighted_sets, through: :exercises
accepts_nested_attributes_for :exercises
accepts_nested_attributes_for :weighted_sets
end
class Exercise < ApplicationRecord
belongs_to :workout_day, optional: true
has_many :weighted_sets, dependent: :destroy
accepts_nested_attributes_for :weighted_sets
end
class WeightedSet < ApplicationRecord
belongs_to :exercise, optional: true
end
Workout Day Controller
class WorkoutDaysController < ApplicationController
before_action :set_workout_day, only: [:show, :edit, :update, :destroy]
...
# GET /workout_days/new
def new
#workout_day = WorkoutDay.new
# has_many association .build method => #parent.child.build
#workout_day.exercises.build
# has_many :through association .build method => #parent.through_child.build
# #workout_day.weighted_sets.build
#workout_day.weighted_sets.build
end
...
# POST /workout_days
# POST /workout_days.json
def create
#workout_day = WorkoutDay.new(workout_day_params)
respond_to do |format|
if #workout_day.save
format.html { redirect_to #workout_day, notice: 'Workout day was successfully created.' }
format.json { render :show, status: :created, location: #workout_day }
else
format.html { render :new }
format.json { render json: #workout_day.errors, status: :unprocessable_entity }
end
end
end
...
private
# Use callbacks to share common setup or constraints between actions.
def set_workout_day
#workout_day = WorkoutDay.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def workout_day_params
params.require(:workout_day).permit(:title, exercises_attributes: [:title, :_destroy, weighted_sets_attributes: [:id, :weight, :repetition]])
end
end
New Workout Day form
<%= form_for #workout_day do |workout_day_form| %>
<% if workout_day.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(workout_day.errors.count, "error") %> prohibited this workout_day from being saved:</h2>
<ul>
<% workout_day.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= workout_day_form.label :title, 'Workout Day Name' %>
<%= workout_day_form.text_field :title %>
</div>
exercise_field will go here
<div>
<%= workout_day_form.fields_for :exercises do |exercise_field| %>
<%= exercise_field.label :title, 'Exercise' %>
<%= exercise_field.text_field :title %>
<% end %>
</div>
weighted_set_fields will go here
<div>
<%= workout_day_form.fields_for :weighted_sets do |set| %>
<%= render 'exercises/weighted_set_fields', f: set %>
<% end %>
</div>
<div>
<%= workout_day_form.submit %>
</div>
<% end %>
The culprit is the workout_day_params. In the form you have the fields of weighted_sets nested under the workout_day. But in the workout_day_params, you have weighted_sets_attributes under exercises_attributes which is the reason for your problem. Changing it to below should solve the issue.
def workout_day_params
params.require(:workout_day).permit(:title, exercises_attributes: [:title, :_destroy], weighted_sets_attributes: [:id, :weight, :repetition])
end
ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrMany‌​Reflection
This due to wrong associations. You should consider tweaking your associations like below
class WorkoutDay < ApplicationRecord
has_many :weighted_sets, dependent: :destroy
has_many :exercises, through: :weighted_sets
accepts_nested_attributes_for :exercises
accepts_nested_attributes_for :weighted_sets
end
class Exercise < ApplicationRecord
has_many :weighted_sets, dependent: :destroy
has_many :workout_days, through: :weighted_sets
end
class WeightedSet < ApplicationRecord
belongs_to :exercise, optional: true
belongs_to :workout_day, optional: true
end

How to create a new nested resource?

A users can have many favorites top_songs,top_movies through songs and movies table.
A user registered user(current_user) want to post his favorites movies and songs.
Perhaps all Model association are right, i am stuck in controller and view (form).
When i submit from, i gets errors-
Can't mass-assign protected attributes: songs
How can i achieve this please?
all codes are below.
User Model
class User < ActiveRecord::Base
attr_accessible :id, :name_special_char, :screenname, :fullname, :username, :prefix, :firstname, :lastname,:middlename, :suffix, :age, :sex, :email,
:top_movies_attributes,:top_songs_attributes
has_many :top_movies
has_many :movies, through: :top_movies
has_many :top_songs
has_many :songs, through: :top_songs
accepts_nested_attributes_for :top_songs, :allow_destroy => true
accepts_nested_attributes_for :top_movies, :allow_destroy => true
end
Movie Model
class Movie < ActiveRecord::Base
attr_accessible :name
has_many :top_movies
has_many :users, through: :top_movies
end
TopMovie Model
class TopMovie < ActiveRecord::Base
belongs_to :user
belongs_to :movie
# attr_accessible :title, :body
end
Song Model
class Song < ActiveRecord::Base
attr_accessible :name
has_many :top_songs
has_many :users, through: :top_songs
end
TopSong Model
class TopSong < ActiveRecord::Base
belongs_to :user
belongs_to :song
# attr_accessible :title, :body
end
Controller
class MyTopFivesController < ApplicationController
def new
#favorites = current_user
#favorites=#favorites.movies.build()
#favorites=#favorites.songs.build()
respond_to do |format|
format.html # new.html.erb
format.json { render json: #useraccounts_my_top_fife }
end
end
def create
#favorites = current_user(params[:user])
#favorites.save!
# Here i have stuck. i am not sure how to save.
end
view form
<%=nested_form_for #favorites ,:url=>favorites_path(#favorites),:method=>'post' do |f| %>
<label >Songs</label>
<%= f.fields_for :songs do |songs| %>
<div id="Topsongs" >
<div class="input-control text span5 place-left ">
<%= songs.text_field :name,:placeholder=>"songs name.." %>
</div>
<div class="span1 place-left">
<%= songs.link_to_remove "", :class=>"icon-minus" %>
</div>
</div>
<% end %>
<span >
<%= f.link_to_add "", :songs, :class=>"icon-plus", :data => { :target => "#Topsongs" } %>
</span>
<label >movies</label>
<%= f.fields_for :movies do |movies| %>
<div id="Topmovies">
<div class="input-control text span5 place-left ">
<%= movies.text_field :name,:placeholder=>"movies name.." %>
</div>
<div class="span1 place-left">
<%= movies.link_to_remove "", :class=>"icon-minus" %>
</div>
</div>
<% end %>
<span>
<%= f.link_to_add "", :movies, :class=>"icon-plus",:style=>"font-size: 14px;", :data => { :target => "#Topmovies" } %>
</span>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
There are two options for setting up the associations here. One you have used by creating two join models for top songs and top movies. And other, to use polymorphic association.
Lets use polymorphic association. We are going to use User, Movie, Song and Favourite models for this stuff. The Favourite model will contain the polymorphic fields.
User.rb
class User < ActiveRecord::Base
attr_accessible :id, :name_special_char, :screenname, :fullname, :username, :prefix, :firstname, :lastname,:middlename, :suffix, :age, :sex, :email
has_many :favourites
has_many :movies, through: :favourites, source: :favouritable, source_type: 'Movie'
has_many :songs, through: :favourites, source: :favouritable, source_type: 'Song'
end
Movie.rb
class Movie < ActiveRecord::Base
attr_accessible :name
has_many :favourites, as: :favouritable
has_many :users, through: :favourites
end
Song.rb
class Song < ActiveRecord::Base
attr_accessible :name
has_many :favourites, as: :favouritable
has_many :users, through: :favourites
end
Favourite.rb
class Favourite < ActiveRecord::Base
belongs_to :users
belongs_to :favouritable, polymorphic: true
end
We also need to create the migration for new model "Favourite". As of now, we just need 3 columns ie. user_id, favouritable_id, favouritable_type. Here favouritable_type and favouritable_id are the polymorphic fields. favouritable_type is a string and favouritable_id is reference type.
Migration File
class CreateFavourites < ActiveRecord::Migration
def change
create_table :favourites do |t|
t.integer :user_id
t.references :favouritable, polymorphic: true
t.timestamps
end
end
end
Now, as we are going to mark some movies and songs to be favourite for a user, then we can place the code for building the data in UsersController instead of creating another controller or we can also create a controller for Favourites. I am going to use UsersController here. I am using update action to update the favourites as we don't need any extra functionality here. You can add a new action if you want.
In UsersController.rb
def edit_favourites #or some generic name
#user = current_user.includes(:movies, :songs)
#movies = Movie.all
#songs = Song.all
end
def update
#user = User.find(params[:id])
if #user.update_attributes(params[:user])
redirect_to users_path #user index page
else
if params[:user][:movie_ids].present? or params[:user][:song_ids].present?
render :edit_favourites
else
render :edit
end
end
end
edit_favourites.html.erb
<%= form_for(#user) do |f| %>
<div class="fields">
<%= f.label :movie_ids, "Favourite Movies: " %>
<%= f.collection_select :movie_ids, #movies, :id, :name, {}, multiple: true %>
</div>
<div class="fields">
<%= f.label :song_ids, "Favourite Songs: " %>
<%= f.collection_select :song_ids, #songs, :id, :name, {}, multiple: true %>
</div>
<% end %>
Also, Add the new action to routes.

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