Rails create multiple rows from one form - ruby-on-rails

I have a rails app that has building and floors models
class Building < ApplicationRecord
has_many :floors
end
class Floor < ApplicationRecord
belongs_to :building
end
In my building form I want to ask the user how many floors the building has, and then when the building is created I want to add that many floors.
So the form would look like so:
<%= form_with(model: building, local: true) do |form| %>
<% if building.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(building.errors.count, "error") %> prohibited this building from being saved:</h2>
<ul>
<% building.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div class="field">
<%= form.label :" How many floors does the building have" %>
<%= form.number :floors %> * not sure how to do this
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
Note I don't want the number of floors saved on the building model, it just creates the number of floors the user specified.
So if I created a building called "Walts Place" and said it has 10 floors it would create: Walts Place with id:1, and 10 floors with the building_id of 1.
Does that make sense?
Your help is greatly appreciated.
Update:
ActiveRecord::Schema.define(version: 2019_07_30_093037) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "buildings", force: :cascade do |t|
t.string "name"
t.float "distancetocondensors"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "floors", force: :cascade do |t|
t.bigint "building_id", null: false
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["building_id"], name: "index_floors_on_building_id"
end
add_foreign_key "floors", "buildings"
end

you can modify it in your building controller in create action.
def create
#building = Building.new(building_params)
if #building.save
floors = params[:number].to_i
floors.times do
Floor.create(building: #building)
end
redirect_to building_path
else
redirect_to error
end
end
in your form add a field for number of floor without erb
<%= form_with(model: building, local: true) do |form| %>
<% if building.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(building.errors.count, "error") %> prohibited this building
from being saved:</h2>
<ul>
<% building.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div class="field">
<label> How many floors does the building have</label>
<input type="number" name="number"/>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>

You can use nested attributes to help you here.
First, in your Building model add:
has_many :floors
accepts_nested_attributes_for :floors
Then in your BuildingController's create action do:
class BuildingController < ApplicationController
...
def create
floors_amount = params[:building][:floors] || 0
building_params = params.require(:building).permit(:name).merge({
# just fill floors array with empty hashes since `id` will be added automatically
floors_attributes: (1..floors_amount).to_a.fill({})
})
#building = Building.create(building_params)
if #buildnig.errors
render :new # show the form page with an error
else
redirect_to #building # or whenever you want to redirect
end
end
...
end
The advantage here is that it wraps the creation of building and floors into a transaction so if something goes wrong it will rollback all changes. I.e. there won't be a case where new Building is inserted into DB but floors didn't due to some error during their creation.
Another convenience here is that if some validation error appears either on building or on any of the floor then it will be set into building.errors. Means you can easily do render :new in the case of errors to display them.
Note:
I see that in your form view you access Building instance as building. So I'm not really sure how you pass it to this view.
In my example I saved building into #building variable so that in this view form you will have to access it as #building. I believe in your new action in BuildingController you should set it to #building as well:
class BuildingController < ApplicationController
...
def new
#building = Building.new
end
...
end
And in your view you will access it as #building then, not building.
Hope, that makes sense

Related

Create objects based on received data from params in Ruby on Rails

Description
I am trying to create messages based on selected (via check box) users from the browser in Ruby on Rails.
Snapshot:
Steps to reproduce
My schema
ActiveRecord::Schema.define(version: 2021_11_13_142255) do
create_table "messages", force: :cascade do |t|
t.text "content"
t.integer "user_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "users", force: :cascade do |t|
t.string "name"
t.string "role"
t.integer "phone"
t.boolean "admin"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
end
messages_controller.rb
class MessagesController < ApplicationController
def new
#users = User.all
#message = Message.new(message_params)
end
def create
params[:user_objs].each do |u|
# "params.inspect" returns
# {"authenticity_token"=>"[FILTERED]",
# "user_objs"=>
# ["{\"id\":1,\"name\":\"Alex\",\"role\":\"Engineer\",\"phone\":998943333303,\"admin\":true,\"created_at\":\"2021-11-13T14:37:54.962Z\",\"updated_at\":\"2021-11-13T14:37:54.962Z\"}",
# "{\"id\":2,\"name\":\"Lucy\",\"role\":\"Accountant\",\"phone\":998943333303,\"admin\":false,\"created_at\":\"2021-11-13T14:39:52.742Z\",\"updated_at\":\"2021-11-13T14:39:52.742Z\"}"],
# "message"=>{"content"=>"Message from the browser"},
# "commit"=>"Send"}
person = JSON.parse(u)
#message = person.messages.new(message_params)
if #message.save
redirect_to root_path
else
#users = User.all
render :new
end
end
end
private
def message_params
params.permit(
:content,
:user_id
)
end
end
messages => new.html.erb
<div>
<h1>Create and send a new message!</h1>
<%= form_for(#message) do |form| %>
<% if #message.errors.any? %>
<div class="alert alert-danger">
<h5 class="fw-bold">Invalid input!</h5>
<%= #message.errors.full_messages.each do |error| %>
<div><%= error %></div>
<% end %>
</div>
<% end %>
<% #users.each do |u| %>
<div>
<p><%= check_box_tag "user_objs[]", u.to_json %> <%= u.name %></p>
</div>
<% end %>
<p class="mb-3">
<%= form.label :content, class: "form-label" %>
<%= form.text_field :content, class: "form-control", autofocus: true, placeholder: "John_D" %>
</p>
<p class="mb-3">
<%= form.submit "Send", class: "btn btn-primary" %>
</p>
<% end %>
</div>
<%= params.inspect %>
Models
# user.rb
class User < ApplicationRecord
has_many :messages
end
# message.rb
class Message < ApplicationRecord
belongs_to :user
end
Expected behavior
I was expecting the creation of messages for all selected users
Actual behavior
NoMethodError in MessagesController#create
undefined method `messages' for #<Hash:0x000000011fe2b420>
I tried different ways, but can't convert Ruby objects to JSON in my params user_objs[] so that I can parse it in my controller to create messages based on those selected users in the user_objs[] params.
Environment info
ruby -v
ruby 2.7.3p183 (2021-04-05 revision 6847ee089d) [arm64-darwin20]
rails -v
Rails 6.1.4.1
Thanks for any given help 🙏
If you want to create a system where you send a single message to multiple users you would setup a join table:
class User < ApplicationRecord
has_many :user_messages
has_many :recieved_messages, though: :user_messages,
source: :message,
inverse_of: :recipients
end
# rails g model user_message user:belongs_to message:belongs_to read:boolean
class UserMessage < ApplicationRecord
belongs_to :user
belongs_to :message
# make sure to add a compound unique index to the migration as well
validates_uniqueness_of :user_id, scope: :message_id
delegate :content, to: :message
end
class Message < ApplicationRecord
has_many :user_messages
has_many :recipients, though: :user_messages,
source: :user,
inverse_of: :recieved_messages
end
has_many :recipients will create a recipient_ids= setter and a recipient_ids getter that you can use in your form:
<div>
<h1>Create and send a new message!</h1>
<%= form_with(model: #message) do |form| %>
<% if #message.errors.any? %>
<div class="alert alert-danger">
<h5 class="fw-bold">Invalid input!</h5>
<%= #message.errors.full_messages.each do |error| %>
<div><%= error %></div>
<% end %>
</div>
<% end %>
<p class="mb-3">
<%= form.collection_checkboxes(:recipient_ids, #users, :id, :name) %>
</p>
<p class="mb-3">
<%= form.label :content, class: "form-label" %>
<%= form.text_field :content, class: "form-control", autofocus: true, placeholder: "John_D" %>
</p>
<p class="mb-3">
<%= form.submit "Send", class: "btn btn-primary" %>
</p>
<% end %>
</div>
There is absolutely no need to pass the entire record as JSON - you just pass an array of IDs and rails will do all the work of creating the join table rows for you:
class MessagesController < ApplicationController
def new
#users = User.all
#message = Message.new
end
def create
#message = Message.new(message_params)
if #message.save
redirect_to root_path
else
#users = User.all
render :new
end
end
private
def message_params
params.require(:message)
.permit(
:content,
recipient_ids: []
)
end
end
This avoids the complexity of creating multiple records from a single request and the whole conundrum that you're binding the form to a single instance of Message but creating a bunch of records which is bound to lead to confusion.
If you want to create multiple records at once it can be done but the complexity is far higher and you have to deal with stuff like how to handle errors if creating one message fails and this might be beyond your current skill level.
The issue is that you are assigning a json object/hash in person = JSON.parse(u). This is not an active record so when doing person.messages it throws the error. I believe what you need in the create action is something like:
user = JSON.parse(u)
# make sure user.inspect gives you the user object you want
person = User.find(user["id"])
# person.inspect should give you the active record for the user

if statement for a model on a different show page

I have two tables
create_table "book_rooms", force: :cascade do |t|
t.integer "room"
t.string "room_number"
end
and
create_table "rooms", force: :cascade do |t|
t.string "room_number"
end
in the rooms show page i want to display 'book_rooms.room_number' with same value with 'rooms.room_number' but I keep getting all book room objects printed out as an array in every room show page
rooms show page
<% #customer = BookRoom.all %>
<% if #customer[0].room_number = #room.room_number %>
<% #customer.each do |c| %>
<%= c.first_name %>
<% end %>
<% end %>
pls is there something I am not doing right? the if statement is meant to be false if the room_numbers dont tally but they still get printed out regardless! i am stuck
BookRoom.where(:room_number => #room.room_number)
This will fetch all the BookRoom with room_number from a specific Room (which is #room).
By the way I guess your association is not correct. There must be a has_many relation between BookRoom and Room.
The problem you're facing is, in your #customer variable you assign all BookRooms. Then based on this variable you created a loop. THus all the BookRoom are displaying.
Also, in your book_rooms schema there is no first_name.
EDIT
BUG: In your if condition you used the = operator instead of ==
<% #customer = BookRoom.all %>
<% #customer.each do |c| %>
<% if c.room_number == #room.room_number %>
<%= c.first_name %>
<% end %>
<% end %>

Rails 4: child record ID is saved but not its attributes, using nested form with has_many through relationship

I have 3 models with a has_many through relationship: Food (eg: Chocolate), Sub (Chocolate food substitute), Joint (joint table).
Say #food = Food.find(1); The has_many through relationship allows me to do #subs = #food.subs which return all substitutes associated with #food. This work fine, however only the Sub id is saved and not its attributes which are :name and :description as you can see it returned nil when trying to save #food.subs in my create action in my controller:
=> #<ActiveRecord::Associations::CollectionProxy [#<Sub id: 28,name:nil,description:nil,created_at:
"2015-01-07 00:40:35", updated_at: "2015-01-07 00:40:35">]>
I guess the issue lies with my create action in my food controller and perhaps something to do with my nested form as well. I spent countless hours trying to figure this out I am so desperate to find an answer. I really do not know where to look anymore.
I am new to rails so thanks a lot for your help and your time, I really appreciate it. Please if possible adapt your answer to my beginner level :-) .
Down below are samples of my controller, form and relevant information.
Here are my models:
class Food < ActiveRecord::Base
has_many :joints
has_many :subs, :through => :joints
accepts_nested_attributes_for :subs
end
class Sub < ActiveRecord::Base
has_many :joints
has_many :foods, :through => :joints
accepts_nested_attributes_for :foods
end
class Joint < ActiveRecord::Base
belongs_to :food
belongs_to :sub
end
Here is my db-schema FYI:
create_table "foods", force: true do |t|
t.string "name"
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "joints", force: true do |t|
t.integer "food_id"
t.integer "sub_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "subs", force: true do |t|
t.string "name"
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Here is my foods_controller:
def new
#food = Food.new
#sub = Sub.new
end
def create
#food = Food.new(food_params)
#food.subs.build(params[:subs])
#food.save
respond_to do |format|
if #food.save
format.html { redirect_to #food, notice: 'Food was successfully created.' }
format.json { render :show, status: :created, location: #food }
else
format.html { render :new }
format.json { render json: #food.errors, status: :unprocessable_entity }
end
end
end
private
def food_params
params.require(:food).permit(:name, :description, subs_attributes: [:name, :description])
end
end
Here is my views/foods/_form:
<%= form_for(#food) do |f| %>
<% if #food.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#food.errors.count, "error") %> prohibited this food from being saved:</h2>
<ul>
<% #food.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :description %><br>
<%= f.text_area :description %>
</div>
<div>
<%= f.fields_for(#sub) do |sub| %>
<div class="field">
<%= sub.label :name %>
<%= sub.text_field :name %>
</div>
<div class="field">
<%= sub.label :description %>
<%= sub.text_area :description %>
</div>
<% end %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
My routes in case it helps:
resources :foods
resources :subs
resources :joints
root "foods#index"
Thank you very much !
Antoine.
In your new action:
def new
#food = Food.new
#food.subs.build
end
and in your view:
<%= f.fields_for :subs do |sub| %>
When you're passing directly an object, this object becomes the new form_builder's object - rails have no idea it is in any way connected with original object so it will result in different field names.
When you pass a symbol, rails will first try to find if your current object defines subs_attributes method. If so it will loop over subs association and build the fields for each associated model.
Reference here.
UPDATE - answer to comment:
Firstly - #subs is not a symbol, it is an instance variable. Symbols start with a colon like :subs. When fields_for receives an argument, it checks whether it is a symbol or object. In former case it search an object associated with form builder (f.object) to find out if it defines <passed_symbol>_attributes=. That way it knows that the model accepts nested attributes for this association so it can behave accordingly (the new form builder is created for each associated object with a correct name - <symbol>_attributes).
When object is passed, rails has no way of detecting if this is in ay way connected to the current object - you could have two associations for the same type of objects, or even it might have absolutely nothing to do with the original object. In that case fields_for acts like it was a nested form_for - resulting form builder will carry the model name of the object (f.object.class.model_name.singular)

problems accessing variables in a loop within my view

I'm fairly new to Ruby/Rails and I'm having a weird issue that I can't seem to understand what exactly I'm doing wrong.
I have the following code in my view
<% gameNum = 0 %>
gameNum: <%= gameNum %>
homeTeamIndex: <%= #games[gameNum].homeTeamIndex %>
awayTeamIndex: <%= #games[gameNum].awayTeamIndex %><br />
<%= #NflTeams[#games[gameNum].homeTeamIndex].name %>
<%= #NflTeams[#games[gameNum].awayTeamIndex].name %><br />
<% gameNum = 1 %>
gameNum: <%= gameNum %>
homeTeamIndex: <%= #games[gameNum].homeTeamIndex %>
awayTeamIndex: <%= #games[gameNum].awayTeamIndex %><br />
<%= #NflTeams[#games[gameNum].homeTeamIndex].name %>
<%= #NflTeams[#games[gameNum].awayTeamIndex].name %><br />
<% (0..#games.count).each do |gameNum| %>
gameNum: <%= gameNum %>
homeTeamIndex: <%#= #games[gameNum].homeTeamIndex %>
awayTeamIndex: <%#= #games[gameNum].awayTeamIndex %> <br />
<%#= #NflTeams[#games[gameNum].homeTeamIndex].name %>
<%#= #NflTeams[#games[gameNum].awayTeamIndex].name %>
<% end %>
And I get the following results when I view my view:
gameNum: 0 homeTeamIndex: 10 awayTeamIndex: 3
Detroit Lions Buffalo Bills
gameNum: 1 homeTeamIndex: awayTeamIndex:
Cinncinatti Bengals Cleveland Browns
gameNum: 0 homeTeamIndex: awayTeamIndex:
homeTeamIndex: awayTeamIndex:
gameNum: 1 homeTeamIndex: awayTeamIndex:
homeTeamIndex: awayTeamIndex:
gameNum: 2 homeTeamIndex: awayTeamIndex:
homeTeamIndex: awayTeamIndex:
gameNum: 3 homeTeamIndex: awayTeamIndex:
homeTeamIndex: awayTeamIndex:
gameNum: 4 homeTeamIndex: awayTeamIndex:
However, if I uncomment any of the lines:
homeTeamIndex: <%#= #games[gameNum].homeTeamIndex %>
awayTeamIndex: <%#= #games[gameNum].awayTeamIndex %> <br />
<%#= #NflTeams[#games[gameNum].homeTeamIndex].name %>
<%#= #NflTeams[#games[gameNum].awayTeamIndex].name %>
I get the following error(method name changes based on which line I uncomment):
undefined method `homeTeamIndex' for nil:NilClass
I really don't understand what is happening within the loop that makes the instance variables unavailable.
I'm hoping someone can tell me what the heck I'm doing wrong because this sure seems like a simple thing to do in a view and I can't get it to work.
UPDATE
I changed my view as follows, per Dmitry's advice:
<% gameNum = 0 %>
<% (#games).each do |game| %>
<strong>Game <%= gameNum+1 %> </strong> <br />
<%= image_tag(#NflTeams[game.homeTeamIndex].imagePath,
size: "40") %>
<%= #NflTeams[game.homeTeamIndex].name %>
VS
<%= image_tag(#NflTeams[game.awayTeamIndex].imagePath,
size: "40") %>
<%= #NflTeams[game.awayTeamIndex].name %> <br />
<% gameNum += 1 %>
<% end %>
And the Models are as follows:
NflTeams
class CreateNflTeams < ActiveRecord::Migration
def change
create_table :nfl_teams do |t|
t.string :name
t.string :imagePath
t.timestamps
end
end
end
Game
class CreateGames < ActiveRecord::Migration
def change
create_table :games do |t|
t.integer :homeTeamIndex
t.integer :awayTeamIndex
t.integer :spread
t.integer :week_id
t.timestamps
end
end
end
The awayTeamIndex and homTeamIndex are an index into the NflTeams model so I can easily pull out the Name and ImagePath.
I am still getting the undefined method on the line:
<%= image_tag(#NflTeams[game.homeTeamIndex].imagePath,
size: "40") %>
Any other suggestions?
UPDATE 2
I only included the migrations because there isn't much in the models for these two models.
But here it is.
class NflTeam < ActiveRecord::Base
end
class Game < ActiveRecord::Base
belongs_to :week
validates :homeTeamIndex, :inclusion => { :in => 0..100 }
validates :awayTeamIndex, :inclusion => { :in => 0..100 }
end
And here is the controller code:
class WeeksController < ApplicationController
before_action :signed_in_user
before_action :confirmed_user
...
def show
#week = Week.find(params[:id])
#games = #week.games
#NflTeams = NflTeam.all
end
...
end
You are not accessing you #games in a loop correctly. It looks like you are thinking about gameNum in this line:
<% (0..#games.count).each do |gameNum| %>
as if it is a numerical index and using it to access an object in a collection, in the following line:
homeTeamIndex: <%#= #games[gameNum].homeTeamIndex %>
This is wrong because "gameNum" is actually your object from a collection of #games fetched for you by magic ruby, so what you need is something like this:
<% (#games).each do |game| %>
gameNum: <%= game.number %>
homeTeamIndex: <%= game.homeTeamIndex %>
awayTeamIndex: <%= game.awayTeamIndex %> <br />
<%= game.homeTeam.name %>
<%= game.awayTeam.name %>
<% end %>
Hope this helps :)
P.S.
This solution depends on how your models are set up of course, if you can show those it might make your problem clearer for others to explain.
EDIT - Complete solution
I will just show you the way to present a peace of info as in your solution.
To start off a small advise, you mentioned in your comment
changed it to just query the database in the form for both the
homeTeamIndex and the awayTeamIndex
While this might work it is a bad practice to place such routines in the view, it is better to define a method to complete such routine in the helper and call it in the view.
Now, to the solution...
First you need to properly define model associations, which are only partially defined judging from the code you have shared. What you need is:
Tell all classes that they are related (which they are) like this:
I would loose 'Nfl' in team since it makes naming of classes confusing
class Week < ActiveRecord::Base
has_many :games
end
class Game < ActiveRecord::Base
belongs_to :week
belongs_to :home_team, class_name: "Team", foreign_key: :home_team_id
belongs_to :away_team, class_name: "Team", foreign_key: :away_team_id
end
class Team < ActiveRecord::Base
has_many :home_games, class_name: "Game", foreign_key: :home_team_id
has_many :away_games, class_name: "Game", foreign_key: :away_team_id
end
Note: Have a look at Active Record Associations for a more in-depth understanding of those.
EDIT
Corrected model associations above and tested to make sure those are working:)
Make sure you have the following database schema to back up the associations...
create_table "games", force: true do |t|
t.integer "game_number"
t.integer "week_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "home_team_id"
t.integer "away_team_id"
end
create_table "teams", force: true do |t|
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "weeks", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
EDIT: mind the fact that all tables do include id field by default when you create models in rails
With these associations in place you can now get the week in your controller as you did before:
class WeeksController < ApplicationController
...
def show
#week = Week.find(params[:id])
#games = #week.games
end
...
end
And finally construct your view:
In WeeksController#show
/views/weeks/show.html.erb
<% if #games.any? %>
<% render #games %>
<% end %>
This line "render #games' will search for '/views/games/_game.html.erb' and render it for every game in the collection #games
*/views/games/_game.html.erb - partial view*
<strong> Game <%= game.game_number %> </strong> <br />
<%= image_tag(game.home_team.imagePath, size: "40") %>
<%= game.home_team.name %>
VS
<%= image_tag(game.away_team.imagePath, size: "40") %>
<%= game.away_team.name %> <br />
And that is it, the only fiddly bit is getting the model associations wright which is not too difficult.

Submit multiple model objects from one form with Rails

I've tried following this advice but I haven't succeeded yet in generating a form containing 3 objects of the same type under one submit button.
When I navigate to a page that should show a form containing fields for 3 objects (called elements in this example) I get the following error:
undefined method 'elements' for nil:NilClass
Any pointers would be much appreciated! My code is as follows:
app/controllers/elements_controller.rb
class ElementsController < ApplicationController
def index
#element_group = ElementGroup.new
render 'pages/index'
end
end
app/views/pages/home.html.erb
<%= render 'element_groups/form'%>
app/views/element_groups/_form.html.erb
<% form_for :element_group do |f|%>
## The error comes from this next line, as f.object is nil
<% f.object.elements.each do |element| %>
<% f.fields_for element do |element_form| %>
<%= element_form.text_field :content %>
<%= element_form.text_field :element_type %>
<%= element_form.text_field :subtype %>
<% end %>
<% end %>
<% end %>
app/models/element_group.rb
class ElementGroup
attr_accessor :elements
def elements
#elements = []
3.times do
#elements << Element.new
end
#elements
end
end
app/models/element.rb
class Element < ActiveRecord::Base
attr_accessible :element_type, :subtype, :content
end
db/schema.rb
create_table "elements", :force => true do |t|
t.string "element_type"
t.string "subtype"
t.string "content"
t.datetime "created_at"
t.datetime "updated_at"
end
Have you tried to change to <% form_for #element_group do |f|%> ?

Resources